Development of a display program for a hydroacoustic station under Linux

The hydroacoustic station is serial, called the Echologger MRS-900. Single-beam, the beam scans in a circle or in a sector. Communication with PC via RS-232/RS-485 interface. There is standard software, it works under Windows. I needed a Linux version.

The software of this sonar can be roughly divided into two parts. Display of echo signals on a pie chart and user interface (range selection, pulse modulation, palette selection, etc.). We only wanted to display data on a pie chart, the data could be either from a recorded file or received from a serial port. Pie chart example:

The application should have been packaged as a library and called in conjunction with another program that provides the user interface. There was a requirement for compatibility with any Linux and with any computer that included a video card with a 3D accelerator. After discussion, the Customer agreed to limit himself to Debian 9/10 version, one of the current versions of Ubuntu and one computer architecture on which he himself will check. Not the fastest and newest, something slightly below average at that time.

Prior to that, there was experience in developing a program for a scientific sonar, multibeam, sector survey. Windows program in Visual C++. Picture from this program:

I had to delve into the topic of hydroacoustics, read about similar serial civilian hydroacoustic stations. I have the impression that most civil hydroacoustic stations use the Windows operating system. The Customer had his own programmers. Apparently, that’s why we were hired as specialists in software development for Linux, at the same time having some experience in hydroacoustics.

On the one hand, the program proposed for development was simpler – the input data stream did not need serious additional processing, such as the imposition of various VARs, the selection of a bottom line, the detection of schools of fish and the determination of their parameters. All that remained was simple processing – the imposition of a “dead zone”, amplification, brightness and contrast adjustment. This is all at the level of very simple operations with counts, addition and multiplication.

On the other hand, it was necessary to constantly change the scale, in fact, to show an enlarged “window” of the general circular panorama if necessary, and it was also necessary to move the window. To make it clearer why this is not very pleasant, I want to simply, in a nutshell, tell you how such a sonar works.

Imagine a beam 2 degrees wide. The beam moves in a circle or in a given sector. The angular velocity depends on the operating mode, for example, on the set maximum range. Periodically, the transmitter emits a short pulse, then the receiver begins to receive echo signals. The emission period also depends on the settings (selected range, etc.). The received signals are processed, temporary automatic gain control (TAG) is implemented, the results in the form of a set of samples are received via a serial interface for display.

It is clear that each new set of readings must be displayed as a set of points limited in range and angle, completely filling the necessary areas. In this case, part of the readings must be discarded based on a given “dead zone” – immediately after the radiation there is too much non-informative “garbage” on the screen.

With the sonar parameters that were indicated on the official website, the size of the set of samples will be at least 700 samples, with a radiation period of at least 10ms. To debug the software, the Customer transferred the recordings made on his real sonar. Of course, with such a stream of input data, it is better not to try Python, only C / C ++.

To simplify the solution of the scaling problem, I decided to draw the most detailed picture in memory, based on the parameters of the sonar, namely, the resolution in range and angle. Finally, the value of the discretization in terms of angle and range was agreed with the Customer – he, as a developer, knows his equipment and its real capabilities best of all.

For each discret, tables of coordinates of points x, y were preliminarily calculated. Here is an example of calculation functions:

// maximum amplitude samples in one beam
#define MAX_SAMPLES_TO_SHOW 1024

// beams per 360 degrees, maximum angles with step 0.1125 corresponf to 3600 beams, 720 beams correpond to 0.5 degrees
#define BEAMS 1440

// limit of reserved real screen pixels for each sample
#define MAX_PIXELS_PER_SAMPLE 32

// arrays of pixels coordinates for each sonar ADC sample 
int16_t pixel_table_x[BEAMS][MAX_SAMPLES_TO_SHOW][MAX_PIXELS_PER_SAMPLE],\
    pixel_table_y[BEAMS][MAX_SAMPLES_TO_SHOW][MAX_PIXELS_PER_SAMPLE],\
    pixel_table_n[BEAMS][MAX_SAMPLES_TO_SHOW];

// samples number equal to diameter
int samples_per_beam; // have to be lower than MAX_SAMPLES_TO_SHOW


// calculate angle between vector (x,y) and X axis, the result in the sonar coordinates
float angle(float x, float y) {
	float alpha, pi=3.1415926, tpi = 2*pi;
	if (abs(x)<0.0001) { 
		if (y>0) alpha=pi/2.0;
		else alpha=3.0*pi/2.0;
	}
    else if(abs(y)<0.0001) {
        if (x>0) alpha=0;
        else alpha=pi;
    }
	else {
		alpha = atan(y/x);
		if( (x<0)&&(y>0) ) alpha += pi;
		if( (x<0)&&(y<0) ) alpha += pi;
		if( (x>0)&&(y<0) ) alpha += tpi;
	}
    alpha = 3*pi/2.0-alpha;
    
    if ( alpha<0 ) alpha+=tpi;
    if ( alpha>tpi) alpha-=tpi;
    
	return alpha;
}


// calculate table of the pixels
void pixel_table_ini( int diagram_diameter_in_pixels ) {
	float r, alpha;
	int beam, i; // indexes in the table
	int j;
	float xc,yc,pi=3.1415926;
	float dyy=0;

    // reset the tables
	memset( pixel_table_n, 0, sizeof( pixel_table_n ) ); 
	memset( pixel_table_x, 0, sizeof( pixel_table_x ) ); 
	memset( pixel_table_y, 0, sizeof( pixel_table_y ) ); 
    
    // check for MAX_SAMPLES_TO_SHOW
    if (diagram_diameter_in_pixels > (MAX_SAMPLES_TO_SHOW*2))
        diagram_diameter_in_pixels = MAX_SAMPLES_TO_SHOW*2;
    
    samples_per_beam = diagram_diameter_in_pixels/2;
    float Radius = diagram_diameter_in_pixels / 2.0;

    // check all pixels inside diameter*diameter bar to fill pixels arrays
	for(float x=0; x < diagram_diameter_in_pixels; x++ ) {
        
		for(float y=0; y < diagram_diameter_in_pixels; y++ ) {
			xc = x - Radius;
			yc = y - Radius;
			r = sqrt( xc*xc + yc*yc ); // distance to center
			alpha = angle(xc,yc);

			beam = alpha/(3.1415926*2.0) * (float)BEAMS;
			i = (r + 0.5)*2.0; // index of the sample inside the beam
			i /= 2;
			if ( (i<Radius)&&(beam>=0)&&(beam<=BEAMS) ) {
				j = pixel_table_n[beam][i]; // index of the pixel in the table
				if( (j<MAX_PIXELS_PER_SAMPLE) ) {
					pixel_table_x[beam][i][j] = x;
					pixel_table_y[beam][i][j] = y;
					++pixel_table_n[beam][i];
				}
			}
		}
    }
}

The graphic library was chosen by SDL2. There was a positive experience of using it under Windows, just for scientific sonar software, together with Visual C ++. Another picture from this program:

Later used in conjunction with CodeBlocks and MinGW. Sound was not needed. To install SDL2 on Debian 10, the command was enough for me:

sudo apt-get install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev libsdl2-gfx-dev

To compile a program as a library named libsonarwindow.so you can use one command, example:

g++ -std=c++0x -shared -fPIC -pedantic main.cpp -o libsonarwindow.so `sdl2-config --cflags --libs` -lSDL2_image -lSDL2_ttf -lpthread

To compile to an executable:

g++ -std=c++0x -pedantic main.cpp -o main `sdl2-config --cflags --libs` -lSDL2_image -lSDL2_ttf -lpthread

“Moving” the display window over the big picture with changing the magnification scale was eventually implemented by copying the “reduced” window from the large SDL2 texture. I believe that due to this approach and the capabilities of SDL2, the implementation of the task has been greatly simplified. Below is part of the source code that accomplishes this task:

//----------------------- показать один луч ( набор отсчетов ) -----------------------------            
if (isDataBufferReady || usefile) {

    if (usefile) {
        // прочитать один набор отсчетов из файла
        GetUnpackedDesc( saved_beam_i, dataHeader); 
    }
    else {															
        dataHeader = dataHeaderStream;                    
        pUnpackedSamples = (uint32_t*)(comBuffer);
    }
    
    // предварительная обработка
    AddDeadZone( deadzone_value ); // default 5
    AddGain( gain_value, dataHeader.samples); // default 8
    AddBrightness( brightness_value, dataHeader.samples); // default 400
    AddContrast( contrast_value, dataHeader.samples); // default 2
    
    isDataBufferReady = false;
    
    int samples_n = dataHeader.samples; // number of samples in the burst

    // определить фактическую дальность
    current_range = 10.0; // значение по умолчанию, при этом реально в файле может быть 1392, по таблице 1376
    for (int i=0;i<15;i++) {
        if ( samples_set[i] == samples_n ) {
            current_range = range_set[i];
            break;
        }
    }

    // вычислить углы и номер луча для предварительно вычисленных координат точек на экране
    tetha = dataHeader.angle*0.0125/360.0;
    ray = tetha*BEAMS;
    beam = ray;
    
    // сделать аппроксимацию, если samplesnum >= samples в таблице
    if ( samples_per_beam <= samples_n ) { // too much samples
        
        float kpN = (float)samples_n / (float)samples_per_beam; // scale ratio
            for(int i=0; i<samples_per_beam; i++) {

                int start = kpN*(float)i - 0.5;
                if (start<0) start=0;
                end   = kpN*(float)(i+1) + 0.5;
                if( end >= samples_n ) end = samples_n - 1;

                for( int j=start; j<=end; j++) {
                    a = pUnpackedSamples[j];
                    if(j==start) Amax=a;
                    else if (a>Amax) Amax=a;
                }
                
                samples_to_show[i] = Amax;
            }
    }
    else { // если samplesnum < samples в таблице
        float kpN = (float)samples_n / (float)samples_per_beam;
        int j; // sample inside beam index
        for(int i=0; i<samples_per_beam; i++) {
            j = (kpN*(float)i);
            samples_to_show[i] = pUnpackedSamples[j];
        }
    }

    
    // после аппроксимации заполнить samples_drawn[] правильными точками,
    // это преобразование из samples_to_show[] в реальные точки в текстуре
    for(int i=0; i < samples_per_beam; i++ ) {
        sample = samples_to_show[i];

        if (sample>4096) sample=4095;
    
        for ( int j = 0; j < pixel_table_n[beam][i]; j++ ) {
            x = pixel_table_x[beam][i][j];
            y = pixel_table_y[beam][i][j];
            samples_drawn[x+y*MAX_SAMPLES_TO_SHOW] = sample;
        }
    }

}

// нарисовать красный курсор по текущему положению луча, если это задано в настройках
if (pointer_show)
    FillRedCursor(cursor, diameter, tetha);
           
is_beam_ready = false;

// преобразования точек по палитре, добавление курсора и шкал дальности, результат в pixels[]
int samples_index = 0;
int pixels_index = 0;
for(y=0;y<diameter;y++) {
    for(x=0;x<diameter;x++) {
        sample = samples_drawn[x+samples_index];
        if (sample<4096) {
            
            if (show_in_rainbow_palette) pixels[pixels_index] = palette_rainbow[ sample ];
            else pixels[pixels_index] = palette_brown[ sample ];
        }
        else pixels[pixels_index]=background_color;
        
        if (cursor[pixels_index])
            pixels[pixels_index] = cursor[pixels_index];
        else 
            if (grid_show) 
                if (net[pixels_index]) pixels[pixels_index] = net[pixels_index];
                            
        ++pixels_index;
    }
    samples_index+=MAX_SAMPLES_TO_SHOW;
}


// преобразовать pixels[] в SDL2 текстуру
SDL_UpdateTexture(texture, NULL, pixels, diameter * sizeof(uint32_t));

// ----        вычисления, какую часть текстуры отображать на экране,         ----
// ---- чтобы обеспечить масштабирование и передвижение по картинке от сонара ----

//--- значения по умолчанию, куда копировать
dest.w = diameter;
dest.h = diameter;

// центрирование
dest.x = (window_width-diameter)/2;  
dest.y = (window_height-diameter)/2;
if (dest.y>16) dest.y-=16;

// прямоугольник, который копировать из большой текстуры
src.x = source_rect.x;
src.y = source_rect.y;
src.w = source_rect.w;
src.h = source_rect.h;


if ( window_height < window_width ) { // альбомная ориентация окна отображения
    float window_ratio = (float)window_width/((float)window_height-10);
    // .h не менять, увеличивать .w:
    dest.x = dest.x-(window_ratio-1.0)*(float)dest.w/2;
    src.x  = src.x-(window_ratio-1.0)*(float)src.w/2;

    dest.w = dest.w * window_ratio;
    src.w  = src.w  * window_ratio;
}
else { // портретная ориентация окна отображения
    float window_ratio = ((float)window_height-10)/((float)window_width);

    dest.y = dest.y-(window_ratio-1.0)*(float)dest.h/2;
    src.y  = src.y-(window_ratio-1.0)*(float)src.h/2;

    dest.h = dest.h * window_ratio;
    src.h  = src.h  * window_ratio;
}

if ((src.w+src.x)>diameter) {
    float k = (float)(diameter - src.x)/(float)src.w;
    src.w = diameter - src.x;
    dest.w *= k;
    dest.x = 0;
}

if (src.x<0) {
    float k_short = (float)(-src.x)/(float)src.w;
    float k = (float)(src.w+src.x)/(float)src.w;
    src.w = src.w+src.x; // cut left side
    src.x = 0;
    dest.x = (float)dest.w * k_short;
    dest.w *= k;
}

if ((src.h+src.y)>diameter) {
    float k = (float)(diameter - src.y)/(float)src.h;
    src.h = diameter - src.y;
    dest.h *= k;
    dest.y = 0;
}

if (src.y<0) {
    float k_short = (float)(-src.y)/(float)src.h;
    float k = (float)(src.h+src.y)/(float)src.h;
    src.h = src.h+src.y; // cut left side
    src.y = 0;
    dest.y = (float)dest.h * k_short;
    dest.h *= k;
}

// ---- конец вычислений, какую часть текстуры отображать на экране ----

// копирование в окно отображения прямоугольную часть большой картины
SDL_RenderCopy(renderer, texture, &src, &dest);

The result of displaying echoes without scaling:

The same panorama, but with a maximum increase in the area:

Display in another palette:

It was important to demonstrate that the developed program can work together with other libraries, with simple examples of use.

An example of using it with Qt, main.cpp file:

#include "mainwindow.h"
#include "sonarwindow.h"
#include <QApplication>
#include <QWidget>

// to compile:
// Build->Rebuild all

// to launch from command line copy gost_b.ttf, hrs900_20150526_063659.bin
// to directory ~/build-qt_example-Desktop-Debug  , then:

// cd ~/build-qt_example-Desktop-Debug
// export LD_LIBRARY_PATH=/home/<user>/qt_example:$LD_LIBRARY_PATH
// ./qt_example

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    // 
    File_Read("hrs900_20150526_063659.bin");
    File_Play(1); // 1 максимальная скорость, 2,3,4 медленнее

    View_setting(8, 5, 1, 400, 0, true, true, true, true, true);
    Create_Window(100,120,820,820);

    //Change_Window(200,200,620,720); 

    w.move(95,40);
    w.show();
    w.raise();
    w.activateWindow();

    return a.exec();
}

This is how the Qt example looks on the screen:

Sonarwindow.h library header file:

void View_setting(int gain, int deadzone, int contrast, int brightness, bool pallete, bool grid, bool autocontrast, bool pointer, bool fitwindow, bool repeatplayback);

// рекомендованные значения:
// bool grid_show = true, show_in_rainbow_palette = false, pointer = true, repeat_playback = true;
// int contrast_value = 1, deadzone_value = 5, gain_value = 8, brightness_value = 400;

int Create_Window( int x_pos, int y_pos, int width, int height );
int Change_Window( int x_pos, int y_pos, int width, int height );
int FillRawComBufferArray(unsigned char* inArray, int inArraySize);
int File_Read(const char* afilename);
int File_Play(int speed);
void File_Hold();
void File_Stop();

An example of using the library with the FLTK GUI library:

#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>

#include "sonarwindow.h"

#include <pthread.h>

#include <string.h>
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Menu_Bar.H>

// To install FLTK:
// sudo apt install fltk1.3-dev

// To compile and run:
// g++ -L/home/tolik/sonar_FLTK -o test main.cpp -lsonarwindow -L/usr/local/lib -lfltk -lXext -lXinerama -lXft -lX11 -lXi -lm -lpthread
// export LD_LIBRARY_PATH=/home/user/sonar_FLTK:$LD_LIBRARY_PATH
// ./test

// FLTK buttons callbacks
void start_button_callback(Fl_Widget *w, void *data) {
    File_Play(1);
    printf(" start \n");
}

void stop_button_callback(Fl_Widget *w, void *data) {
    File_Stop();
    printf(" stop \n");
}

bool stop_serial_reading=false;
bool usefile = false;
const char* USBname=NULL;
int USB; //USB port id
char buf="\0";
struct termios tty;
struct termios tty_old;

#define DEMOCOMBUFSIZE 128
unsigned char DemoComBuffer[DEMOCOMBUFSIZE];
int DemoComBufferLast=0;

int UpdateRawComBuffer()
{		
	while (DemoComBufferLast<sizeof(DemoComBuffer))
	{
		int testReading=read( USB, &DemoComBuffer[DemoComBufferLast], 1);
		if (testReading==-1)
		{
			break; //COM is empty				
		}		
		DemoComBufferLast++;		
		while((DemoComBufferLast<sizeof(DemoComBuffer)) && (testReading!=-1))
		{				
			testReading=read( USB, &DemoComBuffer[DemoComBufferLast++], 1);				
			//if (testReading!=-1) TotalComRead++;
			//printf("+%c", DemoComBuffer[DemoComBufferLast-1]);
		}	
		if (testReading==-1) DemoComBufferLast--;		
	}		
	int UsedElements=FillRawComBufferArray(&DemoComBuffer[0], DemoComBufferLast);		
	memmove((unsigned char*)&DemoComBuffer[0], ((unsigned char*)&DemoComBuffer[0])+UsedElements, sizeof(DemoComBuffer)-UsedElements);
	DemoComBufferLast-=UsedElements;
    return 0;
}    


void* thread_serial_port_parsing_auto2( void* args ) {      
    printf("thread_serial_port_parsing_auto2 - started\n");
    while(!stop_serial_reading) {                   
        UpdateRawComBuffer();
    }
    return &stop_serial_reading;
}




int main( int argc, char **argv ) {
	
    usefile = true;
    
    if( !usefile) { // just to communicate via serial port
        
        // select COM or file
        USBname="/dev/ttyS1";
        printf("%s\n", USBname);        
        USB = open( USBname, O_RDWR| O_NOCTTY | O_NONBLOCK);	
        if (USB < 0) printf("Error %i from open: %s\n", errno, strerror(errno));		
        memset (&tty, 0, sizeof tty);
        
        if ( tcgetattr ( USB, &tty ) != 0 )
        {
            //std::cout << "Error " << errno << " from tcgetattr: " << strerror(errno) << std::endl;
            printf("Error");
        }
        
        tty_old = tty;
        /* Set Baud Rate */
        //cfsetospeed (&tty, (speed_t)B38400);
        //cfsetispeed (&tty, (speed_t)B38400);
        
        cfsetospeed (&tty, (speed_t)B115200);
        cfsetispeed (&tty, (speed_t)B115200);		
        /* Setting other Port Stuff */
        tty.c_cflag     &=  ~PARENB;            // Make 8n1
        tty.c_cflag     &=  ~CSTOPB;
        tty.c_cflag     &=  ~CSIZE;
        tty.c_cflag     |=  CS8;
        tty.c_cflag     &=  ~CRTSCTS;           // no flow control
        tty.c_cc[VMIN]   =  0;                  // read doesn't block
        tty.c_cc[VTIME]  =  0;                  // 
        tty.c_cflag     |=  CREAD | CLOCAL;     // turn on READ & ignore ctrl lines
        /* Make raw */
        cfmakeraw(&tty);
        /* Flush Port, then applies attributes */
        tcflush( USB, TCIFLUSH );
        if ( tcsetattr ( USB, TCSANOW, &tty ) != 0)
        {
            //std::cout << "Error " << errno << " from tcsetattr" << std::endl;	
            printf("Error 1");
        }
        int n = 0,
        spot = 0;	
        n = read( USB, &buf, 1 );
        if (n < 0) {
            printf("Error 2\n");//std::cout << "Error reading: " << strerror(errno) << std::endl;	
        }			
        
        // launch COM serial reading thread (should be external)
        pthread_t id;    
        if (!usefile) {                
            int ret;        
            ret = pthread_create(&id, NULL, &thread_serial_port_parsing_auto2, NULL);
            if(ret==0) printf("main(): thread_serial_port_parsing_auto2() thread created successfully.\n");
            else{
                printf("main(): thread_serial_port_parsing_auto2() thread not created.\n");
                return 0; /*return from main*/
            }        
        }	

    }
    
    Fl_Menu_Item menuitems[] = {
      { "&File",              0, 0, 0, FL_SUBMENU },
        { "E&xit", 0, (Fl_Callback *)stop_button_callback, 0 },
        { 0 },
      { "&Settings", 0, 0, 0, FL_SUBMENU },
        { "&Language",       0, (Fl_Callback *)stop_button_callback, 0, FL_MENU_DIVIDER },
        { "&Sonar settings",        FL_COMMAND + 's', (Fl_Callback *)stop_button_callback },
        { "&View settings",       0, (Fl_Callback *)stop_button_callback },
        { "&Working folder",      0, (Fl_Callback *)stop_button_callback },
        { "&Autoscreenshot",     0, (Fl_Callback *)stop_button_callback },
        { 0 },
      { "&Help", 0, 0, 0, FL_SUBMENU },
        { "&About...",       0, (Fl_Callback *)stop_button_callback },
        { 0 },
      { 0 }
    };
    
    
    Fl_Window *window = new Fl_Window(80,10,850,800,"Control");
    Fl_Button *button_start = new Fl_Button(710, 130, 100, 25, "Start");
    Fl_Button *button_stop = new Fl_Button(710, 170, 100, 25, "Stop");

    Fl_Menu_Bar *m = new Fl_Menu_Bar(0, 0, 680, 30);
    m->copy(menuitems);

    int xyz_data;
    button_start->callback(start_button_callback, &xyz_data);
    button_stop->callback(stop_button_callback, &xyz_data);
  
    File_Read("hrs900_20150526_063659.bin");
    File_Play(1);
    View_setting(8, 5, 1, 400, true, true, true, true, true, true);
    
    Create_Window(100,100,650,800);   
    sleep(1);
    
    // Change_Window(200,200,620,720);
    window->end();
    window->show(argc, argv);
  
    return Fl::run();
}

Installing FLTK for Debian/Ubuntu:

sudo apt install fltk1.3-dev

Compiling the example with our library:

g++ -L/home/tolik/sonar_FLTK -o test main.cpp -lsonarwindow -L/usr/local/lib -lfltk -lXext -lXinerama -lXft -lX11 -lXi -lm -lpthread
 export LD_LIBRARY_PATH=/home/user/sonar_FLTK:$LD_LIBRARY_PATH

The program works according to the recorded data on the video:

The customer, before starting development, provided the source code of his program for Windows. In terms of displaying the pie chart, our source code turned out to be 5 times shorter, all thanks to the use of SDL2.

We were interested in trying to compile and run this display program under the Raspberry Pi. Rasbian, although based on Debian, has a different architecture, a good test in terms of cross-platform. It turned out to install SDL2, it turned out to compile, it turned out to run. The program worked much slower than on my old i3-2100, with an old Radeon 512MB video card. But it worked, without modifications, which is good news. The customer used this library mainly with Ubuntu.

Similar Posts

Leave a Reply