Пару лет назад, я приобрел свой первый более-менее серьезный радоприемник: 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-машине.
Теперь, самое сложное — клиент. Сейчас вы поймете почему. 🙂
Начну с конца — с показа результата, того что у нас должно получиться.
Не сказать что дизайн блещет изысками, он был создан для моего личного использования. Итак, что мы тут видим: управление модуляцией, шириной фильтра, громкостью, шумодавом, и сдвигом промежуточной частоты. Кнопки для включения и выключения AGC, антишумового фильтра и аттенюатора.
Управление частотой немного урезано, отсутствует поле для гигагерц (ну не заглядываю я на столь высокие частоты), и для герц (тоже как-то не сильно требовалось точная подстройка). Частоту можно менять:
а) Полозунком
б) Кнопками «+» и «-«. При этом частота поменяется на выбранное в выпадающем меню слева кол-во мегагерц или килогерц.
в) Прямым вводом значения частоты в поля.
На вкладки «Auto» и «Programmable» можно не обращать внимания, это я хотел реализовать автоматический и программируемый проход по частотам, но лень-матушка помешала. А вкладки остались.
Теперь посмотрм как это выглядит в GnuRadio. Зажмурьтесь, это страшно 🙂
И это еще на картинке не все убралось. Для полноты ощущений рекомендую скачать граф, и открыть его в 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 граф для клиента
Изначальный скрипт клиента
Модифицированный клиент