How To - Forward CAN frames over TCP sockets and simple CAN parsing

The following document describes two examples on how to work with CAN frames on a Netmodule router. Aside of the basic setup, forwarding CAN traffic over TCP sockets as well as simple CAN frame parsing are covered.

Environment setup

can_environment.jpg

The above graphic illustrates how the test environment was setup and what data gets transmitted. First to be able to simulate a CAN bus in a local environment PC1 runs Peak System PCAN-FMS Simulator to generate CAN Frames based on the FMS Standard (supports FMS v 1.0 – 3.0). The generated frames are transmitted over Peak System PCAN-USB into a Netmodule Router (NB2800 used in this example). Two example scripts on the router work with the incoming CAN traffic and send it back out over TCP, where SocketTest runs an PC2 as a simple TCP server. SocketTest is a tool based on Java that allows testing of UDP and TCP client and server sockets. In our case SocketTest is configured as a TCP server that is listening on Port 2000.

Setup the CAN Interface on the Router

To use the CAN Interface on the Router first you need to enable the CAN Interface with the correct Bitrate (250kBits used).

Peaksystem PCAN-FMS Simulator

As mentioned in the introduction Peaksystems PCAN-FMS simluator is used to generate CAN frames. The software is easy to use and allows a good starting point for local development. peaksystem-pcan-fms-simulator.jpg

Incoming RAW CAN Frames on the Router:

The first and most simple test on the router is to use candump provided by the can-utils package to verify a working environment and to see the generated CAN traffic. Login to your router (over ssh or telnet) and run candump <can-interface>

By default, candump provides the following information:

candump.jpg

Interface, CAN Identifier, DLC and DATA

Forward CAN Frames over TCP Sockets (can-tcp-broadcast.are)

To show a possible use of forwarding CAN Frames over Ethernet can-tcp-broadcast.are provides a TCP client that broadcasts all incoming CAN Frames to one or several servers that can be passed as arguments. The SDK API already provides several functions to interact with CAN (see ftp://share.netmodule.com/router/public/system-software/latest/NB_SDK_API_Manual.pdf). The function start_can() sets required attributes to the can interface and sets up a raw socket descriptor that is used in the main part of the script. Additionally filtering of CAN Identifiers is possible via nb_can_setfilter() to specify which CAN frames shall be filtered out and which shall be passed to upper layers.

Please be aware that extended use of filtering or processing on the router itself may affect the overall processing power. An advice would be, to do data processing on a more powerful backend according to ones needs.

from can-tcp-broadcast.are
int start_can() 
{
	ret = nb_can_setattr(DEV, BITRATE, LISTENONLY, RESTART_TIMEOUT);
	if (ret != 0) {
    		printf("Unable to set attributes of %s\n", DEV);
    		exit(1);
	}
	printf("Attributes set for device %s\n", DEV);
	can_sock = nb_can_open(DEV);
	if ( can_sock < 0){
		printf("Unable to open %s\n", DEV);
		exit(1);
	}
	// CAN Filtering can be done here
	if (0) {
    		FILTER_ID = 1;
    		FILTER_MASK = 0xFFFFFFFF;
 
    		if (nb_can_setfilter(sock, FILTER_ID, FILTER_MASK) < 0) {
        			printf("Unable to set filter on %s\n", DEV);
        			can_close(can_sock);
        			exit(1);
    		}
	}
	return can_sock;
}

Within the main loop receiving CAN messages is done with nb_can_recvmsg() which returns CAN frames as a msg struct. Since the CAN specification defines two different identifier formats (11-Bit Identifier - Base Frame Format & 29-Bit Identifier - Extended Frame Format) the SDK offers different bit operators to examine the CAN ID of a frame. The following bit operators are available for nb_can_recvmsg():

  • CAN_EFF_FLAG — EFF/SFF is set in the MSB
  • CAN_RTR_FLAG — remote transmission request
  • CAN_ERR_FLAG — error frame
  • CAN_SFF_MASK — standard frame format (SFF)
  • CAN_EFF_MASK — extended frame format (EFF)
  • CAN_ERR_MASK — omit EFF , RTR , ERR flags

Since FMS uses the extended frame format the CAN_EFF_MASK bit operator was applied.

from can-tcp-broadcast.are
if (msg) {
 		/* check received can message */
    		id = struct_get(msg, "id") & CAN_EFF_MASK; /* extended frame format (EFF) */
    		len = struct_get(msg, "len");
    		data = struct_get(msg, "data");
 
		/* This is used to concatenate the CAN Identifier & CAN Data
	    	to later send the string over TCP
		*/
		msgbuffer = sprintf("CAN ID: %04X DLC: %i DATA: ",id, len);
		for (i = 0; i < len; i++) {
			msgbuffer = sprintf("%s%02X ", msgbuffer, data[i]);
		}
		msgbuffer = sprintf("%s\n", msgbuffer);
		msg = msgbuffer;
}

On the receiving end SocketTest listens on TCP port 2000 and displays the raw CAN frames:

Simple CAN Frame parsing (simple-can-parser.are)

An additional test was made to show a simple way of parsing CAN Frames. The example is based on checking for a specific PGN (parameter group number) and then get a SPN (supect parameter name) to display its value in human readable format which then gets logged in a file. The example used was “Wheel based speed”, SPN=84 PGN=0x00FEF1 (Cruise Control/Vehicle Speed: CCVS) according to FMS Standard 3.0. The below graphic shows the parmeter group and its signals.

fms-pgn-00fef1.jpg

from simple-can-parser.are
int parse_can_frame(int can_identifier, array can_data){
        shifted_id = can_identifier >> 8;
        pgn = shifted_id & pgn_mask;
		// FMS Standard 3.0:
		// PGN 0x00FEF1 --> Cruise Control / Vehicle Speed: CCVS
 
        if(pgn == 0x00FEF1){
                /* Get Wheel based speed (DATA BYTES 2 &3) from Cruise Control/Vehicle Speed: CCVS
		   SPN: 84 */
                speed = ((can_data[2] << 8) + can_data[1])/256;
		/* Log the parsed message */		
		now = localtime(time());
		timestamp = strftime("%Y-%m-%d %H:%M:%S", now);
		fwrite(fp, sprintf("%s -- Wheel based speed: %f\n",timestamp, speed));
		return 0;
       	} else{
		return -1;
	}
}

SDK Scripts

Below the used arena scripts (can-tcp-broadcast.are & simple-can-parser.are) are listed completly.

can-tcp-broadcast.are
/* 
 * Forward incoming CAN frames as concatenated strings to one 
 * or several servers over TCP sockets.
 * Copyright (C) 2017 NetModule AG, Switzerland
 */
 
 
/* CAN Interface options */
DEV = "can0"; /* CAN Interface to listen on */
BITRATE = 250000; /* run 250kbit bitrate */
LISTENONLY = 0; /* open in listen-only mode (normaly set to 1) */
RESTART_TIMEOUT = 0; /* restart timeout in case of bus-off (msecs, 0 = disabled) */
 
/* TCP Options */
/* number of retries when connecting to a slave */
RETRY = 3;	
/* the interval in seconds for each retry */
RETRY_INTERVAL = 3;	
/* recovery interval after slave got disconnected or retries were unsuccessful */
RECOVER_INTERVAL = 120;
 
 
/* 
 * Only changed things below if you know what you are doing 
 */
 
POLL_TIMEOUT = 10;  /* seconds */
REMOTE_PORT =2000;  /* port of the remote serial slaves (default 2000) */ 
 
/* slave template */
template slave 
{
    hostname = "";
    fd = -1;
    state = "down";
    since = 0; 
 
    void slave(string remotehost) 
    { 
        printf("Creating slave %s\n",remotehost);
        if (strlen(remotehost) > 0) {
            this.hostname = remotehost;
        } else {
            printf("ERROR: no hostname defined, slave could not be created\n");
        }
    }
 
    int jiffy()
    {
        sys = sysinfo();
        u = struct_get(sys, "uptime");
 
        if (is_void(u) || u < 1) {
            return 0;
        } else {
            return u;
        }
    } 
 
    int connect() {
        if (this.fd > -1) {
            close(this.fd);
            this.fd = -1;
        }
        printf("Connecting to %s\n", this.hostname);
 
        /* open TCP socket */
        sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE,1);
        setsockopt(sock, SOL_TCP, TCP_KEEPIDLE,1);
        setsockopt(sock, SOL_TCP, TCP_KEEPINTVL,5);
        setsockopt(sock, SOL_TCP, TCP_KEEPCNT,5);
 
        if (sock < 0) {
            printf("ERROR: unable to open socket\n");
        }
        for (i = 0; i < RETRY; i++) {
	    printf("Connect now\n");
            if (connect(sock, this.hostname, REMOTE_PORT) < 0){
                printf("ERROR: could not connect to %s:%d\n", this.hostname, REMOTE_PORT);
                printf("Retrying to connect (trial #%i, waiting %s sec)\n", i, RETRY_INTERVAL);
                sleep(RETRY_INTERVAL);
            } else {
                printf("TCP connection established to host %s:%d\n", this.hostname, REMOTE_PORT);
                this.fd = sock;
                this.state = "up";
                return 0;
            }
        }
        printf("ERROR:  max. retries reached for %s, giving up\n", this.hostname);  
        this.since = this.jiffy();
        this.state = "down";
        close(this.fd);
        this.fd = -1;
 
        return -1;
    }
 
    int disconnect() {
        if (this.fd > -1) {
            close(this.fd);
            this.fd = -1;
        }
        this.state = "down";
        this.since = this.jiffy();
        printf("Disconnected host %s\n", this.hostname);  
        return 0;
    }
 
 
} /* endof template slave */
 
int start_can() 
{
	ret = nb_can_setattr(DEV, BITRATE, LISTENONLY, RESTART_TIMEOUT);
	if (ret != 0) {
    		printf("Unable to set attributes of %s\n", DEV);
    		exit(1);
	}
	printf("Attributes set for device %s\n", DEV);
	can_sock = nb_can_open(DEV);
	if ( can_sock < 0){
		printf("Unable to open %s\n", DEV);
		exit(1);	
	}
 
 
	// CAN Filtering can be done here
	if (0) {
    		FILTER_ID = 1;
    		FILTER_MASK = 0xFFFFFFFF;
 
    		if (nb_can_setfilter(sock, FILTER_ID, FILTER_MASK) < 0) {
        		printf("Unable to set filter on %s\n", DEV);
        		nb_can_close(can_sock);
        		exit(1);
    		}
	}
 
	return can_sock;
}
 
int stop_can() 
{
    if (can_sock > -1) {
        nb_can_close(can_sock);
        can_sock = -1;
    }
    return 0; 
}
 
void usage()
{
    printf("usage: can-tcp-broadcast.are <remote-ip-1>  <remote-ip-2> .... <remote-ip-n>\n");
    exit(1);
}
 
/* ------------------------ main ------------------------------- */
 
if (strlen(argv[1]) == 0) {
    printf("ERROR: no remote host specified\n");
    usage();
}
 
/* create slaves */
printf("Creating slaves\n");
for (i = 1; i < argc; i++) {
    slaves[i-1] = new slave(argv[i]);
}
 
/* connect slave */
for (i = 0; i < length(slaves); i++) {
    printf("connecting slave %d\n", i);
    rc = slaves[i].connect();
    if (rc < 0) {
        printf("ERROR: could not connect %s\n", slaves[i].hostname);   
    }
}
 
printf("Starting can\n");
can_sock = start_can();
 
/* create file descriptor array */
fds = mkarray();
tcp_fds = mkarray();
j = 0;
for (i = 0; i < length(slaves); i++) {
     if (slaves[i].state == "up") {
        tcp_fds[j] = slaves[i].fd;
        j++;   
    }
}
fds = array_merge(can_sock, tcp_fds);
 
 
 
/* loop infinitely */
while (1) {
    buffer = "";
    rc = select(fds, POLL_TIMEOUT);
    if (rc == -1) {
        printf("ERROR: select failed\n");
    } else if (rc == can_sock) {
        /* received something from CAN interface */
    	msg = nb_can_recvmsg(can_sock, 5);
    	len = 0;
    	msgbuffer = "";
    	if (msg) {
 		/* get recived CAN message  & */
    		id = struct_get(msg, "id") & CAN_EFF_MASK; /* extended frame format (EFF) */
    		len = struct_get(msg, "len");
    		data = struct_get(msg, "data");
 
		/* Concatenate the CAN Identifier & CAN Data
	    	to later send the string over a TCP socket
		*/
		msgbuffer = sprintf("CAN ID: %04X DLC: %i DATA: ",id, len); 
		for (i = 0; i < len; i++) {
			msgbuffer = sprintf("%s%02X ", msgbuffer, data[i]);
		}
		msgbuffer = sprintf("%s\n", msgbuffer);
		msg = msgbuffer;
    	}
    } else if (rc == 0){
		// Implement a timeout routine if required below 
		// printf("Timeout\n");	
    }
 
 
    /* send TCP packet to every slave */
    if (len > 0) {
        for (i = 0; i < length(slaves); i++) {
            if (slaves[i].state == "up" ) {
  		// Sending CAN Message over TCP Socket 
                sent = send(slaves[i].fd, msg);
                if (sent < 0) {
                    printf("ERROR: failed to send packet to %s\n", slaves[i].hostname);
                    /* disconnect slave */
                    slaves[i].disconnect();   
                }
            }
        }
    }
    /* check if we have an error on the socket */
    for (i = 0; i < length(slaves); i++) {
        if (slaves[i].state == "up" ) {
            opt = getsockopt(slaves[i].fd, SOL_SOCKET, 4);
            if (opt != 0) {
               printf("ERROR: getsockopt(%d) on slave %s failed, reconnecting\n", opt, slaves[i].hostname);
               slaves[i].disconnect();
            }
        }
    }
 
    /* reconnect to disconnected slaves */
    for ( i=0; i < length(slaves); i++) {
        if (slaves[i].state == "down" ) {
            if ( (slaves[i].since + RECOVER_INTERVAL) < slaves[i].jiffy() ) {
              slaves[i].connect();
            }
        }
    }
 
    /* re-create file descriptor array */
    fds = mkarray();
    tcp_fds = mkarray();
    j = 0;
    for (i = 0; i < length(slaves); i++) {
         if (slaves[i].state == "up") {
             tcp_fds[j] = slaves[i].fd;
             j++;
        }
    }
    fds = array_merge(can_sock, tcp_fds);
}
stop_can();
exit(0);
simple-can-parser.are
/* DESC: This script shows an example of parsing a PGN and a specific SPN 
 * based on the FMS Standard 3.0
 * Copyright (C) 2017 NetModule AG, Switzerland
*/
 
/* CAN Interface options */
DEV = "can0"; /* CAN Interface to listen on */
BITRATE = 250000; /* run 250kbit bitrate */
LISTENONLY = 0; /* open in listen-only mode (normaly set to 1) */
RESTART_TIMEOUT = 0; /* restart timeout in case of bus-off (msecs, 0 = disabled) */
MAX_MESSAGES = 1024; /* max number of incoming messages */
 
int start_can() 
{
	ret = nb_can_setattr(DEV, BITRATE, LISTENONLY, RESTART_TIMEOUT);
	if (ret != 0) {
    		nb_syslog("Unable to set attributes of %s\n", DEV);
    		exit(1);
	}
	can_sock = nb_can_open(DEV);
	if ( can_sock < 0){
		nb_syslog("Unable to open %s\n", DEV);
		exit(1);	
	}
 
 
	// CAN Filtering can be done here
	if (0) {
    		FILTER_ID = 1;
    		FILTER_MASK = 0xFFFFFFFF;
 
    		if (nb_can_setfilter(sock, FILTER_ID, FILTER_MASK) < 0) {
        		nb_syslog("Unable to set filter on %s\n", DEV);
        		nb_can_close(can_sock);
        		exit(1);
    		}
	}
 
	return can_sock;
}
 
int stop_can() 
{
    if (can_sock > -1) {
        nb_can_close(can_sock);
        can_sock = -1;
    }
    return 0; 
}
 
 
/* Parse an example CAN Message and get the PGN and the value of
   a specified SPN based on FMS */ 
 
int parse_can_frame(int can_identifier, array can_data){
        shifted_id = can_identifier >> 8;
        pgn = shifted_id & pgn_mask;
	// FMS Standard 3.0:
	// PGN 0x00FEF1 --> Cruise Control / Vehicle Speed: CCVS
 
        if(pgn == 0x00FEF1){
                /* Get Wheel based speed (DATA BYTES 2 &3) from Cruise Control/Vehicle Speed: CCVS
		   SPN: 84 */
                speed = ((can_data[2] << 8) + can_data[1])/256;
		/* Log the parsed message */		
		now = localtime(time());
		timestamp = strftime("%Y-%m-%d %H:%M:%S", now);
		fwrite(fp, sprintf("%s -- Wheel based speed: %f\n",timestamp, speed));
		return 0;
       	} else{
		return -1;
	}
}
 
/* ------------------------ main ------------------------------- */
 
/* Bitmask to get PGN from CAN Identifier */
pgn_mask = (1 << 18) -1;
 
pcount = 0;
 
printf("Listening on %s\n", DEV);
can_sock = start_can();
 
fp = fopen("/tmp/can_parser_results.log", "w+");
if (is_void(fp)){
	nb_syslog("unable to open %s", fp);
	exit(1);
}
 
for(count = 0; count < MAX_MESSAGES;) {
    	msg = nb_can_recvmsg(can_sock, 5);
	if (is_void(msg)) continue;
    	len = 0;
 
    	if (msg) {
 
 		/* Strip recived CAN frame  & parse an example */
    		id = struct_get(msg, "id") & CAN_EFF_MASK; /* extended frame format (EFF) */
    		len = struct_get(msg, "len");
    		data = struct_get(msg, "data");
 
		if(parse_can_frame(id, data) == 0 ){
			pcount++;	
		}
    	}
	count++;
}
 
fwrite(fp, sprintf("Parsed %i Frames accoding to your specifications\n", pcount));
fclose(fp);
stop_can();
exit(0);