diff --git a/grc/CMakeLists.txt b/grc/CMakeLists.txt index 6fb7c98..c635b93 100644 --- a/grc/CMakeLists.txt +++ b/grc/CMakeLists.txt @@ -16,10 +16,10 @@ # along with GNU Radio; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, # Boston, MA 02110-1301, USA. - install(FILES crfa_multi_rds_printer.xml crfa_qtguitest.xml crfa_rds_table_qt.xml - crfa_rds_parser_table_qt.xml DESTINATION share/gnuradio/grc/blocks + crfa_rds_parser_table_qt.xml + crfa_rds_decoder.xml DESTINATION share/gnuradio/grc/blocks ) diff --git a/grc/crfa_rds_decoder.xml b/grc/crfa_rds_decoder.xml new file mode 100644 index 0000000..9bb12c9 --- /dev/null +++ b/grc/crfa_rds_decoder.xml @@ -0,0 +1,45 @@ + + + RDS Decoder (cr) + crfa_rds_decoder + [crfa] + import crfa + crfa.rds_decoder($log, $debug) + + Log + log + False + bool + + + + + Debug + debug + False + bool + + + + + in + byte + + + out + message + 1 + + diff --git a/grc/rds_decoder.xml b/grc/rds_decoder.xml new file mode 100644 index 0000000..3d6384b --- /dev/null +++ b/grc/rds_decoder.xml @@ -0,0 +1,45 @@ + + + RDS Decoder (cr) + gr_rds_decoder_cr + [RDS] + import rds + rds.decoder($log, $debug) + + Log + log + False + bool + + + + + Debug + debug + False + bool + + + + + in + byte + + + out + message + 1 + + diff --git a/include/crfa/CMakeLists.txt b/include/crfa/CMakeLists.txt index 3261a5f..5743aa6 100644 --- a/include/crfa/CMakeLists.txt +++ b/include/crfa/CMakeLists.txt @@ -22,5 +22,5 @@ ######################################################################## install(FILES api.h - DESTINATION include/crfa + rds_decoder.h DESTINATION include/crfa ) diff --git a/include/crfa/rds_decoder.h b/include/crfa/rds_decoder.h new file mode 100644 index 0000000..7557e65 --- /dev/null +++ b/include/crfa/rds_decoder.h @@ -0,0 +1,56 @@ +/* -*- c++ -*- */ +/* + * Copyright 2016 <+YOU OR YOUR COMPANY+>. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3, or (at your option) + * any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + + +#ifndef INCLUDED_CRFA_RDS_DECODER_H +#define INCLUDED_CRFA_RDS_DECODER_H + +#include +#include + +namespace gr { + namespace crfa { + + /*! + * \brief <+description of block+> + * \ingroup crfa + * + */ + class CRFA_API rds_decoder : virtual public gr::sync_block + { + public: + typedef boost::shared_ptr sptr; + + /*! + * \brief Return a shared_ptr to a new instance of crfa::rds_decoder. + * + * To avoid accidental use of raw pointers, crfa::rds_decoder's + * constructor is in a private implementation + * class. crfa::rds_decoder::make is the public interface for + * creating new instances. + */ + static sptr make(bool log, bool debug); + }; + + } // namespace crfa +} // namespace gr + +#endif /* INCLUDED_CRFA_RDS_DECODER_H */ + diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 21eb294..385b6c2 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -24,9 +24,8 @@ include(GrPlatform) #define LIB_SUFFIX include_directories(${Boost_INCLUDE_DIR}) link_directories(${Boost_LIBRARY_DIRS}) - list(APPEND crfa_sources -) + rds_decoder_impl.cc ) set(crfa_sources "${crfa_sources}" PARENT_SCOPE) if(NOT crfa_sources) diff --git a/lib/constants.h b/lib/constants.h new file mode 100644 index 0000000..cb1f85c --- /dev/null +++ b/lib/constants.h @@ -0,0 +1,201 @@ +/* + * Copyright 2004 Free Software Foundation, Inc. + * + * This file is part of GNU Radio + * + * GNU Radio is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * GNU Radio is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Radio; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + + +/* see page 59, Annex C, table C.1 in the standard + * offset word C' has been put at the end */ +static const unsigned int offset_pos[5]={0,1,2,3,2}; +static const unsigned int offset_word[5]={252,408,360,436,848}; +static const unsigned int syndrome[5]={383,14,303,663,748}; +static const char * const offset_name[]={"A","B","C","D","C'"}; + +/* Annex F of RBDS Standard Table F.1 (North America) and + * Table F.2 (Europe) */ +const std::string pty_table[32][2]={ + {"Undefined", "Undefined"}, + {"News", "News"}, + {"Current Affairs", "Information"}, + {"Information", "Sports"}, + {"Sport", "Talk"}, + {"Education", "Rock"}, + {"Drama", "Classic Rock"}, + {"Culture", "Adult Hits"}, + {"Science", "Soft Rock"}, + {"Varied", "Top 40"}, + {"Pop Music", "Country"}, + {"Rock Music", "Oldies"}, + {"Easy Listening", "Soft"}, + {"Light Classical", "Nostalgia"}, + {"Serious Classical", "Jazz"}, + {"Other Music", "Classical"}, + {"Weather", "Rhythm & Blues"}, + {"Finance", "Soft Rhythm & Blues"}, + {"Children’s Programmes", "Language"}, + {"Social Affairs", "Religious Music"}, + {"Religion", "Religious Talk"}, + {"Phone-In", "Personality"}, + {"Travel", "Public"}, + {"Leisure", "College"}, + {"Jazz Music", "Spanish Talk"}, + {"Country Music", "Spanish Music"}, + {"National Music", "Hip Hop"}, + {"Oldies Music", "Unassigned"}, + {"Folk Music", "Unassigned"}, + {"Documentary", "Weather"}, + {"Alarm Test", "Emergency Test"}, + {"Alarm", "Emergency"}}; + +/* page 71, Annex D, table D.1 in the standard */ +const std::string pi_country_codes[15][5]={ + {"DE","GR","MA","__","MD"}, + {"DZ","CY","CZ","IE","EE"}, + {"AD","SM","PL","TR","__"}, + {"IL","CH","VA","MK","__"}, + {"IT","JO","SK","__","__"}, + {"BE","FI","SY","__","UA"}, + {"RU","LU","TN","__","__"}, + {"PS","BG","__","NL","PT"}, + {"AL","DK","LI","LV","SI"}, + {"AT","GI","IS","LB","__"}, + {"HU","IQ","MC","__","__"}, + {"MT","GB","LT","HR","__"}, + {"DE","LY","YU","__","__"}, + {"__","RO","ES","SE","__"}, + {"EG","FR","NO","BY","BA"}}; + +/* page 72, Annex D, table D.2 in the standard */ +const std::string coverage_area_codes[16]={ + "Local", + "International", + "National", + "Supra-regional", + "Regional 1", + "Regional 2", + "Regional 3", + "Regional 4", + "Regional 5", + "Regional 6", + "Regional 7", + "Regional 8", + "Regional 9", + "Regional 10", + "Regional 11", + "Regional 12"}; + +const std::string rds_group_acronyms[16]={ + "BASIC", + "PIN/SL", + "RT", + "AID", + "CT", + "TDC", + "IH", + "RP", + "TMC", + "EWS", + "___", + "___", + "___", + "___", + "EON", + "___"}; + +/* page 74, Annex E, table E.1 in the standard: that's the ASCII table!!! */ + +/* see page 84, Annex J in the standard */ +const std::string language_codes[44]={ + "Unkown/not applicable", + "Albanian", + "Breton", + "Catalan", + "Croatian", + "Welsh", + "Czech", + "Danish", + "German", + "English", + "Spanish", + "Esperanto", + "Estonian", + "Basque", + "Faroese", + "French", + "Frisian", + "Irish", + "Gaelic", + "Galician", + "Icelandic", + "Italian", + "Lappish", + "Latin", + "Latvian", + "Luxembourgian", + "Lithuanian", + "Hungarian", + "Maltese", + "Dutch", + "Norwegian", + "Occitan", + "Polish", + "Portuguese", + "Romanian", + "Romansh", + "Serbian", + "Slovak", + "Slovene", + "Finnish", + "Swedish", + "Turkish", + "Flemish", + "Walloon"}; + +/* see page 12 in ISO 14819-1 */ +const std::string tmc_duration[8][2]={ + {"no duration given", "no duration given"}, + {"15 minutes", "next few hours"}, + {"30 minutes", "rest of the day"}, + {"1 hour", "until tomorrow evening"}, + {"2 hours", "rest of the week"}, + {"3 hours", "end of next week"}, + {"4 hours", "end of the month"}, + {"rest of the day", "long period"}}; + +/* optional message content, data field lengths and labels + * see page 15 in ISO 14819-1 */ +const int optional_content_lengths[16]={3,3,5,5,5,8,8,8,8,11,16,16,16,16,0,0}; + +const std::string label_descriptions[16]={ + "Duration", + "Control code", + "Length of route affected", + "Speed limit advice", + "Quantifier", + "Quantifier", + "Supplementary information code", + "Explicit start time", + "Explicit stop time", + "Additional event", + "Detailed diversion instructions", + "Destination", + "RFU (Reserved for future use)", + "Cross linkage to source of problem, or another route", + "Separator", + "RFU (Reserved for future use)"}; diff --git a/lib/rds_decoder_impl.cc b/lib/rds_decoder_impl.cc new file mode 100644 index 0000000..7bacaa5 --- /dev/null +++ b/lib/rds_decoder_impl.cc @@ -0,0 +1,256 @@ +/* -*- c++ -*- */ +/* + * Copyright 2016 <+YOU OR YOUR COMPANY+>. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3, or (at your option) + * any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define dout debug && std::cout +#define lout log && std::cout + +#include +#include "constants.h" +#include "rds_decoder_impl.h" + +namespace gr { + namespace crfa { + + rds_decoder::sptr + rds_decoder::make(bool log, bool debug) + { + return gnuradio::get_initial_sptr + (new rds_decoder_impl(log, debug)); + } + + /* + * The private constructor + */ + rds_decoder_impl::rds_decoder_impl(bool log, bool debug) + : gr::sync_block("rds_decoder", + gr::io_signature::make (1, 1, sizeof(char)), + gr::io_signature::make (0, 0, 0)), + log(log), + debug(debug) +{ + set_output_multiple(104); // 1 RDS datagroup = 104 bits + message_port_register_out(pmt::mp("out")); + enter_no_sync(); +} + + /* + * Our virtual destructor. + */ + rds_decoder_impl::~rds_decoder_impl() + { + } + +////////////////////////// HELPER FUNTIONS ///////////////////////// + +void rds_decoder_impl::enter_no_sync() { + presync = false; + d_state = NO_SYNC; +} + +void rds_decoder_impl::enter_sync(unsigned int sync_block_number) { + last_wrong_blocks_counter = 0; + wrong_blocks_counter = 0; + blocks_counter = 0; + block_bit_counter = 0; + block_number = (sync_block_number + 1) % 4; + group_assembly_started = false; + d_state = SYNC; +} + +/* see Annex B, page 64 of the standard */ +unsigned int rds_decoder_impl::calc_syndrome(unsigned long message, + unsigned char mlen) { + unsigned long reg = 0; + unsigned int i; + const unsigned long poly = 0x5B9; + const unsigned char plen = 10; + + for (i = mlen; i > 0; i--) { + reg = (reg << 1) | ((message >> (i-1)) & 0x01); + if (reg & (1 << plen)) reg = reg ^ poly; + } + for (i = plen; i > 0; i--) { + reg = reg << 1; + if (reg & (1<> 8U) & 0xffU; + bytes[1] = (group[0] ) & 0xffU; + bytes[2] = (group[1] >> 8U) & 0xffU; + bytes[3] = (group[1] ) & 0xffU; + bytes[4] = (group[2] >> 8U) & 0xffU; + bytes[5] = (group[2] ) & 0xffU; + bytes[6] = (group[3] >> 8U) & 0xffU; + bytes[7] = (group[3] ) & 0xffU; + + // RDS offset words + bytes[8] = offset_chars[0]; + bytes[9] = offset_chars[1]; + bytes[10] = offset_chars[2]; + bytes[11] = offset_chars[3]; + bytes[12]=last_wrong_blocks_counter; + pmt::pmt_t data(pmt::make_blob(bytes, 13)); + pmt::pmt_t meta(pmt::PMT_NIL); + + pmt::pmt_t pdu(pmt::cons(meta, data)); // make PDU: (metadata, data) pair + message_port_pub(pmt::mp("out"), pdu); +} +//work function +int rds_decoder_impl::work (int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items) +{ + const bool *in = (const bool *) input_items[0]; + + dout << "RDS data decoder at work: input_items = " + << noutput_items << ", /104 = " + << noutput_items / 104 << std::endl; + + int i=0,j; + unsigned long bit_distance, block_distance; + unsigned int block_calculated_crc, block_received_crc, checkword,dataword; + unsigned int reg_syndrome; + unsigned char offset_char('x'); // x = error while decoding the word offset + +/* the synchronization process is described in Annex C, page 66 of the standard */ + while (i=offset_pos[j]) + block_distance=offset_pos[j]+4-offset_pos[lastseen_offset]; + else + block_distance=offset_pos[j]-offset_pos[lastseen_offset]; + if ((block_distance*26)!=bit_distance) presync=false; + else { + lout << "@@@@@ Sync State Detected" << std::endl; + enter_sync(j); + } + } + break; //syndrome found, no more cycles + } + } + break; + case SYNC: +/* wait until 26 bits enter the buffer */ + if (block_bit_counter<25) block_bit_counter++; + else { + good_block=false; + dataword=(reg>>10) & 0xffff; + block_calculated_crc=calc_syndrome(dataword,16); + checkword=reg & 0x3ff; +/* manage special case of C or C' offset word */ + if (block_number==2) { + block_received_crc=checkword^offset_word[block_number]; + if (block_received_crc==block_calculated_crc) { + good_block=true; + offset_char = 'C'; + } else { + block_received_crc=checkword^offset_word[4]; + if (block_received_crc==block_calculated_crc) { + good_block=true; + offset_char = 'c'; // C' (C-Tag) + } else { + wrong_blocks_counter++; + good_block=false; + } + } + } + else { + block_received_crc=checkword^offset_word[block_number]; + if (block_received_crc==block_calculated_crc) { + good_block=true; + if (block_number==0) offset_char = 'A'; + else if (block_number==1) offset_char = 'B'; + else if (block_number==3) offset_char = 'D'; + } else { + wrong_blocks_counter++; + good_block=false; + } + } +/* done checking CRC */ + if (block_number==0 && good_block) { + group_assembly_started=true; + group_good_blocks_counter=1; + } + if (group_assembly_started) { + if (!good_block) group_assembly_started=false; + else { + group[block_number]=dataword; + offset_chars[block_number] = offset_char; + group_good_blocks_counter++; + } + if (group_good_blocks_counter==5) decode_group(group); + } + block_bit_counter=0; + block_number=(block_number+1) % 4; + blocks_counter++; +/* 1187.5 bps / 104 bits = 11.4 groups/sec, or 45.7 blocks/sec */ + if (blocks_counter==50) { + last_wrong_blocks_counter=wrong_blocks_counter; + if (wrong_blocks_counter>35) { + lout << "@@@@@ Lost Sync (Got " << wrong_blocks_counter + << " bad blocks on " << blocks_counter + << " total)" << std::endl; + enter_no_sync(); + } else { + lout << "@@@@@ Still Sync-ed (Got " << wrong_blocks_counter + << " bad blocks on " << blocks_counter + << " total)" << std::endl; + } + blocks_counter=0; + wrong_blocks_counter=0; + } + } + break; + default: + d_state=NO_SYNC; + break; + } + i++; + bit_counter++; + } + return noutput_items; + }/*work function*/ + } /* namespace crfa */ +} /* namespace gr */ + diff --git a/lib/rds_decoder_impl.h b/lib/rds_decoder_impl.h new file mode 100644 index 0000000..2fa36a3 --- /dev/null +++ b/lib/rds_decoder_impl.h @@ -0,0 +1,70 @@ +/* -*- c++ -*- */ +/* + * Copyright 2016 <+YOU OR YOUR COMPANY+>. + * + * This is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3, or (at your option) + * any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef INCLUDED_CRFA_RDS_DECODER_IMPL_H +#define INCLUDED_CRFA_RDS_DECODER_IMPL_H + +#include + +namespace gr { + namespace crfa { + + class rds_decoder_impl : public rds_decoder + { +public: + rds_decoder_impl(bool log, bool debug); + +private: + ~rds_decoder_impl(); + + // Where all the action really happens + int work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items); + void enter_no_sync(); + void enter_sync(unsigned int); + unsigned int calc_syndrome(unsigned long, unsigned char); + void decode_group(unsigned int*); + + unsigned long bit_counter; + unsigned long lastseen_offset_counter, reg; + unsigned int block_bit_counter; + unsigned int wrong_blocks_counter; + unsigned int blocks_counter; + unsigned int group_good_blocks_counter; + unsigned int group[4]; + unsigned char offset_chars[4]; // [ABCcDEx] (x=error) + bool debug; + bool log; + bool presync; + bool good_block; + bool group_assembly_started; + unsigned char last_wrong_blocks_counter; + unsigned char lastseen_offset; + unsigned char block_number; + enum { NO_SYNC, SYNC } d_state; + + }; + + } // namespace crfa +} // namespace gr + +#endif /* INCLUDED_CRFA_RDS_DECODER_IMPL_H */ + diff --git a/python/piechart.py b/python/piechart.py new file mode 100644 index 0000000..fbece54 --- /dev/null +++ b/python/piechart.py @@ -0,0 +1,38 @@ +from PyQt4.QtGui import QGraphicsScene, QApplication, QGraphicsView, QGraphicsEllipseItem +from PyQt4 import Qt, QtCore, QtGui +from PyQt4.Qt import QColor +import sys, random,code + + +app = QApplication(sys.argv) +scene = QGraphicsScene() + +families = [1,2,3,4,5,6,7,8,9,10] +total = 0 +set_angle = 0 +count1 = 0 +colours = [] +total = sum(families) +size=300 +for count in range(len(families)): + number = [] + for count in range(3): + number.append(random.randrange(0, 255)) + colours.append(QColor(number[0],number[1],number[2])) + +for family in families: + # Max span is 5760, so we have to calculate corresponding span angle + angle = round(float(family*5760)/total) + ellipse = QGraphicsEllipseItem(0,0,size,size) + ellipse.setPos(0,0) + ellipse.setStartAngle(set_angle) + ellipse.setSpanAngle(angle) + ellipse.setBrush(colours[count1]) + set_angle += angle + count1 += 1 + scene.addItem(ellipse) + +view = QGraphicsView(scene) +view.show() +view.setFixedSize(size+10,size+10) +app.exec_(code.interact(local=locals())) diff --git a/python/python/rds_parser_table_qt.py b/python/python/rds_parser_table_qt.py deleted file mode 100644 index b5e0866..0000000 --- a/python/python/rds_parser_table_qt.py +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2016 <+YOU OR YOUR COMPANY+>. -# -# This is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3, or (at your option) -# any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this software; see the file COPYING. If not, write to -# the Free Software Foundation, Inc., 51 Franklin Street, -# Boston, MA 02110-1301, USA. -# - -import numpy -from gnuradio import gr -import code,pmt,functools -from PyQt4 import Qt, QtCore, QtGui -import pprint -pp = pprint.PrettyPrinter() - -from PyQt4.QtCore import QObject, pyqtSignal - -class rds_parser_table_qt_Signals(QObject): - DataUpdateEvent = QtCore.pyqtSignal(dict) - def __init__(self, parent=None): - super(QtCore.QObject, self).__init__() - -class rds_parser_table_qt(gr.sync_block): - """ - docstring for block qtguitest - """ - def __init__(self,signals,nPorts): - #QObject.__init__() - gr.sync_block.__init__(self, - name="RDS Table", - in_sig=None, - out_sig=None) - for i in range(0,nPorts): - self.message_port_register_in(pmt.intern('in%d'%i)) - self.set_msg_handler(pmt.intern('in%d'%i), functools.partial(self.handle_msg, port=i)) - - self.signals=signals - self.RTdict={} - self.RTvalid={} - self.PSNdict={} - self.PSNvalid={} - self.AFdata={} - self.blockcounts={} - self.printcounter=0 - def handle_msg(self, msg, port): - #code.interact(local=locals()) - array=pmt.to_python(msg)[1] - groupNR=array[2]&0b11110000 - groupVar=array[2]&0b00001000 - if (groupVar == 0): - groupType=str(groupNR >> 4)+"A" - else: - groupType=str(groupNR >> 4)+"B" - #print("raw:"+str(pmt.to_python(msg))+"\n") - PI="%02X%02X" %(array[0],array[1]) - #print("1st block:"+str(array[0])+","+str(array[1])+"= ID: %s" %PI) - #print("2st block:"+str(array[2])+","+str(array[3])+"= type:"+groupType) - #print("3st block:"+str(array[4])+","+str(array[5])) - #print("4st block:"+str(array[6])+","+str(array[7])) - if (groupType == "0A"):#AF PSN - adr=array[3]&0b00000011 - segment=self.decode_chars(chr(array[6])+chr(array[7])) - if(not self.PSNdict.has_key(PI)):#initialize dict - self.PSNdict[PI]="_"*8 - self.PSNvalid[PI]=[False]*8 - self.AFdata[PI]={} - #1110 0000 = no AF - #1110 0001 = 1AF - #1111 1001 = 25AF - - if(array[5]>= 224 and array[5]<= 249): - print("AF1 detected") - self.AFdata[PI]['number']=array[5]-224 - self.signals.DataUpdateEvent.emit({'row':port,'AF':self.AFdata[PI]}) - if(array[6]>= 224 and array[6]<= 249): - print("AF2 detected") - - name_list=list(self.PSNdict[PI]) - if (name_list[adr*2:adr*2+2]==list(segment)):#segment already there - segmentcolor="green" - elif(name_list[adr*2:adr*2+2]==['_']*2): #segment new - segmentcolor="orange" - name_list[adr*2:adr*2+2]=segment - else:#name changed (böse) - segmentcolor="red" - name_list=['_']*8 #reset name - name_list[adr*2:adr*2+2]=segment - #reset stored text: - self.PSNdict[PI]="_"*8 - self.PSNvalid[PI]=[False]*8 - self.PSNvalid[PI][adr*2:adr*2+2]=[True] *2 - self.PSNdict[PI]="".join(name_list) - #determine if text is valid - valid=True - for i in range(0,8): - if (not self.PSNvalid[PI][i]): - valid = False - if(valid): - textcolor="black" - else: - textcolor="gray" - formatted_text=self.color_text(self.PSNdict[PI],adr*2,adr*2+2,textcolor,segmentcolor) - self.signals.DataUpdateEvent.emit({'col':5,'row':port,'PI':PI,'PSN':formatted_text}) - elif (groupType == "2A"):#RT radiotext - if(not self.RTdict.has_key(PI)):#initialize dict - self.RTdict[PI]="_"*64 - self.RTvalid[PI]=[False]*64 - else: - adr=array[3]&0b00001111 - segment=self.decode_chars(chr(array[4])+chr(array[5])+chr(array[6])+chr(array[7])) - #print("RT:adress: %d, segment:%s"%(adr,segment)) - #self.signals.DataUpdateEvent.emit({'col':5,'row':port,'PI':PI,'groupType':groupType,'adress':adr,'segment':segment}) - text_list=list(self.RTdict[PI]) - #determine text length: - try: - text_end=text_list.index('\r') - except ValueError: - text_end=64 #assume whole string is important - pass - - if (text_list[adr*4:adr*4+4]==list(segment)):#segment already there - segmentcolor="green" - elif (text_list[adr*4:adr*4+4]==['_']*4):#segment new - segmentcolor="orange" - text_list[adr*4:adr*4+4]=segment - else: - segmentcolor="red" - text_list=['_']*64 #clear text - text_list[adr*4:adr*4+4]=segment - #reset stored text: - self.RTdict[PI]="_"*64 - self.RTvalid[PI]=[False]*64 - - self.RTvalid[PI][adr*4:adr*4+4]=[True] *4 - self.RTdict[PI]="".join(text_list) - - #determine if (new) text is valid - valid=True - for i in range(0,text_end): - if (not self.RTvalid[PI][i]): - valid = False - if(valid): - textcolor="black" - else: - textcolor="gray" - #formatted_text="%s%s%s"% (textcolor,self.RTdict[PI][:adr*4],segmentcolor,self.RTdict[PI][adr*4:adr*4+4],textcolor,self.RTdict[PI][adr*4+4:]) - formatted_text=self.color_text(self.RTdict[PI],adr*4,adr*4+4,textcolor,segmentcolor) - #print(self.RTdict[PI]+" valid:"+str(valid)+"valarr:"+str(self.RTvalid[PI])) - - - self.signals.DataUpdateEvent.emit({'col':5,'row':port,'PI':PI,'string':formatted_text}) - #code.interact(local=locals()) - elif (groupType == "4A"):#CT clock time - datecode=((array[3] & 0x03) << 15) | (array[4] <<7)|((array[5] >> 1) & 0x7f) - hours=((array[5] & 0x1) << 4) | ((array[6] >> 4) & 0x0f) - minutes=((array[6] &0x0F)<<2)|((array[7] >>6)&0x3) - offsetdir=(array[7]>>5)&0x1 - local_time_offset=0.5*((array[7])&0x1F) - if(offsetdir==1): - local_time_offset*=-1 - year=int((datecode - 15078.2) / 365.25) - month=int((datecode - 14956.1 - int(year * 365.25)) / 30.6001) - day=datecode - 14956 - int(year * 365.25) - int(month * 30.6001) - if(month == 14 or month == 15):#no idea why -> annex g of RDS spec - year += 1; - month -= 13 - year+=1900 - datestring="%02i.%02i.%4i, %02i:%02i (%+.1fh)" % (day,month,year,hours,minutes,local_time_offset) - self.signals.DataUpdateEvent.emit({'col':4,'row':port,'PI':PI,'string':datestring}) - else:#other group - printfreq=100 - self.printcounter+=1 - if self.blockcounts.has_key(PI):#1st group on this station - if self.blockcounts[PI].has_key(groupType):#1st group of this type - self.blockcounts[PI][groupType] +=1 #increment - else: - self.blockcounts[PI][groupType] = 1 #initialize - else: - self.blockcounts[PI]={}#initialize dict - if self.printcounter == printfreq: - pp.pprint(self.blockcounts) - self.printcounter=0 - #print("group of type %s not decoded on station %s"% (groupType,PI)) - def decode_chars(self,charstring): - alphabet={ - 0b1000:u"áàéèíìóòúùÑÇŞßiIJ", - 0b1001:u"âäêëîïôöûüñçş??ij", - 0b1100:u"ÁÀÉÈÍÌÓÒÚÙŘČŠŽĐĿ", - 0b1101:u"áàéèíìóòúùřčšžđŀ"} - charlist=list(charstring) - for i,char in enumerate(charstring): - #code.interact(local=locals()) - if ord(char)<= 0b01111111: - charlist[i]=char #use ascii - else: - #split byte - alnr=(ord(char)&0xF0 )>>4 #upper 4 bit - index=ord(char)&0x0F #lower 4 bit - #code.interact(local=locals()) - try: - charlist[i]=alphabet[alnr][index] - except KeyError: - charlist[i]=char - pass - return "".join(charlist) - def color_text(self, text, start,end,textcolor,segmentcolor): - formatted_text="%s%s%s"% (textcolor,text[:start],segmentcolor,text[start:end],textcolor,text[end:]) - return formatted_text -class rds_parser_table_qt_Widget(QtGui.QWidget): - def __init__(self, signals,label): - print("gui initializing") - self.signals = signals - self.signals.DataUpdateEvent.connect(self.display_data) - """ Creates the QT Range widget """ - QtGui.QWidget.__init__(self) - layout = Qt.QVBoxLayout() - self.label = Qt.QLabel(label) - layout.addWidget(self.label) - self.setLayout(layout) - self.table=QtGui.QTableWidget(self) - self.table.setRowCount(5) - self.table.setColumnCount(7) - self.table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) #disallow editing - #Data - empty_text32='________________________________' - empty_text64='________________________________________________________________' - #empty_text64='\xe4'*64 - self.data = {'ID':range(1,6), - 'freq':['','','',''], - 'name':[], - 'AF':['','','',''], - 'time':[], - 'text':[], - 'buttons':[]} - #Enter data onto Table - horHeaders = [] - for n, key in enumerate(['ID','freq','name','AF','time','text','buttons']): - #for n, key in enumerate(sorted(self.data.keys())): - horHeaders.append(key) - for m, item in enumerate(self.data[key]): - if type(item)==int:#convert ints to strings - newitem = QtGui.QTableWidgetItem(str(item)) - else: - newitem = QtGui.QTableWidgetItem(item) - self.table.setItem(m, n, newitem) - for i in range(0,4):#create buttons - button=QtGui.QPushButton("play") - self.table.setCellWidget(i,self.table.columnCount()-1,button) - button.clicked.connect(self.onCLick) - for i in range(0,4):#create text labels - label=QtGui.QLabel(empty_text64) - #label.setFont(QtGui.QFont("Courier New")) - self.table.setCellWidget(i,self.table.columnCount()-2,label) - for i in range(0,4):#create name labels - label=QtGui.QLabel("_"*8) - #label.setFont(QtGui.QFont("Courier New")) - self.table.setCellWidget(i,2,label) - for i in range(0,4):#create time labels - label=QtGui.QLabel() - #label.setFont(QtGui.QFont("Courier New")) - self.table.setCellWidget(i,4,label) - #Add Header - self.table.setHorizontalHeaderLabels(horHeaders) - layout.addWidget(self.label) - layout.addWidget(self.table) - self.button = QtGui.QPushButton("i am a button") - layout.addWidget(self.button) - - def display_data(self, event): - #pp.pprint(event) - if type(event)==dict and event.has_key('row'): - if event.has_key('string'): - item=self.table.cellWidget(event['row'],event['col']) - item.setText(event['string']) - if event.has_key('PI'): - #setPI - PIcol=0 - self.table.item(event['row'],PIcol).setText(event['PI']) - if event.has_key('AF'): - #setAF - PIcol=3 - self.table.item(event['row'],PIcol).setText(event['AF']['number']) - if event.has_key('PSN'): - #setPSN - PSNcol=2 - item=self.table.cellWidget(event['row'],PSNcol) - item.setText(event['PSN']) - self.table.resizeColumnsToContents() - #def reset_color(self): - #for i in range(0,self.table.rowCount()): - #for j in range(0,self.table.columnCount()): - #item = self.table.item(i,j) - ##code.interact(local=locals()) - ##print(item.type()) - #if item != '': - #try: - #item.setTextColor(QtCore.Qt.black) - #except: - #pass - def onCLick(self): - print("button clicked") - #self.reset_color() - #pp.pprint(event) -if __name__ == "__main__": - from PyQt4 import Qt - import sys - - # def valueChanged(frequency): - # print("Value updated - " + str(frequency)) - - app = Qt.QApplication(sys.argv) - # widget = RangeWidget(Range(0, 100, 10, 1, 100), valueChanged, "Test", "counter_slider", int) - mainobj= rds_parser_table_qt_Signals() - #mainobj=None - widget = rds_parser_table_qt_Widget(mainobj,"TestLabel") - widget.show() - widget.setWindowTitle("Test Qt gui") - widget.setGeometry(200,200,600,300) - #code.interact(local=locals()) - sys.exit(app.exec_()) - - widget = None diff --git a/python/rds_parser_table_qt.py b/python/rds_parser_table_qt.py index 6fa7bba..e965b6d 100644 --- a/python/rds_parser_table_qt.py +++ b/python/rds_parser_table_qt.py @@ -52,6 +52,8 @@ class rds_parser_table_qt(gr.sync_block): self.printcounter=0 self.ODA_application_names={} self.TMC_data={} + self.colorder=['ID','freq','name','PTY','AF','time','text','quality','buttons'] + #workdir="/user/wire2/richter/hackrf_prototypes/" workdir="/media/clemens/intdaten/uni_bulk/forschungsarbeit/hackrf_prototypes/" reader = csv.reader(open(workdir+'RDS_ODA AIDs_names_only.csv'), delimiter=',', quotechar='"') reader.next()#skip header @@ -69,6 +71,12 @@ class rds_parser_table_qt(gr.sync_block): reader = csv.reader(open(workdir+'event-list_code+de-name_sort.csv'), delimiter=',', quotechar='"') #no header self.ecl_dict=dict((int(rows[0]),rows[1]) for rows in reader) + #read PTY list + f=open(workdir+'pty-list.csv') + reader = csv.reader(f, delimiter=',', quotechar='"') + reader.next()#skip header + self.pty_dict=dict((int(rows[0]),rows[1]) for rows in reader) + f.close() def handle_msg(self, msg, port): #code.interact(local=locals()) array=pmt.to_python(msg)[1] @@ -79,20 +87,40 @@ class rds_parser_table_qt(gr.sync_block): else: groupType=str(groupNR >> 4)+"B" PI="%02X%02X" %(array[0],array[1]) + TP=(array[2]>>2)&0x1 + block2=(array[2]<<8)|(array[3]) #block2 + PTY=(block2>>5)&0x1F + wrong_blocks=int(array[12]) + #initialize dict 1st packet from station: if not self.RDS_data.has_key(PI): self.RDS_data[PI]={} self.RDS_data[PI]["blockcounts"]={} + self.RDS_data[PI]["blockcounts"]["any"]=0 self.RDS_data[PI]["AID_list"]={} self.RDS_data[PI]["PSN"]="_"*8 self.RDS_data[PI]["PSN_valid"]=[False]*8 self.RDS_data[PI]["AF"]={} + self.RDS_data[PI]["DI"]=[2,2,2,2] print("found station %s"%PI) - + self.RDS_data[PI]["blockcounts"]["any"]+=1 + if self.RDS_data[PI]["blockcounts"]["any"]==5: + self.RDS_data[PI]["blockcounts"]["any"]=0 + dots="."*self.RDS_data[PI]["blockcounts"]["any"] + self.signals.DataUpdateEvent.emit({'row':port,'PI':PI,'PTY':self.pty_dict[PTY],'TP':TP,'wrong_blocks':wrong_blocks,'dots':dots}) if (groupType == "0A"):#AF PSN adr=array[3]&0b00000011 segment=self.decode_chars(chr(array[6])+chr(array[7])) - + d=(array[3]>>2)&0x1 + self.RDS_data[PI]["DI"][3-adr]=d + #DI[0]=d0 0=Mono 1=Stereo + #d1 Not artificial head Artificial head + #d2 Not compressed Compressed + #d3 Static PTY Dynamic PTY + TA=(array[3]>>4)&0x1 + MS=(array[3]>>3)&0x1 + flag_string="TP:%i, TA:%i, MS:%i, DI:%s"%(TP,TA,MS,str(self.RDS_data[PI]["DI"])) + self.signals.DataUpdateEvent.emit({'row':port,'PI':PI,'flags':flag_string}) #1110 0000 = no AF #1110 0001 = 1AF #1111 1001 = 25AF @@ -129,13 +157,14 @@ class rds_parser_table_qt(gr.sync_block): else: textcolor="gray" formatted_text=self.color_text(self.RDS_data[PI]["PSN"],adr*2,adr*2+2,textcolor,segmentcolor) - self.signals.DataUpdateEvent.emit({'col':5,'row':port,'PI':PI,'PSN':formatted_text}) + self.signals.DataUpdateEvent.emit({'row':port,'PI':PI,'PSN':formatted_text}) elif (groupType == "2A"):#RT radiotext if(not self.RDS_data[PI].has_key("RT")):#initialize variables self.RDS_data[PI]["RT"]="_"*64 self.RDS_data[PI]["RT_valid"]=[False]*64 + self.RDS_data[PI]["RT_all_valid"]=False else: adr=array[3]&0b00001111 segment=self.decode_chars(chr(array[4])+chr(array[5])+chr(array[6])+chr(array[7])) @@ -178,8 +207,8 @@ class rds_parser_table_qt(gr.sync_block): formatted_text=self.color_text(self.RDS_data[PI]["RT"],adr*4,adr*4+4,textcolor,segmentcolor) #print(self.RDS_data[PI]["RT"]+" valid:"+str(valid)+"valarr:"+str(self.RDS_data[PI]["RT_valid"])) - - self.signals.DataUpdateEvent.emit({'col':5,'row':port,'PI':PI,'string':formatted_text}) + rtcol=self.colorder.index('text') + self.signals.DataUpdateEvent.emit({'col':rtcol,'row':port,'PI':PI,'string':formatted_text}) #code.interact(local=locals()) elif (groupType == "3A"):#ODA announcements (contain application ID "AID") AID=(array[6]<<8)|(array[7])#combine 2 bytes into 1 block @@ -205,15 +234,20 @@ class rds_parser_table_qt(gr.sync_block): local_time_offset=0.5*((array[7])&0x1F) if(offsetdir==1): local_time_offset*=-1 - year=int((datecode - 15078.2) / 365.25) + year=int((datecode - 15078.2) / 365.25) month=int((datecode - 14956.1 - int(year * 365.25)) / 30.6001) day=datecode - 14956 - int(year * 365.25) - int(month * 30.6001) if(month == 14 or month == 15):#no idea why -> annex g of RDS spec year += 1; month -= 13 year+=1900 - datestring="%02i.%02i.%4i, %02i:%02i (%+.1fh)" % (day,month,year,hours,minutes,local_time_offset) - self.signals.DataUpdateEvent.emit({'col':4,'row':port,'PI':PI,'string':datestring}) + #month was off by one different rounding in c and python? + month-=1 + #datestring="%02i.%02i.%4i, %02i:%02i (%+.1fh)" % (day,month,year,hours,minutes,local_time_offset) + timestring="%02i:%02i (%+.1fh)" % (hours,minutes,local_time_offset) + datestring="%02i.%02i.%4i" % (day,month,year) + ctcol=self.colorder.index('time') + self.signals.DataUpdateEvent.emit({'col':ctcol,'row':port,'PI':PI,'string':timestring,'tooltip':datestring}) #TMC-alert-c (grouptype mostly 8A): elif self.RDS_data[PI]["AID_list"].has_key(52550) and self.RDS_data[PI]["AID_list"][52550]["groupType"]==groupType: tmc_x=array[3]&0x1f #lower 5 bit of block2 @@ -285,7 +319,7 @@ class rds_parser_table_qt(gr.sync_block): tag2_len=int(tag2&(2**5-1)) if not self.RDS_data[PI].has_key("RT+"): self.RDS_data[PI]["RT+"]={} - if(self.RDS_data[PI].has_key("RT") and self.RDS_data[PI]["RT_all_valid"]): + if(self.RDS_data[PI].has_key("RT") and self.RDS_data[PI]["RT_all_valid"]):#TODO better (more fine grained) detection of valid RT+ info rt=self.RDS_data[PI]["RT"] if not tag1_type=="DUMMY_CLASS": self.RDS_data[PI]["RT+"][tag1_type]=rt[tag1_start:tag1_start+tag1_len+1] @@ -297,7 +331,9 @@ class rds_parser_table_qt(gr.sync_block): artist=rt[tag1_start:tag1_start+tag1_len+1] song=rt[tag2_start:tag2_start+tag2_len+1] formatted_text="%s by %s"%(song,artist) - self.signals.DataUpdateEvent.emit({'col':6,'row':port,'PI':PI,'string':formatted_text}) + rtcol=self.colorder.index('text') + self.signals.DataUpdateEvent.emit({'col':rtcol,'row':port,'PI':PI,'tooltip':formatted_text}) + #self.signals.DataUpdateEvent.emit({'col':8,'row':port,'PI':PI,'string':formatted_text}) elif(not tag1_type=="ITEM.ARTIST" and not tag1_type=="DUMMY_CLASS"): print("%s:RT+: tag1_type:%s, tag2_type:%s"%(PI,tag1_type,tag2_type)) else:#other group @@ -373,7 +409,7 @@ class rds_parser_table_qt_Widget(QtGui.QWidget): self.setLayout(layout) self.table=QtGui.QTableWidget(self) self.table.setRowCount(5) - self.table.setColumnCount(8) + self.table.setColumnCount(9) self.table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) #disallow editing #Data empty_text32='________________________________' @@ -382,14 +418,18 @@ class rds_parser_table_qt_Widget(QtGui.QWidget): self.data = {'ID':range(1,6), 'freq':['','','',''], 'name':[ QtGui.QLabel() for i in range(4)], + 'PTY':[ QtGui.QLabel() for i in range(4)], + #'flags':[ QtGui.QLabel() for i in range(4)], 'AF':['','','',''], 'time':[ QtGui.QLabel() for i in range(4)], 'text':[ QtGui.QLabel("_"*64) for i in range(4)], - 'RT+':[ QtGui.QLabel() for i in range(4)], + #'RT+':[ QtGui.QLabel() for i in range(4)], + 'quality':[ QtGui.QLabel() for i in range(4)], 'buttons':[]} #Enter data onto Table + self.colorder=['ID','freq','name','PTY','AF','time','text','quality','buttons'] horHeaders = [] - for n, key in enumerate(['ID','freq','name','AF','time','text','RT+','buttons']): + for n, key in enumerate(self.colorder): #for n, key in enumerate(sorted(self.data.keys())): horHeaders.append(key) for m, item in enumerate(self.data[key]): @@ -407,31 +447,52 @@ class rds_parser_table_qt_Widget(QtGui.QWidget): button.clicked.connect(self.onCLick) #Add Header self.table.setHorizontalHeaderLabels(horHeaders) + self.tmc_message_label=QtGui.QLabel("TMC messages:") + self.event_filter=QtGui.QLineEdit()#QPlainTextEdit ? + self.location_filter=QtGui.QLineEdit() layout.addWidget(self.label) layout.addWidget(self.table) self.button = QtGui.QPushButton("i am a button") layout.addWidget(self.button) + layout.addWidget(self.tmc_message_label) + layout.addWidget(self.event_filter) + layout.addWidget(self.location_filter) def display_data(self, event): #pp.pprint(event) - if type(event)==dict and event.has_key('row'): + if type(event)==dict and event.has_key('row'): + if event.has_key('wrong_blocks'): + item=self.table.cellWidget(event['row'],self.colorder.index('quality')) + quality_string="%i%% %s"% (100-2*event['wrong_blocks'],event['dots']) + item.setText(quality_string) + if event.has_key('PTY'): + item=self.table.cellWidget(event['row'],self.colorder.index('PTY')) + item.setText(event['PTY']) + if event.has_key('flags'): + item=self.table.cellWidget(event['row'],self.colorder.index('PTY')) + item.setToolTip(Qt.QString(event['flags'])) if event.has_key('string'): item=self.table.cellWidget(event['row'],event['col']) item.setText(event['string']) + if event.has_key('tooltip'): + item=self.table.cellWidget(event['row'],event['col']) + item.setToolTip(Qt.QString(event['tooltip'])) if event.has_key('PI'): #setPI - PIcol=0 - rtpcol=6 + PIcol=self.colorder.index('ID') + #rtpcol=self.colorder.index('RT+') + rtcol=self.colorder.index('text') if not self.table.item(event['row'],PIcol).text() == event['PI']: - self.table.cellWidget(event['row'],rtpcol).setText("")#clear RT+ on changed PI + #self.table.cellWidget(event['row'],rtpcol).setText("")#clear RT+ on changed PI + self.table.cellWidget(event['row'],rtcol).setToolTip(Qt.QString("")) self.table.item(event['row'],PIcol).setText(event['PI']) if event.has_key('AF'): #setAF - PIcol=3 + PIcol=self.colorder.index('AF') self.table.item(event['row'],PIcol).setText(event['AF']['number']) if event.has_key('PSN'): #setPSN - PSNcol=2 + PSNcol=self.colorder.index('name') item=self.table.cellWidget(event['row'],PSNcol) item.setText(event['PSN']) self.table.resizeColumnsToContents() diff --git a/swig/crfa_swig.i b/swig/crfa_swig.i index b31d241..273d991 100644 --- a/swig/crfa_swig.i +++ b/swig/crfa_swig.i @@ -8,6 +8,8 @@ %include "crfa_swig_doc.i" %{ +#include "crfa/rds_decoder.h" %} - +%include "crfa/rds_decoder.h" +GR_SWIG_BLOCK_MAGIC2(crfa, rds_decoder);