Управляем Icom PCR-1000 при помощи GnuRadio

Пару лет назад, я приобрел свой первый более-менее серьезный радоприемник: Icom PCR-1000. Одной из причин, по которой я выбрал именно его, было управление по RS-232. Итак, получил я его, подключил, погонял под Windows. Красота! Лишь одно омрачало прекрасное будущее, я пользуюсь Linux и FreeBSD, а родного софта от Icom под *nix системы как-то не наблюдалось.
Пошарившись в Интернете, я обнаружил что протокол управления приемником довольно прост, а посему почему бы не попробовать написать программу управления самому? Что ж, расчехляем свой любимый текстовый редактор,  понеслись!
Все самое важное для нас сконцентрированно на странице GM4JJJ о этом приемнике тут.
Внимательно изучаем и выписываем полезные команды, такие как включение-выключение, перестройка по частоте, и прочее. Далее. Нам понадобится графический интерфейс для управления. Признаюсь честно, я решил ваять GUI для PCR1000 используя GnuRadio только потому, что не владею написанием графических приложений под Linux 🙂 А GnuRadio нам предоставляет готовые блоки. Бери, да строй.
Так же подумаем об архитектуре нашей программы управления. Я решил разнести часть выполняющую непосредственно управление приемником и часть отвечающую за интерфейс.  Связь между серверной частью и клиентской будет реализована через блоки RPCXML Client. Это позволит, если я захочу, управлять приемником через Интернет.

Вот код нашего сервера:

#!/usr/bin/env python
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
import serial

#  INIT
sFrGiga = '0' #Переменная для гигагерц
sFrMega = '000' #Переменная для мегагерц
sFrKilo = '000' #Переменная для килогерц
sFrHz='000' #Переменная для герц
sModulation = '02' #Переменная для типа модуляции, стандартное значение 02 - амплитудная модудляция  
sFilter = '03' #Ширина фильтра, стандартное значение 03 - 50 КГц
sVolume = '50' # Громкость, стандартное значение 50 (максимальное 255)
sPower='00' #Питание
sSquelch = '00' #Щумоподавитель, по умолчанию выключен
sIfshift = '00' #Сдвиг промежуточной частоты
sAgc = '00' # AGC выключен
sAt = '00' # аттенюатор выключен
sNb = '00' # Антишумовой фильтр выключен

#Создаем объект для работы с последовательным портом
#У меня PCR1000 подключен через USB-RS323 переходник, поэтому я использую устройство /dev/ttyUSB0
#К слову под Linux мне не удалось заставить стабильно работать PCR1000 через RS-232, приемник выключался
# по неактивности через 15 секунд. При работе через переходник - все отлично.
# Забавно то, что под FreeBSD приемник стабильно работал и через RS-232 и через USB переходник.

ser = serial.Serial("/dev/ttyUSB0")
#ser = serial.Serial("/dev/ttyS0")

#Функции производящие передачу настроек в PCR1000
def set_pcr_frequency(): #Функция для задания частоты
	conf_string = 'K0'+ sFrGiga + sFrMega + sFrKilo + sFrHz + sModulation + sFilter + '00' #Конструируем строку для передачи
	print conf_string #Выведем строку в консоль, для того чтобы видеть что мы отсылаем.
	ser.write(conf_string + '\r\n') #Пишем в последовательный порт

def set_pcr_volume(): #Функция для задания громкости
	conf_string = 'J40'+ sVolume
	print conf_string
	ser.write(conf_string + '\r\n')

def set_pcr_squelch(): #Функция для управления шумоподавителем
	conf_string = 'J41'+ sSquelch
	print conf_string
	ser.write(conf_string + '\r\n')

def set_pcr_ifshift(): #Функция для контроля сдвигом частоты
	conf_string = 'J43'+ sIfshift
	print conf_string
	ser.write(conf_string + '\r\n')

def set_pcr_agc(): #Функция контроля AGC
	conf_string = 'J45'+ sAgc
	print conf_string
	ser.write(conf_string + '\r\n')

def set_pcr_nb():#Управление антишумовым фильтром
	conf_string = 'J46'+ sNb
	print conf_string
	ser.write(conf_string + '\r\n')

def set_pcr_at():#Управление аттенютенатором
	conf_string = 'J46'+ sAt
	print conf_string
	ser.write(conf_string + '\r\n')

def set_pcr_power():#Функция управления питанием
	conf_string = 'H1'+ sPower
	print conf_string
	ser.write(conf_string + '\r\n')	#Пишем в порт
	reply = ser.readline() #Считываем ответ из порта
	print reply #выводим ответ в консоль

#-----------Основнная программа------------------------
#Открываем последовательный порт
if ser.isOpen(): # Если он уже открыт 
	ser.close() # Закроем
	ser.open() # Откроем вновь
	ser.write("test\n") # Запишем что-нибудь
else: # Если закрыт
	ser.open()  # Откроем порт
	ser.write("test\n") # Пишем что-нибудь
reply = ser.readline(2) # Читаем ответ
print reply # Выводим ответ
ser.write("G300\r\n") # Переводим приемник в режим простого приема команд. В этом режиме PCR1000 не будет немедленно отвечать, была ли
#принята команда, или она была отвергнута из-за ошибки. 

class RequestHandler(SimpleXMLRPCRequestHandler):
	    rpc_paths = ('/RPC2',)
# Создаем наш XMLRPC сервер, и настраиваем его работать на localhost, на порту 8088
server = SimpleXMLRPCServer(("localhost", 8088),
		requestHandler=RequestHandler,
		logRequests = False,
		 allow_none=True)
server.register_introspection_functions()

#Начинаем определять наши callback функции которые будут вызываться клиентом
class ControlFunctions:
	def set_fr_mhz(self, x): #Эта функция будет вызвана как только будет измененно значение мегагерц
		global sFrMega # Используем нашу глобальную переменную sFrMega
		sFrMega = "%03d" % (x) # Присваиваем переменной sFrMega пришедшее значение, используем десятичный формат 
		set_pcr_frequency() # Вызваем нашу функцию установки частоты
	def set_fr_khz(self, x):
		global sFrKilo
		sFrKilo = "%03d" % (x)
		set_pcr_frequency()
	def set_fr_modulation(self, x):
		global sModulation
		sModulation = "%02d" % (x)
		set_pcr_frequency()
	def set_fr_filter(self, x):
		global sFilter
		sFilter = "%02d" % (x)
		set_pcr_frequency()
	def set_volume(self, x):
		global sVolume
		sVolume = "%02X" % (x) #Обратите внимание что тут мы используем шестнадцатиричный формат
		set_pcr_volume()
	def set_squelch(self, x):
		global sSquelch
		sSquelch = "%02X" % (x)
		set_pcr_squelch()
	def set_ifshift(self, x):
		global sIfshift
		sIfshift = "%02X" % (x)
		set_pcr_ifshift()
	def set_agc(self, x):
		global sAgc
		sAgc = "%02d" % (x)
		set_pcr_agc()
	def set_nb(self, x):
		global sNb
		sNb = "%02d" % (x)
		set_pcr_nb()
	def set_at(self, x):
		global sAt
		sAt = "%02d" % (x)
		set_pcr_at()
	def set_power(self, x):
		global sPower
		sPower = "%02d" % (x)
		set_pcr_power()
server.register_instance(ControlFunctions()) #регистрируем наши callback функции
server.serve_forever() # Запускаем наш сервер

Кстати, код сервера котрый мы написали, полностью GnuRadio-независим, он использует только стандартные модули Питона, и может быть запущен хоть на Raspberry Pi, хоть на Windows-машине.

Теперь, самое сложное — клиент. Сейчас вы поймете почему. 🙂
Начну с конца — с показа результата, того что у нас должно получиться.
pcr1000_1Не сказать что дизайн блещет изысками, он был создан для моего личного использования. Итак, что мы тут видим: управление модуляцией, шириной фильтра, громкостью, шумодавом, и сдвигом промежуточной частоты.  Кнопки для включения и выключения AGC, антишумового фильтра и аттенюатора.
Управление частотой немного урезано, отсутствует поле для гигагерц (ну не заглядываю я на столь высокие частоты), и для герц (тоже как-то не сильно требовалось точная подстройка). Частоту можно менять:
а) Полозунком
б) Кнопками «+» и «-«. При этом частота поменяется на выбранное в выпадающем меню слева кол-во мегагерц или килогерц.
в) Прямым вводом значения частоты в поля.

На вкладки «Auto» и  «Programmable» можно не обращать внимания, это я хотел реализовать автоматический и программируемый проход по частотам, но лень-матушка помешала. А вкладки остались.

Теперь посмотрм как это выглядит в GnuRadio. Зажмурьтесь, это страшно 🙂
pcr1000_2
И это еще на картинке не все убралось. Для полноты ощущений рекомендую скачать граф, и открыть его в GnuRadio (ссылки как обычно внизу статьи).
Сердце и суть нашего графа — это блоки XMLRPC Client (по одному на каждый передаваемый серверу параметр) и блоки Variable Config которые служат нам для хранения и загрузки настроек.

Что, нетерпится проверить как все работает? Так давайте проверим!
Подключаем наш ICOM PCR-1000 к компьютеру, и запускаем серверную часть командой:
$ sudo ./rpcserver.py

Далее запускаем граф клиентской части на выполнение.
Теперь нажмем кнопку «Power» в нашем клиенте, и немного подвигаем полозунок громкости. В этот момент, в консоли где был запущен наш сервер управления, вы должны увидеть управляющие команды передаваемые приемнику:

H101
G000

J405D
J405D
J405B
J405B
J405B
J405B

И да, естественно вы должны услышать звук из динамика приемника.
Подвигайте полозунки, меняя частототу, и типы модуляции. Все это должно работать.
А теперь попробуйте понажимать кнопки «+» и «-» для движения повышения и понижения частоты. Хм… А они не работают. Почему? Да потому что в GnuRadio увы не возможно в графическом режиме настроить логику работы этих кнопок. Ведь что эти кнопки должны делать? Они должны брать текущее значение шага частоты из выпадающего списка, и добавлять (или вычитать) его к текущему значению частоты. Ну и попутно отслеживать не получилось ли значение больше 999 или меньше 0.
Но такую логику реализовать чисто графическими методами мы не можем. Но! Выход есть! Как всегда, нам поможет «доработка напильником».
Как я раньше уже упоминал, GnuRadio при запуске создает из  графической схемы блоков Python скрипт, и именно его запускает на исполнение. А что это значит? А то что мы можем внутри этого скрипта дописать свою, какую угодно сложную логику работы.
Идем в каталог с нашим grc файлом для клиента. Так как схему мы уже запускали, то мы видим рядом с grc файлом графа нужный нам питон-скрипт.
Вы можете его запустить щелкнув по нему мышью, или запустив в консоли командой:
$ ./pcrcontrol.py

Давайте создадим копию этого файла, потому что если мы будем вносить изменения в существующий файл, то при следующем запуске он будет опять перезаписан сгенерированным из графа скриптом. Пусть наш скопированный файл называется pcrcontrol_alt.py

Итак, нам предстоит усовершенствовать и доделать следующие вещи:
1) Наладить работу кнопок для передвижения по частоте.
2) Сделать так, чтобы по нажатию кнопки питания приемник сразу включался на нужной частоте. Сейчас кнопка включения только включает 🙂

Итак приступим к ковырянию. Открываем наш скрипт в текстовом редакторе.
Нам нам надо модифицировать код для кнопок mhz_up, mhz_down, khz_up и khz_down
Вот как, к примеру, выглядит сгенерированный GnuRadio код для кнопки mhz_up.

def set_mhz_up(self, mhz_up):
        self.mhz_up = mhz_up
        self._mhz_up_chooser.set_value(self.mhz_up)

Нам надо чтобы эта кнопка увеличивала текущую частоту, добавляя текущее значение выпадающего списка mhz_step к значению поля fr_mhz. Нет ничего проще! Берем и складываем значения возвращаемые функциями get_fr_mhz() и get_mhz_step(), а получившееся значение присваиваем полю fr_mhz используя функцию set_fr_mhz()

    def set_mhz_up(self, mhz_up):	#Mhz up button
        self.mhz_up = mhz_up
        self._mhz_up_chooser.set_value(self.mhz_up)
        self.set_fr_mhz(self.get_fr_mhz() + self.get_mhz_step())

Таким же образом модифицируем функции для остальных кнопок:

    def set_mhz_down(self, mhz_down):	#Mhz down button
        self.mhz_down = mhz_down
        self._mhz_down_chooser.set_value(self.mhz_down)
        self.set_fr_mhz(self.get_fr_mhz() - self.get_mhz_step())

 

    def set_khz_up(self, khz_up):	#KHz up button
        self.khz_up = khz_up
        self._khz_up_chooser.set_value(self.khz_up)
        self.set_fr_khz(self.get_fr_khz() + self.get_khz_step())

 

    def set_khz_down(self, khz_down):	#Khz down button
        self.khz_down = khz_down
        self._khz_down_chooser.set_value(self.khz_down)
        self.set_fr_khz(self.get_fr_khz() - self.get_khz_step())

Так, с кнопками расправились. Почти. Теперь надо сделать так чтобы значения в полях частот не вылезали за пределы 999 и 0.
Отыщем функцию set_fr_mhz

    def set_fr_mhz(self, fr_mhz):
        self.fr_mhz = fr_mhz
        self.xmlrpc_client_0.set_fr_mhz(self.fr_mhz)
        self._fr_mhz_init_config = ConfigParser.ConfigParser()
        self._fr_mhz_init_config.read("pcrcontrol.conf")
        if not self._fr_mhz_init_config.has_section("main"):
                self._fr_mhz_init_config.add_section("main")
        self._fr_mhz_init_config.set("main", "fr_mhz", str(self.fr_mhz))
        self._fr_mhz_init_config.write(open("pcrcontrol.conf", 'w'))
        self._fr_mhz_slider.set_value(self.fr_mhz)
        self._fr_mhz_text_box.set_value(self.fr_mhz)

И добавим  условие в самое начало функции, что если значение получиось больше 999 то оно 0, и наоборот, если меньше 0, то 999.

    def set_fr_mhz(self, fr_mhz):
       if fr_mhz > 999:
	    fr_mhz = 0
        elif fr_mhz < 0:
	    fr_mhz = 999
        self.fr_mhz = fr_mhz 
        self.xmlrpc_client_0.set_fr_mhz(self.fr_mhz)
        self._fr_mhz_init_config = ConfigParser.ConfigParser()
        self._fr_mhz_init_config.read("pcrcontrol.conf")
        if not self._fr_mhz_init_config.has_section("main"):
                self._fr_mhz_init_config.add_section("main")
        self._fr_mhz_init_config.set("main", "fr_mhz", str(self.fr_mhz))
        self._fr_mhz_init_config.write(open("pcrcontrol.conf", 'w'))
        self._fr_mhz_slider.set_value(self.fr_mhz)
        self._fr_mhz_text_box.set_value(self.fr_mhz)

Теперь подправим функцию set_fr_khz(). Тут чуточку сложнее, ибо нам при выходе за значение 999 надо добавить плюс 1 Мгц.
Было:

    def set_fr_khz(self, fr_khz):
        self.fr_khz = fr_khz
        self.xmlrpc_client_1.set_fr_khz(self.fr_khz)
        self._fr_khz_init_config = ConfigParser.ConfigParser()
        self._fr_khz_init_config.read("pcrcontrol.conf")
        if not self._fr_khz_init_config.has_section("main"):
                self._fr_khz_init_config.add_section("main")
        self._fr_khz_init_config.set("main", "fr_khz", str(self.fr_khz))
        self._fr_khz_init_config.write(open("pcrcontrol.conf", 'w'))
        self._fr_khz_slider.set_value(self.fr_khz)
        self._fr_khz_text_box.set_value(self.fr_khz)

Стало:

 def set_fr_khz(self, fr_khz):
        if fr_khz > 999:
		fr_khz =0
		self.set_fr_mhz(self.get_fr_mhz() + 1)
        elif fr_khz < 0:
		fr_khz = 999
		self.set_fr_mhz(self.get_fr_mhz() - 1)
        self.fr_khz = fr_khz
        self.xmlrpc_client_1.set_fr_khz(self.fr_khz)
        self._fr_khz_init_config = ConfigParser.ConfigParser()
        self._fr_khz_init_config.read("pcrcontrol.conf")
        if not self._fr_khz_init_config.has_section("main"):
                self._fr_khz_init_config.add_section("main")
        self._fr_khz_init_config.set("main", "fr_khz", str(self.fr_khz))
        self._fr_khz_init_config.write(open("pcrcontrol.conf", 'w'))
        self._fr_khz_slider.set_value(self.fr_khz)
        self._fr_khz_text_box.set_value(self.fr_khz)

С кнопками и границами закончили. Теперь делаем так, чтобы при нажатии кнопки включения передавались все настройки из клиента в приемник. Это реализуется тоже легко. Отыщем функцию set_power(), и в ней укажем передать через XMLRPC клиенты все текущие значения параметров.

    def set_power(self, power):
        self.power = power
        self.xmlrpc_client_5.set_power(self.power)
        self._power_chooser.set_value(self.power)

Берем, и втыкаем передачу значений.

    def set_power(self, power):
        self.power = power
        self.xmlrpc_client_5.set_power(self.power)
        self.xmlrpc_client_0.set_fr_mhz(self.get_fr_mhz())
        self.xmlrpc_client_1.set_fr_khz(self.get_fr_khz())
        self.xmlrpc_client_2.set_fr_modulation(self.get_fr_modulation())
        self.xmlrpc_client_3.set_fr_filter(self.get_fr_filter())
        self.xmlrpc_client_4.set_volume(self.get_volume())
        self.xmlrpc_client_6.set_squelch(self.get_squelch())
        self.xmlrpc_client_7.set_ifshift(self.get_ifshift())
        self.xmlrpc_client_8.set_agc(self.get_agc())
        self.xmlrpc_client_9.set_nb(self.get_nb())
        self.xmlrpc_client_10.set_at(self.get_at())
        self._power_chooser.set_value(self.power)

Вот впринципе и все. Все должно работать. Теоретически. Но вот в процессе написания этого поста я наткнулся на неприятный глюк, который я не наблюдал раньше. При нажатии на кнопку «Power» радио включалось, устанавливалсь частота, но вот уровень громкости, шумодава, и прочих вещей не передавались. Причем если нажимать все эти кнопки по отдельности — все отлично отрабатывало, громкость менялась, модуляция менялась. Все работало. Но вот по нажатию кнопки все разом не инициализировалось. Хотя раньше — все было ОК.
Постучавишь пару недель головой о стену корень проблемы был найден. XMLRPC сервер не хотел получать данные так быстро. Пришлось в кнопке реализовать «тормоз» поставив задержку в 1 секунду. В секцию импорта вписываем import time

from gnuradio import eng_notation
from gnuradio import gr
from gnuradio.eng_option import eng_option
from gnuradio.filter import firdes
from gnuradio.wxgui import forms
from grc_gnuradio import wxgui as grc_wxgui
from optparse import OptionParser
import ConfigParser
import wx
import xmlrpclib
import time

А посредь вызовов XMLRPC в коде кнопки питания вписываем sleep в 1 секунду.

        self.xmlrpc_client_1.set_fr_khz(self.get_fr_khz())
        self.xmlrpc_client_2.set_fr_modulation(self.get_fr_modulation())
        self.xmlrpc_client_3.set_fr_filter(self.get_fr_filter())
        time.sleep(1)
        self.xmlrpc_client_4.set_volume(self.get_volume())
        self.xmlrpc_client_6.set_squelch(self.get_squelch())
        self.xmlrpc_client_7.set_ifshift(self.get_ifshift())

Вот теперь все! Можете сохранять файл, запускать его, и наслаждаться управлением приемником из GnuRadio. 🙂

Python сервер для управления ICOM PCR-1000
GRC граф для клиента
Изначальный скрипт клиента
Модифицированный клиент

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

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