├── python ├── tenna │ ├── bindings │ │ ├── README.md │ │ ├── docstrings │ │ │ └── README.md │ │ ├── CMakeLists.txt │ │ ├── python_bindings.cc │ │ ├── bind_oot_file.py │ │ └── header_utils.py │ ├── .gitignore │ ├── __init__.py │ ├── qa_pdu_to_pcapng.py │ ├── qa_gotenna_decoder.py │ ├── CMakeLists.txt │ ├── pdu_to_pcapng.py │ └── gotenna_decoder.py ├── gotenna_packet │ ├── proto │ │ ├── __init__.py │ │ ├── CMakeLists.txt │ │ ├── base_message_pb2.py │ │ ├── header_pb2.py │ │ ├── frequency_pb2.py │ │ ├── data_type_pb2.py │ │ ├── location_pb2.py │ │ └── message_pb2.py │ ├── __init__.py │ └── CMakeLists.txt └── test_gotenna_packet.py ├── .gitignore ├── requirements.txt ├── examples └── README ├── protobuf └── gotenna_packet │ └── proto │ ├── base_message.proto │ ├── header.proto │ ├── frequency.proto │ ├── location.proto │ ├── data_type.proto │ └── message.proto ├── docs ├── doxygen │ ├── other │ │ ├── group_defs.dox │ │ └── main_page.dox │ ├── doxyxml │ │ ├── generated │ │ │ ├── __init__.py │ │ │ └── index.py │ │ ├── text.py │ │ ├── __init__.py │ │ ├── base.py │ │ └── doxyindex.py │ ├── pydoc_macros.h │ ├── CMakeLists.txt │ └── update_pydoc.py ├── README.tenna └── CMakeLists.txt ├── apps ├── CMakeLists.txt ├── decode_pcap.py ├── keygen.py ├── decrypt_gotenna_qr_key.py ├── decrypt_shared_preferences.py ├── gotenna_rx_usrp.py ├── gotenna_rx_hackrf.py ├── gotenna_rx_hackrf.grc ├── gotenna_tx_usrp.py ├── gotenna_tx_hackrf.py ├── gotenna_tx_hackrf.grc ├── gotenna_pro_tx_usrp.py ├── gotenna_pro_rx_usrp.py └── gotenna_pro_rx_hackrf.py ├── grc ├── CMakeLists.txt ├── tenna_gotenna_decoder.block.yml └── tenna_pdu_to_pcapng.block.yml ├── cmake ├── Modules │ ├── targetConfig.cmake.in │ ├── gnuradio-tennaConfig.cmake │ └── CMakeParseArgumentsCopy.cmake └── cmake_uninstall.cmake.in ├── MANIFEST.md ├── include └── gnuradio │ └── tenna │ ├── CMakeLists.txt │ └── api.h ├── .github └── workflows │ └── ci.yml ├── README.md ├── lib └── CMakeLists.txt └── CMakeLists.txt /python/tenna/bindings/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pcapng 3 | /build/ 4 | -------------------------------------------------------------------------------- /python/gotenna_packet/__init__.py: -------------------------------------------------------------------------------- 1 | from .gotenna_packet import * 2 | -------------------------------------------------------------------------------- /python/tenna/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.pyo 4 | build*/ 5 | examples/grc/*.py 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography >= 3.1 2 | protobuf >= 4.21 3 | reedsolo >= 1.4 4 | python-pcapng >= 2.0.0 5 | -------------------------------------------------------------------------------- /python/tenna/bindings/docstrings/README.md: -------------------------------------------------------------------------------- 1 | This directory stores templates for docstrings that are scraped from the include header files for each block 2 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | It is considered good practice to add examples in here to demonstrate the 2 | functionality of your OOT module. Python scripts, GRC flow graphs or other 3 | code can go here. 4 | -------------------------------------------------------------------------------- /protobuf/gotenna_packet/proto/base_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "gotenna_packet/proto/header.proto"; 4 | 5 | message PBBaseMessage { 6 | PBBaseHeader header = 1; 7 | bytes messageData = 2; 8 | } 9 | -------------------------------------------------------------------------------- /docs/doxygen/other/group_defs.dox: -------------------------------------------------------------------------------- 1 | /*! 2 | * \defgroup block GNU Radio TENNA C++ Signal Processing Blocks 3 | * \brief All C++ blocks that can be used from the TENNA GNU Radio 4 | * module are listed here or in the subcategories below. 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /docs/doxygen/doxyxml/generated/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains generated files produced by generateDS.py. 3 | 4 | These do the real work of parsing the doxygen xml files but the 5 | resultant classes are not very friendly to navigate so the rest of the 6 | doxyxml module processes them further. 7 | """ 8 | -------------------------------------------------------------------------------- /apps/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | include(GrPython) 10 | 11 | gr_python_install(PROGRAMS DESTINATION bin) 12 | -------------------------------------------------------------------------------- /docs/doxygen/other/main_page.dox: -------------------------------------------------------------------------------- 1 | /*! \mainpage 2 | 3 | Welcome to the GNU Radio TENNA Block 4 | 5 | This is the intro page for the Doxygen manual generated for the TENNA 6 | block (docs/doxygen/other/main_page.dox). Edit it to add more detailed 7 | documentation about the new GNU Radio modules contained in this 8 | project. 9 | 10 | */ 11 | -------------------------------------------------------------------------------- /grc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | install(FILES 10 | tenna_gotenna_decoder.block.yml 11 | tenna_pdu_to_pcapng.block.yml DESTINATION share/gnuradio/grc/blocks) 12 | -------------------------------------------------------------------------------- /protobuf/gotenna_packet/proto/header.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "gotenna_packet/proto/data_type.proto"; 4 | 5 | message PBBaseHeader { 6 | uint64 sender_gid = 1; 7 | string callsign = 2; 8 | PBMessageDataType messageType = 3; 9 | string keyUuid = 4; 10 | bytes iv = 5; 11 | PBConversationType conversationType = 6; 12 | double timestamp = 7; 13 | } 14 | -------------------------------------------------------------------------------- /cmake/Modules/targetConfig.cmake.in: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Free Software Foundation, Inc. 2 | # 3 | # This file is part of GNU Radio 4 | # 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | # 7 | 8 | include(CMakeFindDependencyMacro) 9 | 10 | set(target_deps "@TARGET_DEPENDENCIES@") 11 | foreach(dep IN LISTS target_deps) 12 | find_dependency(${dep}) 13 | endforeach() 14 | include("${CMAKE_CURRENT_LIST_DIR}/@TARGET@Targets.cmake") 15 | -------------------------------------------------------------------------------- /docs/README.tenna: -------------------------------------------------------------------------------- 1 | This is the tenna-write-a-block package meant as a guide to building 2 | out-of-tree packages. To use the tenna blocks, the Python namespaces 3 | is in 'tenna', which is imported as: 4 | 5 | import tenna 6 | 7 | See the Doxygen documentation for details about the blocks available 8 | in this package. A quick listing of the details can be found in Python 9 | after importing by using: 10 | 11 | help(tenna) 12 | -------------------------------------------------------------------------------- /MANIFEST.md: -------------------------------------------------------------------------------- 1 | title: gr-tenna 2 | brief: goTenna receiver & transmitter 3 | tags: 4 | - goTenna 5 | author: 6 | - Clayton Smith 7 | copyright_owner: 8 | - Clayton Smith 9 | license: GPLv3 10 | gr_supported_version: v3.10 11 | repo: https://github.com/argilo/gr-tenna.git 12 | --- 13 | This module implements a basic receiver & trasmitter for goTenna Mesh, 14 | as well as a basic receiver for goTenna Pro. 15 | -------------------------------------------------------------------------------- /grc/tenna_gotenna_decoder.block.yml: -------------------------------------------------------------------------------- 1 | id: tenna_gotenna_decoder 2 | label: goTenna Decoder 3 | category: '[gr-tenna]' 4 | 5 | templates: 6 | imports: from gnuradio import tenna 7 | make: tenna.gotenna_decoder(${mode}) 8 | 9 | parameters: 10 | - id: mode 11 | label: Mode 12 | dtype: int 13 | default: 0 14 | inputs: 15 | - label: in 16 | domain: stream 17 | dtype: byte 18 | outputs: 19 | - label: pdu 20 | domain: message 21 | 22 | file_format: 1 23 | -------------------------------------------------------------------------------- /protobuf/gotenna_packet/proto/frequency.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "gotenna_packet/proto/data_type.proto"; 4 | 5 | message PBChannel { 6 | uint32 hertz = 1; 7 | bool is_control = 2; 8 | } 9 | 10 | message PBFrequency { 11 | string id = 1; 12 | string title = 2; 13 | string call_sign = 3; 14 | PBPowerType power = 4; 15 | PBBandwidthType bandwidth = 5; 16 | bool use_only = 6; 17 | repeated PBChannel channels = 7; 18 | } 19 | -------------------------------------------------------------------------------- /grc/tenna_pdu_to_pcapng.block.yml: -------------------------------------------------------------------------------- 1 | id: tenna_pdu_to_pcapng 2 | label: PDU to PcapNG 3 | category: '[gr-tenna]' 4 | 5 | templates: 6 | imports: from gnuradio import tenna 7 | make: tenna.pdu_to_pcapng(${filename}, ${append}) 8 | 9 | parameters: 10 | - id: filename 11 | label: Filename 12 | dtype: string 13 | default: gotenna.pcapng 14 | - id: append 15 | label: Append 16 | dtype: bool 17 | default: True 18 | inputs: 19 | - label: pdu 20 | domain: message 21 | 22 | file_format: 1 23 | -------------------------------------------------------------------------------- /include/gnuradio/tenna/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011,2012 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | ######################################################################## 10 | # Install public header files 11 | ######################################################################## 12 | install(FILES api.h DESTINATION include/gnuradio/tenna) 13 | -------------------------------------------------------------------------------- /apps/decode_pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import pcapng 5 | import gotenna_packet 6 | 7 | parser = argparse.ArgumentParser(description="Decode Gotenna packets from a PcapNG file.") 8 | parser.add_argument("filename") 9 | args = parser.parse_args() 10 | 11 | with open(args.filename, "rb") as f: 12 | scanner = pcapng.FileScanner(f) 13 | for block in scanner: 14 | if isinstance(block, pcapng.blocks.EnhancedPacket): 15 | packet = block.packet_data 16 | gotenna_packet.ingest_packet(packet) 17 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Clayton Smith. 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | # 5 | 6 | ######################################################################## 7 | # Install python sources 8 | ######################################################################## 9 | gr_python_install(FILES 10 | __init__.py 11 | base_message_pb2.py 12 | data_type_pb2.py 13 | frequency_pb2.py 14 | header_pb2.py 15 | location_pb2.py 16 | message_pb2.py 17 | DESTINATION ${GR_PYTHON_DIR}/gotenna_packet/proto) 18 | -------------------------------------------------------------------------------- /include/gnuradio/tenna/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 Free Software Foundation, Inc. 3 | * 4 | * This file was generated by gr_modtool, a tool from the GNU Radio framework 5 | * This file is a part of gr-tenna 6 | * 7 | * SPDX-License-Identifier: GPL-3.0-or-later 8 | * 9 | */ 10 | 11 | #ifndef INCLUDED_TENNA_API_H 12 | #define INCLUDED_TENNA_API_H 13 | 14 | #include 15 | 16 | #ifdef gnuradio_tenna_EXPORTS 17 | #define TENNA_API __GR_ATTR_EXPORT 18 | #else 19 | #define TENNA_API __GR_ATTR_IMPORT 20 | #endif 21 | 22 | #endif /* INCLUDED_TENNA_API_H */ 23 | -------------------------------------------------------------------------------- /python/gotenna_packet/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Clayton Smith. 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | # 5 | 6 | ######################################################################## 7 | # Install python sources 8 | ######################################################################## 9 | gr_python_install(FILES 10 | __init__.py 11 | gotenna_packet.py 12 | DESTINATION ${GR_PYTHON_DIR}/gotenna_packet) 13 | 14 | 15 | ######################################################################## 16 | # Add subdirectories 17 | ######################################################################## 18 | add_subdirectory(proto) 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | name: "Python ${{ matrix.python-version }}" 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | matrix: 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 12 | steps: 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: Install Python packages 20 | run: pip install -r requirements.txt 21 | - name: Test 22 | run: python/test_gotenna_packet.py 23 | -------------------------------------------------------------------------------- /python/tenna/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2008,2009 Free Software Foundation, Inc. 3 | # 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | # 6 | 7 | # The presence of this file turns this directory into a Python package 8 | 9 | ''' 10 | This is the GNU Radio TENNA module. Place your Python package 11 | description here (python/__init__.py). 12 | ''' 13 | import os 14 | 15 | # import pybind11 generated symbols into the tenna namespace 16 | try: 17 | # this might fail if the module is python-only 18 | from .tenna_python import * 19 | except ModuleNotFoundError: 20 | pass 21 | 22 | # import any pure python here 23 | from .gotenna_decoder import gotenna_decoder 24 | from .pdu_to_pcapng import pdu_to_pcapng 25 | # 26 | -------------------------------------------------------------------------------- /python/tenna/qa_pdu_to_pcapng.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2024 Clayton Smith. 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | from gnuradio import gr, gr_unittest 10 | # from gnuradio import blocks 11 | from gnuradio.tenna import pdu_to_pcapng 12 | 13 | class qa_pdu_to_pcapng(gr_unittest.TestCase): 14 | 15 | def setUp(self): 16 | self.tb = gr.top_block() 17 | 18 | def tearDown(self): 19 | self.tb = None 20 | 21 | def test_instance(self): 22 | # FIXME: Test will fail until you pass sensible arguments to the constructor 23 | instance = pdu_to_pcapng() 24 | 25 | def test_001_descriptive_test_name(self): 26 | # set up fg 27 | self.tb.run() 28 | # check data 29 | 30 | 31 | if __name__ == '__main__': 32 | gr_unittest.run(qa_pdu_to_pcapng) 33 | -------------------------------------------------------------------------------- /python/tenna/qa_gotenna_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2024 Clayton Smith. 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | from gnuradio import gr, gr_unittest 10 | # from gnuradio import blocks 11 | from gnuradio.tenna import gotenna_decoder 12 | 13 | class qa_gotenna_decoder(gr_unittest.TestCase): 14 | 15 | def setUp(self): 16 | self.tb = gr.top_block() 17 | 18 | def tearDown(self): 19 | self.tb = None 20 | 21 | def test_instance(self): 22 | # FIXME: Test will fail until you pass sensible arguments to the constructor 23 | instance = gotenna_decoder() 24 | 25 | def test_001_descriptive_test_name(self): 26 | # set up fg 27 | self.tb.run() 28 | # check data 29 | 30 | 31 | if __name__ == '__main__': 32 | gr_unittest.run(qa_gotenna_decoder) 33 | -------------------------------------------------------------------------------- /docs/doxygen/pydoc_macros.h: -------------------------------------------------------------------------------- 1 | #ifndef PYDOC_MACROS_H 2 | #define PYDOC_MACROS_H 3 | 4 | #define __EXPAND(x) x 5 | #define __COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT 6 | #define __VA_SIZE(...) __EXPAND(__COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1)) 7 | #define __CAT1(a, b) a##b 8 | #define __CAT2(a, b) __CAT1(a, b) 9 | #define __DOC1(n1) __doc_##n1 10 | #define __DOC2(n1, n2) __doc_##n1##_##n2 11 | #define __DOC3(n1, n2, n3) __doc_##n1##_##n2##_##n3 12 | #define __DOC4(n1, n2, n3, n4) __doc_##n1##_##n2##_##n3##_##n4 13 | #define __DOC5(n1, n2, n3, n4, n5) __doc_##n1##_##n2##_##n3##_##n4##_##n5 14 | #define __DOC6(n1, n2, n3, n4, n5, n6) __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 15 | #define __DOC7(n1, n2, n3, n4, n5, n6, n7) \ 16 | __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 17 | #define DOC(...) __EXPAND(__EXPAND(__CAT2(__DOC, __VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) 18 | 19 | #endif // PYDOC_MACROS_H 20 | -------------------------------------------------------------------------------- /docs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | ######################################################################## 10 | # Setup dependencies 11 | ######################################################################## 12 | find_package(Doxygen) 13 | 14 | ######################################################################## 15 | # Begin conditional configuration 16 | ######################################################################## 17 | if(ENABLE_DOXYGEN) 18 | 19 | ######################################################################## 20 | # Add subdirectories 21 | ######################################################################## 22 | add_subdirectory(doxygen) 23 | 24 | endif(ENABLE_DOXYGEN) 25 | -------------------------------------------------------------------------------- /cmake/Modules/gnuradio-tennaConfig.cmake: -------------------------------------------------------------------------------- 1 | find_package(PkgConfig) 2 | 3 | PKG_CHECK_MODULES(PC_GR_TENNA gnuradio-tenna) 4 | 5 | FIND_PATH( 6 | GR_TENNA_INCLUDE_DIRS 7 | NAMES gnuradio/tenna/api.h 8 | HINTS $ENV{TENNA_DIR}/include 9 | ${PC_TENNA_INCLUDEDIR} 10 | PATHS ${CMAKE_INSTALL_PREFIX}/include 11 | /usr/local/include 12 | /usr/include 13 | ) 14 | 15 | FIND_LIBRARY( 16 | GR_TENNA_LIBRARIES 17 | NAMES gnuradio-tenna 18 | HINTS $ENV{TENNA_DIR}/lib 19 | ${PC_TENNA_LIBDIR} 20 | PATHS ${CMAKE_INSTALL_PREFIX}/lib 21 | ${CMAKE_INSTALL_PREFIX}/lib64 22 | /usr/local/lib 23 | /usr/local/lib64 24 | /usr/lib 25 | /usr/lib64 26 | ) 27 | 28 | include("${CMAKE_CURRENT_LIST_DIR}/gnuradio-tennaTarget.cmake") 29 | 30 | INCLUDE(FindPackageHandleStandardArgs) 31 | FIND_PACKAGE_HANDLE_STANDARD_ARGS(GR_TENNA DEFAULT_MSG GR_TENNA_LIBRARIES GR_TENNA_INCLUDE_DIRS) 32 | MARK_AS_ADVANCED(GR_TENNA_LIBRARIES GR_TENNA_INCLUDE_DIRS) 33 | -------------------------------------------------------------------------------- /protobuf/gotenna_packet/proto/location.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "gotenna_packet/proto/data_type.proto"; 4 | 5 | message PBShapeData { 6 | uint32 color = 1; 7 | PBGeofenceType geofence_type = 2; 8 | 9 | oneof map_object { 10 | PBRouteMessageData route_data = 4; 11 | PBPerimeterMessageData perimeter_data = 5; 12 | PBCircleMessageData circle_data = 6; 13 | PBRectangleMessageData rectangle_data = 7; 14 | } 15 | } 16 | 17 | message PBPinData { 18 | bytes coordinate = 1; 19 | PBPinType pin_type = 2; 20 | } 21 | 22 | message PBRouteMessageData { 23 | bytes data_points = 1; 24 | } 25 | 26 | message PBPerimeterMessageData { 27 | bytes data_points = 1; 28 | } 29 | 30 | message PBCircleMessageData { 31 | bytes center = 1; 32 | uint64 radius = 2; 33 | } 34 | 35 | message PBRectangleMessageData { 36 | bytes cornerOne = 1; 37 | bytes cornerTwo = 2; 38 | bytes depth = 3; 39 | } 40 | 41 | message PBCoordinate { 42 | double latitude = 1; 43 | double longitude = 2; 44 | } 45 | -------------------------------------------------------------------------------- /protobuf/gotenna_packet/proto/data_type.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | enum PBMessageDataType { 4 | GROUP_CREATION = 0; 5 | MAP_OBJECT = 1; 6 | REQUEST = 2; 7 | TEXT = 3; 8 | FREQUENCY = 4; 9 | PING = 5; 10 | EMERGENCY_BEACON = 6; 11 | PUBLIC_KEY_REQUEST = 7; 12 | PUBLIC_KEY_RESPONSE = 8; 13 | BROADCAST_QR = 9; 14 | PLI = 10; 15 | SHARED_LOCATION = 11; 16 | COMMS_CHECK = 12; 17 | MANUAL_PLI = 13; 18 | } 19 | 20 | enum PBGeofenceType { 21 | GEOFENCING_NONE = 0; 22 | GEOFENCING_ENTRY = 1; 23 | GEOFENCING_EXIT = 2; 24 | GEOFENCING_BOTH = 3; 25 | } 26 | 27 | enum PBPowerType { 28 | HALF_WATT = 0; 29 | ONE_WATT = 1; 30 | TWO_WATT = 2; 31 | FOUR_WATT = 3; 32 | FIVE_WATT = 4; 33 | } 34 | 35 | enum PBBandwidthType { 36 | BW_4_84KHZ = 0; 37 | BW_7_28KHZ = 1; 38 | BW_11_80KHZ = 2; 39 | } 40 | 41 | enum PBPinType { 42 | DEFAULT = 0; 43 | EMERGENCY = 1; 44 | WARNING = 2; 45 | } 46 | 47 | enum PBConversationType { 48 | UNKNOWN = 0; 49 | COMMAND = 1; 50 | } 51 | -------------------------------------------------------------------------------- /apps/keygen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import gotenna_packet 5 | from cryptography.hazmat.primitives.asymmetric import ec 6 | from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, PublicFormat 7 | 8 | ASN1_PREFIX_PUBLIC = bytes.fromhex("3076301006072a8648ce3d020106052b81040022036200") 9 | 10 | parser = argparse.ArgumentParser(description="Generate a key pair for Gotenna Pro.") 11 | parser.add_argument("gid", type=int) 12 | args = parser.parse_args() 13 | 14 | curve = ec.SECP384R1() 15 | private_key = ec.generate_private_key(curve) 16 | private_key_bytes = private_key.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption()) 17 | gotenna_packet.private_keys[args.gid] = private_key_bytes 18 | 19 | public_key = private_key.public_key() 20 | public_key_bytes = public_key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) 21 | if public_key_bytes[:len(ASN1_PREFIX_PUBLIC)] == ASN1_PREFIX_PUBLIC: 22 | gotenna_packet.public_keys[args.gid] = public_key_bytes[len(ASN1_PREFIX_PUBLIC):] 23 | else: 24 | print("Error: public key not in expected format") 25 | 26 | gotenna_packet.save_keys() 27 | -------------------------------------------------------------------------------- /apps/decrypt_gotenna_qr_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import base64 4 | import gotenna_packet 5 | 6 | # Define password below 7 | password = b"0fo79vu6" 8 | 9 | # Add information directly from the BROADCAST_QR message 10 | 11 | header = { 12 | "senderGid": "95681472401029", 13 | "callsign": "Dollarhyde", 14 | "messageType": "BROADCAST_QR", 15 | "iv": "/AAAAA==", 16 | "timestamp": 1707450048.91588 17 | } 18 | data = { 19 | "name": "dGh1cnMgICAgIA==", 20 | "uuid": "Bw==", 21 | "salt": "kkn+R3vB2JaAr8BhZzFWJA==", 22 | "iv": "/AAAAA==", 23 | "keyData": "Ge12ZsekePbnBIEwAasXsZOpMyrPeoTut9FoIU2crhJgEx6aVRgraV1KCN6W0d0OhUWWGK53C/yzmWVJqdcbn9CfGbjAHc4C4fKOeyGyZm0=" 24 | } 25 | 26 | data = {key: base64.b64decode(value.encode()) for key, value in data.items()} 27 | payload = gotenna_packet.decrypt_qr_message(password, data["salt"], data["iv"], int(header["senderGid"]), data["keyData"]) 28 | name = data["name"].decode('utf-8').strip() 29 | key_UUID = data["uuid"].hex() + hex(gotenna_packet.gid_hash(int(header["senderGid"])))[2:].upper().zfill(4) 30 | 31 | print(f"Key \"{name}\", UUID \"{key_UUID}\": {payload.hex()}") 32 | #print(f"Add below to gotenna_packet broadcast_keys:") 33 | #print(f"\"{key_UUID}\": bytes.fromhex(\"{payload.hex()}\")") 34 | 35 | gotenna_packet.broadcast_keys[key_UUID] = payload 36 | gotenna_packet.save_keys() 37 | 38 | -------------------------------------------------------------------------------- /protobuf/gotenna_packet/proto/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "gotenna_packet/proto/location.proto"; 4 | import "gotenna_packet/proto/frequency.proto"; 5 | 6 | message PBMapObjectMessageData { 7 | double timestamp = 1; 8 | uint64 uuid = 2; 9 | string title = 3; 10 | 11 | oneof map_object { 12 | PBShapeData shape = 4; 13 | PBPinData pin = 5; 14 | } 15 | } 16 | 17 | message PBLocationMessageData { 18 | bytes coordinate = 1; 19 | uint32 pli_sharing_frequency_index = 2; 20 | uint32 pli_location_accuracy = 3; 21 | double timestamp = 4; 22 | string text = 5; 23 | } 24 | 25 | message PBRequestMessageData { 26 | string text = 1; 27 | } 28 | 29 | message PBTextMessageData { 30 | string text = 1; 31 | string uuid = 2; 32 | } 33 | 34 | message PBFrequencyMessageData { 35 | string text = 1; 36 | PBFrequency frequency = 2; 37 | } 38 | 39 | message PBGroupCreationMessageData { 40 | repeated PBGroupMember group_members = 1; 41 | message PBGroupMember { 42 | uint64 gid = 1; 43 | } 44 | 45 | bytes group_shared_key = 2; 46 | uint64 group_gid = 3; 47 | } 48 | 49 | message PBPublicKeyMessageData { 50 | bytes public_key = 1; 51 | } 52 | 53 | message PBBroadcastQrMessageData { 54 | bytes name = 1; 55 | bytes uuid = 2; 56 | bytes salt = 3; 57 | bytes iv = 4; 58 | bytes keyData = 5; 59 | } 60 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/base_message_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: gotenna_packet/proto/base_message.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | from gotenna_packet.proto import header_pb2 as gotenna__packet_dot_proto_dot_header__pb2 16 | 17 | 18 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'gotenna_packet/proto/base_message.proto\x1a!gotenna_packet/proto/header.proto\"C\n\rPBBaseMessage\x12\x1d\n\x06header\x18\x01 \x01(\x0b\x32\r.PBBaseHeader\x12\x13\n\x0bmessageData\x18\x02 \x01(\x0c\x62\x06proto3') 19 | 20 | _globals = globals() 21 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 22 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gotenna_packet.proto.base_message_pb2', _globals) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | DESCRIPTOR._options = None 25 | _globals['_PBBASEMESSAGE']._serialized_start=78 26 | _globals['_PBBASEMESSAGE']._serialized_end=145 27 | # @@protoc_insertion_point(module_scope) 28 | -------------------------------------------------------------------------------- /docs/doxygen/doxyxml/text.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010 Free Software Foundation, Inc. 3 | # 4 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 5 | # This file is a part of gr-tenna 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | # 10 | """ 11 | Utilities for extracting text from generated classes. 12 | """ 13 | 14 | 15 | def is_string(txt): 16 | if isinstance(txt, str): 17 | return True 18 | try: 19 | if isinstance(txt, str): 20 | return True 21 | except NameError: 22 | pass 23 | return False 24 | 25 | 26 | def description(obj): 27 | if obj is None: 28 | return None 29 | return description_bit(obj).strip() 30 | 31 | 32 | def description_bit(obj): 33 | if hasattr(obj, 'content'): 34 | contents = [description_bit(item) for item in obj.content] 35 | result = ''.join(contents) 36 | elif hasattr(obj, 'content_'): 37 | contents = [description_bit(item) for item in obj.content_] 38 | result = ''.join(contents) 39 | elif hasattr(obj, 'value'): 40 | result = description_bit(obj.value) 41 | elif is_string(obj): 42 | return obj 43 | else: 44 | raise Exception( 45 | 'Expecting a string or something with content, content_ or value attribute') 46 | # If this bit is a paragraph then add one some line breaks. 47 | if hasattr(obj, 'name') and obj.name == 'para': 48 | result += "\n\n" 49 | return result 50 | -------------------------------------------------------------------------------- /cmake/cmake_uninstall.cmake.in: -------------------------------------------------------------------------------- 1 | # http://www.vtk.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F 2 | 3 | IF(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") 4 | MESSAGE(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") 5 | ENDIF(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") 6 | 7 | FILE(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) 8 | STRING(REGEX REPLACE "\n" ";" files "${files}") 9 | FOREACH(file ${files}) 10 | MESSAGE(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") 11 | IF(EXISTS "$ENV{DESTDIR}${file}") 12 | EXEC_PROGRAM( 13 | "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" 14 | OUTPUT_VARIABLE rm_out 15 | RETURN_VALUE rm_retval 16 | ) 17 | IF(NOT "${rm_retval}" STREQUAL 0) 18 | MESSAGE(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") 19 | ENDIF(NOT "${rm_retval}" STREQUAL 0) 20 | ELSEIF(IS_SYMLINK "$ENV{DESTDIR}${file}") 21 | EXEC_PROGRAM( 22 | "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" 23 | OUTPUT_VARIABLE rm_out 24 | RETURN_VALUE rm_retval 25 | ) 26 | IF(NOT "${rm_retval}" STREQUAL 0) 27 | MESSAGE(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") 28 | ENDIF(NOT "${rm_retval}" STREQUAL 0) 29 | ELSE(EXISTS "$ENV{DESTDIR}${file}") 30 | MESSAGE(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") 31 | ENDIF(EXISTS "$ENV{DESTDIR}${file}") 32 | ENDFOREACH(file) 33 | -------------------------------------------------------------------------------- /python/tenna/bindings/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Free Software Foundation, Inc. 2 | # 3 | # This file is part of GNU Radio 4 | # 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | # 7 | 8 | ######################################################################## 9 | # Check if there is C++ code at all 10 | ######################################################################## 11 | if(NOT tenna_sources) 12 | message(STATUS "No C++ sources... skipping python bindings") 13 | return() 14 | endif(NOT tenna_sources) 15 | 16 | ######################################################################## 17 | # Check for pygccxml 18 | ######################################################################## 19 | gr_python_check_module_raw("pygccxml" "import pygccxml" PYGCCXML_FOUND) 20 | 21 | include(GrPybind) 22 | 23 | ######################################################################## 24 | # Python Bindings 25 | ######################################################################## 26 | 27 | list(APPEND tenna_python_files python_bindings.cc) 28 | 29 | gr_pybind_make_oot(tenna ../../.. gr::tenna "${tenna_python_files}") 30 | 31 | # copy bindings extension for use in QA test module 32 | add_custom_command( 33 | TARGET tenna_python 34 | POST_BUILD 35 | COMMAND ${CMAKE_COMMAND} -E copy $ 36 | ${PROJECT_BINARY_DIR}/test_modules/gnuradio/tenna/) 37 | 38 | install( 39 | TARGETS tenna_python 40 | DESTINATION ${GR_PYTHON_DIR}/gnuradio/tenna 41 | COMPONENT pythonapi) 42 | -------------------------------------------------------------------------------- /python/tenna/bindings/python_bindings.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * SPDX-License-Identifier: GPL-3.0-or-later 7 | * 8 | */ 9 | 10 | #include 11 | 12 | #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION 13 | #include 14 | 15 | namespace py = pybind11; 16 | 17 | // Headers for binding functions 18 | /**************************************/ 19 | // The following comment block is used for 20 | // gr_modtool to insert function prototypes 21 | // Please do not delete 22 | /**************************************/ 23 | // BINDING_FUNCTION_PROTOTYPES( 24 | // ) END BINDING_FUNCTION_PROTOTYPES 25 | 26 | 27 | // We need this hack because import_array() returns NULL 28 | // for newer Python versions. 29 | // This function is also necessary because it ensures access to the C API 30 | // and removes a warning. 31 | void* init_numpy() 32 | { 33 | import_array(); 34 | return NULL; 35 | } 36 | 37 | PYBIND11_MODULE(tenna_python, m) 38 | { 39 | // Initialize the numpy C API 40 | // (otherwise we will see segmentation faults) 41 | init_numpy(); 42 | 43 | // Allow access to base block methods 44 | py::module::import("gnuradio.gr"); 45 | 46 | /**************************************/ 47 | // The following comment block is used for 48 | // gr_modtool to insert binding function calls 49 | // Please do not delete 50 | /**************************************/ 51 | // BINDING_FUNCTION_CALLS( 52 | // ) END BINDING_FUNCTION_CALLS 53 | } 54 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/header_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: gotenna_packet/proto/header.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | from gotenna_packet.proto import data_type_pb2 as gotenna__packet_dot_proto_dot_data__type__pb2 16 | 17 | 18 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!gotenna_packet/proto/header.proto\x1a$gotenna_packet/proto/data_type.proto\"\xbc\x01\n\x0cPBBaseHeader\x12\x12\n\nsender_gid\x18\x01 \x01(\x04\x12\x10\n\x08\x63\x61llsign\x18\x02 \x01(\t\x12\'\n\x0bmessageType\x18\x03 \x01(\x0e\x32\x12.PBMessageDataType\x12\x0f\n\x07keyUuid\x18\x04 \x01(\t\x12\n\n\x02iv\x18\x05 \x01(\x0c\x12-\n\x10\x63onversationType\x18\x06 \x01(\x0e\x32\x13.PBConversationType\x12\x11\n\ttimestamp\x18\x07 \x01(\x01\x62\x06proto3') 19 | 20 | _globals = globals() 21 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 22 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gotenna_packet.proto.header_pb2', _globals) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | DESCRIPTOR._options = None 25 | _globals['_PBBASEHEADER']._serialized_start=76 26 | _globals['_PBBASEHEADER']._serialized_end=264 27 | # @@protoc_insertion_point(module_scope) 28 | -------------------------------------------------------------------------------- /docs/doxygen/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | ######################################################################## 10 | # Create the doxygen configuration file 11 | ######################################################################## 12 | file(TO_NATIVE_PATH ${PROJECT_SOURCE_DIR} top_srcdir) 13 | file(TO_NATIVE_PATH ${PROJECT_BINARY_DIR} top_builddir) 14 | file(TO_NATIVE_PATH ${PROJECT_SOURCE_DIR} abs_top_srcdir) 15 | file(TO_NATIVE_PATH ${PROJECT_BINARY_DIR} abs_top_builddir) 16 | 17 | set(HAVE_DOT ${DOXYGEN_DOT_FOUND}) 18 | set(enable_html_docs YES) 19 | set(enable_latex_docs NO) 20 | set(enable_mathjax NO) 21 | set(enable_xml_docs YES) 22 | 23 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in 24 | ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) 25 | 26 | set(BUILT_DIRS ${CMAKE_CURRENT_BINARY_DIR}/xml ${CMAKE_CURRENT_BINARY_DIR}/html) 27 | 28 | ######################################################################## 29 | # Make and install doxygen docs 30 | ######################################################################## 31 | add_custom_command( 32 | OUTPUT ${BUILT_DIRS} 33 | COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile 34 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} 35 | COMMENT "Generating documentation with doxygen") 36 | 37 | add_custom_target(doxygen_target ALL DEPENDS ${BUILT_DIRS}) 38 | 39 | install(DIRECTORY ${BUILT_DIRS} DESTINATION ${GR_PKG_DOC_DIR}) 40 | -------------------------------------------------------------------------------- /python/tenna/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | ######################################################################## 10 | # Include python install macros 11 | ######################################################################## 12 | include(GrPython) 13 | if(NOT PYTHONINTERP_FOUND) 14 | return() 15 | endif() 16 | 17 | add_subdirectory(bindings) 18 | 19 | ######################################################################## 20 | # Install python sources 21 | ######################################################################## 22 | gr_python_install(FILES 23 | __init__.py 24 | gotenna_decoder.py 25 | pdu_to_pcapng.py 26 | DESTINATION ${GR_PYTHON_DIR}/gnuradio/tenna) 27 | 28 | ######################################################################## 29 | # Handle the unit tests 30 | ######################################################################## 31 | include(GrTest) 32 | 33 | set(GR_TEST_TARGET_DEPS gnuradio-tenna) 34 | 35 | # Create a package directory that tests can import. It includes everything 36 | # from `python/`. 37 | add_custom_target( 38 | copy_module_for_tests ALL 39 | COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR} 40 | ${PROJECT_BINARY_DIR}/test_modules/gnuradio/tenna/) 41 | GR_ADD_TEST(qa_gotenna_decoder ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/qa_gotenna_decoder.py) 42 | GR_ADD_TEST(qa_pdu_to_pcapng ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/qa_pdu_to_pcapng.py) 43 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/frequency_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: gotenna_packet/proto/frequency.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | from gotenna_packet.proto import data_type_pb2 as gotenna__packet_dot_proto_dot_data__type__pb2 16 | 17 | 18 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$gotenna_packet/proto/frequency.proto\x1a$gotenna_packet/proto/data_type.proto\".\n\tPBChannel\x12\r\n\x05hertz\x18\x01 \x01(\r\x12\x12\n\nis_control\x18\x02 \x01(\x08\"\xad\x01\n\x0bPBFrequency\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x11\n\tcall_sign\x18\x03 \x01(\t\x12\x1b\n\x05power\x18\x04 \x01(\x0e\x32\x0c.PBPowerType\x12#\n\tbandwidth\x18\x05 \x01(\x0e\x32\x10.PBBandwidthType\x12\x10\n\x08use_only\x18\x06 \x01(\x08\x12\x1c\n\x08\x63hannels\x18\x07 \x03(\x0b\x32\n.PBChannelb\x06proto3') 19 | 20 | _globals = globals() 21 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 22 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gotenna_packet.proto.frequency_pb2', _globals) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | DESCRIPTOR._options = None 25 | _globals['_PBCHANNEL']._serialized_start=78 26 | _globals['_PBCHANNEL']._serialized_end=124 27 | _globals['_PBFREQUENCY']._serialized_start=127 28 | _globals['_PBFREQUENCY']._serialized_end=300 29 | # @@protoc_insertion_point(module_scope) 30 | -------------------------------------------------------------------------------- /python/tenna/pdu_to_pcapng.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2024 Clayton Smith. 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | 10 | from gnuradio import gr 11 | import time 12 | import pmt 13 | import pcapng 14 | 15 | 16 | class pdu_to_pcapng(gr.sync_block): 17 | """Write PDUs to PcapNG file""" 18 | 19 | def __init__(self, filename="gotenna.pcapng", append=True): 20 | gr.sync_block.__init__( 21 | self, 22 | name="pdu_to_pcapng", 23 | in_sig=None, 24 | out_sig=None 25 | ) 26 | self.message_port_register_in(pmt.intern("pdu")) 27 | self.set_msg_handler(pmt.intern("pdu"), self.handle_msg) 28 | 29 | self.shb = pcapng.blocks.SectionHeader( 30 | options={ 31 | "shb_userappl": "gr-tenna", 32 | } 33 | ) 34 | idb = self.shb.new_member( 35 | pcapng.blocks.InterfaceDescription, 36 | link_type=147, 37 | options={ 38 | "if_description": "Gotenna receiver", 39 | }, 40 | ) 41 | self.file = open(filename, "ab" if append else "wb") 42 | self.writer = pcapng.FileWriter(self.file, self.shb) 43 | 44 | def handle_msg(self, msg): 45 | metadata = pmt.car(msg) 46 | packet = bytes(pmt.u8vector_elements(pmt.cdr(msg))) 47 | 48 | spb = self.shb.new_member(pcapng.blocks.EnhancedPacket) 49 | spb.packet_data = packet 50 | 51 | if pmt.dict_has_key(metadata, pmt.intern("system_time")): 52 | system_time = pmt.to_double(pmt.dict_ref(metadata, pmt.intern("system_time"), pmt.PMT_NIL)) 53 | else: 54 | system_time = time.time() 55 | 56 | pcap_time = int(system_time * 1000000) 57 | spb.timestamp_high = pcap_time >> 32 58 | spb.timestamp_low = pcap_time & 0xffffffff 59 | 60 | self.writer.write_block(spb) 61 | self.file.flush() 62 | -------------------------------------------------------------------------------- /python/tenna/bindings/bind_oot_file.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import argparse 3 | from gnuradio.bindtool import BindingGenerator 4 | import sys 5 | import tempfile 6 | 7 | parser = argparse.ArgumentParser(description='Bind a GR Out of Tree Block') 8 | parser.add_argument('--module', type=str, 9 | help='Name of gr module containing file to bind (e.g. fft digital analog)') 10 | 11 | parser.add_argument('--output_dir', default=tempfile.gettempdir(), 12 | help='Output directory of generated bindings') 13 | parser.add_argument('--prefix', help='Prefix of Installed GNU Radio') 14 | 15 | parser.add_argument( 16 | '--filename', help="File to be parsed") 17 | 18 | parser.add_argument( 19 | '--defines', help='Set additional defines for precompiler', default=(), nargs='*') 20 | parser.add_argument( 21 | '--include', help='Additional Include Dirs, separated', default=(), nargs='*') 22 | 23 | parser.add_argument( 24 | '--status', help='Location of output file for general status (used during cmake)', default=None 25 | ) 26 | parser.add_argument( 27 | '--flag_automatic', default='0' 28 | ) 29 | parser.add_argument( 30 | '--flag_pygccxml', default='0' 31 | ) 32 | 33 | args = parser.parse_args() 34 | 35 | prefix = args.prefix 36 | output_dir = args.output_dir 37 | defines = tuple(','.join(args.defines).split(',')) 38 | includes = ','.join(args.include) 39 | name = args.module 40 | 41 | namespace = ['gr', name] 42 | prefix_include_root = name 43 | 44 | 45 | with warnings.catch_warnings(): 46 | warnings.filterwarnings("ignore", category=DeprecationWarning) 47 | 48 | bg = BindingGenerator(prefix, namespace, 49 | prefix_include_root, output_dir, define_symbols=defines, addl_includes=includes, 50 | catch_exceptions=False, write_json_output=False, status_output=args.status, 51 | flag_automatic=True if args.flag_automatic.lower() in [ 52 | '1', 'true'] else False, 53 | flag_pygccxml=True if args.flag_pygccxml.lower() in ['1', 'true'] else False) 54 | bg.gen_file_binding(args.filename) 55 | -------------------------------------------------------------------------------- /python/tenna/gotenna_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2018-2024 Clayton Smith. 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | 10 | import numpy 11 | from gnuradio import gr 12 | import pmt 13 | import gotenna_packet 14 | 15 | 16 | class gotenna_decoder(gr.sync_block): 17 | """Decode Gotenna Mesh packets""" 18 | 19 | def __init__(self, mode=0): 20 | gr.sync_block.__init__( 21 | self, 22 | name="gotenna_decoder", 23 | in_sig=[numpy.int8], 24 | out_sig=None 25 | ) 26 | self.message_port_register_out(pmt.intern("pdu")) 27 | if mode == 0: 28 | self.prefix = "10"*16 + "0010110111010100" 29 | self.whitening = [0x00] * 256 30 | elif mode == 1: 31 | self.prefix = "1000"*8 + "0010110111010100" 32 | self.whitening = gotenna_packet.WHITENING 33 | self.bits = "" 34 | self.my_log = gr.logger(self.alias()) 35 | 36 | def work(self, input_items, output_items): 37 | self.bits += "".join([str(n) for n in input_items[0]]) 38 | 39 | idx = self.bits[:-2048].find(self.prefix) 40 | while idx >= 0: 41 | self.bits = self.bits[idx + len(self.prefix):] 42 | length = int(self.bits[0:8], 2) ^ self.whitening[0] 43 | 44 | packet = bytes(int(self.bits[i*8:i*8 + 8], 2) ^ self.whitening[i] for i in range(length + 1)) 45 | self.my_log.debug(f"Raw packet: {packet.hex()}") 46 | try: 47 | packet = gotenna_packet.correct_packet(packet) 48 | pdu = pmt.cons(pmt.make_dict(), pmt.init_u8vector(len(packet), list(packet))) 49 | self.message_port_pub(pmt.intern("pdu"), pdu) 50 | gotenna_packet.ingest_packet(packet) 51 | except Exception as err: 52 | self.my_log.warn(f"Error decoding packet: {err}") 53 | 54 | self.bits = self.bits[(length + 1) * 8:] 55 | idx = self.bits[:-2048].find(self.prefix) 56 | 57 | self.bits = self.bits[-2048 - len(self.prefix) + 1:] 58 | return len(input_items[0]) 59 | -------------------------------------------------------------------------------- /docs/doxygen/doxyxml/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010 Free Software Foundation, Inc. 3 | # 4 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 5 | # This file is a part of gr-tenna 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | # 10 | """ 11 | Python interface to contents of doxygen xml documentation. 12 | 13 | Example use: 14 | See the contents of the example folder for the C++ and 15 | doxygen-generated xml used in this example. 16 | 17 | >>> # Parse the doxygen docs. 18 | >>> import os 19 | >>> this_dir = os.path.dirname(globals()['__file__']) 20 | >>> xml_path = this_dir + "/example/xml/" 21 | >>> di = DoxyIndex(xml_path) 22 | 23 | Get a list of all top-level objects. 24 | 25 | >>> print([mem.name() for mem in di.members()]) 26 | [u'Aadvark', u'aadvarky_enough', u'main'] 27 | 28 | Get all functions. 29 | 30 | >>> print([mem.name() for mem in di.in_category(DoxyFunction)]) 31 | [u'aadvarky_enough', u'main'] 32 | 33 | Check if an object is present. 34 | 35 | >>> di.has_member(u'Aadvark') 36 | True 37 | >>> di.has_member(u'Fish') 38 | False 39 | 40 | Get an item by name and check its properties. 41 | 42 | >>> aad = di.get_member(u'Aadvark') 43 | >>> print(aad.brief_description) 44 | Models the mammal Aadvark. 45 | >>> print(aad.detailed_description) 46 | Sadly the model is incomplete and cannot capture all aspects of an aadvark yet. 47 | 48 | This line is uninformative and is only to test line breaks in the comments. 49 | >>> [mem.name() for mem in aad.members()] 50 | [u'aadvarkness', u'print', u'Aadvark', u'get_aadvarkness'] 51 | >>> aad.get_member(u'print').brief_description 52 | u'Outputs the vital aadvark statistics.' 53 | 54 | """ 55 | 56 | from .doxyindex import DoxyIndex, DoxyFunction, DoxyParam, DoxyClass, DoxyFile, DoxyNamespace, DoxyGroup, DoxyFriend, DoxyOther 57 | 58 | 59 | def _test(): 60 | import os 61 | this_dir = os.path.dirname(globals()['__file__']) 62 | xml_path = this_dir + "/example/xml/" 63 | di = DoxyIndex(xml_path) 64 | # Get the Aadvark class 65 | aad = di.get_member('Aadvark') 66 | aad.brief_description 67 | import doctest 68 | return doctest.testmod() 69 | 70 | 71 | if __name__ == "__main__": 72 | _test() 73 | -------------------------------------------------------------------------------- /docs/doxygen/doxyxml/generated/index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Generated Mon Feb 9 19:08:05 2009 by generateDS.py. 5 | """ 6 | 7 | from xml.dom import minidom 8 | 9 | import os 10 | import sys 11 | from . import compound 12 | 13 | from . import indexsuper as supermod 14 | 15 | 16 | class DoxygenTypeSub(supermod.DoxygenType): 17 | def __init__(self, version=None, compound=None): 18 | supermod.DoxygenType.__init__(self, version, compound) 19 | 20 | def find_compounds_and_members(self, details): 21 | """ 22 | Returns a list of all compounds and their members which match details 23 | """ 24 | 25 | results = [] 26 | for compound in self.compound: 27 | members = compound.find_members(details) 28 | if members: 29 | results.append([compound, members]) 30 | else: 31 | if details.match(compound): 32 | results.append([compound, []]) 33 | 34 | return results 35 | 36 | 37 | supermod.DoxygenType.subclass = DoxygenTypeSub 38 | # end class DoxygenTypeSub 39 | 40 | 41 | class CompoundTypeSub(supermod.CompoundType): 42 | def __init__(self, kind=None, refid=None, name='', member=None): 43 | supermod.CompoundType.__init__(self, kind, refid, name, member) 44 | 45 | def find_members(self, details): 46 | """ 47 | Returns a list of all members which match details 48 | """ 49 | 50 | results = [] 51 | 52 | for member in self.member: 53 | if details.match(member): 54 | results.append(member) 55 | 56 | return results 57 | 58 | 59 | supermod.CompoundType.subclass = CompoundTypeSub 60 | # end class CompoundTypeSub 61 | 62 | 63 | class MemberTypeSub(supermod.MemberType): 64 | 65 | def __init__(self, kind=None, refid=None, name=''): 66 | supermod.MemberType.__init__(self, kind, refid, name) 67 | 68 | 69 | supermod.MemberType.subclass = MemberTypeSub 70 | # end class MemberTypeSub 71 | 72 | 73 | def parse(inFilename): 74 | 75 | doc = minidom.parse(inFilename) 76 | rootNode = doc.documentElement 77 | rootObj = supermod.DoxygenType.factory() 78 | rootObj.build(rootNode) 79 | 80 | return rootObj 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | # 3 | # Copyright 2018-2024 Clayton Smith (argilo@gmail.com) 4 | # 5 | # This file is part of gr-tenna. 6 | # 7 | # gr-tenna is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gr-tenna is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gr-tenna. If not, see . 19 | # 20 | ``` 21 | 22 | gr-tenna 23 | ======== 24 | 25 | The goal of this project is to implement the goTenna Mesh protocol in GNU Radio. 26 | So far there are flow graphs for receiving and transmitting "Shout" messages 27 | using a USRP B200 or HackRF. 28 | 29 | ## Installation 30 | 31 | ``` 32 | mkdir build 33 | cd build 34 | cmake .. 35 | make 36 | sudo make install 37 | ``` 38 | 39 | ## Usage 40 | 41 | To receive messages using a USRP: 42 | ``` 43 | apps/gotenna_rx_usrp.py 44 | ``` 45 | 46 | To receive messages using a HackRF: 47 | ``` 48 | apps/gotenna_rx_hackrf.py 49 | ``` 50 | 51 | To transmit a shout message using a USRP: 52 | ``` 53 | apps/gotenna_tx_usrp.py --app-id=0x3fff --sender-gid=1234567890 --initials=XYZ --message="Hello world!" 54 | ``` 55 | 56 | To transmit a shout message using a HackRF: 57 | ``` 58 | apps/gotenna_tx_hackrf.py --app-id=0x3fff --sender-gid=1234567890 --initials=XYZ --message="Hello world!" 59 | ``` 60 | 61 | ## Protocol Buffers 62 | 63 | If `.proto` files are updated, the corresponding Python code can be regenerated as follows: 64 | 65 | ``` 66 | protoc -Iprotobuf --python_out=python protobuf/gotenna_packet/proto/*.proto 67 | ``` 68 | 69 | ## Credits 70 | 71 | This project builds on reverse engineering work done by Woody [@tb69rr](https://twitter.com/tb69rr) 72 | and Tim [@bjt2n3904](https://twitter.com/bjt2n3904), and presented in the 73 | Wireless Village at DEF CON 25: https://www.youtube.com/watch?v=pKP74WGa_s0 74 | 75 | [`reedsolo.py`](https://github.com/tomerfiliba/reedsolomon) was written by Tomer 76 | Filiba and released into the public domain. 77 | -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011,2012,2016,2018,2019 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | ######################################################################## 10 | # Setup library 11 | ######################################################################## 12 | include(GrPlatform) #define LIB_SUFFIX 13 | 14 | list(APPEND tenna_sources) 15 | 16 | set(tenna_sources 17 | "${tenna_sources}" 18 | PARENT_SCOPE) 19 | if(NOT tenna_sources) 20 | message(STATUS "No C++ sources... skipping lib/") 21 | return() 22 | endif(NOT tenna_sources) 23 | 24 | add_library(gnuradio-tenna SHARED ${tenna_sources}) 25 | target_link_libraries(gnuradio-tenna gnuradio::gnuradio-runtime) 26 | target_include_directories( 27 | gnuradio-tenna 28 | PUBLIC $ 29 | PUBLIC $) 30 | set_target_properties(gnuradio-tenna PROPERTIES DEFINE_SYMBOL "gnuradio_tenna_EXPORTS") 31 | 32 | if(APPLE) 33 | set_target_properties(gnuradio-tenna PROPERTIES INSTALL_NAME_DIR 34 | "${CMAKE_INSTALL_PREFIX}/lib") 35 | endif(APPLE) 36 | 37 | ######################################################################## 38 | # Install built library files 39 | ######################################################################## 40 | include(GrMiscUtils) 41 | gr_library_foo(gnuradio-tenna) 42 | 43 | ######################################################################## 44 | # Print summary 45 | ######################################################################## 46 | message(STATUS "Using install prefix: ${CMAKE_INSTALL_PREFIX}") 47 | message(STATUS "Building for version: ${VERSION} / ${LIBVER}") 48 | 49 | ######################################################################## 50 | # Build and register unit test 51 | ######################################################################## 52 | include(GrTest) 53 | 54 | # If your unit tests require special include paths, add them here 55 | #include_directories() 56 | # List all files that contain Boost.UTF unit tests here 57 | list(APPEND test_tenna_sources) 58 | # Anything we need to link to for the unit tests go here 59 | list(APPEND GR_TEST_TARGET_DEPS gnuradio-tenna) 60 | 61 | if(NOT test_tenna_sources) 62 | message(STATUS "No C++ unit tests... skipping") 63 | return() 64 | endif(NOT test_tenna_sources) 65 | 66 | foreach(qa_file ${test_tenna_sources}) 67 | gr_add_cpp_test("tenna_${qa_file}" ${CMAKE_CURRENT_SOURCE_DIR}/${qa_file}) 68 | endforeach(qa_file) 69 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/data_type_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: gotenna_packet/proto/data_type.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$gotenna_packet/proto/data_type.proto*\xff\x01\n\x11PBMessageDataType\x12\x12\n\x0eGROUP_CREATION\x10\x00\x12\x0e\n\nMAP_OBJECT\x10\x01\x12\x0b\n\x07REQUEST\x10\x02\x12\x08\n\x04TEXT\x10\x03\x12\r\n\tFREQUENCY\x10\x04\x12\x08\n\x04PING\x10\x05\x12\x14\n\x10\x45MERGENCY_BEACON\x10\x06\x12\x16\n\x12PUBLIC_KEY_REQUEST\x10\x07\x12\x17\n\x13PUBLIC_KEY_RESPONSE\x10\x08\x12\x10\n\x0c\x42ROADCAST_QR\x10\t\x12\x07\n\x03PLI\x10\n\x12\x13\n\x0fSHARED_LOCATION\x10\x0b\x12\x0f\n\x0b\x43OMMS_CHECK\x10\x0c\x12\x0e\n\nMANUAL_PLI\x10\r*e\n\x0ePBGeofenceType\x12\x13\n\x0fGEOFENCING_NONE\x10\x00\x12\x14\n\x10GEOFENCING_ENTRY\x10\x01\x12\x13\n\x0fGEOFENCING_EXIT\x10\x02\x12\x13\n\x0fGEOFENCING_BOTH\x10\x03*V\n\x0bPBPowerType\x12\r\n\tHALF_WATT\x10\x00\x12\x0c\n\x08ONE_WATT\x10\x01\x12\x0c\n\x08TWO_WATT\x10\x02\x12\r\n\tFOUR_WATT\x10\x03\x12\r\n\tFIVE_WATT\x10\x04*B\n\x0fPBBandwidthType\x12\x0e\n\nBW_4_84KHZ\x10\x00\x12\x0e\n\nBW_7_28KHZ\x10\x01\x12\x0f\n\x0b\x42W_11_80KHZ\x10\x02*4\n\tPBPinType\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\r\n\tEMERGENCY\x10\x01\x12\x0b\n\x07WARNING\x10\x02*.\n\x12PBConversationType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x43OMMAND\x10\x01\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gotenna_packet.proto.data_type_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_PBMESSAGEDATATYPE']._serialized_start=41 25 | _globals['_PBMESSAGEDATATYPE']._serialized_end=296 26 | _globals['_PBGEOFENCETYPE']._serialized_start=298 27 | _globals['_PBGEOFENCETYPE']._serialized_end=399 28 | _globals['_PBPOWERTYPE']._serialized_start=401 29 | _globals['_PBPOWERTYPE']._serialized_end=487 30 | _globals['_PBBANDWIDTHTYPE']._serialized_start=489 31 | _globals['_PBBANDWIDTHTYPE']._serialized_end=555 32 | _globals['_PBPINTYPE']._serialized_start=557 33 | _globals['_PBPINTYPE']._serialized_end=609 34 | _globals['_PBCONVERSATIONTYPE']._serialized_start=611 35 | _globals['_PBCONVERSATIONTYPE']._serialized_end=657 36 | # @@protoc_insertion_point(module_scope) 37 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/location_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: gotenna_packet/proto/location.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | from gotenna_packet.proto import data_type_pb2 as gotenna__packet_dot_proto_dot_data__type__pb2 16 | 17 | 18 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#gotenna_packet/proto/location.proto\x1a$gotenna_packet/proto/data_type.proto\"\x90\x02\n\x0bPBShapeData\x12\r\n\x05\x63olor\x18\x01 \x01(\r\x12&\n\rgeofence_type\x18\x02 \x01(\x0e\x32\x0f.PBGeofenceType\x12)\n\nroute_data\x18\x04 \x01(\x0b\x32\x13.PBRouteMessageDataH\x00\x12\x31\n\x0eperimeter_data\x18\x05 \x01(\x0b\x32\x17.PBPerimeterMessageDataH\x00\x12+\n\x0b\x63ircle_data\x18\x06 \x01(\x0b\x32\x14.PBCircleMessageDataH\x00\x12\x31\n\x0erectangle_data\x18\x07 \x01(\x0b\x32\x17.PBRectangleMessageDataH\x00\x42\x0c\n\nmap_object\"=\n\tPBPinData\x12\x12\n\ncoordinate\x18\x01 \x01(\x0c\x12\x1c\n\x08pin_type\x18\x02 \x01(\x0e\x32\n.PBPinType\")\n\x12PBRouteMessageData\x12\x13\n\x0b\x64\x61ta_points\x18\x01 \x01(\x0c\"-\n\x16PBPerimeterMessageData\x12\x13\n\x0b\x64\x61ta_points\x18\x01 \x01(\x0c\"5\n\x13PBCircleMessageData\x12\x0e\n\x06\x63\x65nter\x18\x01 \x01(\x0c\x12\x0e\n\x06radius\x18\x02 \x01(\x04\"M\n\x16PBRectangleMessageData\x12\x11\n\tcornerOne\x18\x01 \x01(\x0c\x12\x11\n\tcornerTwo\x18\x02 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x03 \x01(\x0c\"3\n\x0cPBCoordinate\x12\x10\n\x08latitude\x18\x01 \x01(\x01\x12\x11\n\tlongitude\x18\x02 \x01(\x01\x62\x06proto3') 19 | 20 | _globals = globals() 21 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 22 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gotenna_packet.proto.location_pb2', _globals) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | DESCRIPTOR._options = None 25 | _globals['_PBSHAPEDATA']._serialized_start=78 26 | _globals['_PBSHAPEDATA']._serialized_end=350 27 | _globals['_PBPINDATA']._serialized_start=352 28 | _globals['_PBPINDATA']._serialized_end=413 29 | _globals['_PBROUTEMESSAGEDATA']._serialized_start=415 30 | _globals['_PBROUTEMESSAGEDATA']._serialized_end=456 31 | _globals['_PBPERIMETERMESSAGEDATA']._serialized_start=458 32 | _globals['_PBPERIMETERMESSAGEDATA']._serialized_end=503 33 | _globals['_PBCIRCLEMESSAGEDATA']._serialized_start=505 34 | _globals['_PBCIRCLEMESSAGEDATA']._serialized_end=558 35 | _globals['_PBRECTANGLEMESSAGEDATA']._serialized_start=560 36 | _globals['_PBRECTANGLEMESSAGEDATA']._serialized_end=637 37 | _globals['_PBCOORDINATE']._serialized_start=639 38 | _globals['_PBCOORDINATE']._serialized_end=690 39 | # @@protoc_insertion_point(module_scope) 40 | -------------------------------------------------------------------------------- /python/tenna/bindings/header_utils.py: -------------------------------------------------------------------------------- 1 | # Utilities for reading values in header files 2 | 3 | from argparse import ArgumentParser 4 | import re 5 | 6 | 7 | class PybindHeaderParser: 8 | def __init__(self, pathname): 9 | with open(pathname, 'r') as f: 10 | self.file_txt = f.read() 11 | 12 | def get_flag_automatic(self): 13 | # p = re.compile(r'BINDTOOL_GEN_AUTOMATIC\(([^\s])\)') 14 | # m = p.search(self.file_txt) 15 | m = re.search(r'BINDTOOL_GEN_AUTOMATIC\(([^\s])\)', self.file_txt) 16 | if (m and m.group(1) == '1'): 17 | return True 18 | else: 19 | return False 20 | 21 | def get_flag_pygccxml(self): 22 | # p = re.compile(r'BINDTOOL_USE_PYGCCXML\(([^\s])\)') 23 | # m = p.search(self.file_txt) 24 | m = re.search(r'BINDTOOL_USE_PYGCCXML\(([^\s])\)', self.file_txt) 25 | if (m and m.group(1) == '1'): 26 | return True 27 | else: 28 | return False 29 | 30 | def get_header_filename(self): 31 | # p = re.compile(r'BINDTOOL_HEADER_FILE\(([^\s]*)\)') 32 | # m = p.search(self.file_txt) 33 | m = re.search(r'BINDTOOL_HEADER_FILE\(([^\s]*)\)', self.file_txt) 34 | if (m): 35 | return m.group(1) 36 | else: 37 | return None 38 | 39 | def get_header_file_hash(self): 40 | # p = re.compile(r'BINDTOOL_HEADER_FILE_HASH\(([^\s]*)\)') 41 | # m = p.search(self.file_txt) 42 | m = re.search(r'BINDTOOL_HEADER_FILE_HASH\(([^\s]*)\)', self.file_txt) 43 | if (m): 44 | return m.group(1) 45 | else: 46 | return None 47 | 48 | def get_flags(self): 49 | return f'{self.get_flag_automatic()};{self.get_flag_pygccxml()};{self.get_header_filename()};{self.get_header_file_hash()};' 50 | 51 | 52 | def argParse(): 53 | """Parses commandline args.""" 54 | desc = 'Reads the parameters from the comment block in the pybind files' 55 | parser = ArgumentParser(description=desc) 56 | 57 | parser.add_argument("function", help="Operation to perform on comment block of pybind file", choices=[ 58 | "flag_auto", "flag_pygccxml", "header_filename", "header_file_hash", "all"]) 59 | parser.add_argument( 60 | "pathname", help="Pathname of pybind c++ file to read, e.g. blockname_python.cc") 61 | 62 | return parser.parse_args() 63 | 64 | 65 | if __name__ == "__main__": 66 | # Parse command line options and set up doxyxml. 67 | args = argParse() 68 | 69 | pbhp = PybindHeaderParser(args.pathname) 70 | 71 | if args.function == "flag_auto": 72 | print(pbhp.get_flag_automatic()) 73 | elif args.function == "flag_pygccxml": 74 | print(pbhp.get_flag_pygccxml()) 75 | elif args.function == "header_filename": 76 | print(pbhp.get_header_filename()) 77 | elif args.function == "header_file_hash": 78 | print(pbhp.get_header_file_hash()) 79 | elif args.function == "all": 80 | print(pbhp.get_flags()) 81 | -------------------------------------------------------------------------------- /python/test_gotenna_packet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2024 Clayton Smith (argilo@gmail.com) 4 | # 5 | # This file is part of gr-tenna. 6 | # 7 | # gr-tenna is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gr-tenna is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gr-tenna. If not, see . 19 | # 20 | 21 | import base64 22 | import hashlib 23 | import random 24 | import unittest 25 | import gotenna_packet 26 | 27 | 28 | class TestGotennaPacket(unittest.TestCase): 29 | def test_gid_hash(self): 30 | hashes = { 31 | 7339731589953: 0x7c2f, 32 | 91241030429057: 0x2401, 33 | 91251136197976: 0x9b82, 34 | 91341708520410: 0xe39d, 35 | 95681472401029: 0x008c, 36 | 100839877062356: 0x052c, 37 | 243995255577472: 0x907a, 38 | } 39 | 40 | for gid, expected_hash in hashes.items(): 41 | hash = gotenna_packet.gid_hash(gid) 42 | self.assertEqual(hash, expected_hash) 43 | 44 | def test_gid_hash_many(self): 45 | random.seed(1) 46 | h = hashlib.sha256() 47 | for n in range(100000): 48 | gid = random.randrange(0, 2**48) 49 | hash = gotenna_packet.gid_hash(gid) 50 | h.update(bytes([hash >> 8, hash & 0xff])) 51 | self.assertEqual(h.hexdigest(), "f475d7d672eaec0e42ba76a104833df3f9b26667cc45e054f50cda07d4a50219") 52 | 53 | def test_decrypt_qr_message_valid(self): 54 | password = b"tvhoy7z2" 55 | salt = base64.b64decode(b"+tnmT6KSlkxzbDvj37vhLA==") 56 | iv = base64.b64decode(b"VgAAAA==") 57 | sender_gid = 95681472401029 58 | key_data = base64.b64decode(b"r0f4ZohFV5TQQoi/Xt4jNNm8piRZBtU9tVGXVUPSIKX1SJhkGawdjidEQirlPVaFo8JU4VoSV7ui8bCNU+sj6v5PYRmhqItgEbHbX5rQJpw=") 59 | 60 | expected_payload = bytes.fromhex("4121c0677575fa79b55fcd000b650378aaed861e03ad9a16e79a2354d7c831c8") 61 | payload = gotenna_packet.decrypt_qr_message(password, salt, iv, sender_gid, key_data) 62 | self.assertEqual(payload, expected_payload) 63 | 64 | def test_decrypt_qr_message_incorrect_password(self): 65 | password = b"tvhoy7z3" 66 | salt = base64.b64decode(b"+tnmT6KSlkxzbDvj37vhLA==") 67 | iv = base64.b64decode(b"VgAAAA==") 68 | sender_gid = 95681472401029 69 | key_data = base64.b64decode(b"r0f4ZohFV5TQQoi/Xt4jNNm8piRZBtU9tVGXVUPSIKX1SJhkGawdjidEQirlPVaFo8JU4VoSV7ui8bCNU+sj6v5PYRmhqItgEbHbX5rQJpw=") 70 | 71 | with self.assertRaises(Exception): 72 | gotenna_packet.decrypt_qr_message(password, salt, iv, sender_gid, key_data) 73 | 74 | 75 | if __name__ == "__main__": 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /python/gotenna_packet/proto/message_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: gotenna_packet/proto/message.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | from gotenna_packet.proto import location_pb2 as gotenna__packet_dot_proto_dot_location__pb2 16 | from gotenna_packet.proto import frequency_pb2 as gotenna__packet_dot_proto_dot_frequency__pb2 17 | 18 | 19 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"gotenna_packet/proto/message.proto\x1a#gotenna_packet/proto/location.proto\x1a$gotenna_packet/proto/frequency.proto\"\x90\x01\n\x16PBMapObjectMessageData\x12\x11\n\ttimestamp\x18\x01 \x01(\x01\x12\x0c\n\x04uuid\x18\x02 \x01(\x04\x12\r\n\x05title\x18\x03 \x01(\t\x12\x1d\n\x05shape\x18\x04 \x01(\x0b\x32\x0c.PBShapeDataH\x00\x12\x19\n\x03pin\x18\x05 \x01(\x0b\x32\n.PBPinDataH\x00\x42\x0c\n\nmap_object\"\x90\x01\n\x15PBLocationMessageData\x12\x12\n\ncoordinate\x18\x01 \x01(\x0c\x12#\n\x1bpli_sharing_frequency_index\x18\x02 \x01(\r\x12\x1d\n\x15pli_location_accuracy\x18\x03 \x01(\r\x12\x11\n\ttimestamp\x18\x04 \x01(\x01\x12\x0c\n\x04text\x18\x05 \x01(\t\"$\n\x14PBRequestMessageData\x12\x0c\n\x04text\x18\x01 \x01(\t\"/\n\x11PBTextMessageData\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0c\n\x04uuid\x18\x02 \x01(\t\"G\n\x16PBFrequencyMessageData\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x1f\n\tfrequency\x18\x02 \x01(\x0b\x32\x0c.PBFrequency\"\xa9\x01\n\x1aPBGroupCreationMessageData\x12@\n\rgroup_members\x18\x01 \x03(\x0b\x32).PBGroupCreationMessageData.PBGroupMember\x12\x18\n\x10group_shared_key\x18\x02 \x01(\x0c\x12\x11\n\tgroup_gid\x18\x03 \x01(\x04\x1a\x1c\n\rPBGroupMember\x12\x0b\n\x03gid\x18\x01 \x01(\x04\",\n\x16PBPublicKeyMessageData\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"a\n\x18PBBroadcastQrMessageData\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x0c\n\x04uuid\x18\x02 \x01(\x0c\x12\x0c\n\x04salt\x18\x03 \x01(\x0c\x12\n\n\x02iv\x18\x04 \x01(\x0c\x12\x0f\n\x07keyData\x18\x05 \x01(\x0c\x62\x06proto3') 20 | 21 | _globals = globals() 22 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 23 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gotenna_packet.proto.message_pb2', _globals) 24 | if _descriptor._USE_C_DESCRIPTORS == False: 25 | DESCRIPTOR._options = None 26 | _globals['_PBMAPOBJECTMESSAGEDATA']._serialized_start=114 27 | _globals['_PBMAPOBJECTMESSAGEDATA']._serialized_end=258 28 | _globals['_PBLOCATIONMESSAGEDATA']._serialized_start=261 29 | _globals['_PBLOCATIONMESSAGEDATA']._serialized_end=405 30 | _globals['_PBREQUESTMESSAGEDATA']._serialized_start=407 31 | _globals['_PBREQUESTMESSAGEDATA']._serialized_end=443 32 | _globals['_PBTEXTMESSAGEDATA']._serialized_start=445 33 | _globals['_PBTEXTMESSAGEDATA']._serialized_end=492 34 | _globals['_PBFREQUENCYMESSAGEDATA']._serialized_start=494 35 | _globals['_PBFREQUENCYMESSAGEDATA']._serialized_end=565 36 | _globals['_PBGROUPCREATIONMESSAGEDATA']._serialized_start=568 37 | _globals['_PBGROUPCREATIONMESSAGEDATA']._serialized_end=737 38 | _globals['_PBGROUPCREATIONMESSAGEDATA_PBGROUPMEMBER']._serialized_start=709 39 | _globals['_PBGROUPCREATIONMESSAGEDATA_PBGROUPMEMBER']._serialized_end=737 40 | _globals['_PBPUBLICKEYMESSAGEDATA']._serialized_start=739 41 | _globals['_PBPUBLICKEYMESSAGEDATA']._serialized_end=783 42 | _globals['_PBBROADCASTQRMESSAGEDATA']._serialized_start=785 43 | _globals['_PBBROADCASTQRMESSAGEDATA']._serialized_end=882 44 | # @@protoc_insertion_point(module_scope) 45 | -------------------------------------------------------------------------------- /apps/decrypt_shared_preferences.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import xml.etree.ElementTree as ET 5 | import gotenna_packet 6 | from cryptography.hazmat.primitives.asymmetric import ec 7 | from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption 8 | 9 | ASN1_PREFIX_PUBLIC = bytes.fromhex("3076301006072a8648ce3d020106052b81040022036200") 10 | ASN1_PREFIX_PRIVATE = bytes.fromhex("3081b6020100301006072a8648ce3d020106052b8104002204819e30819b0201010430") 11 | 12 | parser = argparse.ArgumentParser(description="Decrypt keys stored in Android SharedPreferences.") 13 | parser.add_argument("filename") 14 | args = parser.parse_args() 15 | 16 | tree = ET.parse(args.filename) 17 | map = tree.getroot() 18 | 19 | # Load keys into a dictionary 20 | keys = {} 21 | for child in map: 22 | name = child.attrib["name"] 23 | key = bytes.fromhex(child.text) 24 | keys[name] = key 25 | 26 | # Look for a known public or private key 27 | target_name = None 28 | target_key = None 29 | for name, key in sorted(keys.items(), key=lambda item: -len(item[1])): 30 | if name.startswith("PRIVATE"): 31 | gid = int(name[7:]) 32 | if gid in gotenna_packet.private_keys: 33 | target_name = name 34 | target_key = gotenna_packet.private_keys[gid] 35 | break 36 | elif name.startswith("PUBLIC_SELF"): 37 | gid = int(name[11:]) 38 | if gid in gotenna_packet.public_keys: 39 | target_name = name 40 | target_key = ASN1_PREFIX_PUBLIC + gotenna_packet.public_keys[gid] 41 | break 42 | elif name.startswith("PUBLIC_OTHER"): 43 | gid = int(name[12:]) 44 | if gid in gotenna_packet.public_keys: 45 | target_name = name 46 | target_key = gotenna_packet.public_keys[gid] 47 | break 48 | 49 | if not target_name: 50 | raise Exception("No known public or private key found") 51 | 52 | known_plaintext = target_key.hex().upper().encode("ASCII") 53 | ciphertext = keys[target_name] 54 | keystream = bytes(p ^ c for p, c in zip(known_plaintext, ciphertext)) 55 | 56 | for name, key in keys.items(): 57 | plaintext = bytes(k ^ c for k, c in zip(keystream, key[:-16])).decode("ASCII").lower() 58 | plaintext_bytes = bytes.fromhex(plaintext) 59 | 60 | if len(keystream) < len(key[:-16]): 61 | plaintext += "?" * (len(key[:-16]) - len(keystream)) 62 | 63 | # Attempt to recover partial private keys 64 | if name.startswith("PRIVATE") and len(plaintext_bytes) >= len(ASN1_PREFIX_PRIVATE) + 48: 65 | gid = int(name[7:]) 66 | if plaintext_bytes[:len(ASN1_PREFIX_PRIVATE)] == ASN1_PREFIX_PRIVATE: 67 | curve = ec.SECP384R1() 68 | private_value = int.from_bytes(plaintext_bytes[len(ASN1_PREFIX_PRIVATE):len(ASN1_PREFIX_PRIVATE) + 48]) 69 | private_key = ec.derive_private_key(private_value, curve) 70 | private_key_bytes = private_key.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption()) 71 | if plaintext_bytes == private_key_bytes[:len(plaintext_bytes)]: 72 | gotenna_packet.private_keys[gid] = private_key_bytes 73 | else: 74 | print(f"Error: Recovered {name} has incorrect prefix") 75 | else: 76 | print(f"Error: {name} has unexpected prefix") 77 | else: 78 | # Got the entire plaintext. Store it for future use. 79 | if name.startswith("PRIVATE"): 80 | gid = int(name[7:]) 81 | gotenna_packet.private_keys[gid] = plaintext_bytes 82 | elif name.startswith("PUBLIC_SELF"): 83 | gid = int(name[11:]) 84 | if plaintext_bytes[:len(ASN1_PREFIX_PUBLIC)] == ASN1_PREFIX_PUBLIC: 85 | gotenna_packet.public_keys[gid] = plaintext_bytes[len(ASN1_PREFIX_PUBLIC):] 86 | else: 87 | print(f"Error: {name} has unexpected prefix") 88 | elif name.startswith("PUBLIC_OTHER"): 89 | gid = int(name[12:]) 90 | gotenna_packet.public_keys[gid] = plaintext_bytes 91 | elif len(name) == 6: 92 | gotenna_packet.broadcast_keys[name] = plaintext_bytes 93 | 94 | print(f"{name}: {plaintext}") 95 | 96 | gotenna_packet.save_keys() 97 | -------------------------------------------------------------------------------- /apps/gotenna_rx_usrp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Rx Usrp 9 | # GNU Radio version: 3.10.9.2 10 | 11 | from gnuradio import analog 12 | import math 13 | from gnuradio import blocks 14 | from gnuradio import digital 15 | from gnuradio import filter 16 | from gnuradio.filter import firdes 17 | from gnuradio import gr 18 | from gnuradio.fft import window 19 | import sys 20 | import signal 21 | from argparse import ArgumentParser 22 | from gnuradio.eng_arg import eng_float, intx 23 | from gnuradio import eng_notation 24 | from gnuradio import tenna 25 | from gnuradio import uhd 26 | import time 27 | 28 | 29 | 30 | 31 | class gotenna_rx_usrp(gr.top_block): 32 | 33 | def __init__(self): 34 | gr.top_block.__init__(self, "Gotenna Rx Usrp", catch_exceptions=True) 35 | 36 | ################################################## 37 | # Variables 38 | ################################################## 39 | self.samp_rate = samp_rate = 32000000 40 | self.fsk_deviation_hz = fsk_deviation_hz = 12500 41 | self.chan_spacing = chan_spacing = 500000 42 | self.baud_rate = baud_rate = 24000 43 | 44 | ################################################## 45 | # Blocks 46 | ################################################## 47 | 48 | self.uhd_usrp_source_0 = uhd.usrp_source( 49 | ",".join(("", '')), 50 | uhd.stream_args( 51 | cpu_format="fc32", 52 | args='', 53 | channels=list(range(0,1)), 54 | ), 55 | ) 56 | self.uhd_usrp_source_0.set_samp_rate(samp_rate) 57 | self.uhd_usrp_source_0.set_time_unknown_pps(uhd.time_spec(0)) 58 | 59 | self.uhd_usrp_source_0.set_center_freq(915000000, 0) 60 | self.uhd_usrp_source_0.set_antenna('TX/RX', 0) 61 | self.uhd_usrp_source_0.set_gain(5, 0) 62 | self.tenna_pdu_to_pcapng_0 = tenna.pdu_to_pcapng('gotenna.pcapng', True) 63 | self.tenna_gotenna_decoder_0 = tenna.gotenna_decoder(0) 64 | self.rational_resampler_xxx_0 = filter.rational_resampler_fff( 65 | interpolation=1, 66 | decimation=4, 67 | taps=[], 68 | fractional_bw=0) 69 | self.digital_symbol_sync_xx_0 = digital.symbol_sync_ff( 70 | digital.TED_MENGALI_AND_DANDREA_GMSK, 71 | (float(chan_spacing) / baud_rate / 4), 72 | 0.05, 73 | 1.5, 74 | 1.0, 75 | (0.001 * float(chan_spacing) / baud_rate / 4), 76 | 1, 77 | digital.constellation_bpsk().base(), 78 | digital.IR_MMSE_8TAP, 79 | 128, 80 | []) 81 | self.digital_binary_slicer_fb_0 = digital.binary_slicer_fb() 82 | self.blocks_keep_one_in_n_0 = blocks.keep_one_in_n(gr.sizeof_gr_complex*1, (samp_rate // chan_spacing)) 83 | self.analog_quadrature_demod_cf_0 = analog.quadrature_demod_cf((chan_spacing/(2*math.pi*fsk_deviation_hz))) 84 | 85 | 86 | ################################################## 87 | # Connections 88 | ################################################## 89 | self.msg_connect((self.tenna_gotenna_decoder_0, 'pdu'), (self.tenna_pdu_to_pcapng_0, 'pdu')) 90 | self.connect((self.analog_quadrature_demod_cf_0, 0), (self.rational_resampler_xxx_0, 0)) 91 | self.connect((self.blocks_keep_one_in_n_0, 0), (self.analog_quadrature_demod_cf_0, 0)) 92 | self.connect((self.digital_binary_slicer_fb_0, 0), (self.tenna_gotenna_decoder_0, 0)) 93 | self.connect((self.digital_symbol_sync_xx_0, 0), (self.digital_binary_slicer_fb_0, 0)) 94 | self.connect((self.rational_resampler_xxx_0, 0), (self.digital_symbol_sync_xx_0, 0)) 95 | self.connect((self.uhd_usrp_source_0, 0), (self.blocks_keep_one_in_n_0, 0)) 96 | 97 | 98 | def get_samp_rate(self): 99 | return self.samp_rate 100 | 101 | def set_samp_rate(self, samp_rate): 102 | self.samp_rate = samp_rate 103 | self.blocks_keep_one_in_n_0.set_n((self.samp_rate // self.chan_spacing)) 104 | self.uhd_usrp_source_0.set_samp_rate(self.samp_rate) 105 | 106 | def get_fsk_deviation_hz(self): 107 | return self.fsk_deviation_hz 108 | 109 | def set_fsk_deviation_hz(self, fsk_deviation_hz): 110 | self.fsk_deviation_hz = fsk_deviation_hz 111 | self.analog_quadrature_demod_cf_0.set_gain((self.chan_spacing/(2*math.pi*self.fsk_deviation_hz))) 112 | 113 | def get_chan_spacing(self): 114 | return self.chan_spacing 115 | 116 | def set_chan_spacing(self, chan_spacing): 117 | self.chan_spacing = chan_spacing 118 | self.analog_quadrature_demod_cf_0.set_gain((self.chan_spacing/(2*math.pi*self.fsk_deviation_hz))) 119 | self.blocks_keep_one_in_n_0.set_n((self.samp_rate // self.chan_spacing)) 120 | self.digital_symbol_sync_xx_0.set_sps((float(self.chan_spacing) / self.baud_rate / 4)) 121 | 122 | def get_baud_rate(self): 123 | return self.baud_rate 124 | 125 | def set_baud_rate(self, baud_rate): 126 | self.baud_rate = baud_rate 127 | self.digital_symbol_sync_xx_0.set_sps((float(self.chan_spacing) / self.baud_rate / 4)) 128 | 129 | 130 | 131 | 132 | def main(top_block_cls=gotenna_rx_usrp, options=None): 133 | tb = top_block_cls() 134 | 135 | def sig_handler(sig=None, frame=None): 136 | tb.stop() 137 | tb.wait() 138 | 139 | sys.exit(0) 140 | 141 | signal.signal(signal.SIGINT, sig_handler) 142 | signal.signal(signal.SIGTERM, sig_handler) 143 | 144 | tb.start() 145 | 146 | try: 147 | input('Press Enter to quit: ') 148 | except EOFError: 149 | pass 150 | tb.stop() 151 | tb.wait() 152 | 153 | 154 | if __name__ == '__main__': 155 | main() 156 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2011-2020 Free Software Foundation, Inc. 2 | # 3 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 4 | # This file is a part of gr-tenna 5 | # 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | 9 | # Select the release build type by default to get optimization flags. 10 | # This has to come before project() which otherwise initializes it. 11 | # Build type can still be overridden by setting -DCMAKE_BUILD_TYPE= 12 | set(CMAKE_BUILD_TYPE 13 | "Release" 14 | CACHE STRING "") 15 | 16 | ######################################################################## 17 | # Project setup 18 | ######################################################################## 19 | cmake_minimum_required(VERSION 3.8) 20 | project(gr-tenna CXX C) 21 | enable_testing() 22 | 23 | # Install to PyBOMBS target prefix if defined 24 | if(DEFINED ENV{PYBOMBS_PREFIX}) 25 | set(CMAKE_INSTALL_PREFIX 26 | $ENV{PYBOMBS_PREFIX} 27 | CACHE PATH "") 28 | message( 29 | STATUS 30 | "PyBOMBS installed GNU Radio. Defaulting CMAKE_INSTALL_PREFIX to $ENV{PYBOMBS_PREFIX}" 31 | ) 32 | endif() 33 | 34 | # Make sure our local CMake Modules path comes first 35 | list(INSERT CMAKE_MODULE_PATH 0 ${PROJECT_SOURCE_DIR}/cmake/Modules) 36 | # Find gnuradio to get access to the cmake modules 37 | find_package(Gnuradio "3.10" REQUIRED) 38 | 39 | # Set the version information here 40 | # cmake-format: off 41 | set(VERSION_MAJOR 1) 42 | set(VERSION_API 0) 43 | set(VERSION_ABI 0) 44 | set(VERSION_PATCH 0) 45 | # cmake-format: on 46 | 47 | cmake_policy(SET CMP0011 NEW) 48 | 49 | # Enable generation of compile_commands.json for code completion engines 50 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 51 | 52 | ######################################################################## 53 | # Minimum Version Requirements 54 | ######################################################################## 55 | 56 | include(GrMinReq) 57 | 58 | ######################################################################## 59 | # Compiler settings 60 | ######################################################################## 61 | 62 | include(GrCompilerSettings) 63 | 64 | ######################################################################## 65 | # Install directories 66 | ######################################################################## 67 | include(GrVersion) 68 | 69 | include(GrPlatform) #define LIB_SUFFIX 70 | 71 | if(NOT CMAKE_MODULES_DIR) 72 | set(CMAKE_MODULES_DIR lib${LIB_SUFFIX}/cmake) 73 | endif(NOT CMAKE_MODULES_DIR) 74 | 75 | set(GR_INCLUDE_DIR include/gnuradio/tenna) 76 | set(GR_CMAKE_DIR ${CMAKE_MODULES_DIR}/gnuradio-tenna) 77 | set(GR_PKG_DATA_DIR ${GR_DATA_DIR}/${CMAKE_PROJECT_NAME}) 78 | set(GR_PKG_DOC_DIR ${GR_DOC_DIR}/${CMAKE_PROJECT_NAME}) 79 | set(GR_PKG_CONF_DIR ${GR_CONF_DIR}/${CMAKE_PROJECT_NAME}/conf.d) 80 | set(GR_PKG_LIBEXEC_DIR ${GR_LIBEXEC_DIR}/${CMAKE_PROJECT_NAME}) 81 | 82 | ######################################################################## 83 | # On Apple only, set install name and use rpath correctly, if not already set 84 | ######################################################################## 85 | if(APPLE) 86 | if(NOT CMAKE_INSTALL_NAME_DIR) 87 | set(CMAKE_INSTALL_NAME_DIR 88 | ${CMAKE_INSTALL_PREFIX}/${GR_LIBRARY_DIR} 89 | CACHE PATH "Library Install Name Destination Directory" FORCE) 90 | endif(NOT CMAKE_INSTALL_NAME_DIR) 91 | if(NOT CMAKE_INSTALL_RPATH) 92 | set(CMAKE_INSTALL_RPATH 93 | ${CMAKE_INSTALL_PREFIX}/${GR_LIBRARY_DIR} 94 | CACHE PATH "Library Install RPath" FORCE) 95 | endif(NOT CMAKE_INSTALL_RPATH) 96 | if(NOT CMAKE_BUILD_WITH_INSTALL_RPATH) 97 | set(CMAKE_BUILD_WITH_INSTALL_RPATH 98 | ON 99 | CACHE BOOL "Do Build Using Library Install RPath" FORCE) 100 | endif(NOT CMAKE_BUILD_WITH_INSTALL_RPATH) 101 | endif(APPLE) 102 | 103 | ######################################################################## 104 | # Find gnuradio build dependencies 105 | ######################################################################## 106 | find_package(Doxygen) 107 | 108 | ######################################################################## 109 | # Setup doxygen option 110 | ######################################################################## 111 | if(DOXYGEN_FOUND) 112 | option(ENABLE_DOXYGEN "Build docs using Doxygen" ON) 113 | else(DOXYGEN_FOUND) 114 | option(ENABLE_DOXYGEN "Build docs using Doxygen" OFF) 115 | endif(DOXYGEN_FOUND) 116 | 117 | ######################################################################## 118 | # Create uninstall target 119 | ######################################################################## 120 | configure_file(${PROJECT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in 121 | ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake @ONLY) 122 | 123 | add_custom_target(uninstall ${CMAKE_COMMAND} -P 124 | ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) 125 | 126 | ######################################################################## 127 | # Add subdirectories 128 | ######################################################################## 129 | add_subdirectory(include/gnuradio/tenna) 130 | add_subdirectory(lib) 131 | add_subdirectory(apps) 132 | add_subdirectory(docs) 133 | # NOTE: manually update below to use GRC to generate C++ flowgraphs w/o python 134 | if(ENABLE_PYTHON) 135 | message(STATUS "PYTHON and GRC components are enabled") 136 | add_subdirectory(python/tenna) 137 | add_subdirectory(python/gotenna_packet) 138 | add_subdirectory(grc) 139 | else(ENABLE_PYTHON) 140 | message(STATUS "PYTHON and GRC components are disabled") 141 | endif(ENABLE_PYTHON) 142 | 143 | ######################################################################## 144 | # Install cmake search helper for this library 145 | ######################################################################## 146 | 147 | install(FILES cmake/Modules/gnuradio-tennaConfig.cmake DESTINATION ${GR_CMAKE_DIR}) 148 | 149 | include(CMakePackageConfigHelpers) 150 | configure_package_config_file( 151 | ${PROJECT_SOURCE_DIR}/cmake/Modules/targetConfig.cmake.in 152 | ${CMAKE_CURRENT_BINARY_DIR}/cmake/Modules/${target}Config.cmake 153 | INSTALL_DESTINATION ${GR_CMAKE_DIR}) 154 | -------------------------------------------------------------------------------- /cmake/Modules/CMakeParseArgumentsCopy.cmake: -------------------------------------------------------------------------------- 1 | # CMAKE_PARSE_ARGUMENTS( args...) 2 | # 3 | # CMAKE_PARSE_ARGUMENTS() is intended to be used in macros or functions for 4 | # parsing the arguments given to that macro or function. 5 | # It processes the arguments and defines a set of variables which hold the 6 | # values of the respective options. 7 | # 8 | # The argument contains all options for the respective macro, 9 | # i.e. keywords which can be used when calling the macro without any value 10 | # following, like e.g. the OPTIONAL keyword of the install() command. 11 | # 12 | # The argument contains all keywords for this macro 13 | # which are followed by one value, like e.g. DESTINATION keyword of the 14 | # install() command. 15 | # 16 | # The argument contains all keywords for this macro 17 | # which can be followed by more than one value, like e.g. the TARGETS or 18 | # FILES keywords of the install() command. 19 | # 20 | # When done, CMAKE_PARSE_ARGUMENTS() will have defined for each of the 21 | # keywords listed in , and 22 | # a variable composed of the given 23 | # followed by "_" and the name of the respective keyword. 24 | # These variables will then hold the respective value from the argument list. 25 | # For the keywords this will be TRUE or FALSE. 26 | # 27 | # All remaining arguments are collected in a variable 28 | # _UNPARSED_ARGUMENTS, this can be checked afterwards to see whether 29 | # your macro was called with unrecognized parameters. 30 | # 31 | # As an example here a my_install() macro, which takes similar arguments as the 32 | # real install() command: 33 | # 34 | # function(MY_INSTALL) 35 | # set(options OPTIONAL FAST) 36 | # set(oneValueArgs DESTINATION RENAME) 37 | # set(multiValueArgs TARGETS CONFIGURATIONS) 38 | # cmake_parse_arguments(MY_INSTALL "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) 39 | # ... 40 | # 41 | # Assume my_install() has been called like this: 42 | # my_install(TARGETS foo bar DESTINATION bin OPTIONAL blub) 43 | # 44 | # After the cmake_parse_arguments() call the macro will have set the following 45 | # variables: 46 | # MY_INSTALL_OPTIONAL = TRUE 47 | # MY_INSTALL_FAST = FALSE (this option was not used when calling my_install() 48 | # MY_INSTALL_DESTINATION = "bin" 49 | # MY_INSTALL_RENAME = "" (was not used) 50 | # MY_INSTALL_TARGETS = "foo;bar" 51 | # MY_INSTALL_CONFIGURATIONS = "" (was not used) 52 | # MY_INSTALL_UNPARSED_ARGUMENTS = "blub" (no value expected after "OPTIONAL" 53 | # 54 | # You can the continue and process these variables. 55 | # 56 | # Keywords terminate lists of values, e.g. if directly after a one_value_keyword 57 | # another recognized keyword follows, this is interpreted as the beginning of 58 | # the new option. 59 | # E.g. my_install(TARGETS foo DESTINATION OPTIONAL) would result in 60 | # MY_INSTALL_DESTINATION set to "OPTIONAL", but MY_INSTALL_DESTINATION would 61 | # be empty and MY_INSTALL_OPTIONAL would be set to TRUE therefore. 62 | 63 | #============================================================================= 64 | # Copyright 2010 Alexander Neundorf 65 | # 66 | # Distributed under the OSI-approved BSD License (the "License"); 67 | # see accompanying file Copyright.txt for details. 68 | # 69 | # This software is distributed WITHOUT ANY WARRANTY; without even the 70 | # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 71 | # See the License for more information. 72 | #============================================================================= 73 | # (To distribute this file outside of CMake, substitute the full 74 | # License text for the above reference.) 75 | 76 | 77 | if(__CMAKE_PARSE_ARGUMENTS_INCLUDED) 78 | return() 79 | endif() 80 | set(__CMAKE_PARSE_ARGUMENTS_INCLUDED TRUE) 81 | 82 | 83 | function(CMAKE_PARSE_ARGUMENTS prefix _optionNames _singleArgNames _multiArgNames) 84 | # first set all result variables to empty/FALSE 85 | foreach(arg_name ${_singleArgNames} ${_multiArgNames}) 86 | set(${prefix}_${arg_name}) 87 | endforeach(arg_name) 88 | 89 | foreach(option ${_optionNames}) 90 | set(${prefix}_${option} FALSE) 91 | endforeach(option) 92 | 93 | set(${prefix}_UNPARSED_ARGUMENTS) 94 | 95 | set(insideValues FALSE) 96 | set(currentArgName) 97 | 98 | # now iterate over all arguments and fill the result variables 99 | foreach(currentArg ${ARGN}) 100 | list(FIND _optionNames "${currentArg}" optionIndex) # ... then this marks the end of the arguments belonging to this keyword 101 | list(FIND _singleArgNames "${currentArg}" singleArgIndex) # ... then this marks the end of the arguments belonging to this keyword 102 | list(FIND _multiArgNames "${currentArg}" multiArgIndex) # ... then this marks the end of the arguments belonging to this keyword 103 | 104 | if(${optionIndex} EQUAL -1 AND ${singleArgIndex} EQUAL -1 AND ${multiArgIndex} EQUAL -1) 105 | if(insideValues) 106 | if("${insideValues}" STREQUAL "SINGLE") 107 | set(${prefix}_${currentArgName} ${currentArg}) 108 | set(insideValues FALSE) 109 | elseif("${insideValues}" STREQUAL "MULTI") 110 | list(APPEND ${prefix}_${currentArgName} ${currentArg}) 111 | endif() 112 | else(insideValues) 113 | list(APPEND ${prefix}_UNPARSED_ARGUMENTS ${currentArg}) 114 | endif(insideValues) 115 | else() 116 | if(NOT ${optionIndex} EQUAL -1) 117 | set(${prefix}_${currentArg} TRUE) 118 | set(insideValues FALSE) 119 | elseif(NOT ${singleArgIndex} EQUAL -1) 120 | set(currentArgName ${currentArg}) 121 | set(${prefix}_${currentArgName}) 122 | set(insideValues "SINGLE") 123 | elseif(NOT ${multiArgIndex} EQUAL -1) 124 | set(currentArgName ${currentArg}) 125 | set(${prefix}_${currentArgName}) 126 | set(insideValues "MULTI") 127 | endif() 128 | endif() 129 | 130 | endforeach(currentArg) 131 | 132 | # propagate the result variables to the caller: 133 | foreach(arg_name ${_singleArgNames} ${_multiArgNames} ${_optionNames}) 134 | set(${prefix}_${arg_name} ${${prefix}_${arg_name}} PARENT_SCOPE) 135 | endforeach(arg_name) 136 | set(${prefix}_UNPARSED_ARGUMENTS ${${prefix}_UNPARSED_ARGUMENTS} PARENT_SCOPE) 137 | 138 | endfunction(CMAKE_PARSE_ARGUMENTS _options _singleArgs _multiArgs) 139 | -------------------------------------------------------------------------------- /docs/doxygen/doxyxml/base.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010 Free Software Foundation, Inc. 3 | # 4 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 5 | # This file is a part of gr-tenna 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | # 10 | """ 11 | A base class is created. 12 | 13 | Classes based upon this are used to make more user-friendly interfaces 14 | to the doxygen xml docs than the generated classes provide. 15 | """ 16 | 17 | import os 18 | import pdb 19 | 20 | from xml.parsers.expat import ExpatError 21 | 22 | from .generated import compound 23 | 24 | 25 | class Base(object): 26 | 27 | class Duplicate(Exception): 28 | pass 29 | 30 | class NoSuchMember(Exception): 31 | pass 32 | 33 | class ParsingError(Exception): 34 | pass 35 | 36 | def __init__(self, parse_data, top=None): 37 | self._parsed = False 38 | self._error = False 39 | self._parse_data = parse_data 40 | self._members = [] 41 | self._dict_members = {} 42 | self._in_category = {} 43 | self._data = {} 44 | if top is not None: 45 | self._xml_path = top._xml_path 46 | # Set up holder of references 47 | else: 48 | top = self 49 | self._refs = {} 50 | self._xml_path = parse_data 51 | self.top = top 52 | 53 | @classmethod 54 | def from_refid(cls, refid, top=None): 55 | """ Instantiate class from a refid rather than parsing object. """ 56 | # First check to see if its already been instantiated. 57 | if top is not None and refid in top._refs: 58 | return top._refs[refid] 59 | # Otherwise create a new instance and set refid. 60 | inst = cls(None, top=top) 61 | inst.refid = refid 62 | inst.add_ref(inst) 63 | return inst 64 | 65 | @classmethod 66 | def from_parse_data(cls, parse_data, top=None): 67 | refid = getattr(parse_data, 'refid', None) 68 | if refid is not None and top is not None and refid in top._refs: 69 | return top._refs[refid] 70 | inst = cls(parse_data, top=top) 71 | if refid is not None: 72 | inst.refid = refid 73 | inst.add_ref(inst) 74 | return inst 75 | 76 | def add_ref(self, obj): 77 | if hasattr(obj, 'refid'): 78 | self.top._refs[obj.refid] = obj 79 | 80 | mem_classes = [] 81 | 82 | def get_cls(self, mem): 83 | for cls in self.mem_classes: 84 | if cls.can_parse(mem): 85 | return cls 86 | raise Exception(("Did not find a class for object '%s'." 87 | % (mem.get_name()))) 88 | 89 | def convert_mem(self, mem): 90 | try: 91 | cls = self.get_cls(mem) 92 | converted = cls.from_parse_data(mem, self.top) 93 | if converted is None: 94 | raise Exception('No class matched this object.') 95 | self.add_ref(converted) 96 | return converted 97 | except Exception as e: 98 | print(e) 99 | 100 | @classmethod 101 | def includes(cls, inst): 102 | return isinstance(inst, cls) 103 | 104 | @classmethod 105 | def can_parse(cls, obj): 106 | return False 107 | 108 | def _parse(self): 109 | self._parsed = True 110 | 111 | def _get_dict_members(self, cat=None): 112 | """ 113 | For given category a dictionary is returned mapping member names to 114 | members of that category. For names that are duplicated the name is 115 | mapped to None. 116 | """ 117 | self.confirm_no_error() 118 | if cat not in self._dict_members: 119 | new_dict = {} 120 | for mem in self.in_category(cat): 121 | if mem.name() not in new_dict: 122 | new_dict[mem.name()] = mem 123 | else: 124 | new_dict[mem.name()] = self.Duplicate 125 | self._dict_members[cat] = new_dict 126 | return self._dict_members[cat] 127 | 128 | def in_category(self, cat): 129 | self.confirm_no_error() 130 | if cat is None: 131 | return self._members 132 | if cat not in self._in_category: 133 | self._in_category[cat] = [mem for mem in self._members 134 | if cat.includes(mem)] 135 | return self._in_category[cat] 136 | 137 | def get_member(self, name, cat=None): 138 | self.confirm_no_error() 139 | # Check if it's in a namespace or class. 140 | bits = name.split('::') 141 | first = bits[0] 142 | rest = '::'.join(bits[1:]) 143 | member = self._get_dict_members(cat).get(first, self.NoSuchMember) 144 | # Raise any errors that are returned. 145 | if member in set([self.NoSuchMember, self.Duplicate]): 146 | raise member() 147 | if rest: 148 | return member.get_member(rest, cat=cat) 149 | return member 150 | 151 | def has_member(self, name, cat=None): 152 | try: 153 | mem = self.get_member(name, cat=cat) 154 | return True 155 | except self.NoSuchMember: 156 | return False 157 | 158 | def data(self): 159 | self.confirm_no_error() 160 | return self._data 161 | 162 | def members(self): 163 | self.confirm_no_error() 164 | return self._members 165 | 166 | def process_memberdefs(self): 167 | mdtss = [] 168 | for sec in self._retrieved_data.compounddef.sectiondef: 169 | mdtss += sec.memberdef 170 | # At the moment we lose all information associated with sections. 171 | # Sometimes a memberdef is in several sectiondef. 172 | # We make sure we don't get duplicates here. 173 | uniques = set([]) 174 | for mem in mdtss: 175 | converted = self.convert_mem(mem) 176 | pair = (mem.name, mem.__class__) 177 | if pair not in uniques: 178 | uniques.add(pair) 179 | self._members.append(converted) 180 | 181 | def retrieve_data(self): 182 | filename = os.path.join(self._xml_path, self.refid + '.xml') 183 | try: 184 | self._retrieved_data = compound.parse(filename) 185 | except ExpatError: 186 | print('Error in xml in file %s' % filename) 187 | self._error = True 188 | self._retrieved_data = None 189 | 190 | def check_parsed(self): 191 | if not self._parsed: 192 | self._parse() 193 | 194 | def confirm_no_error(self): 195 | self.check_parsed() 196 | if self._error: 197 | raise self.ParsingError() 198 | 199 | def error(self): 200 | self.check_parsed() 201 | return self._error 202 | 203 | def name(self): 204 | # first see if we can do it without processing. 205 | if self._parse_data is not None: 206 | return self._parse_data.name 207 | self.check_parsed() 208 | return self._retrieved_data.compounddef.name 209 | -------------------------------------------------------------------------------- /apps/gotenna_rx_hackrf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Rx Hackrf 9 | # GNU Radio version: 3.10.9.2 10 | 11 | from gnuradio import analog 12 | import math 13 | from gnuradio import blocks 14 | from gnuradio import digital 15 | from gnuradio import filter 16 | from gnuradio.filter import firdes 17 | from gnuradio import gr 18 | from gnuradio.fft import window 19 | import sys 20 | import signal 21 | from argparse import ArgumentParser 22 | from gnuradio.eng_arg import eng_float, intx 23 | from gnuradio import eng_notation 24 | from gnuradio import soapy 25 | from gnuradio import tenna 26 | 27 | 28 | 29 | 30 | class gotenna_rx_hackrf(gr.top_block): 31 | 32 | def __init__(self): 33 | gr.top_block.__init__(self, "Gotenna Rx Hackrf", catch_exceptions=True) 34 | 35 | ################################################## 36 | # Variables 37 | ################################################## 38 | self.chan_spacing = chan_spacing = 500000 39 | self.samp_rate = samp_rate = chan_spacing * 2 40 | self.offset = offset = chan_spacing // 2 41 | self.lp_taps = lp_taps = firdes.low_pass(1.0, chan_spacing, 75000,50000, window.WIN_HAMMING, 6.76) 42 | self.fsk_deviation_hz = fsk_deviation_hz = 12500 43 | self.center_freq = center_freq = 915000000 44 | self.baud_rate = baud_rate = 24000 45 | 46 | ################################################## 47 | # Blocks 48 | ################################################## 49 | 50 | self.tenna_pdu_to_pcapng_0 = tenna.pdu_to_pcapng('gotenna.pcapng', True) 51 | self.tenna_gotenna_decoder_0 = tenna.gotenna_decoder(0) 52 | self.soapy_hackrf_source_0 = None 53 | dev = 'driver=hackrf' 54 | stream_args = '' 55 | tune_args = [''] 56 | settings = [''] 57 | 58 | self.soapy_hackrf_source_0 = soapy.source(dev, "fc32", 1, '', 59 | stream_args, tune_args, settings) 60 | self.soapy_hackrf_source_0.set_sample_rate(0, samp_rate) 61 | self.soapy_hackrf_source_0.set_bandwidth(0, 28000000) 62 | self.soapy_hackrf_source_0.set_frequency(0, (center_freq - offset)) 63 | self.soapy_hackrf_source_0.set_gain(0, 'AMP', False) 64 | self.soapy_hackrf_source_0.set_gain(0, 'LNA', min(max(16, 0.0), 40.0)) 65 | self.soapy_hackrf_source_0.set_gain(0, 'VGA', min(max(16, 0.0), 62.0)) 66 | self.rational_resampler_xxx_0 = filter.rational_resampler_fff( 67 | interpolation=1, 68 | decimation=4, 69 | taps=[], 70 | fractional_bw=0) 71 | self.freq_xlating_fir_filter_xxx_0 = filter.freq_xlating_fir_filter_ccc(1, lp_taps, offset, chan_spacing) 72 | self.digital_symbol_sync_xx_0 = digital.symbol_sync_ff( 73 | digital.TED_MENGALI_AND_DANDREA_GMSK, 74 | (float(chan_spacing) / baud_rate / 4), 75 | 0.05, 76 | 1.5, 77 | 1.0, 78 | (0.001 * float(chan_spacing) / baud_rate / 4), 79 | 1, 80 | digital.constellation_bpsk().base(), 81 | digital.IR_MMSE_8TAP, 82 | 128, 83 | []) 84 | self.digital_binary_slicer_fb_0 = digital.binary_slicer_fb() 85 | self.dc_blocker_xx_0 = filter.dc_blocker_ff(1024, False) 86 | self.blocks_keep_one_in_n_0 = blocks.keep_one_in_n(gr.sizeof_gr_complex*1, (samp_rate // chan_spacing)) 87 | self.analog_quadrature_demod_cf_0 = analog.quadrature_demod_cf((chan_spacing/(2*math.pi*fsk_deviation_hz))) 88 | 89 | 90 | ################################################## 91 | # Connections 92 | ################################################## 93 | self.msg_connect((self.tenna_gotenna_decoder_0, 'pdu'), (self.tenna_pdu_to_pcapng_0, 'pdu')) 94 | self.connect((self.analog_quadrature_demod_cf_0, 0), (self.dc_blocker_xx_0, 0)) 95 | self.connect((self.blocks_keep_one_in_n_0, 0), (self.freq_xlating_fir_filter_xxx_0, 0)) 96 | self.connect((self.dc_blocker_xx_0, 0), (self.rational_resampler_xxx_0, 0)) 97 | self.connect((self.digital_binary_slicer_fb_0, 0), (self.tenna_gotenna_decoder_0, 0)) 98 | self.connect((self.digital_symbol_sync_xx_0, 0), (self.digital_binary_slicer_fb_0, 0)) 99 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.analog_quadrature_demod_cf_0, 0)) 100 | self.connect((self.rational_resampler_xxx_0, 0), (self.digital_symbol_sync_xx_0, 0)) 101 | self.connect((self.soapy_hackrf_source_0, 0), (self.blocks_keep_one_in_n_0, 0)) 102 | 103 | 104 | def get_chan_spacing(self): 105 | return self.chan_spacing 106 | 107 | def set_chan_spacing(self, chan_spacing): 108 | self.chan_spacing = chan_spacing 109 | self.set_lp_taps(firdes.low_pass(1.0, self.chan_spacing, 75000, 50000, window.WIN_HAMMING, 6.76)) 110 | self.set_offset(self.chan_spacing // 2) 111 | self.set_samp_rate(self.chan_spacing * 2) 112 | self.analog_quadrature_demod_cf_0.set_gain((self.chan_spacing/(2*math.pi*self.fsk_deviation_hz))) 113 | self.blocks_keep_one_in_n_0.set_n((self.samp_rate // self.chan_spacing)) 114 | self.digital_symbol_sync_xx_0.set_sps((float(self.chan_spacing) / self.baud_rate / 4)) 115 | 116 | def get_samp_rate(self): 117 | return self.samp_rate 118 | 119 | def set_samp_rate(self, samp_rate): 120 | self.samp_rate = samp_rate 121 | self.blocks_keep_one_in_n_0.set_n((self.samp_rate // self.chan_spacing)) 122 | self.soapy_hackrf_source_0.set_sample_rate(0, self.samp_rate) 123 | 124 | def get_offset(self): 125 | return self.offset 126 | 127 | def set_offset(self, offset): 128 | self.offset = offset 129 | self.freq_xlating_fir_filter_xxx_0.set_center_freq(self.offset) 130 | self.soapy_hackrf_source_0.set_frequency(0, (self.center_freq - self.offset)) 131 | 132 | def get_lp_taps(self): 133 | return self.lp_taps 134 | 135 | def set_lp_taps(self, lp_taps): 136 | self.lp_taps = lp_taps 137 | self.freq_xlating_fir_filter_xxx_0.set_taps(self.lp_taps) 138 | 139 | def get_fsk_deviation_hz(self): 140 | return self.fsk_deviation_hz 141 | 142 | def set_fsk_deviation_hz(self, fsk_deviation_hz): 143 | self.fsk_deviation_hz = fsk_deviation_hz 144 | self.analog_quadrature_demod_cf_0.set_gain((self.chan_spacing/(2*math.pi*self.fsk_deviation_hz))) 145 | 146 | def get_center_freq(self): 147 | return self.center_freq 148 | 149 | def set_center_freq(self, center_freq): 150 | self.center_freq = center_freq 151 | self.soapy_hackrf_source_0.set_frequency(0, (self.center_freq - self.offset)) 152 | 153 | def get_baud_rate(self): 154 | return self.baud_rate 155 | 156 | def set_baud_rate(self, baud_rate): 157 | self.baud_rate = baud_rate 158 | self.digital_symbol_sync_xx_0.set_sps((float(self.chan_spacing) / self.baud_rate / 4)) 159 | 160 | 161 | 162 | 163 | def main(top_block_cls=gotenna_rx_hackrf, options=None): 164 | tb = top_block_cls() 165 | 166 | def sig_handler(sig=None, frame=None): 167 | tb.stop() 168 | tb.wait() 169 | 170 | sys.exit(0) 171 | 172 | signal.signal(signal.SIGINT, sig_handler) 173 | signal.signal(signal.SIGTERM, sig_handler) 174 | 175 | tb.start() 176 | 177 | try: 178 | input('Press Enter to quit: ') 179 | except EOFError: 180 | pass 181 | tb.stop() 182 | tb.wait() 183 | 184 | 185 | if __name__ == '__main__': 186 | main() 187 | -------------------------------------------------------------------------------- /apps/gotenna_rx_hackrf.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | catch_exceptions: 'True' 5 | category: '[GRC Hier Blocks]' 6 | cmake_opt: '' 7 | comment: '' 8 | copyright: '' 9 | description: '' 10 | gen_cmake: 'On' 11 | gen_linking: dynamic 12 | generate_options: no_gui 13 | hier_block_src_path: '.:' 14 | id: gotenna_rx_hackrf 15 | max_nouts: '0' 16 | output_language: python 17 | placement: (0,0) 18 | qt_qss_theme: '' 19 | realtime_scheduling: '' 20 | run: 'True' 21 | run_command: '{python} -u {filename}' 22 | run_options: prompt 23 | sizing_mode: fixed 24 | thread_safe_setters: '' 25 | title: '' 26 | window_size: '' 27 | states: 28 | bus_sink: false 29 | bus_source: false 30 | bus_structure: null 31 | coordinate: [8, 12.0] 32 | rotation: 0 33 | state: enabled 34 | 35 | blocks: 36 | - name: baud_rate 37 | id: variable 38 | parameters: 39 | comment: '' 40 | value: '24000' 41 | states: 42 | bus_sink: false 43 | bus_source: false 44 | bus_structure: null 45 | coordinate: [8, 420.0] 46 | rotation: 0 47 | state: enabled 48 | - name: center_freq 49 | id: variable 50 | parameters: 51 | comment: '' 52 | value: '915000000' 53 | states: 54 | bus_sink: false 55 | bus_source: false 56 | bus_structure: null 57 | coordinate: [8, 164.0] 58 | rotation: 0 59 | state: true 60 | - name: chan_spacing 61 | id: variable 62 | parameters: 63 | comment: '' 64 | value: '500000' 65 | states: 66 | bus_sink: false 67 | bus_source: false 68 | bus_structure: null 69 | coordinate: [8, 228.0] 70 | rotation: 0 71 | state: enabled 72 | - name: fsk_deviation_hz 73 | id: variable 74 | parameters: 75 | comment: '' 76 | value: '12500' 77 | states: 78 | bus_sink: false 79 | bus_source: false 80 | bus_structure: null 81 | coordinate: [8, 356.0] 82 | rotation: 0 83 | state: enabled 84 | - name: lp_taps 85 | id: variable_low_pass_filter_taps 86 | parameters: 87 | beta: '6.76' 88 | comment: '' 89 | cutoff_freq: '75000' 90 | gain: '1.0' 91 | samp_rate: chan_spacing 92 | value: '' 93 | width: '50000' 94 | win: window.WIN_HAMMING 95 | states: 96 | bus_sink: false 97 | bus_source: false 98 | bus_structure: null 99 | coordinate: [640, 20.0] 100 | rotation: 0 101 | state: true 102 | - name: offset 103 | id: variable 104 | parameters: 105 | comment: '' 106 | value: chan_spacing // 2 107 | states: 108 | bus_sink: false 109 | bus_source: false 110 | bus_structure: null 111 | coordinate: [8, 292.0] 112 | rotation: 0 113 | state: true 114 | - name: samp_rate 115 | id: variable 116 | parameters: 117 | comment: '' 118 | value: chan_spacing * 2 119 | states: 120 | bus_sink: false 121 | bus_source: false 122 | bus_structure: null 123 | coordinate: [8, 100.0] 124 | rotation: 0 125 | state: enabled 126 | - name: analog_quadrature_demod_cf_0 127 | id: analog_quadrature_demod_cf 128 | parameters: 129 | affinity: '' 130 | alias: '' 131 | comment: '' 132 | gain: chan_spacing/(2*math.pi*fsk_deviation_hz) 133 | maxoutbuf: '0' 134 | minoutbuf: '0' 135 | states: 136 | bus_sink: false 137 | bus_source: false 138 | bus_structure: null 139 | coordinate: [440, 196.0] 140 | rotation: 180 141 | state: enabled 142 | - name: blocks_keep_one_in_n_0 143 | id: blocks_keep_one_in_n 144 | parameters: 145 | affinity: '' 146 | alias: '' 147 | comment: '' 148 | maxoutbuf: '0' 149 | minoutbuf: '0' 150 | n: samp_rate // chan_spacing 151 | type: complex 152 | vlen: '1' 153 | states: 154 | bus_sink: false 155 | bus_source: false 156 | bus_structure: null 157 | coordinate: [920, 184.0] 158 | rotation: 180 159 | state: enabled 160 | - name: dc_blocker_xx_0 161 | id: dc_blocker_xx 162 | parameters: 163 | affinity: '' 164 | alias: '' 165 | comment: '' 166 | length: '1024' 167 | long_form: 'False' 168 | maxoutbuf: '0' 169 | minoutbuf: '0' 170 | type: ff 171 | states: 172 | bus_sink: false 173 | bus_source: false 174 | bus_structure: null 175 | coordinate: [240, 188.0] 176 | rotation: 180 177 | state: enabled 178 | - name: digital_binary_slicer_fb_0 179 | id: digital_binary_slicer_fb 180 | parameters: 181 | affinity: '' 182 | alias: '' 183 | comment: '' 184 | maxoutbuf: '0' 185 | minoutbuf: '0' 186 | states: 187 | bus_sink: false 188 | bus_source: false 189 | bus_structure: null 190 | coordinate: [808, 304.0] 191 | rotation: 0 192 | state: enabled 193 | - name: digital_symbol_sync_xx_0 194 | id: digital_symbol_sync_xx 195 | parameters: 196 | affinity: '' 197 | alias: '' 198 | comment: '' 199 | constellation: digital.constellation_bpsk().base() 200 | damping: '1.5' 201 | loop_bw: '0.05' 202 | max_dev: 0.001 * float(chan_spacing) / baud_rate / 4 203 | maxoutbuf: '0' 204 | minoutbuf: '0' 205 | nfilters: '128' 206 | osps: '1' 207 | pfb_mf_taps: '[]' 208 | resamp_type: digital.IR_MMSE_8TAP 209 | sps: float(chan_spacing) / baud_rate / 4 210 | ted_gain: '1.0' 211 | ted_type: digital.TED_MENGALI_AND_DANDREA_GMSK 212 | type: ff 213 | states: 214 | bus_sink: false 215 | bus_source: false 216 | bus_structure: null 217 | coordinate: [416, 292.0] 218 | rotation: 0 219 | state: enabled 220 | - name: freq_xlating_fir_filter_xxx_0 221 | id: freq_xlating_fir_filter_xxx 222 | parameters: 223 | affinity: '' 224 | alias: '' 225 | center_freq: offset 226 | comment: '' 227 | decim: '1' 228 | maxoutbuf: '0' 229 | minoutbuf: '0' 230 | samp_rate: chan_spacing 231 | taps: lp_taps 232 | type: ccc 233 | states: 234 | bus_sink: false 235 | bus_source: false 236 | bus_structure: null 237 | coordinate: [640, 172.0] 238 | rotation: 180 239 | state: true 240 | - name: import_0 241 | id: import 242 | parameters: 243 | alias: '' 244 | comment: '' 245 | imports: import math 246 | states: 247 | bus_sink: false 248 | bus_source: false 249 | bus_structure: null 250 | coordinate: [184, 12.0] 251 | rotation: 0 252 | state: true 253 | - name: rational_resampler_xxx_0 254 | id: rational_resampler_xxx 255 | parameters: 256 | affinity: '' 257 | alias: '' 258 | comment: '' 259 | decim: '4' 260 | fbw: '0' 261 | interp: '1' 262 | maxoutbuf: '0' 263 | minoutbuf: '0' 264 | taps: '' 265 | type: fff 266 | states: 267 | bus_sink: false 268 | bus_source: false 269 | bus_structure: null 270 | coordinate: [216, 324.0] 271 | rotation: 0 272 | state: enabled 273 | - name: soapy_hackrf_source_0 274 | id: soapy_hackrf_source 275 | parameters: 276 | affinity: '' 277 | alias: '' 278 | amp: 'False' 279 | bandwidth: '28000000' 280 | center_freq: center_freq - offset 281 | comment: '' 282 | dev_args: '' 283 | gain: '16' 284 | maxoutbuf: '0' 285 | minoutbuf: '0' 286 | samp_rate: samp_rate 287 | type: fc32 288 | vga: '16' 289 | states: 290 | bus_sink: false 291 | bus_source: false 292 | bus_structure: null 293 | coordinate: [1080, 172.0] 294 | rotation: 180 295 | state: true 296 | - name: tenna_gotenna_decoder_0 297 | id: tenna_gotenna_decoder 298 | parameters: 299 | affinity: '' 300 | alias: '' 301 | comment: '' 302 | maxoutbuf: '0' 303 | minoutbuf: '0' 304 | mode: '0' 305 | states: 306 | bus_sink: false 307 | bus_source: false 308 | bus_structure: null 309 | coordinate: [976, 300.0] 310 | rotation: 0 311 | state: enabled 312 | - name: tenna_pdu_to_pcapng_0 313 | id: tenna_pdu_to_pcapng 314 | parameters: 315 | affinity: '' 316 | alias: '' 317 | append: 'True' 318 | comment: '' 319 | filename: gotenna.pcapng 320 | states: 321 | bus_sink: false 322 | bus_source: false 323 | bus_structure: null 324 | coordinate: [944, 364.0] 325 | rotation: 180 326 | state: enabled 327 | 328 | connections: 329 | - [analog_quadrature_demod_cf_0, '0', dc_blocker_xx_0, '0'] 330 | - [blocks_keep_one_in_n_0, '0', freq_xlating_fir_filter_xxx_0, '0'] 331 | - [dc_blocker_xx_0, '0', rational_resampler_xxx_0, '0'] 332 | - [digital_binary_slicer_fb_0, '0', tenna_gotenna_decoder_0, '0'] 333 | - [digital_symbol_sync_xx_0, '0', digital_binary_slicer_fb_0, '0'] 334 | - [freq_xlating_fir_filter_xxx_0, '0', analog_quadrature_demod_cf_0, '0'] 335 | - [rational_resampler_xxx_0, '0', digital_symbol_sync_xx_0, '0'] 336 | - [soapy_hackrf_source_0, '0', blocks_keep_one_in_n_0, '0'] 337 | - [tenna_gotenna_decoder_0, pdu, tenna_pdu_to_pcapng_0, pdu] 338 | 339 | metadata: 340 | file_format: 1 341 | grc_version: 3.10.9.2 342 | -------------------------------------------------------------------------------- /docs/doxygen/doxyxml/doxyindex.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010 Free Software Foundation, Inc. 3 | # 4 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 5 | # This file is a part of gr-tenna 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | # 10 | """ 11 | Classes providing more user-friendly interfaces to the doxygen xml 12 | docs than the generated classes provide. 13 | """ 14 | 15 | import os 16 | 17 | from .generated import index 18 | from .base import Base 19 | from .text import description 20 | 21 | 22 | class DoxyIndex(Base): 23 | """ 24 | Parses a doxygen xml directory. 25 | """ 26 | 27 | __module__ = "gnuradio.utils.doxyxml" 28 | 29 | def _parse(self): 30 | if self._parsed: 31 | return 32 | super(DoxyIndex, self)._parse() 33 | self._root = index.parse(os.path.join(self._xml_path, 'index.xml')) 34 | for mem in self._root.compound: 35 | converted = self.convert_mem(mem) 36 | # For files and namespaces we want the contents to be 37 | # accessible directly from the parent rather than having 38 | # to go through the file object. 39 | if self.get_cls(mem) == DoxyFile: 40 | if mem.name.endswith('.h'): 41 | self._members += converted.members() 42 | self._members.append(converted) 43 | elif self.get_cls(mem) == DoxyNamespace: 44 | self._members += converted.members() 45 | self._members.append(converted) 46 | else: 47 | self._members.append(converted) 48 | 49 | 50 | class DoxyCompMem(Base): 51 | 52 | kind = None 53 | 54 | def __init__(self, *args, **kwargs): 55 | super(DoxyCompMem, self).__init__(*args, **kwargs) 56 | 57 | @classmethod 58 | def can_parse(cls, obj): 59 | return obj.kind == cls.kind 60 | 61 | def set_descriptions(self, parse_data): 62 | bd = description(getattr(parse_data, 'briefdescription', None)) 63 | dd = description(getattr(parse_data, 'detaileddescription', None)) 64 | self._data['brief_description'] = bd 65 | self._data['detailed_description'] = dd 66 | 67 | def set_parameters(self, data): 68 | vs = [ddc.value for ddc in data.detaileddescription.content_] 69 | pls = [] 70 | for v in vs: 71 | if hasattr(v, 'parameterlist'): 72 | pls += v.parameterlist 73 | pis = [] 74 | for pl in pls: 75 | pis += pl.parameteritem 76 | dpis = [] 77 | for pi in pis: 78 | dpi = DoxyParameterItem(pi) 79 | dpi._parse() 80 | dpis.append(dpi) 81 | self._data['params'] = dpis 82 | 83 | 84 | class DoxyCompound(DoxyCompMem): 85 | pass 86 | 87 | 88 | class DoxyMember(DoxyCompMem): 89 | pass 90 | 91 | 92 | class DoxyFunction(DoxyMember): 93 | 94 | __module__ = "gnuradio.utils.doxyxml" 95 | 96 | kind = 'function' 97 | 98 | def _parse(self): 99 | if self._parsed: 100 | return 101 | super(DoxyFunction, self)._parse() 102 | self.set_descriptions(self._parse_data) 103 | self.set_parameters(self._parse_data) 104 | if not self._data['params']: 105 | # If the params weren't set by a comment then just grab the names. 106 | self._data['params'] = [] 107 | prms = self._parse_data.param 108 | for prm in prms: 109 | self._data['params'].append(DoxyParam(prm)) 110 | 111 | brief_description = property(lambda self: self.data()['brief_description']) 112 | detailed_description = property( 113 | lambda self: self.data()['detailed_description']) 114 | params = property(lambda self: self.data()['params']) 115 | 116 | 117 | Base.mem_classes.append(DoxyFunction) 118 | 119 | 120 | class DoxyParam(DoxyMember): 121 | 122 | __module__ = "gnuradio.utils.doxyxml" 123 | 124 | def _parse(self): 125 | if self._parsed: 126 | return 127 | super(DoxyParam, self)._parse() 128 | self.set_descriptions(self._parse_data) 129 | self._data['declname'] = self._parse_data.declname 130 | 131 | @property 132 | def description(self): 133 | descriptions = [] 134 | if self.brief_description: 135 | descriptions.append(self.brief_description) 136 | if self.detailed_description: 137 | descriptions.append(self.detailed_description) 138 | return '\n\n'.join(descriptions) 139 | 140 | brief_description = property(lambda self: self.data()['brief_description']) 141 | detailed_description = property( 142 | lambda self: self.data()['detailed_description']) 143 | name = property(lambda self: self.data()['declname']) 144 | 145 | 146 | class DoxyParameterItem(DoxyMember): 147 | """A different representation of a parameter in Doxygen.""" 148 | 149 | def _parse(self): 150 | if self._parsed: 151 | return 152 | super(DoxyParameterItem, self)._parse() 153 | names = [] 154 | for nl in self._parse_data.parameternamelist: 155 | for pn in nl.parametername: 156 | names.append(description(pn)) 157 | # Just take first name 158 | self._data['name'] = names[0] 159 | # Get description 160 | pd = description(self._parse_data.get_parameterdescription()) 161 | self._data['description'] = pd 162 | 163 | description = property(lambda self: self.data()['description']) 164 | name = property(lambda self: self.data()['name']) 165 | 166 | 167 | class DoxyClass(DoxyCompound): 168 | 169 | __module__ = "gnuradio.utils.doxyxml" 170 | 171 | kind = 'class' 172 | 173 | def _parse(self): 174 | if self._parsed: 175 | return 176 | super(DoxyClass, self)._parse() 177 | self.retrieve_data() 178 | if self._error: 179 | return 180 | self.set_descriptions(self._retrieved_data.compounddef) 181 | self.set_parameters(self._retrieved_data.compounddef) 182 | # Sectiondef.kind tells about whether private or public. 183 | # We just ignore this for now. 184 | self.process_memberdefs() 185 | 186 | brief_description = property(lambda self: self.data()['brief_description']) 187 | detailed_description = property( 188 | lambda self: self.data()['detailed_description']) 189 | params = property(lambda self: self.data()['params']) 190 | 191 | 192 | Base.mem_classes.append(DoxyClass) 193 | 194 | 195 | class DoxyFile(DoxyCompound): 196 | 197 | __module__ = "gnuradio.utils.doxyxml" 198 | 199 | kind = 'file' 200 | 201 | def _parse(self): 202 | if self._parsed: 203 | return 204 | super(DoxyFile, self)._parse() 205 | self.retrieve_data() 206 | self.set_descriptions(self._retrieved_data.compounddef) 207 | if self._error: 208 | return 209 | self.process_memberdefs() 210 | 211 | brief_description = property(lambda self: self.data()['brief_description']) 212 | detailed_description = property( 213 | lambda self: self.data()['detailed_description']) 214 | 215 | 216 | Base.mem_classes.append(DoxyFile) 217 | 218 | 219 | class DoxyNamespace(DoxyCompound): 220 | 221 | __module__ = "gnuradio.utils.doxyxml" 222 | 223 | kind = 'namespace' 224 | 225 | def _parse(self): 226 | if self._parsed: 227 | return 228 | super(DoxyNamespace, self)._parse() 229 | self.retrieve_data() 230 | self.set_descriptions(self._retrieved_data.compounddef) 231 | if self._error: 232 | return 233 | self.process_memberdefs() 234 | 235 | 236 | Base.mem_classes.append(DoxyNamespace) 237 | 238 | 239 | class DoxyGroup(DoxyCompound): 240 | 241 | __module__ = "gnuradio.utils.doxyxml" 242 | 243 | kind = 'group' 244 | 245 | def _parse(self): 246 | if self._parsed: 247 | return 248 | super(DoxyGroup, self)._parse() 249 | self.retrieve_data() 250 | if self._error: 251 | return 252 | cdef = self._retrieved_data.compounddef 253 | self._data['title'] = description(cdef.title) 254 | # Process inner groups 255 | grps = cdef.innergroup 256 | for grp in grps: 257 | converted = DoxyGroup.from_refid(grp.refid, top=self.top) 258 | self._members.append(converted) 259 | # Process inner classes 260 | klasses = cdef.innerclass 261 | for kls in klasses: 262 | converted = DoxyClass.from_refid(kls.refid, top=self.top) 263 | self._members.append(converted) 264 | # Process normal members 265 | self.process_memberdefs() 266 | 267 | title = property(lambda self: self.data()['title']) 268 | 269 | 270 | Base.mem_classes.append(DoxyGroup) 271 | 272 | 273 | class DoxyFriend(DoxyMember): 274 | 275 | __module__ = "gnuradio.utils.doxyxml" 276 | 277 | kind = 'friend' 278 | 279 | 280 | Base.mem_classes.append(DoxyFriend) 281 | 282 | 283 | class DoxyOther(Base): 284 | 285 | __module__ = "gnuradio.utils.doxyxml" 286 | 287 | kinds = set(['variable', 'struct', 'union', 'define', 'typedef', 'enum', 288 | 'dir', 'page', 'signal', 'slot', 'property']) 289 | 290 | @classmethod 291 | def can_parse(cls, obj): 292 | return obj.kind in cls.kinds 293 | 294 | 295 | Base.mem_classes.append(DoxyOther) 296 | -------------------------------------------------------------------------------- /apps/gotenna_tx_usrp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Tx Usrp 9 | # GNU Radio version: 3.10.9.2 10 | 11 | from gnuradio import blocks 12 | from gnuradio import digital 13 | from gnuradio import filter 14 | from gnuradio.filter import firdes 15 | from gnuradio import gr 16 | from gnuradio.fft import window 17 | import sys 18 | import signal 19 | from argparse import ArgumentParser 20 | from gnuradio.eng_arg import eng_float, intx 21 | from gnuradio import eng_notation 22 | from gnuradio import uhd 23 | import time 24 | import gotenna_packet 25 | import math 26 | 27 | 28 | 29 | 30 | class gotenna_tx_usrp(gr.top_block): 31 | 32 | def __init__(self, app_id=0x3fff, initials='VE3IRR', message='Hello world!', sender_gid=1234567890): 33 | gr.top_block.__init__(self, "Gotenna Tx Usrp", catch_exceptions=True) 34 | 35 | ################################################## 36 | # Parameters 37 | ################################################## 38 | self.app_id = app_id 39 | self.initials = initials 40 | self.message = message 41 | self.sender_gid = sender_gid 42 | 43 | ################################################## 44 | # Variables 45 | ################################################## 46 | self.samp_per_sym = samp_per_sym = 4 47 | self.interp = interp = 20 48 | self.data_chan = data_chan = 2 49 | self.baud_rate = baud_rate = 24000 50 | self.samp_rate = samp_rate = baud_rate * samp_per_sym * interp 51 | self.packets = packets = gotenna_packet.encode_shout_packets(data_chan, app_id, sender_gid, initials, message) 52 | self.control_chan = control_chan = 2 53 | self.center_freq = center_freq = 926250000 54 | 55 | ################################################## 56 | # Blocks 57 | ################################################## 58 | 59 | self.uhd_usrp_sink_0 = uhd.usrp_sink( 60 | ",".join(("", "")), 61 | uhd.stream_args( 62 | cpu_format="fc32", 63 | args='', 64 | channels=list(range(0,1)), 65 | ), 66 | '', 67 | ) 68 | self.uhd_usrp_sink_0.set_samp_rate(samp_rate) 69 | self.uhd_usrp_sink_0.set_time_unknown_pps(uhd.time_spec(0)) 70 | 71 | self.uhd_usrp_sink_0.set_center_freq(center_freq, 0) 72 | self.uhd_usrp_sink_0.set_gain(50, 0) 73 | self.rational_resampler_xxx_0 = filter.rational_resampler_ccc( 74 | interpolation=interp, 75 | decimation=1, 76 | taps=[], 77 | fractional_bw=0) 78 | self.digital_gfsk_mod_0 = digital.gfsk_mod( 79 | samples_per_symbol=samp_per_sym, 80 | sensitivity=1.0, 81 | bt=0.5, 82 | verbose=False, 83 | log=False, 84 | do_unpack=True) 85 | self.blocks_vector_source_x_2 = blocks.vector_source_f(gotenna_packet.vco(center_freq, control_chan, data_chan, packets), False, 1, []) 86 | self.blocks_vector_source_x_1 = blocks.vector_source_c(gotenna_packet.envelope(packets), False, 1, []) 87 | self.blocks_vector_source_x_0 = blocks.vector_source_b(gotenna_packet.gfsk_bytes(packets), False, 1, []) 88 | self.blocks_vco_c_0 = blocks.vco_c(samp_rate, (2*math.pi), 0.9) 89 | self.blocks_repeat_1 = blocks.repeat(gr.sizeof_float*1, (8 * samp_per_sym * interp)) 90 | self.blocks_repeat_0 = blocks.repeat(gr.sizeof_gr_complex*1, (8 * samp_per_sym)) 91 | self.blocks_multiply_xx_1 = blocks.multiply_vcc(1) 92 | self.blocks_multiply_xx_0 = blocks.multiply_vcc(1) 93 | 94 | 95 | ################################################## 96 | # Connections 97 | ################################################## 98 | self.connect((self.blocks_multiply_xx_0, 0), (self.rational_resampler_xxx_0, 0)) 99 | self.connect((self.blocks_multiply_xx_1, 0), (self.uhd_usrp_sink_0, 0)) 100 | self.connect((self.blocks_repeat_0, 0), (self.blocks_multiply_xx_0, 1)) 101 | self.connect((self.blocks_repeat_1, 0), (self.blocks_vco_c_0, 0)) 102 | self.connect((self.blocks_vco_c_0, 0), (self.blocks_multiply_xx_1, 1)) 103 | self.connect((self.blocks_vector_source_x_0, 0), (self.digital_gfsk_mod_0, 0)) 104 | self.connect((self.blocks_vector_source_x_1, 0), (self.blocks_repeat_0, 0)) 105 | self.connect((self.blocks_vector_source_x_2, 0), (self.blocks_repeat_1, 0)) 106 | self.connect((self.digital_gfsk_mod_0, 0), (self.blocks_multiply_xx_0, 0)) 107 | self.connect((self.rational_resampler_xxx_0, 0), (self.blocks_multiply_xx_1, 0)) 108 | 109 | 110 | def get_app_id(self): 111 | return self.app_id 112 | 113 | def set_app_id(self, app_id): 114 | self.app_id = app_id 115 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 116 | 117 | def get_initials(self): 118 | return self.initials 119 | 120 | def set_initials(self, initials): 121 | self.initials = initials 122 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 123 | 124 | def get_message(self): 125 | return self.message 126 | 127 | def set_message(self, message): 128 | self.message = message 129 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 130 | 131 | def get_sender_gid(self): 132 | return self.sender_gid 133 | 134 | def set_sender_gid(self, sender_gid): 135 | self.sender_gid = sender_gid 136 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 137 | 138 | def get_samp_per_sym(self): 139 | return self.samp_per_sym 140 | 141 | def set_samp_per_sym(self, samp_per_sym): 142 | self.samp_per_sym = samp_per_sym 143 | self.set_samp_rate(self.baud_rate * self.samp_per_sym * self.interp) 144 | self.blocks_repeat_0.set_interpolation((8 * self.samp_per_sym)) 145 | self.blocks_repeat_1.set_interpolation((8 * self.samp_per_sym * self.interp)) 146 | 147 | def get_interp(self): 148 | return self.interp 149 | 150 | def set_interp(self, interp): 151 | self.interp = interp 152 | self.set_samp_rate(self.baud_rate * self.samp_per_sym * self.interp) 153 | self.blocks_repeat_1.set_interpolation((8 * self.samp_per_sym * self.interp)) 154 | 155 | def get_data_chan(self): 156 | return self.data_chan 157 | 158 | def set_data_chan(self, data_chan): 159 | self.data_chan = data_chan 160 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 161 | self.blocks_vector_source_x_2.set_data(gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets), []) 162 | 163 | def get_baud_rate(self): 164 | return self.baud_rate 165 | 166 | def set_baud_rate(self, baud_rate): 167 | self.baud_rate = baud_rate 168 | self.set_samp_rate(self.baud_rate * self.samp_per_sym * self.interp) 169 | 170 | def get_samp_rate(self): 171 | return self.samp_rate 172 | 173 | def set_samp_rate(self, samp_rate): 174 | self.samp_rate = samp_rate 175 | self.uhd_usrp_sink_0.set_samp_rate(self.samp_rate) 176 | 177 | def get_packets(self): 178 | return self.packets 179 | 180 | def set_packets(self, packets): 181 | self.packets = packets 182 | self.blocks_vector_source_x_0.set_data(gotenna_packet.gfsk_bytes(self.packets), []) 183 | self.blocks_vector_source_x_1.set_data(gotenna_packet.envelope(self.packets), []) 184 | self.blocks_vector_source_x_2.set_data(gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets), []) 185 | 186 | def get_control_chan(self): 187 | return self.control_chan 188 | 189 | def set_control_chan(self, control_chan): 190 | self.control_chan = control_chan 191 | self.blocks_vector_source_x_2.set_data(gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets), []) 192 | 193 | def get_center_freq(self): 194 | return self.center_freq 195 | 196 | def set_center_freq(self, center_freq): 197 | self.center_freq = center_freq 198 | self.blocks_vector_source_x_2.set_data(gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets), []) 199 | self.uhd_usrp_sink_0.set_center_freq(self.center_freq, 0) 200 | 201 | 202 | 203 | def argument_parser(): 204 | parser = ArgumentParser() 205 | parser.add_argument( 206 | "--app-id", dest="app_id", type=intx, default=0x3fff, 207 | help="Set App ID [default=%(default)r]") 208 | parser.add_argument( 209 | "--initials", dest="initials", type=str, default='VE3IRR', 210 | help="Set Sender initials [default=%(default)r]") 211 | parser.add_argument( 212 | "--message", dest="message", type=str, default='Hello world!', 213 | help="Set Message [default=%(default)r]") 214 | parser.add_argument( 215 | "--sender-gid", dest="sender_gid", type=intx, default=1234567890, 216 | help="Set Sender GID [default=%(default)r]") 217 | return parser 218 | 219 | 220 | def main(top_block_cls=gotenna_tx_usrp, options=None): 221 | if options is None: 222 | options = argument_parser().parse_args() 223 | tb = top_block_cls(app_id=options.app_id, initials=options.initials, message=options.message, sender_gid=options.sender_gid) 224 | 225 | def sig_handler(sig=None, frame=None): 226 | tb.stop() 227 | tb.wait() 228 | 229 | sys.exit(0) 230 | 231 | signal.signal(signal.SIGINT, sig_handler) 232 | signal.signal(signal.SIGTERM, sig_handler) 233 | 234 | tb.start() 235 | 236 | tb.wait() 237 | 238 | 239 | if __name__ == '__main__': 240 | main() 241 | -------------------------------------------------------------------------------- /apps/gotenna_tx_hackrf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Tx Hackrf 9 | # GNU Radio version: 3.10.9.2 10 | 11 | from gnuradio import blocks 12 | from gnuradio import digital 13 | from gnuradio import filter 14 | from gnuradio.filter import firdes 15 | from gnuradio import gr 16 | from gnuradio.fft import window 17 | import sys 18 | import signal 19 | from argparse import ArgumentParser 20 | from gnuradio.eng_arg import eng_float, intx 21 | from gnuradio import eng_notation 22 | from gnuradio import soapy 23 | import gotenna_packet 24 | import math 25 | 26 | 27 | 28 | 29 | class gotenna_tx_hackrf(gr.top_block): 30 | 31 | def __init__(self, app_id=0x3fff, initials='VE3IRR', message='Hello world!', sender_gid=1234567890): 32 | gr.top_block.__init__(self, "Gotenna Tx Hackrf", catch_exceptions=True) 33 | 34 | ################################################## 35 | # Parameters 36 | ################################################## 37 | self.app_id = app_id 38 | self.initials = initials 39 | self.message = message 40 | self.sender_gid = sender_gid 41 | 42 | ################################################## 43 | # Variables 44 | ################################################## 45 | self.data_chan = data_chan = 2 46 | self.silence_time = silence_time = 5000 47 | self.samp_rate = samp_rate = 2000000 48 | self.samp_per_sym = samp_per_sym = 4 49 | self.packets = packets = gotenna_packet.encode_shout_packets(data_chan, app_id, sender_gid, initials, message) 50 | self.interp = interp = 20 51 | self.control_chan = control_chan = 2 52 | self.center_freq = center_freq = 926250000 53 | self.baud_rate = baud_rate = 24000 54 | 55 | ################################################## 56 | # Blocks 57 | ################################################## 58 | 59 | self.soapy_hackrf_sink_0 = None 60 | dev = 'driver=hackrf' 61 | stream_args = '' 62 | tune_args = [''] 63 | settings = [''] 64 | 65 | self.soapy_hackrf_sink_0 = soapy.sink(dev, "fc32", 1, '', 66 | stream_args, tune_args, settings) 67 | self.soapy_hackrf_sink_0.set_sample_rate(0, samp_rate) 68 | self.soapy_hackrf_sink_0.set_bandwidth(0, 0) 69 | self.soapy_hackrf_sink_0.set_frequency(0, center_freq) 70 | self.soapy_hackrf_sink_0.set_gain(0, 'AMP', False) 71 | self.soapy_hackrf_sink_0.set_gain(0, 'VGA', min(max(24, 0.0), 47.0)) 72 | self.rational_resampler_xxx_1 = filter.rational_resampler_ccc( 73 | interpolation=samp_rate, 74 | decimation=(baud_rate * samp_per_sym * interp), 75 | taps=[], 76 | fractional_bw=0) 77 | self.rational_resampler_xxx_0 = filter.rational_resampler_ccc( 78 | interpolation=interp, 79 | decimation=1, 80 | taps=[], 81 | fractional_bw=0) 82 | self.digital_gfsk_mod_0 = digital.gfsk_mod( 83 | samples_per_symbol=samp_per_sym, 84 | sensitivity=1.0, 85 | bt=0.5, 86 | verbose=False, 87 | log=False, 88 | do_unpack=True) 89 | self.blocks_vector_source_x_2 = blocks.vector_source_f([0]*silence_time+gotenna_packet.vco(center_freq, control_chan, data_chan, packets)+[0]*silence_time, False, 1, []) 90 | self.blocks_vector_source_x_1 = blocks.vector_source_c([0]*silence_time+gotenna_packet.envelope(packets)+[0]*silence_time, False, 1, []) 91 | self.blocks_vector_source_x_0 = blocks.vector_source_b([0]*silence_time+gotenna_packet.gfsk_bytes(packets)+[0]*silence_time, False, 1, []) 92 | self.blocks_vco_c_0 = blocks.vco_c((baud_rate * samp_per_sym * interp), (2*math.pi), 0.9) 93 | self.blocks_repeat_1 = blocks.repeat(gr.sizeof_float*1, (8 * samp_per_sym * interp)) 94 | self.blocks_repeat_0 = blocks.repeat(gr.sizeof_gr_complex*1, (8 * samp_per_sym)) 95 | self.blocks_multiply_xx_1 = blocks.multiply_vcc(1) 96 | self.blocks_multiply_xx_0 = blocks.multiply_vcc(1) 97 | 98 | 99 | ################################################## 100 | # Connections 101 | ################################################## 102 | self.connect((self.blocks_multiply_xx_0, 0), (self.rational_resampler_xxx_0, 0)) 103 | self.connect((self.blocks_multiply_xx_1, 0), (self.rational_resampler_xxx_1, 0)) 104 | self.connect((self.blocks_repeat_0, 0), (self.blocks_multiply_xx_0, 1)) 105 | self.connect((self.blocks_repeat_1, 0), (self.blocks_vco_c_0, 0)) 106 | self.connect((self.blocks_vco_c_0, 0), (self.blocks_multiply_xx_1, 1)) 107 | self.connect((self.blocks_vector_source_x_0, 0), (self.digital_gfsk_mod_0, 0)) 108 | self.connect((self.blocks_vector_source_x_1, 0), (self.blocks_repeat_0, 0)) 109 | self.connect((self.blocks_vector_source_x_2, 0), (self.blocks_repeat_1, 0)) 110 | self.connect((self.digital_gfsk_mod_0, 0), (self.blocks_multiply_xx_0, 0)) 111 | self.connect((self.rational_resampler_xxx_0, 0), (self.blocks_multiply_xx_1, 0)) 112 | self.connect((self.rational_resampler_xxx_1, 0), (self.soapy_hackrf_sink_0, 0)) 113 | 114 | 115 | def get_app_id(self): 116 | return self.app_id 117 | 118 | def set_app_id(self, app_id): 119 | self.app_id = app_id 120 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 121 | 122 | def get_initials(self): 123 | return self.initials 124 | 125 | def set_initials(self, initials): 126 | self.initials = initials 127 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 128 | 129 | def get_message(self): 130 | return self.message 131 | 132 | def set_message(self, message): 133 | self.message = message 134 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 135 | 136 | def get_sender_gid(self): 137 | return self.sender_gid 138 | 139 | def set_sender_gid(self, sender_gid): 140 | self.sender_gid = sender_gid 141 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 142 | 143 | def get_data_chan(self): 144 | return self.data_chan 145 | 146 | def set_data_chan(self, data_chan): 147 | self.data_chan = data_chan 148 | self.set_packets(gotenna_packet.encode_shout_packets(self.data_chan, self.app_id, self.sender_gid, self.initials, self.message)) 149 | self.blocks_vector_source_x_2.set_data([0]*self.silence_time+gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets)+[0]*self.silence_time, []) 150 | 151 | def get_silence_time(self): 152 | return self.silence_time 153 | 154 | def set_silence_time(self, silence_time): 155 | self.silence_time = silence_time 156 | self.blocks_vector_source_x_0.set_data([0]*self.silence_time+gotenna_packet.gfsk_bytes(self.packets)+[0]*self.silence_time, []) 157 | self.blocks_vector_source_x_1.set_data([0]*self.silence_time+gotenna_packet.envelope(self.packets)+[0]*self.silence_time, []) 158 | self.blocks_vector_source_x_2.set_data([0]*self.silence_time+gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets)+[0]*self.silence_time, []) 159 | 160 | def get_samp_rate(self): 161 | return self.samp_rate 162 | 163 | def set_samp_rate(self, samp_rate): 164 | self.samp_rate = samp_rate 165 | self.soapy_hackrf_sink_0.set_sample_rate(0, self.samp_rate) 166 | 167 | def get_samp_per_sym(self): 168 | return self.samp_per_sym 169 | 170 | def set_samp_per_sym(self, samp_per_sym): 171 | self.samp_per_sym = samp_per_sym 172 | self.blocks_repeat_0.set_interpolation((8 * self.samp_per_sym)) 173 | self.blocks_repeat_1.set_interpolation((8 * self.samp_per_sym * self.interp)) 174 | 175 | def get_packets(self): 176 | return self.packets 177 | 178 | def set_packets(self, packets): 179 | self.packets = packets 180 | self.blocks_vector_source_x_0.set_data([0]*self.silence_time+gotenna_packet.gfsk_bytes(self.packets)+[0]*self.silence_time, []) 181 | self.blocks_vector_source_x_1.set_data([0]*self.silence_time+gotenna_packet.envelope(self.packets)+[0]*self.silence_time, []) 182 | self.blocks_vector_source_x_2.set_data([0]*self.silence_time+gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets)+[0]*self.silence_time, []) 183 | 184 | def get_interp(self): 185 | return self.interp 186 | 187 | def set_interp(self, interp): 188 | self.interp = interp 189 | self.blocks_repeat_1.set_interpolation((8 * self.samp_per_sym * self.interp)) 190 | 191 | def get_control_chan(self): 192 | return self.control_chan 193 | 194 | def set_control_chan(self, control_chan): 195 | self.control_chan = control_chan 196 | self.blocks_vector_source_x_2.set_data([0]*self.silence_time+gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets)+[0]*self.silence_time, []) 197 | 198 | def get_center_freq(self): 199 | return self.center_freq 200 | 201 | def set_center_freq(self, center_freq): 202 | self.center_freq = center_freq 203 | self.blocks_vector_source_x_2.set_data([0]*self.silence_time+gotenna_packet.vco(self.center_freq, self.control_chan, self.data_chan, self.packets)+[0]*self.silence_time, []) 204 | self.soapy_hackrf_sink_0.set_frequency(0, self.center_freq) 205 | 206 | def get_baud_rate(self): 207 | return self.baud_rate 208 | 209 | def set_baud_rate(self, baud_rate): 210 | self.baud_rate = baud_rate 211 | 212 | 213 | 214 | def argument_parser(): 215 | parser = ArgumentParser() 216 | parser.add_argument( 217 | "--app-id", dest="app_id", type=intx, default=0x3fff, 218 | help="Set App ID [default=%(default)r]") 219 | parser.add_argument( 220 | "--initials", dest="initials", type=str, default='VE3IRR', 221 | help="Set Sender initials [default=%(default)r]") 222 | parser.add_argument( 223 | "--message", dest="message", type=str, default='Hello world!', 224 | help="Set Message [default=%(default)r]") 225 | parser.add_argument( 226 | "--sender-gid", dest="sender_gid", type=intx, default=1234567890, 227 | help="Set Sender GID [default=%(default)r]") 228 | return parser 229 | 230 | 231 | def main(top_block_cls=gotenna_tx_hackrf, options=None): 232 | if options is None: 233 | options = argument_parser().parse_args() 234 | tb = top_block_cls(app_id=options.app_id, initials=options.initials, message=options.message, sender_gid=options.sender_gid) 235 | 236 | def sig_handler(sig=None, frame=None): 237 | tb.stop() 238 | tb.wait() 239 | 240 | sys.exit(0) 241 | 242 | signal.signal(signal.SIGINT, sig_handler) 243 | signal.signal(signal.SIGTERM, sig_handler) 244 | 245 | tb.start() 246 | 247 | tb.wait() 248 | 249 | 250 | if __name__ == '__main__': 251 | main() 252 | -------------------------------------------------------------------------------- /apps/gotenna_tx_hackrf.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | catch_exceptions: 'True' 5 | category: '[GRC Hier Blocks]' 6 | cmake_opt: '' 7 | comment: '' 8 | copyright: '' 9 | description: '' 10 | gen_cmake: 'On' 11 | gen_linking: dynamic 12 | generate_options: no_gui 13 | hier_block_src_path: '.:' 14 | id: gotenna_tx_hackrf 15 | max_nouts: '0' 16 | output_language: python 17 | placement: (0,0) 18 | qt_qss_theme: '' 19 | realtime_scheduling: '' 20 | run: 'True' 21 | run_command: '{python} -u {filename}' 22 | run_options: run 23 | sizing_mode: fixed 24 | thread_safe_setters: '' 25 | title: '' 26 | window_size: '' 27 | states: 28 | bus_sink: false 29 | bus_source: false 30 | bus_structure: null 31 | coordinate: [8, 8] 32 | rotation: 0 33 | state: enabled 34 | 35 | blocks: 36 | - name: baud_rate 37 | id: variable 38 | parameters: 39 | comment: '' 40 | value: '24000' 41 | states: 42 | bus_sink: false 43 | bus_source: false 44 | bus_structure: null 45 | coordinate: [8, 332.0] 46 | rotation: 0 47 | state: enabled 48 | - name: center_freq 49 | id: variable 50 | parameters: 51 | comment: '' 52 | value: '926250000' 53 | states: 54 | bus_sink: false 55 | bus_source: false 56 | bus_structure: null 57 | coordinate: [104, 460.0] 58 | rotation: 0 59 | state: enabled 60 | - name: control_chan 61 | id: variable 62 | parameters: 63 | comment: '' 64 | value: '2' 65 | states: 66 | bus_sink: false 67 | bus_source: false 68 | bus_structure: null 69 | coordinate: [120, 524] 70 | rotation: 0 71 | state: enabled 72 | - name: data_chan 73 | id: variable 74 | parameters: 75 | comment: '' 76 | value: '2' 77 | states: 78 | bus_sink: false 79 | bus_source: false 80 | bus_structure: null 81 | coordinate: [224, 524] 82 | rotation: 0 83 | state: enabled 84 | - name: interp 85 | id: variable 86 | parameters: 87 | comment: '' 88 | value: '20' 89 | states: 90 | bus_sink: false 91 | bus_source: false 92 | bus_structure: null 93 | coordinate: [8, 460] 94 | rotation: 0 95 | state: enabled 96 | - name: packets 97 | id: variable 98 | parameters: 99 | comment: '' 100 | value: gotenna_packet.encode_packets(2, gotenna_packet.encode_encrypted_payload(app_id, 101 | 93100711244021, 93100712457340, 1, bytearray(a^b for a,b in zip([0, 0, 0, 0, 102 | 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 193, 198], [0xae, 0x95, 0x6c, 0x00, 0xd2, 103 | 0x8b, 0x02, 0x12, 0x5b, 0x1b, 0xa3, 0x88, 0xfc, 0xb8, 0x5a, 0x31, 0xe2, 0x7f, 104 | 0xe7])))) 105 | states: 106 | bus_sink: false 107 | bus_source: false 108 | bus_structure: null 109 | coordinate: [8, 588] 110 | rotation: 0 111 | state: disabled 112 | - name: packets 113 | id: variable 114 | parameters: 115 | comment: '' 116 | value: gotenna_packet.encode_shout_packets(data_chan, app_id, sender_gid, initials, 117 | message) 118 | states: 119 | bus_sink: false 120 | bus_source: false 121 | bus_structure: null 122 | coordinate: [8, 652] 123 | rotation: 0 124 | state: enabled 125 | - name: samp_per_sym 126 | id: variable 127 | parameters: 128 | comment: '' 129 | value: '4' 130 | states: 131 | bus_sink: false 132 | bus_source: false 133 | bus_structure: null 134 | coordinate: [8, 524] 135 | rotation: 0 136 | state: enabled 137 | - name: samp_rate 138 | id: variable 139 | parameters: 140 | comment: '' 141 | value: '2000000' 142 | states: 143 | bus_sink: false 144 | bus_source: false 145 | bus_structure: null 146 | coordinate: [8, 396] 147 | rotation: 0 148 | state: enabled 149 | - name: silence_time 150 | id: variable 151 | parameters: 152 | comment: '' 153 | value: '5000' 154 | states: 155 | bus_sink: false 156 | bus_source: false 157 | bus_structure: null 158 | coordinate: [8, 268.0] 159 | rotation: 0 160 | state: enabled 161 | - name: app_id 162 | id: parameter 163 | parameters: 164 | alias: '' 165 | comment: '' 166 | hide: none 167 | label: App ID 168 | short_id: '' 169 | type: intx 170 | value: '0x3fff' 171 | states: 172 | bus_sink: false 173 | bus_source: false 174 | bus_structure: null 175 | coordinate: [296, 44.0] 176 | rotation: 0 177 | state: true 178 | - name: blocks_multiply_xx_0 179 | id: blocks_multiply_xx 180 | parameters: 181 | affinity: '' 182 | alias: '' 183 | comment: '' 184 | maxoutbuf: '0' 185 | minoutbuf: '0' 186 | num_inputs: '2' 187 | type: complex 188 | vlen: '1' 189 | states: 190 | bus_sink: false 191 | bus_source: false 192 | bus_structure: null 193 | coordinate: [576, 216] 194 | rotation: 0 195 | state: enabled 196 | - name: blocks_multiply_xx_1 197 | id: blocks_multiply_xx 198 | parameters: 199 | affinity: '' 200 | alias: '' 201 | comment: '' 202 | maxoutbuf: '0' 203 | minoutbuf: '0' 204 | num_inputs: '2' 205 | type: complex 206 | vlen: '1' 207 | states: 208 | bus_sink: false 209 | bus_source: false 210 | bus_structure: null 211 | coordinate: [904, 296] 212 | rotation: 0 213 | state: enabled 214 | - name: blocks_repeat_0 215 | id: blocks_repeat 216 | parameters: 217 | affinity: '' 218 | alias: '' 219 | comment: '' 220 | interp: 8 * samp_per_sym 221 | maxoutbuf: '0' 222 | minoutbuf: '0' 223 | type: complex 224 | vlen: '1' 225 | states: 226 | bus_sink: false 227 | bus_source: false 228 | bus_structure: null 229 | coordinate: [392, 276.0] 230 | rotation: 0 231 | state: enabled 232 | - name: blocks_repeat_1 233 | id: blocks_repeat 234 | parameters: 235 | affinity: '' 236 | alias: '' 237 | comment: '' 238 | interp: 8 * samp_per_sym * interp 239 | maxoutbuf: '0' 240 | minoutbuf: '0' 241 | type: float 242 | vlen: '1' 243 | states: 244 | bus_sink: false 245 | bus_source: false 246 | bus_structure: null 247 | coordinate: [536, 404] 248 | rotation: 0 249 | state: enabled 250 | - name: blocks_vco_c_0 251 | id: blocks_vco_c 252 | parameters: 253 | affinity: '' 254 | alias: '' 255 | amplitude: '0.9' 256 | comment: '' 257 | maxoutbuf: '0' 258 | minoutbuf: '0' 259 | samp_rate: baud_rate * samp_per_sym * interp 260 | sensitivity: 2*math.pi 261 | states: 262 | bus_sink: false 263 | bus_source: false 264 | bus_structure: null 265 | coordinate: [712, 388] 266 | rotation: 0 267 | state: enabled 268 | - name: blocks_vector_source_x_0 269 | id: blocks_vector_source_x 270 | parameters: 271 | affinity: '' 272 | alias: '' 273 | comment: '' 274 | maxoutbuf: '0' 275 | minoutbuf: '0' 276 | repeat: 'False' 277 | tags: '[]' 278 | type: byte 279 | vector: '[0]*silence_time+gotenna_packet.gfsk_bytes(packets)+[0]*silence_time' 280 | vlen: '1' 281 | states: 282 | bus_sink: false 283 | bus_source: false 284 | bus_structure: null 285 | coordinate: [160, 164.0] 286 | rotation: 0 287 | state: enabled 288 | - name: blocks_vector_source_x_1 289 | id: blocks_vector_source_x 290 | parameters: 291 | affinity: '' 292 | alias: '' 293 | comment: '' 294 | maxoutbuf: '0' 295 | minoutbuf: '0' 296 | repeat: 'False' 297 | tags: '[]' 298 | type: complex 299 | vector: '[0]*silence_time+gotenna_packet.envelope(packets)+[0]*silence_time' 300 | vlen: '1' 301 | states: 302 | bus_sink: false 303 | bus_source: false 304 | bus_structure: null 305 | coordinate: [160, 260.0] 306 | rotation: 0 307 | state: enabled 308 | - name: blocks_vector_source_x_2 309 | id: blocks_vector_source_x 310 | parameters: 311 | affinity: '' 312 | alias: '' 313 | comment: '' 314 | maxoutbuf: '0' 315 | minoutbuf: '0' 316 | repeat: 'False' 317 | tags: '[]' 318 | type: float 319 | vector: '[0]*silence_time+gotenna_packet.vco(center_freq, control_chan, data_chan, 320 | packets)+[0]*silence_time' 321 | vlen: '1' 322 | states: 323 | bus_sink: false 324 | bus_source: false 325 | bus_structure: null 326 | coordinate: [304, 388] 327 | rotation: 0 328 | state: enabled 329 | - name: digital_gfsk_mod_0 330 | id: digital_gfsk_mod 331 | parameters: 332 | affinity: '' 333 | alias: '' 334 | bt: '0.5' 335 | comment: '' 336 | do_unpack: 'True' 337 | log: 'False' 338 | maxoutbuf: '0' 339 | minoutbuf: '0' 340 | samples_per_symbol: samp_per_sym 341 | sensitivity: '1.0' 342 | verbose: 'False' 343 | states: 344 | bus_sink: false 345 | bus_source: false 346 | bus_structure: null 347 | coordinate: [384, 164.0] 348 | rotation: 0 349 | state: enabled 350 | - name: import_0 351 | id: import 352 | parameters: 353 | alias: '' 354 | comment: '' 355 | imports: import gotenna_packet 356 | states: 357 | bus_sink: false 358 | bus_source: false 359 | bus_structure: null 360 | coordinate: [8, 92.0] 361 | rotation: 0 362 | state: enabled 363 | - name: import_1 364 | id: import 365 | parameters: 366 | alias: '' 367 | comment: '' 368 | imports: import math 369 | states: 370 | bus_sink: false 371 | bus_source: false 372 | bus_structure: null 373 | coordinate: [8, 140] 374 | rotation: 0 375 | state: enabled 376 | - name: initials 377 | id: parameter 378 | parameters: 379 | alias: '' 380 | comment: '' 381 | hide: none 382 | label: Sender initials 383 | short_id: '' 384 | type: str 385 | value: VE3IRR 386 | states: 387 | bus_sink: false 388 | bus_source: false 389 | bus_structure: null 390 | coordinate: [520, 44.0] 391 | rotation: 0 392 | state: true 393 | - name: message 394 | id: parameter 395 | parameters: 396 | alias: '' 397 | comment: '' 398 | hide: none 399 | label: Message 400 | short_id: '' 401 | type: str 402 | value: Hello world! 403 | states: 404 | bus_sink: false 405 | bus_source: false 406 | bus_structure: null 407 | coordinate: [656, 44.0] 408 | rotation: 0 409 | state: true 410 | - name: rational_resampler_xxx_0 411 | id: rational_resampler_xxx 412 | parameters: 413 | affinity: '' 414 | alias: '' 415 | comment: '' 416 | decim: '1' 417 | fbw: '0' 418 | interp: interp 419 | maxoutbuf: '0' 420 | minoutbuf: '0' 421 | taps: '' 422 | type: ccc 423 | states: 424 | bus_sink: false 425 | bus_source: false 426 | bus_structure: null 427 | coordinate: [704, 204] 428 | rotation: 0 429 | state: enabled 430 | - name: rational_resampler_xxx_1 431 | id: rational_resampler_xxx 432 | parameters: 433 | affinity: '' 434 | alias: '' 435 | comment: '' 436 | decim: baud_rate * samp_per_sym * interp 437 | fbw: '0' 438 | interp: samp_rate 439 | maxoutbuf: '0' 440 | minoutbuf: '0' 441 | taps: '[]' 442 | type: ccc 443 | states: 444 | bus_sink: false 445 | bus_source: false 446 | bus_structure: null 447 | coordinate: [1048, 284.0] 448 | rotation: 0 449 | state: enabled 450 | - name: sender_gid 451 | id: parameter 452 | parameters: 453 | alias: '' 454 | comment: '' 455 | hide: none 456 | label: Sender GID 457 | short_id: '' 458 | type: intx 459 | value: '1234567890' 460 | states: 461 | bus_sink: false 462 | bus_source: false 463 | bus_structure: null 464 | coordinate: [400, 44.0] 465 | rotation: 0 466 | state: true 467 | - name: soapy_hackrf_sink_0 468 | id: soapy_hackrf_sink 469 | parameters: 470 | affinity: '' 471 | alias: '' 472 | amp: 'False' 473 | bandwidth: '0' 474 | center_freq: center_freq 475 | comment: '' 476 | dev_args: '' 477 | samp_rate: samp_rate 478 | type: fc32 479 | vga: '24' 480 | states: 481 | bus_sink: false 482 | bus_source: false 483 | bus_structure: null 484 | coordinate: [1016, 392.0] 485 | rotation: 180 486 | state: true 487 | 488 | connections: 489 | - [blocks_multiply_xx_0, '0', rational_resampler_xxx_0, '0'] 490 | - [blocks_multiply_xx_1, '0', rational_resampler_xxx_1, '0'] 491 | - [blocks_repeat_0, '0', blocks_multiply_xx_0, '1'] 492 | - [blocks_repeat_1, '0', blocks_vco_c_0, '0'] 493 | - [blocks_vco_c_0, '0', blocks_multiply_xx_1, '1'] 494 | - [blocks_vector_source_x_0, '0', digital_gfsk_mod_0, '0'] 495 | - [blocks_vector_source_x_1, '0', blocks_repeat_0, '0'] 496 | - [blocks_vector_source_x_2, '0', blocks_repeat_1, '0'] 497 | - [digital_gfsk_mod_0, '0', blocks_multiply_xx_0, '0'] 498 | - [rational_resampler_xxx_0, '0', blocks_multiply_xx_1, '0'] 499 | - [rational_resampler_xxx_1, '0', soapy_hackrf_sink_0, '0'] 500 | 501 | metadata: 502 | file_format: 1 503 | grc_version: 3.10.9.2 504 | -------------------------------------------------------------------------------- /apps/gotenna_pro_tx_usrp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Pro Tx Usrp 9 | # GNU Radio version: 3.10.9.2 10 | 11 | from gnuradio import blocks 12 | from gnuradio import digital 13 | from gnuradio import filter 14 | from gnuradio.filter import firdes 15 | from gnuradio import gr 16 | from gnuradio.fft import window 17 | import sys 18 | import signal 19 | from argparse import ArgumentParser 20 | from gnuradio.eng_arg import eng_float, intx 21 | from gnuradio import eng_notation 22 | from gnuradio import uhd 23 | import time 24 | import gotenna_packet 25 | import math 26 | 27 | 28 | 29 | 30 | class gotenna_pro_tx_usrp(gr.top_block): 31 | 32 | def __init__(self, baud_rate=9600, callsign='VE3IRR', counter_num=0, frequency=450000000, message='Hello world!', message_type='BROADCAST', publickey_data='base64key', recipient_gid=1234567891, sender_gid=1234567890): 33 | gr.top_block.__init__(self, "Gotenna Pro Tx Usrp", catch_exceptions=True) 34 | 35 | ################################################## 36 | # Parameters 37 | ################################################## 38 | self.baud_rate = baud_rate 39 | self.callsign = callsign 40 | self.counter_num = counter_num 41 | self.frequency = frequency 42 | self.message = message 43 | self.message_type = message_type 44 | self.publickey_data = publickey_data 45 | self.recipient_gid = recipient_gid 46 | self.sender_gid = sender_gid 47 | 48 | ################################################## 49 | # Variables 50 | ################################################## 51 | self.samp_per_sym = samp_per_sym = 4 52 | self.offset = offset = 250000 53 | self.interp = interp = 480000 // baud_rate 54 | self.taps = taps = firdes.gaussian(1.0, samp_per_sym, 0.5, 64) 55 | self.samp_rate = samp_rate = baud_rate * samp_per_sym * interp 56 | self.packets = packets = gotenna_packet.encode_pro_broadcast_packets(message_type, counter_num, sender_gid, recipient_gid, callsign, message, publickey_data) 57 | self.fsk_deviation_hz = fsk_deviation_hz = {2400: 400, 4800: 750, 9600: 1100}[baud_rate] 58 | self.data_chan = data_chan = 2 59 | self.control_chan = control_chan = 2 60 | self.center_freq = center_freq = frequency - offset 61 | 62 | ################################################## 63 | # Blocks 64 | ################################################## 65 | 66 | self.uhd_usrp_sink_0 = uhd.usrp_sink( 67 | ",".join(("", "")), 68 | uhd.stream_args( 69 | cpu_format="fc32", 70 | args='', 71 | channels=list(range(0,1)), 72 | ), 73 | '', 74 | ) 75 | self.uhd_usrp_sink_0.set_samp_rate(samp_rate) 76 | self.uhd_usrp_sink_0.set_time_unknown_pps(uhd.time_spec(0)) 77 | 78 | self.uhd_usrp_sink_0.set_center_freq(center_freq, 0) 79 | self.uhd_usrp_sink_0.set_antenna("TX/RX", 0) 80 | self.uhd_usrp_sink_0.set_gain(70, 0) 81 | self.rational_resampler_xxx_0 = filter.rational_resampler_ccc( 82 | interpolation=interp, 83 | decimation=1, 84 | taps=[], 85 | fractional_bw=0) 86 | self.interp_fir_filter_xxx_0 = filter.interp_fir_filter_fff(1, taps) 87 | self.interp_fir_filter_xxx_0.declare_sample_delay(0) 88 | self.digital_map_bb_0 = digital.map_bb([-3, -1, 3, 1, 0]) 89 | self.blocks_vector_source_x_2 = blocks.vector_source_f([offset], True, 1, []) 90 | self.blocks_vector_source_x_1 = blocks.vector_source_c([1], True, 1, []) 91 | self.blocks_vector_source_x_0 = blocks.vector_source_b(gotenna_packet.pro_gfsk_symbols(packets), False, 1, []) 92 | self.blocks_vco_c_1 = blocks.vco_c((baud_rate * samp_per_sym), (2 * math.pi * fsk_deviation_hz), 1.0) 93 | self.blocks_vco_c_0 = blocks.vco_c(samp_rate, (2*math.pi), 0.9) 94 | self.blocks_repeat_2 = blocks.repeat(gr.sizeof_float*1, samp_per_sym) 95 | self.blocks_repeat_1 = blocks.repeat(gr.sizeof_float*1, (8 * samp_per_sym * interp)) 96 | self.blocks_repeat_0 = blocks.repeat(gr.sizeof_gr_complex*1, (8 * samp_per_sym)) 97 | self.blocks_multiply_xx_1 = blocks.multiply_vcc(1) 98 | self.blocks_multiply_xx_0 = blocks.multiply_vcc(1) 99 | self.blocks_char_to_float_0 = blocks.char_to_float(1, 1) 100 | 101 | 102 | ################################################## 103 | # Connections 104 | ################################################## 105 | self.connect((self.blocks_char_to_float_0, 0), (self.blocks_repeat_2, 0)) 106 | self.connect((self.blocks_multiply_xx_0, 0), (self.rational_resampler_xxx_0, 0)) 107 | self.connect((self.blocks_multiply_xx_1, 0), (self.uhd_usrp_sink_0, 0)) 108 | self.connect((self.blocks_repeat_0, 0), (self.blocks_multiply_xx_0, 1)) 109 | self.connect((self.blocks_repeat_1, 0), (self.blocks_vco_c_0, 0)) 110 | self.connect((self.blocks_repeat_2, 0), (self.interp_fir_filter_xxx_0, 0)) 111 | self.connect((self.blocks_vco_c_0, 0), (self.blocks_multiply_xx_1, 1)) 112 | self.connect((self.blocks_vco_c_1, 0), (self.blocks_multiply_xx_0, 0)) 113 | self.connect((self.blocks_vector_source_x_0, 0), (self.digital_map_bb_0, 0)) 114 | self.connect((self.blocks_vector_source_x_1, 0), (self.blocks_repeat_0, 0)) 115 | self.connect((self.blocks_vector_source_x_2, 0), (self.blocks_repeat_1, 0)) 116 | self.connect((self.digital_map_bb_0, 0), (self.blocks_char_to_float_0, 0)) 117 | self.connect((self.interp_fir_filter_xxx_0, 0), (self.blocks_vco_c_1, 0)) 118 | self.connect((self.rational_resampler_xxx_0, 0), (self.blocks_multiply_xx_1, 0)) 119 | 120 | 121 | def get_baud_rate(self): 122 | return self.baud_rate 123 | 124 | def set_baud_rate(self, baud_rate): 125 | self.baud_rate = baud_rate 126 | self.set_fsk_deviation_hz({2400: 400, 4800: 750, 9600: 1100}[self.baud_rate]) 127 | self.set_interp(480000 // self.baud_rate) 128 | self.set_samp_rate(self.baud_rate * self.samp_per_sym * self.interp) 129 | 130 | def get_callsign(self): 131 | return self.callsign 132 | 133 | def set_callsign(self, callsign): 134 | self.callsign = callsign 135 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 136 | 137 | def get_counter_num(self): 138 | return self.counter_num 139 | 140 | def set_counter_num(self, counter_num): 141 | self.counter_num = counter_num 142 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 143 | 144 | def get_frequency(self): 145 | return self.frequency 146 | 147 | def set_frequency(self, frequency): 148 | self.frequency = frequency 149 | self.set_center_freq(self.frequency - self.offset) 150 | 151 | def get_message(self): 152 | return self.message 153 | 154 | def set_message(self, message): 155 | self.message = message 156 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 157 | 158 | def get_message_type(self): 159 | return self.message_type 160 | 161 | def set_message_type(self, message_type): 162 | self.message_type = message_type 163 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 164 | 165 | def get_publickey_data(self): 166 | return self.publickey_data 167 | 168 | def set_publickey_data(self, publickey_data): 169 | self.publickey_data = publickey_data 170 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 171 | 172 | def get_recipient_gid(self): 173 | return self.recipient_gid 174 | 175 | def set_recipient_gid(self, recipient_gid): 176 | self.recipient_gid = recipient_gid 177 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 178 | 179 | def get_sender_gid(self): 180 | return self.sender_gid 181 | 182 | def set_sender_gid(self, sender_gid): 183 | self.sender_gid = sender_gid 184 | self.set_packets(gotenna_packet.encode_pro_broadcast_packets(self.message_type, self.counter_num, self.sender_gid, self.recipient_gid, self.callsign, self.message, self.publickey_data)) 185 | 186 | def get_samp_per_sym(self): 187 | return self.samp_per_sym 188 | 189 | def set_samp_per_sym(self, samp_per_sym): 190 | self.samp_per_sym = samp_per_sym 191 | self.set_samp_rate(self.baud_rate * self.samp_per_sym * self.interp) 192 | self.set_taps(firdes.gaussian(1.0, self.samp_per_sym, 0.5, 64)) 193 | self.blocks_repeat_0.set_interpolation((8 * self.samp_per_sym)) 194 | self.blocks_repeat_1.set_interpolation((8 * self.samp_per_sym * self.interp)) 195 | self.blocks_repeat_2.set_interpolation(self.samp_per_sym) 196 | 197 | def get_offset(self): 198 | return self.offset 199 | 200 | def set_offset(self, offset): 201 | self.offset = offset 202 | self.set_center_freq(self.frequency - self.offset) 203 | self.blocks_vector_source_x_2.set_data([self.offset], []) 204 | 205 | def get_interp(self): 206 | return self.interp 207 | 208 | def set_interp(self, interp): 209 | self.interp = interp 210 | self.set_samp_rate(self.baud_rate * self.samp_per_sym * self.interp) 211 | self.blocks_repeat_1.set_interpolation((8 * self.samp_per_sym * self.interp)) 212 | 213 | def get_taps(self): 214 | return self.taps 215 | 216 | def set_taps(self, taps): 217 | self.taps = taps 218 | self.interp_fir_filter_xxx_0.set_taps(self.taps) 219 | 220 | def get_samp_rate(self): 221 | return self.samp_rate 222 | 223 | def set_samp_rate(self, samp_rate): 224 | self.samp_rate = samp_rate 225 | self.uhd_usrp_sink_0.set_samp_rate(self.samp_rate) 226 | 227 | def get_packets(self): 228 | return self.packets 229 | 230 | def set_packets(self, packets): 231 | self.packets = packets 232 | self.blocks_vector_source_x_0.set_data(gotenna_packet.pro_gfsk_symbols(self.packets), []) 233 | 234 | def get_fsk_deviation_hz(self): 235 | return self.fsk_deviation_hz 236 | 237 | def set_fsk_deviation_hz(self, fsk_deviation_hz): 238 | self.fsk_deviation_hz = fsk_deviation_hz 239 | 240 | def get_data_chan(self): 241 | return self.data_chan 242 | 243 | def set_data_chan(self, data_chan): 244 | self.data_chan = data_chan 245 | 246 | def get_control_chan(self): 247 | return self.control_chan 248 | 249 | def set_control_chan(self, control_chan): 250 | self.control_chan = control_chan 251 | 252 | def get_center_freq(self): 253 | return self.center_freq 254 | 255 | def set_center_freq(self, center_freq): 256 | self.center_freq = center_freq 257 | self.uhd_usrp_sink_0.set_center_freq(self.center_freq, 0) 258 | 259 | 260 | 261 | def argument_parser(): 262 | parser = ArgumentParser() 263 | parser.add_argument( 264 | "--baud-rate", dest="baud_rate", type=intx, default=9600, 265 | help="Set Baud rate [default=%(default)r]") 266 | parser.add_argument( 267 | "--callsign", dest="callsign", type=str, default='VE3IRR', 268 | help="Set Sender callsign [default=%(default)r]") 269 | parser.add_argument( 270 | "--counter-num", dest="counter_num", type=intx, default=0, 271 | help="Set Counter [default=%(default)r]") 272 | parser.add_argument( 273 | "--frequency", dest="frequency", type=eng_float, default=eng_notation.num_to_str(float(450000000)), 274 | help="Set Frequency [default=%(default)r]") 275 | parser.add_argument( 276 | "--message", dest="message", type=str, default='Hello world!', 277 | help="Set Message [default=%(default)r]") 278 | parser.add_argument( 279 | "--message-type", dest="message_type", type=str, default='BROADCAST', 280 | help="Set type [default=%(default)r]") 281 | parser.add_argument( 282 | "--publickey-data", dest="publickey_data", type=str, default='base64key', 283 | help="Set Sender Public Key [default=%(default)r]") 284 | parser.add_argument( 285 | "--recipient-gid", dest="recipient_gid", type=intx, default=1234567891, 286 | help="Set Recipient GID [default=%(default)r]") 287 | parser.add_argument( 288 | "--sender-gid", dest="sender_gid", type=intx, default=1234567890, 289 | help="Set Sender GID [default=%(default)r]") 290 | return parser 291 | 292 | 293 | def main(top_block_cls=gotenna_pro_tx_usrp, options=None): 294 | if options is None: 295 | options = argument_parser().parse_args() 296 | tb = top_block_cls(baud_rate=options.baud_rate, callsign=options.callsign, counter_num=options.counter_num, frequency=options.frequency, message=options.message, message_type=options.message_type, publickey_data=options.publickey_data, recipient_gid=options.recipient_gid, sender_gid=options.sender_gid) 297 | 298 | def sig_handler(sig=None, frame=None): 299 | tb.stop() 300 | tb.wait() 301 | 302 | sys.exit(0) 303 | 304 | signal.signal(signal.SIGINT, sig_handler) 305 | signal.signal(signal.SIGTERM, sig_handler) 306 | 307 | tb.start() 308 | 309 | tb.wait() 310 | 311 | 312 | if __name__ == '__main__': 313 | main() 314 | -------------------------------------------------------------------------------- /docs/doxygen/update_pydoc.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2010-2012 Free Software Foundation, Inc. 3 | # 4 | # This file was generated by gr_modtool, a tool from the GNU Radio framework 5 | # This file is a part of gnuradio 6 | # 7 | # SPDX-License-Identifier: GPL-3.0-or-later 8 | # 9 | # 10 | """ 11 | Updates the *pydoc_h files for a module 12 | Execute using: python update_pydoc.py xml_path outputfilename 13 | 14 | The file instructs Pybind11 to transfer the doxygen comments into the 15 | python docstrings. 16 | 17 | """ 18 | 19 | import os 20 | import sys 21 | import time 22 | import glob 23 | import re 24 | import json 25 | from argparse import ArgumentParser 26 | 27 | from doxyxml import DoxyIndex, DoxyClass, DoxyFriend, DoxyFunction, DoxyFile 28 | from doxyxml import DoxyOther, base 29 | 30 | 31 | def py_name(name): 32 | bits = name.split('_') 33 | return '_'.join(bits[1:]) 34 | 35 | 36 | def make_name(name): 37 | bits = name.split('_') 38 | return bits[0] + '_make_' + '_'.join(bits[1:]) 39 | 40 | 41 | class Block(object): 42 | """ 43 | Checks if doxyxml produced objects correspond to a gnuradio block. 44 | """ 45 | 46 | @classmethod 47 | def includes(cls, item): 48 | if not isinstance(item, DoxyClass): 49 | return False 50 | # Check for a parsing error. 51 | if item.error(): 52 | return False 53 | friendname = make_name(item.name()) 54 | is_a_block = item.has_member(friendname, DoxyFriend) 55 | # But now sometimes the make function isn't a friend so check again. 56 | if not is_a_block: 57 | is_a_block = di.has_member(friendname, DoxyFunction) 58 | return is_a_block 59 | 60 | 61 | class Block2(object): 62 | """ 63 | Checks if doxyxml produced objects correspond to a new style 64 | gnuradio block. 65 | """ 66 | 67 | @classmethod 68 | def includes(cls, item): 69 | if not isinstance(item, DoxyClass): 70 | return False 71 | # Check for a parsing error. 72 | if item.error(): 73 | return False 74 | is_a_block2 = item.has_member( 75 | 'make', DoxyFunction) and item.has_member('sptr', DoxyOther) 76 | return is_a_block2 77 | 78 | 79 | def utoascii(text): 80 | """ 81 | Convert unicode text into ascii and escape quotes and backslashes. 82 | """ 83 | if text is None: 84 | return '' 85 | out = text.encode('ascii', 'replace') 86 | # swig will require us to replace blackslash with 4 backslashes 87 | # TODO: evaluate what this should be for pybind11 88 | out = out.replace(b'\\', b'\\\\\\\\') 89 | out = out.replace(b'"', b'\\"').decode('ascii') 90 | return str(out) 91 | 92 | 93 | def combine_descriptions(obj): 94 | """ 95 | Combines the brief and detailed descriptions of an object together. 96 | """ 97 | description = [] 98 | bd = obj.brief_description.strip() 99 | dd = obj.detailed_description.strip() 100 | if bd: 101 | description.append(bd) 102 | if dd: 103 | description.append(dd) 104 | return utoascii('\n\n'.join(description)).strip() 105 | 106 | 107 | def format_params(parameteritems): 108 | output = ['Args:'] 109 | template = ' {0} : {1}' 110 | for pi in parameteritems: 111 | output.append(template.format(pi.name, pi.description)) 112 | return '\n'.join(output) 113 | 114 | 115 | entry_templ = '%feature("docstring") {name} "{docstring}"' 116 | 117 | 118 | def make_entry(obj, name=None, templ="{description}", description=None, params=[]): 119 | """ 120 | Create a docstring key/value pair, where the key is the object name. 121 | 122 | obj - a doxyxml object from which documentation will be extracted. 123 | name - the name of the C object (defaults to obj.name()) 124 | templ - an optional template for the docstring containing only one 125 | variable named 'description'. 126 | description - if this optional variable is set then it's value is 127 | used as the description instead of extracting it from obj. 128 | """ 129 | if name is None: 130 | name = obj.name() 131 | if hasattr(obj, '_parse_data') and hasattr(obj._parse_data, 'definition'): 132 | name = obj._parse_data.definition.split(' ')[-1] 133 | if "operator " in name: 134 | return '' 135 | if description is None: 136 | description = combine_descriptions(obj) 137 | if params: 138 | description += '\n\n' 139 | description += utoascii(format_params(params)) 140 | docstring = templ.format(description=description) 141 | 142 | return {name: docstring} 143 | 144 | 145 | def make_class_entry(klass, description=None, ignored_methods=[], params=None): 146 | """ 147 | Create a class docstring key/value pair. 148 | """ 149 | if params is None: 150 | params = klass.params 151 | output = {} 152 | output.update(make_entry(klass, description=description, params=params)) 153 | for func in klass.in_category(DoxyFunction): 154 | if func.name() not in ignored_methods: 155 | name = klass.name() + '::' + func.name() 156 | output.update(make_entry(func, name=name)) 157 | return output 158 | 159 | 160 | def make_block_entry(di, block): 161 | """ 162 | Create class and function docstrings of a gnuradio block 163 | """ 164 | descriptions = [] 165 | # Get the documentation associated with the class. 166 | class_desc = combine_descriptions(block) 167 | if class_desc: 168 | descriptions.append(class_desc) 169 | # Get the documentation associated with the make function 170 | make_func = di.get_member(make_name(block.name()), DoxyFunction) 171 | make_func_desc = combine_descriptions(make_func) 172 | if make_func_desc: 173 | descriptions.append(make_func_desc) 174 | # Get the documentation associated with the file 175 | try: 176 | block_file = di.get_member(block.name() + ".h", DoxyFile) 177 | file_desc = combine_descriptions(block_file) 178 | if file_desc: 179 | descriptions.append(file_desc) 180 | except base.Base.NoSuchMember: 181 | # Don't worry if we can't find a matching file. 182 | pass 183 | # And join them all together to make a super duper description. 184 | super_description = "\n\n".join(descriptions) 185 | # Associate the combined description with the class and 186 | # the make function. 187 | output = {} 188 | output.update(make_class_entry(block, description=super_description)) 189 | output.update(make_entry(make_func, description=super_description, 190 | params=block.params)) 191 | return output 192 | 193 | 194 | def make_block2_entry(di, block): 195 | """ 196 | Create class and function docstrings of a new style gnuradio block 197 | """ 198 | # For new style blocks all the relevant documentation should be 199 | # associated with the 'make' method. 200 | class_description = combine_descriptions(block) 201 | make_func = block.get_member('make', DoxyFunction) 202 | make_description = combine_descriptions(make_func) 203 | description = class_description + \ 204 | "\n\nConstructor Specific Documentation:\n\n" + make_description 205 | # Associate the combined description with the class and 206 | # the make function. 207 | output = {} 208 | output.update(make_class_entry( 209 | block, description=description, 210 | ignored_methods=['make'], params=make_func.params)) 211 | makename = block.name() + '::make' 212 | output.update(make_entry( 213 | make_func, name=makename, description=description, 214 | params=make_func.params)) 215 | return output 216 | 217 | 218 | def get_docstrings_dict(di, custom_output=None): 219 | 220 | output = {} 221 | if custom_output: 222 | output.update(custom_output) 223 | 224 | # Create docstrings for the blocks. 225 | blocks = di.in_category(Block) 226 | blocks2 = di.in_category(Block2) 227 | 228 | make_funcs = set([]) 229 | for block in blocks: 230 | try: 231 | make_func = di.get_member(make_name(block.name()), DoxyFunction) 232 | # Don't want to risk writing to output twice. 233 | if make_func.name() not in make_funcs: 234 | make_funcs.add(make_func.name()) 235 | output.update(make_block_entry(di, block)) 236 | except block.ParsingError: 237 | sys.stderr.write( 238 | 'Parsing error for block {0}\n'.format(block.name())) 239 | raise 240 | 241 | for block in blocks2: 242 | try: 243 | make_func = block.get_member('make', DoxyFunction) 244 | make_func_name = block.name() + '::make' 245 | # Don't want to risk writing to output twice. 246 | if make_func_name not in make_funcs: 247 | make_funcs.add(make_func_name) 248 | output.update(make_block2_entry(di, block)) 249 | except block.ParsingError: 250 | sys.stderr.write( 251 | 'Parsing error for block {0}\n'.format(block.name())) 252 | raise 253 | 254 | # Create docstrings for functions 255 | # Don't include the make functions since they have already been dealt with. 256 | funcs = [f for f in di.in_category(DoxyFunction) 257 | if f.name() not in make_funcs and not f.name().startswith('std::')] 258 | for f in funcs: 259 | try: 260 | output.update(make_entry(f)) 261 | except f.ParsingError: 262 | sys.stderr.write( 263 | 'Parsing error for function {0}\n'.format(f.name())) 264 | 265 | # Create docstrings for classes 266 | block_names = [block.name() for block in blocks] 267 | block_names += [block.name() for block in blocks2] 268 | klasses = [k for k in di.in_category(DoxyClass) 269 | if k.name() not in block_names and not k.name().startswith('std::')] 270 | for k in klasses: 271 | try: 272 | output.update(make_class_entry(k)) 273 | except k.ParsingError: 274 | sys.stderr.write('Parsing error for class {0}\n'.format(k.name())) 275 | 276 | # Docstrings are not created for anything that is not a function or a class. 277 | # If this excludes anything important please add it here. 278 | 279 | return output 280 | 281 | 282 | def sub_docstring_in_pydoc_h(pydoc_files, docstrings_dict, output_dir, filter_str=None): 283 | if filter_str: 284 | docstrings_dict = { 285 | k: v for k, v in docstrings_dict.items() if k.startswith(filter_str)} 286 | 287 | with open(os.path.join(output_dir, 'docstring_status'), 'w') as status_file: 288 | 289 | for pydoc_file in pydoc_files: 290 | if filter_str: 291 | filter_str2 = "::".join((filter_str, os.path.split( 292 | pydoc_file)[-1].split('_pydoc_template.h')[0])) 293 | docstrings_dict2 = { 294 | k: v for k, v in docstrings_dict.items() if k.startswith(filter_str2)} 295 | else: 296 | docstrings_dict2 = docstrings_dict 297 | 298 | file_in = open(pydoc_file, 'r').read() 299 | for key, value in docstrings_dict2.items(): 300 | file_in_tmp = file_in 301 | try: 302 | doc_key = key.split("::") 303 | # if 'gr' in doc_key: 304 | # doc_key.remove('gr') 305 | doc_key = '_'.join(doc_key) 306 | regexp = r'(__doc_{} =\sR\"doc\()[^)]*(\)doc\")'.format( 307 | doc_key) 308 | regexp = re.compile(regexp, re.MULTILINE) 309 | 310 | (file_in, nsubs) = regexp.subn( 311 | r'\1' + value + r'\2', file_in, count=1) 312 | if nsubs == 1: 313 | status_file.write("PASS: " + pydoc_file + "\n") 314 | except KeyboardInterrupt: 315 | raise KeyboardInterrupt 316 | except: # be permissive, TODO log, but just leave the docstring blank 317 | status_file.write("FAIL: " + pydoc_file + "\n") 318 | file_in = file_in_tmp 319 | 320 | output_pathname = os.path.join(output_dir, os.path.basename( 321 | pydoc_file).replace('_template.h', '.h')) 322 | with open(output_pathname, 'w') as file_out: 323 | file_out.write(file_in) 324 | 325 | 326 | def copy_docstring_templates(pydoc_files, output_dir): 327 | with open(os.path.join(output_dir, 'docstring_status'), 'w') as status_file: 328 | for pydoc_file in pydoc_files: 329 | file_in = open(pydoc_file, 'r').read() 330 | output_pathname = os.path.join(output_dir, os.path.basename( 331 | pydoc_file).replace('_template.h', '.h')) 332 | with open(output_pathname, 'w') as file_out: 333 | file_out.write(file_in) 334 | status_file.write("DONE") 335 | 336 | 337 | def argParse(): 338 | """Parses commandline args.""" 339 | desc = 'Scrape the doxygen generated xml for docstrings to insert into python bindings' 340 | parser = ArgumentParser(description=desc) 341 | 342 | parser.add_argument("function", help="Operation to perform on docstrings", choices=[ 343 | "scrape", "sub", "copy"]) 344 | 345 | parser.add_argument("--xml_path") 346 | parser.add_argument("--bindings_dir") 347 | parser.add_argument("--output_dir") 348 | parser.add_argument("--json_path") 349 | parser.add_argument("--filter", default=None) 350 | 351 | return parser.parse_args() 352 | 353 | 354 | if __name__ == "__main__": 355 | # Parse command line options and set up doxyxml. 356 | args = argParse() 357 | if args.function.lower() == 'scrape': 358 | di = DoxyIndex(args.xml_path) 359 | docstrings_dict = get_docstrings_dict(di) 360 | with open(args.json_path, 'w') as fp: 361 | json.dump(docstrings_dict, fp) 362 | elif args.function.lower() == 'sub': 363 | with open(args.json_path, 'r') as fp: 364 | docstrings_dict = json.load(fp) 365 | pydoc_files = glob.glob(os.path.join( 366 | args.bindings_dir, '*_pydoc_template.h')) 367 | sub_docstring_in_pydoc_h( 368 | pydoc_files, docstrings_dict, args.output_dir, args.filter) 369 | elif args.function.lower() == 'copy': 370 | pydoc_files = glob.glob(os.path.join( 371 | args.bindings_dir, '*_pydoc_template.h')) 372 | copy_docstring_templates(pydoc_files, args.output_dir) 373 | -------------------------------------------------------------------------------- /apps/gotenna_pro_rx_usrp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Pro Rx Usrp 9 | # GNU Radio version: 3.10.7.0 10 | 11 | from packaging.version import Version as StrictVersion 12 | from PyQt5 import Qt 13 | from gnuradio import qtgui 14 | from gnuradio import analog 15 | import math 16 | from gnuradio import blocks 17 | from gnuradio import digital 18 | from gnuradio import filter 19 | from gnuradio.filter import firdes 20 | from gnuradio import gr 21 | from gnuradio.fft import window 22 | import sys 23 | import signal 24 | from PyQt5 import Qt 25 | from argparse import ArgumentParser 26 | from gnuradio.eng_arg import eng_float, intx 27 | from gnuradio import eng_notation 28 | from gnuradio import tenna 29 | from gnuradio import uhd 30 | import time 31 | import sip 32 | 33 | 34 | 35 | class gotenna_pro_rx_usrp(gr.top_block, Qt.QWidget): 36 | 37 | def __init__(self): 38 | gr.top_block.__init__(self, "Gotenna Pro Rx Usrp", catch_exceptions=True) 39 | Qt.QWidget.__init__(self) 40 | self.setWindowTitle("Gotenna Pro Rx Usrp") 41 | qtgui.util.check_set_qss() 42 | try: 43 | self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc')) 44 | except BaseException as exc: 45 | print(f"Qt GUI: Could not set Icon: {str(exc)}", file=sys.stderr) 46 | self.top_scroll_layout = Qt.QVBoxLayout() 47 | self.setLayout(self.top_scroll_layout) 48 | self.top_scroll = Qt.QScrollArea() 49 | self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame) 50 | self.top_scroll_layout.addWidget(self.top_scroll) 51 | self.top_scroll.setWidgetResizable(True) 52 | self.top_widget = Qt.QWidget() 53 | self.top_scroll.setWidget(self.top_widget) 54 | self.top_layout = Qt.QVBoxLayout(self.top_widget) 55 | self.top_grid_layout = Qt.QGridLayout() 56 | self.top_layout.addLayout(self.top_grid_layout) 57 | 58 | self.settings = Qt.QSettings("GNU Radio", "gotenna_pro_rx_usrp") 59 | 60 | try: 61 | if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"): 62 | self.restoreGeometry(self.settings.value("geometry").toByteArray()) 63 | else: 64 | self.restoreGeometry(self.settings.value("geometry")) 65 | except BaseException as exc: 66 | print(f"Qt GUI: Could not restore geometry: {str(exc)}", file=sys.stderr) 67 | 68 | ################################################## 69 | # Variables 70 | ################################################## 71 | self.samp_rate = samp_rate = 1000000 72 | self.baud_rate = baud_rate = 9600 73 | self.offset = offset = 250000 74 | self.lp_taps = lp_taps = firdes.low_pass(1.0, samp_rate, 25000 * (baud_rate / 9600),15000 * (baud_rate / 9600), window.WIN_HAMMING, 6.76) 75 | self.fsk_deviation_hz = fsk_deviation_hz = {2400: 800, 4800: 1500, 9600: 2200}[baud_rate] 76 | self.fsk_constellation = fsk_constellation = digital.constellation_calcdist([-1.5, -0.5, +1.5, +0.5], [0, 1, 2, 3], 77 | 2, 1, digital.constellation.NO_NORMALIZATION).base() 78 | self.decim = decim = round(16 * (9600 / baud_rate)) 79 | self.dc_block_symbols = dc_block_symbols = 128 80 | self.center_freq = center_freq = int(451.000e6) 81 | 82 | ################################################## 83 | # Blocks 84 | ################################################## 85 | 86 | self.uhd_usrp_source_0 = uhd.usrp_source( 87 | ",".join(("", '')), 88 | uhd.stream_args( 89 | cpu_format="fc32", 90 | args='', 91 | channels=list(range(0,1)), 92 | ), 93 | ) 94 | self.uhd_usrp_source_0.set_samp_rate(samp_rate) 95 | self.uhd_usrp_source_0.set_time_unknown_pps(uhd.time_spec(0)) 96 | 97 | self.uhd_usrp_source_0.set_center_freq(center_freq - offset, 0) 98 | self.uhd_usrp_source_0.set_antenna("RX2", 0) 99 | self.uhd_usrp_source_0.set_gain(5, 0) 100 | self.tenna_pdu_to_pcapng_0 = tenna.pdu_to_pcapng('gotenna_pro.pcapng', True) 101 | self.tenna_gotenna_decoder_0 = tenna.gotenna_decoder(1) 102 | self.qtgui_waterfall_sink_x_1_0 = qtgui.waterfall_sink_c( 103 | 1024, #size 104 | window.WIN_FLATTOP, #wintype 105 | center_freq, #fc 106 | (samp_rate / decim), #bw 107 | "", #name 108 | 1, #number of inputs 109 | None # parent 110 | ) 111 | self.qtgui_waterfall_sink_x_1_0.set_update_time(0.01) 112 | self.qtgui_waterfall_sink_x_1_0.enable_grid(False) 113 | self.qtgui_waterfall_sink_x_1_0.enable_axis_labels(True) 114 | 115 | 116 | 117 | labels = ['', '', '', '', '', 118 | '', '', '', '', ''] 119 | colors = [0, 0, 0, 0, 0, 120 | 0, 0, 0, 0, 0] 121 | alphas = [1.0, 1.0, 1.0, 1.0, 1.0, 122 | 1.0, 1.0, 1.0, 1.0, 1.0] 123 | 124 | for i in range(1): 125 | if len(labels[i]) == 0: 126 | self.qtgui_waterfall_sink_x_1_0.set_line_label(i, "Data {0}".format(i)) 127 | else: 128 | self.qtgui_waterfall_sink_x_1_0.set_line_label(i, labels[i]) 129 | self.qtgui_waterfall_sink_x_1_0.set_color_map(i, colors[i]) 130 | self.qtgui_waterfall_sink_x_1_0.set_line_alpha(i, alphas[i]) 131 | 132 | self.qtgui_waterfall_sink_x_1_0.set_intensity_range(-140, 10) 133 | 134 | self._qtgui_waterfall_sink_x_1_0_win = sip.wrapinstance(self.qtgui_waterfall_sink_x_1_0.qwidget(), Qt.QWidget) 135 | 136 | self.top_grid_layout.addWidget(self._qtgui_waterfall_sink_x_1_0_win, 1, 1, 1, 1) 137 | for r in range(1, 2): 138 | self.top_grid_layout.setRowStretch(r, 1) 139 | for c in range(1, 2): 140 | self.top_grid_layout.setColumnStretch(c, 1) 141 | self.qtgui_freq_sink_x_1 = qtgui.freq_sink_c( 142 | 1024, #size 143 | window.WIN_BLACKMAN_hARRIS, #wintype 144 | center_freq, #fc 145 | (samp_rate / decim), #bw 146 | "", #name 147 | 1, 148 | None # parent 149 | ) 150 | self.qtgui_freq_sink_x_1.set_update_time(0.01) 151 | self.qtgui_freq_sink_x_1.set_y_axis((-140), 10) 152 | self.qtgui_freq_sink_x_1.set_y_label('Relative Gain', 'dB') 153 | self.qtgui_freq_sink_x_1.set_trigger_mode(qtgui.TRIG_MODE_FREE, 0.0, 0, "") 154 | self.qtgui_freq_sink_x_1.enable_autoscale(False) 155 | self.qtgui_freq_sink_x_1.enable_grid(False) 156 | self.qtgui_freq_sink_x_1.set_fft_average(1.0) 157 | self.qtgui_freq_sink_x_1.enable_axis_labels(True) 158 | self.qtgui_freq_sink_x_1.enable_control_panel(False) 159 | self.qtgui_freq_sink_x_1.set_fft_window_normalized(False) 160 | 161 | 162 | 163 | labels = ['', '', '', '', '', 164 | '', '', '', '', ''] 165 | widths = [1, 1, 1, 1, 1, 166 | 1, 1, 1, 1, 1] 167 | colors = ["blue", "red", "green", "black", "cyan", 168 | "magenta", "yellow", "dark red", "dark green", "dark blue"] 169 | alphas = [1.0, 1.0, 1.0, 1.0, 1.0, 170 | 1.0, 1.0, 1.0, 1.0, 1.0] 171 | 172 | for i in range(1): 173 | if len(labels[i]) == 0: 174 | self.qtgui_freq_sink_x_1.set_line_label(i, "Data {0}".format(i)) 175 | else: 176 | self.qtgui_freq_sink_x_1.set_line_label(i, labels[i]) 177 | self.qtgui_freq_sink_x_1.set_line_width(i, widths[i]) 178 | self.qtgui_freq_sink_x_1.set_line_color(i, colors[i]) 179 | self.qtgui_freq_sink_x_1.set_line_alpha(i, alphas[i]) 180 | 181 | self._qtgui_freq_sink_x_1_win = sip.wrapinstance(self.qtgui_freq_sink_x_1.qwidget(), Qt.QWidget) 182 | self.top_layout.addWidget(self._qtgui_freq_sink_x_1_win) 183 | self.freq_xlating_fir_filter_xxx_0 = filter.freq_xlating_fir_filter_ccc(decim, lp_taps, offset, samp_rate) 184 | self.digital_symbol_sync_xx_0 = digital.symbol_sync_ff( 185 | digital.TED_GARDNER, 186 | ((samp_rate / decim) / baud_rate), 187 | 0.045, 188 | 1.0, 189 | 1.0, 190 | (0.001 * (samp_rate / decim) / baud_rate), 191 | 1, 192 | digital.constellation_bpsk().base(), 193 | digital.IR_MMSE_8TAP, 194 | 128, 195 | []) 196 | self.digital_constellation_decoder_cb_0 = digital.constellation_decoder_cb(fsk_constellation) 197 | self.dc_blocker_xx_0 = filter.dc_blocker_ff((round(dc_block_symbols * ((samp_rate / decim) / baud_rate))), True) 198 | self.blocks_repack_bits_bb_0 = blocks.repack_bits_bb(2, 1, "", False, gr.GR_MSB_FIRST) 199 | self.blocks_float_to_complex_0 = blocks.float_to_complex(1) 200 | self.analog_quadrature_demod_cf_0 = analog.quadrature_demod_cf(((samp_rate / decim) / (2 * math.pi * fsk_deviation_hz))) 201 | 202 | 203 | ################################################## 204 | # Connections 205 | ################################################## 206 | self.msg_connect((self.tenna_gotenna_decoder_0, 'pdu'), (self.tenna_pdu_to_pcapng_0, 'pdu')) 207 | self.connect((self.analog_quadrature_demod_cf_0, 0), (self.dc_blocker_xx_0, 0)) 208 | self.connect((self.blocks_float_to_complex_0, 0), (self.digital_constellation_decoder_cb_0, 0)) 209 | self.connect((self.blocks_repack_bits_bb_0, 0), (self.tenna_gotenna_decoder_0, 0)) 210 | self.connect((self.dc_blocker_xx_0, 0), (self.digital_symbol_sync_xx_0, 0)) 211 | self.connect((self.digital_constellation_decoder_cb_0, 0), (self.blocks_repack_bits_bb_0, 0)) 212 | self.connect((self.digital_symbol_sync_xx_0, 0), (self.blocks_float_to_complex_0, 0)) 213 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.analog_quadrature_demod_cf_0, 0)) 214 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.qtgui_freq_sink_x_1, 0)) 215 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.qtgui_waterfall_sink_x_1_0, 0)) 216 | self.connect((self.uhd_usrp_source_0, 0), (self.freq_xlating_fir_filter_xxx_0, 0)) 217 | 218 | 219 | def closeEvent(self, event): 220 | self.settings = Qt.QSettings("GNU Radio", "gotenna_pro_rx_usrp") 221 | self.settings.setValue("geometry", self.saveGeometry()) 222 | self.stop() 223 | self.wait() 224 | 225 | event.accept() 226 | 227 | def get_samp_rate(self): 228 | return self.samp_rate 229 | 230 | def set_samp_rate(self, samp_rate): 231 | self.samp_rate = samp_rate 232 | self.set_lp_taps(firdes.low_pass(1.0, self.samp_rate, 25000 * (self.baud_rate / 9600), 15000 * (self.baud_rate / 9600), window.WIN_HAMMING, 6.76)) 233 | self.analog_quadrature_demod_cf_0.set_gain(((self.samp_rate / self.decim) / (2 * math.pi * self.fsk_deviation_hz))) 234 | self.qtgui_freq_sink_x_1.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 235 | self.qtgui_waterfall_sink_x_1_0.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 236 | self.uhd_usrp_source_0.set_samp_rate(self.samp_rate) 237 | 238 | def get_baud_rate(self): 239 | return self.baud_rate 240 | 241 | def set_baud_rate(self, baud_rate): 242 | self.baud_rate = baud_rate 243 | self.set_decim(round(16 * (9600 / self.baud_rate))) 244 | self.set_fsk_deviation_hz({2400: 800, 4800: 1500, 9600: 2200}[self.baud_rate]) 245 | self.set_lp_taps(firdes.low_pass(1.0, self.samp_rate, 25000 * (self.baud_rate / 9600), 15000 * (self.baud_rate / 9600), window.WIN_HAMMING, 6.76)) 246 | 247 | def get_offset(self): 248 | return self.offset 249 | 250 | def set_offset(self, offset): 251 | self.offset = offset 252 | self.freq_xlating_fir_filter_xxx_0.set_center_freq(self.offset) 253 | self.uhd_usrp_source_0.set_center_freq(self.center_freq - self.offset, 0) 254 | 255 | def get_lp_taps(self): 256 | return self.lp_taps 257 | 258 | def set_lp_taps(self, lp_taps): 259 | self.lp_taps = lp_taps 260 | self.freq_xlating_fir_filter_xxx_0.set_taps(self.lp_taps) 261 | 262 | def get_fsk_deviation_hz(self): 263 | return self.fsk_deviation_hz 264 | 265 | def set_fsk_deviation_hz(self, fsk_deviation_hz): 266 | self.fsk_deviation_hz = fsk_deviation_hz 267 | self.analog_quadrature_demod_cf_0.set_gain(((self.samp_rate / self.decim) / (2 * math.pi * self.fsk_deviation_hz))) 268 | 269 | def get_fsk_constellation(self): 270 | return self.fsk_constellation 271 | 272 | def set_fsk_constellation(self, fsk_constellation): 273 | self.fsk_constellation = fsk_constellation 274 | self.digital_constellation_decoder_cb_0.set_constellation(self.fsk_constellation) 275 | 276 | def get_decim(self): 277 | return self.decim 278 | 279 | def set_decim(self, decim): 280 | self.decim = decim 281 | self.analog_quadrature_demod_cf_0.set_gain(((self.samp_rate / self.decim) / (2 * math.pi * self.fsk_deviation_hz))) 282 | self.qtgui_freq_sink_x_1.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 283 | self.qtgui_waterfall_sink_x_1_0.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 284 | 285 | def get_dc_block_symbols(self): 286 | return self.dc_block_symbols 287 | 288 | def set_dc_block_symbols(self, dc_block_symbols): 289 | self.dc_block_symbols = dc_block_symbols 290 | 291 | def get_center_freq(self): 292 | return self.center_freq 293 | 294 | def set_center_freq(self, center_freq): 295 | self.center_freq = center_freq 296 | self.qtgui_freq_sink_x_1.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 297 | self.qtgui_waterfall_sink_x_1_0.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 298 | self.uhd_usrp_source_0.set_center_freq(self.center_freq - self.offset, 0) 299 | 300 | 301 | 302 | 303 | def main(top_block_cls=gotenna_pro_rx_usrp, options=None): 304 | 305 | if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"): 306 | style = gr.prefs().get_string('qtgui', 'style', 'raster') 307 | Qt.QApplication.setGraphicsSystem(style) 308 | qapp = Qt.QApplication(sys.argv) 309 | 310 | tb = top_block_cls() 311 | 312 | tb.start() 313 | 314 | tb.show() 315 | 316 | def sig_handler(sig=None, frame=None): 317 | tb.stop() 318 | tb.wait() 319 | 320 | Qt.QApplication.quit() 321 | 322 | signal.signal(signal.SIGINT, sig_handler) 323 | signal.signal(signal.SIGTERM, sig_handler) 324 | 325 | timer = Qt.QTimer() 326 | timer.start(500) 327 | timer.timeout.connect(lambda: None) 328 | 329 | qapp.exec_() 330 | 331 | if __name__ == '__main__': 332 | main() 333 | -------------------------------------------------------------------------------- /apps/gotenna_pro_rx_hackrf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Gotenna Pro Rx Hackrf 9 | # GNU Radio version: 3.10.9.2 10 | 11 | from PyQt5 import Qt 12 | from gnuradio import qtgui 13 | from gnuradio import analog 14 | import math 15 | from gnuradio import blocks 16 | from gnuradio import digital 17 | from gnuradio import filter 18 | from gnuradio.filter import firdes 19 | from gnuradio import gr 20 | from gnuradio.fft import window 21 | import sys 22 | import signal 23 | from PyQt5 import Qt 24 | from argparse import ArgumentParser 25 | from gnuradio.eng_arg import eng_float, intx 26 | from gnuradio import eng_notation 27 | from gnuradio import soapy 28 | from gnuradio import tenna 29 | import sip 30 | 31 | 32 | 33 | class gotenna_pro_rx_hackrf(gr.top_block, Qt.QWidget): 34 | 35 | def __init__(self): 36 | gr.top_block.__init__(self, "Gotenna Pro Rx Hackrf", catch_exceptions=True) 37 | Qt.QWidget.__init__(self) 38 | self.setWindowTitle("Gotenna Pro Rx Hackrf") 39 | qtgui.util.check_set_qss() 40 | try: 41 | self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc')) 42 | except BaseException as exc: 43 | print(f"Qt GUI: Could not set Icon: {str(exc)}", file=sys.stderr) 44 | self.top_scroll_layout = Qt.QVBoxLayout() 45 | self.setLayout(self.top_scroll_layout) 46 | self.top_scroll = Qt.QScrollArea() 47 | self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame) 48 | self.top_scroll_layout.addWidget(self.top_scroll) 49 | self.top_scroll.setWidgetResizable(True) 50 | self.top_widget = Qt.QWidget() 51 | self.top_scroll.setWidget(self.top_widget) 52 | self.top_layout = Qt.QVBoxLayout(self.top_widget) 53 | self.top_grid_layout = Qt.QGridLayout() 54 | self.top_layout.addLayout(self.top_grid_layout) 55 | 56 | self.settings = Qt.QSettings("GNU Radio", "gotenna_pro_rx_hackrf") 57 | 58 | try: 59 | geometry = self.settings.value("geometry") 60 | if geometry: 61 | self.restoreGeometry(geometry) 62 | except BaseException as exc: 63 | print(f"Qt GUI: Could not restore geometry: {str(exc)}", file=sys.stderr) 64 | 65 | ################################################## 66 | # Variables 67 | ################################################## 68 | self.samp_rate = samp_rate = 1000000 69 | self.baud_rate = baud_rate = 9600 70 | self.offset = offset = 250000 71 | self.lp_taps = lp_taps = firdes.low_pass(1.0, samp_rate, 25000 * (baud_rate / 9600),15000 * (baud_rate / 9600), window.WIN_HAMMING, 6.76) 72 | self.fsk_deviation_hz = fsk_deviation_hz = {2400: 800, 4800: 1500, 9600: 2200}[baud_rate] 73 | self.fsk_constellation = fsk_constellation = digital.constellation_calcdist([-1.5, -0.5, +1.5, +0.5], [0, 1, 2, 3], 74 | 2, 1, digital.constellation.NO_NORMALIZATION).base() 75 | self.fsk_constellation.set_npwr(1.0) 76 | self.decim = decim = round(16 * (9600 / baud_rate)) 77 | self.dc_block_symbols = dc_block_symbols = 128 78 | self.center_freq = center_freq = int(451.003e6) 79 | 80 | ################################################## 81 | # Blocks 82 | ################################################## 83 | 84 | self.tenna_pdu_to_pcapng_0 = tenna.pdu_to_pcapng('gotenna_pro.pcapng', True) 85 | self.tenna_gotenna_decoder_0 = tenna.gotenna_decoder(1) 86 | self.soapy_hackrf_source_0 = None 87 | dev = 'driver=hackrf' 88 | stream_args = '' 89 | tune_args = [''] 90 | settings = [''] 91 | 92 | self.soapy_hackrf_source_0 = soapy.source(dev, "fc32", 1, '', 93 | stream_args, tune_args, settings) 94 | self.soapy_hackrf_source_0.set_sample_rate(0, samp_rate) 95 | self.soapy_hackrf_source_0.set_bandwidth(0, 0) 96 | self.soapy_hackrf_source_0.set_frequency(0, (center_freq - offset)) 97 | self.soapy_hackrf_source_0.set_gain(0, 'AMP', False) 98 | self.soapy_hackrf_source_0.set_gain(0, 'LNA', min(max(16, 0.0), 40.0)) 99 | self.soapy_hackrf_source_0.set_gain(0, 'VGA', min(max(16, 0.0), 62.0)) 100 | self.qtgui_waterfall_sink_x_1_0 = qtgui.waterfall_sink_c( 101 | 1024, #size 102 | window.WIN_FLATTOP, #wintype 103 | center_freq, #fc 104 | (samp_rate / decim), #bw 105 | "", #name 106 | 1, #number of inputs 107 | None # parent 108 | ) 109 | self.qtgui_waterfall_sink_x_1_0.set_update_time(0.01) 110 | self.qtgui_waterfall_sink_x_1_0.enable_grid(False) 111 | self.qtgui_waterfall_sink_x_1_0.enable_axis_labels(True) 112 | 113 | 114 | 115 | labels = ['', '', '', '', '', 116 | '', '', '', '', ''] 117 | colors = [0, 0, 0, 0, 0, 118 | 0, 0, 0, 0, 0] 119 | alphas = [1.0, 1.0, 1.0, 1.0, 1.0, 120 | 1.0, 1.0, 1.0, 1.0, 1.0] 121 | 122 | for i in range(1): 123 | if len(labels[i]) == 0: 124 | self.qtgui_waterfall_sink_x_1_0.set_line_label(i, "Data {0}".format(i)) 125 | else: 126 | self.qtgui_waterfall_sink_x_1_0.set_line_label(i, labels[i]) 127 | self.qtgui_waterfall_sink_x_1_0.set_color_map(i, colors[i]) 128 | self.qtgui_waterfall_sink_x_1_0.set_line_alpha(i, alphas[i]) 129 | 130 | self.qtgui_waterfall_sink_x_1_0.set_intensity_range(-140, 10) 131 | 132 | self._qtgui_waterfall_sink_x_1_0_win = sip.wrapinstance(self.qtgui_waterfall_sink_x_1_0.qwidget(), Qt.QWidget) 133 | 134 | self.top_grid_layout.addWidget(self._qtgui_waterfall_sink_x_1_0_win, 1, 1, 1, 1) 135 | for r in range(1, 2): 136 | self.top_grid_layout.setRowStretch(r, 1) 137 | for c in range(1, 2): 138 | self.top_grid_layout.setColumnStretch(c, 1) 139 | self.qtgui_freq_sink_x_1 = qtgui.freq_sink_c( 140 | 1024, #size 141 | window.WIN_BLACKMAN_hARRIS, #wintype 142 | center_freq, #fc 143 | (samp_rate / decim), #bw 144 | "", #name 145 | 1, 146 | None # parent 147 | ) 148 | self.qtgui_freq_sink_x_1.set_update_time(0.01) 149 | self.qtgui_freq_sink_x_1.set_y_axis((-140), 10) 150 | self.qtgui_freq_sink_x_1.set_y_label('Relative Gain', 'dB') 151 | self.qtgui_freq_sink_x_1.set_trigger_mode(qtgui.TRIG_MODE_FREE, 0.0, 0, "") 152 | self.qtgui_freq_sink_x_1.enable_autoscale(False) 153 | self.qtgui_freq_sink_x_1.enable_grid(False) 154 | self.qtgui_freq_sink_x_1.set_fft_average(1.0) 155 | self.qtgui_freq_sink_x_1.enable_axis_labels(True) 156 | self.qtgui_freq_sink_x_1.enable_control_panel(False) 157 | self.qtgui_freq_sink_x_1.set_fft_window_normalized(False) 158 | 159 | 160 | 161 | labels = ['', '', '', '', '', 162 | '', '', '', '', ''] 163 | widths = [1, 1, 1, 1, 1, 164 | 1, 1, 1, 1, 1] 165 | colors = ["blue", "red", "green", "black", "cyan", 166 | "magenta", "yellow", "dark red", "dark green", "dark blue"] 167 | alphas = [1.0, 1.0, 1.0, 1.0, 1.0, 168 | 1.0, 1.0, 1.0, 1.0, 1.0] 169 | 170 | for i in range(1): 171 | if len(labels[i]) == 0: 172 | self.qtgui_freq_sink_x_1.set_line_label(i, "Data {0}".format(i)) 173 | else: 174 | self.qtgui_freq_sink_x_1.set_line_label(i, labels[i]) 175 | self.qtgui_freq_sink_x_1.set_line_width(i, widths[i]) 176 | self.qtgui_freq_sink_x_1.set_line_color(i, colors[i]) 177 | self.qtgui_freq_sink_x_1.set_line_alpha(i, alphas[i]) 178 | 179 | self._qtgui_freq_sink_x_1_win = sip.wrapinstance(self.qtgui_freq_sink_x_1.qwidget(), Qt.QWidget) 180 | self.top_layout.addWidget(self._qtgui_freq_sink_x_1_win) 181 | self.freq_xlating_fir_filter_xxx_0 = filter.freq_xlating_fir_filter_ccc(decim, lp_taps, offset, samp_rate) 182 | self.digital_symbol_sync_xx_0 = digital.symbol_sync_ff( 183 | digital.TED_GARDNER, 184 | ((samp_rate / decim) / baud_rate), 185 | 0.045, 186 | 1.0, 187 | 1.0, 188 | (0.001 * (samp_rate / decim) / baud_rate), 189 | 1, 190 | digital.constellation_bpsk().base(), 191 | digital.IR_MMSE_8TAP, 192 | 128, 193 | []) 194 | self.digital_constellation_decoder_cb_0 = digital.constellation_decoder_cb(fsk_constellation) 195 | self.dc_blocker_xx_0 = filter.dc_blocker_ff((round(dc_block_symbols * ((samp_rate / decim) / baud_rate))), True) 196 | self.blocks_repack_bits_bb_0 = blocks.repack_bits_bb(2, 1, "", False, gr.GR_MSB_FIRST) 197 | self.blocks_float_to_complex_0 = blocks.float_to_complex(1) 198 | self.analog_quadrature_demod_cf_0 = analog.quadrature_demod_cf(((samp_rate / decim) / (2 * math.pi * fsk_deviation_hz))) 199 | 200 | 201 | ################################################## 202 | # Connections 203 | ################################################## 204 | self.msg_connect((self.tenna_gotenna_decoder_0, 'pdu'), (self.tenna_pdu_to_pcapng_0, 'pdu')) 205 | self.connect((self.analog_quadrature_demod_cf_0, 0), (self.dc_blocker_xx_0, 0)) 206 | self.connect((self.blocks_float_to_complex_0, 0), (self.digital_constellation_decoder_cb_0, 0)) 207 | self.connect((self.blocks_repack_bits_bb_0, 0), (self.tenna_gotenna_decoder_0, 0)) 208 | self.connect((self.dc_blocker_xx_0, 0), (self.digital_symbol_sync_xx_0, 0)) 209 | self.connect((self.digital_constellation_decoder_cb_0, 0), (self.blocks_repack_bits_bb_0, 0)) 210 | self.connect((self.digital_symbol_sync_xx_0, 0), (self.blocks_float_to_complex_0, 0)) 211 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.analog_quadrature_demod_cf_0, 0)) 212 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.qtgui_freq_sink_x_1, 0)) 213 | self.connect((self.freq_xlating_fir_filter_xxx_0, 0), (self.qtgui_waterfall_sink_x_1_0, 0)) 214 | self.connect((self.soapy_hackrf_source_0, 0), (self.freq_xlating_fir_filter_xxx_0, 0)) 215 | 216 | 217 | def closeEvent(self, event): 218 | self.settings = Qt.QSettings("GNU Radio", "gotenna_pro_rx_hackrf") 219 | self.settings.setValue("geometry", self.saveGeometry()) 220 | self.stop() 221 | self.wait() 222 | 223 | event.accept() 224 | 225 | def get_samp_rate(self): 226 | return self.samp_rate 227 | 228 | def set_samp_rate(self, samp_rate): 229 | self.samp_rate = samp_rate 230 | self.set_lp_taps(firdes.low_pass(1.0, self.samp_rate, 25000 * (self.baud_rate / 9600), 15000 * (self.baud_rate / 9600), window.WIN_HAMMING, 6.76)) 231 | self.analog_quadrature_demod_cf_0.set_gain(((self.samp_rate / self.decim) / (2 * math.pi * self.fsk_deviation_hz))) 232 | self.digital_symbol_sync_xx_0.set_sps(((self.samp_rate / self.decim) / self.baud_rate)) 233 | self.qtgui_freq_sink_x_1.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 234 | self.qtgui_waterfall_sink_x_1_0.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 235 | self.soapy_hackrf_source_0.set_sample_rate(0, self.samp_rate) 236 | 237 | def get_baud_rate(self): 238 | return self.baud_rate 239 | 240 | def set_baud_rate(self, baud_rate): 241 | self.baud_rate = baud_rate 242 | self.set_decim(round(16 * (9600 / self.baud_rate))) 243 | self.set_fsk_deviation_hz({2400: 800, 4800: 1500, 9600: 2200}[self.baud_rate]) 244 | self.set_lp_taps(firdes.low_pass(1.0, self.samp_rate, 25000 * (self.baud_rate / 9600), 15000 * (self.baud_rate / 9600), window.WIN_HAMMING, 6.76)) 245 | self.digital_symbol_sync_xx_0.set_sps(((self.samp_rate / self.decim) / self.baud_rate)) 246 | 247 | def get_offset(self): 248 | return self.offset 249 | 250 | def set_offset(self, offset): 251 | self.offset = offset 252 | self.freq_xlating_fir_filter_xxx_0.set_center_freq(self.offset) 253 | self.soapy_hackrf_source_0.set_frequency(0, (self.center_freq - self.offset)) 254 | 255 | def get_lp_taps(self): 256 | return self.lp_taps 257 | 258 | def set_lp_taps(self, lp_taps): 259 | self.lp_taps = lp_taps 260 | self.freq_xlating_fir_filter_xxx_0.set_taps(self.lp_taps) 261 | 262 | def get_fsk_deviation_hz(self): 263 | return self.fsk_deviation_hz 264 | 265 | def set_fsk_deviation_hz(self, fsk_deviation_hz): 266 | self.fsk_deviation_hz = fsk_deviation_hz 267 | self.analog_quadrature_demod_cf_0.set_gain(((self.samp_rate / self.decim) / (2 * math.pi * self.fsk_deviation_hz))) 268 | 269 | def get_fsk_constellation(self): 270 | return self.fsk_constellation 271 | 272 | def set_fsk_constellation(self, fsk_constellation): 273 | self.fsk_constellation = fsk_constellation 274 | self.digital_constellation_decoder_cb_0.set_constellation(self.fsk_constellation) 275 | 276 | def get_decim(self): 277 | return self.decim 278 | 279 | def set_decim(self, decim): 280 | self.decim = decim 281 | self.analog_quadrature_demod_cf_0.set_gain(((self.samp_rate / self.decim) / (2 * math.pi * self.fsk_deviation_hz))) 282 | self.digital_symbol_sync_xx_0.set_sps(((self.samp_rate / self.decim) / self.baud_rate)) 283 | self.qtgui_freq_sink_x_1.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 284 | self.qtgui_waterfall_sink_x_1_0.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 285 | 286 | def get_dc_block_symbols(self): 287 | return self.dc_block_symbols 288 | 289 | def set_dc_block_symbols(self, dc_block_symbols): 290 | self.dc_block_symbols = dc_block_symbols 291 | 292 | def get_center_freq(self): 293 | return self.center_freq 294 | 295 | def set_center_freq(self, center_freq): 296 | self.center_freq = center_freq 297 | self.qtgui_freq_sink_x_1.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 298 | self.qtgui_waterfall_sink_x_1_0.set_frequency_range(self.center_freq, (self.samp_rate / self.decim)) 299 | self.soapy_hackrf_source_0.set_frequency(0, (self.center_freq - self.offset)) 300 | 301 | 302 | 303 | 304 | def main(top_block_cls=gotenna_pro_rx_hackrf, options=None): 305 | 306 | qapp = Qt.QApplication(sys.argv) 307 | 308 | tb = top_block_cls() 309 | 310 | tb.start() 311 | 312 | tb.show() 313 | 314 | def sig_handler(sig=None, frame=None): 315 | tb.stop() 316 | tb.wait() 317 | 318 | Qt.QApplication.quit() 319 | 320 | signal.signal(signal.SIGINT, sig_handler) 321 | signal.signal(signal.SIGTERM, sig_handler) 322 | 323 | timer = Qt.QTimer() 324 | timer.start(500) 325 | timer.timeout.connect(lambda: None) 326 | 327 | qapp.exec_() 328 | 329 | if __name__ == '__main__': 330 | main() 331 | --------------------------------------------------------------------------------