From aea5479646620e7250d6d4c757e5c2e42c4aeaf7 Mon Sep 17 00:00:00 2001 From: Balint Seeber Date: Sat, 19 Sep 2015 17:31:41 -0700 Subject: [PATCH 001/102] Big enhancements: Decryption support (DES-OFB) Fixes to GRC files Optional logging to console Optional disabling of output silence during idle time More error correction Decoding of LSDW BCH error correction fix (after IT++ BCH breaking change) Fix for GR 3.7 build process --- op25/gr-op25/CMakeLists.txt | 6 +- op25/gr-op25/grc/op25_decoder_ff.xml | 12 +- op25/gr-op25/grc/op25_fsk4_demod_ff.xml | 13 +- op25/gr-op25/include/op25/decoder_bf.h | 13 +- op25/gr-op25/lib/CMakeLists.txt | 7 + op25/gr-op25/lib/abstract_data_unit.cc | 21 +- op25/gr-op25/lib/abstract_data_unit.h | 24 +- op25/gr-op25/lib/bch.cc | 162 ++++++++++ op25/gr-op25/lib/bch.h | 4 + op25/gr-op25/lib/crypto.cc | 284 ++++++++++++++++++ op25/gr-op25/lib/crypto.h | 72 +++++ op25/gr-op25/lib/crypto_module_du_handler.cc | 63 ++++ op25/gr-op25/lib/crypto_module_du_handler.h | 21 ++ op25/gr-op25/lib/data_unit.h | 6 +- op25/gr-op25/lib/decoder_bf_impl.cc | 108 +++++-- op25/gr-op25/lib/decoder_bf_impl.h | 26 +- op25/gr-op25/lib/decoder_ff_impl.cc | 8 +- op25/gr-op25/lib/des.h | 15 + op25/gr-op25/lib/deskey.c | 124 ++++++++ op25/gr-op25/lib/desport.c | 235 +++++++++++++++ op25/gr-op25/lib/dessp.c | 130 ++++++++ op25/gr-op25/lib/hdu.cc | 74 +++-- op25/gr-op25/lib/hdu.h | 15 +- op25/gr-op25/lib/ldu.cc | 100 +++++++ op25/gr-op25/lib/ldu.h | 28 ++ op25/gr-op25/lib/ldu1.cc | 69 ++++- op25/gr-op25/lib/ldu1.h | 40 ++- op25/gr-op25/lib/ldu2.cc | 50 +++- op25/gr-op25/lib/ldu2.h | 17 +- op25/gr-op25/lib/op25.i | 113 ------- op25/gr-op25/lib/op25_imbe_frame.h | 9 +- op25/gr-op25/lib/voice_data_unit.cc | 300 ++++++++++++++++++- op25/gr-op25/lib/voice_data_unit.h | 17 +- op25/gr-op25/lib/voice_du_handler.cc | 7 +- op25/gr-op25/lib/voice_du_handler.h | 5 +- op25/gr-op25/swig/op25_swig.i | 7 + op25/gr-op25_repeater/CMakeLists.txt | 4 +- 37 files changed, 1989 insertions(+), 220 deletions(-) create mode 100644 op25/gr-op25/lib/bch.cc create mode 100644 op25/gr-op25/lib/bch.h create mode 100644 op25/gr-op25/lib/crypto.cc create mode 100644 op25/gr-op25/lib/crypto.h create mode 100644 op25/gr-op25/lib/crypto_module_du_handler.cc create mode 100644 op25/gr-op25/lib/crypto_module_du_handler.h create mode 100644 op25/gr-op25/lib/des.h create mode 100644 op25/gr-op25/lib/deskey.c create mode 100644 op25/gr-op25/lib/desport.c create mode 100644 op25/gr-op25/lib/dessp.c create mode 100644 op25/gr-op25/lib/ldu.cc create mode 100644 op25/gr-op25/lib/ldu.h delete mode 100644 op25/gr-op25/lib/op25.i diff --git a/op25/gr-op25/CMakeLists.txt b/op25/gr-op25/CMakeLists.txt index 9711a91..5d3c369 100644 --- a/op25/gr-op25/CMakeLists.txt +++ b/op25/gr-op25/CMakeLists.txt @@ -83,7 +83,6 @@ set(GRC_BLOCKS_DIR ${GR_PKG_DATA_DIR}/grc/blocks) ######################################################################## # Find gnuradio build dependencies ######################################################################## -find_package(GnuradioRuntime) find_package(CppUnit) # To run a more advanced search for GNU Radio and it's components and @@ -91,8 +90,9 @@ find_package(CppUnit) # of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the # minimum API compatible version required. # -# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...) -# find_package(Gnuradio "version") +set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT) +#find_package(Gnuradio "version") +find_package(Gnuradio) if(NOT GNURADIO_RUNTIME_FOUND) message(FATAL_ERROR "GnuRadio Runtime required to compile op25") diff --git a/op25/gr-op25/grc/op25_decoder_ff.xml b/op25/gr-op25/grc/op25_decoder_ff.xml index c503081..2b3ef88 100644 --- a/op25/gr-op25/grc/op25_decoder_ff.xml +++ b/op25/gr-op25/grc/op25_decoder_ff.xml @@ -4,17 +4,17 @@ op25_decoder_ff op25 import op25 - op25.decoder_ff($) + op25.decoder_ff() - + in - + float - out - + audio + float diff --git a/op25/gr-op25/grc/op25_fsk4_demod_ff.xml b/op25/gr-op25/grc/op25_fsk4_demod_ff.xml index 5c970dc..7354502 100644 --- a/op25/gr-op25/grc/op25_fsk4_demod_ff.xml +++ b/op25/gr-op25/grc/op25_fsk4_demod_ff.xml @@ -4,7 +4,7 @@ op25_fsk4_demod_ff op25 import op25 - op25.fsk4_demod_ff(self.auto_tune_msgq, $sample_rate, $symbol_rate) + op25.fsk4_demod_ff($(id)_msgq_out, $sample_rate, $symbol_rate) Sample Rate @@ -20,7 +20,8 @@ real - + + in @@ -45,4 +46,10 @@ dibits float + + + tune + msg + + diff --git a/op25/gr-op25/include/op25/decoder_bf.h b/op25/gr-op25/include/op25/decoder_bf.h index ca74ad3..a964244 100644 --- a/op25/gr-op25/include/op25/decoder_bf.h +++ b/op25/gr-op25/include/op25/decoder_bf.h @@ -51,7 +51,7 @@ namespace gr { * class. op25::decoder_bf::make is the public interface for * creating new instances. */ - static sptr make(); + static sptr make(bool idle_silence = true, bool verbose = false); /** * Return a pointer to a string identifying the destination of @@ -78,6 +78,17 @@ namespace gr { * message queue. */ virtual void set_msgq(gr::msg_queue::sptr msgq) = 0; + + virtual void set_idle_silence(bool idle_silence = true) = 0; + + virtual void set_logging(bool verbose = true) = 0; + + typedef std::vector key_type; + typedef std::map key_map_type; + + virtual void set_key(const key_type& key) = 0; + + virtual void set_key_map(const key_map_type& keys) = 0; }; } // namespace op25 diff --git a/op25/gr-op25/lib/CMakeLists.txt b/op25/gr-op25/lib/CMakeLists.txt index 255ef93..1befdd9 100644 --- a/op25/gr-op25/lib/CMakeLists.txt +++ b/op25/gr-op25/lib/CMakeLists.txt @@ -53,6 +53,13 @@ list(APPEND op25_sources value_string.cc pickle.cc pcap_source_b_impl.cc + bch.cc + ldu.cc + crypto.cc + crypto_module_du_handler.cc + deskey.c + desport.c + dessp.c ) add_library(gnuradio-op25 SHARED ${op25_sources}) diff --git a/op25/gr-op25/lib/abstract_data_unit.cc b/op25/gr-op25/lib/abstract_data_unit.cc index 74bbfad..3b03efb 100644 --- a/op25/gr-op25/lib/abstract_data_unit.cc +++ b/op25/gr-op25/lib/abstract_data_unit.cc @@ -55,10 +55,10 @@ abstract_data_unit::correct_errors() } void -abstract_data_unit::decode_audio(imbe_decoder& imbe) +abstract_data_unit::decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod) { if(is_complete()) { - do_decode_audio(d_frame_body, imbe); + do_decode_audio(d_frame_body, imbe, crypto_mod); } else { ostringstream msg; msg << "cannot decode audio - frame is not complete" << endl; @@ -153,7 +153,8 @@ abstract_data_unit::dump(ostream& os) const } abstract_data_unit::abstract_data_unit(const_bit_queue& frame_body) : - d_frame_body(frame_body.size()) + d_frame_body(frame_body.size()), + d_logging_enabled(false) { copy(frame_body.begin(), frame_body.end(), d_frame_body.begin()); } @@ -164,7 +165,7 @@ abstract_data_unit::do_correct_errors(bit_vector& frame_body) } void -abstract_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe) +abstract_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod) { } @@ -179,3 +180,15 @@ abstract_data_unit::frame_size() const { return d_frame_body.size(); } + +void +abstract_data_unit::set_logging(bool on) +{ + d_logging_enabled = on; +} + +bool +abstract_data_unit::logging_enabled() const +{ + return d_logging_enabled; +} diff --git a/op25/gr-op25/lib/abstract_data_unit.h b/op25/gr-op25/lib/abstract_data_unit.h index d4fb7f2..1f4ae27 100644 --- a/op25/gr-op25/lib/abstract_data_unit.h +++ b/op25/gr-op25/lib/abstract_data_unit.h @@ -26,6 +26,7 @@ #include "data_unit.h" #include "op25_yank.h" +#include "crypto.h" #include #include @@ -62,7 +63,7 @@ public: * \precondition is_complete() == true. * \param imbe The imbe_decoder to use to generate the audio. */ - virtual void decode_audio(imbe_decoder& imbe); + virtual void decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod); /** * Decode the frame into an octet vector. @@ -117,6 +118,15 @@ public: */ virtual std::string snapshot() const; + /** + * Returns a string describing the Data Unit ID (DUID). + * + * \return A string identifying the DUID. + */ + virtual std::string duid_str() const = 0; + + virtual void set_logging(bool on); + protected: /** @@ -140,7 +150,7 @@ protected: * \param frame_body The const_bit_vector to decode. * \param imbe The imbe_decoder to use. */ - virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe); + virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod); /** * Decode frame_body and write the decoded frame contents to msg. @@ -152,13 +162,6 @@ protected: */ virtual size_t decode_frame(const_bit_vector& frame_body, size_t msg_sz, uint8_t *msg); - /** - * Returns a string describing the Data Unit ID (DUID). - * - * \return A string identifying the DUID. - */ - virtual std::string duid_str() const = 0; - /** * Return a reference to the frame body. */ @@ -180,6 +183,8 @@ protected: */ virtual uint16_t frame_size() const; + virtual bool logging_enabled() const; + private: /** @@ -187,6 +192,7 @@ private: */ bit_vector d_frame_body; + bool d_logging_enabled; }; #endif /* INCLUDED_ABSTRACT_DATA_UNIT_H */ diff --git a/op25/gr-op25/lib/bch.cc b/op25/gr-op25/lib/bch.cc new file mode 100644 index 0000000..e2e58a7 --- /dev/null +++ b/op25/gr-op25/lib/bch.cc @@ -0,0 +1,162 @@ + +#include +#include +#include "bch.h" +/* + * Copyright 2010, KA1RBI + */ +static const int bchGFexp[64] = { + 1, 2, 4, 8, 16, 32, 3, 6, 12, 24, 48, 35, 5, 10, 20, 40, + 19, 38, 15, 30, 60, 59, 53, 41, 17, 34, 7, 14, 28, 56, 51, 37, + 9, 18, 36, 11, 22, 44, 27, 54, 47, 29, 58, 55, 45, 25, 50, 39, + 13, 26, 52, 43, 21, 42, 23, 46, 31, 62, 63, 61, 57, 49, 33, 0 +}; + +static const int bchGFlog[64] = { + -1, 0, 1, 6, 2, 12, 7, 26, 3, 32, 13, 35, 8, 48, 27, 18, + 4, 24, 33, 16, 14, 52, 36, 54, 9, 45, 49, 38, 28, 41, 19, 56, + 5, 62, 25, 11, 34, 31, 17, 47, 15, 23, 53, 51, 37, 44, 55, 40, + 10, 61, 46, 30, 50, 22, 39, 43, 29, 60, 42, 21, 20, 59, 57, 58 +}; + +static const int bchG[48] = { + 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, + 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, + 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1 +}; + +int bchDec(bit_vector& Codeword) +{ + + int elp[24][ 22], S[23]; + int D[23], L[24], uLu[24]; + int root[11], locn[11], reg[12]; + int i,j,U,q,count; + int SynError, CantDecode; + + SynError = 0; CantDecode = 0; + + for(i = 1; i <= 22; i++) { + S[i] = 0; + // FOR j = 0 TO 62 + for(j = 0; j <= 62; j++) { + if( Codeword[j]) { S[i] = S[i] ^ bchGFexp[(i * j) % 63]; } + } + if( S[i]) { SynError = 1; } + S[i] = bchGFlog[S[i]]; + // printf("S[%d] %d\n", i, S[i]); + } + + if( SynError) { //if there are errors, try to correct them + L[0] = 0; uLu[0] = -1; D[0] = 0; elp[0][ 0] = 0; + L[1] = 0; uLu[1] = 0; D[1] = S[1]; elp[1][ 0] = 1; + //FOR i = 1 TO 21 + for(i = 1; i <= 21; i++) { + elp[0][ i] = -1; elp[1][ i] = 0; + } + U = 0; + + do { + U = U + 1; + if( D[U] == -1) { + L[U + 1] = L[U]; + // FOR i = 0 TO L[U] + for(i = 0; i <= L[U]; i++) { + elp[U + 1][ i] = elp[U][ i]; elp[U][ i] = bchGFlog[elp[U][ i]]; + } + } else { + //search for words with greatest uLu(q) for which d(q)!=0 + q = U - 1; + while((D[q] == -1) &&(q > 0)) { q = q - 1; } + //have found first non-zero d(q) + if( q > 0) { + j = q; + do { j = j - 1; if((D[j] != -1) &&(uLu[q] < uLu[j])) { q = j; } + } while( j > 0) ; + } + + //store degree of new elp polynomial + if( L[U] > L[q] + U - q) { + L[U + 1] = L[U] ; + } else { + L[U + 1] = L[q] + U - q; + } + + ///* form new elp(x) */ + // FOR i = 0 TO 21 + for(i = 0; i <= 21; i++) { + elp[U + 1][ i] = 0; + } + // FOR i = 0 TO L(q) + for(i = 0; i <= L[q]; i++) { + if( elp[q][ i] != -1) { + elp[U + 1][ i + U - q] = bchGFexp[(D[U] + 63 - D[q] + elp[q][ i]) % 63]; + } + } + // FOR i = 0 TO L(U) + for(i = 0; i <= L[U]; i++) { + elp[U + 1][ i] = elp[U + 1][ i] ^ elp[U][ i]; + elp[U][ i] = bchGFlog[elp[U][ i]]; + } + } + uLu[U + 1] = U - L[U + 1]; + + //form(u+1)th discrepancy + if( U < 22) { + //no discrepancy computed on last iteration + if( S[U + 1] != -1) { D[U + 1] = bchGFexp[S[U + 1]]; } else { D[U + 1] = 0; } + // FOR i = 1 TO L(U + 1) + for(i = 1; i <= L[U + 1]; i++) { + if((S[U + 1 - i] != -1) &&(elp[U + 1][ i] != 0)) { + D[U + 1] = D[U + 1] ^ bchGFexp[(S[U + 1 - i] + bchGFlog[elp[U + 1][ i]]) % 63]; + } + } + //put d(u+1) into index form */ + D[U + 1] = bchGFlog[D[U + 1]]; + } + } while((U < 22) &&(L[U + 1] <= 11)); + + U = U + 1; + if( L[U] <= 11) { // /* Can correct errors */ + //put elp into index form + // FOR i = 0 TO L[U] + for(i = 0; i <= L[U]; i++) { + elp[U][ i] = bchGFlog[elp[U][ i]]; + } + + //Chien search: find roots of the error location polynomial + // FOR i = 1 TO L(U) + for(i = 1; i <= L[U]; i++) { + reg[i] = elp[U][ i]; + } + count = 0; + // FOR i = 1 TO 63 + for(i = 1; i <= 63; i++) { + q = 1; + //FOR j = 1 TO L(U) + for(j = 1; j <= L[U]; j++) { + if( reg[j] != -1) { + reg[j] =(reg[j] + j) % 63; q = q ^ bchGFexp[reg[j]]; + } + } + if( q == 0) { //store root and error location number indices + root[count] = i; locn[count] = 63 - i; count = count + 1; + } + } + if( count == L[U]) { + //no. roots = degree of elp hence <= t errors + //FOR i = 0 TO L[U] - 1 + for(i = 0; i <= L[U]-1; i++) { + Codeword[locn[i]] = Codeword[locn[i]] ^ 1; + } + CantDecode = count; + } else { //elp has degree >t hence cannot solve + CantDecode = -1; + } + } else { + CantDecode = -2; + } + } + return CantDecode; +} + diff --git a/op25/gr-op25/lib/bch.h b/op25/gr-op25/lib/bch.h new file mode 100644 index 0000000..d151405 --- /dev/null +++ b/op25/gr-op25/lib/bch.h @@ -0,0 +1,4 @@ +#include +typedef std::vector bit_vector; +int bchDec(bit_vector& Codeword); + diff --git a/op25/gr-op25/lib/crypto.cc b/op25/gr-op25/lib/crypto.cc new file mode 100644 index 0000000..2d528bb --- /dev/null +++ b/op25/gr-op25/lib/crypto.cc @@ -0,0 +1,284 @@ +#include "crypto.h" + +#include +#include +#include + +extern "C" { +#include "des.h" +} + +static unsigned long long swap_bytes(uint64_t l) +{ + unsigned long long r; + unsigned char* pL = (unsigned char*)&l; + unsigned char* pR = (unsigned char*)&r; + for (int i = 0; i < sizeof(l); ++i) + pR[i] = pL[(sizeof(l) - 1) - i]; + return r; +} + +/////////////////////////////////////////////////////////////////////////////// +/* +class null_algorithm : public crypto_algorithm // This is an algorithm skeleton (can be used for no encryption as pass-through) +{ +private: + size_t m_generated_bits; +public: + null_algorithm() + : m_generated_bits(0) + { + } + const type_id id() const + { + return crypto_algorithm::NONE; + } + bool update(const struct CryptoState& state) + { + fprintf(stderr, "NULL:\t%d bits generated\n", m_generated_bits); + + m_generated_bits = 0; + + return true; + } + bool set_key(const crypto_algorithm::key_type& key) + { + return true; + } + uint64_t generate(size_t n) + { + m_generated_bits += n; + return 0; + } +}; +*/ +/////////////////////////////////////////////////////////////////////////////// + +class des_ofb : public crypto_algorithm +{ +public: + unsigned long long m_key_des, m_next_iv, m_ks; + int m_ks_idx; + DES_KS m_ksDES; + int m_iterations; + uint16_t m_current_kid; + key_type m_default_key; + key_map_type m_key_map; + bool m_verbose; +public: + des_ofb() + : m_current_kid(-1) + { + memset(&m_ksDES, 0, sizeof(m_ksDES)); + m_key_des = 0; + m_next_iv = 0; + m_ks_idx = 0; + m_ks = 0; + m_iterations = 0; + } + + void set_logging(bool on) + { + m_verbose = on; + } + + const type_id id() const + { + return crypto_algorithm::DES_OFB; + } + + bool update(const struct CryptoState& state) + { + if (m_current_kid != state.kid) + { + if (m_key_map.empty()) + { + // Nothing to do + } + else + { + key_map_type::iterator it = m_key_map.find(state.kid); + if (it != m_key_map.end()) + { + set_key(it->second); + } + else if (!m_default_key.empty()) + { + /*if (m_verbose) */fprintf(stderr, "Key 0x%04x not found in key map - using default key\n", state.kid); + + set_key(m_default_key); + } + else + { + /*if (m_verbose) */fprintf(stderr, "Key 0x%04x not found in key map and no default key\n", state.kid); + } + } + + m_current_kid = state.kid; + } + + uint64_t iv = 0; + size_t n = std::min(sizeof(iv), state.mi.size()); + memcpy(&iv, &state.mi[0], n); + set_iv(iv); + + return (n == 8); + } + + void set_key_map(const key_map_type& key_map) + { + m_key_map = key_map; + + m_current_kid = -1; // To refresh on next update if it has changed + } + + bool set_key(const crypto_algorithm::key_type& key) + { + const size_t valid_key_length = 8; + + if (key.size() != valid_key_length) + { + if (m_verbose) fprintf(stderr, "DES:\tIncorrect key length of %lu (should be %lu)\n", key.size(), valid_key_length); + return false; + } + + m_default_key = key; + + memcpy(&m_key_des, &key[0], std::min(key.size(), sizeof(m_key_des))); + + if (m_verbose) + { + std::stringstream ss; + for (int i = 0; i < valid_key_length; ++i) + ss << boost::format("%02X") % (int)key[i]; + std::cerr << "DES:\tKey: " << ss.str() << std::endl; + } + + deskey(m_ksDES, (unsigned char*)&m_key_des, 0); // 0: encrypt (for OFB mode) + + return true; + } + + void set_iv(uint64_t iv) + { + if (m_iterations > 0) + { + if (m_verbose) fprintf(stderr, "DES:\t%i bits used from %i iterations\n", m_ks_idx, m_iterations); + } + + m_next_iv = iv; + + m_ks_idx = 0; + m_iterations = 0; + + m_ks = m_next_iv; + des(m_ksDES, (unsigned char*)&m_ks); // First initialisation + ++m_iterations; + + des(m_ksDES, (unsigned char*)&m_ks); // Throw out first iteration & prepare for second + ++m_iterations; + + generate(64); // Reserved 3 + first 5 of LC (3 left) + generate(3 * 8); // Use remaining 3 bytes for LC + } + + unsigned long long generate(size_t count) // 1..64 + { + unsigned long long ullCurrent = swap_bytes(m_ks); + const int max_len = 64; + int pos = m_ks_idx % max_len; + + m_ks_idx += count; + + if ((pos + count) <= max_len) // Up to 64 + { + if ((m_ks_idx % max_len) == 0) + { + des(m_ksDES, (unsigned char*)&m_ks); // Prepare for next iteration + ++m_iterations; + } + + unsigned long long result = (ullCurrent >> (((max_len - 1) - pos) - (count-1))) & ((count == max_len) ? (unsigned long long)-1 : ((1ULL << count) - 1)); + + return result; + } + + // Over-flow 64-bit boundary (so all of rest of current will be used) + + des(m_ksDES, (unsigned char*)&m_ks); // Compute second part + ++m_iterations; + + unsigned long long first = ullCurrent << pos; // RHS will be zeros + + ullCurrent = swap_bytes(m_ks); + int remainder = count - (max_len - pos); + first >>= (((max_len - 1) - remainder) - ((max_len - 1) - pos)); + unsigned long long next = (ullCurrent >> (((max_len - 1) - 0) - (remainder-1))) & ((1ULL << remainder) - 1); + + return (first | next); + } + +}; + +/////////////////////////////////////////////////////////////////////////////// + +crypto_module::crypto_module(bool verbose/* = true*/) + : d_verbose(verbose) +{ +} + +crypto_algorithm::sptr crypto_module::algorithm(crypto_algorithm::type_id algid) +{ + if ((!d_current_algorithm && (algid == crypto_algorithm::NONE)) || // This line should be commented out if 'null_algorithm' is to be tested + (d_current_algorithm && (algid == d_current_algorithm->id()))) + return d_current_algorithm; + + switch (algid) + { + case crypto_algorithm::DES_OFB: + d_current_algorithm = crypto_algorithm::sptr(new des_ofb()); + break; + //case crypto_algorithm::NONE: + // d_current_algorithm = crypto_algorithm::sptr(new null_algorithm()); + // break; + default: + d_current_algorithm = crypto_algorithm::sptr(); + }; + + if (d_current_algorithm) + { + d_current_algorithm->set_logging(logging_enabled()); + + if (!d_persistent_key_map.empty()) + d_current_algorithm->set_key_map(d_persistent_key_map); + + if (!d_persistent_key.empty()) + d_current_algorithm->set_key(d_persistent_key); + } + + return d_current_algorithm; +} + +void crypto_module::set_key(const crypto_algorithm::key_type& key) +{ + d_persistent_key = key; + + if (d_current_algorithm) + d_current_algorithm->set_key(d_persistent_key); +} + +void crypto_module::set_key_map(const crypto_algorithm::key_map_type& keys) +{ + d_persistent_key_map = keys; + + if (d_current_algorithm) + d_current_algorithm->set_key_map(d_persistent_key_map); +} + +void crypto_module::set_logging(bool on/* = true*/) +{ + d_verbose = on; + + if (d_current_algorithm) + d_current_algorithm->set_logging(on); +} diff --git a/op25/gr-op25/lib/crypto.h b/op25/gr-op25/lib/crypto.h new file mode 100644 index 0000000..3180f5b --- /dev/null +++ b/op25/gr-op25/lib/crypto.h @@ -0,0 +1,72 @@ +#ifndef INCLUDED_CRYPTO_H +#define INCLUDED_CRYPTO_H + +#include +#include +#include + +static const int MESSAGE_INDICATOR_LENGTH = 9; + +class CryptoState +{ +public: + CryptoState() : + kid(0), algid(0), mi(MESSAGE_INDICATOR_LENGTH) + { } +public: + std::vector mi; + uint16_t kid; + uint8_t algid; +}; + +class crypto_state_provider +{ +public: + virtual struct CryptoState crypto_state() const=0; +}; + +class crypto_algorithm +{ +public: + typedef boost::shared_ptr sptr; + typedef std::vector key_type; + typedef std::map key_map_type; + typedef uint8_t type_id; + enum + { + NONE = 0x80, + DES_OFB = 0x81, + }; +public: + virtual const type_id id() const=0; + virtual bool set_key(const key_type& key)=0; + virtual void set_key_map(const key_map_type& key_map)=0; + virtual bool update(const struct CryptoState& state)=0; + virtual uint64_t generate(size_t n_bits)=0; // Can request up to 64 bits of key stream at one time + virtual void set_logging(bool on)=0; +}; + +class crypto_module +{ +public: + typedef boost::shared_ptr sptr; +public: + crypto_module(bool verbose = false); +public: + virtual crypto_algorithm::sptr algorithm(crypto_algorithm::type_id algid); + virtual void set_key(const crypto_algorithm::key_type& key); + virtual void set_key_map(const crypto_algorithm::key_map_type& keys); + virtual void set_logging(bool on = true); +protected: + crypto_algorithm::sptr d_current_algorithm; + crypto_algorithm::key_type d_persistent_key; + crypto_algorithm::key_map_type d_persistent_key_map; + bool d_verbose; +public: + virtual crypto_algorithm::sptr current_algorithm() const + { return d_current_algorithm; } + virtual bool logging_enabled() const + { return d_verbose; } +}; + +#endif // INCLUDED_CRYPTO_H diff --git a/op25/gr-op25/lib/crypto_module_du_handler.cc b/op25/gr-op25/lib/crypto_module_du_handler.cc new file mode 100644 index 0000000..90ac06c --- /dev/null +++ b/op25/gr-op25/lib/crypto_module_du_handler.cc @@ -0,0 +1,63 @@ +#include "crypto_module_du_handler.h" + +#include "abstract_data_unit.h" + +#include +#include + +crypto_module_du_handler::crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod) + : data_unit_handler(next) + , d_crypto_mod(crypto_mod) +{ +} + +void +crypto_module_du_handler::handle(data_unit_sptr du) +{ + if (!d_crypto_mod) + { + data_unit_handler::handle(du); + return; + } + + crypto_state_provider* p = dynamic_cast(du.get()); + if (p == NULL) + { + data_unit_handler::handle(du); + return; + } + + CryptoState state = p->crypto_state(); + + /////////////////////////////////// + + if (d_crypto_mod->logging_enabled()) + { + std::string duid_str("?"); + abstract_data_unit* adu = dynamic_cast(du.get()); + if (adu) + duid_str = adu->duid_str(); + + std::stringstream ss; + for (size_t n = 0; n < state.mi.size(); ++n) + ss << (boost::format("%02x") % (int)state.mi[n]); + + fprintf(stderr, "%s:\tAlgID: 0x%02x, KID: 0x%04x, MI: %s\n", duid_str.c_str(), state.algid, state.kid, ss.str().c_str()); + } + + /////////////////////////////////// + + crypto_algorithm::sptr algorithm = d_crypto_mod->algorithm(state.algid); + if (!algorithm) + { + data_unit_handler::handle(du); + return; + } + + // TODO: Could do key management & selection here with 'state.kid' + // Assuming we're only using one key (ignoring 'kid') + + algorithm->update(state); + + data_unit_handler::handle(du); +} diff --git a/op25/gr-op25/lib/crypto_module_du_handler.h b/op25/gr-op25/lib/crypto_module_du_handler.h new file mode 100644 index 0000000..03479fe --- /dev/null +++ b/op25/gr-op25/lib/crypto_module_du_handler.h @@ -0,0 +1,21 @@ +#ifndef INCLUDED_CRYPTO_MODULE_DU_HANDLER_H +#define INCLUDED_CRYPTO_MODULE_DU_HANDLER_H + +#include + +#include "data_unit_handler.h" +#include "crypto.h" + +class crypto_module_du_handler : public data_unit_handler +{ +public: + crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod); +public: + typedef boost::shared_ptr sptr; +public: + virtual void handle(data_unit_sptr du); +private: + crypto_module::sptr d_crypto_mod; +}; + +#endif //INCLUDED_CRYPTO_MODULE_HANDLER_H diff --git a/op25/gr-op25/lib/data_unit.h b/op25/gr-op25/lib/data_unit.h index bc824d9..670ab3a 100644 --- a/op25/gr-op25/lib/data_unit.h +++ b/op25/gr-op25/lib/data_unit.h @@ -32,6 +32,8 @@ #include #include +#include "crypto.h" + typedef std::deque bit_queue; typedef const std::deque const_bit_queue; @@ -76,7 +78,7 @@ public: * \precondition is_complete() == true. * \param imbe The imbe_decoder to use to generate the audio. */ - virtual void decode_audio(imbe_decoder& imbe) = 0; + virtual void decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod) = 0; /** * Decode the frame into an octet vector. @@ -132,6 +134,8 @@ public: */ virtual std::string snapshot() const = 0; + virtual void set_logging(bool on) = 0; + protected: /** diff --git a/op25/gr-op25/lib/decoder_bf_impl.cc b/op25/gr-op25/lib/decoder_bf_impl.cc index 4b49f57..8120970 100644 --- a/op25/gr-op25/lib/decoder_bf_impl.cc +++ b/op25/gr-op25/lib/decoder_bf_impl.cc @@ -34,6 +34,7 @@ #include "offline_imbe_decoder.h" #include "voice_du_handler.h" #include "op25_yank.h" +#include "bch.h" using namespace std; @@ -41,13 +42,13 @@ namespace gr { namespace op25 { decoder_bf::sptr - decoder_bf::make() + decoder_bf::make(bool idle_silence /*= true*/, bool verbose /*= false*/) { return gnuradio::get_initial_sptr - (new decoder_bf_impl()); + (new decoder_bf_impl(idle_silence, verbose)); } - decoder_bf_impl::decoder_bf_impl() : + decoder_bf_impl::decoder_bf_impl(bool idle_silence /*= true*/, bool verbose /*= false*/) : gr::block("decoder_bf", gr::io_signature::make(1, 1, sizeof(uint8_t)), gr::io_signature::make(0, 1, sizeof(float))), @@ -56,14 +57,25 @@ namespace gr { d_frame_hdr(), d_imbe(imbe_decoder::make()), d_state(SYNCHRONIZING), - d_p25cai_du_handler(NULL) + d_p25cai_du_handler(NULL), + d_idle_silence(idle_silence), + d_verbose(false) { + set_logging(verbose); + d_p25cai_du_handler = new p25cai_du_handler(d_data_unit_handler, "224.0.0.1", 23456); d_data_unit_handler = data_unit_handler_sptr(d_p25cai_du_handler); + d_snapshot_du_handler = new snapshot_du_handler(d_data_unit_handler); d_data_unit_handler = data_unit_handler_sptr(d_snapshot_du_handler); - d_data_unit_handler = data_unit_handler_sptr(new voice_du_handler(d_data_unit_handler, d_imbe)); + + d_crypto_module = crypto_module::sptr(new crypto_module(verbose)); + + d_crypto_module_du_handler = crypto_module_du_handler::sptr(new crypto_module_du_handler(d_data_unit_handler, d_crypto_module)); + d_data_unit_handler = data_unit_handler_sptr(d_crypto_module_du_handler); + + d_data_unit_handler = data_unit_handler_sptr(new voice_du_handler(d_data_unit_handler, d_imbe, d_crypto_module)); } decoder_bf_impl::~decoder_bf_impl() @@ -104,34 +116,35 @@ namespace gr { gr_vector_void_star &output_items) { try { + gr::thread::scoped_lock lock(d_mutex); - // process input - const uint8_t *in = reinterpret_cast(input_items[0]); - for(int i = 0; i < ninput_items[0]; ++i) { - dibit d = in[i] & 0x3; - receive_symbol(d); - } - consume_each(ninput_items[0]); + // process input + const uint8_t *in = reinterpret_cast(input_items[0]); + for(int i = 0; i < ninput_items[0]; ++i) { + dibit d = in[i] & 0x3; + receive_symbol(d); + } + consume_each(ninput_items[0]); - // produce audio - audio_samples *samples = d_imbe->audio(); - float *out = reinterpret_cast(output_items[0]); - const int n = min(static_cast(samples->size()), noutput_items); - if(0 < n) { - copy(samples->begin(), samples->begin() + n, out); - samples->erase(samples->begin(), samples->begin() + n); - } - if(n < noutput_items) { - fill(out + n, out + noutput_items, 0.0); - } - return noutput_items; + // produce audio + audio_samples *samples = d_imbe->audio(); + float *out = reinterpret_cast(output_items[0]); + const int n = min(static_cast(samples->size()), noutput_items); + if(0 < n) { + copy(samples->begin(), samples->begin() + n, out); + samples->erase(samples->begin(), samples->begin() + n); + } + if((d_idle_silence) && (n < noutput_items)) { + fill(out + n, out + noutput_items, 0.0); + } + return (d_idle_silence ? noutput_items : n); } catch(const std::exception& x) { - cerr << x.what() << endl; - exit(1); + cerr << x.what() << endl; + exit(1); } catch(...) { - cerr << "unhandled exception" << endl; - exit(2); } + cerr << "unhandled exception" << endl; + exit(2); } } const char* @@ -177,14 +190,12 @@ namespace gr { }; size_t NID_SZ = sizeof(NID) / sizeof(NID[0]); - itpp::bvec b(63), zeroes(16); - itpp::BCH bch(63, 16, 11, "6 3 3 1 1 4 1 3 6 7 2 3 5 4 5 3", true); + bit_vector b(NID_SZ); yank(d_frame_hdr, NID, NID_SZ, b, 0); - b = bch.decode(b); - if(b != zeroes) { - b = bch.encode(b); + if(bchDec(b) >= 0) { yank_back(b, 0, d_frame_hdr, NID, NID_SZ); d_data_unit = data_unit::make_data_unit(d_frame_hdr); + d_data_unit->set_logging(d_verbose); } else { data_unit_sptr null; d_data_unit = null; @@ -229,5 +240,36 @@ namespace gr { break; } } + + void + decoder_bf_impl::set_idle_silence(bool idle_silence/* = true*/) + { + gr::thread::scoped_lock lock(d_mutex); + + d_idle_silence = idle_silence; + } + + void + decoder_bf_impl::set_logging(bool verbose/* = true*/) + { + if (verbose) fprintf(stderr, "[%s<%lu>] verbose logging enabled\n", name().c_str(), unique_id()); + + d_verbose = verbose; + + if (d_crypto_module) + d_crypto_module->set_logging(verbose); + } + + void + decoder_bf_impl::set_key(const key_type& key) + { + d_crypto_module->set_key(key); + } + + void + decoder_bf_impl::set_key_map(const key_map_type& keys) + { + d_crypto_module->set_key_map(keys); + } } /* namespace op25 */ } /* namespace gr */ diff --git a/op25/gr-op25/lib/decoder_bf_impl.h b/op25/gr-op25/lib/decoder_bf_impl.h index f907863..29b7970 100644 --- a/op25/gr-op25/lib/decoder_bf_impl.h +++ b/op25/gr-op25/lib/decoder_bf_impl.h @@ -24,11 +24,14 @@ #define INCLUDED_OP25_DECODER_BF_IMPL_H #include +#include #include "data_unit.h" #include "data_unit_handler.h" #include "imbe_decoder.h" #include "p25cai_du_handler.h" #include "snapshot_du_handler.h" +#include "crypto.h" +#include "crypto_module_du_handler.h" namespace gr { namespace op25 { @@ -102,8 +105,21 @@ namespace gr { */ class snapshot_du_handler *d_snapshot_du_handler; + /* + * Whether or not to output silence when no audio is synthesised. + */ + bool d_idle_silence; + + bool d_verbose; + + crypto_module::sptr d_crypto_module; + + crypto_module_du_handler::sptr d_crypto_module_du_handler; + + gr::thread::mutex d_mutex; + public: - decoder_bf_impl(); + decoder_bf_impl(bool idle_silence = true, bool verbose = false); ~decoder_bf_impl(); // Where all the action really happens @@ -139,6 +155,14 @@ namespace gr { * message queue. */ void set_msgq(gr::msg_queue::sptr msgq); + + void set_idle_silence(bool idle_silence = true); + + void set_logging(bool verbose = true); + + void set_key(const key_type& key); + + void set_key_map(const key_map_type& keys); }; } // namespace op25 } // namespace gr diff --git a/op25/gr-op25/lib/decoder_ff_impl.cc b/op25/gr-op25/lib/decoder_ff_impl.cc index 60238c7..9115146 100644 --- a/op25/gr-op25/lib/decoder_ff_impl.cc +++ b/op25/gr-op25/lib/decoder_ff_impl.cc @@ -34,6 +34,7 @@ #include "offline_imbe_decoder.h" #include "voice_du_handler.h" #include "op25_yank.h" +#include "bch.h" using namespace std; @@ -185,12 +186,9 @@ namespace gr { }; size_t NID_SZ = sizeof(NID) / sizeof(NID[0]); - itpp::bvec b(63), zeroes(16); - itpp::BCH bch(63, 16, 11, "6 3 3 1 1 4 1 3 6 7 2 3 5 4 5 3", true); + bit_vector b(NID_SZ); yank(d_frame_hdr, NID, NID_SZ, b, 0); - b = bch.decode(b); - if(b != zeroes) { - b = bch.encode(b); + if(bchDec(b) >= 0) { yank_back(b, 0, d_frame_hdr, NID, NID_SZ); d_data_unit = data_unit::make_data_unit(d_frame_hdr); } else { diff --git a/op25/gr-op25/lib/des.h b/op25/gr-op25/lib/des.h new file mode 100644 index 0000000..0dfd56c --- /dev/null +++ b/op25/gr-op25/lib/des.h @@ -0,0 +1,15 @@ +typedef unsigned long DES_KS[16][2]; /* Single-key DES key schedule */ +typedef unsigned long DES3_KS[48][2]; /* Triple-DES key schedule */ + +/* In deskey.c: */ +void deskey(DES_KS,unsigned char *,int); +void des3key(DES3_KS,unsigned char *,int); + +/* In desport.c, desborl.cas or desgnu.s: */ +void des(DES_KS,unsigned char *); +/* In des3port.c, des3borl.cas or des3gnu.s: */ +void des3(DES3_KS,unsigned char *); + +extern int Asmversion; /* 1 if we're linked with an asm version, 0 if C */ + + diff --git a/op25/gr-op25/lib/deskey.c b/op25/gr-op25/lib/deskey.c new file mode 100644 index 0000000..fe8b677 --- /dev/null +++ b/op25/gr-op25/lib/deskey.c @@ -0,0 +1,124 @@ +/* Portable C code to create DES key schedules from user-provided keys + * This doesn't have to be fast unless you're cracking keys or UNIX + * passwords + */ + +#include +#include "des.h" + +/* Key schedule-related tables from FIPS-46 */ + +/* permuted choice table (key) */ +static unsigned char pc1[] = { + 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4 +}; + +/* number left rotations of pc1 */ +static unsigned char totrot[] = { + 1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28 +}; + +/* permuted choice key (table) */ +static unsigned char pc2[] = { + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 +}; + +/* End of DES-defined tables */ + + +/* bit 0 is left-most in byte */ +static int bytebit[] = { + 0200,0100,040,020,010,04,02,01 +}; + + +/* Generate key schedule for encryption or decryption + * depending on the value of "decrypt" + */ +void +deskey(DES_KS k,unsigned char *key,int decrypt) +/* Key schedule array */ +/* 64 bits (will use only 56) */ +/* 0 = encrypt, 1 = decrypt */ +{ + unsigned char pc1m[56]; /* place to modify pc1 into */ + unsigned char pcr[56]; /* place to rotate pc1 into */ + register int i,j,l; + int m; + unsigned char ks[8]; + + for (j=0; j<56; j++) { /* convert pc1 to bits of key */ + l=pc1[j]-1; /* integer bit location */ + m = l & 07; /* find bit */ + pc1m[j]=(key[l>>3] & /* find which key byte l is in */ + bytebit[m]) /* and which bit of that byte */ + ? 1 : 0; /* and store 1-bit result */ + } + for (i=0; i<16; i++) { /* key chunk for each iteration */ + memset(ks,0,sizeof(ks)); /* Clear key schedule */ + for (j=0; j<56; j++) /* rotate pc1 the right amount */ + pcr[j] = pc1m[(l=j+totrot[decrypt? 15-i : i])<(j<28? 28 : 56) ? l: l-28]; + /* rotate left and right halves independently */ + for (j=0; j<48; j++){ /* select bits individually */ + /* check bit that goes to ks[j] */ + if (pcr[pc2[j]-1]){ + /* mask it in if it's there */ + l= j % 6; + ks[j/6] |= bytebit[l] >> 2; + } + } + /* Now convert to packed odd/even interleaved form */ + k[i][0] = ((long)ks[0] << 24) + | ((long)ks[2] << 16) + | ((long)ks[4] << 8) + | ((long)ks[6]); + k[i][1] = ((long)ks[1] << 24) + | ((long)ks[3] << 16) + | ((long)ks[5] << 8) + | ((long)ks[7]); + if(Asmversion){ + /* The assembler versions pre-shift each subkey 2 bits + * so the Spbox indexes are already computed + */ + k[i][0] <<= 2; + k[i][1] <<= 2; + } + } +} + +/* Generate key schedule for triple DES in E-D-E (or D-E-D) mode. + * + * The key argument is taken to be 24 bytes. The first 8 bytes are K1 + * for the first stage, the second 8 bytes are K2 for the middle stage + * and the third 8 bytes are K3 for the last stage + */ +void +des3key(DES3_KS k,unsigned char *key,int decrypt) +/* 192 bits (will use only 168) */ +/* 0 = encrypt, 1 = decrypt */ +{ + if(!decrypt){ + deskey(&k[0],&key[0],0); + deskey(&k[16],&key[8],1); + deskey(&k[32],&key[16],0); + } else { + deskey(&k[32],&key[0],1); + deskey(&k[16],&key[8],0); + deskey(&k[0],&key[16],1); + } +} diff --git a/op25/gr-op25/lib/desport.c b/op25/gr-op25/lib/desport.c new file mode 100644 index 0000000..01bc294 --- /dev/null +++ b/op25/gr-op25/lib/desport.c @@ -0,0 +1,235 @@ +/* Portable C version of des() function */ + +#include "des.h" + +/* Tables defined in the Data Encryption Standard documents + * Three of these tables, the initial permutation, the final + * permutation and the expansion operator, are regular enough that + * for speed, we hard-code them. They're here for reference only. + * Also, the S and P boxes are used by a separate program, gensp.c, + * to build the combined SP box, Spbox[]. They're also here just + * for reference. + */ +#ifdef notdef +/* initial permutation IP */ +static unsigned char ip[] = { + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7 +}; + +/* final permutation IP^-1 */ +static unsigned char fp[] = { + 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25 +}; +/* expansion operation matrix */ +static unsigned char ei[] = { + 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1 +}; +/* The (in)famous S-boxes */ +static unsigned char sbox[8][64] = { + /* S1 */ + 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13, + + /* S2 */ + 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9, + + /* S3 */ + 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12, + + /* S4 */ + 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14, + + /* S5 */ + 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3, + + /* S6 */ + 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13, + + /* S7 */ + 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12, + + /* S8 */ + 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 +}; + +/* 32-bit permutation function P used on the output of the S-boxes */ +static unsigned char p32i[] = { + 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25 +}; +#endif + +int Asmversion = 0; + +/* Combined SP lookup table, linked in + * For best results, ensure that this is aligned on a 32-bit boundary; + * Borland C++ 3.1 doesn't guarantee this! + */ +extern unsigned long Spbox[8][64]; /* Combined S and P boxes */ + +/* Primitive function F. + * Input is r, subkey array in keys, output is XORed into l. + * Each round consumes eight 6-bit subkeys, one for + * each of the 8 S-boxes, 2 longs for each round. + * Each long contains four 6-bit subkeys, each taking up a byte. + * The first long contains, from high to low end, the subkeys for + * S-boxes 1, 3, 5 & 7; the second contains the subkeys for S-boxes + * 2, 4, 6 & 8 (using the origin-1 S-box numbering in the standard, + * not the origin-0 numbering used elsewhere in this code) + * See comments elsewhere about the pre-rotated values of r and Spbox. + */ +#define F(l,r,key){\ + work = ((r >> 4) | (r << 28)) ^ key[0];\ + l ^= Spbox[6][work & 0x3f];\ + l ^= Spbox[4][(work >> 8) & 0x3f];\ + l ^= Spbox[2][(work >> 16) & 0x3f];\ + l ^= Spbox[0][(work >> 24) & 0x3f];\ + work = r ^ key[1];\ + l ^= Spbox[7][work & 0x3f];\ + l ^= Spbox[5][(work >> 8) & 0x3f];\ + l ^= Spbox[3][(work >> 16) & 0x3f];\ + l ^= Spbox[1][(work >> 24) & 0x3f];\ +} +/* Encrypt or decrypt a block of data in ECB mode */ +void +des(unsigned long ks[16][2],unsigned char block[8]) +/* Key schedule */ +/* Data block */ +{ + unsigned long left,right,work; + + /* Read input block and place in left/right in big-endian order */ + left = ((unsigned long)block[0] << 24) + | ((unsigned long)block[1] << 16) + | ((unsigned long)block[2] << 8) + | (unsigned long)block[3]; + right = ((unsigned long)block[4] << 24) + | ((unsigned long)block[5] << 16) + | ((unsigned long)block[6] << 8) + | (unsigned long)block[7]; + + /* Hoey's clever initial permutation algorithm, from Outerbridge + * (see Schneier p 478) + * + * The convention here is the same as Outerbridge: rotate each + * register left by 1 bit, i.e., so that "left" contains permuted + * input bits 2, 3, 4, ... 1 and "right" contains 33, 34, 35, ... 32 + * (using origin-1 numbering as in the FIPS). This allows us to avoid + * one of the two rotates that would otherwise be required in each of + * the 16 rounds. + */ + work = ((left >> 4) ^ right) & 0x0f0f0f0f; + right ^= work; + left ^= work << 4; + work = ((left >> 16) ^ right) & 0xffff; + right ^= work; + left ^= work << 16; + work = ((right >> 2) ^ left) & 0x33333333; + left ^= work; + right ^= (work << 2); + work = ((right >> 8) ^ left) & 0xff00ff; + left ^= work; + right ^= (work << 8); + right = (right << 1) | (right >> 31); + work = (left ^ right) & 0xaaaaaaaa; + left ^= work; + right ^= work; + left = (left << 1) | (left >> 31); + + /* Now do the 16 rounds */ + F(left,right,ks[0]); + F(right,left,ks[1]); + F(left,right,ks[2]); + F(right,left,ks[3]); + F(left,right,ks[4]); + F(right,left,ks[5]); + F(left,right,ks[6]); + F(right,left,ks[7]); + F(left,right,ks[8]); + F(right,left,ks[9]); + F(left,right,ks[10]); + F(right,left,ks[11]); + F(left,right,ks[12]); + F(right,left,ks[13]); + F(left,right,ks[14]); + F(right,left,ks[15]); + + /* Inverse permutation, also from Hoey via Outerbridge and Schneier */ + right = (right << 31) | (right >> 1); + work = (left ^ right) & 0xaaaaaaaa; + left ^= work; + right ^= work; + left = (left >> 1) | (left << 31); + work = ((left >> 8) ^ right) & 0xff00ff; + right ^= work; + left ^= work << 8; + work = ((left >> 2) ^ right) & 0x33333333; + right ^= work; + left ^= work << 2; + work = ((right >> 16) ^ left) & 0xffff; + left ^= work; + right ^= work << 16; + work = ((right >> 4) ^ left) & 0x0f0f0f0f; + left ^= work; + right ^= work << 4; + + /* Put the block back into the user's buffer with final swap */ + block[0] = right >> 24; + block[1] = right >> 16; + block[2] = right >> 8; + block[3] = right; + block[4] = left >> 24; + block[5] = left >> 16; + block[6] = left >> 8; + block[7] = left; +} diff --git a/op25/gr-op25/lib/dessp.c b/op25/gr-op25/lib/dessp.c new file mode 100644 index 0000000..61356f1 --- /dev/null +++ b/op25/gr-op25/lib/dessp.c @@ -0,0 +1,130 @@ +unsigned long Spbox[8][64] = { +0x01010400,0x00000000,0x00010000,0x01010404, +0x01010004,0x00010404,0x00000004,0x00010000, +0x00000400,0x01010400,0x01010404,0x00000400, +0x01000404,0x01010004,0x01000000,0x00000004, +0x00000404,0x01000400,0x01000400,0x00010400, +0x00010400,0x01010000,0x01010000,0x01000404, +0x00010004,0x01000004,0x01000004,0x00010004, +0x00000000,0x00000404,0x00010404,0x01000000, +0x00010000,0x01010404,0x00000004,0x01010000, +0x01010400,0x01000000,0x01000000,0x00000400, +0x01010004,0x00010000,0x00010400,0x01000004, +0x00000400,0x00000004,0x01000404,0x00010404, +0x01010404,0x00010004,0x01010000,0x01000404, +0x01000004,0x00000404,0x00010404,0x01010400, +0x00000404,0x01000400,0x01000400,0x00000000, +0x00010004,0x00010400,0x00000000,0x01010004, +0x80108020,0x80008000,0x00008000,0x00108020, +0x00100000,0x00000020,0x80100020,0x80008020, +0x80000020,0x80108020,0x80108000,0x80000000, +0x80008000,0x00100000,0x00000020,0x80100020, +0x00108000,0x00100020,0x80008020,0x00000000, +0x80000000,0x00008000,0x00108020,0x80100000, +0x00100020,0x80000020,0x00000000,0x00108000, +0x00008020,0x80108000,0x80100000,0x00008020, +0x00000000,0x00108020,0x80100020,0x00100000, +0x80008020,0x80100000,0x80108000,0x00008000, +0x80100000,0x80008000,0x00000020,0x80108020, +0x00108020,0x00000020,0x00008000,0x80000000, +0x00008020,0x80108000,0x00100000,0x80000020, +0x00100020,0x80008020,0x80000020,0x00100020, +0x00108000,0x00000000,0x80008000,0x00008020, +0x80000000,0x80100020,0x80108020,0x00108000, +0x00000208,0x08020200,0x00000000,0x08020008, +0x08000200,0x00000000,0x00020208,0x08000200, +0x00020008,0x08000008,0x08000008,0x00020000, +0x08020208,0x00020008,0x08020000,0x00000208, +0x08000000,0x00000008,0x08020200,0x00000200, +0x00020200,0x08020000,0x08020008,0x00020208, +0x08000208,0x00020200,0x00020000,0x08000208, +0x00000008,0x08020208,0x00000200,0x08000000, +0x08020200,0x08000000,0x00020008,0x00000208, +0x00020000,0x08020200,0x08000200,0x00000000, +0x00000200,0x00020008,0x08020208,0x08000200, +0x08000008,0x00000200,0x00000000,0x08020008, +0x08000208,0x00020000,0x08000000,0x08020208, +0x00000008,0x00020208,0x00020200,0x08000008, +0x08020000,0x08000208,0x00000208,0x08020000, +0x00020208,0x00000008,0x08020008,0x00020200, +0x100802001,0x100002081,0x100002081,0x00000080, +0x00802080,0x100800081,0x100800001,0x100002001, +0x00000000,0x00802000,0x00802000,0x100802081, +0x100000081,0x00000000,0x00800080,0x100800001, +0x100000001,0x00002000,0x00800000,0x100802001, +0x00000080,0x00800000,0x100002001,0x00002080, +0x100800081,0x100000001,0x00002080,0x00800080, +0x00002000,0x00802080,0x100802081,0x100000081, +0x00800080,0x100800001,0x00802000,0x100802081, +0x100000081,0x00000000,0x00000000,0x00802000, +0x00002080,0x00800080,0x100800081,0x100000001, +0x100802001,0x100002081,0x100002081,0x00000080, +0x100802081,0x100000081,0x100000001,0x00002000, +0x100800001,0x100002001,0x00802080,0x100800081, +0x100002001,0x00002080,0x00800000,0x100802001, +0x00000080,0x00800000,0x00002000,0x00802080, +0x00000100,0x02080100,0x02080000,0x42000100, +0x00080000,0x00000100,0x40000000,0x02080000, +0x40080100,0x00080000,0x02000100,0x40080100, +0x42000100,0x42080000,0x00080100,0x40000000, +0x02000000,0x40080000,0x40080000,0x00000000, +0x40000100,0x42080100,0x42080100,0x02000100, +0x42080000,0x40000100,0x00000000,0x42000000, +0x02080100,0x02000000,0x42000000,0x00080100, +0x00080000,0x42000100,0x00000100,0x02000000, +0x40000000,0x02080000,0x42000100,0x40080100, +0x02000100,0x40000000,0x42080000,0x02080100, +0x40080100,0x00000100,0x02000000,0x42080000, +0x42080100,0x00080100,0x42000000,0x42080100, +0x02080000,0x00000000,0x40080000,0x42000000, +0x00080100,0x02000100,0x40000100,0x00080000, +0x00000000,0x40080000,0x02080100,0x40000100, +0x20000010,0x20400000,0x00004000,0x20404010, +0x20400000,0x00000010,0x20404010,0x00400000, +0x20004000,0x00404010,0x00400000,0x20000010, +0x00400010,0x20004000,0x20000000,0x00004010, +0x00000000,0x00400010,0x20004010,0x00004000, +0x00404000,0x20004010,0x00000010,0x20400010, +0x20400010,0x00000000,0x00404010,0x20404000, +0x00004010,0x00404000,0x20404000,0x20000000, +0x20004000,0x00000010,0x20400010,0x00404000, +0x20404010,0x00400000,0x00004010,0x20000010, +0x00400000,0x20004000,0x20000000,0x00004010, +0x20000010,0x20404010,0x00404000,0x20400000, +0x00404010,0x20404000,0x00000000,0x20400010, +0x00000010,0x00004000,0x20400000,0x00404010, +0x00004000,0x00400010,0x20004010,0x00000000, +0x20404000,0x20000000,0x00400010,0x20004010, +0x00200000,0x04200002,0x04000802,0x00000000, +0x00000800,0x04000802,0x00200802,0x04200800, +0x04200802,0x00200000,0x00000000,0x04000002, +0x00000002,0x04000000,0x04200002,0x00000802, +0x04000800,0x00200802,0x00200002,0x04000800, +0x04000002,0x04200000,0x04200800,0x00200002, +0x04200000,0x00000800,0x00000802,0x04200802, +0x00200800,0x00000002,0x04000000,0x00200800, +0x04000000,0x00200800,0x00200000,0x04000802, +0x04000802,0x04200002,0x04200002,0x00000002, +0x00200002,0x04000000,0x04000800,0x00200000, +0x04200800,0x00000802,0x00200802,0x04200800, +0x00000802,0x04000002,0x04200802,0x04200000, +0x00200800,0x00000000,0x00000002,0x04200802, +0x00000000,0x00200802,0x04200000,0x00000800, +0x04000002,0x04000800,0x00000800,0x00200002, +0x10001040,0x00001000,0x00040000,0x10041040, +0x10000000,0x10001040,0x00000040,0x10000000, +0x00040040,0x10040000,0x10041040,0x00041000, +0x10041000,0x00041040,0x00001000,0x00000040, +0x10040000,0x10000040,0x10001000,0x00001040, +0x00041000,0x00040040,0x10040040,0x10041000, +0x00001040,0x00000000,0x00000000,0x10040040, +0x10000040,0x10001000,0x00041040,0x00040000, +0x00041040,0x00040000,0x10041000,0x00001000, +0x00000040,0x10040040,0x00001000,0x00041040, +0x10001000,0x00000040,0x10000040,0x10040000, +0x10040040,0x10000000,0x00040000,0x10001040, +0x00000000,0x10041040,0x00040040,0x10000040, +0x10040000,0x10001000,0x10001040,0x00000000, +0x10041040,0x00041000,0x00041000,0x00001040, +0x00001040,0x00040040,0x10000000,0x10041000, +}; diff --git a/op25/gr-op25/lib/hdu.cc b/op25/gr-op25/lib/hdu.cc index 737fbb7..dc6902c 100644 --- a/op25/gr-op25/lib/hdu.cc +++ b/op25/gr-op25/lib/hdu.cc @@ -28,6 +28,7 @@ #include #include +#include using namespace std; @@ -65,6 +66,8 @@ hdu::do_correct_errors(bit_vector& frame) { apply_golay_correction(frame); apply_rs_correction(frame); + + if (logging_enabled()) fprintf(stderr, "\n"); } void @@ -136,33 +139,58 @@ hdu::frame_size_max() const return 792; } -string -hdu::algid_str() const +uint8_t +hdu::algid() const { const size_t ALGID_BITS[] = { 356, 357, 360, 361, 374, 375, 376, 377 }; const size_t ALGID_BITS_SZ = sizeof(ALGID_BITS) / sizeof(ALGID_BITS[0]); - uint8_t algid = extract(frame_body(), ALGID_BITS, ALGID_BITS_SZ); - return lookup(algid, ALGIDS, ALGIDS_SZ); + return extract(frame_body(), ALGID_BITS, ALGID_BITS_SZ); } string -hdu::kid_str() const +hdu::algid_str() const +{ + uint8_t _algid = algid(); + return lookup(_algid, ALGIDS, ALGIDS_SZ); +} + +uint16_t +hdu::kid() const { const size_t KID_BITS[] = { 378, 379, 392, 393, 394, 395, 396, 397, 410, 411, 412, 413, 414, 415, 428, 429 }; const size_t KID_BITS_SZ = sizeof(KID_BITS) / sizeof(KID_BITS[0]); - uint16_t kid = extract(frame_body(), KID_BITS, KID_BITS_SZ); + return extract(frame_body(), KID_BITS, KID_BITS_SZ); +} + +string +hdu::kid_str() const +{ + uint16_t _kid = kid(); ostringstream os; - os << hex << showbase << setfill('0') << setw(4) << kid; + os << hex << showbase << setfill('0') << setw(4) << _kid; return os.str(); } std::string hdu::mi_str() const +{ + std::vector _mi(mi()); + ostringstream os; + os << "0x"; + for(size_t i = 0; i < _mi.size(); ++i) { + uint16_t octet = _mi[i]; + os << hex << setfill('0') << setw(2) << octet; + } + return os.str(); +} + +std::vector +hdu::mi() const { const size_t MI_BITS[] = { 114, 115, 116, 117, 118, 119, 132, 133, @@ -177,15 +205,9 @@ hdu::mi_str() const }; const size_t MI_BITS_SZ = sizeof(MI_BITS) / sizeof(MI_BITS[0]); - uint8_t mi[9]; - extract(frame_body(), MI_BITS, MI_BITS_SZ, mi); - ostringstream os; - os << "0x"; - for(size_t i = 0; i < (sizeof(mi) / sizeof(mi[0])); ++i) { - uint16_t octet = mi[i]; - os << hex << setfill('0') << setw(2) << octet; - } - return os.str(); + std::vector _mi(((MI_BITS_SZ + 7) / 8)); + extract(frame_body(), MI_BITS, MI_BITS_SZ, &_mi[0]); + return _mi; } string @@ -219,7 +241,21 @@ hdu::tgid_str() const }; const size_t TGID_BITS_SZ = sizeof(TGID_BITS) / sizeof(TGID_BITS[0]); const uint16_t tgid = extract(frame_body(), TGID_BITS, TGID_BITS_SZ); - ostringstream os; - os << hex << showbase << setfill('0') << setw(4) << tgid; - return os.str(); + // Zero fill isn't working properly in original implementation + //ostringstream os; + //os << hex << showbase << setfill('0') << setw(4) << tgid; + //return os.str(); + return (boost::format("0x%04x") % tgid).str(); +} + +struct CryptoState +hdu::crypto_state() const +{ + struct CryptoState state; + + state.mi = mi(); + state.kid = kid(); + state.algid = algid(); + + return state; } diff --git a/op25/gr-op25/lib/hdu.h b/op25/gr-op25/lib/hdu.h index b7586fa..ef2373a 100644 --- a/op25/gr-op25/lib/hdu.h +++ b/op25/gr-op25/lib/hdu.h @@ -25,11 +25,12 @@ #define INCLUDED_HDU_H #include "abstract_data_unit.h" +#include "crypto.h" /** * P25 header data unit (HDU). */ -class hdu : public abstract_data_unit +class hdu : public abstract_data_unit, public crypto_state_provider { public: @@ -96,7 +97,9 @@ protected: */ virtual uint16_t frame_size_max() const; -private: +public: + + uint8_t algid() const; /** * Return a string describing the encryption algorithm ID (ALGID). @@ -105,6 +108,8 @@ private: */ std::string algid_str() const; + virtual uint16_t kid() const; + /** * Returns a string describing the key id (KID). * @@ -119,6 +124,8 @@ private: */ virtual std::string mfid_str() const; + virtual std::vector mi() const; + /** * Returns a string describing the message indicator (MI). * @@ -139,6 +146,10 @@ private: * \return A string identifying the TGID. */ virtual std::string tgid_str() const; + +public: + + struct CryptoState crypto_state() const; }; #endif /* INCLUDED_HDU_H */ diff --git a/op25/gr-op25/lib/ldu.cc b/op25/gr-op25/lib/ldu.cc new file mode 100644 index 0000000..e400858 --- /dev/null +++ b/op25/gr-op25/lib/ldu.cc @@ -0,0 +1,100 @@ +#include "ldu.h" + +#include +#include +#include +#include + +const static itpp::Mat ham_10_6_3_6("1 1 1 0 0 1 1 0 0 0; 1 1 0 1 0 1 0 1 0 0; 1 0 1 1 1 0 0 0 1 0; 0 1 1 1 1 0 0 0 0 1"); + +typedef std::vector > VecArray; + +ldu::ldu(const_bit_queue& frame_body) : + voice_data_unit(frame_body), + m_hamming_error_count(0) +{ +} + +void +ldu::do_correct_errors(bit_vector& frame_body) +{ + voice_data_unit::do_correct_errors(frame_body); +} + +bool +ldu::process_meta_data(bit_vector& frame_body) +{ + m_hamming_error_count = 0; + + //std::vector lc(30); + //std::vector ham(24); + int lc_bit_idx = 0; + VecArray arrayVec; + itpp::Vec vecRaw(10); // First 6 bits contain data + + for (int i = 400; i < 1360; i += 184) + { + for (int j = 0; j < 40; j++) + { + int x = (i + j) + (((i + j) / 70) * 2); // Adjust bit index for status + unsigned char ch = frame_body[x]; + + //lc[lc_bit_idx / 8] |= (ch << (7 - (lc_bit_idx % 8))); + //ham[lc_bit_idx / 10] = ((ham[lc_bit_idx / 10]) << 1) | ch; + vecRaw(lc_bit_idx % 10) = ch; + + ++lc_bit_idx; + + if ((lc_bit_idx % 10) == 0) + arrayVec.push_back(vecRaw); + } + } + + if (lc_bit_idx != 240) // Not enough bits + { + return false; + } + + if (arrayVec.size() != 24) // Not enough vectors + { + return false; + } + + itpp::Vec vecZero(4); + vecZero.zeros(); + + m_raw_meta_data.clear(); + + for (int i = 0; i < arrayVec.size(); ++i) + { + itpp::Vec& vec = arrayVec[i]; + itpp::bvec vB(itpp::to_bvec(vec)); + + itpp::Vec vS = ham_10_6_3_6 * vec; + for (int i = 0; i < vS.length(); ++i) + vS[i] = vS[i] % 2; + itpp::bvec vb(to_bvec(vS)); + if (itpp::bin2dec(vb) != 0) + { + ++m_hamming_error_count; + } + + m_raw_meta_data = concat(m_raw_meta_data, vB.mid(0, 6)); // Includes RS for last 72 bits + } + + if (logging_enabled()) fprintf(stderr, "%s: %lu hamming errors, %s\n", duid_str().c_str(), m_hamming_error_count, (meta_data_valid() ? "valid" : "invalid")); + + return meta_data_valid(); +} + +const itpp::bvec& +ldu::raw_meta_data() const +{ + return m_raw_meta_data; +} + +bool +ldu::meta_data_valid() const +{ + return (m_raw_meta_data.length() == 144); // Not enough bits after Hamming(10,6,3) +} diff --git a/op25/gr-op25/lib/ldu.h b/op25/gr-op25/lib/ldu.h new file mode 100644 index 0000000..75fb3ef --- /dev/null +++ b/op25/gr-op25/lib/ldu.h @@ -0,0 +1,28 @@ +#ifndef INCLUDED_LDU_H +#define INCLUDED_LDU_H + +#include "voice_data_unit.h" + +class ldu : public voice_data_unit +{ +private: + + size_t m_hamming_error_count; + itpp::bvec m_raw_meta_data; + +protected: + + virtual void do_correct_errors(bit_vector& frame_body); + + virtual bool process_meta_data(bit_vector& frame_body); + + virtual const itpp::bvec& raw_meta_data() const; + +public: + + ldu(const_bit_queue& frame_body); + + virtual bool meta_data_valid() const; +}; + +#endif // INCLUDED_LDU_H diff --git a/op25/gr-op25/lib/ldu1.cc b/op25/gr-op25/lib/ldu1.cc index 52f3b00..5acbf45 100644 --- a/op25/gr-op25/lib/ldu1.cc +++ b/op25/gr-op25/lib/ldu1.cc @@ -23,10 +23,19 @@ #include "ldu1.h" +#include +#include + +#include +#include + +#include "pickle.h" +#include "value_string.h" + using std::string; ldu1::ldu1(const_bit_queue& frame_body) : - voice_data_unit(frame_body) + ldu(frame_body) { } @@ -34,6 +43,64 @@ ldu1::~ldu1() { } +void ldu1::do_correct_errors(bit_vector& frame_body) +{ + ldu::do_correct_errors(frame_body); + + if (!process_meta_data(frame_body)) + return; + + const itpp::bvec& data = raw_meta_data(); + + std::stringstream ss; + + m_meta_data.m.lcf = bin2dec(data.mid(0, 8)); + m_meta_data.m.mfid = bin2dec(data.mid(8, 8)); + ss << (boost::format("%s: LCF: 0x%02x, MFID: 0x%02x") % duid_str() % m_meta_data.m.lcf % m_meta_data.m.mfid); + if (m_meta_data.m.lcf == 0x00) + { + m_meta_data.m0.emergency = data[16]; + m_meta_data.m0.reserved = bin2dec(data.mid(17, 15)); + m_meta_data.m0.tgid = bin2dec(data.mid(32, 16)); + m_meta_data.m0.source = bin2dec(data.mid(48, 24)); + ss << (boost::format(", Emergency: 0x%02x, Reserved: 0x%04x, TGID: 0x%04x, Source: 0x%06x") % m_meta_data.m0.emergency % m_meta_data.m0.reserved % m_meta_data.m0.tgid % m_meta_data.m0.source); + } + else if (m_meta_data.m.lcf == 0x03) + { + m_meta_data.m3.reserved = bin2dec(data.mid(16, 8)); + m_meta_data.m3.destination = bin2dec(data.mid(24, 24)); + m_meta_data.m3.source = bin2dec(data.mid(48, 24)); + ss << (boost::format(", Reserved: 0x%02x, Destination: 0x%06x, Source: 0x%06x") % m_meta_data.m3.reserved % m_meta_data.m3.destination % m_meta_data.m3.source); + } + else + { + ss << " (unknown LCF)"; + } + + if (logging_enabled()) std::cerr << ss.str() << std::endl; +} + +std::string +ldu1::snapshot() const +{ + pickle p; + p.add("duid", duid_str()); + p.add("mfid", lookup(m_meta_data.m.mfid, MFIDS, MFIDS_SZ)); + if ((m_meta_data.m.lcf == 0x00) || (m_meta_data.m.lcf == 0x03)) + p.add("source", (boost::format("0x%06x") % m_meta_data.m0.source).str()); + if (m_meta_data.m.lcf == 0x00) + p.add("tgid", (boost::format("0x%04x") % m_meta_data.m0.tgid).str()); + if (m_meta_data.m.lcf == 0x03) + p.add("dest", (boost::format("0x%06x") % m_meta_data.m3.destination).str()); + return p.to_string(); +} + +ldu1::combined_meta_data +ldu1::meta_data() const +{ + return m_meta_data; +} + string ldu1::duid_str() const { diff --git a/op25/gr-op25/lib/ldu1.h b/op25/gr-op25/lib/ldu1.h index b7baf55..9dce260 100644 --- a/op25/gr-op25/lib/ldu1.h +++ b/op25/gr-op25/lib/ldu1.h @@ -24,13 +24,45 @@ #ifndef INCLUDED_LDU1_H #define INCLUDED_LDU1_H -#include "voice_data_unit.h" +#include "ldu.h" /** * P25 Logical Data Unit 1. */ -class ldu1 : public voice_data_unit +class ldu1 : public ldu { +protected: + + void do_correct_errors(bit_vector& frame_body); + +public: + + struct base_meta_data + { + unsigned char lcf; + unsigned char mfid; + unsigned int source; + unsigned short reserved; + }; + + struct meta_data_0 : public base_meta_data + { + bool emergency; + unsigned short tgid; + }; + + struct meta_data_3 : public base_meta_data + { + unsigned int destination; + }; + + union combined_meta_data + { + base_meta_data m; + meta_data_0 m0; + meta_data_3 m3; + } m_meta_data; + public: /** @@ -50,6 +82,10 @@ public: */ std::string duid_str() const; + virtual std::string snapshot() const; + + combined_meta_data meta_data() const; + }; #endif /* INCLUDED_LDU1_H */ diff --git a/op25/gr-op25/lib/ldu2.cc b/op25/gr-op25/lib/ldu2.cc index c3e9932..76cfe1a 100644 --- a/op25/gr-op25/lib/ldu2.cc +++ b/op25/gr-op25/lib/ldu2.cc @@ -23,10 +23,18 @@ #include "ldu2.h" +#include +#include + +#include + +#include "pickle.h" +#include "value_string.h" + using std::string; ldu2::ldu2(const_bit_queue& frame_body) : - voice_data_unit(frame_body) + ldu(frame_body) { } @@ -39,3 +47,43 @@ ldu2::duid_str() const { return string("LDU2"); } + +std::string +ldu2::snapshot() const +{ + pickle p; + p.add("duid", duid_str()); + std::stringstream ss; + ss << "0x"; + for (size_t n = 0; n < m_crypto_state.mi.size(); ++n) + ss << (boost::format("%02x") % (int)m_crypto_state.mi[n]); + p.add("mi", ss.str()); + p.add("algid", lookup(m_crypto_state.algid, ALGIDS, ALGIDS_SZ)); + p.add("kid", (boost::format("0x%04x") % m_crypto_state.kid).str()); + return p.to_string(); +} + +void +ldu2::do_correct_errors(bit_vector& frame_body) +{ + ldu::do_correct_errors(frame_body); + + if (!process_meta_data(frame_body)) + return; + + const itpp::bvec& data = raw_meta_data(); + + for (int i = 0; i < 72; i += 8) + { + m_crypto_state.mi[i/8] = bin2dec(data.mid(i, 8)); + } + + m_crypto_state.algid = bin2dec(data.mid(72, 8)); + m_crypto_state.kid = bin2dec(data.mid(80, 16)); +} + +struct CryptoState +ldu2::crypto_state() const +{ + return m_crypto_state; +} diff --git a/op25/gr-op25/lib/ldu2.h b/op25/gr-op25/lib/ldu2.h index d1e62b7..ce3deef 100644 --- a/op25/gr-op25/lib/ldu2.h +++ b/op25/gr-op25/lib/ldu2.h @@ -24,13 +24,22 @@ #ifndef INCLUDED_LDU2_H #define INCLUDED_LDU2_H -#include "voice_data_unit.h" +#include "ldu.h" +#include "crypto.h" /** * P25 Logical Data Unit 2. */ -class ldu2 : public voice_data_unit +class ldu2 : public ldu, public crypto_state_provider { +private: + + struct CryptoState m_crypto_state; + +protected: + + void do_correct_errors(bit_vector& frame_body); + public: /** @@ -49,6 +58,10 @@ public: * Returns a string describing the Data Unit ID (DUID). */ std::string duid_str() const; + + virtual std::string snapshot() const; + + struct CryptoState crypto_state() const; }; #endif /* INCLUDED_LDU2_H */ diff --git a/op25/gr-op25/lib/op25.i b/op25/gr-op25/lib/op25.i deleted file mode 100644 index 5c4716e..0000000 --- a/op25/gr-op25/lib/op25.i +++ /dev/null @@ -1,113 +0,0 @@ -/* -*- C++ -*- */ - -%feature("autodoc", "1"); - -%{ -#include -%} - -%include "exception.i" -%import "gnuradio.i" - -%{ -#include "gnuradio/swig/gnuradio_swig_bug_workaround.h" -#include "op25_fsk4_demod_ff.h" -#include "op25_fsk4_slicer_fb.h" -#include "op25_decoder_bf.h" -#include "op25_pcap_source_b.h" -%} - -// ---------------------------------------------------------------- - -/* - * This does some behind-the-scenes magic so we can - * access fsk4_square_ff from python as fsk4.square_ff - */ -GR_SWIG_BLOCK_MAGIC(op25, fsk4_demod_ff); - -/* - * Publicly-accesible default constuctor function for op25_fsk4_demod_bf. - */ -op25_fsk4_demod_ff_sptr op25_make_fsk4_demod_ff(gr::msg_queue::sptr queue, float sample_rate, float symbol_rate); - -class op25_fsk4_demod_ff : public gr_block -{ -private: - op25_fsk4_demod_ff(gr::msg_queue::sptr queue, float sample_rate, float symbol_rate); -}; - -// ---------------------------------------------------------------- - -/* - * This does some behind-the-scenes magic so we can invoke - * op25_make_slicer_fb from python as op25.slicer_fbf. - */ -GR_SWIG_BLOCK_MAGIC(op25, fsk4_slicer_fb); - -/* - * Publicly-accesible default constuctor function for op25_decoder_bf. - */ -op25_fsk4_slicer_fb_sptr op25_make_fsk4_slicer_fb(const std::vector &slice_levels); - -/* - * The op25_fsk4_slicer block. Takes a series of float samples and - * partitions them into dibit symbols according to the slices_levels - * provided to the constructor. - */ -class op25_fsk4_slicer_fb : public gr_sync_block -{ -private: - op25_fsk4_slicer_fb (const std::vector &slice_levels); -}; - -// ---------------------------------------------------------------- - -/* - * This does some behind-the-scenes magic so we can invoke - * op25_make_decoder_bsf from python as op25.decoder_bf. - */ -GR_SWIG_BLOCK_MAGIC(op25, decoder_bf); - -/* - * Publicly-accesible default constuctor function for op25_decoder_bf. - */ -op25_decoder_bf_sptr op25_make_decoder_bf(); - -/** - * The op25_decoder_bf block. Accepts a stream of dibit symbols and - * produces an 8KS/s audio stream. - */ -class op25_decoder_bf : public gr_block -{ -private: - op25_decoder_bf(); -public: - const char *destination() const; - gr::msg_queue::sptr get_msgq() const; - void set_msgq(gr::msg_queue::sptr msgq); -}; - -// ---------------------------------------------------------------- - -/* - * This does some behind-the-scenes magic so we can invoke - * op25_make_pcap_source_b from python as op25.pcap_source_b. - */ -GR_SWIG_BLOCK_MAGIC(op25, pcap_source_b); - -/* - * Publicly-accesible constuctor function for op25_pcap_source. - */ -op25_pcap_source_b_sptr op25_make_pcap_source_b(const char *path, float delay); - -/* - * The op25_pcap_source block. Reads symbols from a tcpdump-formatted - * file and produces a stream of symbols of the appropriate size. - */ -class op25_pcap_source_b : public gr_sync_block -{ -private: - op25_pcap_source_b(const char *path, float delay); -}; - -// ---------------------------------------------------------------- diff --git a/op25/gr-op25/lib/op25_imbe_frame.h b/op25/gr-op25/lib/op25_imbe_frame.h index fa00616..2ce6acc 100644 --- a/op25/gr-op25/lib/op25_imbe_frame.h +++ b/op25/gr-op25/lib/op25_imbe_frame.h @@ -251,8 +251,8 @@ pngen23(uint32_t& Pr) * \param u0-u7 Result output vectors */ -static inline void -imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_t& u2, uint32_t& u3, uint32_t& u4, uint32_t& u5, uint32_t& u6, uint32_t& u7, uint32_t& E0, uint32_t& ET) +static inline size_t +imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_t& u2, uint32_t& u3, uint32_t& u4, uint32_t& u5, uint32_t& u6, uint32_t& u7, uint32_t& E0, uint32_t& ET, bool bot_shift = true) { ET = 0; @@ -294,7 +294,10 @@ imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_ u6 = v6; u7 = extract(cw, 137, 144); - u7 <<= 1; /* so that bit0 is free (see note about BOT bit */ + if (bot_shift) + u7 <<= 1; /* so that bit0 is free (see note about BOT bit */ + + return errs; } /* APCO IMBE header encoder. diff --git a/op25/gr-op25/lib/voice_data_unit.cc b/op25/gr-op25/lib/voice_data_unit.cc index c15eb1c..7bdfa60 100644 --- a/op25/gr-op25/lib/voice_data_unit.cc +++ b/op25/gr-op25/lib/voice_data_unit.cc @@ -24,32 +24,324 @@ #include "voice_data_unit.h" #include "op25_imbe_frame.h" -#include +#include +#include +#include + +#include +#include +#include +#include using namespace std; -voice_data_unit::~voice_data_unit() +static void vec_mod(itpp::ivec& vec, int modulus = 2) { + for (int i = 0; i < vec.length(); ++i) + vec[i] = vec[i] % modulus; } +class cyclic_16_8_5_syndromes +{ +public: + typedef map SyndromeTableMap; + + const static itpp::imat cyclic_16_8_5; +private: + SyndromeTableMap m_syndrome_table; +public: + inline const SyndromeTableMap table() const + { + return m_syndrome_table; + } + + cyclic_16_8_5_syndromes(bool generate_now = false) + { + if (generate_now) + generate(); + } + + int generate() + { + if (m_syndrome_table.empty() == false) + return -1; + + // n=16, k=8 + + // E1 + itpp::ivec v("1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"); + itpp::ivec r(cyclic_16_8_5 * v); + vec_mod(r); + itpp::bvec b(to_bvec(r)); + unsigned char ch = (unsigned char)bin2dec(b); + itpp::bvec bV(to_bvec(v)); + unsigned short us = (unsigned short)bin2dec(bV); + m_syndrome_table.insert(make_pair(ch, us)); + + // E2 + for (int i = 0; i <= (16 - 2); ++i) + { + itpp::ivec v2(v); + v2[15-i] = 1; + r = cyclic_16_8_5 * v2; + bV = itpp::to_bvec(v2); + + vec_mod(r); + b = itpp::to_bvec(r); + unsigned char ch = (unsigned char)itpp::bin2dec(b); + unsigned short us = (unsigned short)itpp::bin2dec(bV); + m_syndrome_table.insert(make_pair(ch, us)); + } + + // E3 - disabled: min.d = 5, t=floor(5/2)=2 + /*for (int i = 0; i <= (16 - 2); ++i) + { + for (int j = 0; j < i; ++j) + { + ivec v3(v); + v3[15-i] = 1; + v3[15-j] = 1; + r = cyclic_16_8_5 * v3; + bV = to_bvec(v3); + + vec_mod(r); + b = to_bvec(r); + unsigned char ch = (unsigned char)bin2dec(b); + unsigned short us = (unsigned short)bin2dec(bV); + m_syndrome_table.insert(make_pair(ch, us)); + } + }*/ + + return m_syndrome_table.size(); + } +}; + +const itpp::imat cyclic_16_8_5_syndromes::cyclic_16_8_5( +"0 0 1 1 1 1 0 0 1 0 0 0 0 0 0 0;" +"1 0 0 1 1 1 1 0 0 1 0 0 0 0 0 0;" +"0 1 0 0 1 1 1 1 0 0 1 0 0 0 0 0;" +"0 0 0 1 1 0 1 1 0 0 0 1 0 0 0 0;" +"1 0 1 1 0 0 0 1 0 0 0 0 1 0 0 0;" +"1 1 1 0 0 1 0 0 0 0 0 0 0 1 0 0;" +"1 1 1 1 0 0 1 0 0 0 0 0 0 0 1 0;" +"0 1 1 1 1 0 0 1 0 0 0 0 0 0 0 1" +); + +static cyclic_16_8_5_syndromes g_cyclic_16_8_5_syndromes(true); + +static int decode_cyclic_16_8_5(const itpp::ivec& vec, itpp::ivec& out) +{ + itpp::ivec vc(cyclic_16_8_5_syndromes::cyclic_16_8_5 * vec); + vec_mod(vc); + itpp::bvec vb(to_bvec(vc)); + + unsigned char ch = (unsigned char)itpp::bin2dec(vb); + if (ch == 0x00) + return 0; + + const cyclic_16_8_5_syndromes::SyndromeTableMap& syndrome_table = g_cyclic_16_8_5_syndromes.table(); + cyclic_16_8_5_syndromes::SyndromeTableMap::const_iterator it = syndrome_table.find(ch); + int j = 0; + while (it == syndrome_table.end()) + { + ++j; + vc = itpp::concat(itpp::ivec("0 0 0 0 0 0 0 0"), vc); // Restore to 16 bits + vc.shift_left(vc[0]); // Rotate (s * x) + vc = cyclic_16_8_5_syndromes::cyclic_16_8_5 * vc; + vec_mod(vc); + vb = itpp::to_bvec(vc); + ch = (unsigned char)itpp::bin2dec(vb); + it = syndrome_table.find(ch); + + if (j >= 15) + break; + } + + if (it == syndrome_table.end()) + { + return -1; + } + + unsigned short us = it->second; + itpp::bvec es(itpp::dec2bin(16, us)); + if (j > 0) + es.shift_right(es.mid(16-j, j)); // e + vb = itpp::to_bvec(vec); + vb -= es; + out = itpp::to_ivec(vb); + + vc = cyclic_16_8_5_syndromes::cyclic_16_8_5 * out; + vec_mod(vc); + vb = itpp::to_bvec(vc); + if (itpp::bin2dec(vb) != 0x00) + { + return -1; + } + + return 1; +} + +static int decode_cyclic_16_8_5(itpp::ivec& vec) +{ + return decode_cyclic_16_8_5(vec, vec); +} + +//////////////////////////////////////////////////////////////////////////////////// + voice_data_unit::voice_data_unit(const_bit_queue& frame_body) : - abstract_data_unit(frame_body) + abstract_data_unit(frame_body), + d_lsdw(0), + d_lsdw_valid(false) +{ + memset(d_lsd_byte_valid, 0x00, sizeof(d_lsd_byte_valid)); +} + +voice_data_unit::~voice_data_unit() { } void voice_data_unit::do_correct_errors(bit_vector& frame_body) { + if (logging_enabled()) fprintf(stderr, "\n"); + + d_lsd_byte_valid[0] = d_lsd_byte_valid[1] = false; + d_lsdw_valid = false; + + itpp::ivec lsd1(16), lsd2(16); + + for (int i = 0; i < 32; ++i) + { + int x = 1504 + i; + x = x + ((x / 70) * 2); // Adjust bit index for status + if (i < 16) + lsd1[i] = frame_body[x]; + else + lsd2[i-16] = frame_body[x]; + } + + int iDecode1 = decode_cyclic_16_8_5(lsd1); + if (iDecode1 >= 0) + { + d_lsd_byte_valid[0] = true; + } + else if (iDecode1 == -1) + { + // Error + } + int iDecode2 = decode_cyclic_16_8_5(lsd2); + if (iDecode2 >= 0) + { + d_lsd_byte_valid[1] = true; + } + else + { + // Error + } + + d_lsdw = 0; + for (int i = 0; i < 8; ++i) + d_lsdw = d_lsdw | (lsd1[i] << (7 - i)); // Little-endian byte swap + for (int i = 0; i < 8; ++i) + d_lsdw = d_lsdw | (lsd2[i] << (15 - i)); // Little-endian byte swap + + if (d_lsd_byte_valid[0] && d_lsd_byte_valid[1]) + d_lsdw_valid = true; +} + +uint16_t +voice_data_unit::lsdw() const +{ + return d_lsdw; +} + +bool +voice_data_unit::lsdw_valid() const +{ + return d_lsdw_valid; +} + +static void extract(unsigned int u, size_t n, std::vector& out) +{ + for (size_t i = 0; i < n; ++i) + out.push_back(((u & (1 << (n-1-i))) != 0)); } void -voice_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe) +voice_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod) { voice_codeword cw(voice_codeword_sz); for(size_t i = 0; i < nof_voice_codewords; ++i) { imbe_deinterleave(frame_body, cw, i); + + unsigned int u0 = 0; + unsigned int u1,u2,u3,u4,u5,u6,u7; + unsigned int E0 = 0; + unsigned int ET = 0; + + // PN/Hamming/Golay - etc. + size_t errs = imbe_header_decode(cw, u0, u1, u2, u3, u4, u5, u6, u7, E0, ET, false); // E0 & ET are not used, and are always returned as 0 + + crypto_algorithm::sptr algorithm; + if (crypto_mod) + algorithm = crypto_mod->current_algorithm(); + + if (algorithm) + { + if (i == 8) + { + d_lsdw ^= algorithm->generate(16); // LSDW + } + + u0 ^= (int)algorithm->generate(12); + u1 ^= (int)algorithm->generate(12); + u2 ^= (int)algorithm->generate(12); + u3 ^= (int)algorithm->generate(12); + + u4 ^= (int)algorithm->generate(11); + u5 ^= (int)algorithm->generate(11); + u6 ^= (int)algorithm->generate(11); + + u7 ^= (int)algorithm->generate(7); + + imbe_header_encode(cw, u0, u1, u2, u3, u4, u5, u6, (u7 << 1)); + } + + std::vector cw_raw; + extract(u0, 12, cw_raw); + extract(u1, 12, cw_raw); + extract(u2, 12, cw_raw); + extract(u3, 12, cw_raw); + extract(u4, 11, cw_raw); + extract(u5, 11, cw_raw); + extract(u6, 11, cw_raw); + extract(u7, 7, cw_raw); + + const int cw_octets = 11; + + std::vector cw_vector(cw_octets); + extract(cw_raw, 0, (cw_octets * 8), &cw_vector[0]); + + if (logging_enabled()) + { + std::stringstream ss; + for (size_t n = 0; n < cw_vector.size(); ++n) + { + ss << (boost::format("%02x") % (int)cw_vector[n]); + if (n < (cw_vector.size() - 1)) + ss << " "; + } + + if (errs > 0) + ss << (boost::format(" (%llu errors)") % errs); + + std:cerr << (boost::format("%s:\t%s") % duid_str() % ss.str()) << std::endl; + } + imbe.decode(cw); } + + if (logging_enabled()) fprintf(stderr, "%s: LSDW: 0x%04x, %s\n", duid_str().c_str(), d_lsdw, (d_lsdw_valid ? "valid" : "invalid")); } uint16_t diff --git a/op25/gr-op25/lib/voice_data_unit.h b/op25/gr-op25/lib/voice_data_unit.h index f57b365..6d49485 100644 --- a/op25/gr-op25/lib/voice_data_unit.h +++ b/op25/gr-op25/lib/voice_data_unit.h @@ -38,6 +38,17 @@ public: */ virtual ~voice_data_unit(); + const static int LSD_BYTE_COUNT=2; + +private: + + union { + uint8_t d_lsd_byte[LSD_BYTE_COUNT]; + uint16_t d_lsdw; + }; + bool d_lsdw_valid; + bool d_lsd_byte_valid[LSD_BYTE_COUNT]; + protected: /** @@ -63,7 +74,7 @@ protected: * \param frame_body The const_bit_vector to decode. * \param imbe The imbe_decoder to use to generate the audio. */ - virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe); + virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod); /** * Returns the expected size (in bits) of this data_unit. For @@ -74,6 +85,10 @@ protected: */ virtual uint16_t frame_size_max() const; + virtual uint16_t lsdw() const; + + virtual bool lsdw_valid() const; + }; #endif /* INCLUDED_VOICE_DATA_UNIT_H */ diff --git a/op25/gr-op25/lib/voice_du_handler.cc b/op25/gr-op25/lib/voice_du_handler.cc index 3b52460..cd967bf 100644 --- a/op25/gr-op25/lib/voice_du_handler.cc +++ b/op25/gr-op25/lib/voice_du_handler.cc @@ -28,9 +28,10 @@ using namespace std; -voice_du_handler::voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder) : +voice_du_handler::voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder, crypto_module::sptr crypto_mod) : data_unit_handler(next), - d_decoder(decoder) + d_decoder(decoder), + d_crypto_mod(crypto_mod) { } @@ -42,6 +43,6 @@ voice_du_handler::~voice_du_handler() void voice_du_handler::handle(data_unit_sptr du) { - du->decode_audio(*d_decoder); + du->decode_audio(*d_decoder, d_crypto_mod); data_unit_handler::handle(du); } diff --git a/op25/gr-op25/lib/voice_du_handler.h b/op25/gr-op25/lib/voice_du_handler.h index 9278ddd..6a4f3a1 100644 --- a/op25/gr-op25/lib/voice_du_handler.h +++ b/op25/gr-op25/lib/voice_du_handler.h @@ -26,6 +26,7 @@ #include "data_unit_handler.h" #include "imbe_decoder.h" +#include "crypto.h" #include @@ -43,7 +44,7 @@ public: * \param next The next data_unit_handler in the chain. * \param decoder An imbe_decoder_sptr to the IMBE decoder to use. */ - voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder); + voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder, crypto_module::sptr crypto_mod = crypto_module::sptr()); // TODO: Add capability to decoder_ff (remove default argument) /** * voice_du_handler virtual destructor. @@ -64,6 +65,8 @@ private: */ imbe_decoder_sptr d_decoder; + crypto_module::sptr d_crypto_mod; + }; #endif /* INCLUDED_VOICE_DU_HANDLER_H */ diff --git a/op25/gr-op25/swig/op25_swig.i b/op25/gr-op25/swig/op25_swig.i index 0762474..bd654d5 100644 --- a/op25/gr-op25/swig/op25_swig.i +++ b/op25/gr-op25/swig/op25_swig.i @@ -1,5 +1,7 @@ /* -*- c++ -*- */ +%include "pycontainer.swg" + #define OP25_API %include "gnuradio.i" // the common stuff @@ -15,6 +17,11 @@ #include "op25/pcap_source_b.h" %} +%template(key_type) std::vector; +// This causes SWIG to segfault +//%template(key_map_type) std::map; +%template(key_map_type) std::map >; + %include "op25/fsk4_demod_ff.h" GR_SWIG_BLOCK_MAGIC2(op25, fsk4_demod_ff); %include "op25/fsk4_slicer_fb.h" diff --git a/op25/gr-op25_repeater/CMakeLists.txt b/op25/gr-op25_repeater/CMakeLists.txt index c3859be..9962ba3 100644 --- a/op25/gr-op25_repeater/CMakeLists.txt +++ b/op25/gr-op25_repeater/CMakeLists.txt @@ -83,7 +83,6 @@ set(GRC_BLOCKS_DIR ${GR_PKG_DATA_DIR}/grc/blocks) ######################################################################## # Find gnuradio build dependencies ######################################################################## -find_package(GnuradioRuntime) find_package(CppUnit) # To run a more advanced search for GNU Radio and it's components and @@ -91,8 +90,9 @@ find_package(CppUnit) # of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the # minimum API compatible version required. # -# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...) +set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT) # find_package(Gnuradio "version") +find_package(Gnuradio) if(NOT GNURADIO_RUNTIME_FOUND) message(FATAL_ERROR "GnuRadio Runtime required to compile op25_repeater") From e2cf7087fcd4ff6c1db3b534bf646557dc7530de Mon Sep 17 00:00:00 2001 From: Balint Seeber Date: Tue, 24 Nov 2015 23:18:12 -0800 Subject: [PATCH 002/102] Fixes for compilation under Linux --- op25/gr-op25/lib/crypto.cc | 4 +++- op25/gr-op25/lib/crypto.h | 1 + op25/gr-op25/lib/crypto_module_du_handler.cc | 1 + op25/gr-op25/lib/hdu.cc | 1 + op25/gr-op25/lib/ldu.cc | 2 ++ op25/gr-op25/lib/voice_data_unit.cc | 1 + 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/op25/gr-op25/lib/crypto.cc b/op25/gr-op25/lib/crypto.cc index 2d528bb..c67770c 100644 --- a/op25/gr-op25/lib/crypto.cc +++ b/op25/gr-op25/lib/crypto.cc @@ -3,6 +3,8 @@ #include #include #include +#include +#include extern "C" { #include "des.h" @@ -182,7 +184,7 @@ public: generate(3 * 8); // Use remaining 3 bytes for LC } - unsigned long long generate(size_t count) // 1..64 + uint64_t generate(size_t count) // 1..64 { unsigned long long ullCurrent = swap_bytes(m_ks); const int max_len = 64; diff --git a/op25/gr-op25/lib/crypto.h b/op25/gr-op25/lib/crypto.h index 3180f5b..75785b6 100644 --- a/op25/gr-op25/lib/crypto.h +++ b/op25/gr-op25/lib/crypto.h @@ -1,6 +1,7 @@ #ifndef INCLUDED_CRYPTO_H #define INCLUDED_CRYPTO_H +#include #include #include #include diff --git a/op25/gr-op25/lib/crypto_module_du_handler.cc b/op25/gr-op25/lib/crypto_module_du_handler.cc index 90ac06c..b8baf0c 100644 --- a/op25/gr-op25/lib/crypto_module_du_handler.cc +++ b/op25/gr-op25/lib/crypto_module_du_handler.cc @@ -4,6 +4,7 @@ #include #include +#include crypto_module_du_handler::crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod) : data_unit_handler(next) diff --git a/op25/gr-op25/lib/hdu.cc b/op25/gr-op25/lib/hdu.cc index dc6902c..ae89dc1 100644 --- a/op25/gr-op25/lib/hdu.cc +++ b/op25/gr-op25/lib/hdu.cc @@ -29,6 +29,7 @@ #include #include #include +#include using namespace std; diff --git a/op25/gr-op25/lib/ldu.cc b/op25/gr-op25/lib/ldu.cc index e400858..40e6870 100644 --- a/op25/gr-op25/lib/ldu.cc +++ b/op25/gr-op25/lib/ldu.cc @@ -1,5 +1,7 @@ #include "ldu.h" +#include + #include #include #include diff --git a/op25/gr-op25/lib/voice_data_unit.cc b/op25/gr-op25/lib/voice_data_unit.cc index 7bdfa60..56462b3 100644 --- a/op25/gr-op25/lib/voice_data_unit.cc +++ b/op25/gr-op25/lib/voice_data_unit.cc @@ -27,6 +27,7 @@ #include #include #include +#include #include #include From 9f6c73e280966d32a7e94fee8b839a88675ada6a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 2 Nov 2017 20:28:34 -0400 Subject: [PATCH 003/102] build type debug --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c4f45da..54955d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 2.6) project(gr-op25 CXX C) +set(CMAKE_BUILD_TYPE Debug) + add_subdirectory(op25/gr-op25) add_subdirectory(op25/gr-op25_repeater) From 9858e0cecccfbfb7d46e23da6e71a87de41fe05c Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 2 Nov 2017 20:31:01 -0400 Subject: [PATCH 004/102] dstar alt interleave --- op25/gr-op25_repeater/lib/ambe_encoder.cc | 3 ++- op25/gr-op25_repeater/lib/ambe_encoder.h | 4 +++- op25/gr-op25_repeater/lib/p25p2_vf.cc | 16 ++++++++++++---- op25/gr-op25_repeater/lib/p25p2_vf.h | 4 ++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/op25/gr-op25_repeater/lib/ambe_encoder.cc b/op25/gr-op25_repeater/lib/ambe_encoder.cc index b134294..60465d1 100644 --- a/op25/gr-op25_repeater/lib/ambe_encoder.cc +++ b/op25/gr-op25_repeater/lib/ambe_encoder.cc @@ -545,6 +545,7 @@ static void encode_49bit(uint8_t outp[49], const int b[9]) { ambe_encoder::ambe_encoder(void) : d_49bit_mode(false), d_dstar_mode(false), + d_alt_dstar_interleave(false), d_gain_adjust(0) { mbe_parms enh_mp; @@ -579,7 +580,7 @@ void ambe_encoder::encode(int16_t samples[], uint8_t codeword[]) encode_ambe(vocoder.param(), b, &cur_mp, &prev_mp, d_dstar_mode, d_gain_adjust); if (d_dstar_mode) { - interleaver.encode_dstar(codeword, b); + interleaver.encode_dstar(codeword, b, d_alt_dstar_interleave); } else if (d_49bit_mode) { encode_49bit(codeword, b); } else { diff --git a/op25/gr-op25_repeater/lib/ambe_encoder.h b/op25/gr-op25_repeater/lib/ambe_encoder.h index 0d5f1a1..0b2706f 100644 --- a/op25/gr-op25_repeater/lib/ambe_encoder.h +++ b/op25/gr-op25_repeater/lib/ambe_encoder.h @@ -28,7 +28,8 @@ public: ambe_encoder(void); void set_49bit_mode(void); void set_dstar_mode(void); - void set_gain_adjust(float gain_adjust) {d_gain_adjust = gain_adjust;} + void set_gain_adjust(const float gain_adjust) {d_gain_adjust = gain_adjust;} + void set_alt_dstar_interleave(const bool v) { d_alt_dstar_interleave = v; } private: imbe_vocoder vocoder; p25p2_vf interleaver; @@ -37,6 +38,7 @@ private: bool d_49bit_mode; bool d_dstar_mode; float d_gain_adjust; + bool d_alt_dstar_interleave; }; #endif /* INCLUDED_AMBE_ENCODER_H */ diff --git a/op25/gr-op25_repeater/lib/p25p2_vf.cc b/op25/gr-op25_repeater/lib/p25p2_vf.cc index cab6f7d..e593ea7 100644 --- a/op25/gr-op25_repeater/lib/p25p2_vf.cc +++ b/op25/gr-op25_repeater/lib/p25p2_vf.cc @@ -817,9 +817,11 @@ static const int m_list[] = {0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 17, 18, 19, 20, 2 static const int d_list[] = {7, 1, 11, 21, 31, 25, 35, 45, 55, 49, 59, 69, 6, 0, 10, 20, 30, 24, 34, 44, 54, 48, 58, 68, 5, 15, 9, 19, 29, 39, 33, 43, 53, 63, 57, 67, 4, 14, 8, 18, 28, 38, 32, 42, 52, 62, 56, 66, 3, 13, 23, 17, 27, 37, 47, 41, 51, 61, 71, 65, 2, 12, 22, 16, 26, 36, 46, 40, 50, 60, 70, 64}; +static const int alt_d_list[] = {0, 12, 24, 36, 48, 60, 1, 13, 25, 37, 49, 61, 2, 14, 26, 38, 50, 62, 3, 15, 27, 39, 51, 63, 4, 16, 28, 40, 52, 64, 5, 17, 29, 41, 53, 65, 6, 18, 30, 42, 54, 66, 7, 19, 31, 43, 55, 67, 8, 20, 32, 44, 56, 68, 9, 21, 33, 45, 57, 69, 10, 22, 34, 46, 58, 70, 11, 23, 35, 47, 59, 71}; + static const int b_lengths[] = {7,4,6,9,7,4,4,4,3}; -void p25p2_vf::encode_dstar(uint8_t result[72], const int b[9]) { +void p25p2_vf::encode_dstar(uint8_t result[72], const int b[9], bool alt_dstar_interleave) { uint8_t pbuf[48]; uint8_t tbuf[48]; @@ -842,15 +844,21 @@ void p25p2_vf::encode_dstar(uint8_t result[72], const int b[9]) { store_reg(c1, pre_buf+24, 24); memcpy(pre_buf+48, pbuf+24, 24); for (int i=0; i < 72; i++) - result[d_list[i]] = pre_buf[i]; + if (alt_dstar_interleave) + result[i] = pre_buf[alt_d_list[i]]; + else + result[d_list[i]] = pre_buf[i]; } -void p25p2_vf::decode_dstar(const uint8_t codeword[72], int b[9]) { +void p25p2_vf::decode_dstar(const uint8_t codeword[72], int b[9], bool alt_dstar_interleave) { uint8_t pre_buf[72]; uint8_t post_buf[48]; uint8_t tbuf[48]; for (int i=0; i < 72; i++) - pre_buf[i] = codeword[d_list[i]]; + if (alt_dstar_interleave) + pre_buf[alt_d_list[i]] = codeword[i]; + else + pre_buf[i] = codeword[d_list[i]]; uint32_t c0 = load_reg(pre_buf, 24); uint32_t c1 = load_reg(pre_buf+24, 24); diff --git a/op25/gr-op25_repeater/lib/p25p2_vf.h b/op25/gr-op25_repeater/lib/p25p2_vf.h index b86d500..9b9b63e 100644 --- a/op25/gr-op25_repeater/lib/p25p2_vf.h +++ b/op25/gr-op25_repeater/lib/p25p2_vf.h @@ -26,8 +26,8 @@ class p25p2_vf { public: void process_vcw(const uint8_t vf[], int* b); void encode_vcw(uint8_t vf[], const int* b); - void encode_dstar(uint8_t result[72], const int b[9]); - void decode_dstar(const uint8_t codeword[72], int b[9]); + void encode_dstar(uint8_t result[72], const int b[9], bool alt_dstar_interleave); + void decode_dstar(const uint8_t codeword[72], int b[9], bool alt_dstar_interleave); private: void extract_vcw(const uint8_t _vf[], int& _c0, int& _c1, int& _c2, int& _c3); void interleave_vcw(uint8_t _vf[], int _c0, int _c1, int _c2, int _c3); From e2cfbe9d1ebe0db2d80707219db3f0f88152de06 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 2 Nov 2017 20:39:23 -0400 Subject: [PATCH 005/102] dstar gain --- op25/gr-op25_repeater/lib/ambe.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/lib/ambe.c b/op25/gr-op25_repeater/lib/ambe.c index aa43fb2..0d0449e 100644 --- a/op25/gr-op25_repeater/lib/ambe.c +++ b/op25/gr-op25_repeater/lib/ambe.c @@ -140,7 +140,7 @@ mbe_dequantizeAmbeParms (mbe_parms * cur_mp, mbe_parms * prev_mp, const int *b, #endif if (dstar) { deltaGamma = AmbePlusDg[b2]; - cur_mp->gamma = deltaGamma + ((float) 1.0 * prev_mp->gamma); + cur_mp->gamma = deltaGamma + ((float) 0.5 * prev_mp->gamma); } else { deltaGamma = AmbeDg[b2]; cur_mp->gamma = deltaGamma + ((float) 0.5 * prev_mp->gamma); From a3cab238b505fb4f06951a7826bce40d0d437f6b Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 7 Nov 2017 10:41:03 -0500 Subject: [PATCH 006/102] d2460 --- op25/gr-op25_repeater/lib/CMakeLists.txt | 17 ++ op25/gr-op25_repeater/lib/d2460.cc | 360 +++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 op25/gr-op25_repeater/lib/d2460.cc diff --git a/op25/gr-op25_repeater/lib/CMakeLists.txt b/op25/gr-op25_repeater/lib/CMakeLists.txt index d35a5b4..361f266 100644 --- a/op25/gr-op25_repeater/lib/CMakeLists.txt +++ b/op25/gr-op25_repeater/lib/CMakeLists.txt @@ -97,4 +97,21 @@ target_link_libraries( GR_ADD_TEST(test_op25_repeater test-op25_repeater) +######################################################################## +# d2460 +######################################################################## +list(APPEND d2460_sources + ${CMAKE_CURRENT_SOURCE_DIR}/d2460.cc + ${CMAKE_CURRENT_SOURCE_DIR}/ambe.c + ${CMAKE_CURRENT_SOURCE_DIR}/mbelib.c + ${CMAKE_CURRENT_SOURCE_DIR}/ambe_encoder.cc + ${CMAKE_CURRENT_SOURCE_DIR}/software_imbe_decoder.cc + ${CMAKE_CURRENT_SOURCE_DIR}/imbe_decoder.cc + ${CMAKE_CURRENT_SOURCE_DIR}/p25p2_vf.cc + ${CMAKE_CURRENT_SOURCE_DIR}/rs.cc +) + +add_executable(op25-d2460 ${d2460_sources}) +target_link_libraries(op25-d2460 imbe_vocoder) + add_subdirectory(imbe_vocoder) diff --git a/op25/gr-op25_repeater/lib/d2460.cc b/op25/gr-op25_repeater/lib/d2460.cc new file mode 100644 index 0000000..655d6de --- /dev/null +++ b/op25/gr-op25_repeater/lib/d2460.cc @@ -0,0 +1,360 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include "imbe_vocoder/imbe_vocoder.h" +#include + +static const float GAIN_ADJUST=7.0; /* attenuation (dB) */ + +typedef uint16_t Uns; +static const Uns RC_OK=0; + +static const char prodid[] = "OP25 "; +static const char verstring[] = "1.0"; + +static ambe_encoder encoder; +static software_imbe_decoder software_decoder; +static p25p2_vf interleaver; +static mbe_parms cur_mp; +static mbe_parms prev_mp; + +static const Uns DV3K_START_BYTE = 0x61; +enum +{ + DV3K_CONTROL_RATEP = 0x0A, + DV3K_CONTROL_CHANFMT = 0x15, + DV3K_CONTROL_PRODID = 0x30, + DV3K_CONTROL_VERSTRING = 0x31, + DV3K_CONTROL_RESET = 0x33, + DV3K_CONTROL_READY = 0x39 +}; +static const Uns DV3K_AMBE_FIELD_CHAND = 0x01; +static const Uns DV3K_AMBE_FIELD_CMODE = 0x02; +static const Uns DV3K_AMBE_FIELD_TONE = 0x08; +static const Uns DV3K_AUDIO_FIELD_SPEECHD = 0x00; +static const Uns DV3K_AUDIO_FIELD_CMODE = 0x02; + +#pragma DATA_ALIGN(dstar_state, 2) +static Uns bitstream[72]; + +static Uns get_byte(Uns offset, Uns *p) +{ + Uns word = p[offset >> 1]; + return (offset & 1) ? (word >> 8) : (word & 0xff); +} + +static void set_byte(Uns offset, Uns *p, Uns byte) +{ + p[offset >> 1] = + (offset & 1) ? (byte << 8) | (p[offset >> 1] & 0xff) + : (p[offset >> 1] & 0xff00) | (byte & 0xff); +} + +static Uns get_word(Uns offset, Uns *p) +{ + return get_byte(offset + 1, p) | (get_byte(offset, p) << 8); +} + +static void set_word(Uns offset, Uns *p, Uns word) +{ + set_byte(offset, p, word >> 8); + set_byte(offset + 1, p, word & 0xff); +} + +static void set_cstring(Uns offset, Uns *p, const char *str) +{ + do + set_byte(offset++, p, *str); + while (*str++ != 0); +} + +static Uns pkt_check_ratep(Uns offset, Uns *p) +{ + static const Uns ratep[] = { + 0x01, 0x30, 0x07, 0x63, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x48 }; + Uns i; + for (i = 0; i < sizeof(ratep); ++i) + if (get_byte(offset + i, p) != ratep[i]) + return 0; + return 1; +} + +static void pack(Uns bits, Uns offset, Uns *p, Uns *bitstream) +{ + Uns i; + Uns byte = 0; + for (i = 0; i < bits; ++i) + { + byte |= bitstream[i] << (7 - (i & 7)); + if ((i & 7) == 7) + { + set_byte(offset++, p, byte); + byte = 0; + } + } + if (i & 7) + set_byte(offset, p, byte); +} + +static void unpack(Uns bits, Uns offset, Uns *bitstream, Uns *p) +{ + Uns i; + Uns byte; + for (i = 0; i < bits; ++i) + { + if ((i & 7) == 0) + byte = get_byte(offset++, p); + bitstream[i] = (byte >> (7 - (i & 7))) & 1; + } +} + +static int response_len = -1; + +static void bksnd(void*task, Uns bid, Uns len) +{ + response_len = len; +} + +static void vocoder_setup(void) { + encoder.set_dstar_mode(); + encoder.set_gain_adjust(GAIN_ADJUST); + encoder.set_alt_dstar_interleave(true); +} + +static void dump(unsigned char *p, ssize_t n) +{ + int i; + for (i = 0; i < n; ++i) + printf("%02x%c", p[i], i % 16 == 15 ? '\n' : ' '); + if (i % 16) + printf("\n"); +} + +static Uns pkt_process(Uns*pkt, Uns cnt) +{ + Uns bid=0; + Uns len = cnt << 1; + Uns payload_length; + Uns i; + Uns cmode = 0; + Uns tone = 0; + uint8_t codeword[72]; + int b[9]; + int K; + int rc = -1; + + if (len < 4 || cnt > 256) + goto fail; + + if (get_byte(0, pkt) != DV3K_START_BYTE) + goto fail; + + payload_length = get_word(1, pkt); + if (payload_length == 0) + goto fail; + if (4 + payload_length > len) + goto fail; + + switch (get_byte(3, pkt)) + { + case 0: + switch (get_byte(4, pkt)) + { + case DV3K_CONTROL_RATEP: + if (payload_length != 13) + goto fail; + if (!pkt_check_ratep(5, pkt)) + goto fail; + set_word(1, pkt, 1); + bksnd(NULL, bid, 3); + return RC_OK; + case DV3K_CONTROL_CHANFMT: + if (payload_length != 3) + goto fail; + if (get_word(5, pkt) != 0x0001) + goto fail; + set_word(1, pkt, 2); + set_byte(5, pkt, 0); + bksnd(NULL, bid, 3); + return RC_OK; + case DV3K_CONTROL_PRODID: + set_word(1, pkt, 8); + set_cstring(5, pkt, prodid); + bksnd(NULL, bid, 6); + return RC_OK; + case DV3K_CONTROL_VERSTRING: + set_word(1, pkt, 5); + set_cstring(5, pkt, verstring); + bksnd(NULL, bid, 5); + return RC_OK; + case DV3K_CONTROL_RESET: + if (payload_length != 1) + goto fail; + vocoder_setup(); + set_byte(4, pkt, DV3K_CONTROL_READY); + bksnd(NULL, bid, 3); + return RC_OK; + default: + goto fail; + } + case 1: + switch (payload_length) + { + case 17: + if (get_byte(18, pkt) != DV3K_AMBE_FIELD_TONE) + goto fail; + tone = get_word(19, pkt); + /* FALLTHROUGH */ + case 14: + if (get_byte(15, pkt) != DV3K_AMBE_FIELD_CMODE) + goto fail; + cmode = get_word(16, pkt); + /* FALLTHROUGH */ + case 11: + if (get_byte(4, pkt) != DV3K_AMBE_FIELD_CHAND) + goto fail; + if (get_byte(5, pkt) != 72) + goto fail; + unpack(72, 6, bitstream, pkt); + break; + default: + goto fail; + } + + for (i = 0; i < 72; i++) { + codeword[i] = bitstream[i]; + } + interleaver.decode_dstar(codeword, b, true); + if (b[0] >= 120) { + memset(6+(char*)pkt, 0, 320); // silence + // FIXME: add handling for tone case (b0=126) + } else { + rc = mbe_dequantizeAmbe2400Parms(&cur_mp, &prev_mp, b); + printf("B\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n", b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8]); + K = 12; + if (cur_mp.L <= 36) + K = int(float(cur_mp.L + 2.0) / 3.0); + software_decoder.decode_tap(cur_mp.L, K, cur_mp.w0, &cur_mp.Vl[1], &cur_mp.Ml[1]); + audio_samples *samples = software_decoder.audio(); + int16_t snd; + for (i=0; i < 160; i++) { + if (samples->size() > 0) { + snd = (int16_t)(samples->front()); + samples->pop_front(); + } else { + snd = 0; + } + set_word(6 + (i << 1), pkt, snd); + } + mbe_moveMbeParms (&cur_mp, &prev_mp); + } + + set_word(1, pkt, 322); + set_byte(3, pkt, 2); + set_byte(4, pkt, DV3K_AUDIO_FIELD_SPEECHD); + set_byte(5, pkt, 160); + bksnd(NULL, bid, 165); + return RC_OK; + case 2: + if (payload_length != 322 && payload_length != 325) + goto fail; + if (get_byte(4, pkt) != DV3K_AUDIO_FIELD_SPEECHD) + goto fail; + if (get_byte(5, pkt) != 160) + goto fail; + if (payload_length == 325) + { + if (get_byte(326, pkt) != DV3K_AUDIO_FIELD_CMODE) + goto fail; + cmode = get_word(323, pkt); + } + int16_t samples[160]; + for (i=0; i < 160; i++) { + samples[i] = (int16_t) get_word(6 + (i << 1), pkt); + } + encoder.encode(samples, codeword); + for (i = 0; i < 72; i++) { + bitstream[i] = codeword[i]; + } + set_word(1, pkt, 11); + set_byte(3, pkt, 1); + set_byte(4, pkt, DV3K_AMBE_FIELD_CHAND); + set_byte(5, pkt, 72); + pack(72, 6, pkt, bitstream); + bksnd(NULL, bid, 8); + return RC_OK; + default: + goto fail; + } + +fail: + bksnd(NULL, bid, 0); + return RC_OK; +} + +int main() +{ + int sockfd; + const ssize_t size_max = 1024; + ssize_t size_in, size_out; + char buf_in[size_max], buf_out[size_max]; + socklen_t length = sizeof(struct sockaddr_in); + struct sockaddr_in sa = { 0 }; + Uns rc; + + vocoder_setup(); + + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + exit(2); + + sa.sin_family = AF_INET; + sa.sin_port = htons(2460); + sa.sin_addr.s_addr = htonl(INADDR_ANY); + + if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) + exit(3); + + while (1) + { + if ((size_in = recvfrom(sockfd, buf_in, size_max, + 0, (struct sockaddr *)&sa, &length)) < 0) + exit(4); + + if (size_in & 1) + buf_in[size_in++] = 0; + + rc = pkt_process((Uns*)buf_in, size_in >> 1); + if (response_len <= 0) + exit(9); + + size_out = 4 + ntohs(*(short *)&buf_in[1]); + if (sendto(sockfd, buf_in, size_out, 0, (struct sockaddr *)&sa, + sizeof(struct sockaddr_in)) != size_out) + exit(7); + } + + return 0; +} From 8fc19951819f50bcbd6475a4f1ed78990966f584 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Nov 2017 21:12:03 -0500 Subject: [PATCH 007/102] p25 tx touch up --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 2 +- op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index d3f6082..4667d4a 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -42,7 +42,7 @@ RC_FILTER = {'dmr': 'rrc', 'p25': 'rc', 'ysf': 'rrc', 'dstar': None} output_gains = { 'dmr': 5.5, 'dstar': 0.95, - 'p25': 5.0, + 'p25': 4.5, 'ysf': 5.5 } gain_adjust = { diff --git a/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py b/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py index ace2be3..2c77bd5 100755 --- a/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py +++ b/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py @@ -203,7 +203,7 @@ class p25_mod_bf(gr.hier_block2): if rc: coeffs = filter.firdes.root_raised_cosine(1.0, output_sample_rate, input_sample_rate, 0.2, 91) if rc == 'rc': - coeffs = np.convolve(coeffs, coeffs) + coeffs = c4fm_taps(sample_rate=output_sample_rate).generate() elif self.dstar: coeffs = gmsk_taps(sample_rate=output_sample_rate, bt=self.bt).generate() elif not rc: From 43a70b67db107f8f4a7a3f6793c2d86eaf86b232 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Nov 2017 21:12:46 -0500 Subject: [PATCH 008/102] rx.py AF modes bugfix --- op25/gr-op25_repeater/apps/rx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 95f7cfe..ad1f5a0 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -138,7 +138,7 @@ class p25_rx_block (gr.top_block): self.fft_sink = None self.src = None - if not options.input: + if (not options.input) and (not options.audio) and (not options.audio_if): # check if osmocom is accessible try: import osmosdr From 53dbfb4347f1294ca076bb188f7ae6d4ee239660 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 10 Nov 2017 22:08:07 -0500 Subject: [PATCH 009/102] dv_tx.py crash bugfix --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index 4667d4a..425b12e 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -124,7 +124,7 @@ class my_top_block(gr.top_block): ENCODER = op25_repeater.dstar_tx_sb(options.verbose, options.config_file) elif options.protocol == 'p25': ENCODER = op25_repeater.vocoder(True, # 0=Decode,True=Encode - 0, # Verbose flag + False, # Verbose flag 0, # flex amount "", # udp ip address 0, # udp port From d3719bd11c6dc18de536b219f95f471e915b263e Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 11 Nov 2017 19:59:07 -0500 Subject: [PATCH 010/102] update TX rate conversion logic --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 23 ++++++++------ op25/gr-op25_repeater/apps/tx/multi_tx.py | 30 ++++++++++++++----- .../gr-op25_repeater/apps/tx/op25_c4fm_mod.py | 19 ++++++------ 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index 425b12e..6645f56 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -84,7 +84,7 @@ class my_top_block(gr.top_block): parser.add_option("-f", "--file1", type="string", default=None, help="specify the input file slot 1") parser.add_option("-F", "--file2", type="string", default=None, help="specify the input file slot 2 (DMR)") parser.add_option("-g", "--gain", type="float", default=1.0, help="input gain") - parser.add_option("-i", "--if-rate", type="float", default=960000, help="output rate to sdr") + parser.add_option("-i", "--if-rate", type="int", default=960000, help="output rate to sdr") parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") parser.add_option("-O", "--audio-output", type="string", default="default", help="pcm output device name. E.g., hw:0,0 or /dev/dsp") @@ -94,7 +94,8 @@ class my_top_block(gr.top_block): parser.add_option("-Q", "--frequency", type="float", default=0.0, help="Hz") parser.add_option("-r", "--repeat", action="store_true", default=False, help="input file repeat") parser.add_option("-R", "--fullrate-mode", action="store_true", default=False, help="ysf fullrate") - parser.add_option("-s", "--sample-rate", type="int", default=48000, help="output sample rate") + parser.add_option("-s", "--modulator-rate", type="int", default=48000, help="must be submultiple of IF rate") + parser.add_option("-S", "--alsa-rate", type="int", default=48000, help="sound source/sink sample rate") parser.add_option("-t", "--test", type="string", default=None, help="test pattern symbol file") parser.add_option("-v", "--verbose", type="int", default=0, help="additional output") (options, args) = parser.parse_args() @@ -145,10 +146,10 @@ class my_top_block(gr.top_block): if options.file2 and options.protocol == 'dmr': nfiles += 1 if nfiles < max_inputs and not options.test: - AUDIO = audio.source(options.sample_rate, options.audio_input) - lpf_taps = filter.firdes.low_pass(1.0, options.sample_rate, 3400.0, 3400 * 0.1, filter.firdes.WIN_HANN) + AUDIO = audio.source(options.alsa_rate, options.audio_input) + lpf_taps = filter.firdes.low_pass(1.0, options.alsa_rate, 3400.0, 3400 * 0.1, filter.firdes.WIN_HANN) audio_rate = 8000 - AUDIO_DECIM = filter.fir_filter_fff (int(options.sample_rate / audio_rate), lpf_taps) + AUDIO_DECIM = filter.fir_filter_fff (int(options.alsa_rate / audio_rate), lpf_taps) AUDIO_SCALE = blocks.multiply_const_ff(32767.0 * options.gain) AUDIO_F2S = blocks.float_to_short() self.connect(AUDIO, AUDIO_DECIM, AUDIO_SCALE, AUDIO_F2S) @@ -172,13 +173,13 @@ class my_top_block(gr.top_block): else: self.connect(AUDIO_F2S, ENCODER2) - MOD = p25_mod_bf(output_sample_rate = options.sample_rate, dstar = (options.protocol == 'dstar'), bt = options.bt, rc = RC_FILTER[options.protocol]) + MOD = p25_mod_bf(output_sample_rate = options.modulator_rate, dstar = (options.protocol == 'dstar'), bt = options.bt, rc = RC_FILTER[options.protocol]) AMP = blocks.multiply_const_ff(output_gain) if options.output_file: OUT = blocks.file_sink(gr.sizeof_float, options.output_file) elif not options.args: - OUT = audio.sink(options.sample_rate, options.audio_output) + OUT = audio.sink(options.alsa_rate, options.audio_output) if options.protocol == 'dmr' and not options.test: self.connect(DMR, MOD) @@ -186,8 +187,13 @@ class my_top_block(gr.top_block): self.connect(ENCODER, MOD) if options.args: + f1 = float(options.if_rate) / options.modulator_rate + i1 = int(options.if_rate / options.modulator_rate) + if f1 - i1 > 1e-3: + print '*** Error, sdr rate %d not an integer multiple of modulator rate %d - ratio=%f' % (options.if_rate, options.modulator_rate, f1) + sys.exit(1) self.setup_sdr_output(options, mod_adjust[options.protocol]) - interp = filter.rational_resampler_fff(options.if_rate / options.sample_rate, 1) + interp = filter.rational_resampler_fff(options.if_rate / options.modulator_rate, 1) self.attn = blocks.multiply_const_cc(0.25) self.connect(MOD, AMP, interp, self.fm_modulator, self.attn, self.u) else: @@ -212,7 +218,6 @@ class my_top_block(gr.top_block): print "setting gain %s to %d" % (name, gain) self.u.set_gain(gain, name) - print 'setting sample rate' self.u.set_sample_rate(options.if_rate) self.u.set_center_freq(options.frequency) self.u.set_freq_corr(options.frequency_correction) diff --git a/op25/gr-op25_repeater/apps/tx/multi_tx.py b/op25/gr-op25_repeater/apps/tx/multi_tx.py index 3f93b19..2aa343c 100755 --- a/op25/gr-op25_repeater/apps/tx/multi_tx.py +++ b/op25/gr-op25_repeater/apps/tx/multi_tx.py @@ -107,7 +107,7 @@ class my_top_block(gr.top_block): parser.add_option("-b", "--bt", type="float", default=0.5, help="specify bt value") parser.add_option("-f", "--file", type="string", default=None, help="specify the input file (mono 8000 sps S16_LE)") parser.add_option("-g", "--gain", type="float", default=1.0, help="input gain") - parser.add_option("-i", "--if-rate", type="float", default=960000, help="output rate to sdr") + parser.add_option("-i", "--if-rate", type="int", default=960000, help="output rate to sdr") parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") parser.add_option("-o", "--if-offset", type="float", default=100000, help="channel spacing (Hz)") @@ -115,21 +115,36 @@ class my_top_block(gr.top_block): parser.add_option("-Q", "--frequency", type="float", default=0.0, help="Hz") parser.add_option("-r", "--repeat", action="store_true", default=False, help="input file repeat") parser.add_option("-R", "--fullrate-mode", action="store_true", default=False, help="ysf fullrate") - parser.add_option("-s", "--sample-rate", type="int", default=48000, help="output sample rate") + parser.add_option("-s", "--modulator-rate", type="int", default=48000, help="must be submultiple of IF rate") + parser.add_option("-S", "--alsa-rate", type="int", default=48000, help="sound source/sink sample rate") parser.add_option("-v", "--verbose", type="int", default=0, help="additional output") (options, args) = parser.parse_args() assert options.file # input file name (-f filename) required + f1 = float(options.if_rate) / options.modulator_rate + i1 = int(options.if_rate / options.modulator_rate) + if f1 - i1 > 1e-3: + print '*** Error, sdr rate %d not an integer multiple of modulator rate %d - ratio=%f' % (options.if_rate, options.modulator_rate, f1) + sys.exit(1) + + protocols = 'dmr p25 dstar ysf'.split() + bw = options.if_offset * len(protocols) + 50000 + if bw > options.if_rate: + print '*** Error, a %d Hz band is required for %d channels and guardband.' % (bw, len(protocols)) + print '*** Either reduce channel spacing using -o (current value is %d Hz),' % (options.if_offset) + print '*** or increase SDR output sample rate using -i (current rate is %d Hz)' % (options.if_rate) + sys.exit(1) + max_inputs = 1 from dv_tx import output_gains, gain_adjust, gain_adjust_fullrate, mod_adjust if options.do_audio: - AUDIO = audio.source(options.sample_rate, options.audio_input) - lpf_taps = filter.firdes.low_pass(1.0, options.sample_rate, 3400.0, 3400 * 0.1, filter.firdes.WIN_HANN) + AUDIO = audio.source(options.alsa_rate, options.audio_input) + lpf_taps = filter.firdes.low_pass(1.0, options.alsa_rate, 3400.0, 3400 * 0.1, filter.firdes.WIN_HANN) audio_rate = 8000 - AUDIO_DECIM = filter.fir_filter_fff (int(options.sample_rate / audio_rate), lpf_taps) + AUDIO_DECIM = filter.fir_filter_fff (int(options.alsa_rate / audio_rate), lpf_taps) AUDIO_SCALE = blocks.multiply_const_ff(32767.0 * options.gain) AUDIO_F2S = blocks.float_to_short() self.connect(AUDIO, AUDIO_DECIM, AUDIO_SCALE, AUDIO_F2S) @@ -138,7 +153,6 @@ class my_top_block(gr.top_block): alt_input = None SUM = blocks.add_cc() - protocols = 'dmr p25 dstar ysf'.split() input_repeat = True for i in xrange(len(protocols)): SOURCE = blocks.file_source(gr.sizeof_short, options.file, input_repeat) @@ -159,9 +173,9 @@ class my_top_block(gr.top_block): output_gain = output_gains[protocols[i]], gain_adjust = gain_adj, mod_adjust = mod_adjust[protocols[i]], - if_freq = i * options.if_offset, + if_freq = (i - len(protocols)/2) * options.if_offset, if_rate = options.if_rate, - sample_rate = options.sample_rate, + sample_rate = options.modulator_rate, bt = options.bt, fullrate_mode = options.fullrate_mode, alt_input = alt_input, diff --git a/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py b/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py index 2c77bd5..e1f1276 100755 --- a/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py +++ b/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py @@ -180,9 +180,8 @@ class p25_mod_bf(gr.hier_block2): gr.io_signature(1, 1, gr.sizeof_float)) # Output signature input_sample_rate = 4800 # P25 baseband symbol rate - lcm = gru.lcm(input_sample_rate, output_sample_rate) - self._interp_factor = int(lcm // input_sample_rate) - self._decimation = int(lcm // output_sample_rate) + intermediate_rate = 48000 + self._interp_factor = intermediate_rate / input_sample_rate self.dstar = dstar self.bt = bt @@ -201,13 +200,13 @@ class p25_mod_bf(gr.hier_block2): assert rc is None or rc == 'rc' or rc == 'rrc' if rc: - coeffs = filter.firdes.root_raised_cosine(1.0, output_sample_rate, input_sample_rate, 0.2, 91) + coeffs = filter.firdes.root_raised_cosine(1.0, intermediate_rate, input_sample_rate, 0.2, 91) if rc == 'rc': - coeffs = c4fm_taps(sample_rate=output_sample_rate).generate() + coeffs = c4fm_taps(sample_rate=intermediate_rate).generate() elif self.dstar: - coeffs = gmsk_taps(sample_rate=output_sample_rate, bt=self.bt).generate() + coeffs = gmsk_taps(sample_rate=intermediate_rate, bt=self.bt).generate() elif not rc: - coeffs = c4fm_taps(sample_rate=output_sample_rate, generator=self.generator).generate() + coeffs = c4fm_taps(sample_rate=intermediate_rate, generator=self.generator).generate() self.filter = filter.interp_fir_filter_fff(self._interp_factor, coeffs) if verbose: @@ -217,9 +216,9 @@ class p25_mod_bf(gr.hier_block2): self._setup_logging() self.connect(self, self.C2S, self.polarity, self.filter) - if (self._decimation > 1): - self.decimator = filter.rational_resampler_fff(1, self._decimation) - self.connect(self.filter, self.decimator, self) + if intermediate_rate != output_sample_rate: + self.arb_resamp = filter.pfb.arb_resampler_fff(float(output_sample_rate)/intermediate_rate) + self.connect(self.filter, self.arb_resamp, self) else: self.connect(self.filter, self) From 2d52d95a0a6e7709253aaf0c94ab20fb9f9db294 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 22 Nov 2017 18:56:52 -0500 Subject: [PATCH 011/102] bugfix: static in check_frame_sync --- op25/gr-op25_repeater/lib/check_frame_sync.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/lib/check_frame_sync.h b/op25/gr-op25_repeater/lib/check_frame_sync.h index d361f19..bf2498c 100644 --- a/op25/gr-op25_repeater/lib/check_frame_sync.h +++ b/op25/gr-op25_repeater/lib/check_frame_sync.h @@ -6,7 +6,7 @@ static inline bool check_frame_sync(uint64_t x, int err_threshold, int len) { int errs=0; - static const uint64_t mask = (1LL< Date: Wed, 22 Nov 2017 18:57:41 -0500 Subject: [PATCH 012/102] bugfix: uninitialized struct in d2460.cc --- op25/gr-op25_repeater/lib/d2460.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/op25/gr-op25_repeater/lib/d2460.cc b/op25/gr-op25_repeater/lib/d2460.cc index 655d6de..bf796b8 100644 --- a/op25/gr-op25_repeater/lib/d2460.cc +++ b/op25/gr-op25_repeater/lib/d2460.cc @@ -39,6 +39,7 @@ static software_imbe_decoder software_decoder; static p25p2_vf interleaver; static mbe_parms cur_mp; static mbe_parms prev_mp; +static mbe_parms enh_mp; static const Uns DV3K_START_BYTE = 0x61; enum @@ -142,6 +143,7 @@ static void vocoder_setup(void) { encoder.set_dstar_mode(); encoder.set_gain_adjust(GAIN_ADJUST); encoder.set_alt_dstar_interleave(true); + mbe_initMbeParms (&cur_mp, &prev_mp, &enh_mp); } static void dump(unsigned char *p, ssize_t n) From ef586696a17876b382a2f05526d9895599537e4a Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 22 Nov 2017 19:00:34 -0500 Subject: [PATCH 013/102] dmr hamming 7_4 decode --- op25/gr-op25_repeater/lib/dmr_bs_tx_bb_impl.cc | 2 -- op25/gr-op25_repeater/lib/dmr_const.h | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/op25/gr-op25_repeater/lib/dmr_bs_tx_bb_impl.cc b/op25/gr-op25_repeater/lib/dmr_bs_tx_bb_impl.cc index 3609c58..56e9612 100644 --- a/op25/gr-op25_repeater/lib/dmr_bs_tx_bb_impl.cc +++ b/op25/gr-op25_repeater/lib/dmr_bs_tx_bb_impl.cc @@ -186,8 +186,6 @@ static void generate_cach(uint8_t at, uint8_t tc, uint8_t lcss, const uint8_t ca int tact = hamming_7_4[ (at << 3) | (tc << 2) | lcss ]; //printf ("tact %d %x\n", tact, tact); //print_result("cach_payload_bits", cach_bits, 17); - static const uint8_t cach_tact_bits[] = {0, 4, 8, 12, 14, 18, 22}; - static const uint8_t cach_payload_bits[] = {1,2,3,5,6,7,9,10,11,13,15,16,17,19,20,21,23}; for (int i=0; i<7; i++) { result[cach_tact_bits[i]] = (tact >> (6-i)) & 1; } diff --git a/op25/gr-op25_repeater/lib/dmr_const.h b/op25/gr-op25_repeater/lib/dmr_const.h index 0ea6ca0..e26eb82 100644 --- a/op25/gr-op25_repeater/lib/dmr_const.h +++ b/op25/gr-op25_repeater/lib/dmr_const.h @@ -25,7 +25,18 @@ static const int hamming_7_4[] = { 0, 11, 22, 29, 39, 44, 49, 58, 69, 78, 83, 88, 98, 105, 116, 127, }; - + +static const int hamming_7_4_decode[] = { + 0, 0, 0, 1, 0, 8, 2, 4, 0, 1, 1, 1, 5, 3, 9, 1, + 0, 6, 2, 10, 2, 3, 2, 2, 11, 3, 7, 1, 3, 3, 2, 3, + 0, 6, 12, 4, 5, 4, 4, 4, 5, 13, 7, 1, 5, 5, 5, 4, + 6, 6, 7, 6, 14, 6, 2, 4, 7, 6, 7, 7, 5, 3, 7, 15, + 0, 8, 12, 10, 8, 8, 9, 8, 11, 13, 9, 1, 9, 8, 9, 9, + 11, 10, 10, 10, 14, 8, 2, 10, 11, 11, 11, 10, 11, 3, 9, 15, + 12, 13, 12, 12, 14, 8, 12, 4, 13, 13, 12, 13, 5, 13, 9, 15, + 14, 6, 12, 10, 14, 14, 14, 15, 11, 13, 7, 15, 14, 15, 15, 15 +}; + static const int hamming_17_12[] = { 0, 37, 74, 111, 148, 177, 222, 251, 269, 296, 327, 354, 409, 444, 467, 502, @@ -822,9 +833,14 @@ static const int hamming_16_11[] = { static const uint8_t dmr_bs_voice_sync[24] = { 1,3,1,1,1,1,3,3,3,1,1,3,3,1,3,3,1,3,1,1,3,3,1,3 }; +static const uint64_t DMR_VOICE_SYNC_MAGIC = 0x755fd7df75f7LL; +static const uint64_t DMR_IDLE_SYNC_MAGIC = 0xdff57d75df5dLL; static const uint8_t dmr_bs_idle_sync[24] = { 3,1,3,3,3,3,1,1,1,3,3,1,1,3,1,1,3,1,3,3,1,1,3,1 }; +static const uint8_t cach_tact_bits[] = {0, 4, 8, 12, 14, 18, 22}; +static const uint8_t cach_payload_bits[] = {1,2,3,5,6,7,9,10,11,13,15,16,17,19,20,21,23}; + #endif /* INCLUDED_OP25_REPEATER_DMR_CONST_H */ From 84f0a32ee10a3fdcacac8dbf6d97ca440d2de873 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 22 Nov 2017 19:02:20 -0500 Subject: [PATCH 014/102] p25_frame.h: fix include guard --- op25/gr-op25_repeater/lib/p25_frame.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/op25/gr-op25_repeater/lib/p25_frame.h b/op25/gr-op25_repeater/lib/p25_frame.h index 8afbaf0..6c47b01 100644 --- a/op25/gr-op25_repeater/lib/p25_frame.h +++ b/op25/gr-op25_repeater/lib/p25_frame.h @@ -18,8 +18,8 @@ * Boston, MA 02110-1301, USA. */ -#ifndef INCLUDED_OP25_P25_FRAME_H -#define INCLUDED_OP25_P25_FRAME_H 1 +#ifndef INCLUDED_P25_FRAME_H +#define INCLUDED_P25_FRAME_H 1 #include typedef std::vector bit_vector; @@ -62,4 +62,4 @@ p25_setup_frame_header(bit_vector& frame_body, uint64_t hw) { } // namespace op25_repeater } // namespace gr -#endif /* INCLUDED_OP25_P25_FRAME_H */ +#endif /* INCLUDED_P25_FRAME_H */ From 98e43839b9b04f02864bb33d8755678c48d60f4e Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 22 Nov 2017 19:04:25 -0500 Subject: [PATCH 015/102] ysf frame decode --- op25/gr-op25_repeater/lib/ysf_const.h | 71 ++++++++++++++++++++- op25/gr-op25_repeater/lib/ysf_tx_sb_impl.cc | 14 +--- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/op25/gr-op25_repeater/lib/ysf_const.h b/op25/gr-op25_repeater/lib/ysf_const.h index 3538c50..a9883ab 100644 --- a/op25/gr-op25_repeater/lib/ysf_const.h +++ b/op25/gr-op25_repeater/lib/ysf_const.h @@ -1,5 +1,6 @@ // // YSF Encoder (C) Copyright 2017 Max H. Parke KA1RBI +// thx gr-ysf fr_vch_decoder_bb_impl.cc * Copyright 2015 Mathias Weyland * // // This file is part of OP25 // @@ -21,6 +22,62 @@ #ifndef INCLUDED_YSF_CONST_H #define INCLUDED_YSF_CONST_H +#include + +static void decode_49bit(int b[9], const uint8_t src[49]) { + for (int i=0; i<9; i++) + b[i] = 0; + b[0] |= src[0] << 6; + b[0] |= src[1] << 5; + b[0] |= src[2] << 4; + b[0] |= src[3] << 3; + b[1] |= src[4] << 4; + b[1] |= src[5] << 3; + b[1] |= src[6] << 2; + b[1] |= src[7] << 1; + b[2] |= src[8] << 4; + b[2] |= src[9] << 3; + b[2] |= src[10] << 2; + b[2] |= src[11] << 1; + b[3] |= src[12] << 8; + b[3] |= src[13] << 7; + b[3] |= src[14] << 6; + b[3] |= src[15] << 5; + b[3] |= src[16] << 4; + b[3] |= src[17] << 3; + b[3] |= src[18] << 2; + b[3] |= src[19] << 1; + b[4] |= src[20] << 6; + b[4] |= src[21] << 5; + b[4] |= src[22] << 4; + b[4] |= src[23] << 3; + b[5] |= src[24] << 4; + b[5] |= src[25] << 3; + b[5] |= src[26] << 2; + b[5] |= src[27] << 1; + b[6] |= src[28] << 3; + b[6] |= src[29] << 2; + b[6] |= src[30] << 1; + b[7] |= src[31] << 3; + b[7] |= src[32] << 2; + b[7] |= src[33] << 1; + b[8] |= src[34] << 2; + b[1] |= src[35]; + b[2] |= src[36]; + b[0] |= src[37] << 2; + b[0] |= src[38] << 1; + b[0] |= src[39]; + b[3] |= src[40]; + b[4] |= src[41] << 2; + b[4] |= src[42] << 1; + b[4] |= src[43]; + b[5] |= src[44]; + b[6] |= src[45]; + b[7] |= src[46]; + b[8] |= src[47] << 1; + b[8] |= src[48]; +} + static const int gly_24_12[] = { 0, 6379, 10558, 12757, 19095, 21116, 25513, 31554, 36294, 38189, 42232, 48147, 51025, 57274, 61039, 63108, 66407, 72588, 76377, 78514, 84464, 86299, 90318, 96293, 102049, 104010, 108447, 114548, 115766, 122077, 126216, 128483, @@ -279,7 +336,9 @@ static const int gly_24_12[] = { 16648732, 16650999, 16655138, 16661449, 16662667, 16668768, 16673205, 16675166, 16680922, 16686897, 16690916, 16692751, 16698701, 16700838, 16704627, 16710808, 16714107, 16716176, 16719941, 16726190, 16729068, 16734983, 16739026, 16740921, 16745661, 16751702, 16756099, 16758120, 16764458, 16766657, 16770836, 16777215 }; -static const uint8_t scramble_code[180] = { +static inline void ysf_scramble(uint8_t buf[], const int len) +{ // buffer is (de)scrambled in place + static const uint8_t scramble_code[180] = { 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, @@ -292,11 +351,19 @@ static const uint8_t scramble_code[180] = { 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -}; + }; + + assert(len <= (int)sizeof(scramble_code)); + for (int i=0; i Date: Mon, 27 Nov 2017 13:07:55 -0500 Subject: [PATCH 016/102] cleanup unused vars --- op25/gr-op25_repeater/apps/rx.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index ad1f5a0..d872e0d 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -252,16 +252,6 @@ class p25_rx_block (gr.top_block): self.tdma_state = False self.xor_cache = {} - self.fft_state = False - self.c4fm_state = False - self.fscope_state = False - self.corr_state = False - self.fac_state = False - self.fsk4_demod_connected = False - self.psk_demod_connected = False - self.fsk4_demod_mode = False - self.corr_i_chan = False - if self.baseband_input: self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate) else: # complex input From 796c81f219d3dc23166a8f2109d2a35ffac5cc0a Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 27 Nov 2017 13:08:42 -0500 Subject: [PATCH 017/102] cleanup unused vars --- op25/gr-op25_repeater/lib/software_imbe_decoder.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/op25/gr-op25_repeater/lib/software_imbe_decoder.cc b/op25/gr-op25_repeater/lib/software_imbe_decoder.cc index 77c0b24..e2705c9 100644 --- a/op25/gr-op25_repeater/lib/software_imbe_decoder.cc +++ b/op25/gr-op25_repeater/lib/software_imbe_decoder.cc @@ -935,7 +935,6 @@ software_imbe_decoder::decode_tap(int _L, int _K, float _w0, const int * _v, con int en, tmp_f; L = _L; - int K = _K; w0 = _w0; for(ell = 1; ell <= L; ell++) { vee[ell][ New] = _v[ell - 1]; From 0927bc11a9117d86e9c9a3e2d8fbcf5d4ac1fe8b Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 29 Nov 2017 00:16:18 -0500 Subject: [PATCH 018/102] vocoder: fix excess cpu usage --- op25/gr-op25_repeater/lib/vocoder_impl.cc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/lib/vocoder_impl.cc b/op25/gr-op25_repeater/lib/vocoder_impl.cc index 3f2032d..967dde4 100644 --- a/op25/gr-op25_repeater/lib/vocoder_impl.cc +++ b/op25/gr-op25_repeater/lib/vocoder_impl.cc @@ -40,6 +40,8 @@ namespace gr { namespace op25_repeater { + static const int FRAGMENT_SIZE = 864; + vocoder::sptr vocoder::make(bool encode_flag, bool verbose_flag, int stretch_amt, char* udp_host, int udp_port, bool raw_vectors_flag) { @@ -73,6 +75,8 @@ namespace gr { p1voice_encode(verbose_flag, stretch_amt, udp_host, udp_port, raw_vectors_flag, output_queue), p1voice_decode(verbose_flag, udp_host, udp_port, output_queue_decode) { + if (opt_encode_flag) + set_output_multiple(FRAGMENT_SIZE); } /* @@ -131,13 +135,17 @@ vocoder_impl::general_work_encode (int noutput_items, gr_vector_void_star &output_items) { const short *in = (const short *) input_items[0]; + const int noutput_fragments = noutput_items / FRAGMENT_SIZE; + const int fragments_available = output_queue.size() / FRAGMENT_SIZE; + const int nsamples_consume = std::min(ninput_items[0], std::max(0,(noutput_fragments - fragments_available) * 9 * 160)); - p1voice_encode.compress_samp(in, ninput_items[0]); + if (nsamples_consume > 0) + p1voice_encode.compress_samp(in, nsamples_consume); // Tell runtime system how many input items we consumed on // each input stream. - consume_each (ninput_items[0]); + consume_each (nsamples_consume); if (opt_udp_port > 0) // in udp option, we are a gr sink only return 0; From 5b280ae2ef4944a7831b52620c0dd126fc4fab32 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 29 Nov 2017 13:16:50 -0500 Subject: [PATCH 019/102] updated dv_tx --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index 6645f56..42e5cb1 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -48,7 +48,7 @@ output_gains = { gain_adjust = { 'dmr': 3.0, 'dstar': 7.5, - 'ysf': 5.0 + 'ysf': 4.0 } gain_adjust_fullrate = { 'p25': 2.0, @@ -84,8 +84,9 @@ class my_top_block(gr.top_block): parser.add_option("-f", "--file1", type="string", default=None, help="specify the input file slot 1") parser.add_option("-F", "--file2", type="string", default=None, help="specify the input file slot 2 (DMR)") parser.add_option("-g", "--gain", type="float", default=1.0, help="input gain") - parser.add_option("-i", "--if-rate", type="int", default=960000, help="output rate to sdr") + parser.add_option("-i", "--if-rate", type="int", default=480000, help="output rate to sdr") parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") + parser.add_option("-k", "--symbol-sink", type="string", default=None, help="write symbols to file (optional)") parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") parser.add_option("-O", "--audio-output", type="string", default="default", help="pcm output device name. E.g., hw:0,0 or /dev/dsp") parser.add_option("-o", "--output-file", type="string", default=None, help="specify the output file") @@ -136,7 +137,7 @@ class my_top_block(gr.top_block): ENCODER.set_gain_adjust(gain_adjust_fullrate['ysf']) else: ENCODER.set_gain_adjust(gain_adjust['ysf']) - if options.protocol == 'p25': + if options.protocol == 'p25' and not options.test: ENCODER.set_gain_adjust(gain_adjust_fullrate[options.protocol]) elif not options.test and not options.protocol == 'ysf': ENCODER.set_gain_adjust(gain_adjust[options.protocol]) @@ -181,10 +182,16 @@ class my_top_block(gr.top_block): elif not options.args: OUT = audio.sink(options.alsa_rate, options.audio_output) + if options.symbol_sink: + SYMBOL_SINK = blocks.file_sink(gr.sizeof_char, options.symbol_sink) if options.protocol == 'dmr' and not options.test: self.connect(DMR, MOD) + if options.symbol_sink: + self.connect(DMR, SYMBOL_SINK) else: self.connect(ENCODER, MOD) + if options.symbol_sink: + self.connect(ENCODER, SYMBOL_SINK) if options.args: f1 = float(options.if_rate) / options.modulator_rate From d6615c84fe8653179eeaa61d85a02ec0d613f1a0 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 5 Dec 2017 20:11:02 -0500 Subject: [PATCH 020/102] dv_tx QRO --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index 42e5cb1..7964190 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -201,8 +201,7 @@ class my_top_block(gr.top_block): sys.exit(1) self.setup_sdr_output(options, mod_adjust[options.protocol]) interp = filter.rational_resampler_fff(options.if_rate / options.modulator_rate, 1) - self.attn = blocks.multiply_const_cc(0.25) - self.connect(MOD, AMP, interp, self.fm_modulator, self.attn, self.u) + self.connect(MOD, AMP, interp, self.fm_modulator, self.u) else: self.connect(MOD, AMP, OUT) From ce129911bc0218a700d1f31434c59d7b764e4fe8 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 5 Dec 2017 20:11:46 -0500 Subject: [PATCH 021/102] use 480K multi_tx IF rate --- op25/gr-op25_repeater/apps/tx/multi_tx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/apps/tx/multi_tx.py b/op25/gr-op25_repeater/apps/tx/multi_tx.py index 2aa343c..9135df9 100755 --- a/op25/gr-op25_repeater/apps/tx/multi_tx.py +++ b/op25/gr-op25_repeater/apps/tx/multi_tx.py @@ -107,7 +107,7 @@ class my_top_block(gr.top_block): parser.add_option("-b", "--bt", type="float", default=0.5, help="specify bt value") parser.add_option("-f", "--file", type="string", default=None, help="specify the input file (mono 8000 sps S16_LE)") parser.add_option("-g", "--gain", type="float", default=1.0, help="input gain") - parser.add_option("-i", "--if-rate", type="int", default=960000, help="output rate to sdr") + parser.add_option("-i", "--if-rate", type="int", default=480000, help="output rate to sdr") parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") parser.add_option("-o", "--if-offset", type="float", default=100000, help="channel spacing (Hz)") From 5edfc1c4638c4f56481dbe633438a76fb6beeb0e Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 16:52:30 -0500 Subject: [PATCH 022/102] install.sh --- install.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 042d07e..f52da38 100755 --- a/install.sh +++ b/install.sh @@ -12,7 +12,7 @@ fi sudo apt-get update sudo apt-get build-dep gnuradio -sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev git +sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig mkdir build cd build @@ -20,3 +20,16 @@ cmake ../ make sudo make install sudo ldconfig + +echo ====== +echo ====== NOTICE +echo ====== +echo ====== The gnuplot package is not installed by default here, +echo ====== as its installation requires numerous prerequisite packages +echo ====== that you may not want to install. +echo ====== +echo ====== In order to do plotting in rx.py using the \-P option +echo ====== you must install gnuplot, e.g., manually as follows: +echo ====== +echo ====== sudo apt-get install gnuplot-x11 +echo ====== From 35da7d958a894eb998c2f6fbaf7bb313567076ab Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:02:18 -0500 Subject: [PATCH 023/102] demodulator updates --- op25/gr-op25_repeater/apps/p25_demodulator.py | 127 +++++++++++++++--- 1 file changed, 107 insertions(+), 20 deletions(-) diff --git a/op25/gr-op25_repeater/apps/p25_demodulator.py b/op25/gr-op25_repeater/apps/p25_demodulator.py index 646d621..805746a 100644 --- a/op25/gr-op25_repeater/apps/p25_demodulator.py +++ b/op25/gr-op25_repeater/apps/p25_demodulator.py @@ -45,14 +45,35 @@ _def_costas_alpha = 0.04 _def_symbol_rate = 4800 _def_symbol_deviation = 600.0 _def_bb_gain = 1.0 +_def_excess_bw = 0.2 # ///////////////////////////////////////////////////////////////////////////// # demodulator # ///////////////////////////////////////////////////////////////////////////// +def get_decim(speed): + s = int(speed) + if_freqs = [24000, 25000, 32000] + for i_f in if_freqs: + if s % i_f != 0: + continue + q = s / i_f + if q & 1: + continue + if q >= 40 and q & 3 == 0: + decim = q/4 + decim2 = 4 + else: + decim = q/2 + decim2 = 2 + return decim, decim2 + return None + class p25_demod_base(gr.hier_block2): def __init__(self, if_rate = None, + filter_type = None, + excess_bw = _def_excess_bw, symbol_rate = _def_symbol_rate): """ Hierarchical block for P25 demodulation base class @@ -66,6 +87,12 @@ class p25_demod_base(gr.hier_block2): self.baseband_amp = blocks.multiply_const_ff(_def_bb_gain) coeffs = op25_c4fm_mod.c4fm_taps(sample_rate=self.if_rate, span=9, generator=op25_c4fm_mod.transfer_function_rx).generate() + if filter_type == 'rrc': + sps = self.if_rate / 4800 + ntaps = 7 * sps + if ntaps & 1 == 0: + ntaps += 1 + coeffs = filter.firdes.root_raised_cosine(1.0, if_rate, symbol_rate, excess_bw, ntaps) self.symbol_filter = filter.fir_filter_fff(1, coeffs) autotuneq = gr.msg_queue(2) self.fsk4_demod = op25.fsk4_demod_ff(autotuneq, self.if_rate, self.symbol_rate) @@ -73,6 +100,9 @@ class p25_demod_base(gr.hier_block2): levels = [ -2.0, 0.0, 2.0, 4.0 ] self.slicer = op25_repeater.fsk4_slicer_fb(levels) + def set_symbol_rate(self, rate): + self.symbol_rate = rate + def set_baseband_gain(self, k): self.baseband_amp.set_k(k) @@ -97,6 +127,8 @@ class p25_demod_fb(p25_demod_base): def __init__(self, input_rate = None, + filter_type = None, + excess_bw = _def_excess_bw, symbol_rate = _def_symbol_rate): """ Hierarchical block for P25 demodulation. @@ -110,7 +142,7 @@ class p25_demod_fb(p25_demod_base): gr.io_signature(1, 1, gr.sizeof_float), # Input signature gr.io_signature(1, 1, gr.sizeof_char)) # Output signature - p25_demod_base.__init__(self, if_rate=input_rate, symbol_rate=symbol_rate) + p25_demod_base.__init__(self, if_rate=input_rate, symbol_rate=symbol_rate, filter_type=filter_type) self.input_rate = input_rate self.float_sink = None @@ -135,6 +167,8 @@ class p25_demod_cb(p25_demod_base): def __init__(self, input_rate = None, demod_type = 'cqpsk', + filter_type = None, + excess_bw = _def_excess_bw, relative_freq = 0, offset = 0, if_rate = _def_if_rate, @@ -153,7 +187,7 @@ class p25_demod_cb(p25_demod_base): gr.io_signature(1, 1, gr.sizeof_gr_complex), # Input signature gr.io_signature(1, 1, gr.sizeof_char)) # Output signature # gr.io_signature(0, 0, 0)) # Output signature - p25_demod_base.__init__(self, if_rate=if_rate, symbol_rate=symbol_rate) + p25_demod_base.__init__(self, if_rate=if_rate, symbol_rate=symbol_rate, filter_type=filter_type) self.input_rate = input_rate self.if_rate = if_rate @@ -164,22 +198,52 @@ class p25_demod_cb(p25_demod_base): self.lo_freq = 0 self.float_sink = None self.complex_sink = None + self.if1 = None + self.if2 = None + self.t_cache = {} + if filter_type == 'rrc': + self.set_baseband_gain(0.61) - # local osc - self.lo = analog.sig_source_c (input_rate, analog.GR_SIN_WAVE, 0, 1.0, 0) self.mixer = blocks.multiply_cc() - lpf_coeffs = filter.firdes.low_pass(1.0, input_rate, 7250, 725, filter.firdes.WIN_HANN) - decimation = int(input_rate / if_rate) - self.lpf = filter.fir_filter_ccf(decimation, lpf_coeffs) + decimator_values = get_decim(input_rate) + if decimator_values: + self.decim, self.decim2 = decimator_values + self.if1 = input_rate / self.decim + self.if2 = self.if1 / self.decim2 + sys.stderr.write( 'Using two-stage decimator for speed=%d, decim=%d/%d if1=%d if2=%d\n' % (input_rate, self.decim, self.decim2, self.if1, self.if2)) + bpf_coeffs = filter.firdes.complex_band_pass(1.0, input_rate, -self.if1/2, self.if1/2, self.if1/2, filter.firdes.WIN_HAMMING) + self.t_cache[0] = bpf_coeffs + fa = 6250 + fb = self.if2 / 2 + lpf_coeffs = filter.firdes.low_pass(1.0, self.if1, (fb+fa)/2, fb-fa, filter.firdes.WIN_HAMMING) + self.bpf = filter.fir_filter_ccc(self.decim, bpf_coeffs) + self.lpf = filter.fir_filter_ccf(self.decim2, lpf_coeffs) + resampled_rate = self.if2 + self.bfo = analog.sig_source_c (self.if1, analog.GR_SIN_WAVE, 0, 1.0, 0) + self.connect(self, self.bpf, (self.mixer, 0)) + self.connect(self.bfo, (self.mixer, 1)) + else: + sys.stderr.write( 'Unable to use two-stage decimator for speed=%d\n' % (input_rate)) + # local osc + self.lo = analog.sig_source_c (input_rate, analog.GR_SIN_WAVE, 0, 1.0, 0) + lpf_coeffs = filter.firdes.low_pass(1.0, input_rate, 7250, 725, filter.firdes.WIN_HANN) + decimation = int(input_rate / if_rate) + self.lpf = filter.fir_filter_ccf(decimation, lpf_coeffs) + resampled_rate = float(input_rate) / float(decimation) # rate at output of self.lpf + self.connect(self, (self.mixer, 0)) + self.connect(self.lo, (self.mixer, 1)) + self.connect(self.mixer, self.lpf) - resampled_rate = float(input_rate) / float(decimation) # rate at output of self.lpf + if self.if_rate != resampled_rate: + self.if_out = filter.pfb.arb_resampler_ccf(float(self.if_rate) / resampled_rate) + self.connect(self.lpf, self.if_out) + else: + self.if_out = self.lpf - self.arb_resampler = filter.pfb.arb_resampler_ccf( - float(self.if_rate) / resampled_rate) - - self.connect(self, (self.mixer, 0)) - self.connect(self.lo, (self.mixer, 1)) - self.connect(self.mixer, self.lpf, self.arb_resampler) + fa = 6250 + fb = fa + 625 + cutoff_coeffs = filter.firdes.low_pass(1.0, self.if_rate, (fb+fa)/2, fb-fa, filter.firdes.WIN_HANN) + self.cutoff = filter.fir_filter_ccf(1, cutoff_coeffs) levels = [ -2.0, 0.0, 2.0, 4.0 ] self.slicer = op25_repeater.fsk4_slicer_fb(levels) @@ -214,6 +278,9 @@ class p25_demod_cb(p25_demod_base): self.set_relative_frequency(relative_freq) + def get_freq_error(self): # get error in Hz (approx). + return int(self.clock.get_freq_error() * self.symbol_rate) + def set_omega(self, omega): sps = self.if_rate / float(omega) if sps == self.sps: @@ -228,17 +295,28 @@ class p25_demod_cb(p25_demod_base): return False if freq == self.lo_freq: return True - #print 'set_relative_frequency', freq self.lo_freq = freq - self.lo.set_frequency(self.lo_freq) + if self.if1: + if freq not in self.t_cache.keys(): + self.t_cache[freq] = filter.firdes.complex_band_pass(1.0, self.input_rate, -freq - self.if1/2, -freq + self.if1/2, self.if1/2, filter.firdes.WIN_HAMMING) + self.bpf.set_taps(self.t_cache[freq]) + bfo_f = self.decim * -freq / float(self.input_rate) + bfo_f -= int(bfo_f) + if bfo_f < -0.5: + bfo_f += 1.0 + if bfo_f > 0.5: + bfo_f -= 1.0 + self.bfo.set_frequency(-bfo_f * self.if1) + else: + self.lo.set_frequency(self.lo_freq) return True # assumes lock held or init def disconnect_chain(self): if self.connect_state == 'cqpsk': - self.disconnect(self.arb_resampler, self.agc, self.clock, self.diffdec, self.to_float, self.rescale, self.slicer) + self.disconnect(self.if_out, self.cutoff, self.agc, self.clock, self.diffdec, self.to_float, self.rescale, self.slicer) elif self.connect_state == 'fsk4': - self.disconnect(self.arb_resampler, self.fm_demod, self.baseband_amp, self.symbol_filter, self.fsk4_demod, self.slicer) + self.disconnect(self.if_out, self.cutoff, self.fm_demod, self.baseband_amp, self.symbol_filter, self.fsk4_demod, self.slicer) self.connect_state = None # assumes lock held or init @@ -248,9 +326,9 @@ class p25_demod_cb(p25_demod_base): self.disconnect_chain() self.connect_state = demod_type if demod_type == 'fsk4': - self.connect(self.arb_resampler, self.fm_demod, self.baseband_amp, self.symbol_filter, self.fsk4_demod, self.slicer) + self.connect(self.if_out, self.cutoff, self.fm_demod, self.baseband_amp, self.symbol_filter, self.fsk4_demod, self.slicer) elif demod_type == 'cqpsk': - self.connect(self.arb_resampler, self.agc, self.clock, self.diffdec, self.to_float, self.rescale, self.slicer) + self.connect(self.if_out, self.cutoff, self.agc, self.clock, self.diffdec, self.to_float, self.rescale, self.slicer) else: print 'connect_chain failed, type: %s' % demod_type assert 0 == 1 @@ -299,3 +377,12 @@ class p25_demod_cb(p25_demod_base): elif src == 'src': self.connect(self, sink) self.complex_sink = [self, sink] + elif src == 'bpf': + self.connect(self.bpf, sink) + self.complex_sink = [self.bpf, sink] + elif src == 'if_out': + self.connect(self.if_out, sink) + self.complex_sink = [self.if_out, sink] + elif src == 'agc': + self.connect(self.agc, sink) + self.complex_sink = [self.agc, sink] From 0fcef6a5be9d06ead09ea73f8c46e6245c5295a4 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:05:29 -0500 Subject: [PATCH 024/102] rx.py --- op25/gr-op25_repeater/apps/rx.py | 74 +++++++++++++++++++------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index d872e0d..6717d32 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -65,7 +65,7 @@ from gr_gnuplot import fft_sink_c from gr_gnuplot import symbol_sink_f from gr_gnuplot import eye_sink_f -from terminal import curses_terminal +from terminal import op25_terminal from sockaudio import socket_audio #speeds = [300, 600, 900, 1200, 1440, 1800, 1920, 2400, 2880, 3200, 3600, 3840, 4000, 4800, 6000, 6400, 7200, 8000, 9600, 14400, 19200] @@ -105,6 +105,7 @@ class p25_rx_block (gr.top_block): parser.add_option("-F", "--ifile", type="string", default=None, help="read input from complex capture file") parser.add_option("-H", "--hamlib-model", type="int", default=None, help="specify model for hamlib") parser.add_option("-s", "--seek", type="int", default=0, help="ifile seek in K") + parser.add_option("-l", "--terminal-type", type="string", default='curses', help="'curses' or udp port") parser.add_option("-L", "--logfile-workers", type="int", default=None, help="number of demodulators to instantiate") parser.add_option("-S", "--sample-rate", type="int", default=320e3, help="source samp rate") parser.add_option("-t", "--tone-detect", action="store_true", default=False, help="use experimental tone detect algorithm") @@ -116,7 +117,6 @@ class p25_rx_block (gr.top_block): parser.add_option("-w", "--wireshark", action="store_true", default=False, help="output data to Wireshark") parser.add_option("-W", "--wireshark-host", type="string", default="127.0.0.1", help="Wireshark host") parser.add_option("-r", "--raw-symbols", type="string", default=None, help="dump decoded symbols to file") - parser.add_option("-R", "--rx-subdev-spec", type="subdev", default=(0, 0), help="select USRP Rx side A or B (default=A)") parser.add_option("-g", "--gain", type="eng_float", default=None, help="set USRP gain in dB (default is midpoint) or set audio gain") parser.add_option("-G", "--gain-mu", type="eng_float", default=0.025, help="gardner gain") parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") @@ -156,8 +156,8 @@ class p25_rx_block (gr.top_block): range = self.src.get_gain_range(name) print "gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step()) if options.gains: - for tuple in options.gains.split(","): - name, gain = tuple.split(":") + for tup in options.gains.split(","): + name, gain = tup.split(":") gain = int(gain) print "setting gain %s to %d" % (name, gain) self.src.set_gain(gain, name) @@ -228,7 +228,7 @@ class p25_rx_block (gr.top_block): pass # attach terminal thread - self.terminal = curses_terminal(self.input_q, self.output_q) + self.terminal = op25_terminal(self.input_q, self.output_q, self.options.terminal_type) # attach audio thread if self.options.udp_player: @@ -241,7 +241,8 @@ class p25_rx_block (gr.top_block): def __build_graph(self, source, capture_rate): global speeds global WIRESHARK_PORT - # tell the scope the source rate + + sps = 5 # samples / symbol self.rx_q = gr.msg_queue(100) udp_port = 0 @@ -253,7 +254,7 @@ class p25_rx_block (gr.top_block): self.xor_cache = {} if self.baseband_input: - self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate) + self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate, excess_bw=self.options.excess_bw) else: # complex input # local osc self.lo_freq = self.options.offset + self.options.fine_tune @@ -263,9 +264,10 @@ class p25_rx_block (gr.top_block): demod_type = self.options.demod_type, relative_freq = self.lo_freq, offset = self.options.offset, - if_rate = 48000, + if_rate = sps * 4800, gain_mu = self.options.gain_mu, costas_alpha = self.options.costas_alpha, + excess_bw = self.options.excess_bw, symbol_rate = self.symbol_rate) num_ambe = 0 @@ -294,7 +296,7 @@ class p25_rx_block (gr.top_block): self.kill_sink = self.fft_sink elif self.options.plot_mode == 'datascope': assert self.options.demod_type == 'fsk4' ## datascope requires fsk4 demod-type - self.eye_sink = eye_sink_f(sps=10) + self.eye_sink = eye_sink_f(sps=sps) self.demod.connect_bb('symbol_filter', self.eye_sink) self.kill_sink = self.eye_sink @@ -364,9 +366,11 @@ class p25_rx_block (gr.top_block): if hash not in self.xor_cache: self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars self.decoder.set_xormask(self.xor_cache[hash], hash) - sps = self.basic_rate / 6000 + rate = 6000 else: - sps = self.basic_rate / 4800 + rate = 4800 + sps = self.basic_rate / rate + self.demod.set_symbol_rate(rate) # this and the foll. call should be merged? self.demod.clock.set_omega(float(sps)) def change_freq(self, params): @@ -430,7 +434,8 @@ class p25_rx_block (gr.top_block): def set_audio_scaler(self, vol): #print 'audio scaler: %f' % ((1 / 32768.0) * (vol * 0.1)) - self.decoder.set_scaler_k((1 / 32768.0) * (vol * 0.1)) + if hasattr(self.decoder, 'set_scaler_k'): + self.decoder.set_scaler_k((1 / 32768.0) * (vol * 0.1)) def set_rtl_ppm(self, ppm): self.src.set_freq_corr(ppm) @@ -636,24 +641,35 @@ class du_queue_watcher(threading.Thread): msg = self.msgq.delete_head() self.callback(msg) +class rx_main(object): + def __init__(self): + self.keep_running = True + self.tb = p25_rx_block() + self.q_watcher = du_queue_watcher(self.tb.output_q, self.process_qmsg) + + def process_qmsg(self, msg): + if self.tb.process_qmsg(msg): + self.keep_running = False + + def run(self): + try: + self.tb.start() + while self.keep_running: + time.sleep(1) + except: + sys.stderr.write('main: exception occurred\n') + sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc()) + if self.tb.terminal: + self.tb.terminal.end_terminal() + if self.tb.audio: + self.tb.audio.stop() + self.tb.stop() + if self.tb.kill_sink: + self.tb.kill_sink.kill() + # Start the receiver # if __name__ == "__main__": - tb = p25_rx_block() - tb.start() - try: - while True: - msg = tb.output_q.delete_head() - if tb.process_qmsg(msg): - break - except: - sys.stderr.write('main: exception occurred\n') - sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc()) - if tb.terminal: - tb.terminal.end_curses() - if tb.audio: - tb.audio.stop() - tb.stop() - if tb.kill_sink: - tb.kill_sink.kill() + rx = rx_main() + rx.run() From 04692d23e24a0f31dbd8efcfd993356e359fdf10 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:08:46 -0500 Subject: [PATCH 025/102] terminal.py --- op25/gr-op25_repeater/apps/terminal.py | 140 ++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 13 deletions(-) mode change 100644 => 100755 op25/gr-op25_repeater/apps/terminal.py diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py old mode 100644 new mode 100755 index 46efa1a..82579e8 --- a/op25/gr-op25_repeater/apps/terminal.py +++ b/op25/gr-op25_repeater/apps/terminal.py @@ -28,11 +28,28 @@ import time import json import threading import traceback +import socket from gnuradio import gr +KEEPALIVE_TIME = 3.0 # no data received in (seconds) + +class q_watcher(threading.Thread): + def __init__(self, msgq, callback, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.msgq = msgq + self.callback = callback + self.keep_running = True + self.start() + + def run(self): + while(self.keep_running): + msg = self.msgq.delete_head() + self.callback(msg) + class curses_terminal(threading.Thread): - def __init__(self, input_q, output_q, **kwds): + def __init__(self, input_q, output_q, sock=None, **kwds): threading.Thread.__init__ (self, **kwds) self.setDaemon(1) self.input_q = input_q @@ -41,6 +58,7 @@ class curses_terminal(threading.Thread): self.last_update = 0 self.auto_update = True self.current_nac = None + self.sock = sock self.start() def setup_curses(self): @@ -58,7 +76,7 @@ class curses_terminal(threading.Thread): self.textpad = curses.textpad.Textbox(self.text_win) - def end_curses(self): + def end_terminal(self): try: curses.endwin() except: @@ -81,17 +99,14 @@ class curses_terminal(threading.Thread): COMMANDS = {_ORD_S: 'skip', _ORD_L: 'lockout', _ORD_H: 'hold'} c = self.stdscr.getch() if c == ord('u') or self.do_auto_update(): - msg = gr.message().make_from_string('update', -2, 0, 0) - self.output_q.insert_tail(msg) + self.send_command('update', 0) if c in COMMANDS.keys(): - msg = gr.message().make_from_string(COMMANDS[c], -2, 0, 0) - self.output_q.insert_tail(msg) + self.send_command(COMMANDS[c], 0) elif c == ord('q'): return True elif c == ord('t'): if self.current_nac: - msg = gr.message().make_from_string('add_default_config', -2, int(self.current_nac), 0) - self.output_q.insert_tail(msg) + self.send_command('add_default_config', int(self.current_nac)) elif c == ord('f'): self.prompt.addstr(0, 0, 'Frequency') self.prompt.refresh() @@ -108,8 +123,7 @@ class curses_terminal(threading.Thread): except: freq = None if freq: - msg = gr.message().make_from_string('set_freq', -2, freq, 0) - self.output_q.insert_tail(msg) + self.send_command('set_freq', freq) elif c == ord('x'): assert 1 == 0 return False @@ -170,6 +184,13 @@ class curses_terminal(threading.Thread): return self.process_json(msg.to_string()) return False + def send_command(self, command, data): + if self.sock: + self.sock.send(json.dumps({'command': command, 'data': data})) + else: + msg = gr.message().make_from_string(command, -2, data, 0) + self.output_q.insert_tail(msg) + def run(self): try: self.setup_curses() @@ -182,6 +203,99 @@ class curses_terminal(threading.Thread): sys.stderr.write('terminal: exception occurred\n') sys.stderr.write('terminal: exception:\n%s\n' % traceback.format_exc()) finally: - self.end_curses() - msg = gr.message().make_from_string('quit', -2, 0, 0) - self.output_q.insert_tail(msg) + self.end_terminal() + self.keep_running = False + self.send_command('quit', 0) + +class udp_terminal(threading.Thread): + def __init__(self, input_q, output_q, port, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.input_q = input_q + self.output_q = output_q + self.keep_running = True + self.port = port + self.remote_ip = '127.0.0.1' + self.remote_port = 0 + self.keepalive_until = 0 + + self.setup_socket(port) + self.q_handler = q_watcher(self.input_q, self.process_qmsg) + self.start() + + def setup_socket(self, port): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.bind(('0.0.0.0', port)) + + def process_qmsg(self, msg): + if time.time() >= self.keepalive_until: + return + s = msg.to_string() + if msg.type() == -4 and self.remote_port > 0: + self.sock.sendto(s, (self.remote_ip, self.remote_port)) + + def end_terminal(self): + self.keep_running = False + + def run(self): + while self.keep_running: + data, addr = self.sock.recvfrom(2048) + data = json.loads(data) + if data['command'] == 'quit': + self.keepalive_until = 0 + continue + msg = gr.message().make_from_string(str(data['command']), -2, data['data'], 0) + self.output_q.insert_tail(msg) + self.remote_ip = addr[0] + self.remote_port = addr[1] + self.keepalive_until = time.time() + KEEPALIVE_TIME + +def op25_terminal(input_q, output_q, terminal_type): + if terminal_type == 'curses': + return curses_terminal(input_q, output_q) + elif terminal_type[0].isdigit(): + port = int(terminal_type) + return udp_terminal(input_q, output_q, port) + else: + sys.stderr.write('warning: unsupported terminal type: %s\n', terminal_type) + return None + +class terminal_client(object): + def __init__(self): + self.input_q = gr.msg_queue(10) + self.keep_running = True + self.terminal = None + + ip_addr = sys.argv[1] + port = int(sys.argv[2]) + + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.connect((ip_addr, port)) + self.sock.settimeout(0.1) + + self.terminal = curses_terminal(self.input_q, None, sock=self.sock) + + def run(self): + while self.keep_running: + try: + js, addr = self.sock.recvfrom(2048) + msg = gr.message().make_from_string(js, -4, 0, 0) + self.input_q.insert_tail(msg) + except socket.timeout: + pass + except: + raise + if not self.terminal.keep_running: + self.keep_running = False + +if __name__ == '__main__': + terminal = None + try: + terminal = terminal_client() + terminal.run() + except: + sys.stderr.write('terminal: exception occurred\n') + sys.stderr.write('terminal: exception:\n%s\n' % traceback.format_exc()) + finally: + if terminal is not None and terminal.terminal is not None: + terminal.terminal.end_terminal() From 1fc2df5ac1baef135cb1f24a137e3c9754555a12 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:10:50 -0500 Subject: [PATCH 026/102] dstar cfg --- op25/gr-op25_repeater/apps/tx/dstar-cfg.dat | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 op25/gr-op25_repeater/apps/tx/dstar-cfg.dat diff --git a/op25/gr-op25_repeater/apps/tx/dstar-cfg.dat b/op25/gr-op25_repeater/apps/tx/dstar-cfg.dat new file mode 100644 index 0000000..a1451c7 --- /dev/null +++ b/op25/gr-op25_repeater/apps/tx/dstar-cfg.dat @@ -0,0 +1,41 @@ +################################################################################# +# +# config file for DSTAR TX +# +################################################################################# +# +# This file is part of OP25 +# +# 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. +################################################################################# +# +# NOTE +# +# Syntax is unforgiving - no whitespace allowed (outside of comments) +# +################################################################################# +# flags - specify values in hex +flag1=0 +flag2=0 +flag3=0 +# call sign fields - all have an 8-character limit +rptcall2=DIRECT +rptcall1=DIRECT +urcall=CQCQCQ +# # # # # # # # # # # # set your callsign in next line +mycall1=mycall +# appears to be used as a radio model ID - 4-char limit +mycall2=OP25 From dd9bc87c86cad41cbb751f13ca01af081b0d0d83 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:11:56 -0500 Subject: [PATCH 027/102] dv_tx - add check for dstar cfg file --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index 7964190..e37dbd3 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -107,8 +107,8 @@ class my_top_block(gr.top_block): print 'protocol [-p] option missing' sys.exit(0) - if options.protocol == 'ysf' or options.protocol == 'dmr': - assert options.config_file # dmr and ysf require config file ("-c FILENAME" option) + if options.protocol == 'ysf' or options.protocol == 'dmr' or options.protocol == 'dstar': + assert options.config_file # dstar, dmr and ysf require config file ("-c FILENAME" option) output_gain = output_gains[options.protocol] From 90fbe465187d3abc77a35ed7d0db6d11a6a452f9 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:12:37 -0500 Subject: [PATCH 028/102] dstar cfg file --- op25/gr-op25_repeater/apps/tx/multi_tx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/op25/gr-op25_repeater/apps/tx/multi_tx.py b/op25/gr-op25_repeater/apps/tx/multi_tx.py index 9135df9..1b0f432 100755 --- a/op25/gr-op25_repeater/apps/tx/multi_tx.py +++ b/op25/gr-op25_repeater/apps/tx/multi_tx.py @@ -165,6 +165,8 @@ class my_top_block(gr.top_block): cfg = 'dmr-cfg.dat' elif protocols[i] == 'ysf': cfg = 'ysf-cfg.dat' + elif protocols[i] == 'dstar': + cfg = 'dstar-cfg.dat' else: cfg = None From 1b1c8842c156482714386295c2d5123215f14156 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:16:18 -0500 Subject: [PATCH 029/102] add get_freq_error() --- .../include/op25_repeater/gardner_costas_cc.h | 1 + op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc | 5 ++++- op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h b/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h index 261ca11..c7e9089 100644 --- a/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h +++ b/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h @@ -48,6 +48,7 @@ namespace gr { */ static sptr make(float samples_per_symbol, float gain_mu, float gain_omega, float alpha, float beta, float max_freq, float min_freq); virtual void set_omega(float omega) {} + virtual float get_freq_error(void) {} }; } // namespace op25_repeater diff --git a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc index 766652d..94ad643 100644 --- a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc +++ b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc @@ -146,6 +146,9 @@ void gardner_costas_cc_impl::set_omega (float omega) { memset(d_dl, 0, NUM_COMPLEX * sizeof(gr_complex)); } +float gardner_costas_cc_impl::get_freq_error (void) { + return (d_freq); +} void gardner_costas_cc_impl::forecast(int noutput_items, gr_vector_int &ninput_items_required) @@ -261,7 +264,7 @@ gardner_costas_cc_impl::general_work (int noutput_items, float error_real = (d_last_sample.real() - interp_samp.real()) * interp_samp_mid.real(); float error_imag = (d_last_sample.imag() - interp_samp.imag()) * interp_samp_mid.imag(); gr_complex diffdec = interp_samp * conj(d_last_sample); - (void)slicer(std::arg(diffdec)); + // cpu reduction (void)slicer(std::arg(diffdec)); d_last_sample = interp_samp; // save for next time #if 1 float symbol_error = error_real + error_imag; // Gardner loop error diff --git a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h index 7fc8eb7..e39beb1 100644 --- a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h +++ b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h @@ -67,6 +67,7 @@ namespace gr { //! Sets value of omega and its min and max values void set_omega (float omega); + float get_freq_error(void); protected: bool input_sample0(gr_complex, gr_complex& outp); From db1bd03e25faa38d03fd5b46d26b9ed336476f1d Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:22:46 -0500 Subject: [PATCH 030/102] dstar parameters --- op25/gr-op25_repeater/lib/dstar_header.h | 130 ++++++++++++++++++ op25/gr-op25_repeater/lib/dstar_tx_sb_impl.cc | 42 +++++- op25/gr-op25_repeater/lib/dstar_tx_sb_impl.h | 1 + 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 op25/gr-op25_repeater/lib/dstar_header.h diff --git a/op25/gr-op25_repeater/lib/dstar_header.h b/op25/gr-op25_repeater/lib/dstar_header.h new file mode 100644 index 0000000..034bb26 --- /dev/null +++ b/op25/gr-op25_repeater/lib/dstar_header.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2010,2011,2015 by Jonathan Naylor, G4KLX + * + * This program 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; version 2 of the License. + * + * This program 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. + */ + +/* from OpenDV/DStarRepeater */ + +#ifndef INCLUDED_DSTAR_HEADER_H +#define INCLUDED_DSTAR_HEADER_H + +#include +#include +#include + +#include "CCITTChecksumReverse.h" +#include "bit_utils.h" + +static inline uint8_t rev_byte(const uint8_t b) { + uint8_t rc = 0; + for (int i=0; i<8; i++) { + rc |= ((b >> i) & 1) << (7-i); + } + return(rc); +} + +static inline void make_dstar_header( + uint8_t header_data_buf[480], // result (bits) + const uint8_t flag1, const uint8_t flag2, const uint8_t flag3, + const char RptCall2[], + const char RptCall1[], + const char YourCall[], + const char MyCall1[], + const char MyCall2[]) +{ + uint8_t m_headerData[60]; + static const unsigned int SLOW_DATA_BLOCK_SIZE = 6U; + static const unsigned int SLOW_DATA_FULL_BLOCK_SIZE = SLOW_DATA_BLOCK_SIZE * 10U; + static const unsigned char SLOW_DATA_TYPE_HEADER = 0x50U; + + ::memset(m_headerData, 'f', SLOW_DATA_FULL_BLOCK_SIZE); + + m_headerData[0U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[1U] = flag1; + m_headerData[2U] = flag2; + m_headerData[3U] = flag3; + m_headerData[4U] = RptCall2[0]; + m_headerData[5U] = RptCall2[1]; + + m_headerData[6U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[7U] = RptCall2[2]; + m_headerData[8U] = RptCall2[3]; + m_headerData[9U] = RptCall2[4]; + m_headerData[10U] = RptCall2[5]; + m_headerData[11U] = RptCall2[6]; + + m_headerData[12U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[13U] = RptCall2[7]; + m_headerData[14U] = RptCall1[0]; + m_headerData[15U] = RptCall1[1]; + m_headerData[16U] = RptCall1[2]; + m_headerData[17U] = RptCall1[3]; + + m_headerData[18U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[19U] = RptCall1[4]; + m_headerData[20U] = RptCall1[5]; + m_headerData[21U] = RptCall1[6]; + m_headerData[22U] = RptCall1[7]; + m_headerData[23U] = YourCall[0]; + + m_headerData[24U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[25U] = YourCall[1]; + m_headerData[26U] = YourCall[2]; + m_headerData[27U] = YourCall[3]; + m_headerData[28U] = YourCall[4]; + m_headerData[29U] = YourCall[5]; + + m_headerData[30U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[31U] = YourCall[6]; + m_headerData[32U] = YourCall[7]; + m_headerData[33U] = MyCall1[0]; + m_headerData[34U] = MyCall1[1]; + m_headerData[35U] = MyCall1[2]; + + m_headerData[36U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[37U] = MyCall1[3]; + m_headerData[38U] = MyCall1[4]; + m_headerData[39U] = MyCall1[5]; + m_headerData[40U] = MyCall1[6]; + m_headerData[41U] = MyCall1[7]; + + m_headerData[42U] = SLOW_DATA_TYPE_HEADER | 5U; + m_headerData[43U] = MyCall2[0]; + m_headerData[44U] = MyCall2[1]; + m_headerData[45U] = MyCall2[2]; + m_headerData[46U] = MyCall2[3]; + + CCCITTChecksumReverse cksum; + cksum.update(m_headerData + 1U, 5U); + cksum.update(m_headerData + 7U, 5U); + cksum.update(m_headerData + 13U, 5U); + cksum.update(m_headerData + 19U, 5U); + cksum.update(m_headerData + 25U, 5U); + cksum.update(m_headerData + 31U, 5U); + cksum.update(m_headerData + 37U, 5U); + cksum.update(m_headerData + 43U, 4U); + + unsigned char checkSum[2U]; + cksum.result(checkSum); + + m_headerData[47U] = checkSum[0]; + + m_headerData[48U] = SLOW_DATA_TYPE_HEADER | 1U; + m_headerData[49U] = checkSum[1]; + + for (int i=0; i +#include "dstar_header.h" #include #include @@ -52,6 +53,15 @@ static inline void print_result(char title[], const uint8_t r[], int len) { } #endif +static inline void sstring(const char s[], char dest[8]) { + memset(dest, ' ', 8); + memcpy(dest, s, std::min(strlen(s), (size_t)8)); + for (int i=0; i<8; i++) { + if (dest[i] < ' ') + dest [i] = ' '; + } +} + static const uint8_t FS[24] = { 1,0,1,0,1,0,1,0,1,0,1,1,0,1,0,0,0,1,1,0,1,0,0,0 }; static const uint8_t FS_DUMMY[24] = { 0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1 }; @@ -102,8 +112,13 @@ dstar_tx_sb_impl::config() FILE * fp1 = fopen(d_config_file, "r"); char line[256]; char * cp; - // TODO: add code to generate slow speed datastream - return; + int flag1, flag2, flag3; + char rptcall1[8]; + char rptcall2[8]; + char urcall[8]; + char mycall1[8]; + char mycall2[8]; + if (!fp1) { fprintf(stderr, "dstar_tx_sb_impl:config: failed to open %s\n", d_config_file); return; @@ -112,12 +127,25 @@ dstar_tx_sb_impl::config() cp = fgets(line, sizeof(line) - 2, fp1); if (!cp) break; if (line[0] == '#') continue; -#if 0 - if (memcmp(line, "ft=", 3) == 0) - sscanf(&line[3], "%d", &d_ft); -#endif + if (memcmp(line, "flag1=", 6) == 0) + sscanf(&line[6], "%x", &flag1); + else if (memcmp(line, "flag2=", 6) == 0) + sscanf(&line[6], "%x", &flag2); + else if (memcmp(line, "flag3=", 6) == 0) + sscanf(&line[6], "%x", &flag3); + else if (memcmp(line, "rptcall2=", 9) == 0) + sstring(&line[9], rptcall2); + else if (memcmp(line, "rptcall1=", 9) == 0) + sstring(&line[9], rptcall1); + else if (memcmp(line, "urcall=", 7) == 0) + sstring(&line[7], urcall); + else if (memcmp(line, "mycall1=", 8) == 0) + sstring(&line[8], mycall1); + else if (memcmp(line, "mycall2=", 8) == 0) + sstring(&line[8], mycall2); } fclose(fp1); + make_dstar_header(d_dstar_header_data, flag1 & 0xff, flag2 & 0xff, flag3 & 0xff, rptcall2, rptcall1, urcall, mycall1, mycall2); } void @@ -151,7 +179,7 @@ dstar_tx_sb_impl::general_work (int noutput_items, if (d_frame_counter == 0) memcpy(out+72, FS, 24); else - memcpy(out+72, FS_DUMMY, 24); + memcpy(out+72, d_dstar_header_data+((d_frame_counter-1) * 24), 24); d_frame_counter = (d_frame_counter + 1) % 21; in += 160; nconsumed += 160; diff --git a/op25/gr-op25_repeater/lib/dstar_tx_sb_impl.h b/op25/gr-op25_repeater/lib/dstar_tx_sb_impl.h index 4451702..6570161 100644 --- a/op25/gr-op25_repeater/lib/dstar_tx_sb_impl.h +++ b/op25/gr-op25_repeater/lib/dstar_tx_sb_impl.h @@ -60,6 +60,7 @@ namespace gr { const char * d_config_file; ambe_encoder d_encoder; int d_frame_counter; + uint8_t d_dstar_header_data[480]; }; } // namespace op25_repeater From b055a0b6d0f972fa5de952dcc8405d346d90a12d Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:23:49 -0500 Subject: [PATCH 031/102] dstar CRC --- .../lib/CCITTChecksumReverse.cpp | 93 +++++++++++++++++++ .../lib/CCITTChecksumReverse.h | 39 ++++++++ 2 files changed, 132 insertions(+) create mode 100644 op25/gr-op25_repeater/lib/CCITTChecksumReverse.cpp create mode 100644 op25/gr-op25_repeater/lib/CCITTChecksumReverse.h diff --git a/op25/gr-op25_repeater/lib/CCITTChecksumReverse.cpp b/op25/gr-op25_repeater/lib/CCITTChecksumReverse.cpp new file mode 100644 index 0000000..0b5b0fb --- /dev/null +++ b/op25/gr-op25_repeater/lib/CCITTChecksumReverse.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2009,2011,2014 by Jonathan Naylor, G4KLX + * + * This program 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; version 2 of the License. + * + * This program 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. + */ + +#include +#include + +#include "CCITTChecksumReverse.h" + +static const unsigned short ccittTab[] = { + 0x0000,0x1189,0x2312,0x329b,0x4624,0x57ad,0x6536,0x74bf, + 0x8c48,0x9dc1,0xaf5a,0xbed3,0xca6c,0xdbe5,0xe97e,0xf8f7, + 0x1081,0x0108,0x3393,0x221a,0x56a5,0x472c,0x75b7,0x643e, + 0x9cc9,0x8d40,0xbfdb,0xae52,0xdaed,0xcb64,0xf9ff,0xe876, + 0x2102,0x308b,0x0210,0x1399,0x6726,0x76af,0x4434,0x55bd, + 0xad4a,0xbcc3,0x8e58,0x9fd1,0xeb6e,0xfae7,0xc87c,0xd9f5, + 0x3183,0x200a,0x1291,0x0318,0x77a7,0x662e,0x54b5,0x453c, + 0xbdcb,0xac42,0x9ed9,0x8f50,0xfbef,0xea66,0xd8fd,0xc974, + 0x4204,0x538d,0x6116,0x709f,0x0420,0x15a9,0x2732,0x36bb, + 0xce4c,0xdfc5,0xed5e,0xfcd7,0x8868,0x99e1,0xab7a,0xbaf3, + 0x5285,0x430c,0x7197,0x601e,0x14a1,0x0528,0x37b3,0x263a, + 0xdecd,0xcf44,0xfddf,0xec56,0x98e9,0x8960,0xbbfb,0xaa72, + 0x6306,0x728f,0x4014,0x519d,0x2522,0x34ab,0x0630,0x17b9, + 0xef4e,0xfec7,0xcc5c,0xddd5,0xa96a,0xb8e3,0x8a78,0x9bf1, + 0x7387,0x620e,0x5095,0x411c,0x35a3,0x242a,0x16b1,0x0738, + 0xffcf,0xee46,0xdcdd,0xcd54,0xb9eb,0xa862,0x9af9,0x8b70, + 0x8408,0x9581,0xa71a,0xb693,0xc22c,0xd3a5,0xe13e,0xf0b7, + 0x0840,0x19c9,0x2b52,0x3adb,0x4e64,0x5fed,0x6d76,0x7cff, + 0x9489,0x8500,0xb79b,0xa612,0xd2ad,0xc324,0xf1bf,0xe036, + 0x18c1,0x0948,0x3bd3,0x2a5a,0x5ee5,0x4f6c,0x7df7,0x6c7e, + 0xa50a,0xb483,0x8618,0x9791,0xe32e,0xf2a7,0xc03c,0xd1b5, + 0x2942,0x38cb,0x0a50,0x1bd9,0x6f66,0x7eef,0x4c74,0x5dfd, + 0xb58b,0xa402,0x9699,0x8710,0xf3af,0xe226,0xd0bd,0xc134, + 0x39c3,0x284a,0x1ad1,0x0b58,0x7fe7,0x6e6e,0x5cf5,0x4d7c, + 0xc60c,0xd785,0xe51e,0xf497,0x8028,0x91a1,0xa33a,0xb2b3, + 0x4a44,0x5bcd,0x6956,0x78df,0x0c60,0x1de9,0x2f72,0x3efb, + 0xd68d,0xc704,0xf59f,0xe416,0x90a9,0x8120,0xb3bb,0xa232, + 0x5ac5,0x4b4c,0x79d7,0x685e,0x1ce1,0x0d68,0x3ff3,0x2e7a, + 0xe70e,0xf687,0xc41c,0xd595,0xa12a,0xb0a3,0x8238,0x93b1, + 0x6b46,0x7acf,0x4854,0x59dd,0x2d62,0x3ceb,0x0e70,0x1ff9, + 0xf78f,0xe606,0xd49d,0xc514,0xb1ab,0xa022,0x92b9,0x8330, + 0x7bc7,0x6a4e,0x58d5,0x495c,0x3de3,0x2c6a,0x1ef1,0x0f78}; + +CCCITTChecksumReverse::CCCITTChecksumReverse() : +m_crc16(0xFFFFU) +{ +} + +CCCITTChecksumReverse::~CCCITTChecksumReverse() +{ +} + +void CCCITTChecksumReverse::update(const unsigned char* data, unsigned int length) +{ + assert(data != NULL); + + for (unsigned int i = 0U; i < length; i++) + m_crc16 = ((uint16_t)(m_crc8[1U])) ^ ccittTab[m_crc8[0U] ^ data[i]]; +} + +void CCCITTChecksumReverse::result(unsigned char* data) +{ + assert(data != NULL); + + m_crc16 = ~m_crc16; + + data[0U] = m_crc8[0U]; + data[1U] = m_crc8[1U]; +} + +bool CCCITTChecksumReverse::check(const unsigned char* data) +{ + assert(data != NULL); + + unsigned char sum[2U]; + result(sum); + + return sum[0U] == data[0U] && sum[1U] == data[1U]; +} + +void CCCITTChecksumReverse::reset() +{ + m_crc16 = 0xFFFFU; +} diff --git a/op25/gr-op25_repeater/lib/CCITTChecksumReverse.h b/op25/gr-op25_repeater/lib/CCITTChecksumReverse.h new file mode 100644 index 0000000..e60f93c --- /dev/null +++ b/op25/gr-op25_repeater/lib/CCITTChecksumReverse.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2009,2011,2014 by Jonathan Naylor, G4KLX + * + * This program 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; version 2 of the License. + * + * This program 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. + */ + +#ifndef CCITTChecksumReverse_H +#define CCITTChecksumReverse_H + +#include + +class CCCITTChecksumReverse { +public: + CCCITTChecksumReverse(); + ~CCCITTChecksumReverse(); + + void update(const unsigned char* data, unsigned int length); + + void result(unsigned char* data); + + bool check(const unsigned char* data); + + void reset(); + +private: + union { + uint16_t m_crc16; + uint8_t m_crc8[2U]; + }; +}; + +#endif From 5f264c68bf104f1814031b43a210351066902f8d Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 18:24:05 -0500 Subject: [PATCH 032/102] bit utils --- op25/gr-op25_repeater/lib/bit_utils.h | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 op25/gr-op25_repeater/lib/bit_utils.h diff --git a/op25/gr-op25_repeater/lib/bit_utils.h b/op25/gr-op25_repeater/lib/bit_utils.h new file mode 100644 index 0000000..e8c9daa --- /dev/null +++ b/op25/gr-op25_repeater/lib/bit_utils.h @@ -0,0 +1,59 @@ +// +// This file is part of OP25 +// +// OP25 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. +// +// OP25 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 OP25; see the file COPYING. If not, write to the Free +// Software Foundation, Inc., 51 Franklin Street, Boston, MA +// 02110-1301, USA. + +#ifndef INCLUDED_OP25_REPEATER_BIT_UTILS_H +#define INCLUDED_OP25_REPEATER_BIT_UTILS_H + +#include + +static inline void store_i(int reg, uint8_t val[], int len) { + for (int i=0; i> (len-1-i)) & 1; + } +} + +static inline int load_i(const uint8_t val[], int len) { + int acc = 0; + for (int i=0; i> 1) & 1; + bits[i*2+1] = dibits[i] & 1; + } +} + +static inline void bits_to_dibits(uint8_t* dest, const uint8_t* src, int n_dibits) { + for (int i=0; i Date: Fri, 22 Dec 2017 18:29:43 -0500 Subject: [PATCH 033/102] vocoder_impl --- op25/gr-op25_repeater/lib/vocoder_impl.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/op25/gr-op25_repeater/lib/vocoder_impl.cc b/op25/gr-op25_repeater/lib/vocoder_impl.cc index 967dde4..935c243 100644 --- a/op25/gr-op25_repeater/lib/vocoder_impl.cc +++ b/op25/gr-op25_repeater/lib/vocoder_impl.cc @@ -139,13 +139,14 @@ vocoder_impl::general_work_encode (int noutput_items, const int fragments_available = output_queue.size() / FRAGMENT_SIZE; const int nsamples_consume = std::min(ninput_items[0], std::max(0,(noutput_fragments - fragments_available) * 9 * 160)); - if (nsamples_consume > 0) + if (nsamples_consume > 0) { p1voice_encode.compress_samp(in, nsamples_consume); - // Tell runtime system how many input items we consumed on - // each input stream. + // Tell runtime system how many input items we consumed on + // each input stream. - consume_each (nsamples_consume); + consume_each (nsamples_consume); + } if (opt_udp_port > 0) // in udp option, we are a gr sink only return 0; From 5601e5290ee053b1638b27dac5b3fd6935cbd3ab Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 19:11:29 -0500 Subject: [PATCH 034/102] op25_audio --- op25/gr-op25_repeater/lib/op25_audio.cc | 242 ++++++++++++++++++++++++ op25/gr-op25_repeater/lib/op25_audio.h | 70 +++++++ 2 files changed, 312 insertions(+) create mode 100644 op25/gr-op25_repeater/lib/op25_audio.cc create mode 100644 op25/gr-op25_repeater/lib/op25_audio.h diff --git a/op25/gr-op25_repeater/lib/op25_audio.cc b/op25/gr-op25_repeater/lib/op25_audio.cc new file mode 100644 index 0000000..6397bca --- /dev/null +++ b/op25/gr-op25_repeater/lib/op25_audio.cc @@ -0,0 +1,242 @@ +/* -*- c++ -*- */ +/* + * Copyright 2017 Graham J Norbury, gnorbury@bondcar.com + * from op25_audio; rewrite Nov 2017 Copyright 2017 Max H. Parke KA1RBI + * + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "op25_audio.h" + +// convert hostname to ip address +static int hostname_to_ip(const char *hostname , char *ip) +{ + int sockfd; + struct addrinfo hints, *servinfo, *p; + struct sockaddr_in *h; + int rv; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; // use AF_INET6 to force IPv6 + hints.ai_socktype = SOCK_DGRAM; + + if ( (rv = getaddrinfo( hostname , NULL , &hints , &servinfo)) != 0) + { + fprintf(stderr, "op25_audio::hostname_to_ip() getaddrinfo: %s\n", gai_strerror(rv)); + return -1; + } + + // loop through all the results and connect to the first we can + for(p = servinfo; p != NULL; p = p->ai_next) + { + h = (struct sockaddr_in *) p->ai_addr; + if (h->sin_addr.s_addr != 0) + { + strcpy(ip , inet_ntoa( h->sin_addr ) ); + break; + } + } + + freeaddrinfo(servinfo); // all done with this structure + return 0; + +} + +// constructor +op25_audio::op25_audio(const char* udp_host, int port, int debug) : + d_udp_enabled(false), + d_debug(debug), + d_write_port(port), + d_audio_port(port), + d_write_sock(0), + d_file_enabled(false) +{ + char ip[20]; + if (hostname_to_ip(udp_host, ip) == 0) + { + strncpy(d_udp_host, ip, sizeof(d_udp_host)); + d_udp_host[sizeof(d_udp_host)-1] = 0; + if ( port ) + open_socket(); + } +} + +// destructor +op25_audio::~op25_audio() +{ + if (d_file_enabled) + close(d_write_sock); + close_socket(); +} + +// constructor +op25_audio::op25_audio(const char* destination, int debug) : + d_udp_enabled(false), + d_debug(debug), + d_write_port(0), + d_audio_port(0), + d_write_sock(0), + d_file_enabled(false) +{ + static const int DEFAULT_UDP_PORT = 23456; + static const char P_UDP[] = "udp://"; + static const char P_FILE[] = "file://"; + int port = DEFAULT_UDP_PORT; + + if (memcmp(destination, P_UDP, strlen(P_UDP)) == 0) { + const char * p1 = destination+strlen(P_UDP); + strncpy(d_udp_host, p1, sizeof(d_udp_host)); + d_udp_host[sizeof(d_udp_host)-1] = 0; + char * pc = index(d_udp_host, ':'); + if (pc) { + sscanf(pc+1, "%d", &port); + *pc = 0; + } + d_write_port = d_audio_port = port; + open_socket(); + } else if (memcmp(destination, P_FILE, strlen(P_FILE)) == 0) { + const char * filename = destination+strlen(P_FILE); + size_t l = strlen(filename); + if (l > 4 && (strcmp(&filename[l-4], ".wav") == 0 || strcmp(&filename[l-4], ".WAV") == 0)) { + fprintf (stderr, "Warning! Output file %s will be written, but in raw form ***without*** a WAV file header!\n", filename); + } + d_write_sock = open(filename, O_WRONLY | O_CREAT, 0644); + if (d_write_sock < 0) { + fprintf(stderr, "op25_audio::open file %s: error: %d (%s)\n", filename, errno, strerror(errno)); + d_write_sock = 0; + return; + } + d_file_enabled = true; + } +} +// open socket and set up data structures +void op25_audio::open_socket() +{ + memset (&d_sock_addr, 0, sizeof(d_sock_addr)); + + // open handle to socket + d_write_sock = socket(PF_INET, SOCK_DGRAM, 17); // UDP socket + if ( d_write_sock < 0 ) + { + fprintf(stderr, "op25_audio::open_socket(): error: %d\n", errno); + d_write_sock = 0; + return; + } + + // set up data structure for generic udp host/port + if ( !inet_aton(d_udp_host, &d_sock_addr.sin_addr) ) + { + fprintf(stderr, "op25_audio::open_socket(): inet_aton: bad IP address\n"); + close(d_write_sock); + d_write_sock = 0; + return; + } + d_sock_addr.sin_family = AF_INET; + + fprintf(stderr, "op25_audio::open_socket(): enabled udp host(%s), wireshark(%d), audio(%d)\n", d_udp_host, d_write_port, d_audio_port); + d_udp_enabled = true; +} + +// close socket +void op25_audio::close_socket() +{ + if (!d_udp_enabled) + return; + close(d_write_sock); + d_write_sock = 0; + d_udp_enabled = false; +} + +ssize_t op25_audio::do_send(const void * buf, size_t len, int port, bool is_ctrl ) const { + ssize_t rc = 0; + struct sockaddr_in tmp_sockaddr; + if (len <= 0) + return 0; + if (d_udp_enabled) { + memcpy(&tmp_sockaddr, &d_sock_addr, sizeof(struct sockaddr)); + tmp_sockaddr.sin_port = htons(port); + rc = sendto(d_write_sock, buf, len, 0, (struct sockaddr *)&tmp_sockaddr, sizeof(struct sockaddr_in)); + if (rc == -1) + { + fprintf(stderr, "op25_audio::do_send(length %lu): error(%d): %s\n", len, errno, strerror(errno)); + rc = 0; + } + } else if (d_file_enabled && !is_ctrl) { + size_t amt_written = 0; + for (;;) { + rc = write(d_write_sock, amt_written + (char*)buf, len - amt_written); + if (rc < 0) { + fprintf(stderr, "op25_audio::write(length %lu): error(%d): %s\n", len, errno, strerror(errno)); + rc = 0; + } else if (rc == 0) { + fprintf(stderr, "op25_audio::write(length %lu): error, write rc zero\n", len); + } else { + amt_written += rc; + } + if (rc <= 0 || amt_written >= len) + break; + } /* end of for() */ + rc = amt_written; + } + return rc; +} + +// send generic data to destination +ssize_t op25_audio::send_to(const void *buf, size_t len) const +{ + return do_send(buf, len, d_write_port, false); +} + +// send audio data to destination +ssize_t op25_audio::send_audio(const void *buf, size_t len) const +{ + return do_send(buf, len, d_audio_port, false); +} + +// send audio data on specifed channel to destination +ssize_t op25_audio::send_audio_channel(const void *buf, size_t len, ssize_t slot_id) const +{ + return do_send(buf, len, d_audio_port + slot_id*2, false); +} + +// send flag to audio destination +ssize_t op25_audio::send_audio_flag_channel(const udpFlagEnumType udp_flag, ssize_t slot_id) const +{ + char audio_flag[2]; + // 16 bit little endian encoding + audio_flag[0] = (udp_flag & 0x00ff); + audio_flag[1] = ((udp_flag & 0xff00) >> 8); + return do_send(audio_flag, 2, d_audio_port+slot_id, true); +} + +ssize_t op25_audio::send_audio_flag(const op25_audio::udpFlagEnumType udp_flag) const +{ + return send_audio_flag_channel(udp_flag, 0); +} diff --git a/op25/gr-op25_repeater/lib/op25_audio.h b/op25/gr-op25_repeater/lib/op25_audio.h new file mode 100644 index 0000000..dc715a0 --- /dev/null +++ b/op25/gr-op25_repeater/lib/op25_audio.h @@ -0,0 +1,70 @@ +/* -*- c++ -*- */ +/* + * Copyright 2017 Graham J Norbury, gnorbury@bondcar.com + * from op25_audio; rewrite Nov 2017 Copyright 2017 Max H. Parke KA1RBI + * + * 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_OP25_AUDIO_H +#define INCLUDED_OP25_AUDIO_H + +#include +#include +#include +#include + +class op25_audio +{ +public: + enum udpFlagEnumType + { + DRAIN = 0x0000, // play queued pcm frames + DROP = 0x0001 // discard queued pcm frames + }; + +private: + bool d_udp_enabled; + int d_debug; + int d_write_port; + int d_audio_port; + char d_udp_host[64]; + int d_write_sock; + bool d_file_enabled; + struct sockaddr_in d_sock_addr; + + void open_socket(); + void close_socket(); + ssize_t do_send(const void * bufp, size_t len, int port, bool is_ctrl) const; + +public: + op25_audio(const char* udp_host, int port, int debug); + op25_audio(const char* destination, int debug); + ~op25_audio(); + + inline bool enabled() const { return d_udp_enabled; } + + ssize_t send_to(const void *buf, size_t len) const; + + ssize_t send_audio(const void *buf, size_t len) const; + ssize_t send_audio_flag(const udpFlagEnumType udp_flag) const; + + ssize_t send_audio_channel(const void *buf, size_t len, ssize_t slot_id) const; + ssize_t send_audio_flag_channel(const udpFlagEnumType udp_flag, ssize_t slot_id) const; + +}; // class op25_audio + +#endif /* INCLUDED_OP25_AUDIO_H */ From e79d7eaabc3420c5e2197af4f2eea82a9c83d5f6 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 20:08:10 -0500 Subject: [PATCH 035/102] rx_sync additions --- .../include/op25_repeater/CMakeLists.txt | 1 + .../include/op25_repeater/frame_assembler.h | 59 +++ op25/gr-op25_repeater/lib/CMakeLists.txt | 4 + .../lib/frame_assembler_impl.cc | 96 +++++ .../lib/frame_assembler_impl.h | 70 ++++ op25/gr-op25_repeater/lib/rx_sync.cc | 377 ++++++++++++++++++ op25/gr-op25_repeater/lib/rx_sync.h | 122 ++++++ 7 files changed, 729 insertions(+) create mode 100644 op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h create mode 100644 op25/gr-op25_repeater/lib/frame_assembler_impl.cc create mode 100644 op25/gr-op25_repeater/lib/frame_assembler_impl.h create mode 100644 op25/gr-op25_repeater/lib/rx_sync.cc create mode 100644 op25/gr-op25_repeater/lib/rx_sync.h diff --git a/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt b/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt index 0fbd32c..b93833a 100644 --- a/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt +++ b/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt @@ -25,6 +25,7 @@ install(FILES vocoder.h gardner_costas_cc.h p25_frame_assembler.h + frame_assembler.h ambe_encoder_sb.h fsk4_slicer_fb.h DESTINATION include/op25_repeater ) diff --git a/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h b/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h new file mode 100644 index 0000000..086a1ba --- /dev/null +++ b/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h @@ -0,0 +1,59 @@ +/* -*- c++ -*- */ +/* + * Copyright 2017 Max H. Parke KA1RBI + * + * 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_OP25_REPEATER_FRAME_ASSEMBLER_H +#define INCLUDED_OP25_REPEATER_FRAME_ASSEMBLER_H + +#include +#include +#include + +namespace gr { + namespace op25_repeater { + + /*! + * \brief <+description of block+> + * \ingroup op25_repeater + * + */ + class OP25_REPEATER_API frame_assembler : virtual public gr::block + { + public: + typedef boost::shared_ptr sptr; + + /*! + * \brief Return a shared_ptr to a new instance of op25_repeater::frame_assembler. + * + * To avoid accidental use of raw pointers, op25_repeater::frame_assembler's + * constructor is in a private implementation + * class. op25_repeater::frame_assembler::make is the public interface for + * creating new instances. + */ + static sptr make(const char* options, int debug, gr::msg_queue::sptr queue); + virtual void set_xormask(const char*p) {} + virtual void set_slotid(int slotid) {} + }; + + } // namespace op25_repeater +} // namespace gr + +#endif /* INCLUDED_OP25_REPEATER_FRAME_ASSEMBLER_H */ + diff --git a/op25/gr-op25_repeater/lib/CMakeLists.txt b/op25/gr-op25_repeater/lib/CMakeLists.txt index 361f266..8e73154 100644 --- a/op25/gr-op25_repeater/lib/CMakeLists.txt +++ b/op25/gr-op25_repeater/lib/CMakeLists.txt @@ -32,6 +32,7 @@ list(APPEND op25_repeater_sources vocoder_impl.cc gardner_costas_cc_impl.cc p25_frame_assembler_impl.cc + frame_assembler_impl.cc fsk4_slicer_fb_impl.cc ) list(APPEND op25_repeater_sources @@ -52,6 +53,9 @@ list(APPEND op25_repeater_sources ambe.c mbelib.c ambe_encoder.cc + rx_sync.cc + op25_audio.cc + CCITTChecksumReverse.cpp ) add_library(gnuradio-op25_repeater SHARED ${op25_repeater_sources}) diff --git a/op25/gr-op25_repeater/lib/frame_assembler_impl.cc b/op25/gr-op25_repeater/lib/frame_assembler_impl.cc new file mode 100644 index 0000000..8760a1f --- /dev/null +++ b/op25/gr-op25_repeater/lib/frame_assembler_impl.cc @@ -0,0 +1,96 @@ +/* -*- c++ -*- */ +/* + * Copyright 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI + * + * 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 + +#include +#include "frame_assembler_impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gr { + namespace op25_repeater { + + void frame_assembler_impl::set_xormask(const char*p) { + } + + void frame_assembler_impl::set_slotid(int slotid) { + } + + frame_assembler::sptr + frame_assembler::make(const char* options, int debug, gr::msg_queue::sptr queue) + { + return gnuradio::get_initial_sptr + (new frame_assembler_impl(options, debug, queue)); + } + + /* + * The private constructor + */ + + /* + * Our virtual destructor. + */ + frame_assembler_impl::~frame_assembler_impl() + { + } + +static const int MIN_IN = 1; // mininum number of input streams +static const int MAX_IN = 1; // maximum number of input streams + +/* + * The private constructor + */ + frame_assembler_impl::frame_assembler_impl(const char* options, int debug, gr::msg_queue::sptr queue) + : gr::block("frame_assembler", + gr::io_signature::make (MIN_IN, MAX_IN, sizeof (char)), + gr::io_signature::make (0, 0, 0)), + d_msg_queue(queue), + d_sync(options, debug) +{ +} + +int +frame_assembler_impl::general_work (int noutput_items, + gr_vector_int &ninput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items) +{ + + const uint8_t *in = (const uint8_t *) input_items[0]; + + for (int i=0; i + +#include +#include +#include +#include +#include +#include + +#include "rx_sync.h" + +typedef std::deque dibit_queue; + +namespace gr { + namespace op25_repeater { + + class frame_assembler_impl : public frame_assembler + { + private: + int d_debug; + gr::msg_queue::sptr d_msg_queue; + rx_sync d_sync; + + // internal functions + + void queue_msg(int duid); + void set_xormask(const char*p) ; + void set_slotid(int slotid) ; + + public: + + public: + frame_assembler_impl(const char* options, int debug, gr::msg_queue::sptr queue); + ~frame_assembler_impl(); + + // Where all the action really happens + + int general_work(int noutput_items, + gr_vector_int &ninput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items); + }; + + } // namespace op25_repeater +} // namespace gr + +#endif /* INCLUDED_OP25_REPEATER_FRAME_ASSEMBLER_IMPL_H */ diff --git a/op25/gr-op25_repeater/lib/rx_sync.cc b/op25/gr-op25_repeater/lib/rx_sync.cc new file mode 100644 index 0000000..1ca7f8e --- /dev/null +++ b/op25/gr-op25_repeater/lib/rx_sync.cc @@ -0,0 +1,377 @@ +// P25 Decoder (C) Copyright 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +// +// This file is part of OP25 +// +// OP25 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. +// +// OP25 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 OP25; see the file COPYING. If not, write to the Free +// Software Foundation, Inc., 51 Franklin Street, Boston, MA +// 02110-1301, USA. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rx_sync.h" + +#include "bit_utils.h" + +#include "check_frame_sync.h" + +#include "p25p2_vf.h" +#include "mbelib.h" +#include "ambe.h" +#include "rs.h" +#include "crc16.h" + +#include "ysf_const.h" +#include "dmr_const.h" +#include "p25_frame.h" +#include "op25_imbe_frame.h" +#include "software_imbe_decoder.h" +#include "op25_audio.h" + +namespace gr{ + namespace op25_repeater{ + +void rx_sync::cbuf_insert(const uint8_t c) { + d_cbuf[d_cbuf_idx] = c; + d_cbuf[d_cbuf_idx + CBUF_SIZE] = c; + d_cbuf_idx = (d_cbuf_idx + 1) % CBUF_SIZE; +} + +void rx_sync::sync_reset(void) { + d_threshold = 0; + d_shift_reg = 0; + d_unmute_until[0] = 0; + d_unmute_until[1] = 0; +} + +static int ysf_decode_fich(const uint8_t src[100], uint8_t dest[32]) { // input is 100 dibits, result is 32 bits +// return -1 on decode error, else 0 + static const int pc[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1}; + uint8_t buf[100]; + for (int i=0; i<20; i++) { + for (int j=0; j<5; j++) { + buf[j+i*5] = src[i+j*20]; + } + } + uint8_t dr = 0; + uint8_t ans[100]; + /* fake trellis decode */ + /* TODO: make less fake */ + for (int i=0; i<100; i++) { + uint8_t sym = buf[i]; + uint8_t d0 = ((dr << 1) | 0) & 0x1f; + uint8_t r0 = (pc[ d0 & 0x19 ] << 1) + pc[ d0 & 0x17]; + uint8_t d1 = ((dr << 1) | 1) & 0x1f; + uint8_t r1 = (pc[ d1 & 0x19 ] << 1) + pc[ d1 & 0x17]; + if (sym == r0) { + ans[i] = 0; + dr = d0; + } else if (sym == r1) { + ans[i] = 1; + dr = d1; + } else { + return -1; /* decode error */ + } + } + uint8_t fich_bits[12*4]; + store_i(gly24128Dec(load_i(ans+24*0, 24)), fich_bits+12*0, 12); + store_i(gly24128Dec(load_i(ans+24*1, 24)), fich_bits+12*1, 12); + store_i(gly24128Dec(load_i(ans+24*2, 24)), fich_bits+12*2, 12); + store_i(gly24128Dec(load_i(ans+24*3, 24)), fich_bits+12*3, 12); + uint16_t crc_result = crc16(fich_bits, 48); + if (crc_result != 0) + return -1; // crc failure + memcpy(dest, fich_bits, 32); + return 0; +} + +void rx_sync::ysf_sync(const uint8_t dibitbuf[], bool& ysf_fullrate, bool& unmute) { + uint8_t fich_buf[32]; + int rc = ysf_decode_fich(dibitbuf+20, fich_buf); + if (rc == 0) { + uint32_t fich = load_i(fich_buf, 32); + uint32_t dt = (fich >> 8) & 3; + d_shift_reg = dt; + } + switch(d_shift_reg) { + case 0: // voice/data mode 1 + unmute = false; + break; + case 1: // data mode + unmute = false; + break; + case 2: // voice/data mode 2 + unmute = true; + ysf_fullrate = false; + break; + case 3: // voice fr mode + unmute = true; + ysf_fullrate = true; + break; + } + if (d_debug > 5 && !unmute) + fprintf(stderr, "ysf_sync: muting audio: dt: %d, rc: %d\n", d_shift_reg, rc); +} + +void rx_sync::dmr_sync(const uint8_t bitbuf[], int& current_slot, bool& unmute) { + static const int slot_ids[] = {0, 1, 0, 0, 1, 1, 0, 1}; + int tact; + int chan; + int fstype; + uint8_t tactbuf[sizeof(cach_tact_bits)]; + + for (size_t i=0; i>2) & 1; + d_shift_reg = (d_shift_reg << 1) + chan; + current_slot = slot_ids[d_shift_reg & 7]; + + uint64_t sync = load_reg64(bitbuf + (MODE_DATA[RX_TYPE_DMR].sync_offset << 1), MODE_DATA[RX_TYPE_DMR].sync_len); + if (check_frame_sync(DMR_VOICE_SYNC_MAGIC ^ sync, d_threshold, MODE_DATA[RX_TYPE_DMR].sync_len)) + fstype = 1; + else if (check_frame_sync(DMR_IDLE_SYNC_MAGIC ^ sync, d_threshold, MODE_DATA[RX_TYPE_DMR].sync_len)) + fstype = 2; + else + fstype = 0; + if (fstype > 0) + d_expires = d_symbol_count + MODE_DATA[d_current_type].expiration; + if (fstype == 1) { + if (!d_unmute_until[current_slot] && d_debug > 5) + fprintf(stderr, "unmute slot %d\n", current_slot); + d_unmute_until[current_slot] = d_symbol_count + MODE_DATA[d_current_type].expiration; + } else if (fstype == 2) { + if (d_unmute_until[current_slot] && d_debug > 5) + fprintf(stderr, "mute slot %d\n", current_slot); + d_unmute_until[current_slot] = 0; + } + if (d_unmute_until[current_slot] <= d_symbol_count) { + d_unmute_until[current_slot] = 0; + } + unmute = d_unmute_until[current_slot] > 0; +} + +rx_sync::rx_sync(const char * options, int debug) : // constructor + d_symbol_count(0), + d_sync_reg(0), + d_cbuf_idx(0), + d_current_type(RX_TYPE_NONE), + d_rx_count(0), + d_expires(0), + d_stereo(false), + d_debug(debug), + d_audio(options, debug) +{ + mbe_initMbeParms (&cur_mp[0], &prev_mp[0], &enh_mp[0]); + mbe_initMbeParms (&cur_mp[1], &prev_mp[1], &enh_mp[1]); + sync_reset(); +} + +rx_sync::~rx_sync() // destructor +{ +} + + +void rx_sync::codeword(const uint8_t* cw, const enum codeword_types codeword_type, int slot_id) { + static const int x=4; + static const int y=26; + static const uint8_t majority[8] = {0,0,0,1,0,1,1,1}; + + int b[9]; + uint8_t buf[4*26]; + uint8_t tmp_codeword [144]; + uint32_t E0, ET; + uint32_t u[8]; + bool do_fullrate = false; + bool do_silence = false; + voice_codeword fullrate_cw(voice_codeword_sz); + + switch(codeword_type) { + case CODEWORD_DMR: + interleaver.process_vcw(cw, b); + if (b[0] < 120) + mbe_dequantizeAmbe2250Parms(&cur_mp[slot_id], &prev_mp[slot_id], b); + break; + case CODEWORD_DSTAR: + interleaver.decode_dstar(cw, b, false); + if (b[0] < 120) + mbe_dequantizeAmbe2400Parms(&cur_mp[slot_id], &prev_mp[slot_id], b); + break; + case CODEWORD_YSF_HALFRATE: // 104 bits + for (int i=0; i= 120) { + do_silence = true; + } else { + d_software_decoder[slot_id].decode_tap(cur_mp[slot_id].L, 0, cur_mp[slot_id].w0, &cur_mp[slot_id].Vl[1], &cur_mp[slot_id].Ml[1]); + } + } + audio_samples *samples = d_software_decoder[slot_id].audio(); + float snd; + int16_t samp_buf[NSAMP_OUTPUT]; + for (int i=0; i < NSAMP_OUTPUT; i++) { + if ((!do_silence) && samples->size() > 0) { + snd = samples->front(); + samples->pop_front(); + } else { + snd = 0; + } + if (do_fullrate) + snd *= 32768.0; + samp_buf[i] = snd; + } + output(samp_buf, slot_id); +} + +void rx_sync::output(int16_t * samp_buf, const ssize_t slot_id) { + if (!d_stereo) { + d_audio.send_audio_channel(samp_buf, NSAMP_OUTPUT * sizeof(int16_t), slot_id); + return; + } +} + +void rx_sync::rx_sym(const uint8_t sym) +{ + uint8_t bitbuf[864*2]; + enum rx_types sync_detected = RX_TYPE_NONE; + int current_slot; + bool unmute; + uint8_t tmpcw[144]; + bool ysf_fullrate; + + d_symbol_count ++; + d_sync_reg = (d_sync_reg << 2) | (sym & 3); + for (int i = 1; i < RX_N_TYPES; i++) { + if (check_frame_sync(MODE_DATA[i].sync ^ d_sync_reg, (i == d_current_type) ? d_threshold : 0, MODE_DATA[i].sync_len)) { + sync_detected = (enum rx_types) i; + break; + } + } + cbuf_insert(sym); + if (d_current_type == RX_TYPE_NONE && sync_detected == RX_TYPE_NONE) + return; + d_rx_count ++; + if (sync_detected != RX_TYPE_NONE) { + if (d_current_type != sync_detected) { + d_current_type = sync_detected; + d_expires = d_symbol_count + MODE_DATA[d_current_type].expiration; + d_rx_count = 0; + } + if (d_rx_count != MODE_DATA[d_current_type].sync_offset + (MODE_DATA[d_current_type].sync_len >> 1)) { + if (d_debug) + fprintf(stderr, "resync at count %d for protocol %s\n", d_rx_count, MODE_DATA[d_current_type].type); + sync_reset(); + d_rx_count = MODE_DATA[d_current_type].sync_offset + (MODE_DATA[d_current_type].sync_len >> 1); + } else { + d_threshold = std::min(d_threshold + 1, 2); + } + d_expires = d_symbol_count + MODE_DATA[d_current_type].expiration; + } + if (d_symbol_count >= d_expires) { + if (d_debug) + fprintf(stderr, "%s: timeout, symbol %d\n", MODE_DATA[d_current_type].type, d_symbol_count); + d_current_type = RX_TYPE_NONE; + return; + } + if (d_rx_count < MODE_DATA[d_current_type].fragment_len) + return; + d_rx_count = 0; + int start_idx = d_cbuf_idx + CBUF_SIZE - MODE_DATA[d_current_type].fragment_len; + assert (start_idx >= 0); + uint8_t * symbol_ptr = d_cbuf+start_idx; + uint8_t * bit_ptr = symbol_ptr; + if (d_current_type != RX_TYPE_DSTAR) { + dibits_to_bits(bitbuf, symbol_ptr, MODE_DATA[d_current_type].fragment_len); + bit_ptr = bitbuf; + } + switch (d_current_type) { + case RX_TYPE_NONE: + break; + case RX_TYPE_P25: + for (unsigned int codeword_ct=0; codeword_ct < nof_voice_codewords; codeword_ct++) { + for (unsigned int i=0; i +#include +#include +#include +#include +#include +#include +#include + +#include "bit_utils.h" +#include "check_frame_sync.h" + +#include "p25p2_vf.h" +#include "mbelib.h" +#include "ambe.h" + +#include "ysf_const.h" +#include "dmr_const.h" +#include "p25_frame.h" +#include "op25_imbe_frame.h" +#include "software_imbe_decoder.h" +#include "op25_audio.h" + +namespace gr{ + namespace op25_repeater{ + +static const uint64_t DSTAR_FRAME_SYNC_MAGIC = 0x444445101440LL; // expanded into dibits + +enum rx_types { + RX_TYPE_NONE=0, + RX_TYPE_P25, + RX_TYPE_DMR, + RX_TYPE_DSTAR, + RX_TYPE_YSF, + RX_N_TYPES +}; // also used as array index + +static const struct _mode_data { + const char * type; + int sync_len; + int sync_offset; + int fragment_len; // symbols + int expiration; + uint64_t sync; +} MODE_DATA[RX_N_TYPES] = { + {"NONE", 0,0,0,0,0}, + {"P25", 48,0,864,1728, P25_FRAME_SYNC_MAGIC}, + {"DMR", 48,66,144,1728, DMR_VOICE_SYNC_MAGIC}, + {"DSTAR", 48,72,96,2016*2, DSTAR_FRAME_SYNC_MAGIC}, + {"YSF", 40,0,480,480*2, YSF_FRAME_SYNC_MAGIC} +}; // index order must match rx_types enum + +enum codeword_types { + CODEWORD_P25P1, + CODEWORD_P25P2, + CODEWORD_DMR, + CODEWORD_DSTAR, + CODEWORD_YSF_FULLRATE, + CODEWORD_YSF_HALFRATE +}; + +class rx_sync { +public: + void rx_sym(const uint8_t sym); + void sync_reset(void); + rx_sync(const char * options, int debug); + ~rx_sync(); +private: + void cbuf_insert(const uint8_t c); + void dmr_sync(const uint8_t bitbuf[], int& current_slot, bool& unmute); + void ysf_sync(const uint8_t dibitbuf[], bool& ysf_fullrate, bool& unmute); + void codeword(const uint8_t* cw, const enum codeword_types codeword_type, int slot_id); + void output(int16_t * samp_buf, const ssize_t slot_id); + static const int CBUF_SIZE=864; + static const int NSAMP_OUTPUT = 160; + + unsigned int d_symbol_count; + uint64_t d_sync_reg; + uint8_t d_cbuf[CBUF_SIZE*2]; + unsigned int d_cbuf_idx; + enum rx_types d_current_type; + int d_threshold; + int d_rx_count; + unsigned int d_expires; + int d_shift_reg; + unsigned int d_unmute_until[2]; + p25p2_vf interleaver; + mbe_parms cur_mp[2]; + mbe_parms prev_mp[2]; + mbe_parms enh_mp[2]; + software_imbe_decoder d_software_decoder[2]; + std::deque d_output_queue[2]; + bool d_stereo; + int d_debug; + op25_audio d_audio; +}; + + } // end namespace op25_repeater +} // end namespace gr +#endif // INCLUDED_RX_SYNC_H From e9a6e3806f712987f8e7986da991620a8c18442c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 20:09:37 -0500 Subject: [PATCH 036/102] rx_sync swig interface --- op25/gr-op25_repeater/swig/op25_repeater_swig.i | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/op25/gr-op25_repeater/swig/op25_repeater_swig.i b/op25/gr-op25_repeater/swig/op25_repeater_swig.i index 1f75c5c..3ad3540 100644 --- a/op25/gr-op25_repeater/swig/op25_repeater_swig.i +++ b/op25/gr-op25_repeater/swig/op25_repeater_swig.i @@ -11,6 +11,7 @@ #include "op25_repeater/vocoder.h" #include "op25_repeater/gardner_costas_cc.h" #include "op25_repeater/p25_frame_assembler.h" +#include "op25_repeater/frame_assembler.h" #include "op25_repeater/fsk4_slicer_fb.h" #include "op25_repeater/ambe_encoder_sb.h" #include "op25_repeater/dmr_bs_tx_bb.h" @@ -35,8 +36,12 @@ GR_SWIG_BLOCK_MAGIC2(op25_repeater, vocoder); %include "op25_repeater/gardner_costas_cc.h" GR_SWIG_BLOCK_MAGIC2(op25_repeater, gardner_costas_cc); + %include "op25_repeater/p25_frame_assembler.h" GR_SWIG_BLOCK_MAGIC2(op25_repeater, p25_frame_assembler); +%include "op25_repeater/frame_assembler.h" +GR_SWIG_BLOCK_MAGIC2(op25_repeater, frame_assembler); + %include "op25_repeater/fsk4_slicer_fb.h" GR_SWIG_BLOCK_MAGIC2(op25_repeater, fsk4_slicer_fb); From 682bd4abcb9e97c3ef96b68aa8b179fef4f043d3 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 Dec 2017 20:15:47 -0500 Subject: [PATCH 037/102] crc16.h --- op25/gr-op25_repeater/lib/crc16.h | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 op25/gr-op25_repeater/lib/crc16.h diff --git a/op25/gr-op25_repeater/lib/crc16.h b/op25/gr-op25_repeater/lib/crc16.h new file mode 100644 index 0000000..77f9dd6 --- /dev/null +++ b/op25/gr-op25_repeater/lib/crc16.h @@ -0,0 +1,35 @@ +/* + * This file is part of OP25 + * + * 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_CRC16_H +#define INCLUDED_CRC16_H + +static inline uint16_t crc16(const uint8_t buf[], int len) { + uint32_t poly = (1<<12) + (1<<5) + (1<<0); + uint32_t crc = 0; + for(int i=0; i Date: Sat, 23 Dec 2017 18:54:48 -0500 Subject: [PATCH 038/102] dv_tx resamp hack --- op25/gr-op25_repeater/apps/tx/dv_tx.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py index e37dbd3..1018006 100755 --- a/op25/gr-op25_repeater/apps/tx/dv_tx.py +++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py @@ -79,6 +79,7 @@ class my_top_block(gr.top_block): parser = OptionParser(option_class=eng_option) parser.add_option("-a", "--args", type="string", default="", help="device args") + parser.add_option("-A", "--alt-modulator-rate", type="int", default=50000, help="when mod rate is not a submutiple of IF rate") parser.add_option("-b", "--bt", type="float", default=0.5, help="specify bt value") parser.add_option("-c", "--config-file", type="string", default=None, help="specify the config file name") parser.add_option("-f", "--file1", type="string", default=None, help="specify the input file slot 1") @@ -95,7 +96,7 @@ class my_top_block(gr.top_block): parser.add_option("-Q", "--frequency", type="float", default=0.0, help="Hz") parser.add_option("-r", "--repeat", action="store_true", default=False, help="input file repeat") parser.add_option("-R", "--fullrate-mode", action="store_true", default=False, help="ysf fullrate") - parser.add_option("-s", "--modulator-rate", type="int", default=48000, help="must be submultiple of IF rate") + parser.add_option("-s", "--modulator-rate", type="int", default=48000, help="must be submultiple of IF rate - see also -A") parser.add_option("-S", "--alsa-rate", type="int", default=48000, help="sound source/sink sample rate") parser.add_option("-t", "--test", type="string", default=None, help="test pattern symbol file") parser.add_option("-v", "--verbose", type="int", default=0, help="additional output") @@ -194,14 +195,22 @@ class my_top_block(gr.top_block): self.connect(ENCODER, SYMBOL_SINK) if options.args: + self.setup_sdr_output(options, mod_adjust[options.protocol]) f1 = float(options.if_rate) / options.modulator_rate i1 = int(options.if_rate / options.modulator_rate) if f1 - i1 > 1e-3: - print '*** Error, sdr rate %d not an integer multiple of modulator rate %d - ratio=%f' % (options.if_rate, options.modulator_rate, f1) - sys.exit(1) - self.setup_sdr_output(options, mod_adjust[options.protocol]) - interp = filter.rational_resampler_fff(options.if_rate / options.modulator_rate, 1) - self.connect(MOD, AMP, interp, self.fm_modulator, self.u) + f1 = float(options.if_rate) / options.alt_modulator_rate + i1 = int(options.if_rate / options.alt_modulator_rate) + if f1 - i1 > 1e-3: + print '*** Error, sdr rate %d not an integer multiple of alt modulator rate %d - ratio=%f' % (options.if_rate, options.alt_modulator_rate, f1) + sys.exit(0) + a_resamp = filter.pfb.arb_resampler_fff(options.alt_modulator_rate / float(options.modulator_rate)) + sys.stderr.write('adding resampler for rate change %d ===> %d\n' % (options.modulator_rate, options.alt_modulator_rate)) + interp = filter.rational_resampler_fff(options.if_rate / options.alt_modulator_rate, 1) + self.connect(MOD, AMP, a_resamp, interp, self.fm_modulator, self.u) + else: + interp = filter.rational_resampler_fff(options.if_rate / options.modulator_rate, 1) + self.connect(MOD, AMP, interp, self.fm_modulator, self.u) else: self.connect(MOD, AMP, OUT) From 18e96a230760fcd48660c82fc9c82f4dd2ccbf09 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 25 Dec 2017 19:36:35 -0500 Subject: [PATCH 039/102] update readme --- op25/gr-op25_repeater/apps/README | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/op25/gr-op25_repeater/apps/README b/op25/gr-op25_repeater/apps/README index f02f687..ed92ea5 100644 --- a/op25/gr-op25_repeater/apps/README +++ b/op25/gr-op25_repeater/apps/README @@ -52,6 +52,19 @@ to suspend the program and examine stderr.2 for error messages. If there is a traceback please report the full traceback (and the command line) to the mail list. +REMOTE TERMINAL +=============== +Adding (for example) "-l 56111" to the rx.py command starts rx.py but does +not attach a curses terminal. Instead the program runs as normal in the +foreground (hit CTRL-C to end rx.py as desired). To connect to a running +instance of rx.py, (in this example) + ./terminal.py 127.0.0.1 56111 +NOTE: rx.py and terminal.py need not run on the same machine. The machine +where terminal.py is running need not have an SDR device directly attached; +but GNU Radio (and OP25) must be available. + +WARNING: there is no security or encryption on the UDP port. + EXTERNAL UDP AUDIO SERVER ========================= Starting rx.py with the "-w -W host" options directs udp audio data to From cd16c15c54371b72b7c7a4c7942b8e05f4c535ab Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 25 Dec 2017 20:13:43 -0500 Subject: [PATCH 040/102] readme update II --- op25/gr-op25_repeater/apps/README | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/op25/gr-op25_repeater/apps/README b/op25/gr-op25_repeater/apps/README index ed92ea5..031bc6f 100644 --- a/op25/gr-op25_repeater/apps/README +++ b/op25/gr-op25_repeater/apps/README @@ -174,3 +174,42 @@ Options: -2, --phase2-tdma enable phase2 tdma decode -Z DECIM_AMT, --decim-amt=DECIM_AMT spectrum decimation + +MULTI-RECEIVER +============== +The multi_rx.py app allows an arbitrary number of SDR devices and channels +to be defined. Each channel may have one or more plot windows attached. + +Configuration is achieved via a json file (see cfg.json for an example). +In this version, channels are automatically assigned to the first device +found whose frequency span includes the selected frequency. + +As of this writing (winter, 2017), neither trunking nor P25 P2/TDMA are +supported in multi_rx.py. The rx.py app should be used for P25 trunking, +for either P1/FDMA or P2/TDMA. + +Below is a summary of the major config file keys: +demod_type: 'cqpsk' for qpsk p25 only; 'fsk4' for ysf/dstar/dmr/fsk4 p25 +filter_type: 'rc' for p25; 'rrc' for dmr and ysf; 'gmsk' for d-star +plot: 'fft', 'constellation', 'datascope', 'symbol' + [if more than one plot desired, provide a comma-separated list] +destination: 'udp://host:port' or 'file://' +name: arbitrary string used to identify channels and devices + +bug: 'fft' and 'constellation' currently mutually exclusive +bug: 'gmsk' needs work + +Note: DMR audio for the second time slot is sent on the specified port number +plus two. In the example 'udp://127.0.0.1:56122', audio for the first slot +would use 56122; and 56124 for the second. + +The command line options for multi_rx: +Usage: multi_rx.py [options] + +Options: + -h, --help show this help message and exit + -c CONFIG_FILE, --config-file=CONFIG_FILE + specify config file name + -v VERBOSITY, --verbosity=VERBOSITY + message debug level + -p, --pause block on startup From 9b93b5699d89128bf07f95ee69f77ed640b9bdb8 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 25 Dec 2017 20:15:10 -0500 Subject: [PATCH 041/102] multi rx --- op25/gr-op25_repeater/apps/cfg.json | 49 ++++++ op25/gr-op25_repeater/apps/multi_rx.py | 217 +++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 op25/gr-op25_repeater/apps/cfg.json create mode 100755 op25/gr-op25_repeater/apps/multi_rx.py diff --git a/op25/gr-op25_repeater/apps/cfg.json b/op25/gr-op25_repeater/apps/cfg.json new file mode 100644 index 0000000..9a7b634 --- /dev/null +++ b/op25/gr-op25_repeater/apps/cfg.json @@ -0,0 +1,49 @@ +{ + "channels": [ + { + "demod_type": "cqpsk", + "destination": "udp://127.0.0.1:56120", + "excess_bw": 0.2, + "filter_type": "rc", + "frequency": 460412500, + "if_rate": 24000, + "name": "p25", + "plot": "symbol", + "symbol_rate": 4800 + }, + { + "demod_type": "fsk4", + "destination": "file:///tmp/out1.raw", + "excess_bw": 0.2, + "filter_type": "rrc", + "frequency": 460500000, + "if_rate": 24000, + "name": "ysf", + "plot": "datascope", + "symbol_rate": 4800 + }, + { + "demod_type": "fsk4", + "destination": "udp://127.0.0.1:56122", + "excess_bw": 0.2, + "filter_type": "rrc", + "frequency": 460050000, + "if_rate": 24000, + "name": "dmr", + "plot": "symbol", + "symbol_rate": 4800 + } + ], + "devices": [ + { + "args": "rtl:0", + "frequency": 460100000, + "gains": "lna:49", + "name": "rtl0", + "offset": 0, + "ppm": 38, + "rate": 1000000, + "tunable": false + } + ] +} diff --git a/op25/gr-op25_repeater/apps/multi_rx.py b/op25/gr-op25_repeater/apps/multi_rx.py new file mode 100755 index 0000000..96d03c9 --- /dev/null +++ b/op25/gr-op25_repeater/apps/multi_rx.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python + +# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +# +# This file is part of OP25 +# +# OP25 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. +# +# OP25 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 OP25; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Boston, MA +# 02110-1301, USA. + +import os +import sys +import threading +import time +import json +import traceback +import osmosdr + +from gnuradio import audio, eng_notation, gr, gru, filter, blocks, fft, analog, digital +from gnuradio.eng_option import eng_option +from math import pi +from optparse import OptionParser + +import op25 +import op25_repeater +import p25_demodulator +import p25_decoder + +from gr_gnuplot import constellation_sink_c +from gr_gnuplot import fft_sink_c +from gr_gnuplot import symbol_sink_f +from gr_gnuplot import eye_sink_f + +os.environ['IMBE'] = 'soft' + +_def_symbol_rate = 4800 + +# The P25 receiver +# + +class device(object): + def __init__(self, config): + speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000] + + self.name = config['name'] + + sys.stderr.write('device: %s\n' % config) + if config['args'].startswith('rtl') and config['rate'] not in speeds: + sys.stderr.write('WARNING: requested sample rate %d for device %s may not\n' % (config['rate'], config['name'])) + sys.stderr.write("be optimal. You may want to use one of the following rates\n") + sys.stderr.write('%s\n' % speeds) + self.src = osmosdr.source(config['args']) + + for tup in config['gains'].split(','): + name, gain = tup.split(':') + self.src.set_gain(int(gain), name) + + self.src.set_freq_corr(config['ppm']) + self.ppm = config['ppm'] + + self.src.set_sample_rate(config['rate']) + self.sample_rate = config['rate'] + + self.src.set_center_freq(config['frequency']) + self.frequency = config['frequency'] + + self.offset = config['offset'] + +class channel(object): + def __init__(self, config, dev, verbosity): + sys.stderr.write('channel (dev %s): %s\n' % (dev.name, config)) + self.device = dev + self.name = config['name'] + self.symbol_rate = _def_symbol_rate + if 'symbol_rate' in config.keys(): + self.symbol_rate = config['symbol_rate'] + self.config = config + self.demod = p25_demodulator.p25_demod_cb( + input_rate = dev.sample_rate, + demod_type = config['demod_type'], + filter_type = config['filter_type'], + excess_bw = config['excess_bw'], + relative_freq = dev.frequency + dev.offset - config['frequency'], + offset = dev.offset, + if_rate = config['if_rate'], + symbol_rate = self.symbol_rate) + q = gr.msg_queue(1) + self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q) + + self.kill_sink = [] + + if 'plot' not in config.keys(): + return + + self.sinks = [] + for plot in config['plot'].split(','): + # fixme: allow multiple complex consumers (fft and constellation currently mutually exclusive) + if plot == 'datascope': + assert config['demod_type'] == 'fsk4' ## datascope plot requires fsk4 demod type + sink = eye_sink_f(sps=config['if_rate'] / self.symbol_rate) + self.demod.connect_bb('symbol_filter', sink) + self.kill_sink.append(sink) + elif plot == 'symbol': + sink = symbol_sink_f() + self.demod.connect_float(sink) + self.kill_sink.append(sink) + elif plot == 'fft': + i = len(self.sinks) + self.sinks.append(fft_sink_c()) + self.demod.connect_complex('src', self.sinks[i]) + self.kill_sink.append(self.sinks[i]) + elif plot == 'constellation': + i = len(self.sinks) + assert config['demod_type'] == 'cqpsk' ## constellation plot requires cqpsk demod type + self.sinks.append(constellation_sink_c()) + self.demod.connect_complex('diffdec', self.sinks[i]) + self.kill_sink.append(self.sinks[i]) + else: + sys.stderr.write('unrecognized plot type %s\n' % plot) + return + +class rx_block (gr.top_block): + + # Initialize the receiver + # + def __init__(self, verbosity, config): + self.verbosity = verbosity + gr.top_block.__init__(self) + self.device_id_by_name = {} + self.configure_devices(config['devices']) + self.configure_channels(config['channels']) + + def configure_devices(self, config): + self.devices = [] + for cfg in config: + self.device_id_by_name[cfg['name']] = len(self.devices) + self.devices.append(device(cfg)) + + def find_device(self, chan): + for dev in self.devices: + d = abs(chan['frequency'] - dev.frequency) + nf = dev.sample_rate / 2 + if d + 6250 <= nf: + return dev + return None + + def configure_channels(self, config): + self.channels = [] + for cfg in config: + dev = self.find_device(cfg) + if dev is None: + sys.stderr.write('* * * Frequency %d not within spectrum band of any device - ignoring!\n' % cfg['frequency']) + continue + chan = channel(cfg, dev, self.verbosity) + self.channels.append(chan) + self.connect(dev.src, chan.demod, chan.decoder) + + def scan_channels(self): + for chan in self.channels: + sys.stderr.write('scan %s: error %d\n' % (chan.config['frequency'], chan.demod.get_freq_error())) + +class rx_main(object): + def __init__(self): + def byteify(input): # thx so + if isinstance(input, dict): + return {byteify(key): byteify(value) + for key, value in input.iteritems()} + elif isinstance(input, list): + return [byteify(element) for element in input] + elif isinstance(input, unicode): + return input.encode('utf-8') + else: + return input + + self.keep_running = True + + # command line argument parsing + parser = OptionParser(option_class=eng_option) + parser.add_option("-c", "--config-file", type="string", default=None, help="specify config file name") + parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") + parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") + (options, args) = parser.parse_args() + + # wait for gdb + if options.pause: + print 'Ready for GDB to attach (pid = %d)' % (os.getpid(),) + raw_input("Press 'Enter' to continue...") + + if options.config_file == '-': + config = json.loads(sys.stdin.read()) + else: + config = json.loads(open(options.config_file).read()) + self.tb = rx_block(options.verbosity, config = byteify(config)) + + def run(self): + try: + self.tb.start() + while self.keep_running: + time.sleep(1) + except: + sys.stderr.write('main: exception occurred\n') + sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc()) + +if __name__ == "__main__": + rx = rx_main() + rx.run() From d25d93cf9e9b870e9e1440d9aaa7c9239fda56b6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 Dec 2017 21:16:07 -0500 Subject: [PATCH 042/102] p25 p2/tdma bugfix, code cleanups --- op25/gr-op25_repeater/apps/rx.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 6717d32..0206b50 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -184,7 +184,7 @@ class p25_rx_block (gr.top_block): # setup (read-only) attributes self.symbol_rate = 4800 self.symbol_deviation = 600.0 - self.basic_rate = 48000 + self.basic_rate = 24000 _default_speed = 4800 # keep track of flow graph connections @@ -529,15 +529,6 @@ class p25_rx_block (gr.top_block): pickle.dump(self.info, f) f.close() - # Adjust the channel offset - # - def adjust_channel_offset(self, delta_hz): - max_delta_hz = 12000.0 - delta_hz *= self.symbol_deviation - delta_hz = max(delta_hz, -max_delta_hz) - delta_hz = min(delta_hz, max_delta_hz) - self.channel_filter.set_center_freq(self.channel_offset - delta_hz+ self.options.offset) - def open_ifile(self, capture_rate, gain, input_filename, file_seek): speed = 96000 # TODO: fixme ifile = blocks.file_source(gr.sizeof_gr_complex, input_filename, 1) @@ -588,19 +579,6 @@ class p25_rx_block (gr.top_block): # except Exception, x: # wx.MessageBox("Cannot open USRP: " + x.message, "USRP Error", wx.CANCEL | wx.ICON_EXCLAMATION) - # Set the channel offset - # - def set_channel_offset(self, offset_hz, scale, units): - self.channel_offset = -offset_hz - self.channel_filter.set_center_freq(self.channel_offset+ self.options.offset) - self.frame.SetStatusText("Channel offset: " + str(offset_hz * scale) + units, 1) - - # Set the RF squelch threshold level - # - def set_squelch_threshold(self, squelch_db): - self.squelch.set_threshold(squelch_db) - self.frame.SetStatusText("Squelch: " + str(squelch_db) + "dB", 2) - def process_qmsg(self, msg): # return true = end top block RX_COMMANDS = 'skip lockout hold' From 38b2cb60befdd7d800f2b1f20da3a475ba27ef8e Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 8 Jan 2018 11:09:10 -0500 Subject: [PATCH 043/102] partial workaround for excess cpu usage --- op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc index 1c1acf7..dba0aa9 100644 --- a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc +++ b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc @@ -113,6 +113,7 @@ p25_frame_assembler_impl::forecast(int nof_output_items, gr_vector_int &nof_inpu nof_samples_reqd = nof_output_items; if (d_do_audio_output) nof_samples_reqd = 0.6 * nof_output_items; + nof_samples_reqd = std::max(nof_samples_reqd, 256); std::fill(&nof_input_items_reqd[0], &nof_input_items_reqd[nof_inputs], nof_samples_reqd); } From 25ae933b47ee32ad0bf0d81f76242d2384e337f2 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 8 Jan 2018 11:30:15 -0500 Subject: [PATCH 044/102] possible fixes for two bugs in trunked WAV file logger --- op25/gr-op25_repeater/apps/trunking.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index aaf28b7..204318d 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -787,8 +787,6 @@ class rx_ctl (object): index = tdma_slot if tdma_slot is None: index = 0 - filename = 'idle-channel-%d-%d-%f.wav' % (frequency, index, curr_time) - decoder.set_output(filename, index=index) self.working_frequencies[frequency]['tgids'].pop(tgid) print '%f release tgid %d frequency %d' % (curr_time, tgid, frequency) @@ -822,6 +820,9 @@ class rx_ctl (object): else: #active_tdma_slots = [tgids[tg]['tdma_slot'] for tg in tgids] print '%f new tgid %d slot %s arriving on already active frequency %d' % (curr_time, tgid, tdma_slot, frequency) + previous_tgid = [id for id in tgids if tgids[id]['tdma_slot'] == tdma_slot] + assert len(previous_tgid) == 1 ## check for logic error + self.free_talkgroup(frequency, previous_tgid[0], curr_time) worker = self.working_frequencies[frequency]['worker'] else: worker = self.find_available_worker() From ee399ed96236fe5bceb5d520adc9023a09183bdf Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 14 Jan 2018 11:09:19 -0500 Subject: [PATCH 045/102] multi_tx: 2 bugfixes --- op25/gr-op25_repeater/apps/tx/multi_tx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/tx/multi_tx.py b/op25/gr-op25_repeater/apps/tx/multi_tx.py index 1b0f432..e438695 100755 --- a/op25/gr-op25_repeater/apps/tx/multi_tx.py +++ b/op25/gr-op25_repeater/apps/tx/multi_tx.py @@ -61,10 +61,11 @@ class pipeline(gr.hier_block2): alt_input = self self.connect(alt_input, ENCODER2, (DMR, 1)) elif protocol == 'dstar': - ENCODER = op25_repeater.dstar_tx_sb(verbose, None) + assert config_file + ENCODER = op25_repeater.dstar_tx_sb(verbose, config_file) elif protocol == 'p25': ENCODER = op25_repeater.vocoder(True, # 0=Decode,True=Encode - 0, # Verbose flag + False, # Verbose flag 0, # flex amount "", # udp ip address 0, # udp port From dc5b77ff9bb7df59cb94ffbe3887ebb17832f934 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 15 Jan 2018 13:42:50 -0500 Subject: [PATCH 046/102] sockaudio.py thx boatbod --- op25/gr-op25_repeater/apps/sockaudio.py | 40 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/op25/gr-op25_repeater/apps/sockaudio.py b/op25/gr-op25_repeater/apps/sockaudio.py index d0f7432..e569b9b 100755 --- a/op25/gr-op25_repeater/apps/sockaudio.py +++ b/op25/gr-op25_repeater/apps/sockaudio.py @@ -2,6 +2,8 @@ # Copyright 2017 Graham Norbury # +# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +# # This file is part of OP25 # # OP25 is free software; you can redistribute it and/or modify it @@ -28,7 +30,7 @@ import errno # OP25 defaults PCM_RATE = 8000 # audio sample rate (Hz) -PCM_BUFFER_SIZE = 2000 # size of ALSA buffer in frames +PCM_BUFFER_SIZE = 4000 # size of ALSA buffer in frames MAX_SUPERFRAME_SIZE = 320 # maximum size of incoming UDP audio buffer @@ -128,7 +130,7 @@ class alsasound(object): self.format = pcm_format self.channels = pcm_channels self.rate = pcm_rate - pcm_start_threshold = int(pcm_buffer_size * 0.75) + pcm_buf_sz = c_ulong(pcm_buffer_size) c_pars = (c_void_p * int(self.libasound.snd_pcm_hw_params_sizeof() / sizeof(c_void_p)))() err = self.libasound.snd_pcm_hw_params_any(self.c_pcm, c_pars) @@ -136,40 +138,43 @@ class alsasound(object): sys.stderr.write("hw_params_any failed: %d\n" % err) return err - err = self.libasound.snd_pcm_hw_params_set_access(self.c_pcm, c_pars, SND_PCM_ACCESS_RW_INTERLEAVED); + err = self.libasound.snd_pcm_hw_params_set_access(self.c_pcm, c_pars, SND_PCM_ACCESS_RW_INTERLEAVED) if err < 0: sys.stderr.write("set_access failed: %d\n" % err) return err - err = self.libasound.snd_pcm_hw_params_set_format(self.c_pcm, c_pars, c_uint(self.format)); + err = self.libasound.snd_pcm_hw_params_set_format(self.c_pcm, c_pars, c_uint(self.format)) if err < 0: sys.stderr.write("set_format failed: %d\n" % err) return err - err = self.libasound.snd_pcm_hw_params_set_channels(self.c_pcm, c_pars, c_uint(self.channels)); + err = self.libasound.snd_pcm_hw_params_set_channels(self.c_pcm, c_pars, c_uint(self.channels)) if err < 0: sys.stderr.write("set_channels failed: %d\n" % err) return err - err = self.libasound.snd_pcm_hw_params_set_rate(self.c_pcm, c_pars, c_uint(self.rate), c_int(0)); + err = self.libasound.snd_pcm_hw_params_set_rate(self.c_pcm, c_pars, c_uint(self.rate), c_int(0)) if err < 0: sys.stderr.write("set_rate failed: %d\n" % err) return err - err = 0 ########self.libasound.snd_pcm_hw_params_set_buffer_size(self.c_pcm, c_pars, c_ulong(pcm_buffer_size), c_int(0)); + err = self.libasound.snd_pcm_hw_params_set_buffer_size_near(self.c_pcm, c_pars, byref(pcm_buf_sz)) if err < 0: - sys.stderr.write("set_buffer_size failed: %d\n" % err) + sys.stderr.write("set_buffer_size_near failed: %d\n" % err) return err - err = self.libasound.snd_pcm_hw_params(self.c_pcm, c_pars); + if pcm_buf_sz.value != pcm_buffer_size: + sys.stderr.write("set_buffer_size_near requested %d, but returned %d\n" % (pcm_buffer_size, pcm_buf_sz.value)) + err = self.libasound.snd_pcm_hw_params(self.c_pcm, c_pars) if err < 0: sys.stderr.write("hw_params failed: %d\n" % err) return err self.libasound.snd_pcm_hw_params_current(self.c_pcm, c_pars) c_bits = self.libasound.snd_pcm_hw_params_get_sbits(c_pars) - self.framesize = self.channels * c_bits/8; + self.framesize = self.channels * c_bits/8 c_sw_pars = (c_void_p * int(self.libasound.snd_pcm_sw_params_sizeof() / sizeof(c_void_p)))() err = self.libasound.snd_pcm_sw_params_current(self.c_pcm, c_sw_pars) if err < 0: sys.stderr.write("get_sw_params_current failed: %d\n" % err) return err + pcm_start_threshold = int(pcm_buf_sz.value * 0.75) err = self.libasound.snd_pcm_sw_params_set_start_threshold(self.c_pcm, c_sw_pars, c_uint(pcm_start_threshold)) if err < 0: sys.stderr.write("set_sw_params_start_threshold failed: %d\n" % err) @@ -197,7 +202,7 @@ class alsasound(object): if (ret < 0): if (ret == -errno.EPIPE): # underrun if (LOG_AUDIO_XRUNS): - sys.stderr.write("[%f] PCM underrun\n" % time.time()) + sys.stderr.write("%f PCM underrun\n" % time.time()) ret = self.libasound.snd_pcm_recover(self.c_pcm, ret, 1) if (ret >= 0): ret = self.libasound.snd_pcm_writei(self.c_pcm, cast(c_data, POINTER(c_void_p)), n_frames) @@ -227,6 +232,16 @@ class alsasound(object): time.sleep(1) ret = self.libasound.snd_pcm_prepare(self.c_pcm) + def drop(self): + ret = self.libasound.snd_pcm_drop(self.c_pcm) + if (ret == -errno.ESTRPIPE): # suspended + while True: + ret = self.libasound.snd_pcm_resume(self.c_pcm) + if (ret != -errno.EAGAIN): + break + time.sleep(1) + ret = self.libasound.snd_pcm_prepare(self.c_pcm) + def dump(self): if (self.c_pcm.value == None): return @@ -264,6 +279,8 @@ class socket_audio(threading.Thread): flag = ord(d[0][0]) + (ord(d[0][1]) << 8) # 16 bit little endian if (flag == 0): # 0x0000 = drain pcm buffer self.pcm.drain() + elif (flag == 1): # 0x0001 = flush pcm buffer + self.pcm.drop() else: # audio data self.pcm.write(d[0]) else: @@ -280,7 +297,6 @@ class socket_audio(threading.Thread): def setup_socket(self, udp_host, udp_port): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind((udp_host, udp_port)) - sys.stderr.write('setup_socket: %d\n' % udp_port) return def close_socket(self): From b28a976768af0440429e0fb41a5538efa2c28c05 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 23 Jan 2018 22:23:45 -0500 Subject: [PATCH 047/102] additional trunking data collection --- op25/gr-op25_repeater/apps/trunking.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index 204318d..17dd095 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -55,6 +55,7 @@ class trunked_system (object): self.tsbk_cache = {} self.secondary = {} self.adjacent = {} + self.adjacent_data = {} self.rfss_syid = 0 self.rfss_rfid = 0 self.rfss_stid = 0 @@ -102,12 +103,15 @@ class trunked_system (object): d['secondary'] = self.secondary.keys() d['tsbks'] = self.stats['tsbks'] d['frequencies'] = {} + d['frequency_data'] = {} d['last_tsbk'] = self.last_tsbk t = time.time() for f in self.voice_frequencies.keys(): tgs = '%s %s' % (self.voice_frequencies[f]['tgid'][0], self.voice_frequencies[f]['tgid'][1]) d['frequencies'][f] = 'voice frequency %f tgid(s) %s %4.1fs ago count %d' % (f / 1000000.0, tgs, t - self.voice_frequencies[f]['time'], self.voice_frequencies[f]['counter']) + d['frequency_data'][f] = {'tgids': self.voice_frequencies[f]['tgid'], 'last_activity': '%7.1f' % (t - self.voice_frequencies[f]['time']), 'counter': self.voice_frequencies[f]['counter']} + d['adjacent_data'] = self.adjacent_data return json.dumps(d) def to_string(self): @@ -248,6 +252,7 @@ class trunked_system (object): f2 = self.channel_id_to_frequency(ch2) if f1 and f2: self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f' % (rfid, stid, f2 / 1000000.0) + self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f2, 'table': None} if self.debug > 10: print "mbt3c adjacent sys %x rfid %x stid %x ch1 %x ch2 %x f1 %s f2 %s" %(syid, rfid, stid, ch1, ch2, self.channel_id_to_string(ch1), self.channel_id_to_string(ch2)) elif opcode == 0x3b: # network status @@ -488,6 +493,7 @@ class trunked_system (object): f1 = self.channel_id_to_frequency(ch1) if f1 and table in self.freq_table: self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f tbl:%d' % (rfid, stid, (f1 + self.freq_table[table]['offset']) / 1000000.0, table) + self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f1 + self.freq_table[table]['offset'], 'table': table} if self.debug > 10: print "tsbk3c adjacent: rfid %x stid %d ch1 %x(%s)" %(rfid, stid, ch1, self.channel_id_to_string(ch1)) if table in self.freq_table: From d16702fb9efea347644f46483b2eeeb20b36930b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jan 2018 18:50:43 -0500 Subject: [PATCH 048/102] html/css/js/png file additions --- op25/gr-op25_repeater/www/images/1x1.png | Bin 0 -> 290 bytes .../www/www-static/index.html | 44 ++++ op25/gr-op25_repeater/www/www-static/main.css | 5 + op25/gr-op25_repeater/www/www-static/main.js | 230 ++++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 op25/gr-op25_repeater/www/images/1x1.png create mode 100644 op25/gr-op25_repeater/www/www-static/index.html create mode 100644 op25/gr-op25_repeater/www/www-static/main.css create mode 100644 op25/gr-op25_repeater/www/www-static/main.js diff --git a/op25/gr-op25_repeater/www/images/1x1.png b/op25/gr-op25_repeater/www/images/1x1.png new file mode 100644 index 0000000000000000000000000000000000000000..559d2b0feb9d63a63919c9caca558a893274cef9 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^j3CU&3?x-=hn)gaEa{HEjtmSN`?>!lvVtUwgWR1M z)}51i3FIgwdj$D1FjT2AFf_C + +OP25 + + + + + + + +  |   + +

+ + +

+©Copyright 2017, 2018 Max H. Parke KA1RBI

+ +This program comes with ABSOLUTELY NO WARRANTY.
+OP25 is free software, and you are welcome to redistribute it
+under certain conditions. For further details refer to this link: +LICENSE +
+
+ + diff --git a/op25/gr-op25_repeater/www/www-static/main.css b/op25/gr-op25_repeater/www/www-static/main.css new file mode 100644 index 0000000..cad6b17 --- /dev/null +++ b/op25/gr-op25_repeater/www/www-static/main.css @@ -0,0 +1,5 @@ +body {background-color: yellow;} +table { border-style: solid; background-color: #00c000; border-collapse: collapse} +th {border-style: solid;} +td {border-style: solid;} +input#s_table_nac {text-align: right;} diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js new file mode 100644 index 0000000..353326e --- /dev/null +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -0,0 +1,230 @@ + +// Copyright 2017, 2018 Max H. Parke KA1RBI +// +// This file is part of OP25 +// +// OP25 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. +// +// OP25 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 OP25; see the file COPYING. If not, write to the Free +// Software Foundation, Inc., 51 Franklin Street, Boston, MA +// 02110-1301, USA. + +function find_parent(ele, tagname) { + while (ele) { + if (ele.nodeName == tagname) + return (ele); + else if (ele.nodeName == "HTML") + return null; + ele = ele.parentNode; + } + return null; +} + +function f_command(ele, command) { + var myrow = find_parent(ele, "TR"); + if (command == "delete") { + var ok = confirm ("Confirm delete"); + if (ok) + myrow.parentNode.removeChild(myrow); + } else if (command == "clone") { + var newrow = myrow.cloneNode(true); + if (myrow.nextSibling) + myrow.parentNode.insertBefore(newrow, myrow.nextSibling); + else + myrow.parentNode.appendChild(newrow); + } else if (command == "new") { + var mytbl = find_parent(ele, "TABLE"); + var newrow = null; + if (mytbl.id == "chtable") + newrow = document.getElementById("chrow").cloneNode(true); + else if (mytbl.id == "devtable") + newrow = document.getElementById("devrow").cloneNode(true); + else + return; + mytbl.appendChild(newrow); + } +} + +function f_select(command) { + var div_list = ["status", "plot"]; + for (var i=0; i= "0" && s <= "9") + return true; + else + return false; +} + +function rx_update(d) { + if (d["files"].length > 0) { + for (var i=0; i(" + d['system'] + ") "; + if (d['tgid'] != null) { + html += "Talkgroup ID " + d['tgid']; + html += " (" + d['tag'] + ")"; + } + html += "
"; + var div_s2 = document.getElementById("div_s2"); + div_s2.innerHTML = html; + div_s2.style["display"] = ""; + if (d['tgid'] != null) + current_tgid = d['tgid']; + if (current_tgid != null) { + var div_s3 = document.getElementById("div_s3"); + div_s3.style["display"] = ""; + } +} + +function adjacent_data(d) { + if (Object.keys(d).length < 1) { + var html = ""; + return html; + } + var html = "

"; + html += ""; + html += ""; + html += ""; + var ct = 0; + for (var freq in d) { + var color = "#d0d0d0"; + if ((ct & 1) == 0) + color = "#c0c0c0"; + ct += 1; + html += ""; + } + html += "
Adjacent Sites
FrequencyRF IdSite IdUplink
" + freq / 1000000.0 + "" + d[freq]["rfid"] + "" + d[freq]["stid"] + "" + (d[freq]["uplink"] / 1000000.0) + "
"; + return html; +} + +function trunk_update(d) { + var do_hex = {"syid":0, "sysid":0, "wacn": 0}; + var do_float = {"rxchan":0, "txchan":0}; + var html = ""; + for (var nac in d) { + if (!is_digit(nac.charAt(0))) + continue; + html += ""; + html += "NAC " + "0x" + parseInt(nac).toString(16) + " "; + html += d[nac]['rxchan'] / 1000000.0; + html += " / "; + html += d[nac]['txchan'] / 1000000.0; + html += " tsbks " + d[nac]['tsbks']; + html += "
"; + html += "WACN " + "0x" + parseInt(d[nac]['wacn']).toString(16) + " "; + html += "System ID " + "0x" + parseInt(d[nac]['sysid']).toString(16) + " "; + html += "RF ID " + d[nac]['rfid'] + " "; + html += "Site ID " + d[nac]['stid'] + "
"; + if (d[nac]["secondary"].length) { + html += "Secondary control channel(s): "; + for (i=0; iSystem Frequencies"; + html += "FrequencyLast SeenTalkgoup ID(s)Count"; + var ct = 0; + for (var freq in d[nac]['frequency_data']) { + tg2 = d[nac]['frequency_data'][freq]['tgids'][1]; + if (tg2 == null) + tg2 = " "; + var color = "#d0d0d0"; + if ((ct & 1) == 0) + color = "#c0c0c0"; + ct += 1; + html += "" + parseInt(freq) / 1000000.0 + "" + d[nac]['frequency_data'][freq]['last_activity'] + "" + d[nac]['frequency_data'][freq]['tgids'][0] + "" + tg2 + "" + d[nac]['frequency_data'][freq]['counter'] + ""; + } + html += ""; + html += adjacent_data(d[nac]['adjacent_data']); + } + var div_s1 = document.getElementById("div_s1"); + div_s1.innerHTML = html; +} + +function http_req_cb() { + s = http_req.readyState; + if (s != 4) + return; + if (http_req.status != 200) + return; + var dl = JSON.parse(http_req.responseText); + var dispatch = {'trunk_update': trunk_update, 'change_freq': change_freq, 'rx_update': rx_update} + for (var i=0; i Date: Thu, 25 Jan 2018 18:51:16 -0500 Subject: [PATCH 049/102] allow plot output to files --- op25/gr-op25_repeater/apps/gr_gnuplot.py | 34 ++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/gr_gnuplot.py b/op25/gr-op25_repeater/apps/gr_gnuplot.py index 2fe3c92..dc6342a 100644 --- a/op25/gr-op25_repeater/apps/gr_gnuplot.py +++ b/op25/gr-op25_repeater/apps/gr_gnuplot.py @@ -20,6 +20,8 @@ # 02110-1301, USA. import sys +import os +import time import subprocess from gnuradio import gr, gru, eng_notation @@ -47,6 +49,11 @@ class wrap_gp(object): self.avg_pwr = np.zeros(FFT_BINS) self.buf = [] self.plot_count = 0 + self.last_plot = 0 + self.plot_interval = None + self.sequence = 0 + self.output_dir = None + self.filename = None self.attach_gp() @@ -59,6 +66,12 @@ class wrap_gp(object): self.gp.kill() self.gp.wait() + def set_interval(self, v): + self.plot_interval = v + + def set_output_dir(self, v): + self.output_dir = v + def plot(self, buf, bufsz, mode='eye'): BUFSZ = bufsz consumed = min(len(buf), BUFSZ-len(self.buf)) @@ -72,6 +85,10 @@ class wrap_gp(object): self.buf = [] return consumed + if self.plot_interval and self.last_plot + self.plot_interval > time.time(): + return consumed + self.last_plot = time.time() + plots = [] s = '' while(len(self.buf)): @@ -94,7 +111,7 @@ class wrap_gp(object): s += '%f\n' % (b) s += 'e\n' self.buf = [] - plots.append('"-" with dots') + plots.append('"-" with points') elif mode == 'fft': self.ffts = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ) self.ffts = np.fft.fftshift(self.ffts) @@ -110,7 +127,18 @@ class wrap_gp(object): plots.append('"-" with lines') self.buf = [] - h= 'set terminal x11 noraise\n' + filename = None + if self.output_dir: + if self.sequence >= 2: + delete_pathname = '%s/plot-%s-%d.png' % (self.output_dir, mode, self.sequence-2) + if os.access(delete_pathname, os.W_OK): + os.remove(delete_pathname) + h= 'set terminal png\n' + filename = 'plot-%s-%d.png' % (mode, self.sequence) + self.sequence += 1 + h += 'set output "%s/%s"\n' % (self.output_dir, filename) + else: + h= 'set terminal x11 noraise\n' #background = 'set object 1 circle at screen 0,0 size screen 1 fillcolor rgb"black"\n' #FIXME! background = '' h+= 'set key off\n' @@ -138,6 +166,8 @@ class wrap_gp(object): h+= 'set title "Tuned to %f Mhz"\n' % ((self.center_freq - self.relative_freq) / 1e6) dat = '%splot %s\n%s' % (h, ','.join(plots), s) self.gp.stdin.write(dat) + if filename: + self.filename = filename return consumed def set_center_freq(self, f): From 6ecebfffccba63011fd4c2022b9c281b19be4970 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jan 2018 19:00:53 -0500 Subject: [PATCH 050/102] rx.py http and remote plot additions --- op25/gr-op25_repeater/apps/rx.py | 69 +++++++++++++++++++------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 0206b50..4d880a9 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -75,6 +75,9 @@ os.environ['IMBE'] = 'soft' WIRESHARK_PORT = 23456 +_def_interval = 5.0 # sec +_def_file_dir = '../www/images' + # The P25 receiver # class p25_rx_block (gr.top_block): @@ -84,7 +87,7 @@ class p25_rx_block (gr.top_block): def __init__(self): self.trunk_rx = None - self.kill_sink = None + self.plot_sinks = [] gr.top_block.__init__(self) @@ -100,12 +103,12 @@ class p25_rx_block (gr.top_block): parser.add_option("-c", "--calibration", type="eng_float", default=0.0, help="USRP offset or audio IF frequency", metavar="Hz") parser.add_option("-C", "--costas-alpha", type="eng_float", default=0.04, help="value of alpha for Costas loop", metavar="Hz") parser.add_option("-D", "--demod-type", type="choice", default="cqpsk", choices=('cqpsk', 'fsk4'), help="cqpsk | fsk4") - parser.add_option("-P", "--plot-mode", type="choice", default=None, choices=(None, 'constellation', 'fft', 'symbol', 'datascope'), help="constellation | fft | symbol | datascope") + parser.add_option("-P", "--plot-mode", type="string", default=None, help="one or more of constellation, fft, symbol, datascope (comma-separated)") parser.add_option("-f", "--frequency", type="eng_float", default=0.0, help="USRP center frequency", metavar="Hz") parser.add_option("-F", "--ifile", type="string", default=None, help="read input from complex capture file") parser.add_option("-H", "--hamlib-model", type="int", default=None, help="specify model for hamlib") parser.add_option("-s", "--seek", type="int", default=0, help="ifile seek in K") - parser.add_option("-l", "--terminal-type", type="string", default='curses', help="'curses' or udp port") + parser.add_option("-l", "--terminal-type", type="string", default='curses', help="'curses' or udp port or 'http:host:port'") parser.add_option("-L", "--logfile-workers", type="int", default=None, help="number of demodulators to instantiate") parser.add_option("-S", "--sample-rate", type="int", default=320e3, help="source samp rate") parser.add_option("-t", "--tone-detect", action="store_true", default=False, help="use experimental tone detect algorithm") @@ -186,6 +189,7 @@ class p25_rx_block (gr.top_block): self.symbol_deviation = 600.0 self.basic_rate = 24000 _default_speed = 4800 + self.options = options # keep track of flow graph connections self.cnxns = [] @@ -195,8 +199,6 @@ class p25_rx_block (gr.top_block): self.constellation_scope_connected = False - self.options = options - for i in xrange(len(speeds)): if speeds[i] == _default_speed: self.current_speed = i @@ -279,26 +281,27 @@ class p25_rx_block (gr.top_block): # connect it all up self.connect(source, self.demod, self.decoder) - if self.options.plot_mode == 'constellation': - assert self.options.demod_type == 'cqpsk' ## constellation requires cqpsk demod-type - self.constellation_sink = constellation_sink_c() - self.demod.connect_complex('diffdec', self.constellation_sink) - self.kill_sink = self.constellation_sink - elif self.options.plot_mode == 'symbol': - self.symbol_sink = symbol_sink_f() - self.demod.connect_float(self.symbol_sink) - self.kill_sink = self.symbol_sink - elif self.options.plot_mode == 'fft': - self.fft_sink = fft_sink_c() - self.spectrum_decim = filter.rational_resampler_ccf(1, self.options.decim_amt) - self.connect(self.spectrum_decim, self.fft_sink) - self.demod.connect_complex('src', self.spectrum_decim) - self.kill_sink = self.fft_sink - elif self.options.plot_mode == 'datascope': - assert self.options.demod_type == 'fsk4' ## datascope requires fsk4 demod-type - self.eye_sink = eye_sink_f(sps=sps) - self.demod.connect_bb('symbol_filter', self.eye_sink) - self.kill_sink = self.eye_sink + for plot_mode in self.options.plot_mode.split(','): + if plot_mode == 'constellation': + assert self.options.demod_type == 'cqpsk' ## constellation requires cqpsk demod-type + sink = constellation_sink_c() + self.demod.connect_complex('diffdec', sink) + elif plot_mode == 'symbol': + sink = symbol_sink_f() + self.demod.connect_float(sink) + elif plot_mode == 'fft': + sink = fft_sink_c() + self.spectrum_decim = filter.rational_resampler_ccf(1, self.options.decim_amt) + self.connect(self.spectrum_decim, sink) + self.demod.connect_complex('src', self.spectrum_decim) + elif plot_mode == 'datascope': + assert self.options.demod_type == 'fsk4' ## datascope requires fsk4 demod-type + sink = eye_sink_f(sps=sps) + self.demod.connect_bb('symbol_filter', sink) + self.plot_sinks.append(sink) + if self.options.terminal_type.startswith('http:'): + sink.gnuplot.set_interval(_def_interval) + sink.gnuplot.set_output_dir(_def_file_dir) if self.options.raw_symbols: self.sink_sf = blocks.file_sink(gr.sizeof_char, self.options.raw_symbols) @@ -579,6 +582,17 @@ class p25_rx_block (gr.top_block): # except Exception, x: # wx.MessageBox("Cannot open USRP: " + x.message, "USRP Error", wx.CANCEL | wx.ICON_EXCLAMATION) + def process_ajax(self): + if not self.options.terminal_type.startswith('http:'): + return + filenames = [sink.gnuplot.filename for sink in self.plot_sinks if sink.gnuplot.filename] + error = None + if self.options.demod_type == 'cqpsk': + error = self.demod.get_freq_error() + d = {'json_type': 'rx_update', 'error': error, 'files': filenames} + msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0) + self.input_q.insert_tail(msg) + def process_qmsg(self, msg): # return true = end top block RX_COMMANDS = 'skip lockout hold' @@ -590,6 +604,7 @@ class p25_rx_block (gr.top_block): js = self.trunk_rx.to_json() msg = gr.message().make_from_string(js, -4, 0, 0) self.input_q.insert_tail(msg) + self.process_ajax() elif s == 'set_freq': freq = msg.arg1() self.set_freq(freq) @@ -642,8 +657,8 @@ class rx_main(object): if self.tb.audio: self.tb.audio.stop() self.tb.stop() - if self.tb.kill_sink: - self.tb.kill_sink.kill() + for sink in self.tb.plot_sinks: + sink.kill() # Start the receiver # From 34d08d925516a8373baf6b8b01d31da99cc94e17 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jan 2018 19:02:15 -0500 Subject: [PATCH 051/102] terminal.py http additions --- op25/gr-op25_repeater/apps/terminal.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py index 82579e8..d562880 100755 --- a/op25/gr-op25_repeater/apps/terminal.py +++ b/op25/gr-op25_repeater/apps/terminal.py @@ -207,6 +207,26 @@ class curses_terminal(threading.Thread): self.keep_running = False self.send_command('quit', 0) +class http_terminal(threading.Thread): + def __init__(self, input_q, output_q, endpoint, **kwds): + from http import http_server + + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.input_q = input_q + self.output_q = output_q + self.endpoint = endpoint + self.keep_running = True + self.server = http_server(self.input_q, self.output_q, self.endpoint) + + self.start() + + def end_terminal(self): + self.keep_running = False + + def run(self): + self.server.run() + class udp_terminal(threading.Thread): def __init__(self, input_q, output_q, port, **kwds): threading.Thread.__init__ (self, **kwds) @@ -256,6 +276,8 @@ def op25_terminal(input_q, output_q, terminal_type): elif terminal_type[0].isdigit(): port = int(terminal_type) return udp_terminal(input_q, output_q, port) + elif terminal_type.startswith('http:'): + return http_terminal(input_q, output_q, terminal_type.replace('http:', '')) else: sys.stderr.write('warning: unsupported terminal type: %s\n', terminal_type) return None From d5ee60abf8def19164072d32446b887045c2d737 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jan 2018 19:13:32 -0500 Subject: [PATCH 052/102] http.py --- op25/gr-op25_repeater/apps/http.py | 127 +++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 op25/gr-op25_repeater/apps/http.py diff --git a/op25/gr-op25_repeater/apps/http.py b/op25/gr-op25_repeater/apps/http.py new file mode 100644 index 0000000..afce204 --- /dev/null +++ b/op25/gr-op25_repeater/apps/http.py @@ -0,0 +1,127 @@ + +# Copyright 2017, 2018 Max H. Parke KA1RBI +# +# This file is part of OP25 +# +# OP25 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. +# +# OP25 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 OP25; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Boston, MA +# 02110-1301, USA. + +import sys +import os +import time +import re +import json +import socket +import traceback + +from gnuradio import gr +from waitress.server import create_server + +my_input_q = None +my_output_q = None +my_port = None + +""" +fake http and ajax server module +TODO: make less fake +""" + +def static_file(environ, start_response): + content_types = { 'png': 'image/png', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'gif': 'image/gif', 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html'} + img_types = 'png jpg jpeg gif'.split() + if environ['PATH_INFO'] == '/': + filename = 'index.html' + else: + filename = re.sub(r'[^a-zA-Z0-9_.\-]', '', environ['PATH_INFO']) + suf = filename.split('.')[-1] + pathname = '../www/www-static' + if suf in img_types: + pathname = '../www/images' + pathname = '%s/%s' % (pathname, filename) + if suf not in content_types.keys() or '..' in filename or not os.access(pathname, os.R_OK): + sys.stderr.write('404 %s\n' % pathname) + status = '404 NOT FOUND' + content_type = 'text/plain' + output = status + else: + output = open(pathname).read() + content_type = content_types[suf] + status = '200 OK' + return status, content_type, output + +def post_req(environ, start_response, postdata): + global my_input_q, my_output_q, my_port + try: + data = json.loads(postdata) + msg = gr.message().make_from_string(str(data['command']), -2, data['data'], 0) + my_output_q.insert_tail(msg) + time.sleep(0.2) + except: + sys.stderr.write('post_req: error processing input: %s:\n' % (postdata)) + traceback.print_exc(limit=None, file=sys.stderr) + sys.stderr.write('*** end traceback ***\n') + + resp_msg = [] + while not my_input_q.empty_p(): + msg = my_input_q.delete_head() + if msg.type() == -4: + resp_msg.append(json.loads(msg.to_string())) + status = '200 OK' + content_type = 'application/json' + output = json.dumps(resp_msg) + return status, content_type, output + +def http_request(environ, start_response): + if environ['REQUEST_METHOD'] == 'GET': + status, content_type, output = static_file(environ, start_response) + elif environ['REQUEST_METHOD'] == 'POST': + postdata = environ['wsgi.input'].read() + status, content_type, output = post_req(environ, start_response, postdata) + else: + status = '200 OK' + content_type = 'text/plain' + output = status + sys.stderr.write('http_request: unexpected input %s\n' % environ['PATH_INFO']) + + response_headers = [('Content-type', content_type), + ('Content-Length', str(len(output)))] + start_response(status, response_headers) + + return [output] + +def application(environ, start_response): + failed = False + try: + result = http_request(environ, start_response) + except: + failed = True + sys.stderr.write('application: request failed:\n%s\n' % traceback.format_exc()) + sys.exit(1) + return result + +class http_server(object): + def __init__(self, input_q, output_q, endpoint, **kwds): + global my_input_q, my_output_q, my_port + host, port = endpoint.split(':') + if my_port is not None: + raise AssertionError('this server is already active on port %s' % my_port) + my_input_q = input_q + my_output_q = output_q + my_port = int(port) + + self.server = create_server(application, host=host, port=my_port) + + def run(self): + self.server.run() From 43bd7e7ae1559e083e870689fa946116f2035ae2 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jan 2018 19:36:11 -0500 Subject: [PATCH 053/102] update README --- op25/gr-op25_repeater/apps/README | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/op25/gr-op25_repeater/apps/README b/op25/gr-op25_repeater/apps/README index 031bc6f..b2b9db5 100644 --- a/op25/gr-op25_repeater/apps/README +++ b/op25/gr-op25_repeater/apps/README @@ -142,6 +142,8 @@ Options: -H HAMLIB_MODEL, --hamlib-model=HAMLIB_MODEL specify model for hamlib -s SEEK, --seek=SEEK ifile seek in K + -l TERMINAL_TYPE, --terminal-type=TERMINAL_TYPE + 'curses' or udp port or 'http:host:port' -L LOGFILE_WORKERS, --logfile-workers=LOGFILE_WORKERS number of demodulators to instantiate -S SAMPLE_RATE, --sample-rate=SAMPLE_RATE @@ -175,6 +177,31 @@ Options: -Z DECIM_AMT, --decim-amt=DECIM_AMT spectrum decimation +HTTP CONSOLE +============ +New as of Jan. 2018, the OP25 dashboard is accessible to any Web browser over +HTTP. Include the option "-l http::" when starting the rx.py app, +where is either "127.0.0.1" to limit access from only this host, or +"0.0.0.0" if HTTP access from anywhere is to be allowed*. After rx.py has +started it begins listening on the specified port for incoming connections. + +Once connected the status page should automatically update to show trunking +system status, frequency list, adjacent sites, and other data. + +Example: you have started rx.py with the option "-l http:127.0.0.1:8080". +To connect, set your web browser URL to "http://127.0.0.1:8080". + +If one or more plot modes has been selected using the "-P" option you may +view them by clicking the "PLOT" button. The plots are updated approx. +every five seconds. Click "STATUS" to return to the main status page. + +*WARNING*: there is no security or encryption. Be careful when using "0.0.0.0" +as the listening address since anyone with access to the network can connect. + +NOTE: the python-pyramid package is required when using this option. It can +be installed by running + sudo apt-get install python-pyramid + MULTI-RECEIVER ============== The multi_rx.py app allows an arbitrary number of SDR devices and channels From 4cde5ac5d8c7d014ded958b04fb9b5de12344a35 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 26 Jan 2018 14:55:14 -0500 Subject: [PATCH 054/102] bugfix in -P thx Scott --- op25/gr-op25_repeater/apps/rx.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 4d880a9..0c8580e 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -281,7 +281,10 @@ class p25_rx_block (gr.top_block): # connect it all up self.connect(source, self.demod, self.decoder) - for plot_mode in self.options.plot_mode.split(','): + plot_modes = [] + if self.options.plot_mode is not None: + plot_modes = self.options.plot_mode.split(',') + for plot_mode in plot_modes: if plot_mode == 'constellation': assert self.options.demod_type == 'cqpsk' ## constellation requires cqpsk demod-type sink = constellation_sink_c() @@ -298,6 +301,8 @@ class p25_rx_block (gr.top_block): assert self.options.demod_type == 'fsk4' ## datascope requires fsk4 demod-type sink = eye_sink_f(sps=sps) self.demod.connect_bb('symbol_filter', sink) + else: + raise ValueError('unsupported plot type: %s' % plot_mode) self.plot_sinks.append(sink) if self.options.terminal_type.startswith('http:'): sink.gnuplot.set_interval(_def_interval) From 7bdfe78aa04da83dd3cdf08085e604cf0fcb91ce Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 19 Feb 2018 11:58:40 -0500 Subject: [PATCH 055/102] build fixes thx MattSR via CS --- install.sh | 2 +- op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index f52da38..e0281d3 100755 --- a/install.sh +++ b/install.sh @@ -12,7 +12,7 @@ fi sudo apt-get update sudo apt-get build-dep gnuradio -sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig +sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen mkdir build cd build diff --git a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc index dba0aa9..9d5515d 100644 --- a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc +++ b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc @@ -39,12 +39,12 @@ namespace gr { void p25_frame_assembler_impl::p25p2_queue_msg(int duid) { - static const char wbuf[2] = {0xff, 0xff}; // dummy NAC + static const unsigned char wbuf[2] = {0xff, 0xff}; // dummy NAC if (!d_do_msgq) return; if (d_msg_queue->full_p()) return; - gr::message::sptr msg = gr::message::make_from_string(std::string(wbuf, 2), duid, 0, 0); + gr::message::sptr msg = gr::message::make_from_string(std::string((const char *)wbuf, 2), duid, 0, 0); d_msg_queue->insert_tail(msg); } From 6b45a95754ebea6eee253e578015ab3d18439678 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 19 Feb 2018 20:03:13 -0500 Subject: [PATCH 056/102] http bugfix thx wa8wg for the report --- op25/gr-op25_repeater/apps/http.py | 34 ++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/op25/gr-op25_repeater/apps/http.py b/op25/gr-op25_repeater/apps/http.py index afce204..4a66596 100644 --- a/op25/gr-op25_repeater/apps/http.py +++ b/op25/gr-op25_repeater/apps/http.py @@ -25,12 +25,14 @@ import re import json import socket import traceback +import threading from gnuradio import gr from waitress.server import create_server my_input_q = None my_output_q = None +my_recv_q = None my_port = None """ @@ -62,7 +64,7 @@ def static_file(environ, start_response): return status, content_type, output def post_req(environ, start_response, postdata): - global my_input_q, my_output_q, my_port + global my_input_q, my_output_q, my_recv_q, my_port try: data = json.loads(postdata) msg = gr.message().make_from_string(str(data['command']), -2, data['data'], 0) @@ -74,8 +76,8 @@ def post_req(environ, start_response, postdata): sys.stderr.write('*** end traceback ***\n') resp_msg = [] - while not my_input_q.empty_p(): - msg = my_input_q.delete_head() + while not my_recv_q.empty_p(): + msg = my_recv_q.delete_head() if msg.type() == -4: resp_msg.append(json.loads(msg.to_string())) status = '200 OK' @@ -111,9 +113,16 @@ def application(environ, start_response): sys.exit(1) return result +def process_qmsg(msg): + if my_recv_q.full_p(): + my_recv_q.delete_head_nowait() # ignores result + if my_recv_q.full_p(): + return + my_recv_q.insert_tail(msg) + class http_server(object): def __init__(self, input_q, output_q, endpoint, **kwds): - global my_input_q, my_output_q, my_port + global my_input_q, my_output_q, my_recv_q, my_port host, port = endpoint.split(':') if my_port is not None: raise AssertionError('this server is already active on port %s' % my_port) @@ -121,7 +130,24 @@ class http_server(object): my_output_q = output_q my_port = int(port) + my_recv_q = gr.msg_queue(10) + self.q_watcher = queue_watcher(my_input_q, process_qmsg) + self.server = create_server(application, host=host, port=my_port) def run(self): self.server.run() + +class queue_watcher(threading.Thread): + def __init__(self, msgq, callback, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.msgq = msgq + self.callback = callback + self.keep_running = True + self.start() + + def run(self): + while(self.keep_running): + msg = self.msgq.delete_head() + self.callback(msg) From 2ff4ae5a38f225becb0cd4830d5d7f51df708be3 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 1 Mar 2018 20:10:19 -0500 Subject: [PATCH 057/102] css updates many thx triptolemus --- .../www/www-static/index.html | 126 ++++++--- op25/gr-op25_repeater/www/www-static/main.css | 258 +++++++++++++++++- op25/gr-op25_repeater/www/www-static/main.js | 85 ++++-- 3 files changed, 409 insertions(+), 60 deletions(-) diff --git a/op25/gr-op25_repeater/www/www-static/index.html b/op25/gr-op25_repeater/www/www-static/index.html index 506e52c..75603db 100644 --- a/op25/gr-op25_repeater/www/www-static/index.html +++ b/op25/gr-op25_repeater/www/www-static/index.html @@ -1,44 +1,106 @@ + + -OP25 - - - - + + OP25 + + + + - -  |   - -


-"; return html; } - var html = "

"; - html += ""; + var html = "
"; + html += "
"; html += ""; - html += ""; + html += ""; var ct = 0; for (var freq in d) { var color = "#d0d0d0"; @@ -127,10 +151,15 @@ function adjacent_data(d) { ct += 1; html += ""; } - html += "
Adjacent Sites
FrequencyRF IdSite IdUplink
FrequencyRFSSSiteUplink
" + freq / 1000000.0 + "" + d[freq]["rfid"] + "" + d[freq]["stid"] + "" + (d[freq]["uplink"] / 1000000.0) + "
"; + html += "

"; + +// end adjacent sites table + return html; } +// additional system info: wacn, sysID, rfss, site id, secondary control channels, freq error + function trunk_update(d) { var do_hex = {"syid":0, "sysid":0, "wacn": 0}; var do_float = {"rxchan":0, "txchan":0}; @@ -138,32 +167,36 @@ function trunk_update(d) { for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; - html += ""; + html += ""; html += "NAC " + "0x" + parseInt(nac).toString(16) + " "; html += d[nac]['rxchan'] / 1000000.0; html += " / "; html += d[nac]['txchan'] / 1000000.0; html += " tsbks " + d[nac]['tsbks']; - html += "
"; - html += "WACN " + "0x" + parseInt(d[nac]['wacn']).toString(16) + " "; - html += "System ID " + "0x" + parseInt(d[nac]['sysid']).toString(16) + " "; - html += "RF ID " + d[nac]['rfid'] + " "; - html += "Site ID " + d[nac]['stid'] + "
"; + html += "
"; + + html += "WACN: " + "0x" + parseInt(d[nac]['wacn']).toString(16) + " "; + html += "System ID: " + "0x" + parseInt(d[nac]['sysid']).toString(16) + " "; + html += "RFSS ID: " + d[nac]['rfid'] + " "; + html += "Site ID: " + d[nac]['stid'] + "
"; if (d[nac]["secondary"].length) { - html += "Secondary control channel(s): "; + html += "Secondary control channel(s): "; for (i=0; iFrequency error: " + error_val + " Hz. (approx) "; } - html += "

"; - html += ""; + +// system frequencies table + + html += "

"; + html += "
"; // was width=350 html += ""; - html += ""; + html += ""; var ct = 0; for (var freq in d[nac]['frequency_data']) { tg2 = d[nac]['frequency_data'][freq]['tgids'][1]; @@ -175,13 +208,17 @@ function trunk_update(d) { ct += 1; html += ""; } - html += "
System Frequencies
FrequencyLast SeenTalkgoup ID(s)Count
FrequencyLast SeenTalkgoup IDCount
" + parseInt(freq) / 1000000.0 + "" + d[nac]['frequency_data'][freq]['last_activity'] + "" + d[nac]['frequency_data'][freq]['tgids'][0] + "" + tg2 + "" + d[nac]['frequency_data'][freq]['counter'] + "
"; + html += ""; + +// end system freqencies table + html += adjacent_data(d[nac]['adjacent_data']); } var div_s1 = document.getElementById("div_s1"); div_s1.innerHTML = html; } + function http_req_cb() { s = http_req.readyState; if (s != 4) @@ -204,6 +241,8 @@ function do_onload() { var ele = document.getElementById("div_status"); ele.style["display"] = ""; setInterval(do_update, 1000); + b = document.getElementById("b1"); + b.className = "nav-button-active"; } function do_update() { From 993c52686c82d7840b86a476b5ee8a60a5da42ae Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 1 Mar 2018 20:50:13 -0500 Subject: [PATCH 058/102] bugfix two .h files missing from CMakeLists.txt thx Adrian YO8RZZ --- op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt b/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt index b93833a..8478545 100644 --- a/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt +++ b/op25/gr-op25_repeater/include/op25_repeater/CMakeLists.txt @@ -27,5 +27,7 @@ install(FILES p25_frame_assembler.h frame_assembler.h ambe_encoder_sb.h + ysf_tx_sb.h + dstar_tx_sb.h fsk4_slicer_fb.h DESTINATION include/op25_repeater ) From 69f2091f02b2c6768b70cae9e15bc8c1e91c8c2b Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 3 Mar 2018 16:49:40 -0500 Subject: [PATCH 059/102] rx.py config updates --- op25/gr-op25_repeater/apps/rx.py | 93 ++++++++++++++++---------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 0c8580e..57f7652 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -84,56 +84,13 @@ class p25_rx_block (gr.top_block): # Initialize the P25 receiver # - def __init__(self): + def __init__(self, options): self.trunk_rx = None self.plot_sinks = [] gr.top_block.__init__(self) - # command line argument parsing - parser = OptionParser(option_class=eng_option) - parser.add_option("--args", type="string", default="", help="device args") - parser.add_option("--antenna", type="string", default="", help="select antenna") - parser.add_option("-a", "--audio", action="store_true", default=False, help="use direct audio input") - parser.add_option("-A", "--audio-if", action="store_true", default=False, help="soundcard IF mode (use --calibration to set IF freq)") - parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") - parser.add_option("-i", "--input", default=None, help="input file name") - parser.add_option("-b", "--excess-bw", type="eng_float", default=0.2, help="for RRC filter", metavar="Hz") - parser.add_option("-c", "--calibration", type="eng_float", default=0.0, help="USRP offset or audio IF frequency", metavar="Hz") - parser.add_option("-C", "--costas-alpha", type="eng_float", default=0.04, help="value of alpha for Costas loop", metavar="Hz") - parser.add_option("-D", "--demod-type", type="choice", default="cqpsk", choices=('cqpsk', 'fsk4'), help="cqpsk | fsk4") - parser.add_option("-P", "--plot-mode", type="string", default=None, help="one or more of constellation, fft, symbol, datascope (comma-separated)") - parser.add_option("-f", "--frequency", type="eng_float", default=0.0, help="USRP center frequency", metavar="Hz") - parser.add_option("-F", "--ifile", type="string", default=None, help="read input from complex capture file") - parser.add_option("-H", "--hamlib-model", type="int", default=None, help="specify model for hamlib") - parser.add_option("-s", "--seek", type="int", default=0, help="ifile seek in K") - parser.add_option("-l", "--terminal-type", type="string", default='curses', help="'curses' or udp port or 'http:host:port'") - parser.add_option("-L", "--logfile-workers", type="int", default=None, help="number of demodulators to instantiate") - parser.add_option("-S", "--sample-rate", type="int", default=320e3, help="source samp rate") - parser.add_option("-t", "--tone-detect", action="store_true", default=False, help="use experimental tone detect algorithm") - parser.add_option("-T", "--trunk-conf-file", type="string", default=None, help="trunking config file name") - parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") - parser.add_option("-V", "--vocoder", action="store_true", default=False, help="voice codec") - parser.add_option("-o", "--offset", type="eng_float", default=0.0, help="tuning offset frequency [to circumvent DC offset]", metavar="Hz") - parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") - parser.add_option("-w", "--wireshark", action="store_true", default=False, help="output data to Wireshark") - parser.add_option("-W", "--wireshark-host", type="string", default="127.0.0.1", help="Wireshark host") - parser.add_option("-r", "--raw-symbols", type="string", default=None, help="dump decoded symbols to file") - parser.add_option("-g", "--gain", type="eng_float", default=None, help="set USRP gain in dB (default is midpoint) or set audio gain") - parser.add_option("-G", "--gain-mu", type="eng_float", default=0.025, help="gardner gain") - parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") - parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name") - parser.add_option("-U", "--udp-player", action="store_true", default=False, help="enable built-in udp audio player") - parser.add_option("-q", "--freq-corr", type="eng_float", default=0.0, help="frequency correction") - parser.add_option("-d", "--fine-tune", type="eng_float", default=0.0, help="fine tuning") - parser.add_option("-2", "--phase2-tdma", action="store_true", default=False, help="enable phase2 tdma decode") - parser.add_option("-Z", "--decim-amt", type="int", default=1, help="spectrum decimation") - (options, args) = parser.parse_args() - if len(args) != 0: - parser.print_help() - sys.exit(1) - self.channel_rate = 0 self.baseband_input = False self.rtl_found = False @@ -642,7 +599,8 @@ class du_queue_watcher(threading.Thread): class rx_main(object): def __init__(self): self.keep_running = True - self.tb = p25_rx_block() + self.cli_options() + self.tb = p25_rx_block(self.options) self.q_watcher = du_queue_watcher(self.tb.output_q, self.process_qmsg) def process_qmsg(self, msg): @@ -665,6 +623,51 @@ class rx_main(object): for sink in self.tb.plot_sinks: sink.kill() + def cli_options(self): + # command line argument parsing + parser = OptionParser(option_class=eng_option) + parser.add_option("--args", type="string", default="", help="device args") + parser.add_option("--antenna", type="string", default="", help="select antenna") + parser.add_option("-a", "--audio", action="store_true", default=False, help="use direct audio input") + parser.add_option("-A", "--audio-if", action="store_true", default=False, help="soundcard IF mode (use --calibration to set IF freq)") + parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp") + parser.add_option("-i", "--input", type="string", default=None, help="input file name") + parser.add_option("-b", "--excess-bw", type="eng_float", default=0.2, help="for RRC filter", metavar="Hz") + parser.add_option("-c", "--calibration", type="eng_float", default=0.0, help="USRP offset or audio IF frequency", metavar="Hz") + parser.add_option("-C", "--costas-alpha", type="eng_float", default=0.04, help="value of alpha for Costas loop", metavar="Hz") + parser.add_option("-D", "--demod-type", type="choice", default="cqpsk", choices=('cqpsk', 'fsk4'), help="cqpsk | fsk4") + parser.add_option("-P", "--plot-mode", type="string", default=None, help="one or more of constellation, fft, symbol, datascope (comma-separated)") + parser.add_option("-f", "--frequency", type="eng_float", default=0.0, help="USRP center frequency", metavar="Hz") + parser.add_option("-F", "--ifile", type="string", default=None, help="read input from complex capture file") + parser.add_option("-H", "--hamlib-model", type="int", default=None, help="specify model for hamlib") + parser.add_option("-s", "--seek", type="int", default=0, help="ifile seek in K") + parser.add_option("-l", "--terminal-type", type="string", default='curses', help="'curses' or udp port or 'http:host:port'") + parser.add_option("-L", "--logfile-workers", type="int", default=None, help="number of demodulators to instantiate") + parser.add_option("-S", "--sample-rate", type="int", default=320e3, help="source samp rate") + parser.add_option("-t", "--tone-detect", action="store_true", default=False, help="use experimental tone detect algorithm") + parser.add_option("-T", "--trunk-conf-file", type="string", default=None, help="trunking config file name") + parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") + parser.add_option("-V", "--vocoder", action="store_true", default=False, help="voice codec") + parser.add_option("-o", "--offset", type="eng_float", default=0.0, help="tuning offset frequency [to circumvent DC offset]", metavar="Hz") + parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") + parser.add_option("-w", "--wireshark", action="store_true", default=False, help="output data to Wireshark") + parser.add_option("-W", "--wireshark-host", type="string", default="127.0.0.1", help="Wireshark host") + parser.add_option("-r", "--raw-symbols", type="string", default=None, help="dump decoded symbols to file") + parser.add_option("-g", "--gain", type="eng_float", default=None, help="set USRP gain in dB (default is midpoint) or set audio gain") + parser.add_option("-G", "--gain-mu", type="eng_float", default=0.025, help="gardner gain") + parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") + parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name") + parser.add_option("-U", "--udp-player", action="store_true", default=False, help="enable built-in udp audio player") + parser.add_option("-q", "--freq-corr", type="eng_float", default=0.0, help="frequency correction") + parser.add_option("-d", "--fine-tune", type="eng_float", default=0.0, help="fine tuning") + parser.add_option("-2", "--phase2-tdma", action="store_true", default=False, help="enable phase2 tdma decode") + parser.add_option("-Z", "--decim-amt", type="int", default=1, help="spectrum decimation") + (options, args) = parser.parse_args() + if len(args) != 0: + parser.print_help() + sys.exit(1) + self.options = options + # Start the receiver # From c17443d9ac36f21b59a4f9392f94b1c1d3ded301 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 3 Mar 2018 16:51:49 -0500 Subject: [PATCH 060/102] scan control bugfix, js debug --- op25/gr-op25_repeater/apps/http.py | 5 +- .../www/www-static/index.html | 3 + op25/gr-op25_repeater/www/www-static/main.js | 65 ++++++++++++++++--- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/op25/gr-op25_repeater/apps/http.py b/op25/gr-op25_repeater/apps/http.py index 4a66596..9196005 100644 --- a/op25/gr-op25_repeater/apps/http.py +++ b/op25/gr-op25_repeater/apps/http.py @@ -67,8 +67,9 @@ def post_req(environ, start_response, postdata): global my_input_q, my_output_q, my_recv_q, my_port try: data = json.loads(postdata) - msg = gr.message().make_from_string(str(data['command']), -2, data['data'], 0) - my_output_q.insert_tail(msg) + for d in data: + msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0) + my_output_q.insert_tail(msg) time.sleep(0.2) except: sys.stderr.write('post_req: error processing input: %s:\n' % (postdata)) diff --git a/op25/gr-op25_repeater/www/www-static/index.html b/op25/gr-op25_repeater/www/www-static/index.html index 75603db..b6c00bd 100644 --- a/op25/gr-op25_repeater/www/www-static/index.html +++ b/op25/gr-op25_repeater/www/www-static/index.html @@ -24,6 +24,9 @@
+

diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js index 2ad23cb..bf7d2ab 100644 --- a/op25/gr-op25_repeater/www/www-static/main.js +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -18,6 +18,22 @@ // Software Foundation, Inc., 51 Franklin Street, Boston, MA // 02110-1301, USA. +var d_debug = 1; + +var http_req = new XMLHttpRequest(); +var counter1 = 0; +var error_val = null; +var current_tgid = null; +var send_busy = 0; +var send_qfull = 0; +var send_queue = []; +var req_cb_count = 0; +var request_count = 0; +var nfinal_count = 0; +var n200_count = 0; +var r200_count = 0; +var SEND_QLIMIT = 5; + function find_parent(ele, tagname) { while (ele) { if (ele.nodeName == tagname) @@ -85,12 +101,6 @@ function f_select(command) { nav_update(command); } -var http_req = new XMLHttpRequest(); - -var counter1 = 0; -var error_val = null; -var current_tgid = null; - function is_digit(s) { if (s >= "0" && s <= "9") return true; @@ -188,7 +198,7 @@ function trunk_update(d) { html += "
"; } if (error_val != null) { - html += "Frequency error: " + error_val + " Hz. (approx) "; + html += "Frequency error: " + error_val + " Hz. (approx)
"; } // system frequencies table @@ -220,11 +230,17 @@ function trunk_update(d) { function http_req_cb() { + req_cb_count += 1; s = http_req.readyState; - if (s != 4) + if (s != 4) { + nfinal_count += 1; return; - if (http_req.status != 200) + } + if (http_req.status != 200) { + n200_count += 1; return; + } + r200_count += 1; var dl = JSON.parse(http_req.responseText); var dispatch = {'trunk_update': trunk_update, 'change_freq': change_freq, 'rx_update': rx_update} for (var i=0; i= SEND_QLIMIT) { + send_qfull += 1; + send_queue.unshift(); + } + send_queue.push( {"command": command, "data": data} ); + send_process(); +} + +function send_process() { s = http_req.readyState; if (s != 0 && s != 4) { + send_busy += 1; return; } http_req.open("POST", "/"); http_req.onreadystatechange = http_req_cb; http_req.setRequestHeader("Content-type", "application/json"); - cmd = JSON.stringify( {"command": command, "data": data} ); + cmd = JSON.stringify( send_queue ); + send_queue = []; http_req.send(cmd); } @@ -267,3 +296,19 @@ function f_scan_button(command) { else send_command(command, current_tgid); } + +function f_debug() { + if (!d_debug) return; + var html = "busy " + send_busy; + html += " qfull " + send_qfull; + html += " sendq size " + send_queue.length; + html += " requests " + request_count; + html += "
callbacks:"; + html += " total=" + req_cb_count; + html += " incomplete=" + nfinal_count; + html += " error=" + n200_count; + html += " OK=" + r200_count; + html += "
"; + var div_debug = document.getElementById("div_debug"); + div_debug.innerHTML = html; +} From 4630e585649b45bb9171f55be3867d18333e5cc2 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 15 Mar 2018 14:43:57 -0400 Subject: [PATCH 061/102] json trunking additions --- op25/gr-op25_repeater/apps/terminal.py | 4 +- op25/gr-op25_repeater/apps/trunking.py | 83 ++++++----------------- op25/gr-op25_repeater/apps/tsvfile.py | 91 ++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 66 deletions(-) create mode 100644 op25/gr-op25_repeater/apps/tsvfile.py diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py index d562880..84f7ac3 100755 --- a/op25/gr-op25_repeater/apps/terminal.py +++ b/op25/gr-op25_repeater/apps/terminal.py @@ -2,7 +2,7 @@ # Copyright 2008-2011 Steve Glass # -# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Max H. Parke KA1RBI # # This file is part of OP25 # @@ -132,7 +132,7 @@ class curses_terminal(threading.Thread): # return true signifies end of main event loop msg = json.loads(js) if msg['json_type'] == 'trunk_update': - nacs = [x for x in msg.keys() if x != 'json_type'] + nacs = [x for x in msg.keys() if x != 'json_type' and x != 'data'] if not nacs: return times = {msg[nac]['last_tsbk']:nac for nac in nacs} diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index 17dd095..f6a2053 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -1,5 +1,5 @@ -# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI +# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Max H. Parke KA1RBI # # This file is part of OP25 # @@ -25,6 +25,7 @@ import collections import json sys.path.append('tdma') import lfsr +from tsvfile import make_config, load_tsv def crc16(dat,len): # slow version poly = (1<<12) + (1<<5) + (1<<0) @@ -39,12 +40,6 @@ def crc16(dat,len): # slow version crc = crc ^ 0xffff return crc -def get_frequency(f): # return frequency in Hz - if f.find('.') == -1: # assume in Hz - return int(f) - else: # assume in MHz due to '.' - return int(float(f) * 1000000) - class trunked_system (object): def __init__(self, debug=0, config=None): self.debug = debug @@ -252,7 +247,7 @@ class trunked_system (object): f2 = self.channel_id_to_frequency(ch2) if f1 and f2: self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f' % (rfid, stid, f2 / 1000000.0) - self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f2, 'table': None} + self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f2, 'table': None, 'sysid': syid} if self.debug > 10: print "mbt3c adjacent sys %x rfid %x stid %x ch1 %x ch2 %x f1 %s f2 %s" %(syid, rfid, stid, ch1, ch2, self.channel_id_to_string(ch1), self.channel_id_to_string(ch2)) elif opcode == 0x3b: # network status @@ -486,6 +481,7 @@ class trunked_system (object): if self.debug > 10: print "tsbk3b net stat: wacn %x syid %x ch1 %x(%s)" %(wacn, syid, ch1, self.channel_id_to_string(ch1)) elif opcode == 0x3c: # adjacent status + syid = (tsbk >> 56) & 0xfff rfid = (tsbk >> 48) & 0xff stid = (tsbk >> 40) & 0xff ch1 = (tsbk >> 24) & 0xffff @@ -493,7 +489,7 @@ class trunked_system (object): f1 = self.channel_id_to_frequency(ch1) if f1 and table in self.freq_table: self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f tbl:%d' % (rfid, stid, (f1 + self.freq_table[table]['offset']) / 1000000.0, table) - self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f1 + self.freq_table[table]['offset'], 'table': table} + self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f1 + self.freq_table[table]['offset'], 'table': table, 'sysid':syid} if self.debug > 10: print "tsbk3c adjacent: rfid %x stid %d ch1 %x(%s)" %(rfid, stid, ch1, self.channel_id_to_string(ch1)) if table in self.freq_table: @@ -512,11 +508,6 @@ class trunked_system (object): self.trunk_cc = self.cc_list[self.cc_list_index] sys.stderr.write('%f set trunk_cc to %s\n' % (curr_time, self.trunk_cc)) -def get_int_dict(s): - if s[0].isdigit(): - return dict.fromkeys([int(d) for d in s.split(',')]) - return dict.fromkeys([int(d) for d in open(s).readlines()]) - class rx_ctl (object): def __init__(self, debug=0, frequency_set=None, conf_file=None, logfile_workers=None): class _states(object): @@ -550,6 +541,7 @@ class rx_ctl (object): self.working_frequencies = {} self.xor_cache = {} self.last_garbage_collect = 0 + self.last_command = {'command': None, 'time': time.time()} if self.logfile_workers: self.input_rate = self.logfile_workers[0]['demod'].input_rate @@ -587,38 +579,13 @@ class rx_ctl (object): def add_trunked_system(self, nac): assert nac not in self.trunked_systems # duplicate nac not allowed - blacklist = {} - whitelist = None - tgid_map = {} cfg = None if nac in self.configs: cfg = self.configs[nac] self.trunked_systems[nac] = trunked_system(debug = self.debug, config=cfg) def build_config_tsv(self, tsv_filename): - import csv - hdrmap = [] - configs = {} - with open(tsv_filename, 'rb') as csvfile: - sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) - for row in sreader: - if not hdrmap: - # process first line of tsv file - header line - for hdr in row: - hdr = hdr.replace(' ', '_') - hdr = hdr.lower() - hdrmap.append(hdr) - continue - fields = {} - for i in xrange(len(row)): - if row[i]: - fields[hdrmap[i]] = row[i] - if hdrmap[i] != 'sysname': - fields[hdrmap[i]] = fields[hdrmap[i]].lower() - nac = int(fields['nac'], 0) - configs[nac] = fields - - self.setup_config(configs) + self.setup_config(load_tsv(tsv_filename)) def build_config(self, config_filename): import ConfigParser @@ -662,30 +629,8 @@ class rx_ctl (object): self.nacs.append(nac) def setup_config(self, configs): - for nac in configs: - self.configs[nac] = {'cclist':[], 'offset':0, 'whitelist':None, 'blacklist':{}, 'tgid_map':{}, 'sysname': configs[nac]['sysname'], 'center_frequency': None} - for f in configs[nac]['control_channel_list'].split(','): - self.configs[nac]['cclist'].append(get_frequency(f)) - if 'offset' in configs[nac]: - self.configs[nac]['offset'] = int(configs[nac]['offset']) - if 'modulation' in configs[nac]: - self.configs[nac]['modulation'] = configs[nac]['modulation'] - else: - self.configs[nac]['modulation'] = 'cqpsk' - for k in ['whitelist', 'blacklist']: - if k in configs[nac]: - self.configs[nac][k] = get_int_dict(configs[nac][k]) - if 'tgid_tags_file' in configs[nac]: - import csv - with open(configs[nac]['tgid_tags_file'], 'rb') as csvfile: - sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) - for row in sreader: - tgid = int(row[0]) - txt = row[1] - self.configs[nac]['tgid_map'][tgid] = txt - if 'center_frequency' in configs[nac]: - self.configs[nac]['center_frequency'] = get_frequency(configs[nac]['center_frequency']) - + self.configs = make_config(configs) + for nac in self.configs.keys(): self.add_trunked_system(nac) def find_next_tsys(self): @@ -695,9 +640,15 @@ class rx_ctl (object): return self.nacs[self.current_id] def to_json(self): + current_time = time.time() d = {'json_type': 'trunk_update'} for nac in self.trunked_systems.keys(): d[nac] = json.loads(self.trunked_systems[nac].to_json()) + d['data'] = {'last_command': self.last_command['command'], + 'last_command_time': int(self.last_command['time'] - current_time), + 'tgid_hold': self.tgid_hold, + 'tgid_hold_until': int(self.tgid_hold_until - current_time), + 'hold_mode': self.hold_mode} return json.dumps(d) def to_string(self): @@ -934,6 +885,7 @@ class rx_ctl (object): elif command == 'duid7' or command == 'duid12': pass elif command == 'hold': + self.last_command = {'command': command, 'time': curr_time} if self.hold_mode is False and self.current_tgid: self.tgid_hold = self.current_tgid self.tgid_hold_until = curr_time + 86400 * 10000 @@ -946,18 +898,21 @@ class rx_ctl (object): self.tgid_hold_until = curr_time self.hold_mode = False elif command == 'set_hold': + self.last_command = {'command': command, 'time': curr_time} if self.current_tgid: self.tgid_hold = self.current_tgid self.tgid_hold_until = curr_time + 86400 * 10000 self.hold_mode = True print 'set hold until %f' % self.tgid_hold_until elif command == 'unset_hold': + self.last_command = {'command': command, 'time': curr_time} if self.current_tgid: self.current_tgid = None self.tgid_hold = None self.tgid_hold_until = curr_time self.hold_mode = False elif command == 'skip' or command == 'lockout': + self.last_command = {'command': command, 'time': curr_time} if self.current_tgid: end_time = None if command == 'skip': diff --git a/op25/gr-op25_repeater/apps/tsvfile.py b/op25/gr-op25_repeater/apps/tsvfile.py new file mode 100644 index 0000000..8cb5c6b --- /dev/null +++ b/op25/gr-op25_repeater/apps/tsvfile.py @@ -0,0 +1,91 @@ + +# Copyright 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Max H. Parke KA1RBI +# +# This file is part of OP25 +# +# OP25 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. +# +# OP25 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 OP25; see the file COPYING. If not, write to the Free +# Software Foundation, Inc., 51 Franklin Street, Boston, MA +# 02110-1301, USA. + +import sys +import csv + +def get_frequency(f): # return frequency in Hz + if f.find('.') == -1: # assume in Hz + return int(f) + else: # assume in MHz due to '.' + return int(float(f) * 1000000) + +def get_int_dict(s): + if s[0].isdigit(): + return dict.fromkeys([int(d) for d in s.split(',')]) + return dict.fromkeys([int(d) for d in open(s).readlines()]) + +def load_tsv(tsv_filename): + hdrmap = [] + configs = {} + with open(tsv_filename, 'rb') as csvfile: + sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) + for row in sreader: + if not hdrmap: + # process first line of tsv file - header line + for hdr in row: + hdr = hdr.replace(' ', '_') + hdr = hdr.lower() + hdrmap.append(hdr) + continue + fields = {} + for i in xrange(len(row)): + if row[i]: + fields[hdrmap[i]] = row[i] + if hdrmap[i] != 'sysname': + fields[hdrmap[i]] = fields[hdrmap[i]].lower() + nac = int(fields['nac'], 0) + configs[nac] = fields + return configs + +def make_config(configs): + result_config = {} + for nac in configs: + result_config[nac] = {'cclist':[], 'offset':0, 'whitelist':None, 'blacklist':{}, 'tgid_map':{}, 'sysname': configs[nac]['sysname'], 'center_frequency': None} + for f in configs[nac]['control_channel_list'].split(','): + result_config[nac]['cclist'].append(get_frequency(f)) + if 'offset' in configs[nac]: + result_config[nac]['offset'] = int(configs[nac]['offset']) + if 'modulation' in configs[nac]: + result_config[nac]['modulation'] = configs[nac]['modulation'] + else: + result_config[nac]['modulation'] = 'cqpsk' + for k in ['whitelist', 'blacklist']: + if k in configs[nac]: + result_config[nac][k] = get_int_dict(configs[nac][k]) + if 'tgid_tags_file' in configs[nac]: + import csv + with open(configs[nac]['tgid_tags_file'], 'rb') as csvfile: + sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) + for row in sreader: + tgid = int(row[0]) + txt = row[1] + result_config[nac]['tgid_map'][tgid] = txt + if 'center_frequency' in configs[nac]: + result_config[nac]['center_frequency'] = get_frequency(configs[nac]['center_frequency']) + return result_config + +def main(): + import json + result = make_config(load_tsv(sys.argv[1])) + print json.dumps(result, indent=4, separators=[',',':'], sort_keys=True) + +if __name__ == '__main__': + main() From 5ea5186c5dcf02280848b5cba91fc37d39f8dcc4 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 16 Mar 2018 16:14:43 -0400 Subject: [PATCH 062/102] byteify --- op25/gr-op25_repeater/apps/multi_rx.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/op25/gr-op25_repeater/apps/multi_rx.py b/op25/gr-op25_repeater/apps/multi_rx.py index 96d03c9..36c5091 100755 --- a/op25/gr-op25_repeater/apps/multi_rx.py +++ b/op25/gr-op25_repeater/apps/multi_rx.py @@ -49,6 +49,17 @@ _def_symbol_rate = 4800 # The P25 receiver # +def byteify(input): # thx so + if isinstance(input, dict): + return {byteify(key): byteify(value) + for key, value in input.iteritems()} + elif isinstance(input, list): + return [byteify(element) for element in input] + elif isinstance(input, unicode): + return input.encode('utf-8') + else: + return input + class device(object): def __init__(self, config): speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000] @@ -172,17 +183,6 @@ class rx_block (gr.top_block): class rx_main(object): def __init__(self): - def byteify(input): # thx so - if isinstance(input, dict): - return {byteify(key): byteify(value) - for key, value in input.iteritems()} - elif isinstance(input, list): - return [byteify(element) for element in input] - elif isinstance(input, unicode): - return input.encode('utf-8') - else: - return input - self.keep_running = True # command line argument parsing From abf9b4f06c95f864187c7a2f01141bfadfe46191 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 16 Mar 2018 16:26:42 -0400 Subject: [PATCH 063/102] configuration additions --- op25/gr-op25_repeater/apps/http.py | 158 +++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 8 deletions(-) mode change 100644 => 100755 op25/gr-op25_repeater/apps/http.py diff --git a/op25/gr-op25_repeater/apps/http.py b/op25/gr-op25_repeater/apps/http.py old mode 100644 new mode 100755 index 9196005..2af7e96 --- a/op25/gr-op25_repeater/apps/http.py +++ b/op25/gr-op25_repeater/apps/http.py @@ -1,3 +1,4 @@ +#! /usr/bin/env python # Copyright 2017, 2018 Max H. Parke KA1RBI # @@ -26,14 +27,20 @@ import json import socket import traceback import threading +import glob from gnuradio import gr from waitress.server import create_server +from optparse import OptionParser +from multi_rx import byteify +from rx import p25_rx_block my_input_q = None my_output_q = None my_recv_q = None my_port = None +my_backend = None +CFG_DIR = '../www/config/' """ fake http and ajax server module @@ -63,20 +70,70 @@ def static_file(environ, start_response): status = '200 OK' return status, content_type, output +def valid_tsv(filename): + if not os.access(filename, os.R_OK): + return False + line = open(filename).readline() + for word in 'Sysname Offset NAC Modulation TGID Whitelist Blacklist'.split(): + if word not in line: + return False + return True + +def do_request(d): + global my_backend + TSV_DIR = './' + if d['command'].startswith('rx-'): + msg = gr.message().make_from_string(json.dumps(d), -2, 0, 0) + if not my_backend.input_q.full_p(): + my_backend.input_q.insert_tail(msg) + return None + elif d['command'] == 'config-load': + filename = '%s%s.json' % (CFG_DIR, d['data']) + if not os.access(filename, os.R_OK): + return + js_msg = json.loads(open(filename).read()) + return {'json_type':'config_data', 'data': js_msg} + elif d['command'] == 'config-list': + files = glob.glob('%s*.json' % CFG_DIR) + files = [x.replace('.json', '') for x in files] + files = [x.replace(CFG_DIR, '') for x in files] + if d['data'] == 'tsv': + tsvfiles = glob.glob('%s*.tsv' % TSV_DIR) + tsvfiles = [x for x in tsvfiles if valid_tsv(x)] + tsvfiles = [x.replace('.tsv', '[TSV]') for x in tsvfiles] + tsvfiles = [x.replace(TSV_DIR, '') for x in tsvfiles] + files += tsvfiles + return {'json_type':'config_list', 'data': files} + elif d['command'] == 'config-save': + name = d['data']['name'] + if '..' in name or '.json' in name or '/' in name: + return None + filename = '%s%s.json' % (CFG_DIR, d['data']['name']) + open(filename, 'w').write(json.dumps(d['data']['value'], indent=4, separators=[',',':'], sort_keys=True)) + return None + def post_req(environ, start_response, postdata): global my_input_q, my_output_q, my_recv_q, my_port + resp_msg = [] try: data = json.loads(postdata) - for d in data: - msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0) - my_output_q.insert_tail(msg) - time.sleep(0.2) except: sys.stderr.write('post_req: error processing input: %s:\n' % (postdata)) traceback.print_exc(limit=None, file=sys.stderr) sys.stderr.write('*** end traceback ***\n') + for d in data: + if d['command'].startswith('config-') or d['command'].startswith('rx-'): + resp = do_request(d) + if resp: + resp_msg.append(resp) + continue + msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0) + if my_output_q.full_p(): + my_output_q.delete_head_nowait() # ignores result + if not my_output_q.full_p(): + my_output_q.insert_tail(msg) + time.sleep(0.2) - resp_msg = [] while not my_recv_q.empty_p(): msg = my_recv_q.delete_head() if msg.type() == -4: @@ -124,9 +181,12 @@ def process_qmsg(msg): class http_server(object): def __init__(self, input_q, output_q, endpoint, **kwds): global my_input_q, my_output_q, my_recv_q, my_port - host, port = endpoint.split(':') - if my_port is not None: - raise AssertionError('this server is already active on port %s' % my_port) + if endpoint == 'internal': + return + else: + host, port = endpoint.split(':') + if my_port is not None: + raise AssertionError('this server is already active on port %s' % my_port) my_input_q = input_q my_output_q = output_q my_port = int(port) @@ -152,3 +212,85 @@ class queue_watcher(threading.Thread): while(self.keep_running): msg = self.msgq.delete_head() self.callback(msg) + +class Backend(threading.Thread): + def __init__(self, options, input_q, output_q, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.keep_running = True + self.rx_options = None + self.input_q = input_q + self.output_q = output_q + self.verbosity = options.verbosity + self.start() + + def process_msg(self, msg): + msg = json.loads(msg.to_string()) + if msg['command'] == 'rx-start': + options = rx_options(msg['data']) + options.verbosity = self.verbosity + options._js_config['config-rx-data'] = {'input_q': self.input_q, 'output_q': self.output_q} + self.tb = p25_rx_block(options) + + def run(self): + while self.keep_running: + msg = self.input_q.delete_head() + self.process_msg(msg) + +class rx_options(object): + def __init__(self, name): + def map_name(k): + return k.replace('-', '_') + + filename = '%s%s.json' % (CFG_DIR, name) + if not os.access(filename, os.R_OK): + return + config = byteify(json.loads(open(filename).read())) + dev = [x for x in config['devices'] if x['active']][0] + if not dev: + return + chan = [x for x in config['channels'] if x['active']][0] + if not chan: + return + options = object() + for k in config['backend-rx'].keys(): + setattr(self, map_name(k), config['backend-rx'][k]) + for k in 'args frequency gains offset'.split(): + setattr(self, k, dev[k]) + for k in 'demod_type filter_type'.split(): + setattr(self, k, chan[k]) + self.freq_corr = dev['ppm'] + self.sample_rate = dev['rate'] + self.plot_mode = chan['plot'] + self.phase2_tdma = chan['phase2_tdma'] + self.trunk_conf_file = None + self.terminal_type = None + self._js_config = config + +def http_main(): + global my_backend + # command line argument parsing + parser = OptionParser() + parser.add_option("-c", "--config-file", type="string", default=None, help="specify config file name") + parser.add_option("-e", "--endpoint", type="string", default="127.0.0.1:8080", help="address:port to listen on (use addr 0.0.0.0 to enable external clients)") + parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") + parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") + (options, args) = parser.parse_args() + + # wait for gdb + if options.pause: + print 'Ready for GDB to attach (pid = %d)' % (os.getpid(),) + raw_input("Press 'Enter' to continue...") + + input_q = gr.msg_queue(20) + output_q = gr.msg_queue(20) + backend_input_q = gr.msg_queue(20) + backend_output_q = gr.msg_queue(20) + + my_backend = Backend(options, backend_input_q, backend_output_q) + server = http_server(input_q, output_q, options.endpoint) + + server.run() + +if __name__ == '__main__': + http_main() From 411d385125ce2db960501c8894877128bee8b63c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 16 Mar 2018 16:28:37 -0400 Subject: [PATCH 064/102] temp.png --- op25/gr-op25_repeater/www/images/temp.png | Bin 0 -> 75597 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 op25/gr-op25_repeater/www/images/temp.png diff --git a/op25/gr-op25_repeater/www/images/temp.png b/op25/gr-op25_repeater/www/images/temp.png new file mode 100644 index 0000000000000000000000000000000000000000..c052f3fd354d3bbf8ce55826a8eee0c31a2b6b8d GIT binary patch literal 75597 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 NmS(mW%278X{{>=z78d{j literal 0 HcmV?d00001 From 9d344f283b9a595b35c6b6e9a7e6b514838ed974 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 16 Mar 2018 16:29:54 -0400 Subject: [PATCH 065/102] add config directory --- op25/gr-op25_repeater/www/config/README | 1 + 1 file changed, 1 insertion(+) create mode 100644 op25/gr-op25_repeater/www/config/README diff --git a/op25/gr-op25_repeater/www/config/README b/op25/gr-op25_repeater/www/config/README new file mode 100644 index 0000000..089e50a --- /dev/null +++ b/op25/gr-op25_repeater/www/config/README @@ -0,0 +1 @@ +Saved JSON config files are stored in this directory From e572cfe8022b2d2fd0d591bf90324c18c30090dc Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 16 Mar 2018 16:38:26 -0400 Subject: [PATCH 066/102] configuration additions --- .../www/www-static/index.html | 190 +++++++- op25/gr-op25_repeater/www/www-static/main.css | 91 ++++ op25/gr-op25_repeater/www/www-static/main.js | 410 +++++++++++++++++- 3 files changed, 671 insertions(+), 20 deletions(-) diff --git a/op25/gr-op25_repeater/www/www-static/index.html b/op25/gr-op25_repeater/www/www-static/index.html index b6c00bd..d1b882f 100644 --- a/op25/gr-op25_repeater/www/www-static/index.html +++ b/op25/gr-op25_repeater/www/www-static/index.html @@ -19,8 +19,10 @@

@@ -104,6 +106,190 @@
+ +
+ diff --git a/op25/gr-op25_repeater/www/www-static/main.css b/op25/gr-op25_repeater/www/www-static/main.css index aee67a5..9aa3e5e 100644 --- a/op25/gr-op25_repeater/www/www-static/main.css +++ b/op25/gr-op25_repeater/www/www-static/main.css @@ -153,6 +153,11 @@ div.adjacent { font-size: 24px; } +.boxtitle { + font-weight: bold; + text-align: left; +} + /* the whole NAC string... NAC, freq tsbks, etc. */ .nac { @@ -251,3 +256,89 @@ div.adjacent { background-color: #699; background: linear-gradient(#588, #699); } + +#div_settings table { + border-color: black; +} + +#div_settings tr { + border-top: none; + border-bottom: solid; +} + +#div_settings th.boxtitle-th { + text-align: left; +} + +.div_settings th { + max-width: 75px; + border-style: none; + padding: 3px; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #000; + background: #eee; + text-align: right; + font-weight: normal; +} + +#div_settings td { + max-width: 75px; + border-style: none; + font-weight: bold; + text-align: right; +} + +#div_settings input[type=text] { + max-width: 75px; + border-top: none; + border-bottom-width: 1; + border-bottom: dotted; + border-right: none; + border-left: none; + text-align: right; +} + +#div_settings input[type=button] { + max-width: 75px; + padding: 10px; + color: blue; + border: 0; +} + +#div_settings select { + max-width: 100px; + padding: 0; + border: 0; +} + +#div_settings option { + max-width: 100px; + padding: 0; + border: 0; +} + +.boxtitle { + text-align: left; +} + +div#cfg_list_area select { + width: 250px; + max-width: 250px; +} + +#div_rx_opts td { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + border-style: none; +} + +#div_rx_opts input[type=text] { + max-width: 75px; + border-top: none; + border-bottom-width: 1; + border-bottom: dotted; + border-right: none; + border-left: none; + text-align: right; +} diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js index bf7d2ab..3319b2d 100644 --- a/op25/gr-op25_repeater/www/www-static/main.js +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -47,32 +47,155 @@ function find_parent(ele, tagname) { function f_command(ele, command) { var myrow = find_parent(ele, "TR"); + var mytbl = find_parent(ele, "TABLE"); + amend_d(myrow, mytbl, command); +} + +function edit_freq(freq, to_ui) { + var MHZ = 1000000.0; + if (to_ui) { + var f = (freq / MHZ) + ""; + if (f.indexOf(".") == -1) + f += ".0"; + return f; + } else { + var f = parseFloat(freq); + if (freq.indexOf(".")) + f *= MHZ; + return Math.round(f); + } +} + +function edit_d(d, to_ui) { + var new_d = {}; + var hexints = {"nac":1}; + var ints = {"if_rate":1, "ppm":1, "rate":1, "offset":1, "nac":1, "logfile-workers":1, "decim-amt":1, "seek":1, "hamlib-model":1 }; + var bools = {"active":1, "trunked":1, "rate":1, "offset":1, "phase2_tdma": 1, "phase2-tdma":1, "wireshark":1, "udp-player":1, "audio-if":1, "tone-detect":1, "vocoder":1, "audio":1, "pause":1 }; + var floats = {"costas-alpha":1, "gain-mu":1, "calibration":1, "fine-tune":1, "gain":1, "excess-bw":1, "offset":1} + var lists = {"blacklist":1, "whitelist":1, "cclist":1}; + var freqs = {"frequency":1, "cclist":1}; + + + for (var k in d) { + if (!to_ui) { + if (d[k] == "None") + new_d[k] = null; + else + new_d[k] = d[k]; + if (k == "plot" && !d[k].length) + new_d[k] = null; + if (k in ints) { + new_d[k] = parseInt(new_d[k]); + } else if (k in floats) { + new_d[k] = parseFloat(new_d[k]); + } else if (k in lists) { + var l = new_d[k].split(","); + if (k in freqs) { + var new_l = []; + for (var i in l) + new_l.push(edit_freq(l[i], to_ui)); + new_d[k] = new_l; + } else { + new_d[k] = l; + } + } else if (k in freqs) { + new_d[k] = edit_freq(new_d[k], to_ui); + } + } else { + if (k in hexints) { + new_d[k] = "0x" + d[k].toString(16); + } else if (k in ints) { + if (d[k] == null) + new_d[k] = ""; + else + new_d[k] = d[k].toString(10); + } else if (k in lists) { + if (k in freqs) { + var new_l = []; + for (var i in d[k]) { + new_l.push(edit_freq(d[k][i], to_ui)); + } + new_d[k] = new_l.join(","); + } else { + new_d[k] = d[k].join(","); + } + } else if (k in freqs) { + new_d[k] = edit_freq(d[k], to_ui); + } else { + new_d[k] = d[k]; + } + } + } + return new_d; +} + +function edit_l(cfg, to_ui) { + var new_d = {"devices": [], "channels": []}; + for (var device in cfg['devices']) + new_d["devices"].push(edit_d(cfg['devices'][device], to_ui)); + for (var channel in cfg['channels']) + new_d["channels"].push(edit_d(cfg['channels'][channel], to_ui)); + new_d["backend-rx"] = edit_d(cfg['backend-rx'], to_ui); + return new_d; +} + +function amend_d(myrow, mytbl, command) { + var trunk_row = null; + if (mytbl.id == "chtable") + trunk_row = find_next(myrow, "TR"); if (command == "delete") { var ok = confirm ("Confirm delete"); - if (ok) + if (ok) { myrow.parentNode.removeChild(myrow); + if (mytbl.id == "chtable") + trunk_row.parentNode.removeChild(trunk_row); + } } else if (command == "clone") { var newrow = myrow.cloneNode(true); - if (myrow.nextSibling) - myrow.parentNode.insertBefore(newrow, myrow.nextSibling); - else - myrow.parentNode.appendChild(newrow); + newrow.id = find_free_id("id_"); + if (mytbl.id == "chtable") { + var newrow2 = trunk_row.cloneNode(true); + newrow2.id = "tr_" + newrow.id.substring(3); + if (trunk_row.nextSibling) { + myrow.parentNode.insertBefore(newrow2, trunk_row.nextSibling); + myrow.parentNode.insertBefore(newrow, trunk_row.nextSibling); + } else { + myrow.parentNode.appendChild(newrow); + myrow.parentNode.appendChild(newrow2); + } + } else { + if (myrow.nextSibling) + myrow.parentNode.insertBefore(newrow, myrow.nextSibling); + else + myrow.parentNode.appendChild(newrow); + } } else if (command == "new") { - var mytbl = find_parent(ele, "TABLE"); var newrow = null; - if (mytbl.id == "chtable") + var parent = null; + if (mytbl.id == "chtable") { newrow = document.getElementById("chrow").cloneNode(true); - else if (mytbl.id == "devtable") + parent = document.getElementById("chrow").parentNode; + } else if (mytbl.id == "devtable") { newrow = document.getElementById("devrow").cloneNode(true); - else - return; - mytbl.appendChild(newrow); + parent = document.getElementById("devrow").parentNode; + } else { + return null; + } + newrow.style['display'] = ''; + newrow.id = find_free_id("id_"); + parent.appendChild(newrow); + if (mytbl.id == "chtable") { + var newrow2 = document.getElementById("trrow").cloneNode(true); + newrow2.id = "tr_" + newrow.id.substring(3); + parent.appendChild(newrow2); + } + return newrow.id; } } function nav_update(command) { - var names = ["b1", "b2", "b3"]; - var bmap = { "status": "b1", "plot": "b2", "about": "b3" }; + var names = ["b1", "b2", "b3", "b4", "b5"]; + var bmap = { "status": "b1", "plot": "b2", "settings": "b3", "rx": "b4", "about": "b5" }; var id = bmap[command]; for (var id1 in names) { b = document.getElementById(names[id1]); @@ -85,7 +208,7 @@ function nav_update(command) { } function f_select(command) { - var div_list = ["status", "plot", "about"]; + var div_list = ["status", "plot", "settings", "rx", "about"]; for (var i=0; i"; html += ""; html += ""; - html += ""; + html += ""; var ct = 0; for (var freq in d) { var color = "#d0d0d0"; if ((ct & 1) == 0) color = "#c0c0c0"; ct += 1; - html += ""; + html += ""; } html += "
Adjacent Sites
FrequencyRFSSSiteUplink
FrequencySys IDRFSSSiteUplink
" + freq / 1000000.0 + "" + d[freq]["rfid"] + "" + d[freq]["stid"] + "" + (d[freq]["uplink"] / 1000000.0) + "
" + freq / 1000000.0 + "" + d[freq]['sysid'].toString(16) + "" + d[freq]["rfid"] + "" + d[freq]["stid"] + "" + (d[freq]["uplink"] / 1000000.0) + "


"; @@ -174,6 +299,8 @@ function trunk_update(d) { var do_hex = {"syid":0, "sysid":0, "wacn": 0}; var do_float = {"rxchan":0, "txchan":0}; var html = ""; + var msg = JSON.stringify(d); + document.getElementById("answer_area").innerHTML = msg;msg; for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; @@ -228,6 +355,58 @@ function trunk_update(d) { div_s1.innerHTML = html; } +function config_list(d) { + var html = ""; + html += ""; + document.getElementById("cfg_list_area").innerHTML = html; +} + +function config_data(d) { + var cfg = edit_l(d['data'], true); + open_editor(); + var chtable = document.getElementById("chtable"); + var devtable = document.getElementById("devtable"); + var chrow = document.getElementById("chrow"); + var devrow = document.getElementById("devrow"); + for (var device in cfg['devices']) + rollup_row("dev", document.getElementById(amend_d(devrow, devtable, "new")), cfg['devices'][device]); + for (var channel in cfg['channels']) + rollup_row("ch", document.getElementById(amend_d(chrow, chtable, "new")), cfg['channels'][channel]); + rollup_rx_rows(cfg['backend-rx']); +} + +function open_editor() { + document.getElementById("edit_settings").style["display"] = ""; + var rows = document.querySelectorAll(".dynrow"); + var ct = 0; + for (var r in rows) { + var row = rows[r]; + ct += 1; + if (row.id && (row.id.substring(0,3) == "id_" || row.id.substring(0,3) == "tr_")) { + row.parentNode.removeChild(row); + } + } + var oldtbl = document.getElementById("rt_1"); + if (oldtbl) + oldtbl.parentNode.removeChild(oldtbl); + var tbl = document.getElementById("rxopt-table"); + var newtbl = tbl.cloneNode(true); + newtbl.id = "rt_1"; + newtbl.style["display"] = ""; + var rxrow = newtbl.querySelector(".rxrow"); + var advrow = newtbl.querySelector(".advrow"); + rxrow.id = "rx_1"; + advrow.id = "rx_2"; + if (tbl.nextSibling) + tbl.parentNode.insertBefore(newtbl, tbl.nextSibling); + else + tbl.parentNode.appendChild(newtbl); +} function http_req_cb() { req_cb_count += 1; @@ -242,7 +421,7 @@ function http_req_cb() { } r200_count += 1; var dl = JSON.parse(http_req.responseText); - var dispatch = {'trunk_update': trunk_update, 'change_freq': change_freq, 'rx_update': rx_update} + var dispatch = {'trunk_update': trunk_update, 'change_freq': change_freq, 'rx_update': rx_update, 'config_data': config_data, 'config_list': config_list} for (var i=0; i= SEND_QLIMIT) { send_qfull += 1; send_queue.unshift(); } - send_queue.push( {"command": command, "data": data} ); + send_queue.push( d ); send_process(); } @@ -312,3 +496,193 @@ function f_debug() { var div_debug = document.getElementById("div_debug"); div_debug.innerHTML = html; } + +function find_next(e, tag) { + var n = e.nextSibling; + for (var i=0; i<25; i++) { + if (n == null) + return null; + if (n.nodeName == tag) + return n; + n = n.nextSibling; + } + return null; +} + +function find_free_id(pfx) { + for (var seq = 1; seq < 5000; seq++) { + var test_id = pfx + seq; + var ele = document.getElementById(test_id); + if (!ele) + return test_id; + } + return null; +} + +function f_trunked(e) { + var row = find_parent(e, "TR"); + var trrow = document.getElementById("tr_" + row.id.substring(3)); + trrow['style']["display"] = (e.checked) ? "" : "none"; +} + +function read_write_sel(sel_node, def) { + var result = []; + var elist = sel_node.querySelectorAll("option"); + for (var e in elist) { + var ele = elist[e]; + if (def) { + var options = def[sel_node.name].split(","); + var opts = {}; + for (var o in options) + opts[options[o]] = 1; + if (ele.value in opts) + ele.selected = true; + else + ele.selected = false; + } else { + if (ele.selected) + result.push(ele.value); + } + } + if (!def) + return result.join(); +} + +function read_write(elist, def) { + var result = {}; + var s = "len: " + elist.length + "; "; + for (var e in elist) { + s += elist[e].tagName + "; "; + } + for (var e in elist) { + var ele = elist[e]; + if (ele.nodeName == 'INPUT') { + if (ele.type == 'text') + if (def) { + ele.value = def[ele.name]; + s += ele.name + "=" + ele.value + "; "; + } else + result[ele.name] = ele.value; + else if (ele.type == 'checkbox') + if (def) { + ele.checked = def[ele.name]; + s += "checkbox " + ele.name + "; "; + } + else + result[ele.name] = ele.checked; + } else if (ele.nodeName == 'SELECT') { + if (def) { + read_write_sel(ele, def); + s += "select " + ele.name + "; "; + } + else + result[ele.name] = read_write_sel(ele, def); + } + + } + if (!def) + return result; +} + +function rollup_row(which, row, def) { + var elements = Array.from(row.querySelectorAll("input,select")); + if (which == "ch") { + var trrow = document.getElementById("tr_" + row.id.substring(3)); + elements = elements.concat(Array.from(trrow.querySelectorAll("input,select"))); + } + else if (which == "rx") { + var advrow = document.getElementById("rx_2"); + elements = elements.concat(Array.from(advrow.querySelectorAll("input,select"))); + } + if (def && which == "ch") + trrow.style["display"] = (def["trunked"]) ? "" : "none"; + var result = read_write(elements, def); + if (!def) + return result; +} + +function rollup(which, def) { + var result = []; + var mytbl = document.getElementById(which + "table"); + var elements = mytbl.querySelectorAll(".dynrow"); + for (var e in elements) { + var row = elements[e]; + if (row.id != null && row.id.substring(0,3) == "id_") + result.push(rollup_row(which, row)); + } + if (!def) + return result; +} + +function rollup_rx_rows(def) { + return rollup_row("rx", document.getElementById("rx_1"), def); +} + +function f_save() { + var name = document.getElementById("config_name"); + if (!name.value) { + alert("Name is required"); + name.focus(); + return; + } + if (name.value == "New Configuration") { + alert ("'" + name.value + "' is a reserved name, please retry"); + name.value = ""; + name.focus(); + return; + } + var cfg = { "devices": rollup("dev", null), "channels": rollup("ch", null), "backend-rx": rollup_rx_rows(null) }; + cfg = edit_l(cfg, false); + var request = {"name": name.value, "value": cfg}; + send_command("config-save", request); + f_list(); +} + +function f_list() { + var inp = document.getElementById("include_tsv"); + send_command("config-list", (inp.checked) ? "tsv" : ""); +} + +function f_start() { + var sel = document.getElementById("config_select"); + if (!sel) return; + var val = read_write_sel(sel, null); + if ((!val) || val == "New Configuration") { + alert ("You must select a valid configuration to start"); + return; + } + if (val.indexOf("[TSV]") >= 0) { + alert ("TSV files not supported. First, invoke \"Edit\"; inspect the resulting configuration; then click \"Save\"."); + return; + } + send_command("rx-start", val); +} + +function f_load() { + var sel = document.getElementById("config_select"); + if (!sel) return; + var val = read_write_sel(sel, null); + if (!val) { + alert ("You must select a configuration to edit"); + return; + } + if (val == "New Configuration") { + open_editor(); + } else { + send_command('config-load', val); + var ele = document.getElementById("config_name"); + ele.value = val; + } +} + +function show_advanced(o) { + var tbl = find_parent(o, "TABLE"); + var row = tbl.querySelector(".advrow"); + if (o.value == "Show") { + o.value = "Hide"; + row.style["display"] = ""; + } else { + o.value = "Show"; + row.style["display"] = "none"; + } +} From bd95950d8ca8e31a1c8a785d4add132aa7a202f6 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 16 Mar 2018 16:56:15 -0400 Subject: [PATCH 067/102] main.js edits --- op25/gr-op25_repeater/www/www-static/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js index 3319b2d..dd0706d 100644 --- a/op25/gr-op25_repeater/www/www-static/main.js +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -299,8 +299,6 @@ function trunk_update(d) { var do_hex = {"syid":0, "sysid":0, "wacn": 0}; var do_float = {"rxchan":0, "txchan":0}; var html = ""; - var msg = JSON.stringify(d); - document.getElementById("answer_area").innerHTML = msg;msg; for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; @@ -531,6 +529,8 @@ function read_write_sel(sel_node, def) { for (var e in elist) { var ele = elist[e]; if (def) { + if (!def[sel_node.name]) + return; var options = def[sel_node.name].split(","); var opts = {}; for (var o in options) From 85d09681fe965af8bd2900124f68056139388e14 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 21 Mar 2018 13:58:55 -0400 Subject: [PATCH 068/102] js update --- op25/gr-op25_repeater/www/www-static/main.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js index dd0706d..daefb0d 100644 --- a/op25/gr-op25_repeater/www/www-static/main.js +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -79,7 +79,7 @@ function edit_d(d, to_ui) { for (var k in d) { if (!to_ui) { if (d[k] == "None") - new_d[k] = null; + new_d[k] = ""; else new_d[k] = d[k]; if (k == "plot" && !d[k].length) @@ -643,6 +643,10 @@ function f_list() { send_command("config-list", (inp.checked) ? "tsv" : ""); } +function f_stop() { + send_command("rx-stop", ""); +} + function f_start() { var sel = document.getElementById("config_select"); if (!sel) return; From c0bbdc9c70869a32a9421c41b9cd0058bcf5dc1f Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Apr 2018 14:40:11 -0400 Subject: [PATCH 069/102] http server updates --- op25/gr-op25_repeater/apps/http.py | 95 +++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/op25/gr-op25_repeater/apps/http.py b/op25/gr-op25_repeater/apps/http.py index 2af7e96..38ecba4 100755 --- a/op25/gr-op25_repeater/apps/http.py +++ b/op25/gr-op25_repeater/apps/http.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/python # Copyright 2017, 2018 Max H. Parke KA1RBI # @@ -28,12 +28,13 @@ import socket import traceback import threading import glob +import subprocess +import zmq from gnuradio import gr from waitress.server import create_server from optparse import OptionParser from multi_rx import byteify -from rx import p25_rx_block my_input_q = None my_output_q = None @@ -181,12 +182,9 @@ def process_qmsg(msg): class http_server(object): def __init__(self, input_q, output_q, endpoint, **kwds): global my_input_q, my_output_q, my_recv_q, my_port - if endpoint == 'internal': - return - else: - host, port = endpoint.split(':') - if my_port is not None: - raise AssertionError('this server is already active on port %s' % my_port) + host, port = endpoint.split(':') + if my_port is not None: + raise AssertionError('this server is already active on port %s' % my_port) my_input_q = input_q my_output_q = output_q my_port = int(port) @@ -222,15 +220,85 @@ class Backend(threading.Thread): self.input_q = input_q self.output_q = output_q self.verbosity = options.verbosity + + self.zmq_context = zmq.Context() + self.zmq_port = options.zmq_port + + self.zmq_sub = self.zmq_context.socket(zmq.SUB) + self.zmq_sub.connect('tcp://localhost:%d' % self.zmq_port) + self.zmq_sub.setsockopt(zmq.SUBSCRIBE, '') + + self.zmq_pub = self.zmq_context.socket(zmq.PUB) + self.zmq_pub.sndhwm = 5 + self.zmq_pub.bind('tcp://*:%d' % (self.zmq_port+1)) + self.start() + self.subproc = None + self.backend = '%s/%s' % (os.getcwd(), 'rx.py') + + def check_subproc(self): # return True if subprocess is active + if not self.subproc: + return False + rc = self.subproc.poll() + if rc is None: + return True + else: + self.subproc.wait() + self.subproc = None + return False def process_msg(self, msg): + def make_command(options): + types = {'costas-alpha': 'float', 'trunk-conf-file': 'str', 'demod-type': 'str', 'logfile-workers': 'int', 'decim-amt': 'int', 'wireshark-host': 'str', 'gain-mu': 'float', 'phase2-tdma': 'bool', 'seek': 'int', 'ifile': 'str', 'pause': 'bool', 'antenna': 'str', 'calibration': 'float', 'fine-tune': 'float', 'raw-symbols': 'str', 'audio-output': 'str', 'vocoder': 'bool', 'input': 'str', 'wireshark': 'bool', 'gains': 'str', 'args': 'str', 'sample-rate': 'int', 'terminal-type': 'str', 'gain': 'float', 'excess-bw': 'float', 'offset': 'float', 'audio-input': 'str', 'audio': 'bool', 'plot-mode': 'str', 'audio-if': 'bool', 'tone-detect': 'bool', 'frequency': 'int', 'freq-corr': 'float', 'hamlib-model': 'int', 'udp-player': 'bool', 'verbosity': 'int'} + opts = [self.backend] + for k in [ x for x in dir(options) if not x.startswith('_') ]: + kw = k.replace('_', '-') + val = getattr(options, k) + if kw not in types.keys(): + print 'make_command: unknown option: %s %s type %s' % (k, val, type(val)) + return None + elif types[kw] == 'str': + if val: + opts.append('--%s' % kw) + opts.append('%s' % (val)) + elif types[kw] == 'float': + opts.append('--%s' % kw) + if val: + opts.append('%f' % (val)) + else: + opts.append('%f' % (0)) + elif types[kw] == 'int': + opts.append('--%s' % kw) + if val: + opts.append('%d' % (val)) + else: + opts.append('%d' % (0)) + elif types[kw] == 'bool': + if val: + opts.append('--%s' % kw) + else: + print 'make_command: unknown2 option: %s %s type %s' % (k, val, type(val)) + return None + return opts + msg = json.loads(msg.to_string()) if msg['command'] == 'rx-start': + if self.check_subproc(): + sys.stderr.write('command failed: subprocess pid %d already active\n' % self.subproc.pid) + return options = rx_options(msg['data']) options.verbosity = self.verbosity - options._js_config['config-rx-data'] = {'input_q': self.input_q, 'output_q': self.output_q} - self.tb = p25_rx_block(options) + options.terminal_type = 'zmq:tcp:%d' % (self.zmq_port) + cmd = make_command(options) + self.subproc = subprocess.Popen(cmd) + elif msg['command'] == 'rx-stop': + if not self.check_subproc(): + sys.stderr.write('command failed: subprocess not active\n') + return + if msg['data'] == 'kill': + self.subproc.kill() + else: + self.subproc.terminate() def run(self): while self.keep_running: @@ -257,14 +325,12 @@ class rx_options(object): setattr(self, map_name(k), config['backend-rx'][k]) for k in 'args frequency gains offset'.split(): setattr(self, k, dev[k]) - for k in 'demod_type filter_type'.split(): - setattr(self, k, chan[k]) + self.demod_type = chan['demod_type'] self.freq_corr = dev['ppm'] self.sample_rate = dev['rate'] self.plot_mode = chan['plot'] self.phase2_tdma = chan['phase2_tdma'] - self.trunk_conf_file = None - self.terminal_type = None + self.trunk_conf_file = "" self._js_config = config def http_main(): @@ -275,6 +341,7 @@ def http_main(): parser.add_option("-e", "--endpoint", type="string", default="127.0.0.1:8080", help="address:port to listen on (use addr 0.0.0.0 to enable external clients)") parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level") parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup") + parser.add_option("-z", "--zmq-port", type="int", default=25000, help="backend sub port") (options, args) = parser.parse_args() # wait for gdb From 7bc0a5ef96011b7e99594cb34e0528e657265bae Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Apr 2018 15:15:44 -0400 Subject: [PATCH 070/102] http server updates --- op25/gr-op25_repeater/apps/rx.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 57f7652..e25232f 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -261,7 +261,7 @@ class p25_rx_block (gr.top_block): else: raise ValueError('unsupported plot type: %s' % plot_mode) self.plot_sinks.append(sink) - if self.options.terminal_type.startswith('http:'): + if self.is_http_term(): sink.gnuplot.set_interval(_def_interval) sink.gnuplot.set_output_dir(_def_file_dir) @@ -544,8 +544,16 @@ class p25_rx_block (gr.top_block): # except Exception, x: # wx.MessageBox("Cannot open USRP: " + x.message, "USRP Error", wx.CANCEL | wx.ICON_EXCLAMATION) + def is_http_term(self): + if self.options.terminal_type.startswith('http:'): + return True + elif self.options.terminal_type.startswith('zmq:'): + return True + else: + return False + def process_ajax(self): - if not self.options.terminal_type.startswith('http:'): + if not self.is_http_term(): return filenames = [sink.gnuplot.filename for sink in self.plot_sinks if sink.gnuplot.filename] error = None @@ -594,6 +602,8 @@ class du_queue_watcher(threading.Thread): def run(self): while(self.keep_running): msg = self.msgq.delete_head() + if not self.keep_running: + break self.callback(msg) class rx_main(object): From e0eabf10f047f70e017510ed9b2ee0b8a808f161 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Apr 2018 15:18:58 -0400 Subject: [PATCH 071/102] http server updates --- op25/gr-op25_repeater/apps/terminal.py | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py index 84f7ac3..cbc0114 100755 --- a/op25/gr-op25_repeater/apps/terminal.py +++ b/op25/gr-op25_repeater/apps/terminal.py @@ -207,6 +207,53 @@ class curses_terminal(threading.Thread): self.keep_running = False self.send_command('quit', 0) +class zeromq_terminal(threading.Thread): + def __init__(self, input_q, output_q, endpoint, **kwds): + import zmq + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.input_q = input_q + self.output_q = output_q + self.endpoint = endpoint + self.keep_running = True + + if not endpoint.startswith('tcp:'): + sys.stderr.write('zeromq_terminal unsupported endpoint: %s\n' % endpoint) + return + port = endpoint.replace('tcp:', '') + port = int(port) + + self.zmq_context = zmq.Context() + + self.zmq_sub = self.zmq_context.socket(zmq.SUB) + self.zmq_sub.connect('tcp://localhost:%d' % (port+1)) + self.zmq_sub.setsockopt(zmq.SUBSCRIBE, '') + + self.zmq_pub = self.zmq_context.socket(zmq.PUB) + self.zmq_pub.sndhwm = 5 + self.zmq_pub.bind('tcp://*:%d' % port) + + self.queue_watcher = q_watcher(self.input_q, self.process_qmsg) + self.start() + + def end_terminal(self): + self.keep_running = False + + def process_qmsg(self, msg): + msg = self.input_q.delete_head() + self.zmq_pub.send(msg.to_string()) + + def run(self): + while self.keep_running: + js = self.zmq_sub.recv() + if not self.keep_running: + break + msg = gr.message().make_from_string(js, -4, 0, 0) + if self.output_q.full_p(): + self.output_q.delete_head() + if not self.output_q.full_p(): + self.output_q.insert_tail(msg) + class http_terminal(threading.Thread): def __init__(self, input_q, output_q, endpoint, **kwds): from http import http_server @@ -273,6 +320,8 @@ class udp_terminal(threading.Thread): def op25_terminal(input_q, output_q, terminal_type): if terminal_type == 'curses': return curses_terminal(input_q, output_q) + elif terminal_type.startswith('zmq:'): + return zeromq_terminal(input_q, output_q, terminal_type.replace('zmq:', '')) elif terminal_type[0].isdigit(): port = int(terminal_type) return udp_terminal(input_q, output_q, port) From c9bfc96ed0306defbc9c9a6d580e2e83e86840e6 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Apr 2018 15:19:57 -0400 Subject: [PATCH 072/102] http server updates --- op25/gr-op25_repeater/apps/trunking.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index f6a2053..1b782c5 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -548,6 +548,8 @@ class rx_ctl (object): if conf_file: if conf_file.endswith('.tsv'): self.build_config_tsv(conf_file) + elif conf_file.endswith('.json'): + self.build_config_json(conf_file) else: self.build_config(conf_file) self.nacs = self.configs.keys() @@ -572,6 +574,21 @@ class rx_ctl (object): 'wacn': None, 'sysid': None}) + def build_config_json(self, conf_file): + d = json.loads(open(conf_file).read()) + chans = [x for x in d['channels'] if x['active'] and x['trunked']] + self.configs = { chan['nac']: {'cclist':chan['cclist'], + 'offset':0, + 'whitelist':chan['whitelist'], + 'blacklist':chan['blacklist'], + 'sysname': chan['name'], + 'center_frequency': chan['frequency'], + 'modulation': chan['demod_type'], + 'tgid_map': {int(row['tg_id']):row['tg_tag'] for row in chan['tgids']}} + for chan in chans} + for nac in self.configs.keys(): + self.add_trunked_system(nac) + def set_frequency(self, params): frequency = params['freq'] if frequency and self.frequency_set: From 31e11e999db04210c2022a68094a7b12795c11f1 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Apr 2018 15:25:46 -0400 Subject: [PATCH 073/102] configuration updates --- .../www/www-static/index.html | 22 ++++++- op25/gr-op25_repeater/www/www-static/main.js | 58 +++++++++++++++---- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/op25/gr-op25_repeater/www/www-static/index.html b/op25/gr-op25_repeater/www/www-static/index.html index d1b882f..e58a103 100644 --- a/op25/gr-op25_repeater/www/www-static/index.html +++ b/op25/gr-op25_repeater/www/www-static/index.html @@ -116,6 +116,7 @@
- Open configuration editor (any previous unsaved work will be lost)
- Run flowgraph using selected configuration
+ - End running flowgraph

@@ -260,7 +261,7 @@  
- +
@@ -277,7 +278,24 @@ - + + +
Trunking Info NAC
+ + + + + + + + + + + + + +
diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js index daefb0d..5917e79 100644 --- a/op25/gr-op25_repeater/www/www-static/main.js +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -172,24 +172,29 @@ function amend_d(myrow, mytbl, command) { } else if (command == "new") { var newrow = null; var parent = null; + var pfx = "id_"; if (mytbl.id == "chtable") { newrow = document.getElementById("chrow").cloneNode(true); parent = document.getElementById("chrow").parentNode; } else if (mytbl.id == "devtable") { newrow = document.getElementById("devrow").cloneNode(true); parent = document.getElementById("devrow").parentNode; + } else if (mytbl.className == "tgtable") { + newrow = mytbl.querySelector(".tgrow").cloneNode(true); + parent = mytbl.querySelector(".tgrow").parentNode; + pfx = "tg_"; } else { return null; } newrow.style['display'] = ''; - newrow.id = find_free_id("id_"); + newrow.id = find_free_id(pfx); parent.appendChild(newrow); if (mytbl.id == "chtable") { var newrow2 = document.getElementById("trrow").cloneNode(true); newrow2.id = "tr_" + newrow.id.substring(3); parent.appendChild(newrow2); } - return newrow.id; + return newrow; } } @@ -372,9 +377,9 @@ function config_data(d) { var chrow = document.getElementById("chrow"); var devrow = document.getElementById("devrow"); for (var device in cfg['devices']) - rollup_row("dev", document.getElementById(amend_d(devrow, devtable, "new")), cfg['devices'][device]); + rollup_row("dev", amend_d(devrow, devtable, "new"), cfg['devices'][device]); for (var channel in cfg['channels']) - rollup_row("ch", document.getElementById(amend_d(chrow, chtable, "new")), cfg['channels'][channel]); + rollup_row("ch", amend_d(chrow, chtable, "new"), cfg['channels'][channel]); rollup_rx_rows(cfg['backend-rx']); } @@ -588,15 +593,37 @@ function rollup_row(which, row, def) { var elements = Array.from(row.querySelectorAll("input,select")); if (which == "ch") { var trrow = document.getElementById("tr_" + row.id.substring(3)); - elements = elements.concat(Array.from(trrow.querySelectorAll("input,select"))); + var trtable = trrow.querySelector("table.trtable"); + elements = elements.concat(Array.from(trtable.querySelectorAll("input,select"))); + if (def) + trrow.style["display"] = (def["trunked"]) ? "" : "none"; } else if (which == "rx") { var advrow = document.getElementById("rx_2"); elements = elements.concat(Array.from(advrow.querySelectorAll("input,select"))); } - if (def && which == "ch") - trrow.style["display"] = (def["trunked"]) ? "" : "none"; var result = read_write(elements, def); + if (which == "ch") { + var tgtable = trrow.querySelector("table.tgtable"); + var tgrow = trrow.querySelector("tr.tgrow"); + if (def) { + for (var i=0; i Date: Tue, 3 Apr 2018 19:40:14 -0400 Subject: [PATCH 074/102] layout updates thx trip --- op25/gr-op25_repeater/www/images/op25.png | Bin 0 -> 2031 bytes .../www/www-static/index.html | 85 ++++---- op25/gr-op25_repeater/www/www-static/main.css | 206 +++++++++++------- op25/gr-op25_repeater/www/www-static/main.js | 47 ++-- 4 files changed, 199 insertions(+), 139 deletions(-) create mode 100644 op25/gr-op25_repeater/www/images/op25.png diff --git a/op25/gr-op25_repeater/www/images/op25.png b/op25/gr-op25_repeater/www/images/op25.png new file mode 100644 index 0000000000000000000000000000000000000000..a5498570c24184a7cc06bc941821b1ceacfa64ac GIT binary patch literal 2031 zcmVd*Y-`v)L4m`Q!m_t=H?9 zmzVi`{`vXIe|2)Rm(v=DhlkB((~Ie1fvr}n(P-r49^GztZ*MOXtO&HOP+|1@eMG5$i{T+| zZf>GEv`}a?8X$&9zg;XAv)L^2hv3Zf^K;L@Ko#PLc(QuE9t43ge_mc*CjR^vfnnl% zy`E)|0N!LWd47KOXf!&7kB^VyOzVa|Xb|QF68=Lh>uE+B1xBVBb8PHCh(<9E{QwhZ zv{)>fdwD)ztycL@mdj-(MDWSUiRIdgE6DxakHt!*k}Wm)lZ!uqy}!ScRcl9p5$boc z?2wD3MvOo13hChd2GGI&EoL87k+Rc2FfzMNmK}1D)S~VOFrqNl>$T<8(--y8z(}Ca z=kpx_Muxq*BUwzXR`UZ)3;-LWA-c%Vi7ZalVniHHr&HutW==R54CHHCs=!1^J{%4q zUbTGk?-M>Om&<7ik%Bm>;6^+d|KY>KLr$$)2r%RE7{MuiRjE`wmj)t}Xnw6b zJ3E7n}wdU^`8IJp5FJAG6+c_kQGjL2-d z)q&&VV=oJILhT-8h~Q!5p*65%0hPym%>8alwOU1%;C(YGua#B+AqWsI%GLw00$Z%` z2Y3?Ul#$IbZD3n8c6O`w4_LaxK9S2PFpvqKZ*T3dQ!bZ7V2WH)AghPlP7_yG!-K&gksPXH+ZsuQWsU8RD*EI$X~nRRtb)x7 zSjwe&IApusR>Y>g8H*GXDsHh@u*Qk2tE)7VOTOC)z#zEdG@3TBe!ow6&mrHy1?1BN z(HH&=0+%RHZC50!lr+59I~yrNlW~84AEJ^^s4cMRbn4dG@VJo`az3P0dbep*i3+lts8g(XhMEN3^;LbGV73h>UY7Ch52Gv zz?G}jDsvVX6LFr1&9JVxTrLgj-nvbuU=h$DAQ8q2n|sBtY?X)EZnExHE$#xUfN{un zF-*srRV8*a1cpmv`n7)2>2wTk#5B}!I3(*^xK|#B&0H&D`qi;Pm`C=0tIQxX4%yUZ ziHroBl?VnlFjZoO3_Mn^*NyMO91?%@dOe_vsIqQ>cBz1SvzygOvj4_>sr-cBD z>q?N(J(fkAss`5388&9kVHS5^H0coB$(zufP`B;+p^r(=4t| z>LI~PO5EN_Rf|jz^hcxwO;#SpEsIQjOD5Z3kSBqri#zoYC{B`DM>omg4Kf{xx#UTZ zs#YXA&1_imM`$jlQRPXHh6o0c=wfp#M3}j-e5th4B^Z&qd zfjsm{-A3LEmmFhQ>BSQi+bPG8>xzA85Fr`dY1R~i5|_+;d91C`v==h+q& zpM;z-C=!O-#~B62xTMqRh{t>6nQzLknKAbblUZ zUfwH{Bjq>N>cHprT0((!<$r7jz#s)olPzSxc4_>zr~d)_1C}n`Pp4D9kSOT^gR}?l zAwgyxCBbIozsE0?JheuVR9b?2|a$w}!TCK*8aw02B^Tk?9!1#`WMCD3=MZUxeP*
+ - - -
- -
- -
- -
- -
- -
- -
-
- + "; + // box br // end trunk_update HTML // end adjacent sites table @@ -303,10 +298,11 @@ function adjacent_data(d) { function trunk_update(d) { var do_hex = {"syid":0, "sysid":0, "wacn": 0}; var do_float = {"rxchan":0, "txchan":0}; - var html = ""; + var html = ""; // begin trunk_update HTML for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; + html += "
"; html += ""; html += "NAC " + "0x" + parseInt(nac).toString(16) + " "; html += d[nac]['rxchan'] / 1000000.0; @@ -333,8 +329,8 @@ function trunk_update(d) { // system frequencies table - html += "

"; - html += ""; // was width=350 + html += "
"; + html += "
"; html += ""; html += ""; var ct = 0; @@ -348,14 +344,31 @@ function trunk_update(d) { ct += 1; html += ""; } - html += "
System Frequencies
FrequencyLast SeenTalkgoup IDCount
" + parseInt(freq) / 1000000.0 + "" + d[nac]['frequency_data'][freq]['last_activity'] + "" + d[nac]['frequency_data'][freq]['tgids'][0] + "" + tg2 + "" + d[nac]['frequency_data'][freq]['counter'] + "
"; - -// end system freqencies table + html += "
"; // end system freqencies table html += adjacent_data(d[nac]['adjacent_data']); + html += "



"; } var div_s1 = document.getElementById("div_s1"); div_s1.innerHTML = html; + + // disply hold indicator + var x = document.getElementById("holdIndicator"); + if (d['data']['hold_mode']) { + x.style.display = "block"; + } + else { + x.style.display = "none"; + } + + // display last command unless it was more than 10 seconds ago + x2 = d['data']['last_command']; + if (x2 && d['data']['last_command_time'] > -10) { + document.getElementById("lastCommand").innerHTML = "Last Command
" + x2.toUpperCase() + "
" + " " + (d['data']['last_command_time'] * -1) + " secs ago"; + } + else { + document.getElementById("lastCommand").innerHTML = ""; + } } function config_list(d) { From 8a8ab6e90bfaa753ccccc1f740105df75dd68afd Mon Sep 17 00:00:00 2001 From: Matt Ames Date: Thu, 12 Apr 2018 11:13:37 +1000 Subject: [PATCH 075/102] Add in P25 compliant test patterns to tx --- .../apps/tx/testpatterns/1011.pattern | Bin 0 -> 1728 bytes .../apps/tx/testpatterns/afc1011.pattern | Bin 0 -> 1728 bytes .../apps/tx/testpatterns/bercalhex.pattern | Bin 0 -> 1728 bytes .../apps/tx/testpatterns/busyhex.pattern | Bin 0 -> 1728 bytes .../apps/tx/testpatterns/idlehex.pattern | Bin 0 -> 1728 bytes .../apps/tx/testpatterns/silencehex.pattern | Bin 0 -> 1728 bytes .../apps/tx/testpatterns/sources/1011hex.dat | 24 ++++++++++++++++++ .../tx/testpatterns/sources/afc1011hex.dat | 24 ++++++++++++++++++ .../tx/testpatterns/sources/bercalhex.dat | 24 ++++++++++++++++++ .../apps/tx/testpatterns/sources/busyhex.dat | 24 ++++++++++++++++++ .../apps/tx/testpatterns/sources/idlehex.dat | 24 ++++++++++++++++++ .../tx/testpatterns/sources/silencehex.dat | 24 ++++++++++++++++++ .../apps/tx/testpatterns/test.wav | Bin 0 -> 136932 bytes 13 files changed, 144 insertions(+) create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/1011.pattern create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/afc1011.pattern create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/bercalhex.pattern create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/busyhex.pattern create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/idlehex.pattern create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/silencehex.pattern create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/sources/1011hex.dat create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/sources/afc1011hex.dat create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/sources/bercalhex.dat create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/sources/busyhex.dat create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/sources/idlehex.dat create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/sources/silencehex.dat create mode 100644 op25/gr-op25_repeater/apps/tx/testpatterns/test.wav diff --git a/op25/gr-op25_repeater/apps/tx/testpatterns/1011.pattern b/op25/gr-op25_repeater/apps/tx/testpatterns/1011.pattern new file mode 100644 index 0000000000000000000000000000000000000000..2f10ee0b202d65e67178694a2bf32d4accf95ba5 GIT binary patch literal 1728 zcmb`Hixz_*3`0x%|Np!jd=;#E+}2IbL<}KK)*2Oj`^CsdW$w+GqpXE%BCI)g(TpoB z+*smn3`}V;gR#$292G=<&>_Po8IleOKA{)nS9nOjP+2&Y%1+M^9(weNefxcue%8j(`P{^bJZknP zh8QBQD1$6`W)|fS_7~a+V)e)bzgYy2z!^R|9X~kW=$oDVtpitd2n`Mt=vdk<%U%)} z=iS^F#1Vz!^*UcJokRzzmeV;W>)n}KGx#!5IvuOX>FR|>82jfb9(nz^2+qFV8yN#m(j z-7s-CI)*Z)Gb{~TR*P2*iVWBlpjH@ytR7F!2DuqX8h(u=&%I4QXZQvEK2SU2jL?oi z6FuK?YCmk?Na&pbw(%r8hk)fXAeHdD3)I>jlQ{Y5b1 zjJ>I$1Yh1_(4D%rZKqOdW3IWpQTq0Ax>_ip8gBVCuP${~bG!TPRkp_Q>+f1w= zN!nWJfMRD$_v#&4O%#;knd|UHcrc16-$PWK%@gn@YVBT)tQInB_95?Kk@wO?UI{jP zIu`kbU`3v?P5#NgZl!EdSs@?nqYYP}UUZIWSR$0lOk=CeCD_&|=(m*~s*DsXLst13ev* zBB&B$z+u^P7%LG|91e3kAA`61^(tVnIe-YSbfU_*iq#a*;-`mHiYzgLbb08eP)nt- zMU>nS<)Y^lO7x83T`Txx45HK+K6`+EvbV)Q9+GYvhdfNaJanJ$#lFcCDV1;R9nPDF z-X!>8pw|Ijj0N$Z=I9zbCH zo+qij3_%R(z-XIBp+estgb%sYYEb2@!QxIz(?;?qd$BkZ-h{Z@KB2bRLsb;YtgMxt VubHEUe(KW5qL!lHx@_cQ`~f1}2sQu! literal 0 HcmV?d00001 diff --git a/op25/gr-op25_repeater/apps/tx/testpatterns/busyhex.pattern b/op25/gr-op25_repeater/apps/tx/testpatterns/busyhex.pattern new file mode 100644 index 0000000000000000000000000000000000000000..52ccd689cc4584f1d5d7b207127c64f8d0e67053 GIT binary patch literal 1728 zcmb`H0hWU>2t&>X~Y2rMt7`n_KTH6W)W`eb!(xGW*)9h<7(Ez zaEUM$w&l!XcpARE^=LS_9QaDmX3itXT8R{VpfDdKBHSV=!6lL+9|p?X5`00I&oa@3 zUK4#o9yh8;1?U_+?2^wsB=|%F?5^Ppx>`kyKUfydisgWRi0>^0GWv6-zHlRI;Q89b zQ}VNkP4$zR7y%hNv@@6|7CBwuEBY}bC-^xc%*e=x_XaKng12a_9QPAA(3M@8K!H|M z+$Wkk^|RAgh^M0#_#hEI5u4jmotl2v`Hj8`(ogzI1DbAYqA5@Le50>x8`PAi^rZn? zSYcW7d&lxdU*L#{X*1RdzMy}jFQ!6*>eAAx;hX~C-H-a}Gbp_SNqzcPkAL$;1pm_4 NBx91J8F@xu&0jQ+2t)t? literal 0 HcmV?d00001 diff --git a/op25/gr-op25_repeater/apps/tx/testpatterns/idlehex.pattern b/op25/gr-op25_repeater/apps/tx/testpatterns/idlehex.pattern new file mode 100644 index 0000000000000000000000000000000000000000..89d1d3dac934942c48399deaffb60170eda55880 GIT binary patch literal 1728 zcmb`I0T#m`2tyI?f9L(6v|4TZ-Zr<|sTeT?Rtl~1jfb9(nz^2+qFV8yN#m(j z-7s-CI)*Z)Gb{~TR*P2*iVWBlpjH@yte%sc4LasT((r2}o!r~-bA+GJ?;W+n&v5N< zG|}@N*M=8hI}dlxi@Sq|6Mmop<`OpF4Sq&{XBnIw%TCN7K0-=-JjS=&-~ut+1@Yp1 z=LS4ZWsOUK3>}(jfsHlY;vukXX&Sk}cZsmp{E3(eC**nRCeg*>g8~CRv^^?tAs&bo zdO?=YFF|c|tKa9BizoO|4sfQ(<7S-A&&dT=-Xo9ECh`-~Px(azvyGHZjt+jxW?u+S zKVxrdD8Z+9)S^G+mn5`lSLfeZ24}}|)UxA%JnITS*GN{EB6)HX#UX1?OQ=XKC73FX rMALeyFf(A0X{hu$zfgsgh(?t>`UB7nX9c#vewAN?ZaP8#%CA~KbX*9f literal 0 HcmV?d00001 diff --git a/op25/gr-op25_repeater/apps/tx/testpatterns/silencehex.pattern b/op25/gr-op25_repeater/apps/tx/testpatterns/silencehex.pattern new file mode 100644 index 0000000000000000000000000000000000000000..051c5d25cf0b4dac445c988e67a1f12e58e75326 GIT binary patch literal 1728 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 Date: Thu, 12 Apr 2018 11:34:06 +1000 Subject: [PATCH 076/102] Update AlgIDs to latest known values --- op25/gr-op25/lib/value_string.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/op25/gr-op25/lib/value_string.cc b/op25/gr-op25/lib/value_string.cc index ce96dc3..3da0d1a 100644 --- a/op25/gr-op25/lib/value_string.cc +++ b/op25/gr-op25/lib/value_string.cc @@ -55,16 +55,19 @@ const value_string ALGIDS[] = { { 0x04, "SAVILLE" }, { 0x41, "BATON (Auto Odd)" }, /* Type III */ - { 0x80, "Plain" }, - { 0x81, "DES-OFB" }, - { 0x82, "2 key Triple DES" }, - { 0x83, "3 key Triple DES" }, + { 0x80, "Unencrypted" }, + { 0x81, "DES-OFB, 56 bit key" }, + { 0x83, "3 key Triple DES, 168 bit key" }, { 0x84, "AES-256" }, + { 0x85, "AES-128-ECB"}, + { 0x88, "AES-CBC"}, /* Motorola proprietary */ { 0x9F, "Motorola DES-XL" }, { 0xA0, "Motorola DVI-XL" }, { 0xA1, "Motorola DVP-XL" }, + { 0xA2, "Motorola DVI-SPFL"}, { 0xAA, "Motorola ADP" }, + { 0xB0, "Motorola DVP"}, }; const size_t ALGIDS_SZ = sizeof(ALGIDS) / sizeof(ALGIDS[0]); From 3c50b3c54b0ba34b665670682fa6d751b17b6bb7 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 13 Apr 2018 09:09:34 -0400 Subject: [PATCH 077/102] test pattern file utility --- .../apps/tx/testpatterns/sources/make-bin.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 op25/gr-op25_repeater/apps/tx/testpatterns/sources/make-bin.py diff --git a/op25/gr-op25_repeater/apps/tx/testpatterns/sources/make-bin.py b/op25/gr-op25_repeater/apps/tx/testpatterns/sources/make-bin.py new file mode 100755 index 0000000..997c956 --- /dev/null +++ b/op25/gr-op25_repeater/apps/tx/testpatterns/sources/make-bin.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python + +""" +utility program to make binary symbol files + +reads source file (stdin); writes binary file to stdout + +""" + +import sys + +s = sys.stdin.read() +s= s.replace(' ', '') +s= s.replace('\n', '') +s = s.strip() + +dibits = '' + +while s: + s0 = int(s[0], 16) + s = s[1:] + dibits += chr(s0>>2) + dibits += chr(s0&3) + +sys.stdout.write(dibits) From 384c8c28d0812486a49dfd45a982c67a3e374264 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 27 Apr 2018 10:02:33 -0400 Subject: [PATCH 078/102] ui additions thx Trip --- .../www/www-static/index.html | 47 ++++++++++++- op25/gr-op25_repeater/www/www-static/main.css | 63 +++++++++++------ op25/gr-op25_repeater/www/www-static/main.js | 68 +++++++++++++------ 3 files changed, 138 insertions(+), 40 deletions(-) diff --git a/op25/gr-op25_repeater/www/www-static/index.html b/op25/gr-op25_repeater/www/www-static/index.html index e35902a..0d754e1 100644 --- a/op25/gr-op25_repeater/www/www-static/index.html +++ b/op25/gr-op25_repeater/www/www-static/index.html @@ -104,6 +104,51 @@



"; // close div-content close div-info box-br hr-separating each NAC } + return html; +} + +function trunk_update(d) { var div_s1 = document.getElementById("div_s1"); + var html; + if (summary_mode) + html = trunk_summary(d); + else + html = trunk_detail(d); div_s1.innerHTML = html; // disply hold indicator @@ -400,6 +506,7 @@ function trunk_update(d) { else { document.getElementById("lastCommand").innerHTML = ""; } + last_srcaddr = d["srcaddr"]; } function config_list(d) { From 66d0ee86fdd8f295248ca2e2b744e7bf3c27f91d Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 4 Jan 2019 22:05:42 -0500 Subject: [PATCH 097/102] fix display of src addr and ess --- op25/gr-op25_repeater/apps/trunking.py | 1 - op25/gr-op25_repeater/www/www-static/main.css | 14 +++ op25/gr-op25_repeater/www/www-static/main.js | 87 ++++++++++++++++--- 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index 232447c..1377b70 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -741,7 +741,6 @@ class rx_ctl (object): 'tgid_hold': self.tgid_hold, 'tgid_hold_until': int(self.tgid_hold_until - current_time), 'hold_mode': self.hold_mode} - d['nac'] = self.current_nac return json.dumps(d) def dump_tgids(self): diff --git a/op25/gr-op25_repeater/www/www-static/main.css b/op25/gr-op25_repeater/www/www-static/main.css index ab242b9..70a723b 100644 --- a/op25/gr-op25_repeater/www/www-static/main.css +++ b/op25/gr-op25_repeater/www/www-static/main.css @@ -227,6 +227,13 @@ div.adjacent { /* adjacent sites container that holds the table */ text-align: center; } +.red_value { + + font-family: Arial, Helvetica, sans-serif; + color: #f00000; + font-weight: bold; +} + .value { font-family: Arial, Helvetica, sans-serif; @@ -242,6 +249,13 @@ div.adjacent { /* adjacent sites container that holds the table */ font-size: 24px; } +.syscrypto { /* alg/key text */ + + font-family: Arial, Helvetica, sans-serif; + color: #f00000; + font-size: 24px; +} + .boxtitle { font-weight: bold; text-align: left; diff --git a/op25/gr-op25_repeater/www/www-static/main.js b/op25/gr-op25_repeater/www/www-static/main.js index f53ff09..08ee375 100644 --- a/op25/gr-op25_repeater/www/www-static/main.js +++ b/op25/gr-op25_repeater/www/www-static/main.js @@ -24,6 +24,8 @@ var http_req = new XMLHttpRequest(); var counter1 = 0; var error_val = null; var current_tgid = null; +var active_tgid = null; +var active_nac = null; var send_busy = 0; var send_qfull = 0; var send_queue = []; @@ -34,9 +36,12 @@ var n200_count = 0; var r200_count = 0; var SEND_QLIMIT = 5; var summary_mode = true; -var last_srcaddr = 0; var enable_changed = false; var enable_status = []; +var last_srcaddr = []; +var last_alg = []; +var last_algid = []; +var last_keyid = []; function find_parent(ele, tagname) { while (ele) { @@ -273,39 +278,55 @@ function rx_update(d) { // frequency, system, and talkgroup display - function change_freq(d) { +function change_freq(d) { var displayTgid = "—"; var displayTag = " "; var display_src = "—"; + var display_alg = "—"; + var display_keyid = "—"; + var e_class = "value"; var doTruncate = document.getElementById("valTruncate").value; // get truncate value from Configuration - if (d['tgid'] != null) { - displayTgid = d['tgid']; - displayTag = d['tag'].substring(0,doTruncate); - } + last_srcaddr[d['nac']] = d['srcaddr']; + last_alg[d['nac']] = d['alg']; + last_algid[d['nac']] = d['algid']; + last_keyid[d['nac']] = d['keyid']; - if (last_srcaddr != null) { - display_src = last_srcaddr; + if (d['tgid'] != null) { + displayTgid = d['tgid']; + displayTag = d['tag'].substring(0,doTruncate); + if (d['srcaddr'] != null && d['srcaddr'] > 0) + display_src = d['srcaddr']; + display_alg = d['alg']; + if (d['algid'] != 128) { + display_keyid = d['keyid']; + e_class = "red_value"; + } } var html = ""; html += ""; - html += ""; + html += ""; html += ""; html += ""; html += ""; - html += ""; + html += ""; html += ""; html += ""; html += ""; - html += ""; + html += ""; html += ""; + html += ""; html += ""; html += "
" + d['system'].substring(0,doTruncate) + "" + d['system'].substring(0,doTruncate) + ""; html += "Frequency
" + d['freq'] / 1000000.0 + "
" + displayTag + "" + displayTag + ""; html += "Talkgroup ID
" + displayTgid + ""; html += "
" + " " + ""; + html += "Encryption
" + display_alg + ""; + html += "
"; - html += "Source ID
" + display_src + ""; + html += "Key ID
" + display_keyid + ""; + html += "
"; + html += "Source Addr
" + display_src + ""; html += "
"; @@ -314,6 +335,8 @@ function rx_update(d) { div_s2.innerHTML = html; div_s2.style["display"] = ""; + active_nac = d['nac']; + active_tgid = d['tgid']; if (d['tgid'] != null) { current_tgid = d['tgid']; } @@ -368,6 +391,10 @@ function trunk_summary(d) { for (nac in d) { if (!is_digit(nac.charAt(0))) continue; + last_srcaddr[nac] = d[nac]['srcaddr']; + last_alg[nac] = d[nac]['alg']; + last_algid[nac] = d[nac]['algid']; + last_keyid[nac] = d[nac]['keyid']; if (!(nac in enable_status)) enable_status[nac] = true; var times = []; @@ -429,6 +456,10 @@ function trunk_detail(d) { for (var nac in d) { if (!is_digit(nac.charAt(0))) continue; + last_srcaddr[nac] = d[nac]['srcaddr']; + last_alg[nac] = d[nac]['alg']; + last_algid[nac] = d[nac]['algid']; + last_keyid[nac] = d[nac]['keyid']; html += "
"; // open div-content html += ""; html += d[nac]["sysname"] + " . . . . . . . . "; @@ -480,6 +511,36 @@ function trunk_detail(d) { return html; } +function update_data(d) { + if (active_nac == null || active_tgid == null) + return; + var display_src = "—"; + var display_alg = "—"; + var display_keyid = "—"; + var e_class = "value"; + if (last_srcaddr[active_nac] != null) { + display_src = last_srcaddr[active_nac]; + var ele = document.getElementById("dSrc"); + if (ele != null) + ele.innerHTML = display_src; + } + if (last_algid[active_nac] == null || last_alg[active_nac] == null || last_keyid[active_nac] == null) + return; + display_alg = last_alg[active_nac]; + if (last_algid[active_nac] != 128) { + display_keyid = last_keyid[active_nac]; + e_class = "red_value"; + } + ele = document.getElementById("dAlg"); + if (ele != null) { + ele.innerHTML = display_alg; + ele.className = e_class; + } + ele = document.getElementById("dKey"); + if (ele != null) + ele.innerHTML = display_keyid; +} + function trunk_update(d) { var div_s1 = document.getElementById("div_s1"); var html; @@ -506,7 +567,7 @@ function trunk_update(d) { else { document.getElementById("lastCommand").innerHTML = ""; } - last_srcaddr = d["srcaddr"]; + update_data(d); } function config_list(d) { From 60642290270f51ee1b5103d897497b8a96b64929 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 12 Jan 2019 13:18:38 -0500 Subject: [PATCH 098/102] filter out some of the more bogus HDU/LLDU FEC failures in p25p1_fdma --- op25/gr-op25_repeater/lib/p25p1_fdma.cc | 104 +++-- op25/gr-op25_repeater/lib/p25p1_fdma.h | 2 +- op25/gr-op25_repeater/lib/p25p2_vf.cc | 1 + op25/gr-op25_repeater/lib/rs.cc | 521 +++++++++++++++++++++++- op25/gr-op25_repeater/lib/rs.h | 2 +- 5 files changed, 594 insertions(+), 36 deletions(-) diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.cc b/op25/gr-op25_repeater/lib/p25p1_fdma.cc index 163eee4..e239864 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.cc +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.cc @@ -234,24 +234,37 @@ p25p1_fdma::process_duid(uint32_t const duid, uint32_t const nac, const uint8_t* void p25p1_fdma::process_HDU(const bit_vector& A) { + uint32_t MFID; + int i, j, k, ec; + uint32_t gly; + std::vector HB(63,0); // hexbit vector + std::string s = ""; + int failures = 0; + k = 0; if (d_debug >= 10) { fprintf (stderr, "%s NAC 0x%03x HDU: ", logts.get(), framer->nac); } - uint32_t MFID; - int i, j, k, ec; - std::vector HB(63,0); // hexbit vector - std::string s = ""; - k = 0; for (i = 0; i < 36; i ++) { uint32_t CW = 0; for (j = 0; j < 18; j++) { // 18 bits / cw CW = (CW << 1) + A [ hdu_codeword_bits[k++] ]; } - HB[27 + i] = gly24128Dec(CW) & 63; + gly = gly24128Dec(CW); + HB[27 + i] = gly & 63; + if (CW ^ gly24128Enc(gly)) + /* "failures" in this context means any mismatch, + * disregarding how "recoverable" the error may be + * and disregarding how many bits may mismatch */ + failures += 1; } ec = rs16.decode(HB); // Reed Solomon (36,20,17) error correction - + if (failures > 6) { + if (d_debug >= 10) { + fprintf (stderr, "ESS computation suppressed: failed %d of 36, ec=%d\n", failures, ec); + } + return; + } if (ec >= 0) { j = 27; // 72 bit MI for (i = 0; i < 9;) { @@ -270,6 +283,7 @@ p25p1_fdma::process_HDU(const bit_vector& A) for (i = 0; i < 9; i++) { fprintf(stderr, "%02x ", ess_mi[i]); } + fprintf(stderr, ", Golay failures=%d of 36, ec=%d", failures, ec); } s = "{\"nac\" : " + std::to_string(framer->nac) + ", \"algid\" : " + std::to_string(ess_algid) + ", \"alg\" : \"" + lookup(ess_algid, ALGIDS, ALGIDS_SZ) + "\", \"keyid\": " + std::to_string(ess_keyid) + "}"; send_msg(s, -3); @@ -280,12 +294,13 @@ p25p1_fdma::process_HDU(const bit_vector& A) } } -void +int p25p1_fdma::process_LLDU(const bit_vector& A, std::vector& HB) -{ +{ // return count of hamming failures process_duid(framer->duid, framer->nac, NULL, 0); int i, j, k; + int failures = 0; k = 0; for (i = 0; i < 24; i ++) { // 24 10-bit codewords uint32_t CW = 0; @@ -293,66 +308,89 @@ p25p1_fdma::process_LLDU(const bit_vector& A, std::vector& HB) CW = (CW << 1) + A[ imbe_ldu_ls_data_bits[k++] ]; } HB[39 + i] = hmg1063Dec( CW >> 4, CW & 0x0f ); + if (CW ^ ((HB[39+i] << 4) + hmg1063EncTbl[HB[39+i]])) + failures += 1; } + return failures; } void p25p1_fdma::process_LDU1(const bit_vector& A) { + int hmg_failures; if (d_debug >= 10) { fprintf (stderr, "%s NAC 0x%03x LDU1: ", logts.get(), framer->nac); } std::vector HB(63,0); // hexbit vector - process_LLDU(A, HB); - process_LCW(HB); + hmg_failures = process_LLDU(A, HB); + if (hmg_failures < 6) + process_LCW(HB); + else { + if (d_debug >= 10) { + fprintf (stderr, " (LCW computation suppressed"); + } + } if (d_debug >= 10) { + fprintf(stderr, ", Hamming failures=%d of 24", hmg_failures); fprintf (stderr, "\n"); } - process_voice(A); + // LDUx frames with exactly 20 failures seem to be not uncommon (and deliberate?) + // a TDU almost always immediately follows these code violations + if (hmg_failures < 16) + process_voice(A); } void p25p1_fdma::process_LDU2(const bit_vector& A) { std::string s = ""; + int hmg_failures; if (d_debug >= 10) { fprintf (stderr, "%s NAC 0x%03x LDU2: ", logts.get(), framer->nac); } std::vector HB(63,0); // hexbit vector - process_LLDU(A, HB); + hmg_failures = process_LLDU(A, HB); - int i, j, ec; - ec = rs8.decode(HB); // Reed Solomon (24,16,9) error correction - if (ec >= 0) { // save info if good decode - j = 39; // 72 bit MI - for (i = 0; i < 9;) { - ess_mi[i++] = (uint8_t) (HB[j ] << 2) + (HB[j+1] >> 4); - ess_mi[i++] = (uint8_t) ((HB[j+1] & 0x0f) << 4) + (HB[j+2] >> 2); - ess_mi[i++] = (uint8_t) ((HB[j+2] & 0x03) << 6) + HB[j+3]; - j += 4; - } - ess_algid = (HB[j ] << 2) + (HB[j+1] >> 4); // 8 bit AlgId - ess_keyid = ((HB[j+1] & 0x0f) << 12) + (HB[j+2] << 6) + HB[j+3]; // 16 bit KeyId - - if (d_debug >= 10) { - fprintf(stderr, "ESS: algid=%x, keyid=%x, mi=", ess_algid, ess_keyid); - for (int i = 0; i < 9; i++) { - fprintf(stderr, "%02x ", ess_mi[i]); + int i, j, ec=0; + if (hmg_failures < 6) { + ec = rs8.decode(HB); // Reed Solomon (24,16,9) error correction + if (ec >= 0) { // save info if good decode + j = 39; // 72 bit MI + for (i = 0; i < 9;) { + ess_mi[i++] = (uint8_t) (HB[j ] << 2) + (HB[j+1] >> 4); + ess_mi[i++] = (uint8_t) ((HB[j+1] & 0x0f) << 4) + (HB[j+2] >> 2); + ess_mi[i++] = (uint8_t) ((HB[j+2] & 0x03) << 6) + HB[j+3]; + j += 4; } + ess_algid = (HB[j ] << 2) + (HB[j+1] >> 4); // 8 bit AlgId + ess_keyid = ((HB[j+1] & 0x0f) << 12) + (HB[j+2] << 6) + HB[j+3]; // 16 bit KeyId + + if (d_debug >= 10) { + fprintf(stderr, "ESS: algid=%x, keyid=%x, mi=", ess_algid, ess_keyid); + for (int i = 0; i < 9; i++) { + fprintf(stderr, "%02x ", ess_mi[i]); + } + } + s = "{\"nac\" : " + std::to_string(framer->nac) + ", \"algid\" : " + std::to_string(ess_algid) + ", \"alg\" : \"" + lookup(ess_algid, ALGIDS, ALGIDS_SZ) + "\", \"keyid\": " + std::to_string(ess_keyid) + "}"; + send_msg(s, -3); + } + } else { + if (d_debug >= 10) { + fprintf (stderr, " (ESS computation suppressed)"); } - s = "{\"nac\" : " + std::to_string(framer->nac) + ", \"algid\" : " + std::to_string(ess_algid) + ", \"alg\" : \"" + lookup(ess_algid, ALGIDS, ALGIDS_SZ) + "\", \"keyid\": " + std::to_string(ess_keyid) + "}"; - send_msg(s, -3); } if (d_debug >= 10) { + fprintf(stderr, ", Hamming failures=%d of 24, ec=%d", hmg_failures, ec); fprintf (stderr, "\n"); } - process_voice(A); + if (hmg_failures < 16) + process_voice(A); } void diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.h b/op25/gr-op25_repeater/lib/p25p1_fdma.h index 0778f7a..e7053ff 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.h +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.h @@ -47,7 +47,7 @@ namespace gr { void process_duid(uint32_t const duid, uint32_t const nac, const uint8_t* buf, const int len); void process_HDU(const bit_vector& A); void process_LCW(std::vector& HB); - void process_LLDU(const bit_vector& A, std::vector& HB); + int process_LLDU(const bit_vector& A, std::vector& HB); void process_LDU1(const bit_vector& A); void process_LDU2(const bit_vector& A); void process_TTDU(); diff --git a/op25/gr-op25_repeater/lib/p25p2_vf.cc b/op25/gr-op25_repeater/lib/p25p2_vf.cc index e593ea7..592befe 100644 --- a/op25/gr-op25_repeater/lib/p25p2_vf.cc +++ b/op25/gr-op25_repeater/lib/p25p2_vf.cc @@ -20,6 +20,7 @@ #include #include #include +#include "op25_golay.h" #include "p25p2_vf.h" #include "rs.h" diff --git a/op25/gr-op25_repeater/lib/rs.cc b/op25/gr-op25_repeater/lib/rs.cc index d1fabe3..272776e 100644 --- a/op25/gr-op25_repeater/lib/rs.cc +++ b/op25/gr-op25_repeater/lib/rs.cc @@ -4,7 +4,6 @@ #include #include #include -#include #ifdef DEBUG /* @@ -157,6 +156,526 @@ static const uint32_t gly23127DecTbl[2048] = { 4718595, 16387, 16387, 16386, 1048579, 2138115, 65539, 16387, 2099203, 69635, 1343491, 16387, 131075, 262147, 4206595, 526339, 1048579, 69635, 141315, 16387, 1048578, 1048579, 1048579, 4456451, 69635, 69634, 524291, 69635, 1048579, 69635, 2113539, 163843 }; +static const uint32_t gly24128EncTbl[4096] = { + 0, 6379, 10558, 12757, 19095, 21116, 25513, 31554, + 36294, 38189, 42232, 48147, 51025, 57274, 61039, 63108, + 66407, 72588, 76377, 78514, 84464, 86299, 90318, 96293, + 102049, 104010, 108447, 114548, 115766, 122077, 126216, 128483, + 132813, 138790, 143347, 145176, 150618, 152753, 157028, 163215, + 166667, 168928, 172597, 178910, 180636, 186743, 190626, 192585, + 198058, 204097, 208020, 210047, 216893, 219094, 222723, 229096, + 231532, 233607, 237906, 244153, 246523, 252432, 256965, 258862, + 265625, 267634, 271527, 277580, 280334, 286693, 290352, 292571, + 295007, 301236, 305505, 307594, 314056, 315939, 320502, 326429, + 331518, 333333, 337856, 343851, 345193, 351362, 355671, 357820, + 361272, 367571, 371206, 373485, 379311, 381252, 385169, 391290, + 396116, 398271, 402026, 408193, 410051, 416040, 420093, 421910, + 427666, 433785, 438188, 440135, 445445, 447726, 451899, 458192, + 460851, 463064, 467213, 473574, 475812, 481871, 486298, 488305, + 493045, 498974, 502987, 504864, 511842, 513929, 517724, 523959, + 525274, 531249, 535268, 537103, 543053, 545190, 548979, 555160, + 560668, 562935, 567074, 573385, 574603, 580704, 585141, 587102, + 590013, 596054, 600451, 602472, 608810, 611009, 615188, 621567, + 626043, 628112, 631877, 638126, 641004, 646919, 650962, 652857, + 656663, 663036, 666665, 668866, 675712, 677739, 681662, 687701, + 690385, 692282, 696815, 702724, 705094, 711341, 715640, 717715, + 722544, 728731, 733006, 735141, 740583, 742412, 746969, 752946, + 756662, 758621, 762504, 768611, 770337, 776650, 780319, 782580, + 790083, 792232, 796541, 802710, 804052, 810047, 814570, 816385, + 820101, 826222, 830139, 832080, 837906, 840185, 843820, 850119, + 855332, 857551, 861210, 867569, 870323, 876376, 880269, 882278, + 884962, 890889, 895452, 897335, 903797, 905886, 910155, 916384, + 919694, 921701, 926128, 932187, 934425, 940786, 944935, 947148, + 951624, 957859, 961654, 963741, 970719, 972596, 976609, 982538, + 986089, 987906, 991959, 997948, 999806, 1005973, 1009728, 1011883, + 1017391, 1023684, 1027857, 1030138, 1035448, 1037395, 1041798, 1047917, + 1050548, 1056607, 1060490, 1062497, 1068323, 1070536, 1074205, 1080566, + 1084018, 1086105, 1090380, 1096615, 1097957, 1103886, 1108443, 1110320, + 1115347, 1121336, 1125869, 1127686, 1134148, 1136303, 1140602, 1146769, + 1149205, 1151486, 1155115, 1161408, 1164162, 1170281, 1174204, 1176151, + 1180025, 1186194, 1189959, 1192108, 1199086, 1200901, 1204944, 1210939, + 1215679, 1217620, 1222017, 1228138, 1230376, 1236675, 1240854, 1243133, + 1245726, 1252085, 1256224, 1258443, 1263753, 1265762, 1270199, 1276252, + 1282008, 1283891, 1287910, 1293837, 1295695, 1301924, 1305713, 1307802, + 1313325, 1315526, 1319699, 1326072, 1327290, 1333329, 1337732, 1339759, + 1345515, 1351424, 1355477, 1357374, 1363324, 1365399, 1369154, 1375401, + 1378634, 1380769, 1384564, 1390751, 1393629, 1399606, 1403619, 1405448, + 1410188, 1416295, 1420722, 1422681, 1429019, 1431280, 1435429, 1441742, + 1445088, 1446923, 1451486, 1457461, 1459831, 1466012, 1470281, 1472418, + 1474854, 1481165, 1484824, 1487091, 1493937, 1495898, 1499791, 1505892, + 1511303, 1513324, 1517241, 1523282, 1525008, 1531387, 1535022, 1537221, + 1540673, 1546922, 1551231, 1553300, 1558742, 1560637, 1565160, 1571075, + 1573998, 1580165, 1584464, 1586619, 1593081, 1594898, 1599431, 1605420, + 1608104, 1610051, 1613974, 1620093, 1622847, 1629140, 1632769, 1635050, + 1640201, 1646562, 1650231, 1652444, 1658270, 1660277, 1664160, 1670219, + 1673935, 1675812, 1680369, 1686298, 1687640, 1693875, 1698150, 1700237, + 1704611, 1710664, 1715101, 1717110, 1722420, 1724639, 1728778, 1735137, + 1740645, 1742734, 1746523, 1752752, 1754610, 1760537, 1764556, 1766439, + 1769924, 1775919, 1779962, 1781777, 1788755, 1790904, 1794669, 1800838, + 1805314, 1807593, 1811772, 1818071, 1820309, 1826430, 1830827, 1832768, + 1837559, 1839388, 1843401, 1849378, 1852256, 1858443, 1862238, 1864373, + 1868849, 1875162, 1879311, 1881572, 1887910, 1889869, 1894296, 1900403, + 1903248, 1905275, 1909678, 1915717, 1916935, 1923308, 1927481, 1929682, + 1935190, 1941437, 1945192, 1947267, 1953217, 1955114, 1959167, 1965076, + 1969978, 1972177, 1975812, 1982191, 1983917, 1989958, 1993875, 1995896, + 1999612, 2005527, 2010050, 2011945, 2017387, 2019456, 2023765, 2030014, + 2034781, 2036918, 2041187, 2047368, 2049738, 2055713, 2060276, 2062111, + 2064795, 2070896, 2074789, 2076750, 2083596, 2085863, 2089522, 2095833, + 2101096, 2103171, 2106966, 2113213, 2115071, 2120980, 2124993, 2126890, + 2130606, 2136645, 2141072, 2143099, 2148409, 2150610, 2154759, 2161132, + 2165775, 2168036, 2172209, 2178522, 2180760, 2186867, 2191270, 2193229, + 2195913, 2201890, 2205943, 2207772, 2214750, 2216885, 2220640, 2226827, + 2230693, 2232654, 2236571, 2242672, 2245426, 2251737, 2255372, 2257639, + 2262115, 2268296, 2272605, 2274742, 2281204, 2283039, 2287562, 2293537, + 2296514, 2298409, 2302972, 2308887, 2310229, 2316478, 2320747, 2322816, + 2328324, 2334703, 2338362, 2340561, 2346387, 2348408, 2352301, 2358342, + 2360049, 2365978, 2370511, 2372388, 2377830, 2379917, 2384216, 2390451, + 2395959, 2398172, 2401801, 2408162, 2409888, 2415947, 2419870, 2421877, + 2425238, 2431357, 2435240, 2437187, 2444033, 2446314, 2449983, 2456276, + 2460752, 2462907, 2467182, 2473349, 2475719, 2481708, 2486265, 2488082, + 2491452, 2497751, 2501890, 2504169, 2510507, 2512448, 2516885, 2523006, + 2525690, 2527505, 2531524, 2537519, 2540397, 2546566, 2550355, 2552504, + 2557787, 2564016, 2567781, 2569870, 2575820, 2577703, 2581746, 2587673, + 2591389, 2593398, 2597795, 2603848, 2605066, 2611425, 2615604, 2617823, + 2624690, 2626649, 2631052, 2637159, 2639397, 2645710, 2649883, 2652144, + 2654580, 2660767, 2664522, 2666657, 2673635, 2675464, 2679517, 2685494, + 2691029, 2692926, 2696939, 2702848, 2704706, 2710953, 2714748, 2716823, + 2720275, 2726648, 2730797, 2732998, 2738308, 2740335, 2744762, 2750801, + 2755199, 2757268, 2761537, 2767786, 2769128, 2775043, 2779606, 2781501, + 2787257, 2793298, 2797191, 2799212, 2805038, 2807237, 2810896, 2817275, + 2820376, 2822643, 2826278, 2832589, 2835343, 2841444, 2845361, 2847322, + 2852062, 2858037, 2862560, 2864395, 2870857, 2872994, 2877303, 2883484, + 2883883, 2890176, 2893845, 2896126, 2902972, 2904919, 2908802, 2914921, + 2919661, 2921478, 2926035, 2932024, 2934394, 2940561, 2944836, 2946991, + 2949708, 2955943, 2960242, 2962329, 2967771, 2969648, 2974181, 2980110, + 2985866, 2987873, 2991796, 2997855, 2999581, 3005942, 3009571, 3011784, + 3016678, 3022605, 3026648, 3028531, 3034481, 3036570, 3040335, 3046564, + 3050016, 3052235, 3056414, 3062773, 3063991, 3070044, 3074441, 3076450, + 3081345, 3087466, 3091903, 3093844, 3100182, 3102461, 3106600, 3112899, + 3115335, 3117484, 3121273, 3127442, 3130320, 3136315, 3140334, 3142149, + 3147996, 3149879, 3154402, 3160329, 3162699, 3168928, 3173237, 3175326, + 3179802, 3186161, 3189796, 3192015, 3198861, 3200870, 3204787, 3210840, + 3214267, 3216208, 3220101, 3226222, 3227948, 3234247, 3237906, 3240185, + 3245693, 3251862, 3256131, 3258280, 3263722, 3265537, 3270100, 3276095, + 3280401, 3282682, 3286831, 3293124, 3294342, 3300461, 3304888, 3306835, + 3310551, 3316540, 3320553, 3322370, 3328320, 3330475, 3334270, 3340437, + 3345782, 3347869, 3351624, 3357859, 3360737, 3366666, 3370719, 3372596, + 3375280, 3381339, 3385742, 3387749, 3394087, 3396300, 3400473, 3406834, + 3409221, 3415470, 3419259, 3421328, 3428306, 3430201, 3434220, 3440135, + 3442819, 3444840, 3449277, 3455318, 3457556, 3463935, 3468074, 3470273, + 3474978, 3481289, 3485468, 3487735, 3493045, 3495006, 3499403, 3505504, + 3509220, 3511055, 3515098, 3521073, 3522931, 3529112, 3532877, 3535014, + 3539848, 3545955, 3549878, 3551837, 3557663, 3559924, 3563553, 3569866, + 3575374, 3577509, 3581808, 3587995, 3589337, 3595314, 3599847, 3601676, + 3604719, 3610628, 3615185, 3617082, 3623544, 3625619, 3629894, 3636141, + 3640617, 3642818, 3646487, 3652860, 3655614, 3661653, 3665536, 3667563, + 3672838, 3675117, 3678776, 3685075, 3686801, 3692922, 3696815, 3698756, + 3704512, 3710507, 3715070, 3716885, 3722327, 3724476, 3728745, 3734914, + 3737697, 3739786, 3744095, 3750324, 3752694, 3758621, 3763144, 3765027, + 3769767, 3775820, 3779737, 3781746, 3788592, 3790811, 3794446, 3800805, + 3804619, 3806496, 3810549, 3816478, 3819356, 3825591, 3829346, 3831433, + 3833869, 3840230, 3844403, 3846616, 3852954, 3854961, 3859364, 3865423, + 3870380, 3872327, 3876754, 3882873, 3884091, 3890384, 3894533, 3896814, + 3900266, 3906433, 3910228, 3912383, 3918333, 3920150, 3924163, 3930152, + 3933855, 3939956, 3944353, 3946314, 3951624, 3953891, 3958070, 3964381, + 3967833, 3969970, 3973735, 3979916, 3981774, 3987749, 3991792, 3993627, + 3999224, 4005139, 4009158, 4011053, 4018031, 4020100, 4023889, 4030138, + 4032574, 4034773, 4038912, 4045291, 4047529, 4053570, 4058007, 4060028, + 4063314, 4069561, 4073836, 4075911, 4082373, 4084270, 4088827, 4094736, + 4099476, 4101503, 4105386, 4111425, 4114179, 4120552, 4124221, 4126422, + 4129589, 4135902, 4139531, 4141792, 4147618, 4149577, 4153500, 4159607, + 4165363, 4167192, 4171725, 4177702, 4179044, 4185231, 4189530, 4191665, + 4195899, 4202192, 4206341, 4208622, 4213932, 4215879, 4220306, 4226425, + 4230141, 4231958, 4235971, 4241960, 4243818, 4249985, 4253780, 4255935, + 4261212, 4267447, 4271202, 4273289, 4280267, 4282144, 4286197, 4292126, + 4294810, 4296817, 4301220, 4307279, 4309517, 4315878, 4320051, 4322264, + 4325622, 4331549, 4336072, 4337955, 4344417, 4346506, 4350815, 4357044, + 4361520, 4363739, 4367374, 4373733, 4376487, 4382540, 4386457, 4388466, + 4391825, 4397946, 4401839, 4403780, 4409606, 4411885, 4415544, 4421843, + 4427351, 4429500, 4433769, 4439938, 4441280, 4447275, 4451838, 4453653, + 4459426, 4461385, 4465308, 4471415, 4473141, 4479454, 4483083, 4485344, + 4490852, 4497039, 4501338, 4503473, 4508915, 4510744, 4515277, 4521254, + 4524229, 4526126, 4530683, 4536592, 4538962, 4545209, 4549484, 4551559, + 4556035, 4562408, 4566077, 4568278, 4575124, 4577151, 4581034, 4587073, + 4590959, 4593028, 4596817, 4603066, 4605944, 4611859, 4615878, 4617773, + 4620457, 4626498, 4630935, 4632956, 4639294, 4641493, 4645632, 4652011, + 4656648, 4658915, 4663094, 4669405, 4670623, 4676724, 4681121, 4683082, + 4686798, 4692773, 4696816, 4698651, 4704601, 4706738, 4710503, 4716684, + 4720097, 4726026, 4730079, 4731956, 4738934, 4741021, 4744776, 4751011, + 4753447, 4755660, 4759833, 4766194, 4768432, 4774491, 4778894, 4780901, + 4785798, 4791917, 4796344, 4798291, 4803601, 4805882, 4810031, 4816324, + 4819776, 4821931, 4825726, 4831893, 4833751, 4839740, 4843753, 4845570, + 4850476, 4856775, 4860434, 4862713, 4868539, 4870480, 4874373, 4880494, + 4886250, 4888065, 4892628, 4898623, 4899965, 4906134, 4910403, 4912552, + 4915275, 4921504, 4925813, 4927902, 4934364, 4936247, 4940770, 4946697, + 4951437, 4953446, 4957363, 4963416, 4966170, 4972529, 4976164, 4978383, + 4982904, 4984979, 4989254, 4995501, 4997871, 5003780, 5008337, 5010234, + 5014974, 5021013, 5024896, 5026923, 5033769, 5035970, 5039639, 5046012, + 5049119, 5051380, 5055009, 5061322, 5063048, 5069155, 5073078, 5075037, + 5080793, 5086770, 5091303, 5093132, 5098574, 5100709, 5105008, 5111195, + 5115573, 5117534, 5121931, 5128032, 5129250, 5135561, 5139740, 5142007, + 5145459, 5151640, 5155405, 5157542, 5163492, 5165327, 5169370, 5175345, + 5180882, 5182777, 5186796, 5192711, 5195589, 5201838, 5205627, 5207696, + 5210132, 5216511, 5220650, 5222849, 5229187, 5231208, 5235645, 5241686, + 5243279, 5249380, 5253297, 5255258, 5262104, 5264371, 5268006, 5274317, + 5278793, 5280930, 5285239, 5291420, 5293790, 5299765, 5304288, 5306123, + 5309160, 5315075, 5319638, 5321533, 5326975, 5329044, 5333313, 5339562, + 5345070, 5347269, 5350928, 5357307, 5359033, 5365074, 5368967, 5370988, + 5375810, 5382057, 5385852, 5387927, 5393877, 5395774, 5399787, 5405696, + 5409412, 5411439, 5415866, 5421905, 5423123, 5429496, 5433645, 5435846, + 5440549, 5446862, 5451035, 5453296, 5459634, 5461593, 5465996, 5472103, + 5474787, 5476616, 5480669, 5486646, 5489524, 5495711, 5499466, 5501601, + 5508118, 5510397, 5514536, 5520835, 5523073, 5529194, 5533631, 5535572, + 5538256, 5544251, 5548270, 5550085, 5557063, 5559212, 5563001, 5569170, + 5574513, 5576602, 5580367, 5586596, 5588454, 5594381, 5598424, 5600307, + 5604023, 5610076, 5614473, 5616482, 5621792, 5624011, 5628190, 5634549, + 5638875, 5640752, 5645285, 5651214, 5652556, 5658791, 5663090, 5665177, + 5670685, 5677046, 5680675, 5682888, 5688714, 5690721, 5694644, 5700703, + 5704124, 5706071, 5709954, 5716073, 5718827, 5725120, 5728789, 5731070, + 5735546, 5741713, 5745988, 5748143, 5754605, 5756422, 5760979, 5766968, + 5767765, 5774014, 5778283, 5780352, 5785794, 5787689, 5792252, 5798167, + 5803923, 5805944, 5809837, 5815878, 5817604, 5823983, 5827642, 5829841, + 5833010, 5839321, 5842956, 5845223, 5852069, 5854030, 5857947, 5864048, + 5868788, 5870623, 5875146, 5881121, 5883491, 5889672, 5893981, 5896118, + 5899416, 5905523, 5909926, 5911885, 5918223, 5920484, 5924657, 5930970, + 5933406, 5935541, 5939296, 5945483, 5948361, 5954338, 5958391, 5960220, + 5965823, 5971732, 5975745, 5977642, 5983592, 5985667, 5989462, 5995709, + 5999161, 6001362, 6005511, 6011884, 6013102, 6019141, 6023568, 6025595, + 6033356, 6035239, 6039282, 6045209, 6047067, 6053296, 6057061, 6059150, + 6062602, 6068961, 6073140, 6075359, 6080669, 6082678, 6087075, 6093128, + 6098091, 6100032, 6104469, 6110590, 6112828, 6119127, 6123266, 6125545, + 6127981, 6134150, 6137939, 6140088, 6147066, 6148881, 6152900, 6158895, + 6162689, 6164970, 6168639, 6174932, 6177686, 6183805, 6187688, 6189635, + 6194375, 6200364, 6204921, 6206738, 6213200, 6215355, 6219630, 6225797, + 6228582, 6230669, 6234968, 6241203, 6242545, 6248474, 6253007, 6254884, + 6260640, 6266699, 6270622, 6272629, 6278455, 6280668, 6284297, 6290658, + 6293843, 6295992, 6299757, 6305926, 6308804, 6314799, 6318842, 6320657, + 6325397, 6331518, 6335915, 6337856, 6344194, 6346473, 6350652, 6356951, + 6359604, 6361823, 6365962, 6372321, 6373539, 6379592, 6384029, 6386038, + 6391794, 6397721, 6401740, 6403623, 6409573, 6411662, 6415451, 6421680, + 6426526, 6428533, 6432416, 6438475, 6440201, 6446562, 6450231, 6452444, + 6455896, 6462131, 6466406, 6468493, 6473935, 6475812, 6480369, 6486298, + 6491385, 6493202, 6497735, 6503724, 6506094, 6512261, 6516560, 6518715, + 6521151, 6527444, 6531073, 6533354, 6540200, 6542147, 6546070, 6552189, + 6554826, 6560801, 6565364, 6567199, 6573661, 6575798, 6580067, 6586248, + 6588684, 6590951, 6594610, 6600921, 6603675, 6609776, 6613669, 6615630, + 6621101, 6627142, 6631059, 6633080, 6638906, 6641105, 6644740, 6651119, + 6654571, 6656640, 6660949, 6667198, 6668540, 6674455, 6678978, 6680873, + 6685191, 6691564, 6695737, 6697938, 6703248, 6705275, 6709678, 6715717, + 6721473, 6723370, 6727423, 6733332, 6735190, 6741437, 6745192, 6747267, + 6750560, 6756747, 6760542, 6762677, 6769655, 6771484, 6775497, 6781474, + 6786214, 6788173, 6792600, 6798707, 6800945, 6807258, 6811407, 6813668, + 6818441, 6820450, 6824887, 6830940, 6832158, 6838517, 6842656, 6844875, + 6850383, 6856612, 6860401, 6862490, 6868440, 6870323, 6874342, 6880269, + 6883822, 6885637, 6889680, 6895675, 6898553, 6904722, 6908487, 6910636, + 6915112, 6921411, 6925590, 6927869, 6934207, 6936148, 6940545, 6946666, + 6949956, 6952111, 6956410, 6962577, 6964947, 6970936, 6975469, 6977286, + 6979970, 6986089, 6990012, 6991959, 6998805, 7001086, 7004715, 7011008, + 7016227, 7018440, 7022109, 7028470, 7030196, 7036255, 7040138, 7042145, + 7045861, 7051790, 7056347, 7058224, 7063666, 7065753, 7070028, 7076263, + 7079696, 7086075, 7089710, 7091909, 7097735, 7099756, 7103673, 7109714, + 7113430, 7115325, 7119848, 7125763, 7127105, 7133354, 7137663, 7139732, + 7144567, 7150748, 7155017, 7157154, 7163616, 7165451, 7170014, 7175989, + 7178673, 7180634, 7184527, 7190628, 7193382, 7199693, 7203352, 7205619, + 7209437, 7215414, 7219427, 7221256, 7228234, 7230369, 7234164, 7240351, + 7244827, 7247088, 7251237, 7257550, 7259788, 7265895, 7270322, 7272281, + 7275194, 7281233, 7285636, 7287663, 7292973, 7295174, 7299347, 7305720, + 7311228, 7313303, 7317058, 7323305, 7325163, 7331072, 7335125, 7337022, + 7343847, 7345676, 7350233, 7356210, 7357552, 7363739, 7368014, 7370149, + 7373601, 7379914, 7383583, 7385844, 7391670, 7393629, 7397512, 7403619, + 7409024, 7411051, 7414974, 7421013, 7423767, 7430140, 7433769, 7435970, + 7438406, 7444653, 7448952, 7451027, 7457489, 7459386, 7463919, 7469828, + 7473194, 7475393, 7479572, 7485951, 7488189, 7494230, 7498627, 7500648, + 7505388, 7511303, 7515346, 7517241, 7524219, 7526288, 7530053, 7536302, + 7539533, 7541670, 7545459, 7551640, 7553498, 7559473, 7563492, 7565327, + 7571083, 7577184, 7581621, 7583582, 7588892, 7591159, 7595298, 7601609, + 7603070, 7609237, 7612992, 7615147, 7621097, 7622914, 7626967, 7632956, + 7638712, 7640659, 7645062, 7651181, 7652399, 7658692, 7662865, 7665146, + 7667737, 7674098, 7678247, 7680460, 7686798, 7688805, 7693232, 7699291, + 7704031, 7705908, 7709921, 7715850, 7718728, 7724963, 7728758, 7730845, + 7734707, 7740760, 7744653, 7746662, 7753508, 7755727, 7759386, 7765745, + 7768181, 7770270, 7774539, 7780768, 7783138, 7789065, 7793628, 7795511, + 7800532, 7806527, 7811050, 7812865, 7818307, 7820456, 7824765, 7830934, + 7834386, 7836665, 7840300, 7846599, 7848325, 7854446, 7858363, 7860304, + 7867709, 7869910, 7873539, 7879912, 7882666, 7888705, 7892628, 7894655, + 7897339, 7903248, 7907781, 7909678, 7916140, 7918215, 7922514, 7928761, + 7933530, 7935665, 7939940, 7946127, 7947469, 7953446, 7958003, 7959832, + 7963548, 7969655, 7973538, 7975497, 7981323, 7983584, 7987253, 7993566, + 7998448, 8000283, 8004302, 8010277, 8012135, 8018316, 8022105, 8024242, + 8029750, 8036061, 8040200, 8042467, 8047777, 8049738, 8054175, 8060276, + 8063127, 8065148, 8069545, 8075586, 8077824, 8084203, 8088382, 8090581, + 8095057, 8101306, 8105071, 8107140, 8114118, 8116013, 8120056, 8125971, + 8126628, 8132687, 8137114, 8139121, 8145459, 8147672, 8151821, 8158182, + 8162658, 8164745, 8168540, 8174775, 8177653, 8183582, 8187595, 8189472, + 8192963, 8198952, 8203005, 8204822, 8210772, 8212927, 8216682, 8222849, + 8228357, 8230638, 8234811, 8241104, 8242322, 8248441, 8252844, 8254791, + 8259177, 8265346, 8269655, 8271804, 8277246, 8279061, 8283584, 8289579, + 8293295, 8295236, 8299153, 8305274, 8307000, 8313299, 8316934, 8319213, + 8324366, 8330725, 8334384, 8336603, 8343449, 8345458, 8349351, 8355404, + 8358088, 8359971, 8364534, 8370461, 8372831, 8379060, 8383329, 8385418, + 8391797, 8393886, 8398155, 8404384, 8406754, 8412681, 8417244, 8419127, + 8421811, 8427864, 8431757, 8433766, 8440612, 8442831, 8446490, 8452849, + 8458002, 8460281, 8463916, 8470215, 8471941, 8478062, 8481979, 8483920, + 8487636, 8493631, 8498154, 8499969, 8505411, 8507560, 8511869, 8518038, + 8522424, 8524371, 8528774, 8534893, 8536111, 8542404, 8546577, 8548858, + 8554366, 8560533, 8564288, 8566443, 8572393, 8574210, 8578263, 8584252, + 8587743, 8589620, 8593633, 8599562, 8602440, 8608675, 8612470, 8614557, + 8619033, 8625394, 8629543, 8631756, 8638094, 8640101, 8644528, 8650587, + 8651244, 8657159, 8661202, 8663097, 8670075, 8672144, 8675909, 8682158, + 8686634, 8688833, 8693012, 8699391, 8701629, 8707670, 8712067, 8714088, + 8716939, 8723040, 8727477, 8729438, 8734748, 8737015, 8741154, 8747465, + 8752973, 8755110, 8758899, 8765080, 8766938, 8772913, 8776932, 8778767, + 8783649, 8789962, 8793631, 8795892, 8801718, 8803677, 8807560, 8813667, + 8817383, 8819212, 8823769, 8829746, 8831088, 8837275, 8841550, 8843685, + 8848454, 8854701, 8859000, 8861075, 8867537, 8869434, 8873967, 8879876, + 8882560, 8884587, 8888510, 8894549, 8897303, 8903676, 8907305, 8909506, + 8916911, 8918852, 8922769, 8928890, 8930616, 8936915, 8940550, 8942829, + 8946281, 8952450, 8956759, 8958908, 8964350, 8966165, 8970688, 8976683, + 8981704, 8983587, 8988150, 8994077, 8996447, 9002676, 9006945, 9009034, + 9011470, 9017829, 9021488, 9023707, 9030553, 9032562, 9036455, 9042508, + 9046370, 9048457, 9052252, 9058487, 9061365, 9067294, 9071307, 9073184, + 9077924, 9083983, 9088410, 9090417, 9096755, 9098968, 9103117, 9109478, + 9112069, 9114350, 9118523, 9124816, 9126034, 9132153, 9136556, 9138503, + 9144259, 9150248, 9154301, 9156118, 9162068, 9164223, 9167978, 9174145, + 9175606, 9181917, 9186056, 9188323, 9193633, 9195594, 9200031, 9206132, + 9211888, 9213723, 9217742, 9223717, 9225575, 9231756, 9235545, 9237682, + 9240913, 9247162, 9250927, 9252996, 9259974, 9261869, 9265912, 9271827, + 9276567, 9278588, 9282985, 9289026, 9291264, 9297643, 9301822, 9304021, + 9307387, 9313296, 9317829, 9319726, 9326188, 9328263, 9332562, 9338809, + 9341245, 9343446, 9347075, 9353448, 9356202, 9362241, 9366164, 9368191, + 9373596, 9379703, 9383586, 9385545, 9391371, 9393632, 9397301, 9403614, + 9407066, 9409201, 9413476, 9419663, 9421005, 9426982, 9431539, 9433368, + 9440193, 9442090, 9446143, 9452052, 9453910, 9460157, 9463912, 9465987, + 9471495, 9477868, 9482041, 9484242, 9489552, 9491579, 9495982, 9502021, + 9504934, 9506893, 9511320, 9517427, 9519665, 9525978, 9530127, 9532388, + 9536864, 9543051, 9546846, 9548981, 9555959, 9557788, 9561801, 9567778, + 9571596, 9573863, 9577522, 9583833, 9586587, 9592688, 9596581, 9598542, + 9601226, 9607201, 9611764, 9613599, 9620061, 9622198, 9626467, 9632648, + 9637483, 9639552, 9643861, 9650110, 9651452, 9657367, 9661890, 9663785, + 9667501, 9673542, 9677459, 9679480, 9685306, 9687505, 9691140, 9697519, + 9700952, 9707187, 9711462, 9713549, 9718991, 9720868, 9725425, 9731354, + 9735070, 9737077, 9740960, 9747019, 9748745, 9755106, 9758775, 9760988, + 9766207, 9772500, 9776129, 9778410, 9785256, 9787203, 9791126, 9797245, + 9799929, 9801746, 9806279, 9812268, 9814638, 9820805, 9825104, 9827259, + 9830549, 9836670, 9841067, 9843008, 9849346, 9851625, 9855804, 9862103, + 9866579, 9868728, 9872493, 9878662, 9881540, 9887535, 9891578, 9893393, + 9896946, 9902873, 9906892, 9908775, 9914725, 9916814, 9920603, 9926832, + 9932340, 9934559, 9938698, 9945057, 9946275, 9952328, 9956765, 9958774, + 9963547, 9965808, 9969957, 9976270, 9978508, 9984615, 9989042, 9991001, + 9995741, 10001718, 10005731, 10007560, 10014538, 10016673, 10020468, 10026655, + 10029948, 10032023, 10035778, 10042025, 10043883, 10049792, 10053845, 10055742, + 10061498, 10067537, 10071940, 10073967, 10079277, 10081478, 10085651, 10092024, + 10096342, 10098237, 10102760, 10108675, 10110017, 10116266, 10120575, 10122644, + 10126096, 10132475, 10136110, 10138309, 10144135, 10146156, 10150073, 10156114, + 10161585, 10163546, 10167439, 10173540, 10176294, 10182605, 10186264, 10188531, + 10190967, 10197148, 10201417, 10203554, 10210016, 10211851, 10216414, 10222389, + 10225026, 10231145, 10235068, 10237015, 10243861, 10246142, 10249771, 10256064, + 10258500, 10260655, 10264954, 10271121, 10273491, 10279480, 10284013, 10285830, + 10290917, 10296846, 10301403, 10303280, 10308722, 10310809, 10315084, 10321319, + 10324771, 10326984, 10330653, 10337014, 10338740, 10344799, 10348682, 10350689, + 10355535, 10361764, 10365553, 10367642, 10373592, 10375475, 10379494, 10385421, + 10391177, 10393186, 10397623, 10403676, 10404894, 10411253, 10415392, 10417611, + 10420264, 10426563, 10430742, 10433021, 10439359, 10441300, 10445697, 10451818, + 10456558, 10458373, 10462416, 10468411, 10471289, 10477458, 10481223, 10483372, + 10486557, 10492918, 10496547, 10498760, 10504586, 10506593, 10510516, 10516575, + 10522331, 10524208, 10528741, 10534670, 10536012, 10542247, 10546546, 10548633, + 10551418, 10557585, 10561860, 10564015, 10570477, 10572294, 10576851, 10582840, + 10587580, 10589527, 10593410, 10599529, 10602283, 10608576, 10612245, 10614526, + 10618320, 10624315, 10628334, 10630149, 10637127, 10639276, 10643065, 10649234, + 10651670, 10653949, 10658088, 10664387, 10666625, 10672746, 10677183, 10679124, + 10684087, 10690140, 10694537, 10696546, 10701856, 10704075, 10708254, 10714613, + 10718065, 10720154, 10723919, 10730148, 10732006, 10737933, 10741976, 10743859, + 10751620, 10753647, 10758074, 10764113, 10765331, 10771704, 10775853, 10778054, + 10781506, 10787753, 10791548, 10793623, 10799573, 10801470, 10805483, 10811392, + 10816995, 10818824, 10822877, 10828854, 10831732, 10837919, 10841674, 10843809, + 10846245, 10852558, 10856731, 10858992, 10865330, 10867289, 10871692, 10877799, + 10881097, 10883234, 10887543, 10893724, 10896094, 10902069, 10906592, 10908427, + 10913167, 10919268, 10923185, 10925146, 10931992, 10934259, 10937894, 10944205, + 10947374, 10949573, 10953232, 10959611, 10961337, 10967378, 10971271, 10973292, + 10979048, 10984963, 10989526, 10991421, 10996863, 10998932, 11003201, 11009450, + 11010247, 11016236, 11020793, 11022610, 11029072, 11031227, 11035502, 11041669, + 11046145, 11048426, 11052095, 11058388, 11061142, 11067261, 11071144, 11073091, + 11076512, 11082571, 11086494, 11088501, 11094327, 11096540, 11100169, 11106530, + 11112038, 11114125, 11118424, 11124659, 11126001, 11131930, 11136463, 11138340, + 11142666, 11149025, 11153204, 11155423, 11160733, 11162742, 11167139, 11173192, + 11176908, 11178791, 11182834, 11188761, 11190619, 11196848, 11200613, 11202702, + 11208045, 11214214, 11218003, 11220152, 11227130, 11228945, 11232964, 11238959, + 11241643, 11243584, 11248021, 11254142, 11256380, 11262679, 11266818, 11269097, + 11275614, 11277749, 11281504, 11287691, 11290569, 11296546, 11300599, 11302428, + 11305112, 11311219, 11315622, 11317581, 11323919, 11326180, 11330353, 11336666, + 11341369, 11343570, 11347719, 11354092, 11355310, 11361349, 11365776, 11367803, + 11371519, 11377428, 11381441, 11383338, 11389288, 11391363, 11395158, 11401405, + 11406227, 11408248, 11412141, 11418182, 11419908, 11426287, 11429946, 11432145, + 11437653, 11443902, 11448171, 11450240, 11455682, 11457577, 11462140, 11468055, + 11471092, 11472927, 11477450, 11483425, 11485795, 11491976, 11496285, 11498422, + 11502898, 11509209, 11512844, 11515111, 11521957, 11523918, 11527835, 11533936, + 11535529, 11541570, 11546007, 11548028, 11554366, 11556565, 11560704, 11567083, + 11569519, 11571588, 11575377, 11581626, 11584504, 11590419, 11594438, 11596333, + 11601870, 11607845, 11611888, 11613723, 11619673, 11621810, 11625575, 11631756, + 11635208, 11637475, 11641654, 11647965, 11649183, 11655284, 11659681, 11661642, + 11666020, 11672207, 11676506, 11678641, 11684083, 11685912, 11690445, 11696422, + 11702178, 11704137, 11708060, 11714167, 11715893, 11722206, 11725835, 11728096, + 11731203, 11737576, 11741245, 11743446, 11750292, 11752319, 11756202, 11762241, + 11766981, 11768878, 11773435, 11779344, 11781714, 11787961, 11792236, 11794311, + 11798832, 11801051, 11804686, 11811045, 11813799, 11819852, 11823769, 11825778, + 11830518, 11836445, 11840968, 11842851, 11849313, 11851402, 11855711, 11861940, + 11864663, 11866812, 11871081, 11877250, 11878592, 11884587, 11889150, 11890965, + 11896721, 11902842, 11906735, 11908676, 11914502, 11916781, 11920440, 11926739, + 11931645, 11933462, 11937475, 11943464, 11945322, 11951489, 11955284, 11957439, + 11960891, 11967184, 11971333, 11973614, 11978924, 11980871, 11985298, 11991417, + 11996314, 11998321, 12002724, 12008783, 12011021, 12017382, 12021555, 12023768, + 12026204, 12032439, 12036194, 12038281, 12045259, 12047136, 12051189, 12057118, + 12060531, 12066712, 12070477, 12072614, 12078564, 12080399, 12084442, 12090417, + 12094133, 12096094, 12100491, 12106592, 12107810, 12114121, 12118300, 12120567, + 12125204, 12131583, 12135722, 12137921, 12144259, 12146280, 12150717, 12156758, + 12159442, 12161337, 12165356, 12171271, 12174149, 12180398, 12184187, 12186256, + 12190142, 12196181, 12200064, 12202091, 12208937, 12211138, 12214807, 12221180, + 12225656, 12227731, 12232006, 12238253, 12240623, 12246532, 12251089, 12252986, + 12255961, 12261938, 12266471, 12268300, 12273742, 12275877, 12280176, 12286363, + 12291871, 12294132, 12297761, 12304074, 12305800, 12311907, 12315830, 12317789, + 12323562, 12325377, 12329940, 12335935, 12337277, 12343446, 12347715, 12349864, + 12355372, 12361671, 12365330, 12367609, 12373435, 12375376, 12379269, 12385390, + 12388749, 12390758, 12394675, 12400728, 12403482, 12409841, 12413476, 12415695, + 12420171, 12426400, 12430709, 12432798, 12439260, 12441143, 12445666, 12451593, + 12454951, 12457164, 12461337, 12467698, 12469936, 12475995, 12480398, 12482405, + 12485089, 12491018, 12495071, 12496948, 12503926, 12506013, 12509768, 12516003, + 12521280, 12523435, 12527230, 12533397, 12535255, 12541244, 12545257, 12547074, + 12550790, 12556909, 12561336, 12563283, 12568593, 12570874, 12575023, 12581316, + 12585550, 12587685, 12591984, 12598171, 12599513, 12605490, 12610023, 12611852, + 12617608, 12623715, 12627638, 12629597, 12635423, 12637684, 12641313, 12647626, + 12650793, 12652994, 12656663, 12663036, 12665790, 12671829, 12675712, 12677739, + 12682479, 12688388, 12692945, 12694842, 12701304, 12703379, 12707654, 12713901, + 12717187, 12719208, 12723645, 12729686, 12731924, 12738303, 12742442, 12744641, + 12747077, 12753326, 12757115, 12759184, 12766162, 12768057, 12772076, 12777991, + 12783588, 12785423, 12789466, 12795441, 12797299, 12803480, 12807245, 12809382, + 12812834, 12819145, 12823324, 12825591, 12830901, 12832862, 12837259, 12843360, + 12847063, 12853052, 12857065, 12858882, 12864832, 12866987, 12870782, 12876949, + 12880401, 12882682, 12886831, 12893124, 12894342, 12900461, 12904888, 12906835, + 12911792, 12917851, 12922254, 12924261, 12930599, 12932812, 12936985, 12943346, + 12945782, 12947869, 12951624, 12957859, 12960737, 12966666, 12970719, 12972596, + 12976410, 12982769, 12986404, 12988623, 12995469, 12997478, 13001395, 13007448, + 13012188, 13014071, 13018594, 13024521, 13026891, 13033120, 13037429, 13039518, + 13042301, 13048470, 13052739, 13054888, 13060330, 13062145, 13066708, 13072703, + 13078459, 13080400, 13084293, 13090414, 13092140, 13098439, 13102098, 13104377, + 13109652, 13111679, 13115562, 13121601, 13124355, 13130728, 13134397, 13136598, + 13141074, 13147321, 13151596, 13153671, 13160133, 13162030, 13166587, 13172496, + 13175539, 13177368, 13181901, 13187878, 13189220, 13195407, 13199706, 13201841, + 13207349, 13213662, 13217291, 13219552, 13225378, 13227337, 13231260, 13237367, + 13242201, 13244338, 13248103, 13254284, 13256142, 13262117, 13266160, 13267995, + 13271711, 13277812, 13282209, 13284170, 13289480, 13291747, 13295926, 13302237, + 13306942, 13309141, 13313280, 13319659, 13321897, 13327938, 13332375, 13334396, + 13337080, 13342995, 13347014, 13348909, 13355887, 13357956, 13361745, 13367994, + 13370381, 13376742, 13380915, 13383128, 13389466, 13391473, 13395876, 13401935, + 13404619, 13406496, 13410549, 13416478, 13419356, 13425591, 13429346, 13431433, + 13436778, 13442945, 13446740, 13448895, 13454845, 13456662, 13460675, 13466664, + 13470380, 13472327, 13476754, 13482873, 13484091, 13490384, 13494533, 13496814, + 13501120, 13507115, 13511678, 13513493, 13518935, 13521084, 13525353, 13531522, + 13537030, 13539309, 13542968, 13549267, 13550993, 13557114, 13561007, 13562948, + 13566375, 13572428, 13576345, 13578354, 13585200, 13587419, 13591054, 13597413, + 13601889, 13603978, 13608287, 13614516, 13616886, 13622813, 13627336, 13629219, + 13635066, 13636881, 13640900, 13646895, 13649773, 13655942, 13659731, 13661880, + 13664316, 13670615, 13674754, 13677033, 13683371, 13685312, 13689749, 13695870, + 13700765, 13702774, 13707171, 13713224, 13714442, 13720801, 13724980, 13727199, + 13730651, 13736880, 13740645, 13742734, 13748684, 13750567, 13754610, 13760537, + 13765431, 13767644, 13771273, 13777634, 13779360, 13785419, 13789342, 13791349, + 13797105, 13803034, 13807567, 13809444, 13814886, 13816973, 13821272, 13827507, + 13830224, 13832379, 13836654, 13842821, 13845191, 13851180, 13855737, 13857554, + 13862294, 13868413, 13872296, 13874243, 13881089, 13883370, 13887039, 13893332, + 13893731, 13899912, 13904221, 13906358, 13912820, 13914655, 13919178, 13925153, + 13929893, 13931854, 13935771, 13941872, 13944626, 13950937, 13954572, 13956839, + 13959940, 13966319, 13969978, 13972177, 13978003, 13980024, 13983917, 13989958, + 13995714, 13997609, 14002172, 14008087, 14009429, 14015678, 14019947, 14022016, + 14026414, 14032453, 14036880, 14038907, 14044217, 14046418, 14050567, 14056940, + 14060392, 14062467, 14066262, 14072509, 14074367, 14080276, 14084289, 14086186, + 14091721, 14097698, 14101751, 14103580, 14110558, 14112693, 14116448, 14122635, + 14125071, 14127332, 14131505, 14137818, 14140056, 14146163, 14150566, 14152525, + 14159392, 14161611, 14165790, 14172149, 14173367, 14179420, 14183817, 14185826, + 14189542, 14195469, 14199512, 14201395, 14207345, 14209434, 14213199, 14219428, + 14224711, 14226860, 14230649, 14236818, 14239696, 14245691, 14249710, 14251525, + 14254209, 14260330, 14264767, 14266708, 14273046, 14275325, 14279464, 14285763, + 14289133, 14290950, 14295507, 14301496, 14303866, 14310033, 14314308, 14316463, + 14320939, 14327232, 14330901, 14333182, 14340028, 14341975, 14345858, 14351977, + 14355338, 14357345, 14361268, 14367327, 14369053, 14375414, 14379043, 14381256, + 14386764, 14392999, 14397298, 14399385, 14404827, 14406704, 14411237, 14417166, + 14418873, 14424914, 14428807, 14430828, 14436654, 14438853, 14442512, 14448891, + 14454399, 14456468, 14460737, 14466986, 14468328, 14474243, 14478806, 14480701, + 14483678, 14489653, 14494176, 14496011, 14502473, 14504610, 14508919, 14515100, + 14519576, 14521843, 14525478, 14531789, 14534543, 14540644, 14544561, 14546522, + 14550388, 14556575, 14560330, 14562465, 14569443, 14571272, 14575325, 14581302, + 14583986, 14585945, 14590348, 14596455, 14598693, 14605006, 14609179, 14611440, + 14616083, 14622456, 14626605, 14628806, 14634116, 14636143, 14640570, 14646609, + 14650325, 14652222, 14656235, 14662144, 14664002, 14670249, 14674044, 14676119, + 14681382, 14687693, 14691352, 14693619, 14700465, 14702426, 14706319, 14712420, + 14715104, 14716939, 14721502, 14727477, 14729847, 14736028, 14740297, 14742434, + 14747201, 14753450, 14757759, 14759828, 14765270, 14767165, 14771688, 14777603, + 14781319, 14783340, 14787257, 14793298, 14795024, 14801403, 14805038, 14807237, + 14812139, 14818048, 14822101, 14823998, 14829948, 14832023, 14835778, 14842025, + 14847533, 14849734, 14853907, 14860280, 14861498, 14867537, 14871940, 14873967, + 14876812, 14882919, 14887346, 14889305, 14895643, 14897904, 14902053, 14908366, + 14912842, 14914977, 14918772, 14924959, 14927837, 14933814, 14937827, 14939656, + 14944447, 14946388, 14950785, 14956906, 14959144, 14965443, 14969622, 14971901, + 14976377, 14982546, 14986311, 14988460, 14995438, 14997253, 15001296, 15007291, + 15010776, 15012659, 15016678, 15022605, 15024463, 15030692, 15034481, 15036570, + 15042078, 15048437, 15052576, 15054795, 15060105, 15062114, 15066551, 15072604, + 15076978, 15079065, 15083340, 15089575, 15090917, 15096846, 15101403, 15103280, + 15106996, 15113055, 15116938, 15118945, 15124771, 15126984, 15130653, 15137014, + 15142165, 15144446, 15148075, 15154368, 15157122, 15163241, 15167164, 15169111, + 15171795, 15177784, 15182317, 15184134, 15190596, 15192751, 15197050, 15203217, + 15206140, 15212055, 15216578, 15218473, 15223915, 15225984, 15230293, 15236542, + 15239994, 15242193, 15245828, 15252207, 15253933, 15259974, 15263891, 15265912, + 15271323, 15277424, 15281317, 15283278, 15290124, 15292391, 15296050, 15302361, + 15304797, 15306934, 15311203, 15317384, 15319754, 15325729, 15330292, 15332127, + 15335473, 15341786, 15345935, 15348196, 15354534, 15356493, 15360920, 15367027, + 15371767, 15373596, 15377609, 15383586, 15386464, 15392651, 15396446, 15398581, + 15401814, 15408061, 15411816, 15413891, 15419841, 15421738, 15425791, 15431700, + 15437456, 15439483, 15443886, 15449925, 15451143, 15457516, 15461689, 15463890, + 15469413, 15471502, 15475291, 15481520, 15483378, 15489305, 15493324, 15495207, + 15500963, 15507016, 15511453, 15513462, 15518772, 15520991, 15525130, 15531489, + 15534082, 15536361, 15540540, 15546839, 15549077, 15555198, 15559595, 15561536, + 15566276, 15572271, 15576314, 15578129, 15585107, 15587256, 15591021, 15597190, + 15601064, 15603011, 15606934, 15613053, 15615807, 15622100, 15625729, 15628010, + 15630446, 15636613, 15640912, 15643067, 15649529, 15651346, 15655879, 15661868, + 15666895, 15668772, 15673329, 15679258, 15680600, 15686835, 15691110, 15693197, + 15696649, 15703010, 15706679, 15708892, 15714718, 15716725, 15720608, 15726667, + 15729298, 15735417, 15739820, 15741767, 15747077, 15749358, 15753531, 15759824, + 15765332, 15767487, 15771242, 15777409, 15779267, 15785256, 15789309, 15791126, + 15794677, 15800606, 15804619, 15806496, 15813474, 15815561, 15819356, 15825591, + 15830067, 15832280, 15836429, 15842790, 15845028, 15851087, 15855514, 15857521, + 15860831, 15867060, 15871329, 15873418, 15879880, 15881763, 15886326, 15892253, + 15894937, 15896946, 15900839, 15906892, 15909646, 15916005, 15919664, 15921883, + 15927096, 15933395, 15937030, 15939309, 15945135, 15947076, 15950993, 15957114, + 15960830, 15962645, 15967168, 15973163, 15974505, 15980674, 15984983, 15987132, + 15994635, 15996896, 16000565, 16006878, 16008604, 16014711, 16018594, 16020553, + 16024269, 16030246, 16034803, 16036632, 16042074, 16044209, 16048484, 16054671, + 16059500, 16061575, 16065874, 16072121, 16074491, 16080400, 16084933, 16086830, + 16089514, 16095553, 16099476, 16101503, 16108349, 16110550, 16114179, 16120552, + 16124358, 16126253, 16130296, 16136211, 16139089, 16145338, 16149103, 16151172, + 16155648, 16162027, 16166206, 16168405, 16174743, 16176764, 16181161, 16187202, + 16190113, 16192074, 16196511, 16202612, 16203830, 16210141, 16214280, 16216547, + 16222055, 16228236, 16232025, 16234162, 16240112, 16241947, 16245966, 16251941, + 16253256, 16259491, 16263286, 16265373, 16272351, 16274228, 16278241, 16284170, + 16288910, 16290917, 16295344, 16301403, 16303641, 16310002, 16314151, 16316364, + 16319023, 16325316, 16329489, 16331770, 16337080, 16339027, 16343430, 16349549, + 16355305, 16357122, 16361175, 16367164, 16369022, 16375189, 16378944, 16381099, + 16385925, 16392046, 16395963, 16397904, 16403730, 16406009, 16409644, 16415943, + 16419395, 16421544, 16425853, 16432022, 16433364, 16439359, 16443882, 16445697, + 16450786, 16456713, 16461276, 16463159, 16469621, 16471710, 16475979, 16482208, + 16484644, 16486863, 16490522, 16496881, 16499635, 16505688, 16509581, 16511590, + 16518353, 16520250, 16524783, 16530692, 16533062, 16539309, 16543608, 16545683, + 16548119, 16554492, 16558121, 16560322, 16567168, 16569195, 16573118, 16579157, + 16584630, 16586589, 16590472, 16596579, 16598305, 16604618, 16608287, 16610548, + 16614000, 16620187, 16624462, 16626597, 16632039, 16633868, 16638425, 16644402, + 16648732, 16650999, 16655138, 16661449, 16662667, 16668768, 16673205, 16675166, + 16680922, 16686897, 16690916, 16692751, 16698701, 16700838, 16704627, 16710808, + 16714107, 16716176, 16719941, 16726190, 16729068, 16734983, 16739026, 16740921, + 16745661, 16751702, 16756099, 16758120, 16764458, 16766657, 16770836, 16777215 +}; + +uint32_t gly24128Enc(uint32_t n) { + assert (n <= 4095); + return gly24128EncTbl[n]; +} + uint32_t gly23127GetSyn (uint32_t pattern) { uint32_t aux = 0x400000; diff --git a/op25/gr-op25_repeater/lib/rs.h b/op25/gr-op25_repeater/lib/rs.h index 96e3216..d45093f 100644 --- a/op25/gr-op25_repeater/lib/rs.h +++ b/op25/gr-op25_repeater/lib/rs.h @@ -5,8 +5,8 @@ #include #include #include -#include +uint32_t gly24128Enc (uint32_t n) ; uint32_t gly24128Dec (uint32_t n) ; uint32_t gly23127Dec (uint32_t n) ; From f974a32a19e670d80dbca93f81abb944087abf4f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 12 Jan 2019 14:27:58 -0500 Subject: [PATCH 099/102] eliminate warning: large integer implicitly truncated to unsigned type --- op25/gr-op25/lib/desport.c | 3 ++- op25/gr-op25/lib/dessp.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/op25/gr-op25/lib/desport.c b/op25/gr-op25/lib/desport.c index 01bc294..0073be3 100644 --- a/op25/gr-op25/lib/desport.c +++ b/op25/gr-op25/lib/desport.c @@ -1,5 +1,6 @@ /* Portable C version of des() function */ +#include #include "des.h" /* Tables defined in the Data Encryption Standard documents @@ -115,7 +116,7 @@ int Asmversion = 0; * For best results, ensure that this is aligned on a 32-bit boundary; * Borland C++ 3.1 doesn't guarantee this! */ -extern unsigned long Spbox[8][64]; /* Combined S and P boxes */ +extern uint64_t Spbox[8][64]; /* Combined S and P boxes */ /* Primitive function F. * Input is r, subkey array in keys, output is XORed into l. diff --git a/op25/gr-op25/lib/dessp.c b/op25/gr-op25/lib/dessp.c index 61356f1..2470400 100644 --- a/op25/gr-op25/lib/dessp.c +++ b/op25/gr-op25/lib/dessp.c @@ -1,4 +1,5 @@ -unsigned long Spbox[8][64] = { +#include +uint64_t Spbox[8][64] = { 0x01010400,0x00000000,0x00010000,0x01010404, 0x01010004,0x00010404,0x00000004,0x00010000, 0x00000400,0x01010400,0x01010404,0x00000400, From 111b311d88d4c591687eb12160206bd9c8a6f2ea Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Jan 2019 21:08:01 -0500 Subject: [PATCH 100/102] trim mixer plot CPU usage and add plot command logfile --- op25/gr-op25_repeater/apps/gr_gnuplot.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/op25/gr-op25_repeater/apps/gr_gnuplot.py b/op25/gr-op25_repeater/apps/gr_gnuplot.py index eecf65b..c9326e4 100644 --- a/op25/gr-op25_repeater/apps/gr_gnuplot.py +++ b/op25/gr-op25_repeater/apps/gr_gnuplot.py @@ -55,7 +55,7 @@ def limit(a,lim): return a class wrap_gp(object): - def __init__(self, sps=_def_sps): + def __init__(self, sps=_def_sps, logfile=None): self.sps = sps self.center_freq = 0.0 self.relative_freq = 0.0 @@ -72,6 +72,7 @@ class wrap_gp(object): self.sequence = 0 self.output_dir = None self.filename = None + self.logfile = logfile self.attach_gp() @@ -250,6 +251,9 @@ class wrap_gp(object): h+= 'set yrange [-2:2]\n' h+= 'set title "Oscilloscope"\n' dat = '%s%splot %s\n%s' % (h0, h, ','.join(plots), s) + if self.logfile is not None: + with open(self.logfile, 'a') as fd: + fd.write(dat) self.gp.poll() if self.gp.returncode is None: # make sure gnuplot is still running try: @@ -272,6 +276,9 @@ class wrap_gp(object): def set_width(self, w): self.width = w + def set_logfile(self, logfile=None): + self.logfile = logfile + class eye_sink_f(gr.sync_block): """ """ @@ -325,7 +332,7 @@ class fft_sink_c(gr.sync_block): def work(self, input_items, output_items): self.skip += 1 - if self.skip == 50: + if self.skip >= 50: self.skip = 0 in0 = input_items[0] self.gnuplot.plot(in0, FFT_BINS, mode='fft') @@ -357,10 +364,14 @@ class mixer_sink_c(gr.sync_block): out_sig=None) self.debug = debug self.gnuplot = wrap_gp() + self.skip = 0 def work(self, input_items, output_items): - in0 = input_items[0] - self.gnuplot.plot(in0, FFT_BINS, mode='mixer') + self.skip += 1 + if self.skip >= 10: + self.skip = 0 + in0 = input_items[0] + self.gnuplot.plot(in0, FFT_BINS, mode='mixer') return len(input_items[0]) def kill(self): From 538ae6015b60ebb2de4556e8736e668c31f9a347 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 25 Jan 2019 20:40:21 -0500 Subject: [PATCH 101/102] debug: dump raw IMBE frame data --- op25/gr-op25/lib/op25_imbe_frame.h | 21 +++++++++++++++++++++ op25/gr-op25_repeater/lib/p25p1_fdma.cc | 9 +++++++++ 2 files changed, 30 insertions(+) diff --git a/op25/gr-op25/lib/op25_imbe_frame.h b/op25/gr-op25/lib/op25_imbe_frame.h index 7c05ba1..0ee0e7a 100644 --- a/op25/gr-op25/lib/op25_imbe_frame.h +++ b/op25/gr-op25/lib/op25_imbe_frame.h @@ -10,6 +10,7 @@ #include typedef std::vector voice_codeword; +typedef std::vector packed_codeword; typedef const std::vector const_bit_vector; typedef std::vector bit_vector; @@ -344,6 +345,26 @@ imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_ return errs; } +/* + * Pack 88 bit IMBE parameters into uint8_t vector + */ +static inline void +imbe_pack(packed_codeword& cw, uint32_t u0, uint32_t u1, uint32_t u2, uint32_t u3, uint32_t u4, uint32_t u5, uint32_t u6, uint32_t u7) +{ + cw.empty(); + cw.push_back(u0 >> 4); + cw.push_back(((u0 & 0xf) << 4) + (u1 >> 8)); + cw.push_back(u1 & 0xff); + cw.push_back(u2 >> 4); + cw.push_back(((u2 & 0xf) << 4) + (u3 >> 8)); + cw.push_back(u3 & 0xff); + cw.push_back(u4 >> 3); + cw.push_back(((u4 & 0x7) << 5) + (u5 >> 6)); + cw.push_back(((u5 & 0x3f) << 2) + (u6 >> 9)); + cw.push_back(u6 >> 1); + cw.push_back(((u6 & 0x1) << 7) + (u7 >> 1)); +} + /* APCO IMBE header encoder. * * given 88 bits of IMBE parameters, construct output 144-bit IMBE codeword diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.cc b/op25/gr-op25_repeater/lib/p25p1_fdma.cc index e239864..f18a99a 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.cc +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.cc @@ -625,6 +625,15 @@ p25p1_fdma::process_voice(const bit_vector& A) imbe_deinterleave(A, cw, i); // recover 88-bit IMBE voice code word imbe_header_decode(cw, u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7], E0, ET); + + if (d_debug >= 10) { + packed_codeword p_cw; + imbe_pack(p_cw, u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7]); + sprintf(s,"%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", + p_cw[0], p_cw[1], p_cw[2], p_cw[3], p_cw[4], p_cw[5], + p_cw[6], p_cw[7], p_cw[8], p_cw[9], p_cw[10]); + fprintf(stderr, "%s IMBE %s\n", logts.get(), s); // print to log in one operation + } // output one 32-byte msg per 0.020 sec. // also, 32*9 = 288 byte pkts (for use via UDP) sprintf(s, "%03x %03x %03x %03x %03x %03x %03x %03x\n", u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7]); From 140a76ac76b6acd4dc68e5ab6a05ed7f35710017 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 11 May 2019 16:41:49 -0400 Subject: [PATCH 102/102] nxdn48 initial checkin for rx --- op25/gr-op25_repeater/apps/multi_rx.py | 3 +- op25/gr-op25_repeater/apps/p25_demodulator.py | 4 +- .../gr-op25_repeater/apps/tx/op25_c4fm_mod.py | 38 +++- op25/gr-op25_repeater/lib/CMakeLists.txt | 1 + op25/gr-op25_repeater/lib/nxdn.cc | 176 ++++++++++++++++++ op25/gr-op25_repeater/lib/nxdn.h | 28 +++ op25/gr-op25_repeater/lib/nxdn_const.h | 32 ++++ op25/gr-op25_repeater/lib/rx_sync.cc | 14 ++ op25/gr-op25_repeater/lib/rx_sync.h | 11 +- 9 files changed, 297 insertions(+), 10 deletions(-) create mode 100644 op25/gr-op25_repeater/lib/nxdn.cc create mode 100644 op25/gr-op25_repeater/lib/nxdn.h create mode 100644 op25/gr-op25_repeater/lib/nxdn_const.h diff --git a/op25/gr-op25_repeater/apps/multi_rx.py b/op25/gr-op25_repeater/apps/multi_rx.py index 20cc34c..a7dde1b 100755 --- a/op25/gr-op25_repeater/apps/multi_rx.py +++ b/op25/gr-op25_repeater/apps/multi_rx.py @@ -129,7 +129,8 @@ class channel(object): if dev.args.startswith('audio:'): self.demod = p25_demodulator.p25_demod_fb( input_rate = dev.sample_rate, - filter_type = config['filter_type']) + filter_type = config['filter_type'], + symbol_rate = self.symbol_rate) else: self.demod = p25_demodulator.p25_demod_cb( input_rate = dev.sample_rate, diff --git a/op25/gr-op25_repeater/apps/p25_demodulator.py b/op25/gr-op25_repeater/apps/p25_demodulator.py index e53d3eb..334cb63 100644 --- a/op25/gr-op25_repeater/apps/p25_demodulator.py +++ b/op25/gr-op25_repeater/apps/p25_demodulator.py @@ -98,6 +98,8 @@ class p25_demod_base(gr.hier_block2): if ntaps & 1 == 0: ntaps += 1 coeffs = filter.firdes.root_raised_cosine(1.0, if_rate, symbol_rate, excess_bw, ntaps) + if filter_type == 'nxdn': + coeffs = op25_c4fm_mod.c4fm_taps(sample_rate=self.if_rate, span=9, generator=op25_c4fm_mod.transfer_function_nxdn).generate() if filter_type == 'gmsk': # lifted from gmsk.py _omega = sps @@ -393,7 +395,7 @@ class p25_demod_cb(p25_demod_base): elif src == 'diffdec': self.connect(self.diffdec, sink) elif src == 'mixer': - self.connect(self.mixer, sink) + self.connect(self.agc, sink) elif src == 'src': self.connect(self, sink) elif src == 'bpf': diff --git a/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py b/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py index e1f1276..5c99017 100755 --- a/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py +++ b/op25/gr-op25_repeater/apps/tx/op25_c4fm_mod.py @@ -49,14 +49,14 @@ _def_span = 13 #desired number of impulse response coeffs, in units of symbols _def_gmsk_span = 4 _def_bt = 0.25 -def transfer_function_rx(): +def transfer_function_rx(symbol_rate=_def_symbol_rate): # p25 c4fm de-emphasis filter # Specs undefined above 2,880 Hz. It would be nice to have a sharper # rolloff, but this filter is cheap enough.... xfer = [] # frequency domain transfer function - for f in xrange(0,4800): + for f in xrange(0,symbol_rate): # D(f) - t = pi * f / 4800 + t = pi * f / symbol_rate if t < 1e-6: df = 1.0 else: @@ -64,7 +64,7 @@ def transfer_function_rx(): xfer.append(df) return xfer -def transfer_function_tx(): +def transfer_function_tx(symbol_rate=_def_symbol_rate): xfer = [] # frequency domain transfer function for f in xrange(0, 2881): # specs cover 0 - 2,880 Hz # H(f) @@ -82,7 +82,7 @@ def transfer_function_tx(): xfer.append(pf * hf) return xfer -def transfer_function_dmr(): +def transfer_function_dmr(symbol_rate=_def_symbol_rate): xfer = [] # frequency domain transfer function for f in xrange(0, 2881): # specs cover 0 - 2,880 Hz if f < 1920: @@ -94,6 +94,32 @@ def transfer_function_dmr(): xfer = np.sqrt(xfer) # root cosine return xfer +def transfer_function_nxdn(symbol_rate=_def_symbol_rate): + assert symbol_rate == 2400 or symbol_rate == 4800 + T = 1.0 / symbol_rate + a = 0.2 # rolloff + fl = int(0.5+(1-a)/(2*T)) + fh = int(0.5+(1+a)/(2*T)) + + xfer = [] + for f in xrange(0, symbol_rate): + if f < fl: + hf = 1.0 + elif f >= fl and f <= fh: + hf = cos((T/(4*a)) * (2*pi*f - pi*(1-a)/T)) + else: + hf = 0.0 + x = pi * f * T + if f <= fh: + if x == 0 or sin(x) == 0: + df = 1.0 + else: + df = x / sin(x) + else: + df = 2.0 + xfer.append(hf * df) + return xfer + class c4fm_taps(object): """Generate filter coefficients as per P25 C4FM spec""" def __init__(self, filter_gain = 1.0, sample_rate=_def_output_sample_rate, symbol_rate=_def_symbol_rate, span=_def_span, generator=transfer_function_tx): @@ -105,7 +131,7 @@ class c4fm_taps(object): self.generator = generator def generate(self): - impulse_response = np.fft.fftshift(np.fft.irfft(self.generator(), self.sample_rate)) + impulse_response = np.fft.fftshift(np.fft.irfft(self.generator(symbol_rate=self.symbol_rate), self.sample_rate)) start = np.argmax(impulse_response) - (self.ntaps-1) / 2 coeffs = impulse_response[start: start+self.ntaps] gain = self.filter_gain / sum(coeffs) diff --git a/op25/gr-op25_repeater/lib/CMakeLists.txt b/op25/gr-op25_repeater/lib/CMakeLists.txt index 020a689..983b5b5 100644 --- a/op25/gr-op25_repeater/lib/CMakeLists.txt +++ b/op25/gr-op25_repeater/lib/CMakeLists.txt @@ -57,6 +57,7 @@ list(APPEND op25_repeater_sources op25_audio.cc CCITTChecksumReverse.cpp value_string.cc + nxdn.cc ) add_library(gnuradio-op25_repeater SHARED ${op25_repeater_sources}) diff --git a/op25/gr-op25_repeater/lib/nxdn.cc b/op25/gr-op25_repeater/lib/nxdn.cc new file mode 100644 index 0000000..13703ce --- /dev/null +++ b/op25/gr-op25_repeater/lib/nxdn.cc @@ -0,0 +1,176 @@ +/* -*- c++ -*- */ +/* + * NXDN Encoder/Decoder (C) Copyright 2019 Max H. Parke KA1RBI + * + * This file is part of OP25 + * + * 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. + */ + +#include +#include +#include +#include +#include "bit_utils.h" + +#include "nxdn.h" + +static const uint16_t perm_25_12[300] = { // perumtation schedule for 25x12 + 0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288, + 1,13,25,37,49,61,73,85,97,109,121,133,145,157,169,181,193,205,217,229,241,253,265,277,289, + 2,14,26,38,50,62,74,86,98,110,122,134,146,158,170,182,194,206,218,230,242,254,266,278,290, + 3,15,27,39,51,63,75,87,99,111,123,135,147,159,171,183,195,207,219,231,243,255,267,279,291, + 4,16,28,40,52,64,76,88,100,112,124,136,148,160,172,184,196,208,220,232,244,256,268,280,292, + 5,17,29,41,53,65,77,89,101,113,125,137,149,161,173,185,197,209,221,233,245,257,269,281,293, + 6,18,30,42,54,66,78,90,102,114,126,138,150,162,174,186,198,210,222,234,246,258,270,282,294, + 7,19,31,43,55,67,79,91,103,115,127,139,151,163,175,187,199,211,223,235,247,259,271,283,295, + 8,20,32,44,56,68,80,92,104,116,128,140,152,164,176,188,200,212,224,236,248,260,272,284,296, + 9,21,33,45,57,69,81,93,105,117,129,141,153,165,177,189,201,213,225,237,249,261,273,285,297, + 10,22,34,46,58,70,82,94,106,118,130,142,154,166,178,190,202,214,226,238,250,262,274,286,298, + 11,23,35,47,59,71,83,95,107,119,131,143,155,167,179,191,203,215,227,239,251,263,275,287,299}; + +static const uint8_t scramble_t[] = { + 2, 5, 6, 7, 10, 12, 14, 16, 17, 22, 23, 25, 26, 27, 28, 30, 33, 34, 36, 37, 38, 41, 45, 47, + 52, 54, 56, 57, 59, 62, 63, 64, 65, 66, 67, 69, 70, 73, 76, 79, 81, 82, 84, 85, 86, 87, 88, + 89, 92, 95, 96, 98, 100, 103, 104, 107, 108, 116, 117, 121, 122, 125, 127, 131, 132, 134, + 137, 139, 140, 141, 142, 143, 144, 145, 147, 151, 153, 154, 158, 159, 160, 162, 164, 165, + 168, 170, 171, 174, 175, 176, 177, 181}; + +static const int PARITY[] = {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1}; + +static inline uint16_t crc16(const uint8_t buf[], int len, uint32_t crc) { + uint32_t poly = (1<<12) + (1<<5) + (1<<0); + for(int i=0; i>1]; + result[i] = PARITY[reg & 0x19]; + result[i+1] = PARITY[reg & 0x17]; + } +} + +// simplified trellis 2:1 decode; source and result in bits +// assumes that encoding was done with NTEST trailing zero bits +// result_len should be set to the actual number of data bits +// in the original unencoded message (excl. these trailing bits) +static inline void trellis_decode(uint8_t result[], const uint8_t source[], int result_len) +{ + int reg = 0; + int min_d; + int min_bt; + static const int NTEST = 4; + static const int NTESTC = 1 << NTEST; + uint8_t bt[NTEST]; + uint8_t tt[NTEST*2]; + int dstats[4]; + int sum; + for (int p=0; p < 4; p++) + dstats[p] = 0; + for (int p=0; p < result_len; p++) { + for (int i=0; i>3; + bt[1] = (i&4)>>2; + bt[2] = (i&2)>>1; + bt[3] = (i&1); + trellis_encode(tt, bt, NTEST*2, reg); + sum=0; + for (int j=0; j 3) ? 3 : min_d] += 1; + } + // fprintf (stderr, "stats\t%d %d %d %d\n", dstats[0], dstats[1], dstats[2], dstats[3]); +} + +void nxdn_descramble(uint8_t dibits[], int len) { + for (int i=0; i= len) + break; + dibits[scramble_t[i]] ^= 0x2; // invert sign of scrambled dibits + } +} + +static inline void decode_cac(const uint8_t dibits[], int len) { + uint8_t cacbits[300]; + uint8_t deperm[300]; + uint8_t depunc[350]; + uint8_t decode[171]; + int id=0; + uint16_t crc; + + dibits_to_bits(cacbits, dibits, 150); + for (int i=0; i<300; i++) { + deperm[perm_25_12[i]] = cacbits[i]; + } + for (int i=0; i<25; i++) { + depunc[id++] = deperm[i*12]; + depunc[id++] = deperm[i*12+1]; + depunc[id++] = deperm[i*12+2]; + depunc[id++] = 0; + depunc[id++] = deperm[i*12+3]; + depunc[id++] = deperm[i*12+4]; + depunc[id++] = deperm[i*12+5]; + depunc[id++] = deperm[i*12+6]; + depunc[id++] = deperm[i*12+7]; + depunc[id++] = deperm[i*12+8]; + depunc[id++] = deperm[i*12+9]; + depunc[id++] = 0; + depunc[id++] = deperm[i*12+10]; + depunc[id++] = deperm[i*12+11]; + } + trellis_decode(decode, depunc, 171); + crc = crc16(decode, 171, 0xc3ee); + if (crc != 0) + return; // ignore msg if crc failed + uint8_t msg_type = load_i(decode+8, 8) & 0x3f; + // todo: forward CAC message +} + +void nxdn_frame(const uint8_t dibits[], int ndibits) { + uint8_t descrambled[182]; + uint8_t lich; + uint8_t lich_test; + uint8_t bit72[72]; + + assert (ndibits >= 170); + memcpy(descrambled, dibits, ndibits); + nxdn_descramble(descrambled, ndibits); + lich = 0; + for (int i=0; i<8; i++) + lich |= (descrambled[i] >> 1) << (7-i); + /* todo: parity check & process LICH */ + if (lich >> 1 == 0x01) + decode_cac(descrambled+8, 150); + /* todo: process E: 12 dibits at descrambed+158; */ +} diff --git a/op25/gr-op25_repeater/lib/nxdn.h b/op25/gr-op25_repeater/lib/nxdn.h new file mode 100644 index 0000000..e74d954 --- /dev/null +++ b/op25/gr-op25_repeater/lib/nxdn.h @@ -0,0 +1,28 @@ +// +// NXDN Encoder (C) Copyright 2019 Max H. Parke KA1RBI +// thx gr-ysf fr_vch_decoder_bb_impl.cc * Copyright 2015 Mathias Weyland * +// +// This file is part of OP25 +// +// OP25 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. +// +// OP25 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 OP25; see the file COPYING. If not, write to the Free +// Software Foundation, Inc., 51 Franklin Street, Boston, MA +// 02110-1301, USA. + +#ifndef INCLUDED_NXDN_H +#define INCLUDED_NXDN_H + +void nxdn_frame(const uint8_t dibits[], int ndibits); +void nxdn_descramble(uint8_t dibits[], int len); + +#endif /* INCLUDED_NXDN_H */ diff --git a/op25/gr-op25_repeater/lib/nxdn_const.h b/op25/gr-op25_repeater/lib/nxdn_const.h new file mode 100644 index 0000000..fa9cbca --- /dev/null +++ b/op25/gr-op25_repeater/lib/nxdn_const.h @@ -0,0 +1,32 @@ +// +// NXDN Encoder (C) Copyright 2019 Max H. Parke KA1RBI +// thx gr-ysf fr_vch_decoder_bb_impl.cc * Copyright 2015 Mathias Weyland * +// +// This file is part of OP25 +// +// OP25 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. +// +// OP25 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 OP25; see the file COPYING. If not, write to the Free +// Software Foundation, Inc., 51 Franklin Street, Boston, MA +// 02110-1301, USA. + +#ifndef INCLUDED_NXDN_CONST_H +#define INCLUDED_NXDN_CONST_H + +#include + +/* postamble + frame sync (FS) */ +static const uint64_t NXDN_POSTFS_SYNC_MAGIC = 0x5775fdcdf59LL; +/* frame sync + scrambled rendition of LICH=0x6e (a halfrate voice 4V) */ +static const uint64_t NXDN_FS6E_SYNC_MAGIC = 0xcdf5975d7LL; + +#endif /* INCLUDED_NXDN_CONST_H */ diff --git a/op25/gr-op25_repeater/lib/rx_sync.cc b/op25/gr-op25_repeater/lib/rx_sync.cc index 1ca7f8e..02478a0 100644 --- a/op25/gr-op25_repeater/lib/rx_sync.cc +++ b/op25/gr-op25_repeater/lib/rx_sync.cc @@ -207,6 +207,7 @@ void rx_sync::codeword(const uint8_t* cw, const enum codeword_types codeword_typ switch(codeword_type) { case CODEWORD_DMR: + case CODEWORD_NXDN_EHR: // halfrate interleaver.process_vcw(cw, b); if (b[0] < 120) mbe_dequantizeAmbe2250Parms(&cur_mp[slot_id], &prev_mp[slot_id], b); @@ -287,6 +288,7 @@ void rx_sync::rx_sym(const uint8_t sym) bool unmute; uint8_t tmpcw[144]; bool ysf_fullrate; + uint8_t dbuf[182]; d_symbol_count ++; d_sync_reg = (d_sync_reg << 2) | (sym & 3); @@ -368,6 +370,18 @@ void rx_sync::rx_sym(const uint8_t sym) } } break; + case RX_TYPE_NXDN_CAC: + nxdn_frame(symbol_ptr+22, 170); + break; + case RX_TYPE_NXDN_EHR: + memcpy(dbuf, symbol_ptr+10, sizeof(dbuf)); + nxdn_descramble(dbuf, sizeof(dbuf)); + // todo: process SACCH + codeword(dbuf+38+36*0, CODEWORD_NXDN_EHR, 0); + codeword(dbuf+38+36*1, CODEWORD_NXDN_EHR, 0); + codeword(dbuf+38+36*2, CODEWORD_NXDN_EHR, 0); + codeword(dbuf+38+36*3, CODEWORD_NXDN_EHR, 0); + break; case RX_N_TYPES: assert(0==1); /* should not occur */ break; diff --git a/op25/gr-op25_repeater/lib/rx_sync.h b/op25/gr-op25_repeater/lib/rx_sync.h index bbad158..7ab2922 100644 --- a/op25/gr-op25_repeater/lib/rx_sync.h +++ b/op25/gr-op25_repeater/lib/rx_sync.h @@ -42,6 +42,8 @@ #include "op25_imbe_frame.h" #include "software_imbe_decoder.h" #include "op25_audio.h" +#include "nxdn_const.h" +#include "nxdn.h" namespace gr{ namespace op25_repeater{ @@ -54,6 +56,8 @@ enum rx_types { RX_TYPE_DMR, RX_TYPE_DSTAR, RX_TYPE_YSF, + RX_TYPE_NXDN_EHR, + RX_TYPE_NXDN_CAC, RX_N_TYPES }; // also used as array index @@ -69,7 +73,9 @@ static const struct _mode_data { {"P25", 48,0,864,1728, P25_FRAME_SYNC_MAGIC}, {"DMR", 48,66,144,1728, DMR_VOICE_SYNC_MAGIC}, {"DSTAR", 48,72,96,2016*2, DSTAR_FRAME_SYNC_MAGIC}, - {"YSF", 40,0,480,480*2, YSF_FRAME_SYNC_MAGIC} + {"YSF", 40,0,480,480*2, YSF_FRAME_SYNC_MAGIC}, + {"NXDN_EHR", 36,0,192,192*2, NXDN_FS6E_SYNC_MAGIC}, + {"NXDN_CAC", 44,0,192,192*2, NXDN_POSTFS_SYNC_MAGIC} }; // index order must match rx_types enum enum codeword_types { @@ -78,7 +84,8 @@ enum codeword_types { CODEWORD_DMR, CODEWORD_DSTAR, CODEWORD_YSF_FULLRATE, - CODEWORD_YSF_HALFRATE + CODEWORD_YSF_HALFRATE, + CODEWORD_NXDN_EHR }; class rx_sync {