I'm building a SCPI shell for Linux. I have the basis working over TCP/IP on a Raspberry Pi and a BeagleBone.
In this post I'll show the basics: the Pi or BB listens to SCPI commands and sends replies. I'll include the LabVIEW test bed that I used to get this working. |
In this series, I'm focusing on the specifics for TCP/IP communication with a SCPI service. I will not make an instrument but this project can serve as the basis of a SCPI programmable Linux device.
The SCPI Server
A program on the Linux board will run and will perform two things:
- Listen to SCPI commands on a TCP/IP port
- Send SCPI replies
I've written the program in C. It uses common, fairly stable Linux libraries and uses them in shared object mode, so there's no hard dependencies on Linux version.
Because of that, it also cross-compiles and debugs straight away from the Arm DS-5 Community Edition IDE.
The program registers itself as the listener for a socket (passed as a command line parameter, then waits until traffic arrives.
When traffic arrives, it sends that directly to the SCPI library for parsing. This is very similar to what I had implemented for USB SCPI devices before.
When the SCPI library decides to send results, it'll use the same socket to talk back.
This is a partial implementation of the usual SCPI TCP/IP functionality, enough to fulfill the typical command / reply scenario.
The full implementation allows negotiation between server and client to open a pipe for continuous data exchange.
attribution |
---|
TCP/IP Socket code is adapted from BinaryTides Server and client example with C sockets on Linux by Silver Moon. The SCPI library is the one I mostly use in projects here on element14, Jan Breuer's really excellent SCPI lib. |
The main function is very straightforward. Init our SCPI library and prepare the program to act as a server side Socket listener.
int main(int argc, char *argv[]) { int c, read_size, portno; struct sockaddr_in server, client; char client_message[2000]; signal(SIGINT, sig_handler); SCPI_Init(&scpi_context, scpi_commands, &scpi_interface, scpi_units_def, SCPI_IDN1, SCPI_IDN2, SCPI_IDN3, SCPI_IDN4, scpi_input_buffer, SCPI_INPUT_BUFFER_LENGTH, scpi_error_queue_data, SCPI_ERROR_QUEUE_SIZE); if (argc < 2) { fprintf(stderr, "ERROR, no port provided\n"); exit(1); } portno = atoi(argv[1]); //Create socket socket_desc = socket(AF_INET, SOCK_STREAM, 0); if (socket_desc == -1) { printf("Could not create socket"); } puts("Socket created"); //Prepare the sockaddr_in structure server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(portno); //Bind if (bind(socket_desc, (struct sockaddr *) &server, sizeof(server)) < 0) { //print the error message perror("bind failed. Error"); return 1; } puts("bind done");
Some of the functions called here, like signal() and SCPI_xxx(), I'll discuss later on in this post.
But for now, you can follow the main flow. When this code has executed, our SCPI library is initialised and the server is ready to start the listen and reply cycle.
while (1) { //Listen listen(socket_desc, 3); //Accept and incoming connection puts("Waiting for incoming connections..."); c = sizeof(struct sockaddr_in); //accept connection from an incoming client client_sock = accept(socket_desc, (struct sockaddr *) &client, (socklen_t*) &c); if (client_sock < 0) { perror("accept failed"); return 1; } puts("Connection accepted"); //Receive a message from client while ((read_size = recv(client_sock, client_message, 2000, 0)) > 0) { SCPI_Input(&scpi_context, client_message, strlen(client_message)); } if (read_size == 0) { puts("Client disconnected"); fflush(stdout); } else if (read_size == -1) { perror("recv failed"); } close(client_sock); }
I'm open for advise on this code. It works but I took a very naive approach of trying until I got a working device. I'm not an expert on this matter.
You'll see that the main loop never writes to the socket. That's because the SCPI library uses a callback function. You have to write that function yourself.
Here's the one I wrote for the SCPI TCP/IP server:
size_t SCPI_Write(scpi_t * context, const char * data, size_t len) { (void) context; int n = write(client_sock, data, len); if (n < 0) error("ERROR writing to socket"); return n; }
I've also provided a handler to clean up resources when Linux ends the program.
void sig_handler(int signo) { if (signo == SIGINT) { shutdown(socket_desc, SHUT_RDWR); } }
Todo: make this a deamon that can be started at the bootup of the Linux device.
I think my C code is almost ready for that. I'll have to write the instructions to register it.
When using it for a real instrument, I suggest to only put the SCPI handling in this process and run your instrument in another one.
You can use POSIX messaging or the Socket API to let the two processes work together.
LabVIEW Test Bed
The code isn't production ready yet, but good enough as a test bed. It sends the *IDN? command to the Linux device and validates the return value.
The IP address and port are fixed here but that's not the end state.
One thing to look at is the Read block.
I've added an error handling block to discard TCP/IP timeouts. I have this issue that when the reply is less than the byte count passed to the read block, it tims out.
I've tried to check the number of bytes waiting for read, and to implement the termination attribute for the VISA connection, but both constructs are flagged as unsupported at runtime.
So I've decided to overrule the timeout error.
If this gives an issue (it doesn't in the current test bed because there are further checks that validate the data), I can improve this construct.
The great thing is that it works.
Top Comments