Создание простого Python блока для GnuRadio

В один прекрасный момент хочется релизовать что-то особое, что отсутвует в стандартной поставке GnuRadio. Выход из этого один — написание своего блока. Информациии об этом не так много,  и большая часть информации сконцентрированна в официальной документации.
В этой статье я покажу как написать простой вспомогательный блок для GnuRadio используя Python. Это блок можно будет использовать для простого управления внешними устройствами, например включения усилителя, переключения антенного коммутатора и прочих вещей.

Для начала  — общая информация. Блоки в GnuRadio могут быть написаны на C++ или Python. C++ — универсальный выбор, с его помощью вы можете создавать как блоки для ЦОС, так и вспомогательные блоки. Блоки написанные на Python подходят только для вспомогательных блоков, так как их производительность гораздо ниже чем аналогичных блоков на C++.
В нашм случае, создания служебного модуля, Python будет подходящим выбором (а так же по той причине, что автор этих строк не может С++ 🙂 )

Итак, какие же блоки могут быть, согласно документации.

Синхронный блок

Выбор данного типа создает блок, который получает и выдает одинаковое количество объектов или семплов на каждый порт. Синхронный блок может иметь любое число входов или выходов.
Синхронный блок не имеющий входов называется источником («Source»), а блок не имеющий выходных портов — выходом («Sink»).

Прореживающий (децимирующий) блок

В прореживающем блоке поток данных фиксированной скорости всегда имеет количество входных семплов кратное фиксированному значению количества выходных семплов.
Пример прореживающего блока на C++

#include 

class my_decim_block : public gr_sync_decimator
{
public:
  my_decim_block(...):
    gr_sync_decimator("my decim block", 
                      in_sig,
                      out_sig,
                      decimation)
  {
    //constructor stuff
  }

  //work function here...
};

Как можем увидеть конструктор четвертым параметром принимает значение децимации
Так же логично предположить что количество выходных семплов равно количеству выходных деленному на коэффициент децимации.

Интерполирующий блок

Интерполирующий блок так же поток данных фиксированной скорости и имеет количество входных семплов кратное фиксированному значению количества выходных семплов.
Пример интерполирущего блока на C++

#include 

class my_interp_block : public gr_sync_interpolator
{
public:
  my_interp_block(...):
    gr_sync_interpolator("my interp block", 
                         in_sig,
                         out_sig,
                         interpolation)
  {
    //constructor stuff
  }

  //work function here...
};

Количество выходных семплов равно количеству выходных умноженному на коэффициент интерполяции.

 Базовый блок

У базового блока нет никаких зависмостей между количеством входных портов и количеством выходных. Все другие типы блоков являются упрощениями базового блока. При написании своего блока смело выполняте наследование от данного блока, если другие типы блоков вам не подходят.

Вот пример базового блока:

#include 

class my_basic_block : public gr_block
{
public:
  my_basic_adder_block(...):
    gr_block("another adder block",
             in_sig,
             out_sig)
  {
    //constructor stuff
  }

  int general_work(int noutput_items,
                   gr_vector_int &ninput_items,
                   gr_vector_const_void_star &input_items,
                   gr_vector_void_star &output_items)
  {
 //cast buffers
    const float* in0 = reinterpret_cast(input_items[0]);
    const float* in1 = reinterpret_cast(input_items[1]);
    float* out = reinterpret_cast(output_items[0]);

    //process data
    for(size_t i = 0; i < noutput_items; i++) {       out[i] = in0[i] + in1[i];     }     //consume the inputs
    this->consume(0, noutput_items); //consume port 0 input
    this->consume(1, noutput_items); //consume port 1 input
    //this->consume_each(noutput_items); //or shortcut to consume on all inputs

    //return produced
    return noutput_items;
  }
};

Пару замечаний по поводу базового блока:

— Класс перезагружает метод general_work(), а не work()
-У general_work() есть параметр ninput_items. ninput_items явлется вектором описывающим размер буфера для каждого входного порта.
— Перед возвратом значения из general_work() нужно потребить (consume) использованные входы.
— Предполагается что количество элементов во входных буферах равно noutput_items. Пользователь может изменить это поведение перезагрузкой метода forecast()

Таковы 3 базовых типа блоков в GnuRadio. И судя по этой информации нам потребутеся базовый блок. Ну что же, давайте займемся непосредсвенно созданием блока.

GnuRadio имеет в своем составе утилиту для генерации скелета нового блока gr_modtool.  Откроем консоль, перейдем в каталог в котором будем работать, а затем сгенерируем скелет блока который мы назовем «srig» следующей командой:

$ gr_modtool newmod srig
Creating out-of-tree module in ./gr-srig… Done.
Use ‘gr_modtool add’ to add a new block to this currently empty module.

Утилита создала каталог с именем «gr-srig». Переместимся в него, и инициализируем служебные файлы для блока котрый будет написан на Python

$ cd gr-srig
$ gr_modtool add -t general -l python
GNU Radio module name identified: srig
Language: Python
Enter name of block/code (without module name prefix): srig_py
Block/code identifier: srig_py
Enter valid argument list, including default arguments: serialPort, pin0, pin1, pin2, pin3
Add Python QA code? [Y/n] n
Adding file ‘python/srig_py.py’…
Adding file ‘grc/srig_srig_py.xml’…
Editing grc/CMakeLists.txt…

Утилит спросит нас имя для файла в котором будет код (srig_py), параметры которые должны быть переданы при инициализации (у нас это будет имя последовательного порта, и состояния 4 выходов устройства), а так же требуется ли добавлять код для Unit тестирования и запускать его (в нашем случае мы можем не делать этого, но в случае серьезного проекта это настоятельно рекомендуется).

Теперь переместимся в каталог «python» и и откроем сгенерированный файл srig_py.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import numpy
from gnuradio import gr

class srig_py(gr.basic_block):
   """
   docstring for block srig_py
   """
   def __init__(self, serialPort, pin0, pin1, pin2, pin3):
      gr.basic_block.__init__(self,
         name="srig_py",
         in_sig=[<+numpy.float+>],
         out_sig=[<+numpy.float+>])

   def forecast(self, noutput_items, ninput_items_required):
      #setup size of input_items[i] for work call
      for i in range(len(ninput_items_required)):
         ninput_items_required[i] = noutput_items

   def general_work(self, input_items, output_items):
      output_items[0][:] = input_items[0]
      consume(0, len(input_items[0]))
      #self.consume_each(len(input_items[0]))
      return len(output_items[0])

Скелет состоит из 3 методов: __init__() для первичной инициализации, forecast() для входных буферов, и general_work() в котором и должна происходить обрабаботка сигналов. В нашем случае блок не будет обрабатывать никаких сигналов, а будет лишь менять состояние внутренних переменных по запросу, и при изменении состояиния переменных- отдавать команду внешнему устройству через последовательный порт.
Теперь я модифицирую скелет блока, и дам пояснения.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import numpy
import serial
from gnuradio import gr

class srig_py(gr.basic_block):
    """
    docstring for block srig_py
    """
    def __init__(self, serialPort, pin0, pin1, pin2, pin3):
        gr.basic_block.__init__(self,
            name="srig_py",
            in_sig=[numpy.float32],
            out_sig=[numpy.float32])
	self.states = {True:'1', False:'0'}
	self.port = serial.Serial(serialPort, baudrate=9600, timeout=3.0)
	if (self.port.isOpen()):
	    print "SRIG: Re-open serial port %s" % serialPort
	    self.port.close()
	    self.port.open()
	else:
	    print "SRIG: Open serial port %s" % serialPort
	    self.port.open()
	self.p0_cstate = pin0
	self.p1_cstate = pin1
	self.p2_cstate = pin2
	self.p3_cstate = pin3

    def forecast(self, noutput_items, ninput_items_required):
        #setup size of input_items[i] for work call
        for i in range(len(ninput_items_required)):
            ninput_items_required[i] = noutput_items
    def set_pin0(self, pin0):
	if self.p0_cstate != pin0:
	    self.p0_cstate = pin0
	    cmd = """S0P%s\n""" % self.states[pin0]
	    self.port.write(cmd)
	    print "SRIG: Pin 0 %s" % pin0
    def set_pin1(self, pin1):
	if self.p1_cstate != pin1:
	    self.p1_cstate = pin1
	    cmd = """S1P%s\n""" % self.states[pin1]
	    self.port.write(cmd)
	    print "SRIG: Pin 1 %s" % pin1
    def set_pin2(self, pin2):
	if self.p2_cstate != pin2:
	    self.p2_cstate = pin2
	    cmd = """S2P%s\n""" % self.states[pin2]
	    self.port.write(cmd)
	    print "SRIG: Pin 2 %s" % pin2
    def set_pin3(self, pin3):
	if self.p3_cstate != pin3:
	    self.p3_cstate = pin3
	    cmd = """S3P%s\n""" % self.states[pin3]
	    self.port.write(cmd)
	    print "SRIG: Pin 3 %s" % pin3

    def general_work(self, input_items, output_items):
        output_items[0][:] = input_items[0]
        consume(0, len(input_items[0]))
        #self.consume_each(len(input_items[0]))
        return len(output_items[0])

строка 4: Импортируем модуль для работы с последовательным портом.
строки 14-15:  Здесь я просто раскомментировал указания, данные какого типа должны быть на входах и выходах. Для нас это не имеет смысла, така как блок не обрабатывает данные.
строка 16: создаем прееменную типа «словарь», дабы сопоставить значение True и False с ASCII символами «1» и «0». Пригодится для отправки команды устройству.
строка 17: инициализируем объект для работы с последоватльным портом.
строки 18-24: Открываем порт.
строки 25-28: создаем 4 внутренних переменных для хранения состояния выхода устройства, и присвояем им первичные значения полученные при иницилизации.
строки 34-57: Здесь я написал 4 практически одинаковых сеттера для проверки значения и передачи команды устройству. Суть такова, что если при работе графа будет изменена какая-нибудь переменная этого блока, то это вызовет метод «set_имяпеременной(новое_значение)«. Следовательно создав методы set_pin0, set_pin1 и т.д. мы можем поймать новое значение переменной. Далее мы сравниваем новое значение с текущим, и если они отличаются, сохраняем новое, и отправляем команду устройству через последовательный порт. Формат команды незатейлив:»Sномер_портаPсостояние_порта«

С кодом все :). Теперь создадим графическое представление для нашего блока, которое мы будем добавлять на схему. Сохраняем файл, перемещаемся из каталога python выше, и заходим в каталог grc. В нем мы открываем файл srig_srig_py.xml и обновляем его как показано ниже.

<?xml version="1.0"?>
<block>
  <name>Serial Rig</name>
  <key>srig_srig_py</key>
  <category>Device control</category>
  <import>import srig</import>
  <make>srig.srig_py($serialPort, $pin0, $pin1, $pin2, $pin3)</make>
  <callback>set_pin0($pin0)</callback>
  <callback>set_pin1($pin1)</callback>
  <callback>set_pin2($pin2)</callback>
  <callback>set_pin3($pin3)</callback>
  <!-- Make one 'param' node for every Parameter you want settable from the GUI.
       Sub-nodes:
       * name
       * key (makes the value accessible as $keyname, e.g. in the make node)
       * type -->
  <param>
    <name>Device name</name>
    <key>serialPort</key>
    <type>string</type>
  </param>
  <param>
    <name>Pin 0</name>
    <key>pin0</key>
    <type>bool</type>
  </param>
  <param>
    <name>Pin 1</name>
    <key>pin1</key>
    <type>bool</type>
  </param>
  <param>
    <name>Pin 2</name>
    <key>pin2</key>
    <type>bool</type>
  </param>
  <param>
    <name>Pin 3</name>
    <key>pin3</key>
    <type>bool</type>
  </param>

  <!-- Make one 'sink' node per input. Sub-nodes:
       * name (an identifier for the GUI)
       * type
       * vlen
       * optional (set to 1 for optional inputs) -->

  <!-- Make one 'source' node per output. Sub-nodes:
       * name (an identifier for the GUI)
       * type
       * vlen
       * optional (set to 1 for optional inputs) -->
</block>
in_list

строка 3: Задаем отображаемое на блоке название
строка 5: Категория в которой находится блок. Если она не существует, то будет создана.  В нашем случае это будет выглядеть вот так:
строки 8-11: тут мы и указываем, что при изменении переменных «pinN» нужно вызывать метод set_pinN().
строки 17-41: Описываем какие поля будут отображаться в блоке когда мы его откроем, их описания. Все довольно ясно.

Сохраняем xml файл описания блока. Осталось совсем немного, нужно лишь немного подправить место, куда будут скопированны файлы блока. В том же каталоге grc открываем файл CMakeLists.txt  и изменяем строку

srig_srig_py.xml DESTINATION share/gnuradio/grc/blocks
на
srig_srig_py.xml DESTINATION /usr/share/gnuradio/grc/blocks

Все готово для установки и проверки. Перемещаемся на уровень выше в каталог gr-srig, и выполняем стандартную процедуру сборки:
$ mkdir build
$ cd build/
$ cmake ../
— The CXX compiler identification is GNU 5.4.0
— The C compiler identification is GNU 5.4.0
— Check for working CXX compiler: /usr/bin/c++
— Check for working CXX compiler: /usr/bin/c++ — works
— Detecting CXX compiler ABI info
— Detecting CXX compiler ABI info — done
— Detecting CXX compile features
— Detecting CXX compile features — done
— Check for working C compiler: /usr/bin/cc
— Check for working C compiler: /usr/bin/cc — works
— Detecting C compiler ABI info
— Detecting C compiler ABI info — done
— Detecting C compile features
— Detecting C compile features — done
— Build type not specified: defaulting to release.
— Boost version: 1.58.0
— Found the following Boost libraries:
— filesystem
— system
— Found PkgConfig: /usr/bin/pkg-config (found version «0.29.1»)
— Checking for module ‘cppunit’
— Found cppunit, version 1.13.2
— Found CPPUNIT: /usr/lib/x86_64-linux-gnu/libcppunit.so;dl
— Could NOT find Doxygen (missing: DOXYGEN_EXECUTABLE)
Checking for GNU Radio Module: RUNTIME
— Checking for module ‘gnuradio-runtime’
— Found gnuradio-runtime, version 3.7.9
* INCLUDES=/usr/include
* LIBS=/usr/lib/x86_64-linux-gnu/libgnuradio-runtime.so;/usr/lib/x86_64-linux-gnu/libgnuradio-pmt.so
— Found GNURADIO_RUNTIME: /usr/lib/x86_64-linux-gnu/libgnuradio-runtime.so;/usr/lib/x86_64-linux-gnu/libgnuradio-pmt.so
GNURADIO_RUNTIME_FOUND = TRUE
— No C++ sources… skipping lib/
— No C++ sources… skipping swig/
— Found PythonInterp: /usr/bin/python2 (found suitable version «2.7.12», minimum required is «2»)
— Could NOT find Doxygen (missing: DOXYGEN_EXECUTABLE)
— Configuring done
— Generating done
— Build files have been written to: /storage/Temp/gr-srig/build

$ make
Scanning dependencies of target pygen_python_31622
[ 50%] Generating __init__.pyc, srig_py.pyc
[100%] Generating __init__.pyo, srig_py.pyo
[100%] Built target pygen_python_31622
Scanning dependencies of target pygen_apps_9a6dd
[100%] Built target pygen_apps_9a6dd

$ sudo make install
[sudo] пароль для igor:
[100%] Built target pygen_python_31622
[100%] Built target pygen_apps_9a6dd
Install the project…
— Install configuration: «Release»
— Up-to-date: /usr/local/lib/cmake/srig/srigConfig.cmake
— Up-to-date: /usr/local/include/srig/api.h
— Up-to-date: /usr/local/lib/python2.7/dist-packages/srig/__init__.py
— Up-to-date: /usr/local/lib/python2.7/dist-packages/srig/srig_py.py
— Installing: /usr/local/lib/python2.7/dist-packages/srig/__init__.pyc
— Installing: /usr/local/lib/python2.7/dist-packages/srig/srig_py.pyc
— Installing: /usr/local/lib/python2.7/dist-packages/srig/__init__.pyo
— Installing: /usr/local/lib/python2.7/dist-packages/srig/srig_py.pyo
— Up-to-date: /usr/share/gnuradio/grc/blocks/srig_srig_py.xml
$ sudo ldconfig

Все! Мы имеем новый блок в GnuRadio. Можно запустить и проверить что получилось.

srig_in_gnuradio


Давайте теперь проверим как оно работает. Я воспользуюсь своей отладочной платой с микроконтроллером Atmega8, которая будет зажигать светодиоды когда значение соответсвующего выхода будет True.

Код для Atmega8 даю без пояснений, если кто захочет, тот сможет реализовать устройство приинимающие команды от блока на своем любимом микроконтроллере.

#pragma once
#define F_CPU 8000000UL

#define UART_BAUD 9600L
#define prescaler ((F_CPU/(16*UART_BAUD))-1)
#define HI(x) ((x)>>8)
#define LO(x) ((x)& 0xFF)

#define PORT_ON( port_letter, number )                  port_letter |= (1<<number)
#define PORT_OFF( port_letter, number )                 port_letter &= ~(1<<number)
#define sbi(port, bit) (port) |= (1 << (bit))  
#define cbi(port, bit) (port) &= ~(1 << (bit))
#define TRUE 1
#define FALSE 0
#define CHAR_NEWLINE '\n'
#define CHAR_RETURN '\r'
#define RETURN_NEWLINE "\r\n"

#define RIG_REG DDRC 
#define RIG_PORT PORTC
#define RIG_PIN0 0
#define RIG_PIN1 1
#define RIG_PIN2 2
#define RIG_PIN3 3

#include <avr/interrupt.h>
#include <stdlib.h>

char sGreet[]="RIG OK\r\n";
char data_in[5] = "";
volatile uint8_t cmd_len = 0;
uint8_t byte_count = 0;
volatile unsigned char command_ready;

ISR(USART_RXC_vect){
    char inbyte;
    inbyte = UDR;
    if (inbyte == CHAR_NEWLINE) {
        command_ready = TRUE;
	cmd_len = byte_count;
        byte_count = 0;
    } else {
    	data_in[byte_count] = inbyte;
        byte_count++;
    }
}


void USART_Init(void){
        UBRRL = LO(prescaler);
        UBRRH = HI(prescaler);
        UCSRA = 0;
        UCSRB = (1<<RXCIE)|(1<<RXEN)|(1<<TXEN);
        UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); //8 bit 1 stop-bit
}

void USART_SendChar(unsigned char sym)
{
          while( !(UCSRA & (1<<UDRE)) );
            UDR = sym;
}

void USART_SendByte(uint8_t byte)
{
          while( !(UCSRA & (1<<UDRE)) );
            UDR = byte;
}

void USART_SendString(char *string)
{
          while( *string != '\0'){
                USART_SendChar(*string);
                string++;
          }
}


void switch_pin(char pin, char status){
	if (pin == '0'){
		if (status == '1'){
			PORT_ON(RIG_PORT, RIG_PIN0);
		}else{
			PORT_OFF(RIG_PORT, RIG_PIN0);
		}
	}
	if (pin == '1'){
		if (status == '1'){
			PORT_ON(RIG_PORT, RIG_PIN1);
		}else{
			PORT_OFF(RIG_PORT, RIG_PIN1);
		}
	}
	if (pin == '2'){
		if (status == '1'){
			PORT_ON(RIG_PORT, RIG_PIN2);
		}else{
			PORT_OFF(RIG_PORT, RIG_PIN2);
		}
	}
	if (pin == '3'){
		if (status == '1'){
			PORT_ON(RIG_PORT, RIG_PIN3);
		}else{
			PORT_OFF(RIG_PORT, RIG_PIN3);
		}
	}
}
int main(void)
{
    USART_Init();
    sbi(RIG_REG, RIG_PIN0);
    cbi(RIG_PORT, RIG_PIN0);
    sbi(RIG_REG, RIG_PIN1);
    cbi(RIG_PORT, RIG_PIN1);
    sbi(RIG_REG, RIG_PIN2);
    cbi(RIG_PORT, RIG_PIN2);
    sbi(RIG_REG, RIG_PIN3);
    cbi(RIG_PORT, RIG_PIN3);
    sei();
    while(1)
    {
        if ((command_ready == TRUE) && (cmd_len == 4)) {
		if (data_in[0] == 'R'){
    			USART_SendString(sGreet);
		} 
		if (data_in[0] == 'S'){
    			switch_pin(data_in[1], data_in[3]);
		} 
           	command_ready = FALSE;
        } 
    }
}

Один комментарий к “Создание простого Python блока для GnuRadio

  1. Игорь

    Игорь доброго времени суток!
    спасибо за опубликованный контент, он очень помог разобраться и начать использование GNU в обработке как радиосигналов, так и звука.
    Тем не менее, возникла следующая трудность — на могу добавить блок инверсии (https://github.com/gnuradio/gnuradio/blob/master/gr-audio/examples/python/spectrum_inversion.py)
    если можете окажите консультативную помощь. Сильно поможет!!)))

    С уважением Игорь Попов

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *