diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 087fbce1..ecf552bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ permissions: jobs: setup: name: Setup - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: APPNAME: ${{ steps.get_appname.outputs.APPNAME }} DATE: ${{ steps.get_date.outputs.DATE }} @@ -49,7 +49,7 @@ jobs: strategy: matrix: arch: ["amd64", "arm", "aarch64", "armhf"] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.DATE }}-${{ matrix.arch }} DEBIAN_FRONTEND: noninteractive @@ -77,8 +77,7 @@ jobs: fi if [[ "${{ matrix.arch }}" == 'armhf' ]]; then - git clone https://github.com/chriskohlhoff/asio.git - cmake $(echo $build_args) -DCROSS_COMPILE_RPI_ARM=1 -DWITH_ASIO='asio/asio' . + cmake $(echo $build_args) -DCROSS_COMPILE_RPI_ARM=1 . else cmake $(echo $build_args) \ -D "CROSS_COMPILE_$(echo '${{ matrix.arch }}' | tr '[:lower:]' '[:upper:]')=1" . @@ -100,7 +99,7 @@ jobs: if: ${{ github.event == 'push' }} name: Create Release needs: [setup, build] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -120,7 +119,7 @@ jobs: strategy: matrix: arch: ["amd64", "arm", "aarch64", "armhf"] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.DATE }}-${{ matrix.arch }} DEBIAN_FRONTEND: noninteractive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a832def..0bafd4f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ permissions: jobs: setup: name: Setup - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: APPNAME: ${{ steps.get_appname.outputs.APPNAME }} VERSION: ${{ steps.get_version.outputs.VERSION }} @@ -31,7 +31,7 @@ jobs: strategy: matrix: arch: ["amd64", "arm", "aarch64", "armhf"] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.VERSION }}-${{ matrix.arch }} DEBIAN_FRONTEND: noninteractive @@ -51,9 +51,8 @@ jobs: - name: Build run: | if [[ "${{ matrix.arch }}" == 'armhf' ]]; then - git clone https://github.com/chriskohlhoff/asio.git cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \ - -DCROSS_COMPILE_RPI_ARM=1 -DWITH_ASIO='asio/asio' . + -DCROSS_COMPILE_RPI_ARM=1 . else cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \ -D "CROSS_COMPILE_$(echo '${{ matrix.arch }}' | tr '[:lower:]' '[:upper:]')=1" . @@ -74,7 +73,7 @@ jobs: create-release: name: Create Release needs: [setup, build] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -84,7 +83,7 @@ jobs: with: tag_name: ${{ needs.setup.outputs.VERSION }} name: Release ${{ needs.setup.outputs.VERSION }} - draft: true + draft: false prerelease: false upload: @@ -93,7 +92,7 @@ jobs: strategy: matrix: arch: ["amd64", "arm", "aarch64", "armhf"] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.VERSION }}-${{ matrix.arch }} DEBIAN_FRONTEND: noninteractive diff --git a/.gitignore b/.gitignore index ebcd7c42..9d111944 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,23 @@ +# Ignore built binaries +build/ +dvmcmd dvmhost +# Ignore CMake temporary files +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +CPackConfig.cmake +CPackSourceConfig.cmake +Voice.plist +Testing +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + # Ignore thumbnails created by windows Thumbs.db @@ -40,10 +58,10 @@ build/ .vscode/ package/ *.ini +.vs -# Compiled binary files -*.exe -*.dll +# Prerequisites +*.d # Compiled Object files *.slo @@ -51,16 +69,26 @@ package/ *.o *.obj +# Precompiled Headers +*.gch +*.pch + # Compiled Dynamic libraries *.so *.dylib *.dll +# Fortran module files +*.mod +*.smod + # Compiled Static libraries *.lai *.la *.a *.lib -# Visual Studio -.vs +# Executables +*.exe +*.out +*.app diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e8555f6..c50af22f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -346,10 +346,19 @@ else () message(CHECK_PASS "no") endif (CROSS_COMPILE_RPI_ARM) -option(WITH_ASIO "Specifies the location for the ASIO library" off) +option(WITH_ASIO "Manually specify the location for the ASIO library" off) if (WITH_ASIO) set(ASIO_INCLUDE_DIR ${WITH_ASIO}/include) message(CHECK_START "With ASIO: ${ASIO_INCLUDE_DIR}") +else() + message("-- Cloning ASIO") + Include(FetchContent) + FetchContent_Declare( + ASIO + GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git + ) + FetchContent_MakeAvailable(ASIO) + set(ASIO_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/_deps/asio-src/asio/include) endif (WITH_ASIO) # Standard CMake options @@ -379,14 +388,13 @@ add_definitions(-D__GIT_VER_HASH__="${GIT_VER_HASH}") project(dvmhost) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") find_package(Threads REQUIRED) -if (NOT WITH_ASIO) - find_package(ASIO REQUIRED) -else() - add_library(asio::asio INTERFACE IMPORTED) - target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) - target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") - target_link_libraries(asio::asio INTERFACE Threads::Threads) -endif () + +# add ASIO +add_library(asio::asio INTERFACE IMPORTED) +target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) +target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") +target_link_libraries(asio::asio INTERFACE Threads::Threads) + add_executable(dvmhost ${dvmhost_SRC}) target_include_directories(dvmhost PRIVATE src) target_link_libraries(dvmhost PRIVATE asio::asio Threads::Threads util) @@ -419,13 +427,12 @@ include(CPack) project(dvmcmd) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") find_package(Threads REQUIRED) -if (NOT WITH_ASIO) - find_package(ASIO REQUIRED) -else() - target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) - target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") - target_link_libraries(asio::asio INTERFACE Threads::Threads) -endif () + +# add ASIO +target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) +target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") +target_link_libraries(asio::asio INTERFACE Threads::Threads) + add_executable(dvmcmd ${dvmcmd_SRC}) target_link_libraries(dvmcmd PRIVATE asio::asio Threads::Threads) target_include_directories(dvmcmd PRIVATE src) @@ -446,13 +453,12 @@ FetchContent_Declare( FetchContent_MakeAvailable(Catch2) find_package(Threads REQUIRED) -if (NOT WITH_ASIO) - find_package(ASIO REQUIRED) -else() - target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) - target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") - target_link_libraries(asio::asio INTERFACE Threads::Threads) -endif () + +# add ASIO +target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) +target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") +target_link_libraries(asio::asio INTERFACE Threads::Threads) + add_executable(dvmtests ${dvmhost_SRC} ${dvmtests_SRC}) target_compile_definitions(dvmtests PUBLIC -DCATCH2_TEST_COMPILATION) target_link_libraries(dvmtests PRIVATE Catch2::Catch2WithMain asio::asio Threads::Threads util) diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0b859b8b --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# DVM Host Express Makefile +# An express Makefile for easily creating binaries for various architectures. +# Author: K4YT3X + +# This Makefile helps building the dvmcmd and dvmhost binaries for all supported architectures. +# Built binaries will be saved to build/${ARCH}. E.g., The binaries built with `make aarch64` +# will be saved to build/aarch64. + +all: prepare amd64 arm aarch64 armhf + @echo 'All builds completed successfully' + +amd64: + @echo 'Compiling for AMD64' + mkdir -p "build/$@" && cd "build/$@" \ + && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" ../.. \ + && make -j $(nproc) + @echo 'Successfully compiled for AMD64' + +arm: + @echo 'Cross-Compiling for ARM' + mkdir -p "build/$@" && cd "build/$@" \ + && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \ + -DCROSS_COMPILE_ARM=1 ../.. \ + && make -j $(nproc) + @echo 'Successfully compiled for ARM' + +aarch64: + @echo 'Cross-Compiling for AARCH64' + mkdir -p "build/$@" && cd "build/$@" \ + && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \ + -DCROSS_COMPILE_AARCH64=1 ../.. \ + && make -j $(nproc) + @echo 'Successfully compiled for AARCH64' + +armhf: + @echo 'Cross-Compiling for ARMHF' + mkdir -p "build/$@" && cd "build/$@" \ + && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \ + -DCROSS_COMPILE_RPI_ARM=1 ../.. \ + && make -j $(nproc) + @echo 'Successfully compiled for ARMHF' + +clean: + @echo 'Removing all temporary files' + git clean -ffxd + +export_compile_commands: + @echo 'Exporting CMake compile commands' + cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 . + git checkout HEAD -- Makefile + +prepare: + # if the system is Debian + grep 'ID_LIKE=debian' /etc/os-release > /dev/null 2>&1 \ + && echo 'Preparing dependencies for Debian' \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -y git build-essential cmake libasio-dev \ + g++-arm-linux-gnueabihf gcc-arm-linux-gnueabihf g++-aarch64-linux-gnu + # mark directory safe for Git if running in container + [ ! -z "${container}" ] && git config --global --add safe.directory \ + "$(abspath $(dir $(lastword $(MAKEFILE_LIST))))" diff --git a/README.md b/README.md index e963fc44..6b273ec0 100644 --- a/README.md +++ b/README.md @@ -136,23 +136,27 @@ usage: ./dvmhost [-vh] [-f] [--cal] [--setup] [-c ] [--remot There is an automated process to generate a tarball package if required as well, after compiling simply run: `make tarball`. This will generate a tarball package, the tarball package contains the similar pathing that the `make old_install` would generate. -## Notes +## Security Warnings -Security note: It is highly recommended that the REST API interface not be exposed directly to the internet. If such exposure is wanted/needed, it is highly recommended to proxy the dvmhost REST API through a modern web server (like nginx for example) rather then directly exposing dvmhost's REST API port. +It is highly recommended that the REST API interface not be exposed directly to the internet. If such exposure is wanted/needed, it is highly recommended to proxy the dvmhost REST API through a modern web server (like nginx for example) rather then directly exposing dvmhost's REST API port. -Some extra notes for those who are using the Raspberry Pi, default Raspbian OS or Debian OS installations. You will not be able to flash or access the STM32 modem unless you do some things beforehand. +## Raspberry Pi Preparation -1. Disable the Bluetooth services. Bluetooth will share the GPIO serial interface on `/dev/ttyAMA0`. On Rasbian OS or Debian OS, this is done by: `systemctl disable bluetooth` -2. The default Rasbian OS and Debian OS will have a getty instance listening on `/dev/ttyAMA0`. This can conflict with the STM32, and is best if disabled. On Rasbian OS or Debian OS, this is done by: `systemctl disable serial-getty@ttyAMA0.service` -3. There's a default boot option which is also listening on the GPIO serial interface. This **must be disabled**. Open the `/boot/cmdline.txt` file in your favorite editor (vi or pico) and change it from: +Some extra notes for those who are using the Raspberry Pi, default Raspbian OS or Debian OS installations. You will not be able to flash or access the STM32 modem unless you do some things beforehand. -`console=serial0,115200 console=tty1 root=PARTUUID=[this is dynamic per partition] rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait` +1. Disable the Bluetooth services. Bluetooth will share the GPIO serial interface on `/dev/ttyAMA0`. On Rasbian OS or Debian OS, this is done by: `sudo systemctl disable bluetooth` then adding `dtoverlay=disable-bt` to `/boot/config.txt`. +1. The default Rasbian OS and Debian OS will have a getty instance listening on `/dev/ttyAMA0`. This can conflict with the STM32, and is best if disabled. On Rasbian OS or Debian OS, this is done by: `systemctl disable serial-getty@ttyAMA0.service` +1. There's a default boot option which is also listening on the GPIO serial interface. This **must be disabled**. Open the `/boot/cmdline.txt` file in your favorite editor (vi or pico) and remove the `console=serial0,115200` part. -to +The steps above can be done by the following commands: -`console=tty1 root=PARTUUID=[this is dynamic per partition] rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait` +```shell +sudo systemctl disable bluetooth.service serial-getty@ttyAMA0.service +grep -E 'dtoverlay=disable-bt' /boot/config.txt || echo 'dtoverlay=disable-bt' | sudo tee /boot/config.txt +sudo sed -i 's/^console=serial0,115200 *//' /boot/cmdline.txt +``` -All thats being done is to remove the `console=serial0,115200` part. Do not change anything else. Save the file, then reboot. +After finishing these steps, reboot. ## License diff --git a/src/network/BaseNetwork.cpp b/src/network/BaseNetwork.cpp index d8a93e94..dbbf5f6b 100644 --- a/src/network/BaseNetwork.cpp +++ b/src/network/BaseNetwork.cpp @@ -245,6 +245,7 @@ uint8_t* BaseNetwork::readP25(bool& ret, p25::lc::LC& control, p25::data::LowSpe if (m_debug) { LogDebug(LOG_NET, "P25, HDU algId = $%02X, kId = $%02X", algId, kid); + Utils::dump(1U, "P25 HDU MI decoded from network", mi, p25::P25_MI_LENGTH_BYTES); } control.setAlgId(algId); @@ -763,6 +764,10 @@ bool BaseNetwork::writeP25LDU1(const uint32_t id, const uint32_t streamId, const ::memset(mi, 0x00U, p25::P25_MI_LENGTH_BYTES); control.getMI(mi); + if (m_debug) { + Utils::dump(1U, "P25 HDU MI written to network", mi, p25::P25_MI_LENGTH_BYTES); + } + for (uint8_t i = 0; i < p25::P25_MI_LENGTH_BYTES; i++) { buffer[184U + i] = mi[i]; // Message Indicator } diff --git a/src/p25/P25Defines.h b/src/p25/P25Defines.h index 3c52496a..2767ef65 100644 --- a/src/p25/P25Defines.h +++ b/src/p25/P25Defines.h @@ -110,6 +110,7 @@ namespace p25 const uint32_t P25_SS_INCREMENT = 72U; const uint8_t P25_NULL_IMBE[] = { 0x04U, 0x0CU, 0xFDU, 0x7BU, 0xFBU, 0x7DU, 0xF2U, 0x7BU, 0x3DU, 0x9EU, 0x45U }; + const uint8_t P25_ENCRYPTED_NULL_IMBE[] = { 0xFCU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U }; const uint8_t P25_MFG_STANDARD = 0x00U; const uint8_t P25_MFG_MOT = 0x90U; diff --git a/src/p25/packet/Voice.cpp b/src/p25/packet/Voice.cpp index d38861d5..f4f7fdfd 100644 --- a/src/p25/packet/Voice.cpp +++ b/src/p25/packet/Voice.cpp @@ -107,24 +107,11 @@ bool Voice::process(uint8_t* data, uint32_t len) // Decode the NID bool valid = m_p25->m_nid.decode(data + 2U); - - if (m_p25->m_rfState == RS_RF_LISTENING && !valid) + if (!valid) { return false; + } uint8_t duid = m_p25->m_nid.getDUID(); - if (!valid) { - switch (m_lastDUID) { - case P25_DUID_HDU: - case P25_DUID_LDU2: - duid = P25_DUID_LDU1; - break; - case P25_DUID_LDU1: - duid = P25_DUID_LDU2; - break; - default: - break; - } - } // are we interrupting a running CC? if (m_p25->m_ccRunning) { @@ -164,6 +151,13 @@ bool Voice::process(uint8_t* data, uint32_t len) return false; } + if (m_verbose && m_debug) { + uint8_t mi[P25_MI_LENGTH_BYTES]; + ::memset(mi, 0x00U, p25::P25_MI_LENGTH_BYTES); + lc.getMI(mi); + Utils::dump(1U, "P25 HDU MI read from RF", mi, P25_MI_LENGTH_BYTES); + } + if (m_verbose) { LogMessage(LOG_RF, P25_HDU_STR ", HDU_BSDWNACT, dstId = %u, algo = $%02X, kid = $%04X", lc.getDstId(), lc.getAlgId(), lc.getKId()); } @@ -206,9 +200,14 @@ bool Voice::process(uint8_t* data, uint32_t len) return true; } else if (duid == P25_DUID_LDU1) { - bool alreadyDecoded = false; + + // prevent two LDUs of the same type from being sent consecutively + if (m_lastDUID == P25_DUID_LDU1) { + return false; + } m_lastDUID = P25_DUID_LDU1; + bool alreadyDecoded = false; uint8_t frameType = P25_FT_DATA_UNIT; if (m_p25->m_rfState == RS_RF_LISTENING) { // if this is a late entry call, clear states @@ -528,7 +527,12 @@ bool Voice::process(uint8_t* data, uint32_t len) uint8_t buffer[9U * 25U]; ::memset(buffer, 0x00U, 9U * 25U); - insertNullAudio(buffer); + if (m_rfLC.getEncrypted()) { + insertEncryptedNullAudio(buffer); + } + else { + insertNullAudio(buffer); + } LogWarning(LOG_RF, P25_LDU1_STR ", exceeded lost audio threshold, filling in"); @@ -542,15 +546,6 @@ bool Voice::process(uint8_t* data, uint32_t len) m_audio.encode(data + 2U, buffer + 155U, 6U); m_audio.encode(data + 2U, buffer + 180U, 7U); m_audio.encode(data + 2U, buffer + 204U, 8U); - - // reset the encryption flags if necessary - if (m_rfLC.getEncrypted()) { - m_rfLC.setEncrypted(false); - m_rfLC.setAlgId(P25_ALGO_UNENCRYPT); - - // regenerate LDU1 data - m_rfLC.encodeLDU1(data + 2U); - } } m_rfBits += 1233U; @@ -578,6 +573,11 @@ bool Voice::process(uint8_t* data, uint32_t len) } } else if (duid == P25_DUID_LDU2) { + + // prevent two LDUs of the same type from being sent consecutively + if (m_lastDUID == P25_DUID_LDU2) { + return false; + } m_lastDUID = P25_DUID_LDU2; if (m_p25->m_rfState == RS_RF_LISTENING) { @@ -589,6 +589,24 @@ bool Voice::process(uint8_t* data, uint32_t len) LogWarning(LOG_RF, P25_LDU2_STR ", undecodable LC, using last LDU2 LC"); m_rfLC = m_rfLastLDU2; m_rfUndecodableLC++; + + // regenerate the MI using LFSR + uint8_t lastMI[P25_MI_LENGTH_BYTES]; + ::memset(lastMI, 0x00U, P25_MI_LENGTH_BYTES); + + uint8_t nextMI[P25_MI_LENGTH_BYTES]; + ::memset(nextMI, 0x00U, P25_MI_LENGTH_BYTES); + + m_rfLastLDU2.getMI(lastMI); + getNextMI(lastMI, nextMI); + + if (m_verbose && m_debug) { + Utils::dump(1U, "Previous P25 HDU MI", lastMI, P25_MI_LENGTH_BYTES); + Utils::dump(1U, "Calculated next P25 HDU MI", nextMI, P25_MI_LENGTH_BYTES); + } + + m_rfLC.setMI(nextMI); + m_rfLastLDU2.setMI(nextMI); } else { m_rfLastLDU2 = m_rfLC; @@ -616,7 +634,12 @@ bool Voice::process(uint8_t* data, uint32_t len) uint8_t buffer[9U * 25U]; ::memset(buffer, 0x00U, 9U * 25U); - insertNullAudio(buffer); + if (m_rfLC.getEncrypted()) { + insertEncryptedNullAudio(buffer); + } + else { + insertNullAudio(buffer); + } LogWarning(LOG_RF, P25_LDU2_STR ", exceeded lost audio threshold, filling in"); @@ -630,15 +653,6 @@ bool Voice::process(uint8_t* data, uint32_t len) m_audio.encode(data + 2U, buffer + 155U, 6U); m_audio.encode(data + 2U, buffer + 180U, 7U); m_audio.encode(data + 2U, buffer + 204U, 8U); - - // reset the encryption flags if necessary - if (m_rfLC.getEncrypted()) { - m_rfLC.setEncrypted(false); - m_rfLC.setAlgId(P25_ALGO_UNENCRYPT); - - // regenerate LDU2 data - m_rfLC.encodeLDU2(data + 2U); - } } m_rfBits += 1233U; @@ -783,6 +797,9 @@ bool Voice::processNetwork(uint8_t* data, uint32_t len, lc::LC& control, data::L m_netLastLDU1 = control; m_netLastFrameType = frameType; + // save MI to member variable before writing to RF + control.getMI(m_lastMI); + if (m_p25->m_control) { lc::LC control = lc::LC(*m_dfsiLC.control()); m_p25->m_affiliations.touchGrant(control.getDstId()); @@ -935,6 +952,7 @@ Voice::Voice(Control* p25, network::BaseNetwork* network, bool debug, bool verbo m_netLDU2(nullptr), m_lastDUID(P25_DUID_TDU), m_lastIMBE(nullptr), + m_lastMI(nullptr), m_hadVoice(false), m_lastRejectId(0U), m_silenceThreshold(DEFAULT_SILENCE_THRESHOLD), @@ -950,6 +968,9 @@ Voice::Voice(Control* p25, network::BaseNetwork* network, bool debug, bool verbo m_lastIMBE = new uint8_t[11U]; ::memcpy(m_lastIMBE, P25_NULL_IMBE, 11U); + + m_lastMI = new uint8_t[P25_MI_LENGTH_BYTES]; + ::memset(m_lastMI, 0x00U, P25_MI_LENGTH_BYTES); } /// @@ -960,6 +981,7 @@ Voice::~Voice() delete[] m_netLDU1; delete[] m_netLDU2; delete[] m_lastIMBE; + delete[] m_lastMI; } /// @@ -1164,10 +1186,6 @@ void Voice::writeNet_LDU1() } } - if (m_p25->m_control) { - m_p25->m_affiliations.touchGrant(m_rfLC.getDstId()); - } - if (m_debug) { LogMessage(LOG_NET, P25_LDU1_STR " service flags, emerg = %u, encrypt = %u, prio = %u, DFSI emerg = %u, DFSI encrypt = %u, DFSI prio = %u", control.getEmergency(), control.getEncrypted(), control.getPriority(), @@ -1201,17 +1219,16 @@ void Voice::writeNet_LDU1() ::memset(mi, 0x00U, P25_MI_LENGTH_BYTES); if (m_netLastLDU1.getAlgId() != P25_ALGO_UNENCRYPT && m_netLastLDU1.getKId() != 0) { - m_netLastLDU1.getMI(mi); - control.setAlgId(m_netLastLDU1.getAlgId()); control.setKId(m_netLastLDU1.getKId()); } - else { - control.getMI(mi); - } + + + // restore MI from member variable + ::memcpy(mi, m_lastMI, P25_MI_LENGTH_BYTES); if (m_verbose && m_debug) { - Utils::dump(1U, "Network HDU MI", mi, P25_MI_LENGTH_BYTES); + Utils::dump(1U, "P25 HDU MI from network to RF", mi, P25_MI_LENGTH_BYTES); } m_netLC.setMI(mi); @@ -1608,3 +1625,76 @@ void Voice::insertNullAudio(uint8_t* data) ::memcpy(data + 204U, P25_NULL_IMBE, 11U); } } + +/// +/// Helper to insert encrypted IMBE null frames for missing audio. +/// +/// + +void Voice::insertEncryptedNullAudio(uint8_t* data) +{ + if (data[0U] == 0x00U) { + ::memcpy(data + 10U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[25U] == 0x00U) { + ::memcpy(data + 26U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[50U] == 0x00U) { + ::memcpy(data + 55U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[75U] == 0x00U) { + ::memcpy(data + 80U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[100U] == 0x00U) { + ::memcpy(data + 105U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[125U] == 0x00U) { + ::memcpy(data + 130U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[150U] == 0x00U) { + ::memcpy(data + 155U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[175U] == 0x00U) { + ::memcpy(data + 180U, P25_ENCRYPTED_NULL_IMBE, 11U); + } + + if (data[200U] == 0x00U) { + ::memcpy(data + 204U, P25_ENCRYPTED_NULL_IMBE, 11U); + } +} + +/// +/// Given the last MI, generate the next MI using LFSR. +/// +/// +/// + +void Voice::getNextMI(uint8_t lastMI[9U], uint8_t nextMI[9U]) +{ + uint8_t carry, i; + std::copy(lastMI, lastMI + 9, nextMI); + + for (uint8_t cycle = 0; cycle < 64; cycle++) { + // calculate bit 0 for the next cycle + carry = ((nextMI[0] >> 7) ^ (nextMI[0] >> 5) ^ (nextMI[2] >> 5) ^ + (nextMI[3] >> 5) ^ (nextMI[4] >> 2) ^ (nextMI[6] >> 6)) & + 0x01; + + // shift all the list elements, except the last one + for (i = 0; i < 7; i++) { + + // grab high bit from the next element and use it as our low bit + nextMI[i] = ((nextMI[i] & 0x7F) << 1) | (nextMI[i + 1] >> 7); + } + + // shift last element, then copy the bit 0 we calculated in + nextMI[7] = ((nextMI[i] & 0x7F) << 1) | carry; + } +} diff --git a/src/p25/packet/Voice.h b/src/p25/packet/Voice.h index 3b75401c..fee84788 100644 --- a/src/p25/packet/Voice.h +++ b/src/p25/packet/Voice.h @@ -104,6 +104,7 @@ namespace p25 uint8_t m_lastDUID; uint8_t* m_lastIMBE; + uint8_t* m_lastMI; bool m_hadVoice; uint32_t m_lastRejectId; @@ -141,6 +142,10 @@ namespace p25 void insertMissingAudio(uint8_t* data); /// Helper to insert IMBE null frames for missing audio. void insertNullAudio(uint8_t* data); + /// Helper to insert encrypted IMBE null frames for missing audio. + void insertEncryptedNullAudio(uint8_t* data); + /// Given the last MI, generate the next MI using LFSR. + void getNextMI(uint8_t lastMI[9U], uint8_t nestMI[9U]); }; } // namespace packet } // namespace p25 diff --git a/tools/config_annotator.py b/tools/config_annotator.py new file mode 100755 index 00000000..c2803b40 --- /dev/null +++ b/tools/config_annotator.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Name: DVM Host Configuration Annotator +Creator: K4YT3X +Date Created: April 1, 2023 +Last Modified: April 1, 2023 + +This script updates DVM Host's example config with the content of a modified one +to generate a configuration with the settings of the modified config but with the +config from the original example config. + +Example usages: + +1. Annotate a config file in-place using DVM Host's default config file: + +`python config_annotator.py -em modified.yaml` + +2. Annotate a config file using a custom template file and export it to a separate file: + +`python config_annotator.py -t template.yml -m modified.yml -o annotated.yml` +""" +import argparse +import sys +from pathlib import Path +from typing import Dict + +# since this stand-alone script does not have a pyproject.yaml or requirements.txt +# prompt the user to install the required libraries if they're not found +try: + import requests + import ruamel.yaml +except ImportError: + print( + "Error: unable to import ruamel.yaml or requests\n" + "Please install it with `pip install ruamel.yaml requests`", + file=sys.stderr, + ) + sys.exit(1) + +# URL to DVM Host's official example URL +EXAMPLE_CONFIG_URL = ( + "https://github.com/DVMProject/dvmhost/raw/master/configs/config.example.yml" +) + + +def parse_arguments() -> argparse.Namespace: + """ + parse command line arguments + + :return: parsed arguments in an argparse namespace object + """ + parser = argparse.ArgumentParser(description="Update a YAML file with new values.") + template_group = parser.add_mutually_exclusive_group(required=True) + template_group.add_argument( + "-e", + "--example", + help="use DVM Host's default example config as template", + action="store_true", + ) + template_group.add_argument( + "-t", + "--template", + type=Path, + help="path to the template config YAML file with comments", + ) + parser.add_argument( + "-m", "--modified", type=Path, help="path to the modified YAML file" + ) + parser.add_argument( + "-o", "--output", type=Path, help="path to the save the annotated YAML file" + ) + return parser.parse_args() + + +def update_dict(template_dict: Dict, modified_dict: Dict) -> Dict: + """ + Recursively updates the values of template_dict with the values of modified_dict, + without changing the order of the keys in template_dict. + + :param template_dict: the dictionary to update. + :type template_dict: dict + :param modified_dict: the dictionary with the updated values + :type modified_dict: dict + :return: the updated dictionary + :rtype: dict + """ + for key, value in modified_dict.items(): + if isinstance(value, dict) and key in template_dict: + update_dict(template_dict[key], value) + elif key in template_dict: + template_dict[key] = value + return template_dict + + +def main( + example: bool, template_path: Path, modified_path: Path, output_path: Path +) -> None: + # if the example file is to be used + # download it from the GitHub repo + if example is True: + response = requests.get(EXAMPLE_CONFIG_URL) + response.raise_for_status() + template_content = response.text + + # if a custom template file is to be used + else: + with template_path.open(mode="r") as template_file: + template_content = template_file.read() + + # load the YAML file + template = ruamel.yaml.YAML().load(template_content) + + # load the modified YAML file + with modified_path.open(mode="r") as modified_file: + modified = ruamel.yaml.YAML().load(modified_file) + + # update the template with the modified YAML data + update_dict(template, modified) + + # if no output file path is specified, write it back to the modified file + if args.output is None: + write_path = modified_path + + # if an output file path has been specified, write the output to that file + else: + write_path = output_path + + # write the updated YAML file back to disk + with write_path.open(mode="w") as output_file: + yaml = ruamel.yaml.YAML() + + # set the output YAML file's indentation to 4 spaces per project standard + yaml.indent(mapping=4, sequence=6, offset=4) + yaml.dump(template, output_file) + + +if __name__ == "__main__": + args = parse_arguments() + main(args.example, args.template, args.modified, args.output) diff --git a/tools/dvm-watchdog.sh b/tools/dvm-watchdog.sh old mode 100644 new mode 100755 diff --git a/tools/start-dvm.sh b/tools/start-dvm.sh old mode 100644 new mode 100755 diff --git a/tools/stop-dvm.sh b/tools/stop-dvm.sh old mode 100644 new mode 100755 diff --git a/tools/stop-watchdog.sh b/tools/stop-watchdog.sh old mode 100644 new mode 100755