Fixed P25 garbled encrypted voice, plus minor several other improvements (#27)

* changed GHA runners to ubuntu-20.04 since glibc is too new in 22.04

* updated gitignore to exclude CMake and more C++ temporary files

* adjusted scripts' permissions

* created express Makefile to easily build for different arch

* automatically clone ASIO source code using CMake

* updated Raspberry Pi configuration docs

* fixed P25 garbled encrypted voice due to incorrect HDU MIs, frame type predictions, and clear null IMBE frames

* removed ASIO clones from pipelines
pull/28/head
K4YT3X 3 years ago committed by GitHub
parent 556298890d
commit da863f6785
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -31,7 +31,7 @@ permissions:
jobs: jobs:
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-latest runs-on: ubuntu-20.04
outputs: outputs:
APPNAME: ${{ steps.get_appname.outputs.APPNAME }} APPNAME: ${{ steps.get_appname.outputs.APPNAME }}
DATE: ${{ steps.get_date.outputs.DATE }} DATE: ${{ steps.get_date.outputs.DATE }}
@ -49,7 +49,7 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["amd64", "arm", "aarch64", "armhf"] arch: ["amd64", "arm", "aarch64", "armhf"]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
env: env:
PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.DATE }}-${{ matrix.arch }} PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.DATE }}-${{ matrix.arch }}
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
@ -77,8 +77,7 @@ jobs:
fi fi
if [[ "${{ matrix.arch }}" == 'armhf' ]]; then if [[ "${{ matrix.arch }}" == 'armhf' ]]; then
git clone https://github.com/chriskohlhoff/asio.git cmake $(echo $build_args) -DCROSS_COMPILE_RPI_ARM=1 .
cmake $(echo $build_args) -DCROSS_COMPILE_RPI_ARM=1 -DWITH_ASIO='asio/asio' .
else else
cmake $(echo $build_args) \ cmake $(echo $build_args) \
-D "CROSS_COMPILE_$(echo '${{ matrix.arch }}' | tr '[:lower:]' '[:upper:]')=1" . -D "CROSS_COMPILE_$(echo '${{ matrix.arch }}' | tr '[:lower:]' '[:upper:]')=1" .
@ -100,7 +99,7 @@ jobs:
if: ${{ github.event == 'push' }} if: ${{ github.event == 'push' }}
name: Create Release name: Create Release
needs: [setup, build] needs: [setup, build]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
@ -120,7 +119,7 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["amd64", "arm", "aarch64", "armhf"] arch: ["amd64", "arm", "aarch64", "armhf"]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
env: env:
PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.DATE }}-${{ matrix.arch }} PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.DATE }}-${{ matrix.arch }}
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive

@ -13,7 +13,7 @@ permissions:
jobs: jobs:
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-latest runs-on: ubuntu-20.04
outputs: outputs:
APPNAME: ${{ steps.get_appname.outputs.APPNAME }} APPNAME: ${{ steps.get_appname.outputs.APPNAME }}
VERSION: ${{ steps.get_version.outputs.VERSION }} VERSION: ${{ steps.get_version.outputs.VERSION }}
@ -31,7 +31,7 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["amd64", "arm", "aarch64", "armhf"] arch: ["amd64", "arm", "aarch64", "armhf"]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
env: env:
PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.VERSION }}-${{ matrix.arch }} PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.VERSION }}-${{ matrix.arch }}
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive
@ -51,9 +51,8 @@ jobs:
- name: Build - name: Build
run: | run: |
if [[ "${{ matrix.arch }}" == 'armhf' ]]; then 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" \ 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 else
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-s" -DCMAKE_CXX_FLAGS="-s" \
-D "CROSS_COMPILE_$(echo '${{ matrix.arch }}' | tr '[:lower:]' '[:upper:]')=1" . -D "CROSS_COMPILE_$(echo '${{ matrix.arch }}' | tr '[:lower:]' '[:upper:]')=1" .
@ -74,7 +73,7 @@ jobs:
create-release: create-release:
name: Create Release name: Create Release
needs: [setup, build] needs: [setup, build]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
@ -84,7 +83,7 @@ jobs:
with: with:
tag_name: ${{ needs.setup.outputs.VERSION }} tag_name: ${{ needs.setup.outputs.VERSION }}
name: Release ${{ needs.setup.outputs.VERSION }} name: Release ${{ needs.setup.outputs.VERSION }}
draft: true draft: false
prerelease: false prerelease: false
upload: upload:
@ -93,7 +92,7 @@ jobs:
strategy: strategy:
matrix: matrix:
arch: ["amd64", "arm", "aarch64", "armhf"] arch: ["amd64", "arm", "aarch64", "armhf"]
runs-on: ubuntu-latest runs-on: ubuntu-20.04
env: env:
PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.VERSION }}-${{ matrix.arch }} PACKAGENAME: ${{ needs.setup.outputs.APPNAME }}-${{ needs.setup.outputs.VERSION }}-${{ matrix.arch }}
DEBIAN_FRONTEND: noninteractive DEBIAN_FRONTEND: noninteractive

38
.gitignore vendored

@ -1,5 +1,23 @@
# Ignore built binaries
build/
dvmcmd
dvmhost 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 # Ignore thumbnails created by windows
Thumbs.db Thumbs.db
@ -40,10 +58,10 @@ build/
.vscode/ .vscode/
package/ package/
*.ini *.ini
.vs
# Compiled binary files # Prerequisites
*.exe *.d
*.dll
# Compiled Object files # Compiled Object files
*.slo *.slo
@ -51,16 +69,26 @@ package/
*.o *.o
*.obj *.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries # Compiled Dynamic libraries
*.so *.so
*.dylib *.dylib
*.dll *.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries # Compiled Static libraries
*.lai *.lai
*.la *.la
*.a *.a
*.lib *.lib
# Visual Studio # Executables
.vs *.exe
*.out
*.app

@ -346,10 +346,19 @@ else ()
message(CHECK_PASS "no") message(CHECK_PASS "no")
endif (CROSS_COMPILE_RPI_ARM) 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) if (WITH_ASIO)
set(ASIO_INCLUDE_DIR ${WITH_ASIO}/include) set(ASIO_INCLUDE_DIR ${WITH_ASIO}/include)
message(CHECK_START "With ASIO: ${ASIO_INCLUDE_DIR}") 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) endif (WITH_ASIO)
# Standard CMake options # Standard CMake options
@ -379,14 +388,13 @@ add_definitions(-D__GIT_VER_HASH__="${GIT_VER_HASH}")
project(dvmhost) project(dvmhost)
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
if (NOT WITH_ASIO)
find_package(ASIO REQUIRED) # add ASIO
else() add_library(asio::asio INTERFACE IMPORTED)
add_library(asio::asio INTERFACE IMPORTED) target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR})
target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE")
target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") target_link_libraries(asio::asio INTERFACE Threads::Threads)
target_link_libraries(asio::asio INTERFACE Threads::Threads)
endif ()
add_executable(dvmhost ${dvmhost_SRC}) add_executable(dvmhost ${dvmhost_SRC})
target_include_directories(dvmhost PRIVATE src) target_include_directories(dvmhost PRIVATE src)
target_link_libraries(dvmhost PRIVATE asio::asio Threads::Threads util) target_link_libraries(dvmhost PRIVATE asio::asio Threads::Threads util)
@ -419,13 +427,12 @@ include(CPack)
project(dvmcmd) project(dvmcmd)
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
if (NOT WITH_ASIO)
find_package(ASIO REQUIRED) # add ASIO
else() target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR})
target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE")
target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") target_link_libraries(asio::asio INTERFACE Threads::Threads)
target_link_libraries(asio::asio INTERFACE Threads::Threads)
endif ()
add_executable(dvmcmd ${dvmcmd_SRC}) add_executable(dvmcmd ${dvmcmd_SRC})
target_link_libraries(dvmcmd PRIVATE asio::asio Threads::Threads) target_link_libraries(dvmcmd PRIVATE asio::asio Threads::Threads)
target_include_directories(dvmcmd PRIVATE src) target_include_directories(dvmcmd PRIVATE src)
@ -446,13 +453,12 @@ FetchContent_Declare(
FetchContent_MakeAvailable(Catch2) FetchContent_MakeAvailable(Catch2)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
if (NOT WITH_ASIO)
find_package(ASIO REQUIRED) # add ASIO
else() target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR})
target_include_directories(asio::asio INTERFACE ${ASIO_INCLUDE_DIR}) target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE")
target_compile_definitions(asio::asio INTERFACE "ASIO_STANDALONE") target_link_libraries(asio::asio INTERFACE Threads::Threads)
target_link_libraries(asio::asio INTERFACE Threads::Threads)
endif ()
add_executable(dvmtests ${dvmhost_SRC} ${dvmtests_SRC}) add_executable(dvmtests ${dvmhost_SRC} ${dvmtests_SRC})
target_compile_definitions(dvmtests PUBLIC -DCATCH2_TEST_COMPILATION) target_compile_definitions(dvmtests PUBLIC -DCATCH2_TEST_COMPILATION)
target_link_libraries(dvmtests PRIVATE Catch2::Catch2WithMain asio::asio Threads::Threads util) target_link_libraries(dvmtests PRIVATE Catch2::Catch2WithMain asio::asio Threads::Threads util)

@ -0,0 +1,62 @@
# DVM Host Express Makefile
# An express Makefile for easily creating binaries for various architectures.
# Author: K4YT3X <i@k4yt3x.com>
# 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))))"

@ -136,23 +136,27 @@ usage: ./dvmhost [-vh] [-f] [--cal] [--setup] [-c <configuration file>] [--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. 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` 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.
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:
`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 ## License

@ -245,6 +245,7 @@ uint8_t* BaseNetwork::readP25(bool& ret, p25::lc::LC& control, p25::data::LowSpe
if (m_debug) { if (m_debug) {
LogDebug(LOG_NET, "P25, HDU algId = $%02X, kId = $%02X", algId, kid); 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); 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); ::memset(mi, 0x00U, p25::P25_MI_LENGTH_BYTES);
control.getMI(mi); 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++) { for (uint8_t i = 0; i < p25::P25_MI_LENGTH_BYTES; i++) {
buffer[184U + i] = mi[i]; // Message Indicator buffer[184U + i] = mi[i]; // Message Indicator
} }

@ -110,6 +110,7 @@ namespace p25
const uint32_t P25_SS_INCREMENT = 72U; 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_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_STANDARD = 0x00U;
const uint8_t P25_MFG_MOT = 0x90U; const uint8_t P25_MFG_MOT = 0x90U;

@ -107,24 +107,11 @@ bool Voice::process(uint8_t* data, uint32_t len)
// Decode the NID // Decode the NID
bool valid = m_p25->m_nid.decode(data + 2U); bool valid = m_p25->m_nid.decode(data + 2U);
if (!valid) {
if (m_p25->m_rfState == RS_RF_LISTENING && !valid)
return false; return false;
}
uint8_t duid = m_p25->m_nid.getDUID(); 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? // are we interrupting a running CC?
if (m_p25->m_ccRunning) { if (m_p25->m_ccRunning) {
@ -164,6 +151,13 @@ bool Voice::process(uint8_t* data, uint32_t len)
return false; 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) { if (m_verbose) {
LogMessage(LOG_RF, P25_HDU_STR ", HDU_BSDWNACT, dstId = %u, algo = $%02X, kid = $%04X", lc.getDstId(), lc.getAlgId(), lc.getKId()); 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; return true;
} }
else if (duid == P25_DUID_LDU1) { 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; m_lastDUID = P25_DUID_LDU1;
bool alreadyDecoded = false;
uint8_t frameType = P25_FT_DATA_UNIT; uint8_t frameType = P25_FT_DATA_UNIT;
if (m_p25->m_rfState == RS_RF_LISTENING) { if (m_p25->m_rfState == RS_RF_LISTENING) {
// if this is a late entry call, clear states // 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]; uint8_t buffer[9U * 25U];
::memset(buffer, 0x00U, 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"); 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 + 155U, 6U);
m_audio.encode(data + 2U, buffer + 180U, 7U); m_audio.encode(data + 2U, buffer + 180U, 7U);
m_audio.encode(data + 2U, buffer + 204U, 8U); 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; m_rfBits += 1233U;
@ -578,6 +573,11 @@ bool Voice::process(uint8_t* data, uint32_t len)
} }
} }
else if (duid == P25_DUID_LDU2) { 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; m_lastDUID = P25_DUID_LDU2;
if (m_p25->m_rfState == RS_RF_LISTENING) { 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"); LogWarning(LOG_RF, P25_LDU2_STR ", undecodable LC, using last LDU2 LC");
m_rfLC = m_rfLastLDU2; m_rfLC = m_rfLastLDU2;
m_rfUndecodableLC++; 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 { else {
m_rfLastLDU2 = m_rfLC; m_rfLastLDU2 = m_rfLC;
@ -616,7 +634,12 @@ bool Voice::process(uint8_t* data, uint32_t len)
uint8_t buffer[9U * 25U]; uint8_t buffer[9U * 25U];
::memset(buffer, 0x00U, 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"); 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 + 155U, 6U);
m_audio.encode(data + 2U, buffer + 180U, 7U); m_audio.encode(data + 2U, buffer + 180U, 7U);
m_audio.encode(data + 2U, buffer + 204U, 8U); 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; 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_netLastLDU1 = control;
m_netLastFrameType = frameType; m_netLastFrameType = frameType;
// save MI to member variable before writing to RF
control.getMI(m_lastMI);
if (m_p25->m_control) { if (m_p25->m_control) {
lc::LC control = lc::LC(*m_dfsiLC.control()); lc::LC control = lc::LC(*m_dfsiLC.control());
m_p25->m_affiliations.touchGrant(control.getDstId()); 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_netLDU2(nullptr),
m_lastDUID(P25_DUID_TDU), m_lastDUID(P25_DUID_TDU),
m_lastIMBE(nullptr), m_lastIMBE(nullptr),
m_lastMI(nullptr),
m_hadVoice(false), m_hadVoice(false),
m_lastRejectId(0U), m_lastRejectId(0U),
m_silenceThreshold(DEFAULT_SILENCE_THRESHOLD), 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]; m_lastIMBE = new uint8_t[11U];
::memcpy(m_lastIMBE, P25_NULL_IMBE, 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);
} }
/// <summary> /// <summary>
@ -960,6 +981,7 @@ Voice::~Voice()
delete[] m_netLDU1; delete[] m_netLDU1;
delete[] m_netLDU2; delete[] m_netLDU2;
delete[] m_lastIMBE; delete[] m_lastIMBE;
delete[] m_lastMI;
} }
/// <summary> /// <summary>
@ -1164,10 +1186,6 @@ void Voice::writeNet_LDU1()
} }
} }
if (m_p25->m_control) {
m_p25->m_affiliations.touchGrant(m_rfLC.getDstId());
}
if (m_debug) { 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", 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(), control.getEmergency(), control.getEncrypted(), control.getPriority(),
@ -1201,17 +1219,16 @@ void Voice::writeNet_LDU1()
::memset(mi, 0x00U, P25_MI_LENGTH_BYTES); ::memset(mi, 0x00U, P25_MI_LENGTH_BYTES);
if (m_netLastLDU1.getAlgId() != P25_ALGO_UNENCRYPT && m_netLastLDU1.getKId() != 0) { if (m_netLastLDU1.getAlgId() != P25_ALGO_UNENCRYPT && m_netLastLDU1.getKId() != 0) {
m_netLastLDU1.getMI(mi);
control.setAlgId(m_netLastLDU1.getAlgId()); control.setAlgId(m_netLastLDU1.getAlgId());
control.setKId(m_netLastLDU1.getKId()); 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) { 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); m_netLC.setMI(mi);
@ -1608,3 +1625,76 @@ void Voice::insertNullAudio(uint8_t* data)
::memcpy(data + 204U, P25_NULL_IMBE, 11U); ::memcpy(data + 204U, P25_NULL_IMBE, 11U);
} }
} }
/// <summary>
/// Helper to insert encrypted IMBE null frames for missing audio.
/// </summary>
/// <param name="data"></param>
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);
}
}
/// <summary>
/// Given the last MI, generate the next MI using LFSR.
/// </summary>
/// <param name="lastMI"></param>
/// <param name="nextMI"></param>
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;
}
}

@ -104,6 +104,7 @@ namespace p25
uint8_t m_lastDUID; uint8_t m_lastDUID;
uint8_t* m_lastIMBE; uint8_t* m_lastIMBE;
uint8_t* m_lastMI;
bool m_hadVoice; bool m_hadVoice;
uint32_t m_lastRejectId; uint32_t m_lastRejectId;
@ -141,6 +142,10 @@ namespace p25
void insertMissingAudio(uint8_t* data); void insertMissingAudio(uint8_t* data);
/// <summary>Helper to insert IMBE null frames for missing audio.</summary> /// <summary>Helper to insert IMBE null frames for missing audio.</summary>
void insertNullAudio(uint8_t* data); void insertNullAudio(uint8_t* data);
/// <summary>Helper to insert encrypted IMBE null frames for missing audio.</summary>
void insertEncryptedNullAudio(uint8_t* data);
/// <summary>Given the last MI, generate the next MI using LFSR.</summary>
void getNextMI(uint8_t lastMI[9U], uint8_t nestMI[9U]);
}; };
} // namespace packet } // namespace packet
} // namespace p25 } // namespace p25

@ -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)
Loading…
Cancel
Save

Powered by TurnKey Linux.