A simple WiFi scope (Part 2 – Server and Client)

TCP server

As I mentioned in the first article, two TCP servers were designed for this project. While Charlie was in charge of designing the actual TCP server that would run on the Omega, I also had to design my own (simpler) TCP server for testing the graphic frontend.

Since our TCP server running on the Omega is a C application, it needs to be compiled to run on that platform. Therefore you are going to need to install (on your computer) the toolchain which includes the GCC cross-compiler (a compiler that runs on one kind of machine but is able to generate code for another kind of machine). Onion has a very nice article covering the whole toolchain installation step-by-step.

Back to the TCP server code, let’s take a look at Charlie’s code. It is not easy to analyze someone else’s code (I added some comments!) but we can see that it basically opens a serial connection with device /dev/ttyACM0 and uses a queue (txq) to handle inbound and outbound communication (another rxq queue was declared but wasn’t used). It then uses select() and its macros for I/O multiplexing.

Select is a very interesting function which checks all file descriptors below the maximum number provided as argument (nfds). It also receives three fd_set structures which are used to signal when a descriptor is ready for reading, writing or has an exceptional condition (which is not an error). Select will block the code until a descriptor is ready (or it times out, since there is also an optional timeval field), it will then modify the read/write/exception sets to update which descriptors are ready and return the overall number of ready descriptors.

If you are curious (just like me) about how these functions work and how they are implemented in Linux, you can check select.h and select.c files!

Our TCP server code uses four file descriptors:

  • serial_fd, which handles serial communication through ttyACM (serial over USB) with the STM32;
  • listen_sd, the descriptor associated with our TCP listening socket;
  • new_sd, the descriptor associated with our TCP connected socket;
  • max_sd, the highest descriptor we are currently using (either listen_fd or new_sd). This is used by select so that it knows what is the highest descriptor it should monitor.

There are four functions for communication handling:

  • forward_packet() updates txq queue;
  • handleSocketRead() accepts connections (when listen_sd is set) and receives data from TCP socket (when max_sd is set, which at this point is the same as new_sd which is the connected socket descriptor). Received data is stored into a buffer and txq queue is updated;
  • handleRead() checks is descriptor 0 (stdin) is ready and reads data from it, it also checks if serial_fd is ready for reading (meaning that there is data available at the serial port (ttyACM)). It then reads data from serial port and writes into the connected socket. This operation does not use select();
  • handleWrite() checks if serial_fd descriptor is ready for writing (sending data to STM32) and if so, removes data from txq queue and writes to the serial port.

This is how our main loop looks like:

while(osc_alive) {
	FD_ZERO(&rset);
	FD_ZERO(&wset);
	FD_SET(0, &rset);	// or stdin
	FD_SET(serial_fd, &rset);
	// listening socket
	FD_SET(listen_sd, &rset);
	if (new_sd) {
		FD_SET(new_sd, &rset);
	}
	ret = select(max_sd+1, &rset, &wset, NULL, NULL);
	switch (ret) {
		case -1:
			pr_err("select() with error = %d\n", errno);
			continue;
		case 0:
			puts("timeout!!~~");
			/* Wait up to 2 seconds. */
			tv.tv_sec = 2;
			tv.tv_usec = 0;
			continue;
	}
	handleSocketRead();
	handleRead();
	handleWrite();
}

I also decided to include some code to display Onion Omega’s local IP address when TCP server code runs (we need to know its IP address when we connect using our scope client). I have used the code from this page to implement my printLocalIP() function. It uses a SIOCGIFADDR IOCTL to get the address of interface apcli0 (Omega’s WiFi Adapter).

In order to compile our application all you have to do is to use Onion’s xCompile script:

sh xCompile.sh -buildroot <path to your buildroot>

In my case I have edited my BUILDROOT_PATH variable (within xCompile.sh) so that it already points to the location of my Onion buildroot tools (a simple sh xCompile.sh works for me here).

As a result of the building process an executable file is gonna be generated (osc_agent). This is a MIPS24k binary file (as you can see by the result of the file command):

$ file osc_agent
osc_agent: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1, dynamically linked, interpreter /lib/ld-, not stripped

Now all we have to do is to copy this file to Onion Omega and run it from there!

There are multiple ways to run our code on the Omega, one of them (the one Charlie used as you can see from his Makefile) is by using a MicroSD card for storage, so you plug it into your computer, copy the file, plug it into Onion Omega and run it there.

Onion’s website shows other ways to copy/install the application in an Onion Omega. What I chose to do was to copy the file directly to the file system of my Onion Omega 2+ (home directory) using rsync. Remember that you need to navigate to the directory where the binary file is before running rsync or provide the full path for the file:

Figure 1 – Copying and running the application

If you want to run the server on startup, you can add it to the startup script (/etc/rc.local) as Onion shows on this page.

Client

The last piece of our WiFi scope is the client application which consumes data from the our scope hardware and presents it to the user. I opted out for using Lazarus, a free and open source IDE built around Free Pascal. The reason for choosing Lazarus/Pascal is that Pascal was the second language I’ve learned (when I was 14) and the first one I could use with a nice IDE (Delphi). Nowadays, Lazarus is a very nice option for designing multi-platform GUI applications since Free Pascal (FPC) can target almost anything (including RISC-V CPUs!).

Our client code is very simple and was designed in a rush (as everything else in this project). But it works relatively well and performs what we expect from it. We are using INet package and its TCP component to handle event-driven TCP communication with the Onion Omega 2+. A TChart component handles plotting and displaying the graph.

It all starts when the user presses “Connect” button, which triggers the following code:

Once the TCP component connects, it triggers tcpConnect event which among other things enables Timer1, a periodic timer with an interval set to 200ms.

Every time Timer1 fires, it calls this procedure which sends a command to our remote server to perform sample reading:

Our server will forward the command to the STM32 board which will perform it and return the samples that are stored on its buffer. The server forwards that message to our client which is received by the TCP component. Upon receiving data it calls the TCP receive procedure below:

procedure TForm1.tcpReceive(aSocket: TLSocket);
var
  s: string;
  tempString: string;
  temp, temp2: integer;
  stringIndex, arrayIndex: integer;
  cmd: integer;
  value: integer;
  error: integer;
  len : integer;
  freqStart, freqEnd: integer;
  data: array[1..1024] of integer;
  currentCMD: integer;
  frequency : double;
begin
  if tcp.GetMessage(s) > 0 then receiveBuffer := receiveBuffer + s;
  if (Pos('WOSC',receiveBuffer)>0) then
  begin
    stringIndex := Pos('WOSC',receiveBuffer);
    tempString := '0x'+Copy(receiveBuffer,stringIndex+4,4);
    Val(tempString,temp,error);
    len := 0;
    if error=0 then len := temp else exit;
    // get command
    tempString := '0x'+Copy(receiveBuffer,stringIndex+8,2);
    Val(tempString,currentCMD,error);
    if error<>0 then
    begin
      Delete(receiveBuffer,1,stringIndex+10);
      exit;
    end;
    temp2 := Length(receiveBuffer)-10;
    // if current packet length is less than packet length field we exit and wait for more data
    if temp2<len then exit;
    // now decode the remainder of string
    stringIndex := Pos('WOSC',receiveBuffer)+10;
    temp2 := len;
    arrayIndex := 1;
    if currentCMD = CMD_READ_DEBUG_DATA then
    begin
      // if we have a debug command, append data to memo, delete string and return
      memo1.Append(Copy(receiveBuffer,stringIndex,temp2));
      Delete(receiveBuffer,1,stringIndex+temp2);
      exit;
    end;
    // we have a reply for a command
    while len>0 do
    begin
      tempString := '0x'+Copy(receiveBuffer,stringIndex,4);
      Val(tempString,temp,error);
      if error=0 then data[arrayIndex]:=temp;
      len := len-4;
      stringIndex := stringIndex + 4;
      inc(arrayIndex);
    end;
    if temp2=4 then
    begin
      case currentCMD of
        CMD_READ_SAMPLE_RATE : sampleRate:= data[1];
        CMD_READ_TRIGGER_LVL : triggerLevel := data[1];
        CMD_READ_TRIGGER_TYPE : triggerMode := data[1];
      end;
    end else
    begin
      numSamples := temp2 div 4;
      freqStart := -1;
      freqEnd := -1;
      updateXaxis(numSamples,sampleRate);
      sr1.Clear;
      for arrayIndex:=1 to numSamples do
      begin
        plotPoint(arrayIndex,data[arrayIndex]);
        if arrayIndex>1 then
        begin
          if (data[arrayIndex]>=triggerLevel) and (data[arrayIndex-1]<triggerLevel) and (freqStart = -1) then freqStart := arrayIndex;
          if (freqStart <> -1) and (data[arrayIndex]<triggerLevel) and (freqEnd = -1) then freqEnd := arrayIndex;
        end;
      end;
      // now we try to calculate trigger frequency
      if (freqStart=-1) or (freqEnd=-1) then
      begin
        // we can't calculate trigger frequency
        TriggerFrequencyLabel.Caption:='F = ---- Hz';
      end else
      begin
        // calculate frequency
        case sampleRate of
          0 : frequency := 1/((freqEnd-freqStart)*0.0025*2);
          1 : frequency := 1/((freqEnd-freqStart)*0.0005*2);
          2 : frequency := 1/((freqEnd-freqStart)*0.00025*2);
          3 : frequency := 1/((freqEnd-freqStart)*0.0001*2);
          4 : frequency := 1/((freqEnd-freqStart)*0.00005*2);
          5 : frequency := 1/((freqEnd-freqStart)*0.000025*2);
          6 : frequency := 1/((freqEnd-freqStart)*0.00001*2);
          7 : frequency := 1/((freqEnd-freqStart)*0.000005*2);
          8 : frequency := 1/((freqEnd-freqStart)*0.0000025*2);
          9 : frequency := 1/((freqEnd-freqStart)*0.000001*2);
        end;
        TriggerFrequencyLabel.Caption:='F = '+FormatFloat('#0.00',frequency)+'Hz';
        TriggerFrequencyLabel.Visible:= true;
      end;
    end;
    Delete(receiveBuffer,1,Pos('WOSC',receiveBuffer)+10+temp2);
  end else
  begin
    receiveBuffer := '';
  end;
end;

The procedure above calls plotPoint() which plots the points/lines that we see on the screen:

procedure TForm1.plotPoint(arrayIndex: integer; value: integer);
var
   multiplier : single;
begin
  if value<0 then value := 0;
  if value>4095 then value := 4095;
  multiplier := (Chart1.BottomAxis.Range.Max-Chart1.BottomAxis.Range.Min)/numSamples;
  sr1.Add((arrayIndex-11)*multiplier,(3.3/4096)*value);
end;

That is it! Now just compile and run the application on Lazarus, provide the IP address of your Onion Omega 2+ in the edit box (bottom right), press “connect” and watch the WiFi scope system coming to life!

Here are some pictures of the whole system in action:

Figure 2 – A 50KHz sinewave (3.2Vpp) is applied to scope input
Figure 3 – The same 50KHz sinewave is displayed by our WiFi scope and a Tektronix scope
Figure 4 – The whole system in action!

Conclusion

The idea of this capstone project was never to design a real or functional oscilloscope, but to apply our knowledge integrating different platforms to build an IoT application that integrates data acquisition, wireless transmission, TCP communication and client/GUI designing. We had a lot of fun and learned a lot. Thanks to Charlie and Divesh for being part of the team!

You can find the source-code for the whole system in my Github!

See you next time!

Leave a Reply