Remove wxWidgets, port to standalone C++17 with cross-platform support

Replace the wxWidgets dependency with standard C++17 throughout the
entire codebase, producing a clean, portable, thoroughly reviewed CLI
daemon that builds on Linux, Windows, and macOS without any GUI toolkit.

== Removed ==

- GUI application and config editor (DStarRepeaterFrame, DStarRepeaterConfig/)
- GUI widget library (GUICommon/)
- Windows USB drivers (WindowsUSB/)
- Visual Studio solutions and project files (.sln, .vcxproj)
- NSIS installer scripts, platform-specific makefiles
- wxWidgets-specific files (LogEvent, LogRedirect, GMSKModemWinUSB)
- Dead code: unused GUI constants, vestigial command methods, unused
  DTMF/queue constants in non-TRX thread variants

== Core conversion (all source files) ==

- wxString -> std::string, wxArrayString -> std::vector<std::string>
- wxFloat32 -> float, wxUint8/16/32 -> uint8_t/16_t/32_t
- wxThread -> std::thread, wxMutex -> std::mutex
- wxStopWatch -> std::chrono::steady_clock
- wxRegEx -> std::regex, wxDynLib -> dlopen/dlsym
- wxFFile -> FILE*, wxTextFile -> std::ifstream
- WX_DECLARE_STRING_HASH_MAP -> std::unordered_map
- volatile -> std::atomic for ring buffer and cross-thread flags
- NULL -> nullptr throughout

== INI config format (MMDVMHost compatible) ==

- [Section]/Key=Value INI format replaces flat key=value
- Config file path is the single required command-line argument:
  dstarrepeaterd <config-file>
- [Log] section: FilePath, FileLevel, DisplayLevel, MQTTLevel
  (levels 0-6 matching MMDVMHost convention)
- [Paths] section: Data and Audio directories from config
- [Whitelist]/[Blacklist]/[Greylist] file paths from config
- All compile-time path defines (CONF_DIR, LOG_DIR) removed
- Example config: Data/dstarrepeater.ini.example

== Build system ==

- Plain g++ -std=c++17, no wx-config dependency
- Defaults: release build, MQTT on, GPIO auto-detected
- GPIO auto-detection checks uname -s (Linux only) and uname -m
  (arm/aarch64) to avoid false match on macOS Apple Silicon
- Zero warnings with -Wall on GCC 13
- Verified on Ubuntu 24.04, Debian Trixie, and Alpine 3.21

== Install layout ==

- Binary: /usr/bin/dstarrepeaterd
- AMBE voice data: /usr/share/dstarrepeater/
- Config directory: /etc/dstarrepeater/ (created on install)
- Example config: /etc/dstarrepeater/dstarrepeater.ini.example

== Cross-platform support ==

- EndianCompat.h: byte-swap functions for Linux, macOS, and Windows
- Serial ports: Win32 CreateFile/ReadFile/WriteFile alongside POSIX
- Sockets: Winsock2 with RAII WSAStartup alongside POSIX
- Sound card: PortAudio for Windows/macOS, ALSA for Linux
- All platform code is additive (#ifdef _WIN32), no Linux code changed

== Standalone logger ==

- Thread-safe file logger with std::mutex and daily UTC rotation
- Level-based filtering for file, display (stdout), and MQTT sinks
- LogLevel enum matches MMDVMHost numbering (1=Debug through 6=Fatal)
- Thread-safe gmtime via gmtime_r/gmtime_s
- Atomic singleton for safe cross-thread access

== Display-Driver MQTT compatibility ==

- All JSON messages match Display-Driver's expected format exactly
- D-Star start/end/lost events with callsigns and source (rf/net)
- BER updates during active RF
- DVAP RSSI signal strength publishing
- Slow-data text forwarding
- MMDVM idle mode transitions
- JSON escaping on all callsign/text fields from over-the-air data
- JSON schema: schema.json with validated $defs structure

== Security and safety fixes ==

- All cross-thread bool flags: std::atomic<bool>
- MQTTConnection m_connected and DVAP m_signal/m_squelchOpen: atomic
- RingBuffer: std::atomic<unsigned int> for ARM correctness
- All sprintf -> snprintf to prevent buffer overflows
- JSON escaping for all MQTT output from untrusted sources
- DVTOOLFileWriter filename sanitization (path traversal prevention)
- Thread-safe localtime_r/localtime_s in DVTOOLFileWriter
- GatewayProtocolHandler: bounded string from network buffer
- Integer underflow guards in protocol handler readData()
- Buffer bounds checks in writePoll()/writeRegister()
- MMDVM printDebug() minimum length guards
- PID file: /var/run with O_NOFOLLOW, PID written for systemd
- Exception-safe createThread() with full cleanup on failure
- Deprecated gethostbyname() replaced with getaddrinfo()
- CRLF stripping in callsign list loader
- Infinite loop exits in DVAP resync() and serial getResponse()
- Null guards in SoundCardReaderWriter::close()
- RingBuffer::hasSpace() off-by-one fixed

== Memory safety ==

- Delete protocol handler, modem, controller on open()/start() failure
- Thread destructors clean up all owned objects with null-after-delete
- Callsign lists properly deleted in TX/RX/TXRX thread no-op setters
- BeaconUnit buffer allocation matched to actual write size (DV_FRAME)
- Null dereference fix in endOfNetworkData() for network-first streams

== Functional fixes ==

- Restored DTMF command execution (system() via checkControl())
- Fixed getControl() parameter order (commands 3-6 were swapped)
- Fixed getStatus() argument order in idle branch (BER was at wrong
  position, causing MQTT to report garbage BER when idle)
- Added missing GMSK interface type config parsing
- Fixed IcomController uninitialized buffer and sync check order
- Fixed SplitController format string vulnerability
- Fixed DVAPController dumpPackets() dead loop
- Inline const arrays in DStarDefines.h (ODR compliance)

== Documentation ==

- Comprehensive comments added to all source files
- README.md: quick start, dependency tables, usage guide
- CONFIGURATION.md: complete INI format reference
- MQTT.md: updated for INI config, Display-Driver integration
- BUILD.md: per-platform instructions (Linux, Windows, macOS)
- CHANGELOG.md: version history, newest first
- schema.json: JSON schema for all MQTT messages

Version bumped to 20260319.
pull/17/head
Andy Taylor 4 months ago
parent 630ff49231
commit c2406d9f11

1
.gitignore vendored

@ -9,3 +9,4 @@ Release
.vs .vs
*.a *.a
*.d *.d
tmp/

@ -0,0 +1,133 @@
# Building DStarRepeater
## Linux
### Dependencies
| Package | Ubuntu / Debian | Alpine | Purpose |
|---------|-----------------|--------|---------|
| C++17 compiler | `g++` (GCC 13+) | `g++` | Compilation |
| GNU Make | `make` | `make` | Build system |
| ALSA dev | `libasound2-dev` | `alsa-lib-dev` | Sound card modem |
| libusb 1.0 dev | `libusb-1.0-0-dev` | `libusb-dev` | USB modem (GMSK) |
| Mosquitto dev | `libmosquitto-dev` | `mosquitto-dev` | MQTT telemetry (on by default) |
| Linux headers | — | `linux-headers` | Required on Alpine only |
**Raspberry Pi (additional):**
| Package | Install | Purpose |
|---------|---------|---------|
| wiringPi | `sudo apt-get install wiringpi` or [build from source](https://github.com/WiringPi/WiringPi) | GPIO controller (on by default on ARM) |
### Install (one-liner)
**Ubuntu / Debian:**
```bash
sudo apt-get install g++ make libasound2-dev libusb-1.0-0-dev libmosquitto-dev
```
**Raspberry Pi:**
```bash
sudo apt-get install g++ make libasound2-dev libusb-1.0-0-dev libmosquitto-dev wiringpi
```
**Alpine:**
```bash
apk add g++ make alsa-lib-dev libusb-dev linux-headers mosquitto-dev
```
### Build
By default, `make` produces a release build with MQTT enabled and GPIO auto-detected (on for ARM).
```bash
make # default: release, MQTT on, GPIO auto
make MQTT=0 # without MQTT (no libmosquitto-dev needed)
make GPIO=0 # without GPIO (no wiringPi needed)
make BUILD=debug # debug build with symbols and assertions
```
### Install
```bash
sudo make install
```
This installs:
- `dstarrepeaterd` to `/usr/bin`
- AMBE voice beacon data to `/usr/share/dstarrepeater/`
- Example config to `/etc/dstarrepeater/dstarrepeater.ini.example`
---
## Windows
The source includes `#if defined(_WIN32)` blocks throughout. The provided `Makefile` targets Linux only — you will need a Visual Studio project or CMakeLists.txt.
### Compiler
| Option | Requirement |
|--------|-------------|
| Visual Studio 2019+ | Enable `/std:c++17` in project properties |
| MinGW-w64 (MSYS2) | `g++ -std=c++17` |
### Libraries
| Library | Source | Required? |
|---------|--------|-----------|
| Windows SDK | Included with Visual Studio | Yes — provides Winsock2, serial port, and console APIs |
| [libusb 1.0](https://libusb.info/) | Pre-built Windows binaries available | Yes — GMSK modem support |
| [PortAudio](http://www.portaudio.com/) | Build from source or use vcpkg | Yes — sound card modem (replaces ALSA) |
| [Eclipse Mosquitto](https://mosquitto.org/) | Installer or vcpkg | Optional — only for MQTT builds |
### Linker flags
```
ws2_32.lib Winsock2 (sockets)
portaudio.lib Sound card modem
libusb-1.0.lib GMSK modem
mosquitto.lib MQTT (optional)
```
### Platform notes
- Serial ports use `CreateFile` / `ReadFile` / `WriteFile` (Win32 API)
- Sockets use Winsock2 with automatic `WSAStartup` / `WSACleanup`
- Signal handling uses `SetConsoleCtrlHandler` (Ctrl+C, close events)
- Single-instance enforcement uses a named mutex (no PID file)
- K8055 (Velleman) and URI USB controllers are stub-only on Windows — they require vendor DLLs that are not yet integrated
---
## macOS
The source includes macOS support for endian handling (`OSByteOrder.h`), serial ports (POSIX `termios`, same as Linux), and PortAudio audio. The `Makefile` currently targets Linux only.
### Dependencies (Homebrew)
```bash
xcode-select --install
brew install libusb portaudio
```
For MQTT support:
```bash
brew install mosquitto
```
### Building
The Makefile needs adjusted compiler flags and Homebrew library paths for macOS. A minimal approach:
```bash
export CFLAGS="-std=c++17 -O2 -Wall -I$(brew --prefix)/include"
export LDFLAGS="-L$(brew --prefix)/lib"
# then adjust the Makefile or build manually
```
### Platform notes
- Endian conversion uses `<libkern/OSByteOrder.h>` (via `EndianCompat.h`)
- Sound card modem uses PortAudio (same as Windows)
- Serial ports use POSIX `termios` (same as Linux)
- ALSA is not available — sound card modem requires PortAudio

@ -1,70 +0,0 @@
Repeater - 20180710
===================
Windows
-------
To use the Repeater control software you will first need to build the latest
version of wxWidgets (http://www.wxwidgets.org), the version I used was 3.0.4.
I also installed it in the default location which is C:\wxWidgets-3.0.4.
You will also need a copy of PortAudio (http://www.portaudio.com), I used the
latest stable version and it appears to be fine. I put that into the
"Visual Studio 2017\Projects" folder alongside the source code for the
Repeater itself.
For compiling I use Visual C++ 2017 Community Edition downloaded from Microsoft
for free. I recommend that you use the same.
To build wxWidgets, you simply need to open Visual Studio 2017 using the File ->
Open -> Projects/Solutions and load the wx_vc12.sln file to be found in
wxWidgets-3.0.4\build\msw directory and then go into Batch Build and select the
DLL Debug and DLL Release entries for every one, this take a little time! Then
build them.
Unfortunately building the software under Windows also requires the downloading
of a very large file from Microsoft, the WDK. There is no easy way around this
but once installed it can be forgotten about.
The path names for things like wxWidgets and PortAudio are embedded within the
Solution and Project preferences, and will need changing if anything other than
these default locations are used. The first pass through the compiler will no
doubt tell you all that you need to know if there are problems.
Once you have built the executables, you will need to copy the correct portaudio
and wxWidgets files to the same location as the executables. For 32-bit systems
these are portaudio_x86.dll, wxbase30u_vc_custom.dll, wxmsw30u_adv_vc_custom.dll,
and wxmsw30u_core_vc_custom.dll. On 64-bit systems you'll need portaudio_x64.dll,
wxbase30u_vc_x64_custom.dll, wxmsw30u_adv_vc_x64_custom.dll, and
wxmsw30u_core_vc_x64_custom.dll
If you are running in debug mode then the required wxWidgets files have the names
xxx30ud_xxxx instead. These can be found in the wxWidgets-3.0.4\lib\vc_dll
directory.
It is also probable that you'll need to install a copy of the latest Visual C++
run-time libraries from Microsoft, if you are not running the repeater software
on the same machine as the development/compilation was done on. You can find the
latest versions at https://support.microsoft.com/en-gb/help/2977003/the-latest-supported-visual-c-downloads
Linux
-----
You need to ensure that wxGTK, ALSA and libusb-1.0 are already
installed on your machine, under Ubuntu these are available from the standard
repositories, the version of wxWidgets is adequate. However you should get the
latest version of PortAudio from its home and build it from scratch.
To install them from scratch, you need to get wxGTK from
<http://www.wxwidgets.org>, and libusb-1.0 from <http://www.libusb.org/wiki/libusb-1.0>.
If you do a "make install" on all of them then they'll be installed in the
right places and nothing more needs to be done.
On the Raspberry Pi you will need to build and install the wiringPi GPIO library
which is available from <https://projects.drogon.net/raspberry-pi/wiringpi>.
This will allow use the GPIO pins as controller pins for the different repeaters
if needed.
To actually build the software, type "make" in the same directory as this file
and all should build without errors, there may be a warning or two though. Once
compiled log in as root or use the sudo command, and do "make install".

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,29 +1,32 @@
# DStarRepeater Configuration Reference # DStarRepeater Configuration Reference
> **Note:** The config parser treats any line beginning with `#` as a The configuration file uses INI format with `[Section]` headers, `Key=Value` pairs, and `#` comments. The format is compatible with MMDVMHost.
> comment, and blank lines are skipped. Use `dstarrepeater.example` as
> your starting point for a clean config.
**Config file location:** `/etc/dstarrepeater` To get started, copy the example config and edit for your installation:
(or `/etc/dstarrepeater_<name>` when using the `-name` command line option) ```bash
sudo cp /etc/dstarrepeater/dstarrepeater.ini.example /etc/dstarrepeater/dstarrepeater.ini
sudo nano /etc/dstarrepeater/dstarrepeater.ini
```
**Format:** `key=value` — one setting per line, no spaces around the `=`. Then start the daemon:
Boolean values use `0` for off/false and `1` for on/true. ```bash
dstarrepeaterd /etc/dstarrepeater/dstarrepeater.ini
```
--- ---
## Callsign Settings ## [General]
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `callsign=GB3IN C` | Repeater callsign, padded to 8 characters. 7-character base callsign + 1-character module suffix (A, B, C, D, or E). | | `Callsign` | `GB3IN C` | Repeater callsign, padded to 8 characters. 7-character base + 1-character module suffix (AE). |
| `gateway=` | Gateway callsign, padded to 8 characters (e.g. `GB3IN G`). Leave empty if not linked to a gateway. | | `Gateway` | *(empty)* | Gateway callsign, padded to 8 characters (e.g. `GB3IN G`). Leave empty if not linked. |
| `mode=0` | Operating mode — see [Mode Values](#mode-values) below. | | `Mode` | `0` | Operating mode — see [Mode Values](#mode-values) below. |
| `ack=1` | Acknowledgement type — see [Ack Values](#ack-values) below. Forced to `0` in Gateway, TX Only, RX Only, and TX and RX modes. | | `Ack` | `1` | Acknowledgement type — see [Ack Values](#ack-values). Forced to `0` in Gateway, TX Only, RX Only, and TX+RX modes. |
| `restriction=0` | `1` = only whitelisted callsigns accepted. | | `Restriction` | `0` | `1` = only whitelisted callsigns accepted. |
| `rpt1Validation=1` | `1` = only accept headers addressed to this repeater. Forced to `1` in Gateway mode. | | `RPT1Validation` | `1` | `1` = only accept headers addressed to this repeater. |
| `dtmfBlanking=1` | `1` = mute DTMF tones from the audio stream. | | `DTMFBlanking` | `1` | `1` = mute DTMF tones from the audio stream. |
| `errorReply=1` | `1` = reply with an error message for invalid commands. | | `ErrorReply` | `1` | `1` = reply with error message for invalid commands. |
### Mode Values ### Mode Values
@ -32,9 +35,9 @@ Boolean values use `0` for off/false and `1` for on/true.
| `0` | Duplex | Full duplex repeater (simultaneous TX and RX) | | `0` | Duplex | Full duplex repeater (simultaneous TX and RX) |
| `1` | Simplex | Simplex repeater | | `1` | Simplex | Simplex repeater |
| `2` | Gateway | Gateway mode (ack forced off, RPT1 validation forced on) | | `2` | Gateway | Gateway mode (ack forced off, RPT1 validation forced on) |
| `3` | TX Only | Transmit only (ack forced off) | | `3` | TX Only | Transmit only |
| `4` | RX Only | Receive only (ack forced off) | | `4` | RX Only | Receive only |
| `5` | TX and RX | Sequential transmit then receive | | `5` | TX and RX | Sequential transmit then receive (split-site) |
### Ack Values ### Ack Values
@ -46,58 +49,82 @@ Boolean values use `0` for off/false and `1` for on/true.
--- ---
## Network Settings ## [Log]
| Setting | Description | Logging levels match MMDVMHost: `0`=None, `1`=Debug, `2`=Message, `3`=Info, `4`=Warning, `5`=Error, `6`=Fatal. Higher values are more selective (fewer messages).
|---------|-------------|
| `gatewayAddress=127.0.0.1` | IP address of the [ircDDBGateway](https://github.com/g4klx/ircDDBGateway) or [DStarGateway](https://github.com/g4klx/DStarGateway). | | Key | Default | Description |
| `gatewayPort=20010` | UDP port the gateway is listening on. | |-----|---------|-------------|
| `localAddress=127.0.0.1` | Local IP address to bind for gateway communication. | | `FilePath` | `/var/log` | Directory for daily log files (`dstarrepeaterd-YYYY-MM-DD.log`). |
| `localPort=20011` | Local UDP port for gateway communication. | | `FileLevel` | `2` | Minimum level to write to log file. `0` = no file logging. |
| `networkName=` | Network name identifier for multi-repeater setups. Leave empty for a single repeater. | | `DisplayLevel` | `2` | Minimum level to write to stdout. `0` = no stdout output. |
| `MQTTLevel` | `2` | Minimum level to forward to MQTT. `0` = no MQTT logging. |
--- ---
## Modem Type ## [Paths]
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `modemType=DVAP` | The type of modem hardware connected to this repeater. | | `Data` | `/usr/share/dstarrepeater` | Directory containing AMBE voice beacon files (`.ambe` and `.indx`). |
| `Audio` | `/var/log` | Directory for DVTool audio recordings and announcements. |
Must be one of the following strings (case-sensitive): ---
- `DVAP` ## [Network]
- `DVMEGA`
- `DV-RPTR V1`
- `DV-RPTR V2`
- `DV-RPTR V3`
- `GMSK Modem`
- `MMDVM`
- `Sound Card`
- `Split`
- `Icom Access Point/Terminal Mode`
Only the settings for your selected modem type need to be configured — see [Modem-Specific Settings](#modem-specific-settings). | Key | Default | Description |
|-----|---------|-------------|
| `GatewayAddress` | `127.0.0.1` | IP address of the gateway (ircDDBGateway or DStarGateway). |
| `GatewayPort` | `20010` | UDP port the gateway is listening on. |
| `LocalAddress` | `127.0.0.1` | Local IP address to bind. |
| `LocalPort` | `20011` | Local UDP port. |
| `Name` | *(empty)* | Network name for multi-repeater setups. |
--- ---
## Timing ## [Modem]
| Key | Default | Description |
|-----|---------|-------------|
| `Type` | `DVAP` | Hardware modem type (case-sensitive). |
Valid modem types:
| Setting | Description | | Value | Hardware |
|---------|-------------| |-------|----------|
| `timeout=180` | Transmission timeout in seconds. `0` to disable. | | `DVAP` | DVAP USB Dongle |
| `ackTime=500` | Acknowledgement delay in milliseconds. | | `DVMEGA` | DV-Mega board |
| `DV-RPTR V1` | DV-RPTR Version 1 |
| `DV-RPTR V2` | DV-RPTR Version 2 |
| `DV-RPTR V3` | DV-RPTR Version 3 |
| `GMSK Modem` | GMSK modem (Dutch*STAR) |
| `MMDVM` | Multi-Mode Digital Voice Modem |
| `Sound Card` | Sound card modem (ALSA/PortAudio) |
| `Split` | Multi-receiver split-site |
| `Icom Access Point/Terminal Mode` | Icom radio in AP/terminal mode |
Only configure the section matching your modem type.
--- ---
## Beacon ## [Times]
| Key | Default | Description |
|-----|---------|-------------|
| `Timeout` | `180` | Transmission timeout in seconds. `0` to disable. |
| `AckTime` | `500` | Acknowledgement delay in milliseconds. |
---
| Setting | Description | ## [Beacon]
|---------|-------------|
| `beaconTime=600` | Beacon interval in seconds. `0` to disable. | | Key | Default | Description |
| `beaconText=D-Star Repeater` | Text transmitted in the beacon slow data field. | |-----|---------|-------------|
| `beaconVoice=0` | `1` = transmit a voice beacon in addition to data. | | `Time` | `600` | Beacon interval in seconds. `0` to disable. |
| `language=0` | Language for voice beacon text-to-speech — see [Language Values](#language-values) below. | | `Text` | `D-Star Repeater` | Text transmitted in the beacon slow data field. |
| `Voice` | `0` | `1` = transmit a voice beacon in addition to data. |
| `Language` | `0` | Language for voice beacon — see [Language Values](#language-values). |
### Language Values ### Language Values
@ -117,233 +144,224 @@ Only the settings for your selected modem type need to be configured — see [Mo
--- ---
## Announcement ## [Announcement]
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `announcementEnabled=0` | `1` = enable the announcement/recording feature. | | `Enabled` | `0` | `1` = enable announcement recording and playback. |
| `announcementTime=500` | Announcement playback interval in milliseconds. | | `Time` | `500` | Announcement playback interval in seconds. |
| `announcementRecordRPT1=` | RPT1 callsign that triggers recording. | | `RecordRPT1` | *(empty)* | RPT1 callsign that triggers recording. |
| `announcementRecordRPT2=` | RPT2 callsign that triggers recording. | | `RecordRPT2` | *(empty)* | RPT2 callsign that triggers recording. |
| `announcementDeleteRPT1=` | RPT1 callsign that triggers deletion. | | `DeleteRPT1` | *(empty)* | RPT1 callsign that triggers deletion. |
| `announcementDeleteRPT2=` | RPT2 callsign that triggers deletion. | | `DeleteRPT2` | *(empty)* | RPT2 callsign that triggers deletion. |
--- ---
## DTMF Control ## [Control]
| Setting | Description | DTMF remote control via D-Star headers. When enabled, specific YOURCALL values trigger actions.
|---------|-------------|
| `controlEnabled=0` | `1` = enable DTMF remote control. | | Key | Default | Description |
| `controlRPT1=` | RPT1 callsign required for control commands. | |-----|---------|-------------|
| `controlRPT2=` | RPT2 callsign required for control commands. | | `Enabled` | `0` | `1` = enable DTMF remote control. |
| `controlShutdown=` | DTMF sequence to remotely shut down the repeater. | | `RPT1` | *(empty)* | RPT1 callsign required for control commands. |
| `controlStartup=` | DTMF sequence to remotely start up the repeater. | | `RPT2` | *(empty)* | RPT2 callsign required for control commands. |
| `controlStatus1=` | DTMF sequence to trigger status message 1. | | `Shutdown` | *(empty)* | YOURCALL value to shut down the repeater. |
| `controlStatus2=` | DTMF sequence to trigger status message 2. | | `Startup` | *(empty)* | YOURCALL value to start up the repeater. |
| `controlStatus3=` | DTMF sequence to trigger status message 3. | | `Status1``Status5` | *(empty)* | YOURCALL values to trigger status messages 15. |
| `controlStatus4=` | DTMF sequence to trigger status message 4. | | `Command1``Command6` | *(empty)* | YOURCALL values for custom commands 16. |
| `controlStatus5=` | DTMF sequence to trigger status message 5. | | `Command1Line``Command6Line` | *(empty)* | Shell command executed when the corresponding command is triggered. |
| `controlCommand1=` | DTMF sequence for custom command 1. | | `Output1``Output4` | *(empty)* | YOURCALL values to toggle output relays 14. |
| `controlCommand1Line=` | Shell command executed when command 1 is received. |
| `controlCommand2=` | DTMF sequence for custom command 2. |
| `controlCommand2Line=` | Shell command executed when command 2 is received. |
| `controlCommand3=` | DTMF sequence for custom command 3. |
| `controlCommand3Line=` | Shell command executed when command 3 is received. |
| `controlCommand4=` | DTMF sequence for custom command 4. |
| `controlCommand4Line=` | Shell command executed when command 4 is received. |
| `controlCommand5=` | DTMF sequence for custom command 5. |
| `controlCommand5Line=` | Shell command executed when command 5 is received. |
| `controlCommand6=` | DTMF sequence for custom command 6. |
| `controlCommand6Line=` | Shell command executed when command 6 is received. |
| `controlOutput1=` | DTMF sequence to toggle output relay 1. |
| `controlOutput2=` | DTMF sequence to toggle output relay 2. |
| `controlOutput3=` | DTMF sequence to toggle output relay 3. |
| `controlOutput4=` | DTMF sequence to toggle output relay 4. |
--- ---
## External Controller ## [Controller]
External hardware controller for PTT, LEDs, and relay outputs.
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `controllerType=` | Hardware controller type for PTT, LEDs, and relays — see [Controller Types](#controller-types) below. Leave empty for none. | | `Type` | *(empty)* | Controller type — see [Controller Types](#controller-types). |
| `serialConfig=1` | Hardware config selection (15). Selects pin/output mappings on the controller. | | `SerialConfig` | `1` | Pin/output configuration preset (15). |
| `pttInvert=0` | `1` = invert PTT output (active low). | | `PTTInvert` | `0` | `1` = invert PTT output (active low). |
| `activeHangTime=0` | Seconds to hold active state after last transmission ends. `0` to disable. | | `ActiveHangTime` | `0` | Seconds to hold active state after last transmission. `0` to disable. |
### Controller Types ### Controller Types
| Value | Description | | Value | Description |
|-------|-------------| |-------|-------------|
| *(empty)* | No controller | | *(empty)* | No controller (dummy) |
| `GPIO` | Raspberry Pi GPIO pins | | `GPIO` | Raspberry Pi GPIO pins |
| `UDRC` | UDRC board (Raspberry Pi) | | `UDRC` | UDRC board (Raspberry Pi) |
| `Velleman K8055 - 0` | Velleman K8055 USB board at address 03 | | `Velleman K8055 - 0` | Velleman K8055 USB board (address 03) |
| `URI USB - 1` | URI USB relay card at address 16 | | `URI USB - 1` | URI USB relay card (address 16) |
| `Serial - /dev/ttyUSB0` | Serial port controller | | `Serial - /dev/ttyUSB0` | Serial port controller |
| `Arduino - /dev/ttyUSB0` | Arduino controller via serial | | `Arduino - /dev/ttyUSB0` | Arduino controller via serial |
--- ---
## Output Relay Defaults ## [Outputs]
| Setting | Description | Default state of output relays at startup.
|---------|-------------|
| `output1=0` | Default state of output relay 1 at startup. `1` = on. | | Key | Default | Description |
| `output2=0` | Default state of output relay 2 at startup. `1` = on. | |-----|---------|-------------|
| `output3=0` | Default state of output relay 3 at startup. `1` = on. | | `Output1` | `0` | Output relay 1. `1` = on at startup. |
| `output4=0` | Default state of output relay 4 at startup. `1` = on. | | `Output2` | `0` | Output relay 2. |
| `Output3` | `0` | Output relay 3. |
| `Output4` | `0` | Output relay 4. |
--- ---
## Logging ## [Frame Logging]
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `logging=0` | `1` = enable logging to file. | | `Enabled` | `0` | `1` = record all D-Star frames to `.dvtool` files in the audio directory. |
--- ---
## Window Position (GUI only) ## [Whitelist] / [Blacklist] / [Greylist]
Callsign access control lists. Each takes a single `File` key pointing to a text file with one callsign per line.
| Key | Default | Description |
|-----|---------|-------------|
| `File` | *(not set)* | Full path to the list file. Leave commented out to disable. |
| Setting | Description | Example:
|---------|-------------| ```ini
| `windowX=-1` | Window X position. `-1` for system default. | [Whitelist]
| `windowY=-1` | Window Y position. `-1` for system default. | File=/etc/dstarrepeater/rptr_whitelist.dat
```
--- ---
## Modem-Specific Settings ## Modem-Specific Settings
Only configure the section that matches your `modemType`. Only configure the section matching your `[Modem] Type`.
### DVAP ### [DVAP]
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `dvapPort=` | Serial port (e.g. `/dev/ttyUSB0`). | | `Port` | *(empty)* | Serial port (e.g. `/dev/ttyUSB0`). |
| `dvapFrequency=145500000` | Operating frequency in Hz (e.g. `438500000` = 438.500 MHz). | | `Frequency` | `145500000` | Operating frequency in Hz. |
| `dvapPower=10` | Transmit power in dBm. | | `Power` | `10` | Transmit power in dBm. |
| `dvapSquelch=-100` | Squelch level in dBm. Signals below this are ignored. | | `Squelch` | `-100` | Squelch threshold in dBm. |
### GMSK Modem ### [GMSK]
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `gmskAddress=768` | USB address of the GMSK modem (decimal). Default 768 = 0x0300 hex. | | `InterfaceType` | `0` | `0` = libUSB, `1` = direct/serial. |
| `Address` | `768` | USB address in decimal (768 = 0x0300). |
### DV-RPTR V1
### [DV-RPTR V1]
| Setting | Description |
|---------|-------------| | Key | Default | Description |
| `dvrptr1Port=` | Serial port (e.g. `/dev/ttyUSB0`). | |-----|---------|-------------|
| `dvrptr1RXInvert=0` | `1` = invert received signal polarity. | | `Port` | *(empty)* | Serial port (e.g. `/dev/ttyUSB0`). |
| `dvrptr1TXInvert=0` | `1` = invert transmitted signal polarity. | | `RXInvert` | `0` | `1` = invert received signal polarity. |
| `dvrptr1Channel=0` | `0` = Channel A, `1` = Channel B. | | `TXInvert` | `0` | `1` = invert transmitted signal polarity. |
| `dvrptr1ModLevel=20` | Modulation level (0100%). | | `Channel` | `0` | `0` = Channel A, `1` = Channel B. |
| `dvrptr1TXDelay=150` | Transmit delay in milliseconds. | | `ModLevel` | `20` | Modulation level (0100%). |
| `TXDelay` | `150` | Transmit delay in milliseconds. |
### DV-RPTR V2
### [DV-RPTR V2]
| Setting | Description |
|---------|-------------| | Key | Default | Description |
| `dvrptr2Connection=0` | `0` = USB, `1` = Network. | |-----|---------|-------------|
| `dvrptr2USBPort=` | USB serial port (when connection = `0`). | | `Connection` | `0` | `0` = USB, `1` = Network. |
| `dvrptr2Address=127.0.0.1` | Network address (when connection = `1`). | | `USBPort` | *(empty)* | USB serial port. |
| `dvrptr2Port=0` | Network port (when connection = `1`). | | `Address` | `127.0.0.1` | Network address (when Connection=1). |
| `dvrptr2TXInvert=0` | `1` = invert transmitted signal polarity. | | `Port` | `0` | Network port (when Connection=1). |
| `dvrptr2ModLevel=20` | Modulation level (0100%). | | `TXInvert` | `0` | `1` = invert transmitted signal polarity. |
| `dvrptr2TXDelay=150` | Transmit delay in milliseconds. | | `ModLevel` | `20` | Modulation level (0100%). |
| `TXDelay` | `150` | Transmit delay in milliseconds. |
### DV-RPTR V3
### [DV-RPTR V3]
| Setting | Description |
|---------|-------------| Same keys as [DV-RPTR V2].
| `dvrptr3Connection=0` | `0` = USB, `1` = Network. |
| `dvrptr3USBPort=` | USB serial port (when connection = `0`). | ### [DVMEGA]
| `dvrptr3Address=127.0.0.1` | Network address (when connection = `1`). |
| `dvrptr3Port=0` | Network port (when connection = `1`). | | Key | Default | Description |
| `dvrptr3TXInvert=0` | `1` = invert transmitted signal polarity. | |-----|---------|-------------|
| `dvrptr3ModLevel=20` | Modulation level (0100%). | | `Port` | *(empty)* | Serial port (e.g. `/dev/ttyAMA0`). |
| `dvrptr3TXDelay=150` | Transmit delay in milliseconds. | | `Variant` | `0` | `0` = Modem, `1` = Radio 2m, `2` = Radio 70cm, `3` = Radio 2m/70cm. |
| `RXInvert` | `0` | `1` = invert received signal. |
### DVMEGA | `TXInvert` | `0` | `1` = invert transmitted signal. |
| `TXDelay` | `150` | Transmit delay in milliseconds. |
| Setting | Description | | `RXFrequency` | `145500000` | Receive frequency in Hz (radio variants only). |
|---------|-------------| | `TXFrequency` | `145500000` | Transmit frequency in Hz (radio variants only). |
| `dvmegaPort=` | Serial port (e.g. `/dev/ttyAMA0`). | | `Power` | `100` | Transmit power 0100% (radio variants only). |
| `dvmegaVariant=0` | `0` = Modem, `1` = Radio 2m, `2` = Radio 70cm, `3` = Radio 2m/70cm. |
| `dvmegaRXInvert=0` | `1` = invert received signal polarity. | ### [MMDVM]
| `dvmegaTXInvert=0` | `1` = invert transmitted signal polarity. |
| `dvmegaTXDelay=150` | Transmit delay in milliseconds. | | Key | Default | Description |
| `dvmegaRXFrequency=145500000` | Receive frequency in Hz (radio variants only). | |-----|---------|-------------|
| `dvmegaTXFrequency=145500000` | Transmit frequency in Hz (radio variants only). | | `Port` | *(empty)* | Serial port (e.g. `/dev/ttyACM0`). |
| `dvmegaPower=100` | Transmit power as a percentage (0100%, radio variants only). | | `RXInvert` | `0` | `1` = invert received signal. |
| `TXInvert` | `0` | `1` = invert transmitted signal. |
### MMDVM | `PTTInvert` | `0` | `1` = invert PTT signal. |
| `TXDelay` | `50` | Transmit delay in milliseconds. |
| Setting | Description | | `RXLevel` | `100` | Receive level (0100%). |
|---------|-------------| | `TXLevel` | `100` | Transmit level (0100%). |
| `mmdvmPort=` | Serial port (e.g. `/dev/ttyACM0`). |
| `mmdvmRXInvert=0` | `1` = invert received signal polarity. | ### [Sound Card]
| `mmdvmTXInvert=0` | `1` = invert transmitted signal polarity. |
| `mmdvmPTTInvert=0` | `1` = invert PTT signal. | | Key | Default | Description |
| `mmdvmTXDelay=50` | Transmit delay in milliseconds. | |-----|---------|-------------|
| `mmdvmRXLevel=100` | Receive audio level (0100%). | | `RXDevice` | *(empty)* | Sound card device name for receiving. |
| `mmdvmTXLevel=100` | Transmit audio level (0100%). | | `TXDevice` | *(empty)* | Sound card device name for transmitting. |
| `RXInvert` | `0` | `1` = invert received signal. |
### Sound Card | `TXInvert` | `0` | `1` = invert transmitted signal. |
| `RXLevel` | `1.0` | Receive level multiplier (1.0 = unity gain). |
| Setting | Description | | `TXLevel` | `1.0` | Transmit level multiplier (1.0 = unity gain). |
|---------|-------------| | `TXDelay` | `150` | Transmit delay in milliseconds. |
| `soundCardRXDevice=` | Sound card device name for receiving audio. | | `TXTail` | `50` | Transmit tail in milliseconds (extra carrier after last frame). |
| `soundCardTXDevice=` | Sound card device name for transmitting audio. |
| `soundCardRXInvert=0` | `1` = invert received signal polarity. | ### [Split]
| `soundCardTXInvert=0` | `1` = invert transmitted signal polarity. |
| `soundCardRXLevel=1.0000` | Receive level multiplier (1.0 = unity gain). | Multi-receiver split-site configuration using UDP.
| `soundCardTXLevel=1.0000` | Transmit level multiplier (1.0 = unity gain). |
| `soundCardTXDelay=150` | Transmit delay in milliseconds. | | Key | Default | Description |
| `soundCardTXTail=50` | Transmit tail in milliseconds (extra carrier after last frame). | |-----|---------|-------------|
| `LocalAddress` | *(empty)* | Local IP address for split communication. |
### Icom Access Point/Terminal Mode | `LocalPort` | `0` | Local UDP port. |
| `Timeout` | `0` | Timeout in milliseconds. `0` to disable. |
| Setting | Description |
|---------|-------------| TX/RX names are configured as indexed keys: `TXName0``TXName4` and `RXName0``RXName24`.
| `icomPort=` | Serial port (e.g. `/dev/ttyUSB0`). |
### [Icom]
### Split
| Key | Default | Description |
| Setting | Description | |-----|---------|-------------|
|---------|-------------| | `Port` | *(empty)* | Serial port (e.g. `/dev/ttyUSB0`). |
| `splitLocalAddress=` | Local IP address for split RX/TX communication. |
| `splitLocalPort=0` | Local UDP port for split communication. |
| `splitTimeout=0` | Split timeout in seconds. `0` to disable. |
Split TX/RX names are stored as indexed entries (`splitTXName0`, `splitRXName0`, etc.) — up to 5 transmitters and 25 receivers.
--- ---
## MQTT Settings ## [MQTT]
*Only used when built with `make MQTT=1`. See [MQTT.md](MQTT.md) for full details on topics, JSON format, and Display-Driver compatibility.* *Only used when built with `make MQTT=1`. See [MQTT.md](MQTT.md) for full details.*
| Setting | Description | | Key | Default | Description |
|---------|-------------| |-----|---------|-------------|
| `mqttHost=127.0.0.1` | MQTT broker hostname or IP address. Set to empty to disable MQTT at runtime even when compiled in. | | `Host` | `127.0.0.1` | MQTT broker hostname or IP. Empty to disable MQTT at runtime. |
| `mqttPort=1883` | MQTT broker port. | | `Port` | `1883` | Broker port. |
| `mqttAuth=0` | `1` = authenticate with the broker using `mqttUsername` and `mqttPassword`. | | `Auth` | `0` | `1` = authenticate with username/password. |
| `mqttUsername=` | Broker username (when `mqttAuth=1`). | | `Username` | *(empty)* | Broker username (when Auth=1). |
| `mqttPassword=` | Broker password (when `mqttAuth=1`). | | `Password` | *(empty)* | Broker password (when Auth=1). |
| `mqttKeepalive=60` | Keepalive interval in seconds. Minimum 5. | | `Keepalive` | `60` | Keepalive interval in seconds. |
| `mqttName=dstar-repeater` | Client name and topic prefix for all published messages. | | `Name` | `dstar-repeater` | Client name and topic prefix. |
### MQTT Topics ### MQTT Topics
With the default `mqttName` of `dstar-repeater`, the following topics are published: With `Name=dstar-repeater`:
| Topic | Content | | Topic | Content |
|-------|---------| |-------|---------|
| `dstar-repeater/log` | Timestamped log messages, filtered by severity. | | `dstar-repeater/log` | Timestamped log messages (filtered by MQTTLevel). |
| `dstar-repeater/status` | JSON repeater status, published once per second. | | `dstar-repeater/status` | JSON repeater status (1/sec). |
| `dstar-repeater/json` | Display-Driver-compatible events at state transitions. | | `dstar-repeater/json` | Display-Driver-compatible D-Star events. |

@ -18,7 +18,8 @@
#include "AMBEFEC.h" #include "AMBEFEC.h"
#include <wx/wx.h> #include <cassert>
#include <cstddef>
static const unsigned int PRNG_TABLE[] = { static const unsigned int PRNG_TABLE[] = {
0x42CC47U, 0x19D6FEU, 0x304729U, 0x6B2CD0U, 0x60BF47U, 0x39650EU, 0x7354F1U, 0xEACF60U, 0x819C9FU, 0xDE25CEU, 0x42CC47U, 0x19D6FEU, 0x304729U, 0x6B2CD0U, 0x60BF47U, 0x39650EU, 0x7354F1U, 0xEACF60U, 0x819C9FU, 0xDE25CEU,
@ -442,7 +443,7 @@ CAMBEFEC::~CAMBEFEC()
unsigned int CAMBEFEC::regenerate(unsigned char* bytes) const unsigned int CAMBEFEC::regenerate(unsigned char* bytes) const
{ {
wxASSERT(bytes != NULL); assert(bytes != nullptr);
unsigned int a = ((bytes[0] & 0x80) ? 0x800000 : 0x000000) | ((bytes[0] & 0x02) ? 0x400000 : 0x000000) | unsigned int a = ((bytes[0] & 0x80) ? 0x800000 : 0x000000) | ((bytes[0] & 0x02) ? 0x400000 : 0x000000) |
((bytes[1] & 0x08) ? 0x200000 : 0x000000) | ((bytes[2] & 0x20) ? 0x100000 : 0x000000) | ((bytes[1] & 0x08) ? 0x200000 : 0x000000) | ((bytes[2] & 0x20) ? 0x100000 : 0x000000) |
@ -526,7 +527,7 @@ unsigned int CAMBEFEC::regenerate(unsigned char* bytes) const
unsigned int CAMBEFEC::count(const unsigned char* bytes) const unsigned int CAMBEFEC::count(const unsigned char* bytes) const
{ {
wxASSERT(bytes != NULL); assert(bytes != nullptr);
unsigned int a = ((bytes[0] & 0x80) ? 0x800000 : 0x000000) | ((bytes[0] & 0x02) ? 0x400000 : 0x000000) | unsigned int a = ((bytes[0] & 0x80) ? 0x800000 : 0x000000) | ((bytes[0] & 0x02) ? 0x400000 : 0x000000) |
((bytes[1] & 0x08) ? 0x200000 : 0x000000) | ((bytes[2] & 0x20) ? 0x100000 : 0x000000) | ((bytes[1] & 0x08) ? 0x200000 : 0x000000) | ((bytes[2] & 0x20) ? 0x100000 : 0x000000) |

@ -21,12 +21,23 @@
#include "Golay.h" #include "Golay.h"
/*
* Forward error correction for AMBE codec frames.
*
* D-Star uses AMBE+2 (72-bit frames) with a Golay(23,12) FEC scheme applied
* to the most significant bits of each frame. regenerate() attempts to repair
* bit errors in the three Golay codewords that make up one AMBE frame, and
* returns the number of errors that were corrected. count() returns the number
* of uncorrectable errors without modifying the data.
*/
class CAMBEFEC { class CAMBEFEC {
public: public:
CAMBEFEC(); CAMBEFEC();
~CAMBEFEC(); ~CAMBEFEC();
// Attempts in-place error correction; returns number of errors corrected.
unsigned int regenerate(unsigned char* bytes) const; unsigned int regenerate(unsigned char* bytes) const;
// Counts bit errors without modifying the data.
unsigned int count(const unsigned char* bytes) const; unsigned int count(const unsigned char* bytes) const;
private: private:

@ -16,6 +16,8 @@
#include "HeaderData.h" #include "HeaderData.h"
// Callback interface implemented by the repeater thread.
// CAnnouncementUnit calls these to key up the transmitter and push announcement DV frames.
class IAnnouncementCallback { class IAnnouncementCallback {
public: public:
virtual void transmitAnnouncementHeader(CHeaderData* header) = 0; virtual void transmitAnnouncementHeader(CHeaderData* header) = 0;

@ -18,12 +18,20 @@
#include "AnnouncementUnit.h" #include "AnnouncementUnit.h"
#include <wx/filename.h> #include <cassert>
#include <wx/file.h> #include <cstdio>
#include <cstring>
#if defined(_WIN32)
#include <io.h>
#define access _access
#define F_OK 0
#else
#include <unistd.h>
#endif
const wxString GLOBAL_FILE_NAME = wxT("Announce"); static const std::string GLOBAL_FILE_NAME = "Announce";
CAnnouncementUnit::CAnnouncementUnit(IAnnouncementCallback* handler, const wxString& callsign) : CAnnouncementUnit::CAnnouncementUnit(IAnnouncementCallback* handler, const std::string& callsign) :
m_handler(handler), m_handler(handler),
m_localFileName(), m_localFileName(),
m_reader(), m_reader(),
@ -32,14 +40,23 @@ m_time(),
m_out(0U), m_out(0U),
m_sending(false) m_sending(false)
{ {
wxASSERT(handler != NULL); assert(handler != nullptr);
m_localFileName.Printf(wxT("Announce %s"), callsign.c_str()); m_localFileName = "Announce " + callsign;
#if !defined(__WINDOWS__)
m_localFileName.Replace(wxT(" "), wxT("_")); // Replace spaces with underscores in the filename
#endif size_t pos = 0;
while ((pos = m_localFileName.find(' ', pos)) != std::string::npos) {
m_localFileName.replace(pos, 1, "_");
pos += 1;
}
m_writer.setDirectory(wxFileName::GetHomeDir()); #if defined(_WIN32)
const char* home = getenv("USERPROFILE");
#else
const char* home = getenv("HOME");
#endif
m_writer.setDirectory(std::string(home != nullptr ? home : ""));
} }
CAnnouncementUnit::~CAnnouncementUnit() CAnnouncementUnit::~CAnnouncementUnit()
@ -65,23 +82,33 @@ bool CAnnouncementUnit::writeData(const unsigned char* data, unsigned int length
void CAnnouncementUnit::deleteAnnouncement() void CAnnouncementUnit::deleteAnnouncement()
{ {
wxFileName fileName(wxFileName::GetHomeDir(), m_localFileName, wxT("dvtool")); #if defined(_WIN32)
const char* home = getenv("USERPROFILE");
#else
const char* home = getenv("HOME");
#endif
std::string filePath = std::string(home != nullptr ? home : "") + "/" + m_localFileName + ".dvtool";
if (wxFile::Exists(fileName.GetFullPath())) if (access(filePath.c_str(), F_OK) == 0)
::wxRemoveFile(fileName.GetFullPath()); ::remove(filePath.c_str());
} }
void CAnnouncementUnit::startAnnouncement() void CAnnouncementUnit::startAnnouncement()
{ {
wxFileName fileName1(wxFileName::GetHomeDir(), m_localFileName, wxT("dvtool")); #if defined(_WIN32)
wxFileName fileName2(wxFileName::GetHomeDir(), GLOBAL_FILE_NAME, wxT("dvtool")); const char* home = getenv("USERPROFILE");
#else
const char* home = getenv("HOME");
#endif
std::string filePath1 = std::string(home != nullptr ? home : "") + "/" + m_localFileName + ".dvtool";
std::string filePath2 = std::string(home != nullptr ? home : "") + "/" + GLOBAL_FILE_NAME + ".dvtool";
if (wxFile::Exists(fileName1.GetFullPath())) { if (access(filePath1.c_str(), F_OK) == 0) {
bool ret = m_reader.open(fileName1.GetFullPath()); bool ret = m_reader.open(filePath1);
if (!ret) if (!ret)
return; return;
} else if (wxFile::Exists(fileName2.GetFullPath())) { } else if (access(filePath2.c_str(), F_OK) == 0) {
bool ret = m_reader.open(fileName2.GetFullPath()); bool ret = m_reader.open(filePath2);
if (!ret) if (!ret)
return; return;
} else { } else {
@ -90,14 +117,12 @@ void CAnnouncementUnit::startAnnouncement()
DVTFR_TYPE type = m_reader.read(); DVTFR_TYPE type = m_reader.read();
if (type != DVTFR_HEADER) { if (type != DVTFR_HEADER) {
wxLogError(wxT("Invalid header element in the file - %d"), int(type));
m_reader.close(); m_reader.close();
return; return;
} }
CHeaderData* header = m_reader.readHeader(); CHeaderData* header = m_reader.readHeader();
if (header == NULL) { if (header == nullptr) {
wxLogError(wxT("NULL header element in the file"));
m_reader.close(); m_reader.close();
return; return;
} }
@ -107,7 +132,7 @@ void CAnnouncementUnit::startAnnouncement()
m_handler->transmitAnnouncementHeader(header); m_handler->transmitAnnouncementHeader(header);
m_time.Start(); m_time = std::chrono::steady_clock::now();
m_out = 0U; m_out = 0U;
m_sending = true; m_sending = true;
@ -118,12 +143,11 @@ void CAnnouncementUnit::clock()
if (!m_sending) if (!m_sending)
return; return;
unsigned int needed = m_time.Time() / DSTAR_FRAME_TIME_MS; unsigned int needed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_time).count() / DSTAR_FRAME_TIME_MS;
while (m_out < needed) { while (m_out < needed) {
DVTFR_TYPE type = m_reader.read(); DVTFR_TYPE type = m_reader.read();
if (type != DVTFR_DATA) { if (type != DVTFR_DATA) {
wxLogError(wxT("Invalid data element in the file - %d"), int(type));
m_handler->transmitAnnouncementData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true); m_handler->transmitAnnouncementData(END_PATTERN_BYTES, DV_FRAME_LENGTH_BYTES, true);
m_reader.close(); m_reader.close();
m_sending = false; m_sending = false;

@ -25,11 +25,29 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include "HeaderData.h" #include "HeaderData.h"
#include <wx/wx.h> #include "StdCompat.h"
#include <chrono>
/*
* Records and plays back user announcements stored as .dvtool files.
*
* Lifecycle:
* Record The repeater thread calls writeHeader() then writeData() as
* audio arrives over RF. The stream is saved to a per-callsign
* file ("Announce_<callsign>.dvtool") in the user's home directory.
*
* Playback startAnnouncement() opens the callsign-specific file if it
* exists, falling back to the global "Announce.dvtool". It reads
* the header and signals the repeater thread to key up. clock()
* then releases DV frames at air rate (same wall-clock pacing as
* CBeaconUnit) until the end-of-transmission marker is reached.
*
* Delete deleteAnnouncement() removes the callsign-specific file so the
* global announcement (or silence) takes effect on the next play.
*/
class CAnnouncementUnit { class CAnnouncementUnit {
public: public:
CAnnouncementUnit(IAnnouncementCallback* handler, const wxString& callsign); CAnnouncementUnit(IAnnouncementCallback* handler, const std::string& callsign);
~CAnnouncementUnit(); ~CAnnouncementUnit();
bool writeHeader(const CHeaderData& header); bool writeHeader(const CHeaderData& header);
@ -39,15 +57,16 @@ public:
void startAnnouncement(); void startAnnouncement();
// Called every repeater tick; releases frames to the transmitter at air rate.
void clock(); void clock();
private: private:
IAnnouncementCallback* m_handler; IAnnouncementCallback* m_handler;
wxString m_localFileName; std::string m_localFileName; // Callsign-specific filename (no extension).
CDVTOOLFileReader m_reader; CDVTOOLFileReader m_reader;
CDVTOOLFileWriter m_writer; CDVTOOLFileWriter m_writer;
wxStopWatch m_time; std::chrono::steady_clock::time_point m_time;
unsigned int m_out; unsigned int m_out; // Frames dispatched so far during playback.
bool m_sending; bool m_sending;
}; };

@ -30,7 +30,7 @@ const char OUT_PORT7 = 0x40U;
const char OUT_PORT8 = 0x80U; const char OUT_PORT8 = 0x80U;
CArduinoController::CArduinoController(const wxString& port) : CArduinoController::CArduinoController(const std::string& port) :
m_serial(port, SERIAL_19200), m_serial(port, SERIAL_19200),
m_out(0x00U), m_out(0x00U),
m_in(0x00U) m_in(0x00U)

@ -16,13 +16,12 @@
#include "SerialDataController.h" #include "SerialDataController.h"
#include "HardwareController.h" #include "HardwareController.h"
#include "StdCompat.h"
#include <wx/wx.h>
class CArduinoController : public IHardwareController { class CArduinoController : public IHardwareController {
public: public:
CArduinoController(const wxString& port); CArduinoController(const std::string& port);
virtual ~CArduinoController(); virtual ~CArduinoController();
virtual bool open(); virtual bool open();

@ -14,12 +14,13 @@
#ifndef AudioCallback_H #ifndef AudioCallback_H
#define AudioCallback_H #define AudioCallback_H
#include <wx/wx.h> // Callback interface implemented by the sound card modem.
// readCallback() delivers captured RX audio samples; writeCallback() requests
// TX audio samples to be filled into the output buffer.
class IAudioCallback { class IAudioCallback {
public: public:
virtual void readCallback(const wxFloat32* input, unsigned int nSamples, int id) = 0; virtual void readCallback(const float* input, unsigned int nSamples, int id) = 0;
virtual void writeCallback(wxFloat32* output, int& nSamples, int id) = 0; virtual void writeCallback(float* output, int& nSamples, int id) = 0;
private: private:
}; };

@ -14,6 +14,8 @@
#ifndef BeaconCallback_H #ifndef BeaconCallback_H
#define BeaconCallback_H #define BeaconCallback_H
// Callback interface implemented by the repeater thread.
// CBeaconUnit calls these to key up the transmitter and push beacon DV frames.
class IBeaconCallback { class IBeaconCallback {
public: public:
virtual void transmitBeaconHeader() = 0; virtual void transmitBeaconHeader() = 0;

@ -18,19 +18,31 @@
#include "BeaconUnit.h" #include "BeaconUnit.h"
#include <wx/filename.h> #include <fstream>
#include <wx/textfile.h> #include <sstream>
#include <wx/tokenzr.h> #include <cstdio>
#include <wx/ffile.h> #include <cassert>
#include <cstring>
#if defined(_WIN32)
#include <io.h>
#define access _access
#define F_OK 0
#define R_OK 4
#else
#include <unistd.h>
#endif
// Maximum beacon duration: 60 seconds at the D-Star frame rate.
const unsigned int MAX_FRAMES = 60U * DSTAR_FRAMES_PER_SEC; const unsigned int MAX_FRAMES = 60U * DSTAR_FRAMES_PER_SEC;
// Number of silent AMBE frames prepended to the AMBE buffer and mapped to " ".
// These create a short pause before and after each word/character.
const unsigned int SILENCE_LENGTH = 10U; const unsigned int SILENCE_LENGTH = 10U;
CBeaconUnit::CBeaconUnit(IBeaconCallback* handler, const wxString& callsign, const wxString& text, bool voice, TEXT_LANG language) : CBeaconUnit::CBeaconUnit(IBeaconCallback* handler, const std::string& callsign, const std::string& text, bool voice, TEXT_LANG language) :
m_ambe(NULL), m_ambe(nullptr),
m_ambeLength(0U), m_ambeLength(0U),
m_data(NULL), m_data(nullptr),
m_dataLength(0U), m_dataLength(0U),
m_index(), m_index(),
m_language(language), m_language(language),
@ -43,69 +55,69 @@ m_seqNo(0U),
m_time(), m_time(),
m_sending(false) m_sending(false)
{ {
wxASSERT(handler != NULL); assert(handler != nullptr);
wxString slowData = text; std::string slowData = text;
slowData.resize(20U, wxT(' ')); slowData.resize(20U, ' ');
m_encoder.setTextData(slowData); m_encoder.setTextData(slowData);
m_data = new unsigned char[MAX_FRAMES * VOICE_FRAME_LENGTH_BYTES]; m_data = new unsigned char[MAX_FRAMES * DV_FRAME_LENGTH_BYTES];
::memset(m_data, 0x00U, MAX_FRAMES * VOICE_FRAME_LENGTH_BYTES); ::memset(m_data, 0x00U, MAX_FRAMES * DV_FRAME_LENGTH_BYTES);
if (!voice) if (!voice)
return; return;
wxString ambeFileName; std::string ambeFileName;
wxString indxFileName; std::string indxFileName;
switch (m_language) { switch (m_language) {
case TL_DEUTSCH: case TL_DEUTSCH:
ambeFileName = wxT("de_DE.ambe"); ambeFileName = "de_DE.ambe";
indxFileName = wxT("de_DE.indx"); indxFileName = "de_DE.indx";
break; break;
case TL_DANSK: case TL_DANSK:
ambeFileName = wxT("dk_DK.ambe"); ambeFileName = "dk_DK.ambe";
indxFileName = wxT("dk_DK.indx"); indxFileName = "dk_DK.indx";
break; break;
case TL_ITALIANO: case TL_ITALIANO:
ambeFileName = wxT("it_IT.ambe"); ambeFileName = "it_IT.ambe";
indxFileName = wxT("it_IT.indx"); indxFileName = "it_IT.indx";
break; break;
case TL_FRANCAIS: case TL_FRANCAIS:
ambeFileName = wxT("fr_FR.ambe"); ambeFileName = "fr_FR.ambe";
indxFileName = wxT("fr_FR.indx"); indxFileName = "fr_FR.indx";
break; break;
case TL_ESPANOL: case TL_ESPANOL:
ambeFileName = wxT("es_ES.ambe"); ambeFileName = "es_ES.ambe";
indxFileName = wxT("es_ES.indx"); indxFileName = "es_ES.indx";
break; break;
case TL_SVENSKA: case TL_SVENSKA:
ambeFileName = wxT("se_SE.ambe"); ambeFileName = "se_SE.ambe";
indxFileName = wxT("se_SE.indx"); indxFileName = "se_SE.indx";
break; break;
case TL_POLSKI: case TL_POLSKI:
ambeFileName = wxT("pl_PL.ambe"); ambeFileName = "pl_PL.ambe";
indxFileName = wxT("pl_PL.indx"); indxFileName = "pl_PL.indx";
break; break;
case TL_ENGLISH_US: case TL_ENGLISH_US:
ambeFileName = wxT("en_US.ambe"); ambeFileName = "en_US.ambe";
indxFileName = wxT("en_US.indx"); indxFileName = "en_US.indx";
break; break;
case TL_NORSK: case TL_NORSK:
ambeFileName = wxT("no_NO.ambe"); ambeFileName = "no_NO.ambe";
indxFileName = wxT("no_NO.indx"); indxFileName = "no_NO.indx";
break; break;
// case TL_NEDERLANDS_NL: // case TL_NEDERLANDS_NL:
// ambeFileName = wxT("nl_NL.ambe"); // ambeFileName = "nl_NL.ambe";
// indxFileName = wxT("nl_NL.indx"); // indxFileName = "nl_NL.indx";
// break; // break;
// case TL_NEDERLANDS_BE: // case TL_NEDERLANDS_BE:
// ambeFileName = wxT("nl_BE.ambe"); // ambeFileName = "nl_BE.ambe";
// indxFileName = wxT("nl_BE.indx"); // indxFileName = "nl_BE.indx";
// break; // break;
default: default:
ambeFileName = wxT("en_GB.ambe"); ambeFileName = "en_GB.ambe";
indxFileName = wxT("en_GB.indx"); indxFileName = "en_GB.indx";
break; break;
} }
@ -131,14 +143,14 @@ void CBeaconUnit::sendBeacon()
m_sending = true; m_sending = true;
m_time.Start(); m_time = std::chrono::steady_clock::now();
m_in = 0U; m_in = 0U;
m_out = 0U; m_out = 0U;
m_seqNo = 0U; m_seqNo = 0U;
m_dataLength = 0U; m_dataLength = 0U;
if (m_ambe == NULL) { if (m_ambe == nullptr) {
for (unsigned int i = 0U; i < 21U; i++) { for (unsigned int i = 0U; i < 21U; i++) {
unsigned char buffer[DV_FRAME_LENGTH_BYTES]; unsigned char buffer[DV_FRAME_LENGTH_BYTES];
@ -160,17 +172,17 @@ void CBeaconUnit::sendBeacon()
m_in++; m_in++;
} }
} else { } else {
lookup(wxT(" ")); lookup(" ");
lookup(wxT(" ")); lookup(" ");
lookup(wxT(" ")); lookup(" ");
lookup(wxT(" ")); lookup(" ");
spellCallsign(m_callsign); spellCallsign(m_callsign);
lookup(wxT(" ")); lookup(" ");
lookup(wxT(" ")); lookup(" ");
lookup(wxT(" ")); lookup(" ");
lookup(wxT(" ")); lookup(" ");
} }
} }
@ -179,7 +191,10 @@ void CBeaconUnit::clock()
if (!m_sending) if (!m_sending)
return; return;
unsigned int needed = m_time.Time() / DSTAR_FRAME_TIME_MS; // Calculate how many frames should have been sent by now based on elapsed
// wall-clock time, then dispatch any that haven't been sent yet. This
// paces output at exactly the D-Star air rate without sleeping.
unsigned int needed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_time).count() / DSTAR_FRAME_TIME_MS;
while (m_out < needed) { while (m_out < needed) {
m_handler->transmitBeaconData(m_data + m_out * DV_FRAME_LENGTH_BYTES, DV_FRAME_LENGTH_BYTES, false); m_handler->transmitBeaconData(m_data + m_out * DV_FRAME_LENGTH_BYTES, DV_FRAME_LENGTH_BYTES, false);
@ -195,14 +210,16 @@ void CBeaconUnit::clock()
} }
} }
bool CBeaconUnit::lookup(const wxString &id) bool CBeaconUnit::lookup(const std::string& id)
{ {
CIndexRecord* info = m_index[id]; CIndexList_t::iterator it = m_index.find(id);
if (info == NULL) { if (it == m_index.end() || it->second == nullptr) {
// wxLogError(wxT("Cannot find the AMBE index for *%s*"), id.c_str()); // Cannot find the AMBE index for this id
return false; return false;
} }
CIndexRecord* info = it->second;
unsigned int start = info->getStart(); unsigned int start = info->getStart();
unsigned int length = info->getLength(); unsigned int length = info->getLength();
@ -232,82 +249,81 @@ bool CBeaconUnit::lookup(const wxString &id)
return true; return true;
} }
void CBeaconUnit::spellCallsign(const wxString &callsign) void CBeaconUnit::spellCallsign(const std::string& callsign)
{ {
unsigned int length = callsign.Len(); unsigned int length = callsign.length();
for (unsigned int i = 0U; i < (length - 1U); i++) { for (unsigned int i = 0U; i < (length - 1U); i++) {
wxString c = callsign.Mid(i, 1U); std::string c = callsign.substr(i, 1U);
if (!c.IsSameAs(wxT(" "))) if (c != " ")
lookup(c); lookup(c);
} }
wxChar c = callsign.GetChar(length - 1U); char c = callsign[length - 1U];
switch (c) { switch (c) {
case wxT('A'): case 'A':
lookup(wxT("alpha")); lookup("alpha");
break; break;
case wxT('B'): case 'B':
lookup(wxT("bravo")); lookup("bravo");
break; break;
case wxT('C'): case 'C':
lookup(wxT("charlie")); lookup("charlie");
break; break;
case wxT('D'): case 'D':
lookup(wxT("delta")); lookup("delta");
break; break;
default: default:
lookup(wxString(c)); lookup(std::string(1, c));
break; break;
} }
} }
bool CBeaconUnit::readAMBE(const wxString& name) bool CBeaconUnit::readAMBE(const std::string& name)
{ {
wxFileName fileName(wxFileName::GetHomeDir(), name); #if defined(_WIN32)
const char* home = getenv("USERPROFILE");
if (!fileName.IsFileReadable()) {
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str());
#if defined(__WINDOWS__)
fileName.Assign(::wxGetCwd(), name);
#else #else
fileName.Assign(wxT(DATA_DIR), name); const char* home = getenv("HOME");
#endif #endif
if (!fileName.IsFileReadable()) { std::string homePath = std::string(home != nullptr ? home : "") + "/" + name;
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str()); std::string filePath;
if (access(homePath.c_str(), R_OK) == 0) {
filePath = homePath;
} else {
std::string dataPath = std::string(DATA_DIR) + "/" + name;
if (access(dataPath.c_str(), R_OK) == 0) {
filePath = dataPath;
} else {
return false; return false;
} }
} }
wxFFile file; FILE* file = ::fopen(filePath.c_str(), "rb");
if (file == nullptr)
bool ret = file.Open(fileName.GetFullPath().c_str(), wxT("rb"));
if (!ret) {
wxLogMessage(wxT("Cannot open %s for reading"), fileName.GetFullPath().c_str());
return false; return false;
}
wxLogMessage(wxT("Reading %s"), fileName.GetFullPath().c_str());
unsigned char buffer[VOICE_FRAME_LENGTH_BYTES]; unsigned char buffer[VOICE_FRAME_LENGTH_BYTES];
size_t n = file.Read(buffer, 4U); size_t n = ::fread(buffer, 1U, 4U, file);
if (n != 4U) { if (n != 4U) {
wxLogMessage(wxT("Unable to read the header from %s"), fileName.GetFullPath().c_str()); ::fclose(file);
file.Close();
return false; return false;
} }
if (::memcmp(buffer, "AMBE", 4U) != 0) { if (::memcmp(buffer, "AMBE", 4U) != 0) {
wxLogMessage(wxT("Invalid header from %s"), fileName.GetFullPath().c_str()); ::fclose(file);
file.Close();
return false; return false;
} }
// Length of the file minus the header // Determine file length minus the 4-byte header
unsigned int length = file.Length() - 4U; ::fseek(file, 0L, SEEK_END);
long fileSize = ::ftell(file);
::fseek(file, 4L, SEEK_SET);
unsigned int length = (unsigned int)(fileSize - 4L);
// Hold the file data plus silence at the end // Hold the file data plus silence at the end
m_ambe = new unsigned char[length + SILENCE_LENGTH * VOICE_FRAME_LENGTH_BYTES]; m_ambe = new unsigned char[length + SILENCE_LENGTH * VOICE_FRAME_LENGTH_BYTES];
@ -318,77 +334,68 @@ bool CBeaconUnit::readAMBE(const wxString& name)
for (unsigned int i = 0U; i < SILENCE_LENGTH; i++, p += VOICE_FRAME_LENGTH_BYTES) for (unsigned int i = 0U; i < SILENCE_LENGTH; i++, p += VOICE_FRAME_LENGTH_BYTES)
::memcpy(p, NULL_AMBE_DATA_BYTES, VOICE_FRAME_LENGTH_BYTES); ::memcpy(p, NULL_AMBE_DATA_BYTES, VOICE_FRAME_LENGTH_BYTES);
n = file.Read(p, length); n = ::fread(p, 1U, length, file);
if (n != length) { if (n != length) {
wxLogMessage(wxT("Unable to read the AMBE data from %s"), fileName.GetFullPath().c_str()); ::fclose(file);
file.Close();
delete[] m_ambe; delete[] m_ambe;
m_ambe = NULL; m_ambe = nullptr;
return false; return false;
} }
file.Close(); ::fclose(file);
return true; return true;
} }
bool CBeaconUnit::readIndex(const wxString& name) bool CBeaconUnit::readIndex(const std::string& name)
{ {
wxFileName fileName(wxFileName::GetHomeDir(), name); #if defined(_WIN32)
const char* home = getenv("USERPROFILE");
if (!fileName.IsFileReadable()) {
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str());
#if defined(__WINDOWS__)
fileName.Assign(::wxGetCwd(), name);
#else #else
fileName.Assign(wxT(DATA_DIR), name); const char* home = getenv("HOME");
#endif #endif
if (!fileName.IsFileReadable()) { std::string homePath = std::string(home != nullptr ? home : "") + "/" + name;
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str()); std::string filePath;
if (access(homePath.c_str(), R_OK) == 0) {
filePath = homePath;
} else {
std::string dataPath = std::string(DATA_DIR) + "/" + name;
if (access(dataPath.c_str(), R_OK) == 0) {
filePath = dataPath;
} else {
return false; return false;
} }
} }
wxTextFile file; std::ifstream file(filePath);
if (!file.is_open())
bool ret = file.Open(fileName.GetFullPath());
if (!ret) {
wxLogMessage(wxT("Cannot open %s for reading"), fileName.GetFullPath().c_str());
return false; return false;
}
// Add a silence entry at the beginning // Add a silence entry at the beginning
m_index[wxT(" ")] = new CIndexRecord(wxT(" "), 0U, SILENCE_LENGTH); m_index[" "] = new CIndexRecord(" ", 0U, SILENCE_LENGTH);
wxLogMessage(wxT("Reading %s"), fileName.GetFullPath().c_str());
unsigned int nLines = file.GetLineCount(); std::string line;
while (std::getline(file, line)) {
if (!line.empty() && line[0] != '#') {
std::istringstream iss(line);
std::string entryName, startTxt, lengthTxt;
iss >> entryName >> startTxt >> lengthTxt;
for (unsigned int i = 0; i < nLines; i++) { if (!entryName.empty() && !startTxt.empty() && !lengthTxt.empty()) {
wxString line = file.GetLine(i); unsigned long start = std::stoul(startTxt);
unsigned long length = std::stoul(lengthTxt);
if (line.length() > 0 && line.GetChar(0) != wxT('#')) { if (start >= m_ambeLength || (start + length) >= m_ambeLength) {
wxStringTokenizer t(line, wxT(" \t\r\n"), wxTOKEN_STRTOK); // Out of range entry — skip silently
wxString name = t.GetNextToken(); } else {
wxString startTxt = t.GetNextToken(); m_index[entryName] = new CIndexRecord(entryName, start + SILENCE_LENGTH, length);
wxString lengthTxt = t.GetNextToken(); }
if (!name.IsEmpty() && !startTxt.IsEmpty() && !lengthTxt.IsEmpty()) {
unsigned long start;
startTxt.ToULong(&start);
unsigned long length;
lengthTxt.ToULong(&length);
if (start >= m_ambeLength || (start + length) >= m_ambeLength)
wxLogError(wxT("The start or end for *%s* is out of range, start: %lu, end: %lu"), name.c_str(), start, start + length);
else
m_index[name] = new CIndexRecord(name, start + SILENCE_LENGTH, length);
} }
} }
} }
file.Close(); file.close();
return true; return true;
} }

@ -23,18 +23,25 @@
#include "BeaconCallback.h" #include "BeaconCallback.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include <wx/wx.h> #include "StdCompat.h"
#include <chrono>
#include <unordered_map>
/*
* Maps a word or single character (e.g. "alpha", "B", " ") to a slice of the
* pre-recorded AMBE audio buffer. The index file (.indx) contains one entry
* per line: <name> <start_frame> <frame_count>.
*/
class CIndexRecord { class CIndexRecord {
public: public:
CIndexRecord(const wxString& name, unsigned int start, unsigned int length) : CIndexRecord(const std::string& name, unsigned int start, unsigned int length) :
m_name(name), m_name(name),
m_start(start), m_start(start),
m_length(length) m_length(length)
{ {
} }
wxString getName() const std::string getName() const
{ {
return m_name; return m_name;
} }
@ -50,43 +57,65 @@ public:
} }
private: private:
wxString m_name; std::string m_name;
unsigned int m_start; unsigned int m_start; // Frame offset into the AMBE buffer.
unsigned int m_length; unsigned int m_length; // Number of AMBE frames for this word/character.
}; };
WX_DECLARE_STRING_HASH_MAP(CIndexRecord*, CIndexList_t); typedef std::unordered_map<std::string, CIndexRecord*> CIndexList_t;
/*
* Generates a voice ID beacon by assembling pre-recorded AMBE audio frames.
*
* On construction the .ambe file (raw VOICE_FRAME_LENGTH_BYTES chunks) and
* its companion .indx file are loaded for the configured language. When
* sendBeacon() is called, spellCallsign() looks up each character of the
* callsign in the index and copies the corresponding AMBE frames into an
* internal playback buffer. The last character is spoken phonetically
* (e.g. "A" -> "alpha") to follow amateur radio convention.
*
* clock() is called every DSTAR_FRAME_TIME_MS (20 ms) by the repeater thread.
* It compares elapsed wall-clock time against m_out (frames dispatched) to
* pace frame delivery to the transmitter without requiring a dedicated thread.
*
* If no voice files are available, a silent beacon (null AMBE frames) with
* slow-data text is still transmitted.
*/
class CBeaconUnit { class CBeaconUnit {
public: public:
CBeaconUnit(IBeaconCallback* handler, const wxString& callsign, const wxString& text, bool voice, TEXT_LANG language); CBeaconUnit(IBeaconCallback* handler, const std::string& callsign, const std::string& text, bool voice, TEXT_LANG language);
~CBeaconUnit(); ~CBeaconUnit();
// Assembles the beacon frames and signals the repeater thread to begin TX.
void sendBeacon(); void sendBeacon();
// Called every repeater tick; releases frames to the transmitter at air rate.
void clock(); void clock();
private: private:
unsigned char* m_ambe; unsigned char* m_ambe; // Raw AMBE frames loaded from the .ambe file.
unsigned int m_ambeLength; unsigned int m_ambeLength; // Total frames available in m_ambe.
unsigned char* m_data; unsigned char* m_data; // Assembled DV frames ready for transmission.
unsigned int m_dataLength; unsigned int m_dataLength; // Bytes written into m_data so far.
CIndexList_t m_index; CIndexList_t m_index; // Word/character -> AMBE frame mapping.
TEXT_LANG m_language; TEXT_LANG m_language;
IBeaconCallback* m_handler; IBeaconCallback* m_handler;
wxString m_callsign; std::string m_callsign;
CSlowDataEncoder m_encoder; CSlowDataEncoder m_encoder; // Encodes the beacon text into slow-data fields.
unsigned int m_in; unsigned int m_in; // Total frames queued into m_data.
unsigned int m_out; unsigned int m_out; // Frames dispatched to the transmitter so far.
unsigned int m_seqNo; unsigned int m_seqNo; // Slow-data sequence counter (020).
wxStopWatch m_time; std::chrono::steady_clock::time_point m_time; // Wall-clock start time of the beacon.
bool m_sending; bool m_sending;
bool lookup(const wxString& id); // Appends AMBE frames for a word/character from the index into m_data.
void spellCallsign(const wxString& callsign); bool lookup(const std::string& id);
// Spells out the callsign character by character; the last character uses
// the phonetic alphabet (A→alpha, B→bravo, C→charlie, D→delta).
void spellCallsign(const std::string& callsign);
bool readAMBE(const wxString& name); bool readAMBE(const std::string& name);
bool readIndex(const wxString& name); bool readIndex(const std::string& name);
}; };
#endif #endif

@ -15,6 +15,9 @@
#include "Utils.h" #include "Utils.h"
#include <cassert>
#include <cstdint>
static const unsigned short ccittTab[] = { static const unsigned short ccittTab[] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
@ -60,15 +63,15 @@ CCCITTChecksum::~CCCITTChecksum()
void CCCITTChecksum::update(const unsigned char* data, unsigned int length) void CCCITTChecksum::update(const unsigned char* data, unsigned int length)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
for (unsigned int i = 0U; i < length; i++) for (unsigned int i = 0U; i < length; i++)
m_crc16 = (wxUint16(m_crc8[0U]) << 8) ^ ccittTab[m_crc8[1U] ^ data[i]]; m_crc16 = (uint16_t(m_crc8[0U]) << 8) ^ ccittTab[m_crc8[1U] ^ data[i]];
} }
void CCCITTChecksum::result(unsigned char* data) void CCCITTChecksum::result(unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
data[0U] = m_crc8[1U]; data[0U] = m_crc8[1U];
data[1U] = m_crc8[0U]; data[1U] = m_crc8[0U];
@ -76,7 +79,7 @@ void CCCITTChecksum::result(unsigned char* data)
bool CCCITTChecksum::check(const unsigned char* data) bool CCCITTChecksum::check(const unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
unsigned char sum[2U]; unsigned char sum[2U];
result(sum); result(sum);

@ -14,8 +14,18 @@
#ifndef CCITTChecksum_H #ifndef CCITTChecksum_H
#define CCITTChecksum_H #define CCITTChecksum_H
#include <wx/wx.h> #include <cstdint>
/*
* CCITT-16 (CRC-16/CCITT) checksum in standard bit order.
*
* update() feeds bytes into the running CRC. result() writes the two-byte
* checksum into the provided buffer. check() compares a stored two-byte
* checksum against the computed value. reset() resets to the initial state.
*
* See also CCCITTChecksumReverse, which uses a bit-reversed variant required
* by the D-Star radio header and DSRP wire formats.
*/
class CCCITTChecksum { class CCCITTChecksum {
public: public:
CCCITTChecksum(); CCCITTChecksum();
@ -31,8 +41,8 @@ public:
private: private:
union { union {
wxUint16 m_crc16; uint16_t m_crc16;
wxUint8 m_crc8[2U]; uint8_t m_crc8[2U];
}; };
}; };

@ -15,6 +15,9 @@
#include "Utils.h" #include "Utils.h"
#include <cassert>
#include <cstdint>
static const unsigned short ccittTab[] = { static const unsigned short ccittTab[] = {
0x0000,0x1189,0x2312,0x329b,0x4624,0x57ad,0x6536,0x74bf, 0x0000,0x1189,0x2312,0x329b,0x4624,0x57ad,0x6536,0x74bf,
0x8c48,0x9dc1,0xaf5a,0xbed3,0xca6c,0xdbe5,0xe97e,0xf8f7, 0x8c48,0x9dc1,0xaf5a,0xbed3,0xca6c,0xdbe5,0xe97e,0xf8f7,
@ -60,15 +63,15 @@ CCCITTChecksumReverse::~CCCITTChecksumReverse()
void CCCITTChecksumReverse::update(const unsigned char* data, unsigned int length) void CCCITTChecksumReverse::update(const unsigned char* data, unsigned int length)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
for (unsigned int i = 0U; i < length; i++) for (unsigned int i = 0U; i < length; i++)
m_crc16 = wxUint16(m_crc8[1U]) ^ ccittTab[m_crc8[0U] ^ data[i]]; m_crc16 = uint16_t(m_crc8[1U]) ^ ccittTab[m_crc8[0U] ^ data[i]];
} }
void CCCITTChecksumReverse::result(unsigned char* data) void CCCITTChecksumReverse::result(unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
m_crc16 = ~m_crc16; m_crc16 = ~m_crc16;
@ -78,7 +81,7 @@ void CCCITTChecksumReverse::result(unsigned char* data)
bool CCCITTChecksumReverse::check(const unsigned char* data) bool CCCITTChecksumReverse::check(const unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
unsigned char sum[2U]; unsigned char sum[2U];
result(sum); result(sum);

@ -14,8 +14,17 @@
#ifndef CCITTChecksumReverse_H #ifndef CCITTChecksumReverse_H
#define CCITTChecksumReverse_H #define CCITTChecksumReverse_H
#include <wx/wx.h> #include <cstdint>
/*
* CCITT-16 checksum with reversed bit order, as required by the D-Star
* radio header format and the DSRP network protocol.
*
* The API is identical to CCCITTChecksum; only the polynomial bit ordering
* differs. This variant is used whenever a checksum is appended to or
* verified in a D-Star header (CHeaderData, DSRP header packets, .dvtool
* file headers, slow-data header blocks).
*/
class CCCITTChecksumReverse { class CCCITTChecksumReverse {
public: public:
CCCITTChecksumReverse(); CCCITTChecksumReverse();
@ -31,8 +40,8 @@ public:
private: private:
union { union {
wxUint16 m_crc16; uint16_t m_crc16;
wxUint8 m_crc8[2U]; uint8_t m_crc8[2U];
}; };
}; };

@ -19,9 +19,10 @@
#include "CallsignList.h" #include "CallsignList.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include <wx/textfile.h> #include <fstream>
#include <algorithm>
CCallsignList::CCallsignList(const wxString& filename) : CCallsignList::CCallsignList(const std::string& filename) :
m_filename(filename), m_filename(filename),
m_callsigns() m_callsigns()
{ {
@ -34,43 +35,41 @@ CCallsignList::~CCallsignList()
bool CCallsignList::load() bool CCallsignList::load()
{ {
wxTextFile file; std::ifstream file(m_filename);
if (!file.is_open())
bool res = file.Open(m_filename);
if (!res)
return false; return false;
unsigned int lines = file.GetLineCount(); std::string callsign;
if (lines == 0U) { while (std::getline(file, callsign)) {
file.Close(); // Strip trailing CR so CRLF files (Windows line endings) are handled correctly
return true; if (!callsign.empty() && callsign.back() == '\r')
} callsign.pop_back();
m_callsigns.Alloc(lines);
wxString callsign = file.GetFirstLine(); // Convert to uppercase
std::transform(callsign.begin(), callsign.end(), callsign.begin(), ::toupper);
while (!file.Eof()) { // Pad to LONG_CALLSIGN_LENGTH with spaces, then truncate
callsign.MakeUpper(); callsign.append(8U, ' ');
callsign.Append(wxT(" ")); callsign.resize(LONG_CALLSIGN_LENGTH);
callsign.Truncate(LONG_CALLSIGN_LENGTH);
m_callsigns.Add(callsign); m_callsigns.push_back(callsign);
callsign = file.GetNextLine();
} }
file.Close(); file.close();
return true; return true;
} }
unsigned int CCallsignList::getCount() const unsigned int CCallsignList::getCount() const
{ {
return m_callsigns.GetCount(); return m_callsigns.size();
} }
bool CCallsignList::isInList(const wxString& callsign) const bool CCallsignList::isInList(const std::string& callsign) const
{ {
return m_callsigns.Index(callsign) != wxNOT_FOUND; for (const auto& cs : m_callsigns) {
if (cs == callsign)
return true;
}
return false;
} }

@ -19,22 +19,32 @@
#ifndef CallsignList_H #ifndef CallsignList_H
#define CallsignList_H #define CallsignList_H
#include <wx/wx.h> #include "StdCompat.h"
/*
* Loads a callsign access list (whitelist, blacklist, or greylist) from a
* plain-text file, one callsign per line.
*
* Each entry is normalised on load: converted to uppercase and padded/truncated
* to exactly LONG_CALLSIGN_LENGTH (8) characters with trailing spaces. This
* matches the fixed-width format used in D-Star radio headers, so isInList()
* can do a simple string comparison without any normalisation at call time.
*/
class CCallsignList { class CCallsignList {
public: public:
CCallsignList(const wxString& filename); CCallsignList(const std::string& filename);
~CCallsignList(); ~CCallsignList();
bool load(); bool load();
unsigned int getCount() const; unsigned int getCount() const;
bool isInList(const wxString& callsign) const; // Returns true if callsign (already padded to 8 chars) is in the list.
bool isInList(const std::string& callsign) const;
private: private:
wxString m_filename; std::string m_filename;
wxArrayString m_callsigns; std::vector<std::string> m_callsigns;
}; };
#endif #endif

@ -1,266 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{3753EF20-2330-415E-B933-2560A474498B}</ProjectGuid>
<RootNamespace>Common</RootNamespace>
<Keyword>Win32Proj</Keyword>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_ProjectFileVersion>14.0.24720.0</_ProjectFileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
<IntDir>$(Configuration)\</IntDir>
<IncludePath>$(WXWIN)\include;$(IncludePath);$(WXWIN)\lib\vc_dll\mswud;$(SolutionDir)..\libusb-win32-bin-1.2.6.0\include</IncludePath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<IncludePath>$(WXWIN)\include;$(IncludePath);$(WXWIN)\lib\vc_x64_dll\mswud;$(SolutionDir)..\libusb-win32-bin-1.2.6.0\include</IncludePath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
<IntDir>$(Configuration)\</IntDir>
<IncludePath>$(WXWIN)\include;$(IncludePath);$(WXWIN)\lib\vc_dll\mswu;$(SolutionDir)..\libusb-win32-bin-1.2.6.0\include</IncludePath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<IncludePath>$(WXWIN)\include;$(IncludePath);$(WXWIN)\lib\vc_x64_dll\mswu;$(SolutionDir)..\libusb-win32-bin-1.2.6.0\include</IncludePath>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<PreBuildEvent>
<Command />
</PreBuildEvent>
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;WINVER=0x0400;__WXMSW__;WXUSINGDLL;wxUSE_GUI=1;__WXDEBUG__;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<IgnoreStandardIncludePath>false</IgnoreStandardIncludePath>
<MinimalRebuild>true</MinimalRebuild>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader />
<WarningLevel>Level4</WarningLevel>
<DebugInformationFormat>EditAndContinue</DebugInformationFormat>
<AdditionalIncludeDirectories>$(SolutionDir)..\WinUSB\include;$(SolutionDir)..\portaudio\include;$(WXWIN)\include\msvc;$(WXWIN)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;WINVER=0x0400;__WXMSW__;WXUSINGDLL;wxUSE_GUI=1;__WXDEBUG__;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<IgnoreStandardIncludePath>false</IgnoreStandardIncludePath>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level4</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<AdditionalIncludeDirectories>$(SolutionDir)..\WinUSB\include;$(SolutionDir)..\portaudio\include;$(WXWIN)\include\msvc;$(WXWIN)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<PreBuildEvent>
<Command />
</PreBuildEvent>
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>$(SolutionDir)..\WinUSB\include;$(SolutionDir)..\HID;$(SolutionDir)..\portaudio\include;$(WXWIN)\include\msvc;$(WXWIN)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;_WINDOWS;WINVER=0x0400;__WXMSW__;WXUSINGDLL;wxUSE_GUI=1;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<PreBuildEvent>
<Command>
</Command>
</PreBuildEvent>
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>$(SolutionDir)..\WinUSB\include;$(SolutionDir)..\HID;$(SolutionDir)..\portaudio\include;$(WXWIN)\include\msvc;$(WXWIN)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;_WINDOWS;WINVER=0x0400;__WXMSW__;WXUSINGDLL;wxUSE_GUI=1;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PrecompiledHeader>
</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AMBEFEC.cpp" />
<ClCompile Include="AnnouncementUnit.cpp" />
<ClCompile Include="ArduinoController.cpp" />
<ClCompile Include="BeaconUnit.cpp" />
<ClCompile Include="CallsignList.cpp" />
<ClCompile Include="CCITTChecksum.cpp" />
<ClCompile Include="CCITTChecksumReverse.cpp" />
<ClCompile Include="DStarGMSKDemodulator.cpp" />
<ClCompile Include="DStarGMSKModulator.cpp" />
<ClCompile Include="DStarRepeaterConfig.cpp" />
<ClCompile Include="DummyController.cpp" />
<ClCompile Include="DVAPController.cpp" />
<ClCompile Include="DVMegaController.cpp" />
<ClCompile Include="DVRPTRV1Controller.cpp" />
<ClCompile Include="DVRPTRV2Controller.cpp" />
<ClCompile Include="DVRPTRV3Controller.cpp" />
<ClCompile Include="DVTOOLFileReader.cpp" />
<ClCompile Include="DVTOOLFileWriter.cpp" />
<ClCompile Include="ExternalController.cpp" />
<ClCompile Include="FIRFilter.cpp" />
<ClCompile Include="GatewayProtocolHandler.cpp" />
<ClCompile Include="GMSKController.cpp" />
<ClCompile Include="GMSKModem.cpp" />
<ClCompile Include="GMSKModemLibUsb.cpp" />
<ClCompile Include="GMSKModemWinUSB.cpp" />
<ClCompile Include="Golay.cpp" />
<ClCompile Include="HardwareController.cpp" />
<ClCompile Include="HeaderData.cpp" />
<ClCompile Include="IcomController.cpp" />
<ClCompile Include="K8055Controller.cpp" />
<ClCompile Include="LogEvent.cpp" />
<ClCompile Include="Logger.cpp" />
<ClCompile Include="MMDVMController.cpp" />
<ClCompile Include="Modem.cpp" />
<ClCompile Include="OutputQueue.cpp" />
<ClCompile Include="RepeaterProtocolHandler.cpp" />
<ClCompile Include="SerialDataController.cpp" />
<ClCompile Include="SerialLineController.cpp" />
<ClCompile Include="SerialPortSelector.cpp" />
<ClCompile Include="SlowDataDecoder.cpp" />
<ClCompile Include="SlowDataEncoder.cpp" />
<ClCompile Include="SoundCardController.cpp" />
<ClCompile Include="SoundCardReaderWriter.cpp" />
<ClCompile Include="SplitController.cpp" />
<ClCompile Include="TCPReaderWriter.cpp" />
<ClCompile Include="Timer.cpp" />
<ClCompile Include="UDPReaderWriter.cpp" />
<ClCompile Include="URIUSBController.cpp" />
<ClCompile Include="Utils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="AMBEFEC.h" />
<ClInclude Include="AnnouncementCallback.h" />
<ClInclude Include="AnnouncementUnit.h" />
<ClInclude Include="ArduinoController.h" />
<ClInclude Include="AudioCallback.h" />
<ClInclude Include="BeaconCallback.h" />
<ClInclude Include="BeaconUnit.h" />
<ClInclude Include="CallsignList.h" />
<ClInclude Include="CCITTChecksum.h" />
<ClInclude Include="CCITTChecksumReverse.h" />
<ClInclude Include="DStarDefines.h" />
<ClInclude Include="DStarGMSKDemodulator.h" />
<ClInclude Include="DStarGMSKModulator.h" />
<ClInclude Include="DStarRepeaterConfig.h" />
<ClInclude Include="DummyController.h" />
<ClInclude Include="DVAPController.h" />
<ClInclude Include="DVMegaController.h" />
<ClInclude Include="DVRPTRV1Controller.h" />
<ClInclude Include="DVRPTRV2Controller.h" />
<ClInclude Include="DVRPTRV3Controller.h" />
<ClInclude Include="DVTOOLFileReader.h" />
<ClInclude Include="DVTOOLFileWriter.h" />
<ClInclude Include="ExternalController.h" />
<ClInclude Include="FIRFilter.h" />
<ClInclude Include="GatewayProtocolHandler.h" />
<ClInclude Include="GMSKController.h" />
<ClInclude Include="GMSKModem.h" />
<ClInclude Include="GMSKModemLibUsb.h" />
<ClInclude Include="GMSKModemWinUSB.h" />
<ClInclude Include="Golay.h" />
<ClInclude Include="HardwareController.h" />
<ClInclude Include="HeaderData.h" />
<ClInclude Include="IcomController.h" />
<ClInclude Include="K8055Controller.h" />
<ClInclude Include="LogEvent.h" />
<ClInclude Include="Logger.h" />
<ClInclude Include="lusb0_usb.h" />
<ClInclude Include="MMDVMController.h" />
<ClInclude Include="Modem.h" />
<ClInclude Include="OutputQueue.h" />
<ClInclude Include="RepeaterProtocolHandler.h" />
<ClInclude Include="RingBuffer.h" />
<ClInclude Include="SerialDataController.h" />
<ClInclude Include="SerialLineController.h" />
<ClInclude Include="SerialPortSelector.h" />
<ClInclude Include="SlowDataDecoder.h" />
<ClInclude Include="SlowDataEncoder.h" />
<ClInclude Include="SoundCardController.h" />
<ClInclude Include="SoundCardReaderWriter.h" />
<ClInclude Include="SplitController.h" />
<ClInclude Include="TCPReaderWriter.h" />
<ClInclude Include="Timer.h" />
<ClInclude Include="UDPReaderWriter.h" />
<ClInclude Include="URIUSBController.h" />
<ClInclude Include="Utils.h" />
<ClInclude Include="Version.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

@ -1,332 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="AMBEFEC.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="AnnouncementUnit.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ArduinoController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="BeaconUnit.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CallsignList.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CCITTChecksum.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CCITTChecksumReverse.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DStarGMSKDemodulator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DStarGMSKModulator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DStarRepeaterConfig.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DummyController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVAPController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVMegaController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVRPTRV1Controller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVRPTRV2Controller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVRPTRV3Controller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVTOOLFileReader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DVTOOLFileWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ExternalController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FIRFilter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GatewayProtocolHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GMSKController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GMSKModem.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GMSKModemLibUsb.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GMSKModemWinUSB.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Golay.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HardwareController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HeaderData.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="K8055Controller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LogEvent.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Logger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MMDVMController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Modem.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="OutputQueue.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="RepeaterProtocolHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SerialDataController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SerialLineController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SerialPortSelector.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SlowDataDecoder.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SlowDataEncoder.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SoundCardController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SoundCardReaderWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SplitController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TCPReaderWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Timer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="UDPReaderWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Utils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="URIUSBController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="IcomController.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="AMBEFEC.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AnnouncementCallback.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AnnouncementUnit.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ArduinoController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AudioCallback.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="BeaconCallback.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="BeaconUnit.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CallsignList.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CCITTChecksum.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CCITTChecksumReverse.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DStarDefines.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DStarGMSKDemodulator.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DStarGMSKModulator.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DStarRepeaterConfig.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DummyController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVAPController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVMegaController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVRPTRV1Controller.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVRPTRV2Controller.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVRPTRV3Controller.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVTOOLFileReader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DVTOOLFileWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ExternalController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="FIRFilter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GatewayProtocolHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GMSKController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GMSKModem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GMSKModemLibUsb.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GMSKModemWinUSB.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Golay.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HardwareController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HeaderData.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="K8055Controller.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LogEvent.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Logger.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="lusb0_usb.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MMDVMController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Modem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="OutputQueue.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="RepeaterProtocolHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="RingBuffer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SerialDataController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SerialLineController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SerialPortSelector.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SlowDataDecoder.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SlowDataEncoder.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SoundCardController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SoundCardReaderWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SplitController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="TCPReaderWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Timer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="UDPReaderWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Version.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="URIUSBController.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="IcomController.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

@ -11,159 +11,239 @@
* GNU General Public License for more details. * GNU General Public License for more details.
*/ */
/*
* Central D-Star digital voice protocol constants. These values are mandated by
* the D-Star specification and must not be changed without breaking on-air
* compatibility. They cover the physical layer (GMSK modulation), frame
* structure, slow-data encoding, flag fields, callsign formats, timing, and
* network linking ports.
*/
#ifndef DStarDefines_H #ifndef DStarDefines_H
#define DStarDefines_H #define DStarDefines_H
#include <wx/wx.h> #include "StdCompat.h"
// --- Physical / modulation layer ---
// D-Star uses GMSK at 4800 bps with a Gaussian BT product of 0.5.
const unsigned int DSTAR_GMSK_SYMBOL_RATE = 4800U; const unsigned int DSTAR_GMSK_SYMBOL_RATE = 4800U;
const float DSTAR_GMSK_BT = 0.5F; const float DSTAR_GMSK_BT = 0.5F;
const unsigned char DATA_SYNC_BYTES[] = {0x55, 0x2D, 0x16}; // --- Frame structure ---
const unsigned char END_PATTERN_BYTES[] = {0x55, 0x55, 0x55, 0x55, 0xC8, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // Three-byte pattern that marks the start of a DV data frame within a stream.
inline const unsigned char DATA_SYNC_BYTES[] = {0x55, 0x2D, 0x16};
// Sent after the last DV frame to signal end-of-transmission on air.
// Only the first END_PATTERN_LENGTH_BYTES bytes are transmitted; the rest are padding.
inline const unsigned char END_PATTERN_BYTES[] = {0x55, 0x55, 0x55, 0x55, 0xC8, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const unsigned int END_PATTERN_LENGTH_BYTES = 6U; const unsigned int END_PATTERN_LENGTH_BYTES = 6U;
const unsigned char NULL_AMBE_DATA_BYTES[] = {0x9E, 0x8D, 0x32, 0x88, 0x26, 0x1A, 0x3F, 0x61, 0xE8}; // AMBE codec output for a frame of silence (used to fill gaps).
inline const unsigned char NULL_AMBE_DATA_BYTES[] = {0x9E, 0x8D, 0x32, 0x88, 0x26, 0x1A, 0x3F, 0x61, 0xE8};
// Slow-data bytes representing an empty payload.
// Note that these are already scrambled, 0x66 0x66 0x66 otherwise // Note that these are already scrambled, 0x66 0x66 0x66 otherwise
const unsigned char NULL_SLOW_DATA_BYTES[] = {0x16, 0x29, 0xF5}; inline const unsigned char NULL_SLOW_DATA_BYTES[] = {0x16, 0x29, 0xF5};
const unsigned char NULL_FRAME_DATA_BYTES[] = {0x9E, 0x8D, 0x32, 0x88, 0x26, 0x1A, 0x3F, 0x61, 0xE8, 0x16, 0x29, 0xF5}; // Combined null DV frame: NULL_AMBE_DATA_BYTES followed by NULL_SLOW_DATA_BYTES.
inline const unsigned char NULL_FRAME_DATA_BYTES[] = {0x9E, 0x8D, 0x32, 0x88, 0x26, 0x1A, 0x3F, 0x61, 0xE8, 0x16, 0x29, 0xF5};
// Each DV frame carries 9 bytes of AMBE-compressed voice (72 bits at 3600 bps).
const unsigned int VOICE_FRAME_LENGTH_BYTES = 9U; const unsigned int VOICE_FRAME_LENGTH_BYTES = 9U;
// Each DV frame also carries 3 bytes of slow data (24 bits at 1200 bps).
const unsigned int DATA_FRAME_LENGTH_BYTES = 3U; const unsigned int DATA_FRAME_LENGTH_BYTES = 3U;
// Total wire size of one DV frame: voice + slow-data interleaved.
const unsigned int DV_FRAME_LENGTH_BYTES = VOICE_FRAME_LENGTH_BYTES + DATA_FRAME_LENGTH_BYTES; const unsigned int DV_FRAME_LENGTH_BYTES = VOICE_FRAME_LENGTH_BYTES + DATA_FRAME_LENGTH_BYTES;
// The length of the end frame, three bytes extra // The length of the end frame, three bytes extra
// Maximum DV frame buffer size, accommodating the end-of-transmission extra bytes.
const unsigned int DV_FRAME_MAX_LENGTH_BYTES = DV_FRAME_LENGTH_BYTES + 3U; const unsigned int DV_FRAME_MAX_LENGTH_BYTES = DV_FRAME_LENGTH_BYTES + 3U;
// Length of the FEC (forward error correction) section in the radio header.
const unsigned int FEC_SECTION_LENGTH_BYTES = 83U; const unsigned int FEC_SECTION_LENGTH_BYTES = 83U;
// On-air D-Star header size: 41 bytes covering callsigns, flags, and FEC.
const unsigned int RADIO_HEADER_LENGTH_BYTES = 41U; const unsigned int RADIO_HEADER_LENGTH_BYTES = 41U;
// 21 DV frames form one slow-data block (enough to carry the full header in-band).
const unsigned int DATA_BLOCK_SIZE_BYTES = 21U * DV_FRAME_LENGTH_BYTES; const unsigned int DATA_BLOCK_SIZE_BYTES = 21U * DV_FRAME_LENGTH_BYTES;
// --- Audio / DSP sizing ---
// Baseband sample rate used by modem drivers that process raw I/Q or audio.
const unsigned int DSTAR_RADIO_SAMPLE_RATE = 48000U; const unsigned int DSTAR_RADIO_SAMPLE_RATE = 48000U;
// Samples per processing block handed to the modem (20 ms at 48 kHz).
const unsigned int DSTAR_RADIO_BLOCK_SIZE = 960U; const unsigned int DSTAR_RADIO_BLOCK_SIZE = 960U;
// Samples per AMBE codec frame (20 ms at 8 kHz narrowband).
const unsigned int DSTAR_AUDIO_BLOCK_SIZE = 160U; const unsigned int DSTAR_AUDIO_BLOCK_SIZE = 160U;
// --- Callsign / locator field widths ---
// Maidenhead grid locator string length (e.g. "IO91wm").
const unsigned int LOCATOR_LENGTH = 6U; const unsigned int LOCATOR_LENGTH = 6U;
// D-Star callsign field is always 8 characters, right-padded with spaces.
const unsigned int LONG_CALLSIGN_LENGTH = 8U; const unsigned int LONG_CALLSIGN_LENGTH = 8U;
// Suffix / RPT2 suffix field (e.g. " G" for gateway, " A" for port A).
const unsigned int SHORT_CALLSIGN_LENGTH = 4U; const unsigned int SHORT_CALLSIGN_LENGTH = 4U;
// --- Slow-data field encoding ---
// Upper nibble of the slow-data type byte identifies the payload type.
const unsigned char SLOW_DATA_TYPE_MASK = 0xF0U; const unsigned char SLOW_DATA_TYPE_MASK = 0xF0U;
const unsigned char SLOW_DATA_TYPE_GPSDATA = 0x30U; const unsigned char SLOW_DATA_TYPE_GPSDATA = 0x30U; // APRS/GPS position string
const unsigned char SLOW_DATA_TYPE_TEXT = 0x40U; const unsigned char SLOW_DATA_TYPE_TEXT = 0x40U; // Free-text message (TX message)
const unsigned char SLOW_DATA_TYPE_HEADER = 0x50U; const unsigned char SLOW_DATA_TYPE_HEADER = 0x50U; // Repeated radio header
const unsigned char SLOW_DATA_TYPE_SQUELCH = 0xC0U; const unsigned char SLOW_DATA_TYPE_SQUELCH = 0xC0U; // Squelch-tail / keep-alive
// Lower nibble gives the number of valid data bytes in this slow-data chunk.
const unsigned char SLOW_DATA_LENGTH_MASK = 0x0FU; const unsigned char SLOW_DATA_LENGTH_MASK = 0x0FU;
// Maximum length of a slow-data text message in characters.
const unsigned int SLOW_DATA_TEXT_LENGTH = 20U; const unsigned int SLOW_DATA_TEXT_LENGTH = 20U;
// --- Scrambler ---
// XOR pattern applied to each slow-data byte pair to decorrelate bit patterns
// on air. Applied as byte[0] ^= BYTE1, byte[1] ^= BYTE2, byte[2] ^= BYTE3.
const unsigned char SCRAMBLER_BYTE1 = 0x70U; const unsigned char SCRAMBLER_BYTE1 = 0x70U;
const unsigned char SCRAMBLER_BYTE2 = 0x4FU; const unsigned char SCRAMBLER_BYTE2 = 0x4FU;
const unsigned char SCRAMBLER_BYTE3 = 0x93U; const unsigned char SCRAMBLER_BYTE3 = 0x93U;
// --- Header flag byte 1 (RF_HEADER.flag[0]) ---
// Set when the frame carries data rather than voice.
const unsigned char DATA_MASK = 0x80U; const unsigned char DATA_MASK = 0x80U;
// Set when a repeater is involved in the QSO.
const unsigned char REPEATER_MASK = 0x40U; const unsigned char REPEATER_MASK = 0x40U;
// Set when the transmission was interrupted (incomplete header received).
const unsigned char INTERRUPTED_MASK = 0x20U; const unsigned char INTERRUPTED_MASK = 0x20U;
// Set for control-channel signalling frames.
const unsigned char CONTROL_SIGNAL_MASK = 0x10U; const unsigned char CONTROL_SIGNAL_MASK = 0x10U;
// Set to request priority / urgent handling.
const unsigned char URGENT_MASK = 0x08U; const unsigned char URGENT_MASK = 0x08U;
// --- Header flag byte 1 lower nibble: repeater control codes ---
const unsigned char REPEATER_CONTROL_MASK = 0x07U; const unsigned char REPEATER_CONTROL_MASK = 0x07U;
const unsigned char REPEATER_CONTROL = 0x07U; const unsigned char REPEATER_CONTROL = 0x07U; // Normal repeater frame
const unsigned char AUTO_REPLY = 0x06U; const unsigned char AUTO_REPLY = 0x06U; // Automatic acknowledgement
const unsigned char RESEND_REQUESTED = 0x04U; const unsigned char RESEND_REQUESTED = 0x04U; // Request retransmission
const unsigned char ACK_FLAG = 0x03U; const unsigned char ACK_FLAG = 0x03U; // Positive acknowledgement
const unsigned char NO_RESPONSE = 0x02U; const unsigned char NO_RESPONSE = 0x02U; // Destination not responding
const unsigned char RELAY_UNAVAILABLE = 0x01U; const unsigned char RELAY_UNAVAILABLE = 0x01U; // Requested relay is busy/offline
// --- Timing ---
// Number of 48 kHz samples per symbol at 4800 bps.
const unsigned int DSTAR_RADIO_BIT_LENGTH = DSTAR_RADIO_SAMPLE_RATE / DSTAR_GMSK_SYMBOL_RATE; const unsigned int DSTAR_RADIO_BIT_LENGTH = DSTAR_RADIO_SAMPLE_RATE / DSTAR_GMSK_SYMBOL_RATE;
// D-Star frame period: one DV frame every 20 ms = 50 frames per second.
const unsigned int DSTAR_FRAME_TIME_MS = 20U; const unsigned int DSTAR_FRAME_TIME_MS = 20U;
const unsigned int DSTAR_FRAMES_PER_SEC = 50U; const unsigned int DSTAR_FRAMES_PER_SEC = 50U;
// Processing ticks per second derived from the DSP block size (48000/960 = 50).
const unsigned int DSTAR_TICKS_PER_SEC = DSTAR_RADIO_SAMPLE_RATE / DSTAR_RADIO_BLOCK_SIZE; const unsigned int DSTAR_TICKS_PER_SEC = DSTAR_RADIO_SAMPLE_RATE / DSTAR_RADIO_BLOCK_SIZE;
// --- Network linking ports ---
// UDP port used by the DExtra reflector linking protocol.
const unsigned int DEXTRA_PORT = 30001U; const unsigned int DEXTRA_PORT = 30001U;
// UDP port used by the DCS reflector linking protocol.
const unsigned int DCS_PORT = 30051U; const unsigned int DCS_PORT = 30051U;
// --- Modem / protocol tuning ---
// Byte count exchanged per USB transaction with the GMSK modem firmware.
const unsigned int GMSK_MODEM_DATA_LENGTH = 8U; const unsigned int GMSK_MODEM_DATA_LENGTH = 8U;
// Minimum consecutive valid frames required before a radio/local/network
// transmission is considered stable and handed to the repeater logic.
const unsigned int RADIO_RUN_FRAME_COUNT = 5U; const unsigned int RADIO_RUN_FRAME_COUNT = 5U;
const unsigned int LOCAL_RUN_FRAME_COUNT = 1U; const unsigned int LOCAL_RUN_FRAME_COUNT = 1U;
const unsigned int NETWORK_RUN_FRAME_COUNT = 25U; const unsigned int NETWORK_RUN_FRAME_COUNT = 25U;
const unsigned int DSTAR_BLEEP_FREQ = 2000U; // Parameters for the end-of-transmission "bleep" courtesy tone.
const unsigned int DSTAR_BLEEP_LENGTH = 100U; const unsigned int DSTAR_BLEEP_FREQ = 2000U; // Hz
const float DSTAR_BLEEP_AMPL = 0.5F; const unsigned int DSTAR_BLEEP_LENGTH = 100U; // ms
const float DSTAR_BLEEP_AMPL = 0.5F; // 0.01.0 amplitude
// Seconds of silence from the network gateway before treating the link as lost.
const unsigned int NETWORK_TIMEOUT = 2U; const unsigned int NETWORK_TIMEOUT = 2U;
const unsigned int SPLIT_RX_GUI_COUNT = 5U; // --- Split (half-duplex split site) frame counts ---
// How many frames to buffer on RX/TX sides in split mode.
const unsigned int SPLIT_RX_COUNT = 25U; const unsigned int SPLIT_RX_COUNT = 25U;
const unsigned int SPLIT_TX_GUI_COUNT = 3U;
const unsigned int SPLIT_TX_COUNT = 5U; const unsigned int SPLIT_TX_COUNT = 5U;
// --- Enums ---
// Repeater main state machine. Drives the TX/RX loop in the thread classes.
enum DSTAR_RPT_STATE { enum DSTAR_RPT_STATE {
DSRS_SHUTDOWN, DSRS_SHUTDOWN, // Repeater is administratively shut down
DSRS_LISTENING, DSRS_LISTENING, // Idle, waiting for a valid D-Star header
DSRS_VALID, DSRS_VALID, // Receiving a transmission from a known/allowed callsign
DSRS_VALID_WAIT, DSRS_VALID_WAIT, // Transmission ended; waiting for the tail to clear
DSRS_INVALID, DSRS_INVALID, // Receiving from an unknown or blocked callsign
DSRS_INVALID_WAIT, DSRS_INVALID_WAIT, // Invalid transmission ended; clearing tail
DSRS_TIMEOUT, DSRS_TIMEOUT, // Transmission exceeded the configured timeout
DSRS_TIMEOUT_WAIT, DSRS_TIMEOUT_WAIT, // Timeout tail clearing
DSRS_NETWORK DSRS_NETWORK // Relaying audio received from the network gateway
}; };
// Packet type tag returned by the network protocol handler to the repeater thread.
enum NETWORK_TYPE { enum NETWORK_TYPE {
NETWORK_NONE, NETWORK_NONE, // No packet available
NETWORK_HEADER, NETWORK_HEADER, // D-Star link header (start of a network QSO)
NETWORK_DATA, NETWORK_DATA, // DV frame payload
NETWORK_TEXT, NETWORK_TEXT, // Slow-data text message from the gateway
NETWORK_TEMPTEXT, NETWORK_TEMPTEXT, // Temporary/override text message
NETWORK_STATUS1, NETWORK_STATUS1, // ircDDB user status slots 15
NETWORK_STATUS2, NETWORK_STATUS2,
NETWORK_STATUS3, NETWORK_STATUS3,
NETWORK_STATUS4, NETWORK_STATUS4,
NETWORK_STATUS5, NETWORK_STATUS5,
NETWORK_REGISTER NETWORK_REGISTER // Gateway registration / keep-alive packet
}; };
// Operating mode selected in the config file; determines which thread class is
// instantiated and which modem functions are enabled.
enum DSTAR_MODE { enum DSTAR_MODE {
MODE_DUPLEX, MODE_DUPLEX, // Full duplex: separate RX and TX frequencies
MODE_SIMPLEX, MODE_SIMPLEX, // Simplex: share a single frequency for RX and TX
MODE_GATEWAY, MODE_GATEWAY, // Gateway-only: no over-air receive, network traffic only
MODE_TXONLY, MODE_TXONLY, // Transmit only (e.g. beacon node)
MODE_RXONLY, MODE_RXONLY, // Receive only (e.g. logging node)
MODE_TXANDRX MODE_TXANDRX // Independent TX and RX paths (split site)
}; };
// Current state of a reflector or gateway link.
enum LINK_STATUS { enum LINK_STATUS {
LS_NONE, LS_NONE, // Not linked
LS_PENDING_IRCDDB, LS_PENDING_IRCDDB, // Waiting for ircDDB callsign lookup to complete
LS_LINKING_LOOPBACK, LS_LINKING_LOOPBACK, // Handshake in progress for each protocol type:
LS_LINKING_DEXTRA, LS_LINKING_DEXTRA,
LS_LINKING_DPLUS, LS_LINKING_DPLUS,
LS_LINKING_DCS, LS_LINKING_DCS,
LS_LINKING_CCS, LS_LINKING_CCS,
LS_LINKED_LOOPBACK, LS_LINKED_LOOPBACK, // Fully linked on each protocol:
LS_LINKED_DEXTRA, LS_LINKED_DEXTRA,
LS_LINKED_DPLUS, LS_LINKED_DPLUS,
LS_LINKED_DCS, LS_LINKED_DCS,
LS_LINKED_CCS LS_LINKED_CCS
}; };
// What the repeater sends back to the calling station after a transmission.
enum ACK_TYPE { enum ACK_TYPE {
AT_NONE, AT_NONE, // No acknowledgement
AT_BER, AT_BER, // Bit-error-rate report
AT_STATUS AT_STATUS // Repeater status message
}; };
// Language used when generating spoken or text acknowledgement messages.
enum TEXT_LANG { enum TEXT_LANG {
TL_ENGLISH_UK, TL_ENGLISH_UK,
TL_DEUTSCH, TL_DEUTSCH,
@ -178,21 +258,24 @@ enum TEXT_LANG {
TL_NORSK TL_NORSK
}; };
// How the modem hardware is connected to the host.
enum CONNECTION_TYPE { enum CONNECTION_TYPE {
CT_USB, CT_USB,
CT_NETWORK CT_NETWORK
}; };
// Which USB backend to use for USB-connected modems.
enum USB_INTERFACE { enum USB_INTERFACE {
UI_LIBUSB, UI_LIBUSB,
UI_WINUSB UI_WINUSB
}; };
// DVMega radio module variant; determines which band(s) are active.
enum DVMEGA_VARIANT { enum DVMEGA_VARIANT {
DVMV_MODEM, DVMV_MODEM, // Modem-only (external radio)
DVMV_RADIO_2M, DVMV_RADIO_2M, // Built-in 2 m radio
DVMV_RADIO_70CM, DVMV_RADIO_70CM, // Built-in 70 cm radio
DVMV_RADIO_2M_70CM DVMV_RADIO_2M_70CM // Dual-band 2 m / 70 cm radio
}; };
#endif #endif

@ -14,7 +14,7 @@
#include "DStarGMSKDemodulator.h" #include "DStarGMSKDemodulator.h"
// Generated by gaussfir(0.5, 4, 10) // Generated by gaussfir(0.5, 4, 10)
const wxFloat32 COEFFS_TABLE[] = { const float COEFFS_TABLE[] = {
0.000000000000003F, 0.000000000000065F, 0.000000000001037F, 0.000000000014448F, 0.000000000174579F, 0.000000000000003F, 0.000000000000065F, 0.000000000001037F, 0.000000000014448F, 0.000000000174579F,
0.000000001829471F, 0.000000016627294F, 0.000000131062698F, 0.000000895979719F, 0.000005312253663F, 0.000027316243802F, 0.000000001829471F, 0.000000016627294F, 0.000000131062698F, 0.000000895979719F, 0.000005312253663F, 0.000027316243802F,
0.000121821714020F, 0.000471183399421F, 0.001580581180127F, 0.004598383433830F, 0.011602594308899F, 0.025390226926262F, 0.000121821714020F, 0.000471183399421F, 0.001580581180127F, 0.004598383433830F, 0.011602594308899F, 0.025390226926262F,
@ -50,7 +50,7 @@ CDStarGMSKDemodulator::~CDStarGMSKDemodulator()
{ {
} }
TRISTATE CDStarGMSKDemodulator::decode(wxFloat32 val) TRISTATE CDStarGMSKDemodulator::decode(float val)
{ {
TRISTATE state = STATE_UNKNOWN; TRISTATE state = STATE_UNKNOWN;
@ -60,7 +60,7 @@ TRISTATE CDStarGMSKDemodulator::decode(wxFloat32 val)
m_count++; m_count++;
if (m_count >= DCOFFSET_COUNT) { if (m_count >= DCOFFSET_COUNT) {
m_accum /= wxFloat32(DCOFFSET_COUNT); m_accum /= float(DCOFFSET_COUNT);
m_offset = m_offset * 0.9F + m_accum * 0.1F; m_offset = m_offset * 0.9F + m_accum * 0.1F;
@ -69,7 +69,7 @@ TRISTATE CDStarGMSKDemodulator::decode(wxFloat32 val)
} }
} }
wxFloat32 out = m_filter.process(val - m_offset); float out = m_filter.process(val - m_offset);
bool bit = out > 0.0F; bool bit = out > 0.0F;
@ -112,7 +112,7 @@ void CDStarGMSKDemodulator::setInvert(bool set)
void CDStarGMSKDemodulator::lock(bool on) void CDStarGMSKDemodulator::lock(bool on)
{ {
// Debugging only XXX // Debugging only XXX
wxLogMessage(wxT("Current DC offset: %f"), m_offset); // (wxLogMessage removed - was: "Current DC offset: %f", m_offset)
m_locked = on; m_locked = on;

@ -18,28 +18,44 @@
#include "FIRFilter.h" #include "FIRFilter.h"
#include "Utils.h" #include "Utils.h"
/*
* GMSK demodulator for D-Star 4800 bps received audio.
*
* Processes audio samples one at a time via decode(). Internally maintains a
* software PLL to track symbol timing, and a FIR low-pass filter to remove
* out-of-band noise before bit decisions are made.
*
* Returns TRISTATE:
* STATE_TRUE a '1' bit was clocked out at this sample
* STATE_FALSE a '0' bit was clocked out at this sample
* STATE_UNKNOWN mid-symbol; no bit decision yet
*
* lock() can be used to freeze PLL adaptation once a stable signal is detected,
* preventing the PLL from wandering during long runs of the same bit value.
*/
class CDStarGMSKDemodulator { class CDStarGMSKDemodulator {
public: public:
CDStarGMSKDemodulator(); CDStarGMSKDemodulator();
~CDStarGMSKDemodulator(); ~CDStarGMSKDemodulator();
TRISTATE decode(wxFloat32 val); TRISTATE decode(float val);
void setInvert(bool set); void setInvert(bool set);
void reset(); void reset();
// Freezes (on=true) or enables (on=false) PLL frequency adaptation.
void lock(bool on); void lock(bool on);
private: private:
CFIRFilter m_filter; CFIRFilter m_filter;
bool m_invert; bool m_invert;
unsigned int m_pll; unsigned int m_pll; // Current PLL phase accumulator.
bool m_prev; bool m_prev; // Previous bit value, used for GMSK differential decoding.
unsigned int m_inc; unsigned int m_inc; // PLL phase increment per sample.
bool m_locked; bool m_locked;
wxFloat32 m_offset; float m_offset;
wxFloat32 m_accum; float m_accum;
unsigned int m_count; unsigned int m_count;
}; };

@ -15,8 +15,10 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include <cassert>
// Generated by gaussfir(0.5, 4, 10) // Generated by gaussfir(0.5, 4, 10)
const wxFloat32 COEFFS_TABLE[] = { const float COEFFS_TABLE[] = {
0.000000000000003F, 0.000000000000065F, 0.000000000001037F, 0.000000000014448F, 0.000000000174579F, 0.000000000000003F, 0.000000000000065F, 0.000000000001037F, 0.000000000014448F, 0.000000000174579F,
0.000000001829471F, 0.000000016627294F, 0.000000131062698F, 0.000000895979719F, 0.000005312253663F, 0.000027316243802F, 0.000000001829471F, 0.000000016627294F, 0.000000131062698F, 0.000000895979719F, 0.000005312253663F, 0.000027316243802F,
0.000121821714020F, 0.000471183399421F, 0.001580581180127F, 0.004598383433830F, 0.011602594308899F, 0.025390226926262F, 0.000121821714020F, 0.000471183399421F, 0.001580581180127F, 0.004598383433830F, 0.011602594308899F, 0.025390226926262F,
@ -38,10 +40,10 @@ CDStarGMSKModulator::~CDStarGMSKModulator()
{ {
} }
unsigned int CDStarGMSKModulator::code(bool bit, wxFloat32* buffer, unsigned int length) unsigned int CDStarGMSKModulator::code(bool bit, float* buffer, unsigned int length)
{ {
wxASSERT(buffer != NULL); assert(buffer != nullptr);
wxASSERT(length == DSTAR_RADIO_BIT_LENGTH); assert(length == DSTAR_RADIO_BIT_LENGTH);
if (m_invert) if (m_invert)
bit = !bit; bit = !bit;

@ -16,12 +16,21 @@
#include "FIRFilter.h" #include "FIRFilter.h"
/*
* GMSK modulator for D-Star 4800 bps transmissions.
*
* Converts a single NRZ bit into a sequence of float audio samples using a
* Gaussian FIR pulse-shaping filter (BT=0.5). The output sample buffer must
* be pre-allocated by the caller; code() returns the number of samples written.
* setInvert() reverses the polarity of the modulated signal for hardware that
* requires the opposite sense.
*/
class CDStarGMSKModulator { class CDStarGMSKModulator {
public: public:
CDStarGMSKModulator(); CDStarGMSKModulator();
~CDStarGMSKModulator(); ~CDStarGMSKModulator();
unsigned int code(bool bit, wxFloat32* buffer, unsigned int length); unsigned int code(bool bit, float* buffer, unsigned int length);
void setInvert(bool set); void setInvert(bool set);

File diff suppressed because it is too large Load Diff

@ -21,198 +21,248 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include <wx/wx.h> #include <stdexcept>
#include <wx/config.h> #include <string>
#include <wx/filename.h> #include <vector>
/*
* Parses the INI-style repeater configuration file.
*
* File format (MMDVMHost compatible):
* [Section] headers
* Key=Value pairs
* # comment lines
*
* The constructor takes the full path to the config file and throws
* std::runtime_error if the file does not exist or cannot be opened.
* There is no write-back facility; this class is read-only.
*
* Settings are grouped into logical sections exposed via typed getters.
* Every getter reads all fields for that group atomically (all by
* out-reference).
*/
class CDStarRepeaterConfig { class CDStarRepeaterConfig {
public: public:
#if defined(__WINDOWS__) /*
CDStarRepeaterConfig(wxConfigBase* config, const wxString& dir, const wxString& configName, const wxString& name); * Opens and parses filePath. Throws std::runtime_error if the file
#else * does not exist or cannot be opened. All members are initialised to
CDStarRepeaterConfig(const wxString& dir, const wxString& configName, const wxString& name, const bool mustExist=false); * built-in defaults before parsing, so any key absent from the file
#endif * silently retains its default value.
*/
explicit CDStarRepeaterConfig(const std::string& filePath);
~CDStarRepeaterConfig(); ~CDStarRepeaterConfig();
void getCallsign(wxString& callsign, wxString& gateway, DSTAR_MODE& mode, ACK_TYPE& ack, bool& restriction, bool& rpt1Validation, bool& dtmfBlanking, bool& errorReply) const; // [General]
void setCallsign(const wxString& callsign, const wxString& gateway, DSTAR_MODE mode, ACK_TYPE ack, bool restriction, bool rpt1Validation, bool dtmfBlanking, bool errorReply); void getCallsign(std::string& callsign, std::string& gateway, DSTAR_MODE& mode, ACK_TYPE& ack, bool& restriction, bool& rpt1Validation, bool& dtmfBlanking, bool& errorReply) const;
void getNetwork(wxString& gatewayAddress, unsigned int& gatewayPort, wxString& localAddress, unsigned int& localPort, wxString& name) const; // [Log]
void setNetwork(const wxString& gatewayAddress, unsigned int gatewayPort, const wxString& localAddress, unsigned int localPort, const wxString& name); void getLog(std::string& filePath, unsigned int& fileLevel, unsigned int& displayLevel, unsigned int& mqttLevel) const;
void getModem(wxString& type) const; // [Paths]
void setModem(const wxString& type); void getPaths(std::string& dataDir, std::string& audioDir) const;
void getTimes(unsigned int& timeout, unsigned int& ackTime) const; // [Network]
void setTimes(unsigned int timeout, unsigned int ackTime); void getNetwork(std::string& gatewayAddress, unsigned int& gatewayPort, std::string& localAddress, unsigned int& localPort, std::string& name) const;
// [Modem]
void getModem(std::string& type) const;
void getBeacon(unsigned int& time, wxString& text, bool& voice, TEXT_LANG& language) const; // [Times]
void setBeacon(unsigned int time, const wxString& text, bool voice, TEXT_LANG language); void getTimes(unsigned int& timeout, unsigned int& ackTime) const;
void getAnnouncement(bool& enabled, unsigned int& time, wxString& recordRPT1, wxString& recordRPT2, wxString& deleteRPT1, wxString& deleteRPT2) const; // [Beacon]
void setAnnouncement(bool enabled, unsigned int time, const wxString& recordRPT1, const wxString& recordRPT2, const wxString& deleteRPT1, const wxString& deleteRPT2); void getBeacon(unsigned int& time, std::string& text, bool& voice, TEXT_LANG& language) const;
void getControl(bool& enabled, wxString& rpt1Callsign, wxString& rpt2Callsign, wxString& shutdown, wxString& startup, wxString& status1, wxString& status2, wxString& status3, wxString& status4, wxString& status5, wxString& command1, wxString& command1Line, wxString& command2, wxString& command2Line, wxString& command5, wxString& command5Line, wxString& command6, wxString& command6Line, wxString& command3, wxString& command3Line, wxString& command4, wxString& command4Line, wxString& output1, wxString& output2, wxString& output3, wxString& output4) const; // [Announcement]
void getAnnouncement(bool& enabled, unsigned int& time, std::string& recordRPT1, std::string& recordRPT2, std::string& deleteRPT1, std::string& deleteRPT2) const;
void setControl(bool enabled, const wxString& rpt1Callsign, const wxString& rpt2Callsign, const wxString& shutdown, const wxString& startup, const wxString& status1, const wxString& status2, const wxString& status3, const wxString& status4, const wxString& status5, const wxString& command1, const wxString& command1Line, const wxString& command2, const wxString& command2Line, const wxString& command3, const wxString& command3Line, const wxString& command4, const wxString& command4Line, const wxString& command5, const wxString& command5Line, const wxString& command6, const wxString& command6Line, const wxString& output1, const wxString& output2, const wxString& output3, const wxString& output4); // [Control]
void getControl(bool& enabled, std::string& rpt1Callsign, std::string& rpt2Callsign, std::string& shutdown, std::string& startup, std::string& status1, std::string& status2, std::string& status3, std::string& status4, std::string& status5, std::string& command1, std::string& command1Line, std::string& command2, std::string& command2Line, std::string& command3, std::string& command3Line, std::string& command4, std::string& command4Line, std::string& command5, std::string& command5Line, std::string& command6, std::string& command6Line, std::string& output1, std::string& output2, std::string& output3, std::string& output4) const;
void getController(wxString& type, unsigned int& serialConfig, bool& pttInvert, unsigned int& activeHangTime) const; // [Controller]
void setController(const wxString& type, unsigned int serialConfig, bool pttInvert, unsigned int activeHangTime); void getController(std::string& type, unsigned int& serialConfig, bool& pttInvert, unsigned int& activeHangTime) const;
// [Outputs]
void getOutputs(bool& out1, bool& out2, bool& out3, bool& out4) const; void getOutputs(bool& out1, bool& out2, bool& out3, bool& out4) const;
void setOutputs(bool out1, bool out2, bool out3, bool out4);
// [Frame Logging]
void getLogging(bool& logging) const; void getLogging(bool& logging) const;
void setLogging(bool logging);
void getPosition(int& x, int& y) const; // [Whitelist] / [Blacklist] / [Greylist] (empty string = not configured)
void setPosition(int x, int y); void getWhitelist(std::string& file) const;
void getBlacklist(std::string& file) const;
void getGreylist(std::string& file) const;
void getDVAP(wxString& port, unsigned int& frequency, int& power, int& squelch) const; // [DVAP]
void setDVAP(const wxString& port, unsigned int frequency, int power, int squelch); void getDVAP(std::string& port, unsigned int& frequency, int& power, int& squelch) const;
// [GMSK]
void getGMSK(USB_INTERFACE& type, unsigned int& address) const; void getGMSK(USB_INTERFACE& type, unsigned int& address) const;
void setGMSK(USB_INTERFACE type, unsigned int address);
void getDVRPTR1(wxString& port, bool& rxInvert, bool& txInvert, bool& channel, unsigned int& modLevel, unsigned int& txDelay) const; // [DV-RPTR V1]
void setDVRPTR1(const wxString& port, bool rxInvert, bool txInvert, bool channel, unsigned int modLevel, unsigned int txDelay); void getDVRPTR1(std::string& port, bool& rxInvert, bool& txInvert, bool& channel, unsigned int& modLevel, unsigned int& txDelay) const;
void getDVRPTR2(CONNECTION_TYPE& connectionType, wxString& usbPort, wxString& address, unsigned int& port, bool& txInvert, unsigned int& modLevel, unsigned int& txDelay) const; // [DV-RPTR V2]
void setDVRPTR2(CONNECTION_TYPE connectionType, const wxString& usbPort, const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, unsigned int txDelay); void getDVRPTR2(CONNECTION_TYPE& connectionType, std::string& usbPort, std::string& address, unsigned int& port, bool& txInvert, unsigned int& modLevel, unsigned int& txDelay) const;
void getDVRPTR3(CONNECTION_TYPE& connectionType, wxString& usbPort, wxString& address, unsigned int& port, bool& txInvert, unsigned int& modLevel, unsigned int& txDelay) const; // [DV-RPTR V3]
void setDVRPTR3(CONNECTION_TYPE connectionType, const wxString& usbPort, const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, unsigned int txDelay); void getDVRPTR3(CONNECTION_TYPE& connectionType, std::string& usbPort, std::string& address, unsigned int& port, bool& txInvert, unsigned int& modLevel, unsigned int& txDelay) const;
void getDVMEGA(wxString& port, DVMEGA_VARIANT& variant, bool& rxInvert, bool& txInvert, unsigned int& txDelay, unsigned int& rxFrequency, unsigned int& txFrequency, unsigned int& power) const; // [DVMEGA]
void setDVMEGA(const wxString& port, DVMEGA_VARIANT variant, bool rxInvert, bool txInvert, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power); void getDVMEGA(std::string& port, DVMEGA_VARIANT& variant, bool& rxInvert, bool& txInvert, unsigned int& txDelay, unsigned int& rxFrequency, unsigned int& txFrequency, unsigned int& power) const;
void getMMDVM(wxString& port, bool& rxInvert, bool& txInvert, bool& pttInvert, unsigned int& txDelay, unsigned int& rxLevel, unsigned int& txLevel) const; // [MMDVM]
void setMMDVM(const wxString& port, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int rxLevel, unsigned int txLevel); void getMMDVM(std::string& port, bool& rxInvert, bool& txInvert, bool& pttInvert, unsigned int& txDelay, unsigned int& rxLevel, unsigned int& txLevel) const;
void getSoundCard(wxString& rxDevice, wxString& txDevice, bool& rxInvert, bool& txInvert, wxFloat32& rxLevel, wxFloat32& txLevel, unsigned int& txDelay, unsigned int& txTail) const; // [Sound Card]
void setSoundCard(const wxString& rxDevice, const wxString& txDevice, bool rxInvert, bool txInvert, wxFloat32 rxLevel, wxFloat32 txLevel, unsigned int txDelay, unsigned int txTail); void getSoundCard(std::string& rxDevice, std::string& txDevice, bool& rxInvert, bool& txInvert, float& rxLevel, float& txLevel, unsigned int& txDelay, unsigned int& txTail) const;
void getSplit(wxString& localAddress, unsigned int& localPort, wxArrayString& transmitterNames, wxArrayString& receiverNames, unsigned int& timeout) const; // [Split]
void setSplit(const wxString& localAddress, unsigned int localPort, const wxArrayString& transmitterNames, const wxArrayString& receiverNames, unsigned int timeout); void getSplit(std::string& localAddress, unsigned int& localPort, std::vector<std::string>& transmitterNames, std::vector<std::string>& receiverNames, unsigned int& timeout) const;
void getIcom(wxString& port) const; // [Icom]
void setIcom(const wxString& port); void getIcom(std::string& port) const;
#if defined(MQTT) #if defined(MQTT)
void getMQTT(wxString& host, unsigned int& port, bool& auth, wxString& username, wxString& password, unsigned int& keepalive, wxString& name) const; // [MQTT]
void setMQTT(const wxString& host, unsigned int port, bool auth, const wxString& username, const wxString& password, unsigned int keepalive, const wxString& name); void getMQTT(std::string& host, unsigned int& port, bool& auth, std::string& username, std::string& password, unsigned int& keepalive, std::string& name) const;
#endif #endif
bool write();
private: private:
#if defined(__WINDOWS__) // [General]
wxConfigBase* m_config; std::string m_callsign;
wxString m_name; std::string m_gateway;
#endif
wxFileName m_fileName;
wxString m_callsign;
wxString m_gateway;
DSTAR_MODE m_mode; DSTAR_MODE m_mode;
ACK_TYPE m_ack; ACK_TYPE m_ack;
bool m_restriction; bool m_restriction;
bool m_rpt1Validation; bool m_rpt1Validation;
bool m_dtmfBlanking; bool m_dtmfBlanking;
bool m_errorReply; bool m_errorReply;
wxString m_gatewayAddress;
// [Log]
std::string m_logFilePath;
unsigned int m_logFileLevel;
unsigned int m_logDisplayLevel;
unsigned int m_logMQTTLevel;
// [Paths]
std::string m_dataDir;
std::string m_audioDir;
// [Network]
std::string m_gatewayAddress;
unsigned int m_gatewayPort; unsigned int m_gatewayPort;
wxString m_localAddress; std::string m_localAddress;
unsigned int m_localPort; unsigned int m_localPort;
wxString m_networkName; std::string m_networkName;
wxString m_modemType;
// [Modem]
std::string m_modemType;
// [Times]
unsigned int m_timeout; unsigned int m_timeout;
unsigned int m_ackTime; unsigned int m_ackTime;
// [Beacon]
unsigned int m_beaconTime; unsigned int m_beaconTime;
wxString m_beaconText; std::string m_beaconText;
bool m_beaconVoice; bool m_beaconVoice;
TEXT_LANG m_language; TEXT_LANG m_language;
// [Announcement]
bool m_announcementEnabled; bool m_announcementEnabled;
unsigned int m_announcementTime; unsigned int m_announcementTime;
wxString m_announcementRecordRPT1; std::string m_announcementRecordRPT1;
wxString m_announcementRecordRPT2; std::string m_announcementRecordRPT2;
wxString m_announcementDeleteRPT1; std::string m_announcementDeleteRPT1;
wxString m_announcementDeleteRPT2; std::string m_announcementDeleteRPT2;
// [Control]
bool m_controlEnabled; bool m_controlEnabled;
wxString m_controlRpt1Callsign; std::string m_controlRpt1Callsign;
wxString m_controlRpt2Callsign; std::string m_controlRpt2Callsign;
wxString m_controlShutdown; std::string m_controlShutdown;
wxString m_controlStartup; std::string m_controlStartup;
wxString m_controlStatus1; std::string m_controlStatus1;
wxString m_controlStatus2; std::string m_controlStatus2;
wxString m_controlStatus3; std::string m_controlStatus3;
wxString m_controlStatus4; std::string m_controlStatus4;
wxString m_controlStatus5; std::string m_controlStatus5;
wxString m_controlCommand1; std::string m_controlCommand1;
wxString m_controlCommand1Line; std::string m_controlCommand1Line;
wxString m_controlCommand2; std::string m_controlCommand2;
wxString m_controlCommand2Line; std::string m_controlCommand2Line;
wxString m_controlCommand3; std::string m_controlCommand3;
wxString m_controlCommand3Line; std::string m_controlCommand3Line;
wxString m_controlCommand4; std::string m_controlCommand4;
wxString m_controlCommand4Line; std::string m_controlCommand4Line;
wxString m_controlCommand5; std::string m_controlCommand5;
wxString m_controlCommand5Line; std::string m_controlCommand5Line;
wxString m_controlCommand6; std::string m_controlCommand6;
wxString m_controlCommand6Line; std::string m_controlCommand6Line;
wxString m_controlOutput1; std::string m_controlOutput1;
wxString m_controlOutput2; std::string m_controlOutput2;
wxString m_controlOutput3; std::string m_controlOutput3;
wxString m_controlOutput4; std::string m_controlOutput4;
wxString m_controllerType;
// [Controller]
std::string m_controllerType;
unsigned int m_serialConfig; unsigned int m_serialConfig;
bool m_pttInvert; bool m_pttInvert;
unsigned int m_activeHangTime; unsigned int m_activeHangTime;
// [Outputs]
bool m_output1; bool m_output1;
bool m_output2; bool m_output2;
bool m_output3; bool m_output3;
bool m_output4; bool m_output4;
// [Frame Logging]
bool m_logging; bool m_logging;
int m_x;
int m_y;
// DVAP // [Whitelist] / [Blacklist] / [Greylist] (empty = not configured)
wxString m_dvapPort; std::string m_whitelistFile;
std::string m_blacklistFile;
std::string m_greylistFile;
// [DVAP]
std::string m_dvapPort;
unsigned int m_dvapFrequency; unsigned int m_dvapFrequency;
int m_dvapPower; int m_dvapPower;
int m_dvapSquelch; int m_dvapSquelch;
// GMSK // [GMSK]
USB_INTERFACE m_gmskInterface; USB_INTERFACE m_gmskInterface;
unsigned int m_gmskAddress; unsigned int m_gmskAddress;
// DV-RPTR 1 // [DV-RPTR V1]
wxString m_dvrptr1Port; std::string m_dvrptr1Port;
bool m_dvrptr1RXInvert; bool m_dvrptr1RXInvert;
bool m_dvrptr1TXInvert; bool m_dvrptr1TXInvert;
bool m_dvrptr1Channel; bool m_dvrptr1Channel;
unsigned int m_dvrptr1ModLevel; unsigned int m_dvrptr1ModLevel;
unsigned int m_dvrptr1TXDelay; unsigned int m_dvrptr1TXDelay;
// DV-RPTR 2 // [DV-RPTR V2]
CONNECTION_TYPE m_dvrptr2Connection; CONNECTION_TYPE m_dvrptr2Connection;
wxString m_dvrptr2USBPort; std::string m_dvrptr2USBPort;
wxString m_dvrptr2Address; std::string m_dvrptr2Address;
unsigned int m_dvrptr2Port; unsigned int m_dvrptr2Port;
bool m_dvrptr2TXInvert; bool m_dvrptr2TXInvert;
unsigned int m_dvrptr2ModLevel; unsigned int m_dvrptr2ModLevel;
unsigned int m_dvrptr2TXDelay; unsigned int m_dvrptr2TXDelay;
// DV-RPTR 3 // [DV-RPTR V3]
CONNECTION_TYPE m_dvrptr3Connection; CONNECTION_TYPE m_dvrptr3Connection;
wxString m_dvrptr3USBPort; std::string m_dvrptr3USBPort;
wxString m_dvrptr3Address; std::string m_dvrptr3Address;
unsigned int m_dvrptr3Port; unsigned int m_dvrptr3Port;
bool m_dvrptr3TXInvert; bool m_dvrptr3TXInvert;
unsigned int m_dvrptr3ModLevel; unsigned int m_dvrptr3ModLevel;
unsigned int m_dvrptr3TXDelay; unsigned int m_dvrptr3TXDelay;
// DVMEGA // [DVMEGA]
wxString m_dvmegaPort; std::string m_dvmegaPort;
DVMEGA_VARIANT m_dvmegaVariant; DVMEGA_VARIANT m_dvmegaVariant;
bool m_dvmegaRXInvert; bool m_dvmegaRXInvert;
bool m_dvmegaTXInvert; bool m_dvmegaTXInvert;
@ -221,8 +271,8 @@ private:
unsigned int m_dvmegaTXFrequency; unsigned int m_dvmegaTXFrequency;
unsigned int m_dvmegaPower; unsigned int m_dvmegaPower;
// MMDVM // [MMDVM]
wxString m_mmdvmPort; std::string m_mmdvmPort;
bool m_mmdvmRXInvert; bool m_mmdvmRXInvert;
bool m_mmdvmTXInvert; bool m_mmdvmTXInvert;
bool m_mmdvmPTTInvert; bool m_mmdvmPTTInvert;
@ -230,35 +280,35 @@ private:
unsigned int m_mmdvmRXLevel; unsigned int m_mmdvmRXLevel;
unsigned int m_mmdvmTXLevel; unsigned int m_mmdvmTXLevel;
// Sound Card // [Sound Card]
wxString m_soundCardRXDevice; std::string m_soundCardRXDevice;
wxString m_soundCardTXDevice; std::string m_soundCardTXDevice;
bool m_soundCardRXInvert; bool m_soundCardRXInvert;
bool m_soundCardTXInvert; bool m_soundCardTXInvert;
wxFloat32 m_soundCardRXLevel; float m_soundCardRXLevel;
wxFloat32 m_soundCardTXLevel; float m_soundCardTXLevel;
unsigned int m_soundCardTXDelay; unsigned int m_soundCardTXDelay;
unsigned int m_soundCardTXTail; unsigned int m_soundCardTXTail;
// Split // [Split]
wxString m_splitLocalAddress; std::string m_splitLocalAddress;
unsigned int m_splitLocalPort; unsigned int m_splitLocalPort;
wxArrayString m_splitTXNames; std::vector<std::string> m_splitTXNames;
wxArrayString m_splitRXNames; std::vector<std::string> m_splitRXNames;
unsigned int m_splitTimeout; unsigned int m_splitTimeout;
// Icom Access Point/Terminal Mode // [Icom]
wxString m_icomPort; std::string m_icomPort;
#if defined(MQTT) #if defined(MQTT)
// MQTT // [MQTT]
wxString m_mqttHost; std::string m_mqttHost;
unsigned int m_mqttPort; unsigned int m_mqttPort;
bool m_mqttAuth; bool m_mqttAuth;
wxString m_mqttUsername; std::string m_mqttUsername;
wxString m_mqttPassword; std::string m_mqttPassword;
unsigned int m_mqttKeepalive; unsigned int m_mqttKeepalive;
wxString m_mqttName; std::string m_mqttName;
#endif #endif
}; };

@ -13,7 +13,7 @@
#include "DStarScrambler.h" #include "DStarScrambler.h"
#include <wx/wx.h> #include <cassert>
static const bool SCRAMBLER_TABLE_BITS[] = { static const bool SCRAMBLER_TABLE_BITS[] = {
false, false, false, false, true, true, true, false, true, true, true, true, false, false, true, false, false, false, false, false, true, true, true, false, true, true, true, true, false, false, true, false,
@ -85,7 +85,7 @@ CDStarScrambler::~CDStarScrambler()
void CDStarScrambler::process(bool* inOut, unsigned int length) void CDStarScrambler::process(bool* inOut, unsigned int length)
{ {
wxASSERT(inOut != 0); assert(inOut != nullptr);
for (unsigned int i = 0U; i < length; i++) { for (unsigned int i = 0U; i < length; i++) {
inOut[i] ^= SCRAMBLER_TABLE_BITS[m_count++]; inOut[i] ^= SCRAMBLER_TABLE_BITS[m_count++];
@ -97,8 +97,8 @@ void CDStarScrambler::process(bool* inOut, unsigned int length)
void CDStarScrambler::process(const bool* in, bool* out, unsigned int length) void CDStarScrambler::process(const bool* in, bool* out, unsigned int length)
{ {
wxASSERT(in != 0); assert(in != nullptr);
wxASSERT(out != 0); assert(out != nullptr);
for (unsigned int i = 0U; i < length; i++) { for (unsigned int i = 0U; i < length; i++) {
out[i] = in[i] ^ SCRAMBLER_TABLE_BITS[m_count++]; out[i] = in[i] ^ SCRAMBLER_TABLE_BITS[m_count++];
@ -110,7 +110,7 @@ void CDStarScrambler::process(const bool* in, bool* out, unsigned int length)
void CDStarScrambler::process(unsigned char* inOut, unsigned int length) void CDStarScrambler::process(unsigned char* inOut, unsigned int length)
{ {
wxASSERT(inOut != 0); assert(inOut != nullptr);
for (unsigned int i = 0U; i < length; i++) { for (unsigned int i = 0U; i < length; i++) {
inOut[i] ^= SCRAMBLER_TABLE_BYTES[m_count++]; inOut[i] ^= SCRAMBLER_TABLE_BYTES[m_count++];
@ -122,8 +122,8 @@ void CDStarScrambler::process(unsigned char* inOut, unsigned int length)
void CDStarScrambler::process(const unsigned char* in, unsigned char* out, unsigned int length) void CDStarScrambler::process(const unsigned char* in, unsigned char* out, unsigned int length)
{ {
wxASSERT(in != 0); assert(in != nullptr);
wxASSERT(out != 0); assert(out != nullptr);
for (unsigned int i = 0U; i < length; i++) { for (unsigned int i = 0U; i < length; i++) {
out[i] = in[i] ^ SCRAMBLER_TABLE_BYTES[m_count++]; out[i] = in[i] ^ SCRAMBLER_TABLE_BYTES[m_count++];

@ -14,6 +14,18 @@
#ifndef DStarScrambler_H #ifndef DStarScrambler_H
#define DStarScrambler_H #define DStarScrambler_H
/*
* D-Star bit scrambler applied to the physical RF layer.
*
* The D-Star standard mandates scrambling of the bit stream before GMSK
* modulation to randomise the data and avoid long runs of identical bits that
* would stress the PLL in receivers. The scrambler uses a linear feedback
* shift register with the polynomial x^17 + x^12 + 1.
*
* process() operates in-place (inOut form) or with separate input/output
* buffers, and handles both bit-per-bool and packed-byte representations.
* reset() resets the LFSR to its initial state.
*/
class CDStarScrambler { class CDStarScrambler {
public: public:
CDStarScrambler(); CDStarScrambler();
@ -28,7 +40,7 @@ public:
void reset(); void reset();
private: private:
unsigned int m_count; unsigned int m_count; // Current position in the LFSR sequence.
}; };
#endif #endif

@ -19,8 +19,15 @@
#include "CCITTChecksumReverse.h" #include "CCITTChecksumReverse.h"
#include "DVAPController.h" #include "DVAPController.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h" #include "Timer.h"
#include <chrono>
#include <thread>
#include <cassert>
#include <cstring>
#include "EndianCompat.h"
const unsigned char DVAP_REQ_NAME[] = {0x04, 0x20, 0x01, 0x00}; const unsigned char DVAP_REQ_NAME[] = {0x04, 0x20, 0x01, 0x00};
const unsigned int DVAP_REQ_NAME_LEN = 4U; const unsigned int DVAP_REQ_NAME_LEN = 4U;
@ -125,7 +132,7 @@ const unsigned int BUFFER_LENGTH = 200U;
const unsigned int DVAP_DUMP_LENGTH = 30U; const unsigned int DVAP_DUMP_LENGTH = 30U;
CDVAPController::CDVAPController(const wxString& port, unsigned int frequency, int power, int squelch) : CDVAPController::CDVAPController(const std::string& port, unsigned int frequency, int power, int squelch) :
CModem(), CModem(),
m_serial(port, SERIAL_230400), m_serial(port, SERIAL_230400),
m_frequency(frequency), m_frequency(frequency),
@ -133,24 +140,24 @@ m_power(power),
m_squelch(squelch), m_squelch(squelch),
m_squelchOpen(false), m_squelchOpen(false),
m_signal(0), m_signal(0),
m_buffer(NULL), m_buffer(nullptr),
m_streamId(0U), m_streamId(0U),
m_framePos(0U), m_framePos(0U),
m_seq(0U), m_seq(0U),
m_txData(1000U) m_txData(1000U)
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
, ,
m_dvapData(NULL), m_dvapData(nullptr),
m_dvapLength(NULL), m_dvapLength(nullptr),
m_dvapIndex(0U) m_dvapIndex(0U)
#endif #endif
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
wxASSERT((frequency >= 144000000U && frequency <= 148000000U) || assert((frequency >= 144000000U && frequency <= 148000000U) ||
(frequency >= 220000000U && frequency <= 225000000U) || (frequency >= 220000000U && frequency <= 225000000U) ||
(frequency >= 420000000U && frequency <= 450000000U)); (frequency >= 420000000U && frequency <= 450000000U));
wxASSERT(power >= -12 && power <= 10); assert(power >= -12 && power <= 10);
wxASSERT(squelch >= -128 && squelch <= -45); assert(squelch >= -128 && squelch <= -45);
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
@ -238,16 +245,19 @@ bool CDVAPController::start()
return false; return false;
} }
Create(); m_thread = std::thread(&CDVAPController::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CDVAPController::Entry() // Main controller thread. Runs until m_stopped is set by CModem::stop().
// The loop is driven by RT_STATE packets (sent every ~20ms by the hardware)
// which carry the TX buffer space count. Data is written to the wire only
// when space > 0 and an RT_STATE has just been received, so writes are
// naturally rate-limited to the hardware's 20ms frame cadence.
void CDVAPController::entry()
{ {
wxLogMessage(wxT("Starting DVAP Controller thread")); wxLogMessage("Starting DVAP Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 2U); CTimer pollTimer(200U, 2U);
@ -272,12 +282,13 @@ void* CDVAPController::Entry()
case RT_TIMEOUT: case RT_TIMEOUT:
break; break;
case RT_ERROR: case RT_ERROR:
wxLogMessage(wxT("Stopping DVAP Controller thread")); wxLogMessage("Stopping DVAP Controller thread");
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
dumpPackets(); dumpPackets();
#endif #endif
m_serial.close(); m_serial.close();
return NULL; delete[] writeBuffer;
return;
case RT_STATE: case RT_STATE:
m_signal = int(m_buffer[4U]) - 256; m_signal = int(m_buffer[4U]) - 256;
m_squelchOpen = m_buffer[5U] == 0x01U; m_squelchOpen = m_buffer[5U] == 0x01U;
@ -289,14 +300,14 @@ void* CDVAPController::Entry()
case RT_START: case RT_START:
break; break;
case RT_STOP: case RT_STOP:
wxLogWarning(wxT("DVAP has stopped, restarting")); wxLogWarning("DVAP has stopped, restarting");
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
dumpPackets(); dumpPackets();
#endif #endif
startDVAP(); startDVAP();
break; break;
case RT_HEADER: { case RT_HEADER: {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char hdr[2U]; unsigned char hdr[2U];
hdr[0U] = DSMTT_HEADER; hdr[0U] = DSMTT_HEADER;
@ -309,7 +320,7 @@ void* CDVAPController::Entry()
case RT_HEADER_ACK: case RT_HEADER_ACK:
break; break;
case RT_GMSK_DATA: { case RT_GMSK_DATA: {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
bool end = (m_buffer[4U] & 0x40U) == 0x40U; bool end = (m_buffer[4U] & 0x40U) == 0x40U;
if (end) { if (end) {
@ -328,7 +339,7 @@ void* CDVAPController::Entry()
} }
break; break;
case RT_FM_DATA: case RT_FM_DATA:
wxLogWarning(wxT("The DVAP has gone into FM mode, restarting the DVAP")); wxLogWarning("The DVAP has gone into FM mode, restarting the DVAP");
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
dumpPackets(); dumpPackets();
#endif #endif
@ -337,8 +348,8 @@ void* CDVAPController::Entry()
startDVAP(); startDVAP();
break; break;
default: default:
wxLogMessage(wxT("Unknown message")); wxLogMessage("Unknown message");
CUtils::dump(wxT("Buffer dump"), m_buffer, length); CUtils::dump("Buffer dump", m_buffer, length);
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
dumpPackets(); dumpPackets();
#endif #endif
@ -348,7 +359,7 @@ void* CDVAPController::Entry()
// Use the status packet every 20ms to trigger the sending of data to the DVAP // Use the status packet every 20ms to trigger the sending of data to the DVAP
if (space > 0U && type == RT_STATE) { if (space > 0U && type == RT_STATE) {
if (writeLength == 0U && m_txData.hasData()) { if (writeLength == 0U && m_txData.hasData()) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&writeLength, 1U); m_txData.getData(&writeLength, 1U);
m_txData.getData(writeBuffer, writeLength); m_txData.getData(writeBuffer, writeLength);
@ -356,55 +367,53 @@ void* CDVAPController::Entry()
// Only send the header when the TX is off // Only send the header when the TX is off
if (!m_tx && writeLength == DVAP_HEADER_LEN) { if (!m_tx && writeLength == DVAP_HEADER_LEN) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
storePacket(writeBuffer, writeLength); storePacket(writeBuffer, writeLength);
#endif #endif
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the DVAP")); wxLogWarning("Error when writing the header to the DVAP");
writeLength = 0U; writeLength = 0U;
space--; space--;
} }
if (writeLength == DVAP_GMSK_DATA_LEN) { if (writeLength == DVAP_GMSK_DATA_LEN) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength); // CUtils::dump("Write Data", writeBuffer, writeLength);
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
storePacket(writeBuffer, writeLength); storePacket(writeBuffer, writeLength);
#endif #endif
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the DVAP")); wxLogWarning("Error when writing data to the DVAP");
writeLength = 0U; writeLength = 0U;
space--; space--;
} }
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
} }
wxLogMessage(wxT("Stopping DVAP Controller thread")); wxLogMessage("Stopping DVAP Controller thread");
stopDVAP(); stopDVAP();
delete[] writeBuffer; delete[] writeBuffer;
m_serial.close(); m_serial.close();
return NULL;
} }
bool CDVAPController::writeHeader(const CHeaderData& header) bool CDVAPController::writeHeader(const CHeaderData& header)
{ {
bool ret = m_txData.hasSpace(DVAP_HEADER_LEN + 1U); bool ret = m_txData.hasSpace(DVAP_HEADER_LEN + 1U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -414,8 +423,8 @@ bool CDVAPController::writeHeader(const CHeaderData& header)
::memcpy(buffer, DVAP_HEADER, DVAP_HEADER_LEN); ::memcpy(buffer, DVAP_HEADER, DVAP_HEADER_LEN);
wxUint16 sid = wxUINT16_SWAP_ON_BE(m_streamId); uint16_t sid = htole16(m_streamId);
::memcpy(buffer + 2U, &sid, sizeof(wxUint16)); ::memcpy(buffer + 2U, &sid, sizeof(uint16_t));
buffer[4U] = 0x80U; buffer[4U] = 0x80U;
buffer[5U] = 0U; buffer[5U] = 0U;
@ -426,25 +435,25 @@ bool CDVAPController::writeHeader(const CHeaderData& header)
buffer[7U] = header.getFlag2(); buffer[7U] = header.getFlag2();
buffer[8U] = header.getFlag3(); buffer[8U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 9U] = rpt2.GetChar(i); buffer[i + 9U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 17U] = rpt1.GetChar(i); buffer[i + 17U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 25U] = your.GetChar(i); buffer[i + 25U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 33U] = my1.GetChar(i); buffer[i + 33U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 41U] = my2.GetChar(i); buffer[i + 41U] = my2[i];
CCCITTChecksumReverse cksum; CCCITTChecksumReverse cksum;
cksum.update(buffer + 6U, RADIO_HEADER_LENGTH_BYTES - 2U); cksum.update(buffer + 6U, RADIO_HEADER_LENGTH_BYTES - 2U);
@ -453,7 +462,7 @@ bool CDVAPController::writeHeader(const CHeaderData& header)
m_framePos = 0U; m_framePos = 0U;
m_seq = 0U; m_seq = 0U;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = DVAP_HEADER_LEN; unsigned char len = DVAP_HEADER_LEN;
m_txData.addData(&len, 1U); m_txData.addData(&len, 1U);
@ -467,7 +476,7 @@ bool CDVAPController::writeData(const unsigned char* data, unsigned int, bool en
{ {
bool ret = m_txData.hasSpace(DVAP_GMSK_DATA_LEN + 1U); bool ret = m_txData.hasSpace(DVAP_GMSK_DATA_LEN + 1U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
@ -478,8 +487,8 @@ bool CDVAPController::writeData(const unsigned char* data, unsigned int, bool en
if (::memcmp(data + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES) == 0) if (::memcmp(data + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES) == 0)
m_framePos = 0U; m_framePos = 0U;
wxUint16 sid = wxUINT16_SWAP_ON_BE(m_streamId); uint16_t sid = htole16(m_streamId);
::memcpy(buffer + 2U, &sid, sizeof(wxUint16)); ::memcpy(buffer + 2U, &sid, sizeof(uint16_t));
buffer[4U] = m_framePos; buffer[4U] = m_framePos;
buffer[5U] = m_seq; buffer[5U] = m_seq;
@ -489,7 +498,7 @@ bool CDVAPController::writeData(const unsigned char* data, unsigned int, bool en
::memcpy(buffer + 6U, data, DV_FRAME_LENGTH_BYTES); ::memcpy(buffer + 6U, data, DV_FRAME_LENGTH_BYTES);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = DVAP_GMSK_DATA_LEN; unsigned char len = DVAP_GMSK_DATA_LEN;
m_txData.addData(&len, 1U); m_txData.addData(&len, 1U);
@ -545,14 +554,14 @@ bool CDVAPController::getName()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_NAME) { if (resp != RT_NAME) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The Dongle is not replying with its name")); wxLogError("The Dongle is not replying with its name");
return false; return false;
} }
} }
@ -560,7 +569,7 @@ bool CDVAPController::getName()
bool cmp = ::memcmp(m_buffer, DVAP_RESP_NAME, length) == 0; bool cmp = ::memcmp(m_buffer, DVAP_RESP_NAME, length) == 0;
if (!cmp) { if (!cmp) {
wxLogError(wxT("The Dongle is not responding as a DVAP")); wxLogError("The Dongle is not responding as a DVAP");
return false; return false;
} }
@ -579,21 +588,21 @@ bool CDVAPController::getFirmware()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_FIRMWARE) { if (resp != RT_FIRMWARE) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding with its firmware version")); wxLogError("The DVAP is not responding with its firmware version");
return false; return false;
} }
} }
} while (resp != RT_FIRMWARE); } while (resp != RT_FIRMWARE);
unsigned int version = m_buffer[6U] * 256U + m_buffer[5U]; unsigned int version = m_buffer[6U] * 256U + m_buffer[5U];
wxLogInfo(wxT("DVAP Firmware version: %u.%u"), version / 100U, version % 100U); wxLogInfo("DVAP Firmware version: %u.%u", version / 100U, version % 100U);
return true; return true;
} }
@ -610,21 +619,21 @@ bool CDVAPController::getSerial()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_SERIAL) { if (resp != RT_SERIAL) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding with its serial number")); wxLogError("The DVAP is not responding with its serial number");
return false; return false;
} }
} }
} while (resp != RT_SERIAL); } while (resp != RT_SERIAL);
wxString serial((char*)(m_buffer + 4U), wxConvLocal, length - 5U); std::string serial((char*)(m_buffer + 4U), length - 5U);
wxLogInfo(wxT("DVAP Serial number: %s"), serial.c_str()); wxLogInfo("DVAP Serial number: %s", serial.c_str());
return true; return true;
} }
@ -641,14 +650,14 @@ bool CDVAPController::startDVAP()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_START) { if (resp != RT_START) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the start command")); wxLogError("The DVAP is not responding to the start command");
return false; return false;
} }
} }
@ -669,14 +678,14 @@ bool CDVAPController::stopDVAP()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_STOP) { if (resp != RT_STOP) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the stop command")); wxLogError("The DVAP is not responding to the stop command");
return false; return false;
} }
} }
@ -697,14 +706,14 @@ bool CDVAPController::setModulation()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_MODULATION) { if (resp != RT_MODULATION) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the modulation command")); wxLogError("The DVAP is not responding to the modulation command");
return false; return false;
} }
} }
@ -725,14 +734,14 @@ bool CDVAPController::setMode()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_MODE) { if (resp != RT_MODE) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the mode command")); wxLogError("The DVAP is not responding to the mode command");
return false; return false;
} }
} }
@ -749,7 +758,7 @@ bool CDVAPController::setSquelch()
do { do {
unsigned char buffer[10U]; unsigned char buffer[10U];
::memcpy(buffer, DVAP_REQ_SQUELCH, DVAP_REQ_SQUELCH_LEN); ::memcpy(buffer, DVAP_REQ_SQUELCH, DVAP_REQ_SQUELCH_LEN);
::memcpy(buffer + 4U, &m_squelch, sizeof(wxInt8)); ::memcpy(buffer + 4U, &m_squelch, sizeof(int8_t));
int ret = m_serial.write(buffer, DVAP_REQ_SQUELCH_LEN); int ret = m_serial.write(buffer, DVAP_REQ_SQUELCH_LEN);
if (ret != int(DVAP_REQ_SQUELCH_LEN)) { if (ret != int(DVAP_REQ_SQUELCH_LEN)) {
@ -757,14 +766,14 @@ bool CDVAPController::setSquelch()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_SQUELCH) { if (resp != RT_SQUELCH) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the squelch command")); wxLogError("The DVAP is not responding to the squelch command");
return false; return false;
} }
} }
@ -782,8 +791,8 @@ bool CDVAPController::setPower()
unsigned char buffer[10U]; unsigned char buffer[10U];
::memcpy(buffer, DVAP_REQ_POWER, DVAP_REQ_POWER_LEN); ::memcpy(buffer, DVAP_REQ_POWER, DVAP_REQ_POWER_LEN);
wxInt16 power = wxINT16_SWAP_ON_BE(m_power); int16_t power = htole16(m_power);
::memcpy(buffer + 4U, &power, sizeof(wxInt16)); ::memcpy(buffer + 4U, &power, sizeof(int16_t));
int ret = m_serial.write(buffer, DVAP_REQ_POWER_LEN); int ret = m_serial.write(buffer, DVAP_REQ_POWER_LEN);
if (ret != int(DVAP_REQ_POWER_LEN)) { if (ret != int(DVAP_REQ_POWER_LEN)) {
@ -791,14 +800,14 @@ bool CDVAPController::setPower()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_POWER) { if (resp != RT_POWER) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the power command")); wxLogError("The DVAP is not responding to the power command");
return false; return false;
} }
} }
@ -819,29 +828,29 @@ bool CDVAPController::setFrequency()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_FREQLIMITS) { if (resp != RT_FREQLIMITS) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the frequency limits command")); wxLogError("The DVAP is not responding to the frequency limits command");
return false; return false;
} }
} }
} while (resp != RT_FREQLIMITS); } while (resp != RT_FREQLIMITS);
wxUint32* pFreq1 = (wxUint32*)(m_buffer + 4U); uint32_t* pFreq1 = (uint32_t*)(m_buffer + 4U);
wxUint32* pFreq2 = (wxUint32*)(m_buffer + 8U); uint32_t* pFreq2 = (uint32_t*)(m_buffer + 8U);
wxUint32 lower = wxUINT32_SWAP_ON_BE(*pFreq1); uint32_t lower = le32toh(*pFreq1);
wxUint32 upper = wxUINT32_SWAP_ON_BE(*pFreq2); uint32_t upper = le32toh(*pFreq2);
wxLogInfo(wxT("DVAP frequency limits are %u Hz to %u Hz"), lower, upper); wxLogInfo("DVAP frequency limits are %u Hz to %u Hz", lower, upper);
if (m_frequency < lower || m_frequency > upper) { if (m_frequency < lower || m_frequency > upper) {
wxLogError(wxT("The required frequency is out of the range of the DVAP hardware")); wxLogError("The required frequency is out of the range of the DVAP hardware");
m_serial.close(); m_serial.close();
return false; return false;
} }
@ -851,8 +860,8 @@ bool CDVAPController::setFrequency()
unsigned char buffer[10U]; unsigned char buffer[10U];
::memcpy(buffer, DVAP_REQ_FREQUENCY, DVAP_REQ_FREQUENCY_LEN); ::memcpy(buffer, DVAP_REQ_FREQUENCY, DVAP_REQ_FREQUENCY_LEN);
wxUint32 frequency = wxUINT32_SWAP_ON_BE(m_frequency); uint32_t frequency = htole32(m_frequency);
::memcpy(buffer + 4U, &frequency, sizeof(wxUint32)); ::memcpy(buffer + 4U, &frequency, sizeof(uint32_t));
int ret = m_serial.write(buffer, DVAP_REQ_FREQUENCY_LEN); int ret = m_serial.write(buffer, DVAP_REQ_FREQUENCY_LEN);
if (ret != int(DVAP_REQ_FREQUENCY_LEN)) { if (ret != int(DVAP_REQ_FREQUENCY_LEN)) {
@ -860,14 +869,14 @@ bool CDVAPController::setFrequency()
return false; return false;
} }
::wxMilliSleep(50UL); std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT_FREQUENCY) { if (resp != RT_FREQUENCY) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVAP is not responding to the frequency command")); wxLogError("The DVAP is not responding to the frequency command");
return false; return false;
} }
} }
@ -876,6 +885,16 @@ bool CDVAPController::setFrequency()
return true; return true;
} }
// Reads one complete DVAP binary frame.
//
// Wire format: [len_lo] [flags|len_hi] [cmd_lo] [cmd_hi] [payload...]
// Frame length = byte[0] + (byte[1] & 0x1F) * 256.
// Maximum sane length is capped at 50 bytes; anything larger indicates
// framing loss and triggers resync().
//
// The status packet (0x07 0x20 0x90 0x00 ...) occasionally arrives with
// its first byte missing from the UART FIFO; the workaround below detects
// the truncated signature {0x20, 0x90} and prepends the missing 0x07.
RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& length) RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& length)
{ {
int ret = m_serial.read(buffer, DVAP_HEADER_LENGTH); int ret = m_serial.read(buffer, DVAP_HEADER_LENGTH);
@ -898,7 +917,7 @@ RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& leng
// Check for silliness // Check for silliness
if (length > 50U) { if (length > 50U) {
CUtils::dump(wxT("Bad DVAP header"), buffer, DVAP_HEADER_LENGTH); CUtils::dump("Bad DVAP header", buffer, DVAP_HEADER_LENGTH);
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
dumpPackets(); dumpPackets();
#endif #endif
@ -912,11 +931,14 @@ RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& leng
return RT_ERROR; return RT_ERROR;
if (ret > 0) if (ret > 0)
offset += ret; offset += ret;
if (ret == 0) if (ret == 0) {
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (m_stopped)
return RT_TIMEOUT;
}
} }
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
if (::memcmp(buffer, DVAP_STATUS, 4U) == 0) if (::memcmp(buffer, DVAP_STATUS, 4U) == 0)
return RT_STATE; return RT_STATE;
@ -955,7 +977,7 @@ RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& leng
else if (::memcmp(buffer, DVAP_RESP_SQUELCH, 4U) == 0) else if (::memcmp(buffer, DVAP_RESP_SQUELCH, 4U) == 0)
return RT_SQUELCH; return RT_SQUELCH;
else { else {
CUtils::dump(wxT("Bad DVAP data"), buffer, length); CUtils::dump("Bad DVAP data", buffer, length);
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
dumpPackets(); dumpPackets();
#endif #endif
@ -964,17 +986,29 @@ RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& leng
} }
} }
// Discards bytes from the serial port one at a time, shifting them through a
// sliding window, until the 7-byte status packet signature is matched at
// bytes[0..3]. This re-aligns the byte boundary after a framing error.
void CDVAPController::resync() void CDVAPController::resync()
{ {
wxLogWarning(wxT("Resynchronising the DVAP data stream")); wxLogWarning("Resynchronising the DVAP data stream");
unsigned char data[DVAP_STATUS_LEN]; unsigned char data[DVAP_STATUS_LEN];
::memset(data, 0x00U, DVAP_STATUS_LEN); ::memset(data, 0x00U, DVAP_STATUS_LEN);
while (::memcmp(data, DVAP_STATUS, 4U) != 0) { while (::memcmp(data, DVAP_STATUS, 4U) != 0) {
if (m_stopped)
return;
unsigned char c; unsigned char c;
int n = m_serial.read(&c, 1U); int n = m_serial.read(&c, 1U);
if (n > 0) { if (n < 0)
return;
if (n == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
continue;
}
data[0U] = data[1U]; data[0U] = data[1U];
data[1U] = data[2U]; data[1U] = data[2U];
data[2U] = data[3U]; data[2U] = data[3U];
@ -983,11 +1017,10 @@ void CDVAPController::resync()
data[5U] = data[6U]; data[5U] = data[6U];
data[6U] = c; data[6U] = c;
// CUtils::dump(wxT("Resync buffer"), data, DVAP_STATUS_LEN); // CUtils::dump("Resync buffer", data, DVAP_STATUS_LEN);
}
} }
wxLogMessage(wxT("End resynchronising")); wxLogMessage("End resynchronising");
} }
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
@ -1004,22 +1037,15 @@ void CDVAPController::storePacket(const unsigned char* data, unsigned int length
void CDVAPController::dumpPackets() void CDVAPController::dumpPackets()
{ {
unsigned int n = m_dvapIndex; unsigned int n = (m_dvapIndex + 1U) % DVAP_DUMP_LENGTH;
unsigned int i = 0U; for (unsigned int i = 0U; i < DVAP_DUMP_LENGTH; i++) {
while (n != m_dvapIndex) {
if (m_dvapLength[n] > 0U) { if (m_dvapLength[n] > 0U) {
wxString text; char text[32];
text.Printf(wxT("Packet: %u"), i); ::snprintf(text, sizeof(text), "Packet: %u", i);
CUtils::dump(text, m_dvapData[n], m_dvapLength[n]); CUtils::dump(text, m_dvapData[n], m_dvapLength[n]);
} }
n = (n + 1U) % DVAP_DUMP_LENGTH;
i++;
n++;
if (n >= DVAP_DUMP_LENGTH)
n = 0U;
} }
m_dvapIndex = 0U; m_dvapIndex = 0U;
} }

@ -26,8 +26,10 @@
#include "HeaderData.h" #include "HeaderData.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
#include <cstdint>
enum RESP_TYPE { enum RESP_TYPE {
RT_TIMEOUT, RT_TIMEOUT,
@ -53,13 +55,44 @@ enum RESP_TYPE {
RT_FM_DATA RT_FM_DATA
}; };
/*
* CDVAPController - Driver for the DVAP USB dongle (Digital Voice Access Point).
*
* Hardware interface: USB CDC-ACM serial at 230400 baud.
*
* Protocol: Binary, little-endian framing.
* Every frame begins with a 2-byte header:
* byte[0] = total frame length (low byte)
* byte[1] = flags in upper 3 bits; length high bits in lower 5
* The command/type is encoded in bytes[2..3].
*
* Startup sequence (enforced in start()):
* getName() -> getFirmware() -> getSerial() ->
* setModulation() -> setMode() -> setSquelch() -> setPower() -> setFrequency() ->
* startDVAP()
*
* During operation (entry() thread):
* - Polls every ~2s by sending a 3-byte ACK (writePoll()).
* - The DVAP sends RT_STATE packets every ~20ms containing RSSI, squelch
* state, and TX buffer space; these are used to clock outbound data writes.
* - Received headers and GMSK voice frames are forwarded to m_rxData (the
* CModem shared RX ring buffer) as DSMTT_HEADER / DSMTT_DATA / DSMTT_EOT
* typed records.
* - If the DVAP spontaneously sends an FM_DATA packet it has lost GMSK lock;
* the driver restarts modulation to recover.
*
* Power: -12 to +10 dBm (int16_t, little-endian in wire frame).
* Squelch: -128 to -45 (int8_t, signed dBm threshold).
* Frequency: 32-bit Hz, validated against dongle-reported hardware limits.
*
* DVAP_DUMP build flag enables a circular packet trace buffer (last 30 frames)
* that is dumped to the log on any error or unexpected FM mode event.
*/
class CDVAPController : public CModem { class CDVAPController : public CModem {
public: public:
CDVAPController(const wxString& port, unsigned int frequency, int power, int squelch); CDVAPController(const std::string& port, unsigned int frequency, int power, int squelch);
virtual ~CDVAPController(); virtual ~CDVAPController();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -72,16 +105,18 @@ public:
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
private: private:
void entry();
CSerialDataController m_serial; CSerialDataController m_serial;
wxUint32 m_frequency; uint32_t m_frequency;
wxInt16 m_power; int16_t m_power;
wxInt8 m_squelch; int8_t m_squelch;
bool m_squelchOpen; std::atomic<bool> m_squelchOpen;
int m_signal; std::atomic<int> m_signal;
unsigned char* m_buffer; unsigned char* m_buffer;
wxUint16 m_streamId; uint16_t m_streamId;
wxUint8 m_framePos; uint8_t m_framePos;
wxUint8 m_seq; uint8_t m_seq;
CRingBuffer<unsigned char> m_txData; CRingBuffer<unsigned char> m_txData;
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)
unsigned char** m_dvapData; unsigned char** m_dvapData;
@ -103,10 +138,17 @@ private:
bool startDVAP(); bool startDVAP();
bool stopDVAP(); bool stopDVAP();
// Sends a 3-byte keepalive ACK; DVAP requires periodic traffic to stay active.
void writePoll(); void writePoll();
// Resynchronises the byte stream after a framing error by discarding bytes
// one at a time until a complete RT_STATE (status) header is found.
// Called when getResponse() receives an unrecognisable or oversized frame.
void resync(); void resync();
// Reads one complete binary frame from the serial port into buffer.
// Returns a RESP_TYPE identifying the frame kind, or RT_TIMEOUT if no
// data is available and RT_ERROR on a read failure.
RESP_TYPE getResponse(unsigned char* buffer, unsigned int& length); RESP_TYPE getResponse(unsigned char* buffer, unsigned int& length);
#if defined(DVAP_DUMP) #if defined(DVAP_DUMP)

@ -20,13 +20,19 @@
#include "DVMegaController.h" #include "DVMegaController.h"
#include "CCITTChecksum.h" #include "CCITTChecksum.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h" #include "Timer.h"
#include "Utils.h" #include "Utils.h"
#if defined(__WINDOWS__) #include <chrono>
#include <setupapi.h> #include <thread>
#else #include <cassert>
#include <wx/dir.h> #include <cstring>
#include <cstdio>
#include "EndianCompat.h"
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif #endif
const unsigned char DVRPTR_HEADER_LENGTH = 5U; const unsigned char DVRPTR_HEADER_LENGTH = 5U;
@ -57,7 +63,7 @@ const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CDVMegaController::CDVMegaController(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, unsigned int txDelay) : CDVMegaController::CDVMegaController(const std::string& port, const std::string& path, bool rxInvert, bool txInvert, unsigned int txDelay) :
CModem(), CModem(),
m_port(port), m_port(port),
m_path(path), m_path(path),
@ -68,7 +74,7 @@ m_rxFrequency(0U),
m_txFrequency(0U), m_txFrequency(0U),
m_power(0U), m_power(0U),
m_serial(port, SERIAL_115200, true), m_serial(port, SERIAL_115200, true),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_txCounter(0U), m_txCounter(0U),
m_pktCounter(0U), m_pktCounter(0U),
@ -77,12 +83,12 @@ m_txSpace(0U),
m_txEnabled(false), m_txEnabled(false),
m_checksum(false) m_checksum(false)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
} }
CDVMegaController::CDVMegaController(const wxString& port, const wxString& path, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power) : CDVMegaController::CDVMegaController(const std::string& port, const std::string& path, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power) :
CModem(), CModem(),
m_port(port), m_port(port),
m_path(path), m_path(path),
@ -93,7 +99,7 @@ m_rxFrequency(rxFrequency),
m_txFrequency(txFrequency), m_txFrequency(txFrequency),
m_power(power), m_power(power),
m_serial(port, SERIAL_115200, true), m_serial(port, SERIAL_115200, true),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_txCounter(0U), m_txCounter(0U),
m_pktCounter(0U), m_pktCounter(0U),
@ -102,10 +108,10 @@ m_txSpace(0U),
m_txEnabled(false), m_txEnabled(false),
m_checksum(false) m_checksum(false)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
wxASSERT((rxFrequency >= 144000000U && rxFrequency <= 148000000U) || assert((rxFrequency >= 144000000U && rxFrequency <= 148000000U) ||
(rxFrequency >= 420000000U && rxFrequency <= 450000000U)); (rxFrequency >= 420000000U && rxFrequency <= 450000000U));
wxASSERT((txFrequency >= 144000000U && txFrequency <= 148000000U) || assert((txFrequency >= 144000000U && txFrequency <= 148000000U) ||
(txFrequency >= 420000000U && txFrequency <= 450000000U)); (txFrequency >= 420000000U && txFrequency <= 450000000U));
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
@ -126,16 +132,14 @@ bool CDVMegaController::start()
findPath(); findPath();
Create(); m_thread = std::thread(&CDVMegaController::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CDVMegaController::Entry() void CDVMegaController::entry()
{ {
wxLogMessage(wxT("Starting DVMEGA Controller thread")); wxLogMessage("Starting DVMEGA Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U); CTimer pollTimer(200U, 0U, 100U);
@ -154,8 +158,9 @@ void* CDVMegaController::Entry()
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DVMEGA Controller thread")); wxLogMessage("Stopping DVMEGA Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
} }
@ -172,29 +177,30 @@ void* CDVMegaController::Entry()
case RTM_ERROR: { case RTM_ERROR: {
bool ret = findModem(); bool ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DVMEGA Controller thread")); wxLogMessage("Stopping DVMEGA Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
} }
break; break;
case RTM_RXPREAMBLE: case RTM_RXPREAMBLE:
// wxLogMessage(wxT("RT_PREAMBLE")); // wxLogMessage("RT_PREAMBLE");
break; break;
case RTM_START: case RTM_START:
// wxLogMessage(wxT("RT_START")); // wxLogMessage("RT_START");
break; break;
case RTM_HEADER: case RTM_HEADER:
CUtils::dump(wxT("RT_HEADER"), m_buffer, length); CUtils::dump("RT_HEADER", m_buffer, length);
if (length == 7U) { if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK) if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a header NAK from the DVMEGA")); wxLogWarning("Received a header NAK from the DVMEGA");
} else { } else {
bool correct = (m_buffer[5U] & 0x80U) == 0x00U; bool correct = (m_buffer[5U] & 0x80U) == 0x00U;
if (correct) { if (correct) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -209,16 +215,16 @@ void* CDVMegaController::Entry()
break; break;
case RTM_RXSYNC: case RTM_RXSYNC:
// wxLogMessage(wxT("RT_RXSYNC")); // wxLogMessage("RT_RXSYNC");
break; break;
case RTM_DATA: case RTM_DATA:
CUtils::dump(wxT("RT_DATA"), m_buffer, length); CUtils::dump("RT_DATA", m_buffer, length);
if (length == 7U) { if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK) if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a data NAK from the DVMEGA")); wxLogWarning("Received a data NAK from the DVMEGA");
} else { } else {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -232,8 +238,8 @@ void* CDVMegaController::Entry()
break; break;
case RTM_EOT: { case RTM_EOT: {
// wxLogMessage(wxT("RT_EOT")); // wxLogMessage("RT_EOT");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -245,8 +251,8 @@ void* CDVMegaController::Entry()
break; break;
case RTM_RXLOST: { case RTM_RXLOST: {
// wxLogMessage(wxT("RT_LOST")); // wxLogMessage("RT_LOST");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_LOST; data[0U] = DSMTT_LOST;
@ -263,8 +269,8 @@ void* CDVMegaController::Entry()
m_tx = (m_buffer[5U] & 0x02U) == 0x02U; m_tx = (m_buffer[5U] & 0x02U) == 0x02U;
m_txSpace = m_buffer[8U]; m_txSpace = m_buffer[8U];
space = m_txSpace - m_buffer[9U]; space = m_txSpace - m_buffer[9U];
// CUtils::dump(wxT("GET_STATUS"), m_buffer, length); // CUtils::dump("GET_STATUS", m_buffer, length);
// wxLogMessage(wxT("PTT=%d tx=%u space=%u cksum=%d, tx enabled=%d"), int(m_tx), m_txSpace, space, int(m_checksum), int(m_txEnabled)); // wxLogMessage("PTT=%d tx=%u space=%u cksum=%d, tx enabled=%d", int(m_tx), m_txSpace, space, int(m_checksum), int(m_txEnabled));
} }
break; break;
@ -275,14 +281,14 @@ void* CDVMegaController::Entry()
break; break;
default: default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[3U]); wxLogMessage("Unknown message, type: %02X", m_buffer[3U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length); CUtils::dump("Buffer dump", m_buffer, length);
break; break;
} }
if (space > 0U) { if (space > 0U) {
if (writeType == DSMTT_NONE && m_txData.hasData()) { if (writeType == DSMTT_NONE && m_txData.hasData()) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&writeType, 1U); m_txData.getData(&writeType, 1U);
m_txData.getData(&writeLength, 1U); m_txData.getData(&writeLength, 1U);
@ -291,53 +297,51 @@ void* CDVMegaController::Entry()
// Only send the start when the TX is off // Only send the start when the TX is off
if (!m_tx && writeType == DSMTT_START) { if (!m_tx && writeType == DSMTT_START) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the DVMEGA")); wxLogWarning("Error when writing the header to the DVMEGA");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space--; space--;
} }
if (space > 4U && writeType == DSMTT_HEADER) { if (space > 4U && writeType == DSMTT_HEADER) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the DVMEGA")); wxLogWarning("Error when writing the header to the DVMEGA");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space -= 4U; space -= 4U;
} }
if (writeType == DSMTT_DATA || writeType == DSMTT_EOT) { if (writeType == DSMTT_DATA || writeType == DSMTT_EOT) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength); // CUtils::dump("Write Data", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the DVMEGA")); wxLogWarning("Error when writing data to the DVMEGA");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space--; space--;
} }
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
} }
wxLogMessage(wxT("Stopping DVMEGA Controller thread")); wxLogMessage("Stopping DVMEGA Controller thread");
setEnabled(false); setEnabled(false);
delete[] writeBuffer; delete[] writeBuffer;
m_serial.close(); m_serial.close();
return NULL;
} }
bool CDVMegaController::writeHeader(const CHeaderData& header) bool CDVMegaController::writeHeader(const CHeaderData& header)
@ -347,7 +351,7 @@ bool CDVMegaController::writeHeader(const CHeaderData& header)
bool ret = m_txData.hasSpace(64U); bool ret = m_txData.hasSpace(64U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -397,25 +401,25 @@ bool CDVMegaController::writeHeader(const CHeaderData& header)
buffer2[9U] = header.getFlag2(); buffer2[9U] = header.getFlag2();
buffer2[10U] = header.getFlag3(); buffer2[10U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2.GetChar(i); buffer2[i + 11U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1.GetChar(i); buffer2[i + 19U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your.GetChar(i); buffer2[i + 27U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1.GetChar(i); buffer2[i + 35U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2.GetChar(i); buffer2[i + 43U] = my2[i];
CCCITTChecksumReverse cksum1; CCCITTChecksumReverse cksum1;
cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U); cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U);
@ -434,7 +438,7 @@ bool CDVMegaController::writeHeader(const CHeaderData& header)
m_pktCounter = 0U; m_pktCounter = 0U;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type1 = DSMTT_START; unsigned char type1 = DSMTT_START;
m_txData.addData(&type1, 1U); m_txData.addData(&type1, 1U);
@ -462,7 +466,7 @@ bool CDVMegaController::writeData(const unsigned char* data, unsigned int, bool
bool ret = m_txData.hasSpace(26U); bool ret = m_txData.hasSpace(26U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
@ -488,7 +492,7 @@ bool CDVMegaController::writeData(const unsigned char* data, unsigned int, bool
buffer[7U] = 0x0BU; buffer[7U] = 0x0BU;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_EOT; unsigned char type = DSMTT_EOT;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -532,7 +536,7 @@ bool CDVMegaController::writeData(const unsigned char* data, unsigned int, bool
buffer[23U] = 0x0BU; buffer[23U] = 0x0BU;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_DATA; unsigned char type = DSMTT_DATA;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -579,36 +583,43 @@ bool CDVMegaController::readVersion()
buffer[5U] = 0x0BU; buffer[5U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 6U); // CUtils::dump("Written", buffer, 6U);
int ret = m_serial.write(buffer, 6U); int ret = m_serial.write(buffer, 6U);
if (ret != 6) if (ret != 6)
return false; return false;
for (unsigned int count = 0U; count < MAX_RESPONSES; count++) { for (unsigned int count = 0U; count < MAX_RESPONSES; count++) {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
unsigned int length; unsigned int length;
RESP_TYPE_MEGA resp = getResponse(m_buffer, length); RESP_TYPE_MEGA resp = getResponse(m_buffer, length);
if (resp == RTM_GET_VERSION) { if (resp == RTM_GET_VERSION) {
wxString firmware; char firmware[32];
if ((m_buffer[4U] & 0x0FU) > 0x00U) if ((m_buffer[4U] & 0x0FU) > 0x00U)
firmware.Printf(wxT("%u.%u%u%c"), (m_buffer[5U] & 0xF0U) >> 4, m_buffer[5U] & 0x0FU, (m_buffer[4U] & 0xF0U) >> 4, (m_buffer[4U] & 0x0FU) + wxT('a') - 1U); ::snprintf(firmware, sizeof(firmware), "%u.%u%u%c",
(m_buffer[5U] & 0xF0U) >> 4,
m_buffer[5U] & 0x0FU,
(m_buffer[4U] & 0xF0U) >> 4,
(m_buffer[4U] & 0x0FU) + 'a' - 1U);
else else
firmware.Printf(wxT("%u.%u%u"), (m_buffer[5U] & 0xF0U) >> 4, m_buffer[5U] & 0x0FU, (m_buffer[4U] & 0xF0U) >> 4); ::snprintf(firmware, sizeof(firmware), "%u.%u%u",
(m_buffer[5U] & 0xF0U) >> 4,
m_buffer[5U] & 0x0FU,
(m_buffer[4U] & 0xF0U) >> 4);
wxString hardware((char*)(m_buffer + 6U), wxConvLocal, length - DVRPTR_HEADER_LENGTH - 3U); std::string hardware((char*)(m_buffer + 6U), length - DVRPTR_HEADER_LENGTH - 3U);
wxLogInfo(wxT("DVMEGA Firmware version: %s, hardware: %s"), firmware.c_str(), hardware.c_str()); wxLogInfo("DVMEGA Firmware version: %s, hardware: %s", firmware, hardware.c_str());
return true; return true;
} }
} }
::wxSleep(1); std::this_thread::sleep_for(std::chrono::seconds(1));
} }
wxLogError(wxT("Unable to read the firmware version after six attempts")); wxLogError("Unable to read the firmware version after six attempts");
return false; return false;
} }
@ -657,8 +668,8 @@ bool CDVMegaController::setConfig()
if (m_txInvert) if (m_txInvert)
buffer[6U] |= 0x02U; buffer[6U] |= 0x02U;
wxUint16* txDelay = (wxUint16*)(buffer + 8U); uint16_t txDelay = htole16((uint16_t)m_txDelay);
*txDelay = wxUINT16_SWAP_ON_BE(m_txDelay); ::memcpy(buffer + 8U, &txDelay, sizeof(uint16_t));
if (m_checksum) { if (m_checksum) {
CCCITTChecksum cksum; CCCITTChecksum cksum;
@ -669,7 +680,7 @@ bool CDVMegaController::setConfig()
buffer[11U] = 0x0BU; buffer[11U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 12U); // CUtils::dump("Written", buffer, 12U);
int ret = m_serial.write(buffer, 12U); int ret = m_serial.write(buffer, 12U);
if (ret != 12) if (ret != 12)
@ -680,24 +691,24 @@ bool CDVMegaController::setConfig()
RESP_TYPE_MEGA resp; RESP_TYPE_MEGA resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RTM_SET_CONFIG) { if (resp != RTM_SET_CONFIG) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVMEGA is not responding to the SET_CONFIG command")); wxLogError("The DVMEGA is not responding to the SET_CONFIG command");
return false; return false;
} }
} }
} while (resp != RTM_SET_CONFIG); } while (resp != RTM_SET_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U]; unsigned char type = m_buffer[4U];
if (type != DVRPTR_ACK) { if (type != DVRPTR_ACK) {
wxLogError(wxT("Received a NAK to the SET_CONFIG command from the modem")); wxLogError("Received a NAK to the SET_CONFIG command from the modem");
return false; return false;
} }
@ -721,11 +732,11 @@ bool CDVMegaController::setFrequencyAndPower()
buffer[5U] = 0x0CU; // Block length buffer[5U] = 0x0CU; // Block length
wxUint32 rxFreq = wxUINT32_SWAP_ON_BE(wxUint32(m_rxFrequency)); uint32_t rxFreq = htole32((uint32_t)m_rxFrequency);
wxUint32 txFreq = wxUINT32_SWAP_ON_BE(wxUint32(m_txFrequency)); uint32_t txFreq = htole32((uint32_t)m_txFrequency);
::memcpy(buffer + 7U, &rxFreq, sizeof(wxUint32)); ::memcpy(buffer + 7U, &rxFreq, sizeof(uint32_t));
::memcpy(buffer + 11U, &txFreq, sizeof(wxUint32)); ::memcpy(buffer + 11U, &txFreq, sizeof(uint32_t));
buffer[16U] = (m_power * 64U) / 100U; buffer[16U] = (m_power * 64U) / 100U;
@ -738,7 +749,7 @@ bool CDVMegaController::setFrequencyAndPower()
buffer[20U] = 0x0BU; buffer[20U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 21U); // CUtils::dump("Written", buffer, 21U);
int ret = m_serial.write(buffer, 21U); int ret = m_serial.write(buffer, 21U);
if (ret != 21) if (ret != 21)
@ -749,24 +760,24 @@ bool CDVMegaController::setFrequencyAndPower()
RESP_TYPE_MEGA resp; RESP_TYPE_MEGA resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RTM_SET_CONFIG) { if (resp != RTM_SET_CONFIG) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVMEGA is not responding to the SET_CONFIG command")); wxLogError("The DVMEGA is not responding to the SET_CONFIG command");
return false; return false;
} }
} }
} while (resp != RTM_SET_CONFIG); } while (resp != RTM_SET_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U]; unsigned char type = m_buffer[4U];
if (type != DVRPTR_ACK) { if (type != DVRPTR_ACK) {
wxLogError(wxT("Received a NAK to the SET_CONFIG command from the modem")); wxLogError("Received a NAK to the SET_CONFIG command from the modem");
return false; return false;
} }
@ -799,7 +810,7 @@ bool CDVMegaController::setEnabled(bool enable)
buffer[6U] = 0x0BU; buffer[6U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 7U); // CUtils::dump("Written", buffer, 7U);
int ret = m_serial.write(buffer, 7U); int ret = m_serial.write(buffer, 7U);
if (ret != 7) if (ret != 7)
@ -809,36 +820,41 @@ bool CDVMegaController::setEnabled(bool enable)
unsigned int length; unsigned int length;
RESP_TYPE_MEGA resp; RESP_TYPE_MEGA resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RTM_GET_STATUS) { if (resp != RTM_GET_STATUS) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DVMEGA is not responding to the SET_STATUS command")); wxLogError("The DVMEGA is not responding to the SET_STATUS command");
return false; return false;
} }
} }
} while (resp != RTM_GET_STATUS); } while (resp != RTM_GET_STATUS);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U]; unsigned char type = m_buffer[4U];
if (type != DVRPTR_ACK) { if (type != DVRPTR_ACK) {
wxLogError(wxT("Received a NAK to the SET_STATUS command from the modem")); wxLogError("Received a NAK to the SET_STATUS command from the modem");
return false; return false;
} }
return true; return true;
} }
// Reads one complete DVRPTR frame.
// Frame header (5 bytes): [0xD0] [len_lo] [len_hi] [type] [...]
// length = header bytes 1+2 (payload length, not including header itself).
// type has bit 7 set for board responses; strip it before dispatching.
// Returns RTM_TIMEOUT if no start byte is available, RTM_ERROR on I/O failure.
RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned int& length) RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned int& length)
{ {
// Get the start of the frame or nothing at all // Get the start of the frame or nothing at all
int ret = m_serial.read(buffer, 1U); int ret = m_serial.read(buffer, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DVMEGA")); wxLogError("Error when reading from the DVMEGA");
return RTM_ERROR; return RTM_ERROR;
} }
@ -853,21 +869,24 @@ RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned in
while (offset < DVRPTR_HEADER_LENGTH) { while (offset < DVRPTR_HEADER_LENGTH) {
ret = m_serial.read(buffer + offset, DVRPTR_HEADER_LENGTH - offset); ret = m_serial.read(buffer + offset, DVRPTR_HEADER_LENGTH - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DVMEGA")); wxLogError("Error when reading from the DVMEGA");
return RTM_ERROR; return RTM_ERROR;
} }
if (ret > 0) if (ret > 0)
offset += ret; offset += ret;
if (ret == 0) if (ret == 0) {
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (m_stopped)
return RTM_TIMEOUT;
}
} }
length = buffer[1U] + buffer[2U] * 256U; length = buffer[1U] + buffer[2U] * 256U;
if (length >= 100U) { if (length >= 100U) {
wxLogError(wxT("Invalid data received from the DVMEGA")); wxLogError("Invalid data received from the DVMEGA");
return RTM_ERROR; return RTM_ERROR;
} }
@ -879,20 +898,23 @@ RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned in
while (offset < length) { while (offset < length) {
ret = m_serial.read(buffer + offset + DVRPTR_HEADER_LENGTH, length - offset); ret = m_serial.read(buffer + offset + DVRPTR_HEADER_LENGTH, length - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DVMEGA")); wxLogError("Error when reading from the DVMEGA");
return RTM_ERROR; return RTM_ERROR;
} }
if (ret > 0) if (ret > 0)
offset += ret; offset += ret;
if (ret == 0) if (ret == 0) {
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (m_stopped)
return RTM_TIMEOUT;
}
} }
length += DVRPTR_HEADER_LENGTH; length += DVRPTR_HEADER_LENGTH;
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
switch (type) { switch (type) {
case DVRPTR_GET_STATUS: case DVRPTR_GET_STATUS:
@ -926,149 +948,100 @@ RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned in
} }
} }
wxString CDVMegaController::getPath() const std::string CDVMegaController::getPath() const
{ {
return m_path; return m_path;
} }
bool CDVMegaController::findPort() bool CDVMegaController::findPort()
{ {
if (m_path.IsEmpty()) if (m_path.empty())
return false; return false;
#if defined(__WINDOWS__) DIR* dir = ::opendir("/sys/class/tty");
#else if (dir == nullptr) {
wxDir dir; wxLogError("Cannot open directory /sys/class/tty");
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
return false; return false;
} }
wxString fileName; struct dirent* entry;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*")); while ((entry = ::readdir(dir)) != nullptr) {
while (ret1) { std::string fileName(entry->d_name);
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
char cpath[255U]; // Match ttyACM* entries
::memset(cpath, 0x00U, 255U); if (fileName.substr(0, 6) != "ttyACM")
continue;
for (unsigned int i = 0U; i < path.Len(); i++) std::string path = "/sys/class/tty/" + fileName;
cpath[i] = path.GetChar(i);
char cpath[255U];
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U]; char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U); int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, 255U); ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
::closedir(dir);
return false; return false;
} }
symlink[ret2] = '\0';
path = wxString(symlink, wxConvLocal, ret2); path = std::string(symlink, ret2);
} else { } else {
symlink[ret2] = '\0';
std::string fullPath(symlink, ret2);
// Get all but the last section // Get all but the last section
wxString fullPath = wxString(symlink, wxConvLocal, ret2); size_t pos = fullPath.rfind('/');
path = fullPath.BeforeLast(wxT('/')); path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (path.IsSameAs(m_path)) { if (path == m_path) {
m_port.Printf(wxT("/dev/%s"), fileName.c_str()); m_port = "/dev/" + fileName;
wxLogMessage(wxT("Found modem port of %s based on the path"), m_port.c_str()); wxLogMessage("Found modem port of %s based on the path", m_port.c_str());
::closedir(dir);
return true; return true;
} }
ret1 = dir.GetNext(&fileName);
} }
#endif
::closedir(dir);
return false; return false;
} }
bool CDVMegaController::findPath() bool CDVMegaController::findPath()
{ {
#if defined(__WINDOWS__) std::string path = "/sys/class/tty/" + m_port.substr(5U);
#ifdef notdef
GUID guids[5U];
DWORD count;
BOOL res = ::SetupDiClassGuidsFromName(L"Multifunction", guids, 5U, &count);
if (!res) {
wxLogError(wxT("Error from SetupDiClassGuidsFromName: err=%u"), ::GetLastError());
return false;
}
for (DWORD i = 0U; i < count; i++) {
HDEVINFO devInfo = ::SetupDiGetClassDevs(&guids[i], NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%u"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &guids[i], index, &devInfoData); index++) {
// Find the required length of the device structure
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// Get the detailed data into the newly allocated device structure
DWORD required;
res = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!res) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::free(detailData);
}
::SetupDiDestroyDeviceInfoList(devInfo);
}
return false;
#endif
#else
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), m_port.Mid(5U).c_str());
char cpath[255U]; char cpath[255U];
::memset(cpath, 0x00U, 255U); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char symlink[255U]; char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U); int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, 255U); ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
return false; return false;
} }
symlink[ret] = '\0';
path = wxString(symlink, wxConvLocal, ret); path = std::string(symlink, ret);
} else { } else {
wxString fullPath = wxString(symlink, wxConvLocal, ret); symlink[ret] = '\0';
path = fullPath.BeforeLast(wxT('/')); std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (m_path.IsEmpty()) if (m_path.empty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str()); wxLogMessage("Found modem path of %s", path.c_str());
m_path = path; m_path = path;
#endif
return true; return true;
} }
@ -1079,7 +1052,7 @@ bool CDVMegaController::findModem()
// Tell the repeater that the signal has gone away // Tell the repeater that the signal has gone away
if (m_rx) { if (m_rx) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -1095,7 +1068,7 @@ bool CDVMegaController::findModem()
while (!m_stopped) { while (!m_stopped) {
count++; count++;
if (count >= 4U) { if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem")); wxLogMessage("Trying to reopen the modem");
bool ret = findPort(); bool ret = findPort();
if (ret) { if (ret) {
@ -1107,7 +1080,7 @@ bool CDVMegaController::findModem()
count = 0U; count = 0U;
} }
Sleep(500UL); std::this_thread::sleep_for(std::chrono::milliseconds(500));
} }
return false; return false;
@ -1147,4 +1120,3 @@ bool CDVMegaController::openModem()
return true; return true;
} }

@ -23,8 +23,9 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
enum RESP_TYPE_MEGA { enum RESP_TYPE_MEGA {
RTM_TIMEOUT, RTM_TIMEOUT,
@ -45,14 +46,47 @@ enum RESP_TYPE_MEGA {
RTM_SET_TESTMDE RTM_SET_TESTMDE
}; };
/*
* CDVMegaController - Driver for DV-Mega boards (modem and radio variants).
*
* Hardware interface: USB CDC-ACM serial at 115200 baud.
*
* Two hardware variants, selected by constructor:
* 1. Modem variant (port, path, rxInvert, txInvert, txDelay)
* The board is a bare GMSK modem; the host radio provides the RF.
* rxInvert/txInvert flip the baseband polarity to match the radio's
* discriminator/modulator wiring.
* 2. Radio variant (port, path, txDelay, rxFrequency, txFrequency, power)
* The board includes a UHF/VHF transceiver. Frequency and power are
* programmed at startup via a second SET_CONFIG (RF layer) command.
* rxFrequency != 0 is the distinguishing condition checked in openModem().
*
* Protocol: DVRPTR binary framing (shared with DV-RPTR V1).
* Frame structure: [0xD0] [len_lo] [len_hi] [type] [txCount] [pktCount]
* [payload...] [checksum_lo] [checksum_hi]
* The type byte has bit 7 set in responses from the board (stripped with & 0x7F).
* Checksum is either CCITT-16 (when m_checksum is true, reported in GET_STATUS)
* or a fixed sentinel 0x000B when the firmware does not support checksums.
*
* Reconnection (findModem()):
* On any serial error the port is closed. An in-progress RX stream is
* terminated with a synthetic DSMTT_EOT. The driver then polls every 2s
* (4 x 500ms sleep) searching for the device by USB sysfs path before
* reopening and re-initialising.
*
* Sysfs path tracking (findPort / findPath):
* m_path is the stable USB device path (e.g. /sys/devices/platform/.../1-1.2).
* This survives port renumbering across reconnects. findPort() walks
* /sys/class/tty/ttyACM* to find which /dev node currently maps to that path.
*/
class CDVMegaController : public CModem { class CDVMegaController : public CModem {
public: public:
CDVMegaController(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, unsigned int txDelay); // Modem-only variant: no integrated RF, host radio provides the signal.
CDVMegaController(const wxString& port, const wxString& path, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power); CDVMegaController(const std::string& port, const std::string& path, bool rxInvert, bool txInvert, unsigned int txDelay);
// Radio variant: integrated transceiver; frequency in Hz, power in percent.
CDVMegaController(const std::string& port, const std::string& path, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power);
virtual ~CDVMegaController(); virtual ~CDVMegaController();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -61,11 +95,13 @@ public:
virtual bool writeHeader(const CHeaderData& header); virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const; virtual std::string getPath() const;
private: private:
wxString m_port; void entry();
wxString m_path;
std::string m_port;
std::string m_path;
bool m_rxInvert; bool m_rxInvert;
bool m_txInvert; bool m_txInvert;
unsigned int m_txDelay; unsigned int m_txDelay;
@ -93,9 +129,12 @@ private:
bool findPort(); bool findPort();
bool findPath(); bool findPath();
// Closes the port, signals EOT if mid-stream, then polls every 2s until
// findPort() + openModem() succeed or m_stopped is set.
bool findModem(); bool findModem();
// Opens the serial port, sends GET_VERSION, SET_CONFIG (physical layer),
// optionally SET_CONFIG (RF layer for radio variant), then setEnabled(true).
bool openModem(); bool openModem();
}; };
#endif #endif

@ -20,12 +20,18 @@
#include "DVRPTRV1Controller.h" #include "DVRPTRV1Controller.h"
#include "CCITTChecksum.h" #include "CCITTChecksum.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h" #include "Timer.h"
#if defined(__WINDOWS__) #include <chrono>
#include <setupapi.h> #include <thread>
#else #include <cassert>
#include <wx/dir.h> #include <cstring>
#include <cstdio>
#include "EndianCompat.h"
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif #endif
const unsigned char DVRPTR_HEADER_LENGTH = 5U; const unsigned char DVRPTR_HEADER_LENGTH = 5U;
@ -56,7 +62,7 @@ const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CDVRPTRV1Controller::CDVRPTRV1Controller(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, bool channel, unsigned int modLevel, unsigned int txDelay) : CDVRPTRV1Controller::CDVRPTRV1Controller(const std::string& port, const std::string& path, bool rxInvert, bool txInvert, bool channel, unsigned int modLevel, unsigned int txDelay) :
CModem(), CModem(),
m_port(port), m_port(port),
m_path(path), m_path(path),
@ -66,7 +72,7 @@ m_channel(channel),
m_modLevel(modLevel), m_modLevel(modLevel),
m_txDelay(txDelay), m_txDelay(txDelay),
m_serial(port, SERIAL_115200), m_serial(port, SERIAL_115200),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_txCounter(0U), m_txCounter(0U),
m_pktCounter(0U), m_pktCounter(0U),
@ -75,7 +81,7 @@ m_txSpace(0U),
m_txEnabled(false), m_txEnabled(false),
m_checksum(false) m_checksum(false)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
} }
@ -95,16 +101,14 @@ bool CDVRPTRV1Controller::start()
findPath(); findPath();
Create(); m_thread = std::thread(&CDVRPTRV1Controller::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CDVRPTRV1Controller::Entry() void CDVRPTRV1Controller::entry()
{ {
wxLogMessage(wxT("Starting DV-RPTR1 Modem Controller thread")); wxLogMessage("Starting DV-RPTR1 Modem Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U); CTimer pollTimer(200U, 0U, 100U);
@ -123,8 +127,9 @@ void* CDVRPTRV1Controller::Entry()
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR1 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR1 Modem Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
} }
@ -141,29 +146,30 @@ void* CDVRPTRV1Controller::Entry()
case RT1_ERROR: { case RT1_ERROR: {
bool ret = findModem(); bool ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR1 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR1 Modem Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
} }
break; break;
case RT1_RXPREAMBLE: case RT1_RXPREAMBLE:
// wxLogMessage(wxT("RT_PREAMBLE")); // wxLogMessage("RT_PREAMBLE");
break; break;
case RT1_START: case RT1_START:
// wxLogMessage(wxT("RT_START")); // wxLogMessage("RT_START");
break; break;
case RT1_HEADER: case RT1_HEADER:
// CUtils::dump(wxT("RT_HEADER"), m_buffer, length); // CUtils::dump("RT_HEADER", m_buffer, length);
if (length == 7U) { if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK) if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a header NAK from the modem")); wxLogWarning("Received a header NAK from the modem");
} else { } else {
bool correct = (m_buffer[5U] & 0x80U) == 0x00U; bool correct = (m_buffer[5U] & 0x80U) == 0x00U;
if (correct) { if (correct) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -178,16 +184,16 @@ void* CDVRPTRV1Controller::Entry()
break; break;
case RT1_RXSYNC: case RT1_RXSYNC:
// wxLogMessage(wxT("RT_RXSYNC")); // wxLogMessage("RT_RXSYNC");
break; break;
case RT1_DATA: case RT1_DATA:
// CUtils::dump(wxT("RT_DATA"), m_buffer, length); // CUtils::dump("RT_DATA", m_buffer, length);
if (length == 7U) { if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK) if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a data NAK from the modem")); wxLogWarning("Received a data NAK from the modem");
} else { } else {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -201,8 +207,8 @@ void* CDVRPTRV1Controller::Entry()
break; break;
case RT1_EOT: { case RT1_EOT: {
// wxLogMessage(wxT("RT_EOT")); // wxLogMessage("RT_EOT");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -214,8 +220,8 @@ void* CDVRPTRV1Controller::Entry()
break; break;
case RT1_RXLOST: { case RT1_RXLOST: {
// wxLogMessage(wxT("RT_LOST")); // wxLogMessage("RT_LOST");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_LOST; data[0U] = DSMTT_LOST;
@ -232,8 +238,8 @@ void* CDVRPTRV1Controller::Entry()
m_tx = (m_buffer[5U] & 0x02U) == 0x02U; m_tx = (m_buffer[5U] & 0x02U) == 0x02U;
m_txSpace = m_buffer[8U]; m_txSpace = m_buffer[8U];
space = m_txSpace - m_buffer[9U]; space = m_txSpace - m_buffer[9U];
// CUtils::dump(wxT("GET_STATUS"), m_buffer, length); // CUtils::dump("GET_STATUS", m_buffer, length);
// wxLogMessage(wxT("PTT=%d tx=%u space=%u cksum=%d, tx enabled=%d"), int(m_tx), m_txSpace, space, int(m_checksum), int(m_txEnabled)); // wxLogMessage("PTT=%d tx=%u space=%u cksum=%d, tx enabled=%d", int(m_tx), m_txSpace, space, int(m_checksum), int(m_txEnabled));
} }
break; break;
@ -244,14 +250,14 @@ void* CDVRPTRV1Controller::Entry()
break; break;
default: default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[3U]); wxLogMessage("Unknown message, type: %02X", m_buffer[3U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length); CUtils::dump("Buffer dump", m_buffer, length);
break; break;
} }
if (space > 0U) { if (space > 0U) {
if (writeType == DSMTT_NONE && m_txData.hasData()) { if (writeType == DSMTT_NONE && m_txData.hasData()) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&writeType, 1U); m_txData.getData(&writeType, 1U);
m_txData.getData(&writeLength, 1U); m_txData.getData(&writeLength, 1U);
@ -260,53 +266,51 @@ void* CDVRPTRV1Controller::Entry()
// Only send the start when the TX is off // Only send the start when the TX is off
if (!m_tx && writeType == DSMTT_START) { if (!m_tx && writeType == DSMTT_START) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the DV-RPTR modem")); wxLogWarning("Error when writing the header to the DV-RPTR modem");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space--; space--;
} }
if (space > 4U && writeType == DSMTT_HEADER) { if (space > 4U && writeType == DSMTT_HEADER) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the DV-RPTR modem")); wxLogWarning("Error when writing the header to the DV-RPTR modem");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space -= 4U; space -= 4U;
} }
if (writeType == DSMTT_DATA || writeType == DSMTT_EOT) { if (writeType == DSMTT_DATA || writeType == DSMTT_EOT) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength); // CUtils::dump("Write Data", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the DV-RPTR modem")); wxLogWarning("Error when writing data to the DV-RPTR modem");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space--; space--;
} }
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
} }
wxLogMessage(wxT("Stopping DV-RPTR1 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR1 Modem Controller thread");
setEnabled(false); setEnabled(false);
delete[] writeBuffer; delete[] writeBuffer;
m_serial.close(); m_serial.close();
return NULL;
} }
bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header) bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
@ -316,7 +320,7 @@ bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
bool ret = m_txData.hasSpace(64U); bool ret = m_txData.hasSpace(64U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -366,25 +370,25 @@ bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
buffer2[9U] = header.getFlag2(); buffer2[9U] = header.getFlag2();
buffer2[10U] = header.getFlag3(); buffer2[10U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2.GetChar(i); buffer2[i + 11U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1.GetChar(i); buffer2[i + 19U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your.GetChar(i); buffer2[i + 27U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1.GetChar(i); buffer2[i + 35U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2.GetChar(i); buffer2[i + 43U] = my2[i];
CCCITTChecksumReverse cksum1; CCCITTChecksumReverse cksum1;
cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U); cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U);
@ -403,7 +407,7 @@ bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
m_pktCounter = 0U; m_pktCounter = 0U;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type1 = DSMTT_START; unsigned char type1 = DSMTT_START;
m_txData.addData(&type1, 1U); m_txData.addData(&type1, 1U);
@ -431,7 +435,7 @@ bool CDVRPTRV1Controller::writeData(const unsigned char* data, unsigned int, boo
bool ret = m_txData.hasSpace(26U); bool ret = m_txData.hasSpace(26U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
@ -457,7 +461,7 @@ bool CDVRPTRV1Controller::writeData(const unsigned char* data, unsigned int, boo
buffer[7U] = 0x0BU; buffer[7U] = 0x0BU;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_EOT; unsigned char type = DSMTT_EOT;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -501,7 +505,7 @@ bool CDVRPTRV1Controller::writeData(const unsigned char* data, unsigned int, boo
buffer[23U] = 0x0BU; buffer[23U] = 0x0BU;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_DATA; unsigned char type = DSMTT_DATA;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -547,7 +551,7 @@ bool CDVRPTRV1Controller::readVersion()
buffer[5U] = 0x0BU; buffer[5U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 6U); // CUtils::dump("Written", buffer, 6U);
int ret = m_serial.write(buffer, 6U); int ret = m_serial.write(buffer, 6U);
if (ret != 6) if (ret != 6)
@ -557,28 +561,35 @@ bool CDVRPTRV1Controller::readVersion()
unsigned int length; unsigned int length;
RESP_TYPE_V1 resp; RESP_TYPE_V1 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT1_GET_VERSION) { if (resp != RT1_GET_VERSION) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the version command")); wxLogError("The DV-RPTR modem is not responding to the version command");
return false; return false;
} }
} }
} while (resp != RT1_GET_VERSION); } while (resp != RT1_GET_VERSION);
wxString firmware; char firmware[32];
if ((m_buffer[4U] & 0x0FU) > 0x00U) if ((m_buffer[4U] & 0x0FU) > 0x00U)
firmware.Printf(wxT("%u.%u%u%c"), (m_buffer[5U] & 0xF0U) >> 4, m_buffer[5U] & 0x0FU, (m_buffer[4U] & 0xF0U) >> 4, (m_buffer[4U] & 0x0FU) + wxT('a') - 1U); ::snprintf(firmware, sizeof(firmware), "%u.%u%u%c",
(m_buffer[5U] & 0xF0U) >> 4,
m_buffer[5U] & 0x0FU,
(m_buffer[4U] & 0xF0U) >> 4,
(m_buffer[4U] & 0x0FU) + 'a' - 1U);
else else
firmware.Printf(wxT("%u.%u%u"), (m_buffer[5U] & 0xF0U) >> 4, m_buffer[5U] & 0x0FU, (m_buffer[4U] & 0xF0U) >> 4); ::snprintf(firmware, sizeof(firmware), "%u.%u%u",
(m_buffer[5U] & 0xF0U) >> 4,
m_buffer[5U] & 0x0FU,
(m_buffer[4U] & 0xF0U) >> 4);
wxString hardware((char*)(m_buffer + 6U), wxConvLocal, length - DVRPTR_HEADER_LENGTH - 3U); std::string hardware((char*)(m_buffer + 6U), length - DVRPTR_HEADER_LENGTH - 3U);
wxLogInfo(wxT("DV-RPTR Modem Firmware version: %s, hardware: %s"), firmware.c_str(), hardware.c_str()); wxLogInfo("DV-RPTR Modem Firmware version: %s, hardware: %s", firmware, hardware.c_str());
return true; return true;
} }
@ -606,6 +617,10 @@ bool CDVRPTRV1Controller::readStatus()
return m_serial.write(buffer, 6U) == 6; return m_serial.write(buffer, 6U) == 6;
} }
// Sends SET_CONFIG to the DV-RPTR V1 (physical layer, block type 0xC0).
// buffer[6] flags: bit0 = rxInvert, bit1 = txInvert, bit2 = channel select.
// buffer[7] modLevel: 0-100% mapped to 0-255; controls DAC output amplitude.
// buffer[8..9] txDelay: uint16_t in ms; time between PTT assert and first bit.
bool CDVRPTRV1Controller::setConfig() bool CDVRPTRV1Controller::setConfig()
{ {
unsigned char buffer[20U]; unsigned char buffer[20U];
@ -631,8 +646,8 @@ bool CDVRPTRV1Controller::setConfig()
buffer[7U] = (m_modLevel * 256U) / 100U; buffer[7U] = (m_modLevel * 256U) / 100U;
wxUint16* txDelay = (wxUint16*)(buffer + 8U); uint16_t txDelay = htole16((uint16_t)m_txDelay);
*txDelay = wxUINT16_SWAP_ON_BE(m_txDelay); ::memcpy(buffer + 8U, &txDelay, sizeof(uint16_t));
if (m_checksum) { if (m_checksum) {
CCCITTChecksum cksum; CCCITTChecksum cksum;
@ -643,7 +658,7 @@ bool CDVRPTRV1Controller::setConfig()
buffer[11U] = 0x0BU; buffer[11U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 12U); // CUtils::dump("Written", buffer, 12U);
int ret = m_serial.write(buffer, 12U); int ret = m_serial.write(buffer, 12U);
if (ret != 12) if (ret != 12)
@ -654,24 +669,24 @@ bool CDVRPTRV1Controller::setConfig()
RESP_TYPE_V1 resp; RESP_TYPE_V1 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT1_SET_CONFIG) { if (resp != RT1_SET_CONFIG) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the SET_CONFIG command")); wxLogError("The DV-RPTR modem is not responding to the SET_CONFIG command");
return false; return false;
} }
} }
} while (resp != RT1_SET_CONFIG); } while (resp != RT1_SET_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U]; unsigned char type = m_buffer[4U];
if (type != DVRPTR_ACK) { if (type != DVRPTR_ACK) {
wxLogError(wxT("Received a NAK to the SET_CONFIG command from the modem")); wxLogError("Received a NAK to the SET_CONFIG command from the modem");
return false; return false;
} }
@ -704,7 +719,7 @@ bool CDVRPTRV1Controller::setEnabled(bool enable)
buffer[6U] = 0x0BU; buffer[6U] = 0x0BU;
} }
// CUtils::dump(wxT("Written"), buffer, 7U); // CUtils::dump("Written", buffer, 7U);
int ret = m_serial.write(buffer, 7U); int ret = m_serial.write(buffer, 7U);
if (ret != 7) if (ret != 7)
@ -714,24 +729,24 @@ bool CDVRPTRV1Controller::setEnabled(bool enable)
unsigned int length; unsigned int length;
RESP_TYPE_V1 resp; RESP_TYPE_V1 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT1_GET_STATUS) { if (resp != RT1_GET_STATUS) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the SET_STATUS command")); wxLogError("The DV-RPTR modem is not responding to the SET_STATUS command");
return false; return false;
} }
} }
} while (resp != RT1_GET_STATUS); } while (resp != RT1_GET_STATUS);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U]; unsigned char type = m_buffer[4U];
if (type != DVRPTR_ACK) { if (type != DVRPTR_ACK) {
wxLogError(wxT("Received a NAK to the SET_STATUS command from the modem")); wxLogError("Received a NAK to the SET_STATUS command from the modem");
return false; return false;
} }
@ -743,7 +758,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
// Get the start of the frame or nothing at all // Get the start of the frame or nothing at all
int ret = m_serial.read(buffer, 1U); int ret = m_serial.read(buffer, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT1_ERROR; return RT1_ERROR;
} }
@ -758,7 +773,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < DVRPTR_HEADER_LENGTH) { while (offset < DVRPTR_HEADER_LENGTH) {
ret = m_serial.read(buffer + offset, DVRPTR_HEADER_LENGTH - offset); ret = m_serial.read(buffer + offset, DVRPTR_HEADER_LENGTH - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT1_ERROR; return RT1_ERROR;
} }
@ -766,7 +781,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret; offset += ret;
if (ret == 0) if (ret == 0)
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
} }
length = buffer[1U] + buffer[2U] * 256U; length = buffer[1U] + buffer[2U] * 256U;
@ -779,7 +794,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < length) { while (offset < length) {
ret = m_serial.read(buffer + offset + DVRPTR_HEADER_LENGTH, length - offset); ret = m_serial.read(buffer + offset + DVRPTR_HEADER_LENGTH, length - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT1_ERROR; return RT1_ERROR;
} }
@ -787,12 +802,12 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret; offset += ret;
if (ret == 0) if (ret == 0)
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
} }
length += DVRPTR_HEADER_LENGTH; length += DVRPTR_HEADER_LENGTH;
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
switch (type) { switch (type) {
case DVRPTR_GET_STATUS: case DVRPTR_GET_STATUS:
@ -826,149 +841,100 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
} }
} }
wxString CDVRPTRV1Controller::getPath() const std::string CDVRPTRV1Controller::getPath() const
{ {
return m_path; return m_path;
} }
bool CDVRPTRV1Controller::findPort() bool CDVRPTRV1Controller::findPort()
{ {
if (m_path.IsEmpty()) if (m_path.empty())
return false; return false;
#if defined(__WINDOWS__) DIR* dir = ::opendir("/sys/class/tty");
#else if (dir == nullptr) {
wxDir dir; wxLogError("Cannot open directory /sys/class/tty");
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
return false; return false;
} }
wxString fileName; struct dirent* entry;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*")); while ((entry = ::readdir(dir)) != nullptr) {
while (ret1) { std::string fileName(entry->d_name);
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
char cpath[255U]; // Match ttyACM* entries
::memset(cpath, 0x00U, 255U); if (fileName.substr(0, 6) != "ttyACM")
continue;
for (unsigned int i = 0U; i < path.Len(); i++) std::string path = "/sys/class/tty/" + fileName;
cpath[i] = path.GetChar(i);
char cpath[255U];
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U]; char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U); int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, 255U); ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
::closedir(dir);
return false; return false;
} }
symlink[ret2] = '\0';
path = wxString(symlink, wxConvLocal, ret2); path = std::string(symlink, ret2);
} else { } else {
symlink[ret2] = '\0';
std::string fullPath(symlink, ret2);
// Get all but the last section // Get all but the last section
wxString fullPath = wxString(symlink, wxConvLocal, ret2); size_t pos = fullPath.rfind('/');
path = fullPath.BeforeLast(wxT('/')); path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (path.IsSameAs(m_path)) { if (path == m_path) {
m_port.Printf(wxT("/dev/%s"), fileName.c_str()); m_port = "/dev/" + fileName;
wxLogMessage(wxT("Found modem port of %s based on the path"), m_port.c_str()); wxLogMessage("Found modem port of %s based on the path", m_port.c_str());
::closedir(dir);
return true; return true;
} }
ret1 = dir.GetNext(&fileName);
} }
#endif
::closedir(dir);
return false; return false;
} }
bool CDVRPTRV1Controller::findPath() bool CDVRPTRV1Controller::findPath()
{ {
#if defined(__WINDOWS__) std::string path = "/sys/class/tty/" + m_port.substr(5U);
#ifdef notdef
GUID guids[5U];
DWORD count;
BOOL res = ::SetupDiClassGuidsFromName(L"Multifunction", guids, 5U, &count);
if (!res) {
wxLogError(wxT("Error from SetupDiClassGuidsFromName: err=%u"), ::GetLastError());
return false;
}
for (DWORD i = 0U; i < count; i++) {
HDEVINFO devInfo = ::SetupDiGetClassDevs(&guids[i], NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%u"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &guids[i], index, &devInfoData); index++) {
// Find the required length of the device structure
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// Get the detailed data into the newly allocated device structure
DWORD required;
res = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!res) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::free(detailData);
}
::SetupDiDestroyDeviceInfoList(devInfo);
}
return false;
#endif
#else
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), m_port.Mid(5U).c_str());
char cpath[255U]; char cpath[255U];
::memset(cpath, 0x00U, 255U); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char symlink[255U]; char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U); int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, 255U); ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
return false; return false;
} }
symlink[ret] = '\0';
path = wxString(symlink, wxConvLocal, ret); path = std::string(symlink, ret);
} else { } else {
wxString fullPath = wxString(symlink, wxConvLocal, ret); symlink[ret] = '\0';
path = fullPath.BeforeLast(wxT('/')); std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (m_path.IsEmpty()) if (m_path.empty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str()); wxLogMessage("Found modem path of %s", path.c_str());
m_path = path; m_path = path;
#endif
return true; return true;
} }
@ -979,7 +945,7 @@ bool CDVRPTRV1Controller::findModem()
// Tell the repeater that the signal has gone away // Tell the repeater that the signal has gone away
if (m_rx) { if (m_rx) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -995,7 +961,7 @@ bool CDVRPTRV1Controller::findModem()
while (!m_stopped) { while (!m_stopped) {
count++; count++;
if (count >= 4U) { if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem")); wxLogMessage("Trying to reopen the modem");
bool ret = findPort(); bool ret = findPort();
if (ret) { if (ret) {
@ -1007,7 +973,7 @@ bool CDVRPTRV1Controller::findModem()
count = 0U; count = 0U;
} }
Sleep(500UL); std::this_thread::sleep_for(std::chrono::milliseconds(500));
} }
return false; return false;
@ -1039,4 +1005,3 @@ bool CDVRPTRV1Controller::openModem()
return true; return true;
} }

@ -23,8 +23,9 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
enum RESP_TYPE_V1 { enum RESP_TYPE_V1 {
RT1_TIMEOUT, RT1_TIMEOUT,
@ -45,13 +46,28 @@ enum RESP_TYPE_V1 {
RT1_SET_TESTMDE RT1_SET_TESTMDE
}; };
/*
* CDVRPTRV1Controller - Driver for DV-RPTR V1 boards via serial (USB CDC-ACM, 115200 baud).
*
* Uses the same DVRPTR binary framing protocol as CDVMegaController:
* [0xD0] [len_lo] [len_hi] [type] [txCounter] [pktCounter] [payload...] [cksum]
*
* Hardware-specific parameters passed to SET_CONFIG (physical layer):
* channel - selects which of the two physical input channels to use (bit 0x04
* in the config byte). Some V1 boards have two discriminator inputs.
* modLevel - output modulation level, 0-100%, scaled to 0-255 in the wire frame.
* Sets the DAC drive level for the TX audio path.
* txDelay - PTT-to-data delay in milliseconds (little-endian uint16_t in frame).
* Allows time for the radio to reach full output power before
* transmitting the D-Star preamble.
*
* Reconnection and sysfs path tracking work identically to CDVMegaController.
*/
class CDVRPTRV1Controller : public CModem { class CDVRPTRV1Controller : public CModem {
public: public:
CDVRPTRV1Controller(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, bool channel, unsigned int modLevel, unsigned int txDelay); CDVRPTRV1Controller(const std::string& port, const std::string& path, bool rxInvert, bool txInvert, bool channel, unsigned int modLevel, unsigned int txDelay);
virtual ~CDVRPTRV1Controller(); virtual ~CDVRPTRV1Controller();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -60,11 +76,13 @@ public:
virtual bool writeHeader(const CHeaderData& header); virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const; virtual std::string getPath() const;
private: private:
wxString m_port; void entry();
wxString m_path;
std::string m_port;
std::string m_path;
bool m_rxInvert; bool m_rxInvert;
bool m_txInvert; bool m_txInvert;
bool m_channel; bool m_channel;
@ -95,4 +113,3 @@ private:
}; };
#endif #endif

@ -18,19 +18,23 @@
#include "DVRPTRV2Controller.h" #include "DVRPTRV2Controller.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h" #include "Timer.h"
#if defined(__WINDOWS__) #include <chrono>
#include <setupapi.h> #include <thread>
#else #include <cassert>
#include <wx/dir.h> #include <cstring>
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif #endif
const unsigned int MAX_RESPONSES = 30U; const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CDVRPTRV2Controller::CDVRPTRV2Controller(const wxString& port, const wxString& path, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay) : CDVRPTRV2Controller::CDVRPTRV2Controller(const std::string& port, const std::string& path, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay) :
CModem(), CModem(),
m_connection(CT_USB), m_connection(CT_USB),
m_usbPort(port), m_usbPort(port),
@ -42,20 +46,20 @@ m_modLevel(modLevel),
m_duplex(duplex), m_duplex(duplex),
m_callsign(callsign), m_callsign(callsign),
m_txDelay(txDelay), m_txDelay(txDelay),
m_usb(NULL), m_usb(nullptr),
m_network(NULL), m_network(nullptr),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_rx(false) m_rx(false)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
m_usb = new CSerialDataController(port, SERIAL_115200); m_usb = new CSerialDataController(port, SERIAL_115200);
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
} }
CDVRPTRV2Controller::CDVRPTRV2Controller(const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay) : CDVRPTRV2Controller::CDVRPTRV2Controller(const std::string& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay) :
CModem(), CModem(),
m_connection(CT_NETWORK), m_connection(CT_NETWORK),
m_usbPort(), m_usbPort(),
@ -67,14 +71,14 @@ m_modLevel(modLevel),
m_duplex(duplex), m_duplex(duplex),
m_callsign(callsign), m_callsign(callsign),
m_txDelay(txDelay), m_txDelay(txDelay),
m_usb(NULL), m_usb(nullptr),
m_network(NULL), m_network(nullptr),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_rx(false) m_rx(false)
{ {
wxASSERT(!address.IsEmpty()); assert(!address.empty());
wxASSERT(port > 0U); assert(port > 0U);
m_network = new CTCPReaderWriter(address, port); m_network = new CTCPReaderWriter(address, port);
@ -99,16 +103,14 @@ bool CDVRPTRV2Controller::start()
findPath(); findPath();
Create(); m_thread = std::thread(&CDVRPTRV2Controller::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CDVRPTRV2Controller::Entry() void CDVRPTRV2Controller::entry()
{ {
wxLogMessage(wxT("Starting DV-RPTR2 Modem Controller thread")); wxLogMessage("Starting DV-RPTR2 Modem Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 250U); CTimer pollTimer(200U, 0U, 250U);
@ -123,8 +125,8 @@ void* CDVRPTRV2Controller::Entry()
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
return NULL; return;
} }
} }
@ -141,15 +143,15 @@ void* CDVRPTRV2Controller::Entry()
case RT2_ERROR: { case RT2_ERROR: {
bool ret = findModem(); bool ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
return NULL; return;
} }
} }
break; break;
case RT2_HEADER: { case RT2_HEADER: {
// CUtils::dump(wxT("RT2_HEADER"), m_buffer, length); // CUtils::dump("RT2_HEADER", m_buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -174,8 +176,8 @@ void* CDVRPTRV2Controller::Entry()
break; break;
case RT2_DATA: { case RT2_DATA: {
// CUtils::dump(wxT("RT2_DATA"), m_buffer, length); // CUtils::dump("RT2_DATA", m_buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -200,7 +202,7 @@ void* CDVRPTRV2Controller::Entry()
case RT2_SPACE: case RT2_SPACE:
space = m_buffer[9U]; space = m_buffer[9U];
// CUtils::dump(wxT("RT2_SPACE"), m_buffer, length); // CUtils::dump("RT2_SPACE", m_buffer, length);
break; break;
// These should not be received in this loop, but don't complain if we do // These should not be received in this loop, but don't complain if we do
@ -209,8 +211,8 @@ void* CDVRPTRV2Controller::Entry()
break; break;
default: default:
wxLogMessage(wxT("Unknown DV-RPTR2 message, type")); wxLogMessage("Unknown DV-RPTR2 message, type");
CUtils::dump(wxT("Buffer dump"), m_buffer, length); CUtils::dump("Buffer dump", m_buffer, length);
break; break;
} }
@ -220,20 +222,20 @@ void* CDVRPTRV2Controller::Entry()
unsigned char data[200U]; unsigned char data[200U];
{ {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&len, 1U); m_txData.getData(&len, 1U);
m_txData.getData(data, len); m_txData.getData(data, len);
} }
// CUtils::dump(wxT("Write"), data, len); // CUtils::dump("Write", data, len);
bool ret = writeModem(data, len); bool ret = writeModem(data, len);
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
return NULL; return;
} }
} else { } else {
if (len > 100U) if (len > 100U)
@ -244,23 +246,21 @@ void* CDVRPTRV2Controller::Entry()
} }
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
} }
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
closeModem(); closeModem();
return NULL;
} }
bool CDVRPTRV2Controller::writeHeader(const CHeaderData& header) bool CDVRPTRV2Controller::writeHeader(const CHeaderData& header)
{ {
bool ret = m_txData.hasSpace(106U); bool ret = m_txData.hasSpace(106U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -284,27 +284,27 @@ bool CDVRPTRV2Controller::writeHeader(const CHeaderData& header)
buffer[10U] = header.getFlag2(); buffer[10U] = header.getFlag2();
buffer[11U] = header.getFlag3(); buffer[11U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 12U] = rpt2.GetChar(i); buffer[i + 12U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 20U] = rpt1.GetChar(i); buffer[i + 20U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 28U] = your.GetChar(i); buffer[i + 28U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 36U] = my1.GetChar(i); buffer[i + 36U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 44U] = my2.GetChar(i); buffer[i + 44U] = my2[i];
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = 105U; unsigned char len = 105U;
m_txData.addData(&len, 1U); m_txData.addData(&len, 1U);
@ -320,7 +320,7 @@ bool CDVRPTRV2Controller::writeData(const unsigned char* data, unsigned int, boo
{ {
bool ret = m_txData.hasSpace(18U); bool ret = m_txData.hasSpace(18U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
@ -344,7 +344,7 @@ bool CDVRPTRV2Controller::writeData(const unsigned char* data, unsigned int, boo
m_tx = false; m_tx = false;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = 17U; unsigned char len = 17U;
m_txData.addData(&len, 1U); m_txData.addData(&len, 1U);
@ -380,7 +380,7 @@ bool CDVRPTRV2Controller::readSerial()
buffer[7U] = '0'; buffer[7U] = '0';
buffer[8U] = '0'; buffer[8U] = '0';
// CUtils::dump(wxT("Written"), buffer, 105U); // CUtils::dump("Written", buffer, 105U);
bool ret = writeModem(buffer, 105U); bool ret = writeModem(buffer, 105U);
if (!ret) if (!ret)
@ -390,20 +390,20 @@ bool CDVRPTRV2Controller::readSerial()
unsigned int length; unsigned int length;
RESP_TYPE_V2 resp; RESP_TYPE_V2 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT2_QUERY) { if (resp != RT2_QUERY) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the query command")); wxLogError("The DV-RPTR modem is not responding to the query command");
return false; return false;
} }
} }
} while (resp != RT2_QUERY); } while (resp != RT2_QUERY);
wxLogInfo(wxT("DV-RPTR Modem Hardware serial: 0x%02X%02X%02x%02X"), m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U]); wxLogInfo("DV-RPTR Modem Hardware serial: 0x%02X%02X%02x%02X", m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U]);
return true; return true;
} }
@ -423,7 +423,7 @@ bool CDVRPTRV2Controller::readSpace()
buffer[8U] = '1'; buffer[8U] = '1';
buffer[9U] = 0x00U; buffer[9U] = 0x00U;
// CUtils::dump(wxT("Written"), buffer, 10U); // CUtils::dump("Written", buffer, 10U);
return writeModem(buffer, 10U); return writeModem(buffer, 10U);
} }
@ -445,8 +445,8 @@ bool CDVRPTRV2Controller::setConfig()
buffer[8U] = '1'; buffer[8U] = '1';
::memset(buffer + 9U, ' ', LONG_CALLSIGN_LENGTH); ::memset(buffer + 9U, ' ', LONG_CALLSIGN_LENGTH);
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH && i < m_callsign.Len(); i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH && i < m_callsign.size(); i++)
buffer[9U + i] = m_callsign.GetChar(i); buffer[9U + i] = m_callsign[i];
buffer[65U] = m_duplex ? 0x03U : 0x00U; buffer[65U] = m_duplex ? 0x03U : 0x00U;
@ -466,7 +466,7 @@ bool CDVRPTRV2Controller::setConfig()
buffer[89U] = m_txDelay / 10U; buffer[89U] = m_txDelay / 10U;
// CUtils::dump(wxT("Written"), buffer, 105U); // CUtils::dump("Written", buffer, 105U);
bool ret = writeModem(buffer, 105U); bool ret = writeModem(buffer, 105U);
if (!ret) if (!ret)
@ -476,34 +476,41 @@ bool CDVRPTRV2Controller::setConfig()
unsigned int length; unsigned int length;
RESP_TYPE_V2 resp; RESP_TYPE_V2 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT2_CONFIG) { if (resp != RT2_CONFIG) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the configure command")); wxLogError("The DV-RPTR modem is not responding to the configure command");
return false; return false;
} }
} }
} while (resp != RT2_CONFIG); } while (resp != RT2_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
wxString firmware((char*)(m_buffer + 9U), wxConvLocal); std::string firmware((char*)(m_buffer + 9U));
wxLogInfo(wxT("DV-RPTR Modem Firmware version: %s"), firmware.c_str()); wxLogInfo("DV-RPTR Modem Firmware version: %s", firmware.c_str());
return true; return true;
} }
// Reads one "HEAD" protocol frame.
// Scans the byte stream for the 4-byte magic "HEAD". The 5th byte is a type
// character that determines the total frame length:
// 'X' -> 105 bytes (control/header frames, e.g. HEADX9000/9001/0002)
// 'Y' -> 10 bytes (status/space frames, e.g. HEADY9011)
// 'Z' -> 20 bytes (voice data frames, e.g. HEADZ)
// Sub-type is identified by bytes[5..8] (4 ASCII digits) for 'X' and 'Y' frames.
RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned int& length) RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned int& length)
{ {
// Get the start of the frame or nothing at all // Get the start of the frame or nothing at all
int ret = readModem(buffer + 0U, 5U); int ret = readModem(buffer + 0U, 5U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT2_ERROR; return RT2_ERROR;
} }
@ -518,7 +525,7 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
ret = readModem(buffer + 4U, 1U); ret = readModem(buffer + 4U, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT2_ERROR; return RT2_ERROR;
} }
@ -537,7 +544,7 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
length = 20U; length = 20U;
break; break;
default: default:
wxLogError(wxT("DV-RPTR frame type is incorrect - 0x%02X"), buffer[4U]); wxLogError("DV-RPTR frame type is incorrect - 0x%02X", buffer[4U]);
return RT2_UNKNOWN; return RT2_UNKNOWN;
} }
@ -546,7 +553,7 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < length) { while (offset < length) {
ret = readModem(buffer + offset, length - offset); ret = readModem(buffer + offset, length - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT2_ERROR; return RT2_ERROR;
} }
@ -554,10 +561,10 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret; offset += ret;
if (ret == 0) if (ret == 0)
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
} }
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
if (::memcmp(buffer + 0U, "HEADZ", 5U) == 0) { if (::memcmp(buffer + 0U, "HEADZ", 5U) == 0) {
return RT2_DATA; return RT2_DATA;
@ -577,157 +584,115 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
return RT2_UNKNOWN; return RT2_UNKNOWN;
} }
wxString CDVRPTRV2Controller::getPath() const std::string CDVRPTRV2Controller::getPath() const
{ {
return m_usbPath; return m_usbPath;
} }
bool CDVRPTRV2Controller::findPort() bool CDVRPTRV2Controller::findPort()
{ {
#if !defined(_WIN32)
if (m_connection != CT_USB) if (m_connection != CT_USB)
return true; return true;
if (m_usbPath.IsEmpty()) if (m_usbPath.empty())
return false; return false;
#if defined(__WINDOWS__) DIR* dir = ::opendir("/sys/class/tty");
#else if (dir == nullptr) {
wxDir dir; wxLogError("Cannot open directory /sys/class/tty");
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
return false; return false;
} }
wxString fileName; struct dirent* entry;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*")); while ((entry = ::readdir(dir)) != nullptr) {
while (ret1) { std::string fileName(entry->d_name);
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
char cpath[255U]; // Match ttyACM* entries
::memset(cpath, 0x00U, 255U); if (fileName.substr(0, 6) != "ttyACM")
continue;
std::string path = "/sys/class/tty/" + fileName;
for (unsigned int i = 0U; i < path.Len(); i++) char cpath[255U];
cpath[i] = path.GetChar(i); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U]; char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U); int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, 255U); ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
::closedir(dir);
return false; return false;
} }
symlink[ret2] = '\0';
path = wxString(symlink, wxConvLocal, ret2); path = std::string(symlink, ret2);
} else { } else {
// Get all but the last section symlink[ret2] = '\0';
wxString fullPath = wxString(symlink, wxConvLocal, ret2); std::string fullPath(symlink, ret2);
path = fullPath.BeforeLast(wxT('/')); size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (path.IsSameAs(m_usbPath)) { if (path == m_usbPath) {
m_usbPort.Printf(wxT("/dev/%s"), fileName.c_str()); m_usbPort = "/dev/" + fileName;
wxLogMessage(wxT("Found modem port of %s based on the path"), m_usbPort.c_str()); wxLogMessage("Found modem port of %s based on the path", m_usbPort.c_str());
::closedir(dir);
return true; return true;
} }
ret1 = dir.GetNext(&fileName);
} }
#endif
::closedir(dir);
return false; return false;
#else
return true;
#endif
} }
bool CDVRPTRV2Controller::findPath() bool CDVRPTRV2Controller::findPath()
{ {
#if !defined(_WIN32)
if (m_connection != CT_USB) if (m_connection != CT_USB)
return true; return true;
#if defined(__WINDOWS__) std::string path = "/sys/class/tty/" + m_usbPort.substr(5U);
#ifdef notdef
GUID guids[5U];
DWORD count;
BOOL res = ::SetupDiClassGuidsFromName(L"Multifunction", guids, 5U, &count);
if (!res) {
wxLogError(wxT("Error from SetupDiClassGuidsFromName: err=%u"), ::GetLastError());
return false;
}
for (DWORD i = 0U; i < count; i++) {
HDEVINFO devInfo = ::SetupDiGetClassDevs(&guids[i], NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%u"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &guids[i], index, &devInfoData); index++) {
// Find the required length of the device structure
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// Get the detailed data into the newly allocated device structure
DWORD required;
res = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!res) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::free(detailData);
}
::SetupDiDestroyDeviceInfoList(devInfo);
}
return false;
#endif
#else
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), m_usbPort.Mid(5U).c_str());
char cpath[255U]; char cpath[255U];
::memset(cpath, 0x00U, 255U); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char symlink[255U]; char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U); int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, 255U); ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
return false; return false;
} }
symlink[ret] = '\0';
path = wxString(symlink, wxConvLocal, ret); path = std::string(symlink, ret);
} else { } else {
wxString fullPath = wxString(symlink, wxConvLocal, ret); symlink[ret] = '\0';
path = fullPath.BeforeLast(wxT('/')); std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (m_usbPath.IsEmpty()) if (m_usbPath.empty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str()); wxLogMessage("Found modem path of %s", path.c_str());
m_usbPath = path; m_usbPath = path;
#endif
return true; return true;
#else
return true;
#endif
} }
bool CDVRPTRV2Controller::findModem() bool CDVRPTRV2Controller::findModem()
@ -736,7 +701,7 @@ bool CDVRPTRV2Controller::findModem()
// Tell the repeater that the signal has gone away // Tell the repeater that the signal has gone away
if (m_rx) { if (m_rx) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -752,7 +717,7 @@ bool CDVRPTRV2Controller::findModem()
while (!m_stopped) { while (!m_stopped) {
count++; count++;
if (count >= 4U) { if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem")); wxLogMessage("Trying to reopen the modem");
bool ret = findPort(); bool ret = findPort();
if (ret) { if (ret) {
@ -764,7 +729,7 @@ bool CDVRPTRV2Controller::findModem()
count = 0U; count = 0U;
} }
Sleep(500UL); std::this_thread::sleep_for(std::chrono::milliseconds(500));
} }
return false; return false;
@ -782,7 +747,7 @@ bool CDVRPTRV2Controller::openModem()
ret = m_network->open(); ret = m_network->open();
break; break;
default: default:
wxLogError(wxT("Invalid connection type: %d"), int(m_connection)); wxLogError("Invalid connection type: %d", int(m_connection));
break; break;
} }

@ -25,8 +25,9 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
enum RESP_TYPE_V2 { enum RESP_TYPE_V2 {
RT2_TIMEOUT, RT2_TIMEOUT,
@ -39,14 +40,43 @@ enum RESP_TYPE_V2 {
RT2_DATA RT2_DATA
}; };
/*
* CDVRPTRV2Controller - Driver for DV-RPTR V2 boards.
*
* Supports two physical connection types, selected by constructor:
* CT_USB - USB CDC-ACM serial port (same as V1).
* CT_NETWORK - TCP socket (address + port); used for networked / remote modems.
*
* The CONNECTION_TYPE flag (m_connection) controls which of the two backing
* objects (m_usb / m_network) is used; readModem(), writeModem(), closeModem()
* dispatch through it uniformly so the rest of the logic is connection-agnostic.
*
* Protocol: ASCII-framed "HEAD" protocol (distinct from the binary DVRPTR protocol
* used by V1). Each frame starts with the 4-byte magic "HEAD" followed by a
* 1-byte type character:
* 'X' - 105-byte control/header frame (HEADX)
* 'Y' - 10-byte space/status frame (HEADY)
* 'Z' - 20-byte voice data frame (HEADZ)
* Frame subtypes are identified by 4 ASCII digits at bytes [5..8].
*
* On startup the driver sends a HEADX/9000 query to get the hardware serial,
* then HEADX/9001 to configure the modem (callsign, duplex, modLevel, txDelay).
* During operation the driver polls HEADY/9011 every 250ms to get TX buffer space.
*
* The header frame (HEADX/0002) also contains the first DV frame of the
* transmission (at byte offset 51), so the repeater receives both the D-Star
* header and the first voice frame in a single network packet.
*
* Reconnection works identically to V1: findPort() -> openModem() retried every 2s.
*/
class CDVRPTRV2Controller : public CModem { class CDVRPTRV2Controller : public CModem {
public: public:
CDVRPTRV2Controller(const wxString& port, const wxString& path, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay); // USB serial connection.
CDVRPTRV2Controller(const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay); CDVRPTRV2Controller(const std::string& port, const std::string& path, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay);
// TCP network connection.
CDVRPTRV2Controller(const std::string& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay);
virtual ~CDVRPTRV2Controller(); virtual ~CDVRPTRV2Controller();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -55,18 +85,20 @@ public:
virtual bool writeHeader(const CHeaderData& header); virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const; virtual std::string getPath() const;
private: private:
void entry();
CONNECTION_TYPE m_connection; CONNECTION_TYPE m_connection;
wxString m_usbPort; std::string m_usbPort;
wxString m_usbPath; std::string m_usbPath;
wxString m_address; std::string m_address;
unsigned int m_port; unsigned int m_port;
bool m_txInvert; bool m_txInvert;
unsigned int m_modLevel; unsigned int m_modLevel;
bool m_duplex; bool m_duplex;
wxString m_callsign; std::string m_callsign;
unsigned int m_txDelay; unsigned int m_txDelay;
CSerialDataController* m_usb; CSerialDataController* m_usb;
CTCPReaderWriter* m_network; CTCPReaderWriter* m_network;

@ -18,19 +18,23 @@
#include "DVRPTRV3Controller.h" #include "DVRPTRV3Controller.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h" #include "Timer.h"
#if defined(__WINDOWS__) #include <chrono>
#include <setupapi.h> #include <thread>
#else #include <cassert>
#include <wx/dir.h> #include <cstring>
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif #endif
const unsigned int MAX_RESPONSES = 30U; const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CDVRPTRV3Controller::CDVRPTRV3Controller(const wxString& port, const wxString& path, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay) : CDVRPTRV3Controller::CDVRPTRV3Controller(const std::string& port, const std::string& path, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay) :
CModem(), CModem(),
m_connection(CT_USB), m_connection(CT_USB),
m_usbPort(port), m_usbPort(port),
@ -42,20 +46,20 @@ m_modLevel(modLevel),
m_duplex(duplex), m_duplex(duplex),
m_callsign(callsign), m_callsign(callsign),
m_txDelay(txDelay), m_txDelay(txDelay),
m_usb(NULL), m_usb(nullptr),
m_network(NULL), m_network(nullptr),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_rx(false) m_rx(false)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
m_usb = new CSerialDataController(port, SERIAL_115200); m_usb = new CSerialDataController(port, SERIAL_115200);
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
} }
CDVRPTRV3Controller::CDVRPTRV3Controller(const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay) : CDVRPTRV3Controller::CDVRPTRV3Controller(const std::string& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay) :
CModem(), CModem(),
m_connection(CT_NETWORK), m_connection(CT_NETWORK),
m_usbPort(), m_usbPort(),
@ -67,14 +71,14 @@ m_modLevel(modLevel),
m_duplex(duplex), m_duplex(duplex),
m_callsign(callsign), m_callsign(callsign),
m_txDelay(txDelay), m_txDelay(txDelay),
m_usb(NULL), m_usb(nullptr),
m_network(NULL), m_network(nullptr),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_rx(false) m_rx(false)
{ {
wxASSERT(!address.IsEmpty()); assert(!address.empty());
wxASSERT(port > 0U); assert(port > 0U);
m_network = new CTCPReaderWriter(address, port); m_network = new CTCPReaderWriter(address, port);
@ -99,16 +103,14 @@ bool CDVRPTRV3Controller::start()
findPath(); findPath();
Create(); m_thread = std::thread(&CDVRPTRV3Controller::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CDVRPTRV3Controller::Entry() void CDVRPTRV3Controller::entry()
{ {
wxLogMessage(wxT("Starting DV-RPTR3 Modem Controller thread")); wxLogMessage("Starting DV-RPTR3 Modem Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 250U); CTimer pollTimer(200U, 0U, 250U);
@ -123,8 +125,8 @@ void* CDVRPTRV3Controller::Entry()
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR3 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR3 Modem Controller thread");
return NULL; return;
} }
} }
@ -141,15 +143,15 @@ void* CDVRPTRV3Controller::Entry()
case RT3_ERROR: { case RT3_ERROR: {
bool ret = findModem(); bool ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR3 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR3 Modem Controller thread");
return NULL; return;
} }
} }
break; break;
case RT3_HEADER: { case RT3_HEADER: {
// CUtils::dump(wxT("RT3_HEADER"), m_buffer, length); // CUtils::dump("RT3_HEADER", m_buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -174,8 +176,8 @@ void* CDVRPTRV3Controller::Entry()
break; break;
case RT3_DATA: { case RT3_DATA: {
// CUtils::dump(wxT("RT3_DATA"), m_buffer, length); // CUtils::dump("RT3_DATA", m_buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -200,7 +202,7 @@ void* CDVRPTRV3Controller::Entry()
case RT3_SPACE: case RT3_SPACE:
space = m_buffer[9U]; space = m_buffer[9U];
// CUtils::dump(wxT("RT3_SPACE"), m_buffer, length); // CUtils::dump("RT3_SPACE", m_buffer, length);
break; break;
// These should not be received in this loop, but don't complain if we do // These should not be received in this loop, but don't complain if we do
@ -209,8 +211,8 @@ void* CDVRPTRV3Controller::Entry()
break; break;
default: default:
wxLogMessage(wxT("Unknown DV-RPTR3 message, type")); wxLogMessage("Unknown DV-RPTR3 message, type");
CUtils::dump(wxT("Buffer dump"), m_buffer, length); CUtils::dump("Buffer dump", m_buffer, length);
break; break;
} }
@ -220,20 +222,20 @@ void* CDVRPTRV3Controller::Entry()
unsigned char data[200U]; unsigned char data[200U];
{ {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&len, 1U); m_txData.getData(&len, 1U);
m_txData.getData(data, len); m_txData.getData(data, len);
} }
// CUtils::dump(wxT("Write"), data, len); // CUtils::dump("Write", data, len);
bool ret = writeModem(data, len); bool ret = writeModem(data, len);
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR3 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR3 Modem Controller thread");
return NULL; return;
} }
} else { } else {
if (len > 100U) if (len > 100U)
@ -244,23 +246,21 @@ void* CDVRPTRV3Controller::Entry()
} }
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
} }
wxLogMessage(wxT("Stopping DV-RPTR3 Modem Controller thread")); wxLogMessage("Stopping DV-RPTR3 Modem Controller thread");
closeModem(); closeModem();
return NULL;
} }
bool CDVRPTRV3Controller::writeHeader(const CHeaderData& header) bool CDVRPTRV3Controller::writeHeader(const CHeaderData& header)
{ {
bool ret = m_txData.hasSpace(106U); bool ret = m_txData.hasSpace(106U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -284,27 +284,27 @@ bool CDVRPTRV3Controller::writeHeader(const CHeaderData& header)
buffer[10U] = header.getFlag2(); buffer[10U] = header.getFlag2();
buffer[11U] = header.getFlag3(); buffer[11U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 12U] = rpt2.GetChar(i); buffer[i + 12U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 20U] = rpt1.GetChar(i); buffer[i + 20U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 28U] = your.GetChar(i); buffer[i + 28U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 36U] = my1.GetChar(i); buffer[i + 36U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 44U] = my2.GetChar(i); buffer[i + 44U] = my2[i];
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = 105U; unsigned char len = 105U;
m_txData.addData(&len, 1U); m_txData.addData(&len, 1U);
@ -320,7 +320,7 @@ bool CDVRPTRV3Controller::writeData(const unsigned char* data, unsigned int, boo
{ {
bool ret = m_txData.hasSpace(18U); bool ret = m_txData.hasSpace(18U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
@ -344,7 +344,7 @@ bool CDVRPTRV3Controller::writeData(const unsigned char* data, unsigned int, boo
m_tx = false; m_tx = false;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = 17U; unsigned char len = 17U;
m_txData.addData(&len, 1U); m_txData.addData(&len, 1U);
@ -380,7 +380,7 @@ bool CDVRPTRV3Controller::readSerial()
buffer[7U] = '0'; buffer[7U] = '0';
buffer[8U] = '0'; buffer[8U] = '0';
// CUtils::dump(wxT("Written"), buffer, 105U); // CUtils::dump("Written", buffer, 105U);
bool ret = writeModem(buffer, 105U); bool ret = writeModem(buffer, 105U);
if (!ret) if (!ret)
@ -390,20 +390,20 @@ bool CDVRPTRV3Controller::readSerial()
unsigned int length; unsigned int length;
RESP_TYPE_V3 resp; RESP_TYPE_V3 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT3_QUERY) { if (resp != RT3_QUERY) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the query command")); wxLogError("The DV-RPTR modem is not responding to the query command");
return false; return false;
} }
} }
} while (resp != RT3_QUERY); } while (resp != RT3_QUERY);
wxLogInfo(wxT("DV-RPTR Modem Hardware serial: 0x%02X%02X%02x%02X"), m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U]); wxLogInfo("DV-RPTR Modem Hardware serial: 0x%02X%02X%02x%02X", m_buffer[9U], m_buffer[10U], m_buffer[11U], m_buffer[12U]);
return true; return true;
} }
@ -423,7 +423,7 @@ bool CDVRPTRV3Controller::readSpace()
buffer[8U] = '1'; buffer[8U] = '1';
buffer[9U] = 0x00U; buffer[9U] = 0x00U;
// CUtils::dump(wxT("Written"), buffer, 10U); // CUtils::dump("Written", buffer, 10U);
return writeModem(buffer, 10U); return writeModem(buffer, 10U);
} }
@ -445,8 +445,8 @@ bool CDVRPTRV3Controller::setConfig()
buffer[8U] = '1'; buffer[8U] = '1';
::memset(buffer + 9U, ' ', LONG_CALLSIGN_LENGTH); ::memset(buffer + 9U, ' ', LONG_CALLSIGN_LENGTH);
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH && i < m_callsign.Len(); i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH && i < m_callsign.size(); i++)
buffer[9U + i] = m_callsign.GetChar(i); buffer[9U + i] = m_callsign[i];
buffer[65U] = m_duplex ? 0x03U : 0x00U; buffer[65U] = m_duplex ? 0x03U : 0x00U;
@ -466,7 +466,7 @@ bool CDVRPTRV3Controller::setConfig()
buffer[89U] = m_txDelay / 10U; buffer[89U] = m_txDelay / 10U;
// CUtils::dump(wxT("Written"), buffer, 105U); // CUtils::dump("Written", buffer, 105U);
bool ret = writeModem(buffer, 105U); bool ret = writeModem(buffer, 105U);
if (!ret) if (!ret)
@ -476,34 +476,36 @@ bool CDVRPTRV3Controller::setConfig()
unsigned int length; unsigned int length;
RESP_TYPE_V3 resp; RESP_TYPE_V3 resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RT3_CONFIG) { if (resp != RT3_CONFIG) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The DV-RPTR modem is not responding to the configure command")); wxLogError("The DV-RPTR modem is not responding to the configure command");
return false; return false;
} }
} }
} while (resp != RT3_CONFIG); } while (resp != RT3_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
wxString firmware((char*)(m_buffer + 9U), wxConvLocal); std::string firmware((char*)(m_buffer + 9U));
wxLogInfo(wxT("DV-RPTR Modem Firmware version: %s"), firmware.c_str()); wxLogInfo("DV-RPTR Modem Firmware version: %s", firmware.c_str());
return true; return true;
} }
// Reads one "HEAD" protocol frame. See CDVRPTRV2Controller::getResponse()
// for full protocol documentation; V3 uses an identical frame structure.
RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned int& length) RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned int& length)
{ {
// Get the start of the frame or nothing at all // Get the start of the frame or nothing at all
int ret = readModem(buffer + 0U, 5U); int ret = readModem(buffer + 0U, 5U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT3_ERROR; return RT3_ERROR;
} }
@ -518,7 +520,7 @@ RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned in
ret = readModem(buffer + 4U, 1U); ret = readModem(buffer + 4U, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT3_ERROR; return RT3_ERROR;
} }
@ -537,7 +539,7 @@ RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned in
length = 20U; length = 20U;
break; break;
default: default:
wxLogError(wxT("DV-RPTR frame type is incorrect - 0x%02X"), buffer[4U]); wxLogError("DV-RPTR frame type is incorrect - 0x%02X", buffer[4U]);
return RT3_UNKNOWN; return RT3_UNKNOWN;
} }
@ -546,7 +548,7 @@ RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < length) { while (offset < length) {
ret = readModem(buffer + offset, length - offset); ret = readModem(buffer + offset, length - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR")); wxLogError("Error when reading from the DV-RPTR");
return RT3_ERROR; return RT3_ERROR;
} }
@ -554,10 +556,10 @@ RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret; offset += ret;
if (ret == 0) if (ret == 0)
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
} }
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
if (::memcmp(buffer + 0U, "HEADZ", 5U) == 0) { if (::memcmp(buffer + 0U, "HEADZ", 5U) == 0) {
return RT3_DATA; return RT3_DATA;
@ -577,157 +579,115 @@ RESP_TYPE_V3 CDVRPTRV3Controller::getResponse(unsigned char *buffer, unsigned in
return RT3_UNKNOWN; return RT3_UNKNOWN;
} }
wxString CDVRPTRV3Controller::getPath() const std::string CDVRPTRV3Controller::getPath() const
{ {
return m_usbPath; return m_usbPath;
} }
bool CDVRPTRV3Controller::findPort() bool CDVRPTRV3Controller::findPort()
{ {
#if !defined(_WIN32)
if (m_connection != CT_USB) if (m_connection != CT_USB)
return true; return true;
if (m_usbPath.IsEmpty()) if (m_usbPath.empty())
return false; return false;
#if defined(__WINDOWS__) DIR* dir = ::opendir("/sys/class/tty");
#else if (dir == nullptr) {
wxDir dir; wxLogError("Cannot open directory /sys/class/tty");
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
return false; return false;
} }
wxString fileName; struct dirent* entry;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*")); while ((entry = ::readdir(dir)) != nullptr) {
while (ret1) { std::string fileName(entry->d_name);
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
char cpath[255U]; // Match ttyACM* entries
::memset(cpath, 0x00U, 255U); if (fileName.substr(0, 6) != "ttyACM")
continue;
std::string path = "/sys/class/tty/" + fileName;
for (unsigned int i = 0U; i < path.Len(); i++) char cpath[255U];
cpath[i] = path.GetChar(i); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U]; char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U); int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, 255U); ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
::closedir(dir);
return false; return false;
} }
symlink[ret2] = '\0';
path = wxString(symlink, wxConvLocal, ret2); path = std::string(symlink, ret2);
} else { } else {
// Get all but the last section symlink[ret2] = '\0';
wxString fullPath = wxString(symlink, wxConvLocal, ret2); std::string fullPath(symlink, ret2);
path = fullPath.BeforeLast(wxT('/')); size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (path.IsSameAs(m_usbPath)) { if (path == m_usbPath) {
m_usbPort.Printf(wxT("/dev/%s"), fileName.c_str()); m_usbPort = "/dev/" + fileName;
wxLogMessage(wxT("Found modem port of %s based on the path"), m_usbPort.c_str()); wxLogMessage("Found modem port of %s based on the path", m_usbPort.c_str());
::closedir(dir);
return true; return true;
} }
ret1 = dir.GetNext(&fileName);
} }
#endif
::closedir(dir);
return false; return false;
#else
return true;
#endif
} }
bool CDVRPTRV3Controller::findPath() bool CDVRPTRV3Controller::findPath()
{ {
#if !defined(_WIN32)
if (m_connection != CT_USB) if (m_connection != CT_USB)
return true; return true;
#if defined(__WINDOWS__) std::string path = "/sys/class/tty/" + m_usbPort.substr(5U);
#ifdef notdef
GUID guids[5U];
DWORD count;
BOOL res = ::SetupDiClassGuidsFromName(L"Multifunction", guids, 5U, &count);
if (!res) {
wxLogError(wxT("Error from SetupDiClassGuidsFromName: err=%u"), ::GetLastError());
return false;
}
for (DWORD i = 0U; i < count; i++) {
HDEVINFO devInfo = ::SetupDiGetClassDevs(&guids[i], NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%u"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &guids[i], index, &devInfoData); index++) {
// Find the required length of the device structure
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// Get the detailed data into the newly allocated device structure
DWORD required;
res = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!res) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::free(detailData);
}
::SetupDiDestroyDeviceInfoList(devInfo);
}
return false;
#endif
#else
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), m_usbPort.Mid(5U).c_str());
char cpath[255U]; char cpath[255U];
::memset(cpath, 0x00U, 255U); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char symlink[255U]; char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U); int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, 255U); ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
return false; return false;
} }
symlink[ret] = '\0';
path = wxString(symlink, wxConvLocal, ret); path = std::string(symlink, ret);
} else { } else {
wxString fullPath = wxString(symlink, wxConvLocal, ret); symlink[ret] = '\0';
path = fullPath.BeforeLast(wxT('/')); std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (m_usbPath.IsEmpty()) if (m_usbPath.empty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str()); wxLogMessage("Found modem path of %s", path.c_str());
m_usbPath = path; m_usbPath = path;
#endif
return true; return true;
#else
return true;
#endif
} }
bool CDVRPTRV3Controller::findModem() bool CDVRPTRV3Controller::findModem()
@ -736,7 +696,7 @@ bool CDVRPTRV3Controller::findModem()
// Tell the repeater that the signal has gone away // Tell the repeater that the signal has gone away
if (m_rx) { if (m_rx) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -752,7 +712,7 @@ bool CDVRPTRV3Controller::findModem()
while (!m_stopped) { while (!m_stopped) {
count++; count++;
if (count >= 4U) { if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem")); wxLogMessage("Trying to reopen the modem");
bool ret = findPort(); bool ret = findPort();
if (ret) { if (ret) {
@ -764,7 +724,7 @@ bool CDVRPTRV3Controller::findModem()
count = 0U; count = 0U;
} }
Sleep(500UL); std::this_thread::sleep_for(std::chrono::milliseconds(500));
} }
return false; return false;
@ -782,7 +742,7 @@ bool CDVRPTRV3Controller::openModem()
ret = m_network->open(); ret = m_network->open();
break; break;
default: default:
wxLogError(wxT("Invalid connection type: %d"), int(m_connection)); wxLogError("Invalid connection type: %d", int(m_connection));
break; break;
} }

@ -25,8 +25,9 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
enum RESP_TYPE_V3 { enum RESP_TYPE_V3 {
RT3_TIMEOUT, RT3_TIMEOUT,
@ -39,14 +40,22 @@ enum RESP_TYPE_V3 {
RT3_DATA RT3_DATA
}; };
/*
* CDVRPTRV3Controller - Driver for DV-RPTR V3 boards.
*
* Functionally identical to CDVRPTRV2Controller; V3 uses the same ASCII "HEAD"
* protocol and the same CT_USB / CT_NETWORK connection type switching.
* The code is a parallel implementation maintained separately for V3 firmware
* compatibility. See CDVRPTRV2Controller for full protocol documentation.
*/
class CDVRPTRV3Controller : public CModem { class CDVRPTRV3Controller : public CModem {
public: public:
CDVRPTRV3Controller(const wxString& port, const wxString& path, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay); // USB serial connection.
CDVRPTRV3Controller(const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay); CDVRPTRV3Controller(const std::string& port, const std::string& path, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay);
// TCP network connection.
CDVRPTRV3Controller(const std::string& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const std::string& callsign, unsigned int txDelay);
virtual ~CDVRPTRV3Controller(); virtual ~CDVRPTRV3Controller();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -55,18 +64,20 @@ public:
virtual bool writeHeader(const CHeaderData& header); virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const; virtual std::string getPath() const;
private: private:
void entry();
CONNECTION_TYPE m_connection; CONNECTION_TYPE m_connection;
wxString m_usbPort; std::string m_usbPort;
wxString m_usbPath; std::string m_usbPath;
wxString m_address; std::string m_address;
unsigned int m_port; unsigned int m_port;
bool m_txInvert; bool m_txInvert;
unsigned int m_modLevel; unsigned int m_modLevel;
bool m_duplex; bool m_duplex;
wxString m_callsign; std::string m_callsign;
unsigned int m_txDelay; unsigned int m_txDelay;
CSerialDataController* m_usb; CSerialDataController* m_usb;
CTCPReaderWriter* m_network; CTCPReaderWriter* m_network;

@ -20,7 +20,10 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Utils.h" #include "Utils.h"
#include <wx/wx.h> #include <cstdio>
#include <cstring>
#include <cassert>
#include "EndianCompat.h"
static const char DVTOOL_SIGNATURE[] = "DVTOOL"; static const char DVTOOL_SIGNATURE[] = "DVTOOL";
static const unsigned int DVTOOL_SIGNATURE_LENGTH = 6U; static const unsigned int DVTOOL_SIGNATURE_LENGTH = 6U;
@ -41,10 +44,10 @@ const unsigned int BUFFER_LENGTH = 255U;
CDVTOOLFileReader::CDVTOOLFileReader() : CDVTOOLFileReader::CDVTOOLFileReader() :
m_fileName(), m_fileName(),
m_file(), m_file(nullptr),
m_records(0U), m_records(0U),
m_type(DVTFR_NONE), m_type(DVTFR_NONE),
m_buffer(NULL), m_buffer(nullptr),
m_length(0U), m_length(0U),
m_end(false) m_end(false)
{ {
@ -56,7 +59,7 @@ CDVTOOLFileReader::~CDVTOOLFileReader()
delete[] m_buffer; delete[] m_buffer;
} }
wxString CDVTOOLFileReader::getFileName() const std::string CDVTOOLFileReader::getFileName() const
{ {
return m_fileName; return m_fileName;
} }
@ -66,34 +69,38 @@ unsigned int CDVTOOLFileReader::getRecords() const
return m_records; return m_records;
} }
bool CDVTOOLFileReader::open(const wxString& fileName) bool CDVTOOLFileReader::open(const std::string& fileName)
{ {
m_fileName = fileName; m_fileName = fileName;
bool res = m_file.Open(fileName, wxT("rb")); m_file = ::fopen(fileName.c_str(), "rb");
if (!res) if (m_file == nullptr)
return false; return false;
unsigned char buffer[DVTOOL_SIGNATURE_LENGTH]; unsigned char buffer[DVTOOL_SIGNATURE_LENGTH];
size_t n = m_file.Read(buffer, DVTOOL_SIGNATURE_LENGTH); size_t n = ::fread(buffer, 1U, DVTOOL_SIGNATURE_LENGTH, m_file);
if (n != DVTOOL_SIGNATURE_LENGTH) { if (n != DVTOOL_SIGNATURE_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
if (::memcmp(buffer, DVTOOL_SIGNATURE, DVTOOL_SIGNATURE_LENGTH) != 0) { if (::memcmp(buffer, DVTOOL_SIGNATURE, DVTOOL_SIGNATURE_LENGTH) != 0) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
wxUint32 uint32; uint32_t uint32;
n = m_file.Read(&uint32, sizeof(wxUint32)); n = ::fread(&uint32, 1U, sizeof(uint32_t), m_file);
if (n != sizeof(wxUint32)) { if (n != sizeof(uint32_t)) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
m_records = wxUINT32_SWAP_ON_LE(uint32); // wxUINT32_SWAP_ON_LE swaps on little-endian to produce big-endian value
m_records = be32toh(uint32);
m_end = false; m_end = false;
return true; return true;
@ -101,15 +108,16 @@ bool CDVTOOLFileReader::open(const wxString& fileName)
DVTFR_TYPE CDVTOOLFileReader::read() DVTFR_TYPE CDVTOOLFileReader::read()
{ {
wxUint16 uint16; uint16_t uint16;
size_t n = m_file.Read(&uint16, sizeof(wxUint16)); size_t n = ::fread(&uint16, 1U, sizeof(uint16_t), m_file);
if (n != sizeof(wxUint16)) if (n != sizeof(uint16_t))
return DVTFR_NONE; return DVTFR_NONE;
m_length = wxUINT16_SWAP_ON_BE(uint16) - 15U; // wxUINT16_SWAP_ON_BE swaps on big-endian to get little-endian value
m_length = le16toh(uint16) - 15U;
unsigned char bytes[FIXED_DATA_LENGTH]; unsigned char bytes[FIXED_DATA_LENGTH];
n = m_file.Read(bytes, DSVT_SIGNATURE_LENGTH); n = ::fread(bytes, 1U, DSVT_SIGNATURE_LENGTH, m_file);
if (n != DSVT_SIGNATURE_LENGTH) if (n != DSVT_SIGNATURE_LENGTH)
return DVTFR_NONE; return DVTFR_NONE;
@ -117,17 +125,17 @@ DVTFR_TYPE CDVTOOLFileReader::read()
return DVTFR_NONE; return DVTFR_NONE;
char flag; char flag;
n = m_file.Read(&flag, 1U); n = ::fread(&flag, 1U, 1U, m_file);
if (n != 1U) if (n != 1U)
return DVTFR_NONE; return DVTFR_NONE;
m_type = (flag == HEADER_FLAG) ? DVTFR_HEADER : DVTFR_DATA; m_type = (flag == HEADER_FLAG) ? DVTFR_HEADER : DVTFR_DATA;
n = m_file.Read(bytes, FIXED_DATA_LENGTH); n = ::fread(bytes, 1U, FIXED_DATA_LENGTH, m_file);
if (n != FIXED_DATA_LENGTH) if (n != FIXED_DATA_LENGTH)
return DVTFR_NONE; return DVTFR_NONE;
n = m_file.Read(&flag, 1U); n = ::fread(&flag, 1U, 1U, m_file);
if (n != 1U) if (n != 1U)
return DVTFR_NONE; return DVTFR_NONE;
@ -136,7 +144,7 @@ DVTFR_TYPE CDVTOOLFileReader::read()
m_end = true; m_end = true;
} }
n = m_file.Read(m_buffer, m_length); n = ::fread(m_buffer, 1U, m_length, m_file);
if (n != m_length) if (n != m_length)
return DVTFR_NONE; return DVTFR_NONE;
@ -146,7 +154,7 @@ DVTFR_TYPE CDVTOOLFileReader::read()
CHeaderData* CDVTOOLFileReader::readHeader() CHeaderData* CDVTOOLFileReader::readHeader()
{ {
if (m_type != DVTFR_HEADER) if (m_type != DVTFR_HEADER)
return NULL; return nullptr;
if (m_buffer[39U] == 0xFFU && m_buffer[40U] == 0xFFU) if (m_buffer[39U] == 0xFFU && m_buffer[40U] == 0xFFU)
return new CHeaderData(m_buffer, RADIO_HEADER_LENGTH_BYTES, false); return new CHeaderData(m_buffer, RADIO_HEADER_LENGTH_BYTES, false);
@ -155,9 +163,9 @@ CHeaderData* CDVTOOLFileReader::readHeader()
CHeaderData* header = new CHeaderData(m_buffer, RADIO_HEADER_LENGTH_BYTES, true); CHeaderData* header = new CHeaderData(m_buffer, RADIO_HEADER_LENGTH_BYTES, true);
if (!header->isValid()) { if (!header->isValid()) {
CUtils::dump(wxT("Header checksum failure"), m_buffer, RADIO_HEADER_LENGTH_BYTES); CUtils::dump("Header checksum failure", m_buffer, RADIO_HEADER_LENGTH_BYTES);
delete header; delete header;
return NULL; return nullptr;
} }
return header; return header;
@ -165,8 +173,8 @@ CHeaderData* CDVTOOLFileReader::readHeader()
unsigned int CDVTOOLFileReader::readData(unsigned char* buffer, unsigned int length, bool& end) unsigned int CDVTOOLFileReader::readData(unsigned char* buffer, unsigned int length, bool& end)
{ {
wxASSERT(buffer != NULL); assert(buffer != nullptr);
wxASSERT(length > 0U); assert(length > 0U);
if (m_type != DVTFR_DATA) if (m_type != DVTFR_DATA)
return 0U; return 0U;
@ -183,5 +191,8 @@ unsigned int CDVTOOLFileReader::readData(unsigned char* buffer, unsigned int len
void CDVTOOLFileReader::close() void CDVTOOLFileReader::close()
{ {
m_file.Close(); if (m_file != nullptr) {
::fclose(m_file);
m_file = nullptr;
}
} }

@ -21,37 +21,62 @@
#include "HeaderData.h" #include "HeaderData.h"
/*
* .dvtool file format (compatible with the DVTool PC application):
*
* [0..5] "DVTOOL" 6-byte magic signature
* [6..9] record count (big-endian) total number of DSVT records
*
* Each record:
* [0..1] total record length (little-endian, includes 15-byte fixed header)
* [2..5] "DSVT" record signature
* [6] 0x10 = header frame, 0x20 = data/trailer frame
* [7..15] fixed 9-byte field (stream flags, sequence info)
* [16] flags: bit 7 = header record, bit 6 = trailer (end-of-stream)
* [17..] payload (radio header bytes, or DV frame data)
*/
enum DVTFR_TYPE { enum DVTFR_TYPE {
DVTFR_NONE, DVTFR_NONE, // No record read yet, or end of file.
DVTFR_HEADER, DVTFR_HEADER, // Current record contains a radio header.
DVTFR_DATA DVTFR_DATA // Current record contains a DV voice/data frame.
}; };
#include <wx/wx.h> #include "StdCompat.h"
#include <wx/ffile.h> #include <cstdio>
/*
* Sequential reader for .dvtool recording files.
*
* Call open() to validate the signature and read the record count, then loop
* calling read() to advance to each record, followed by readHeader() or
* readData() to extract the payload. close() releases the file handle.
*/
class CDVTOOLFileReader { class CDVTOOLFileReader {
public: public:
CDVTOOLFileReader(); CDVTOOLFileReader();
~CDVTOOLFileReader(); ~CDVTOOLFileReader();
wxString getFileName() const; std::string getFileName() const;
unsigned int getRecords() const; unsigned int getRecords() const;
bool open(const wxString& fileName); bool open(const std::string& fileName);
// Advances to the next record; returns its type or DVTFR_NONE at EOF.
DVTFR_TYPE read(); DVTFR_TYPE read();
// Returns a newly allocated CHeaderData; caller takes ownership.
CHeaderData* readHeader(); CHeaderData* readHeader();
// Copies DV frame payload into buffer; sets end=true on the trailer record.
unsigned int readData(unsigned char* buffer, unsigned int length, bool& end); unsigned int readData(unsigned char* buffer, unsigned int length, bool& end);
void close(); void close();
private: private:
wxString m_fileName; std::string m_fileName;
wxFFile m_file; FILE* m_file;
wxUint32 m_records; uint32_t m_records; // Total record count from the file header.
DVTFR_TYPE m_type; DVTFR_TYPE m_type;
unsigned char* m_buffer; unsigned char* m_buffer;
unsigned int m_length; unsigned int m_length; // Payload length of the current record.
bool m_end; bool m_end; // True if the trailer bit was set in the current record.
}; };
#endif #endif

@ -20,8 +20,11 @@
#include "DVTOOLFileWriter.h" #include "DVTOOLFileWriter.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include <wx/wx.h> #include <cstdio>
#include <wx/filename.h> #include <cstring>
#include <cassert>
#include <ctime>
#include "EndianCompat.h"
static const char DVTOOL_SIGNATURE[] = "DVTOOL"; static const char DVTOOL_SIGNATURE[] = "DVTOOL";
static unsigned int DVTOOL_SIGNATURE_LENGTH = 6U; static unsigned int DVTOOL_SIGNATURE_LENGTH = 6U;
@ -41,11 +44,11 @@ static unsigned int TRAILER_DATA_LENGTH = 12U;
static const unsigned char HEADER_MASK = 0x80; static const unsigned char HEADER_MASK = 0x80;
static const unsigned char TRAILER_MASK = 0x40; static const unsigned char TRAILER_MASK = 0x40;
wxString CDVTOOLFileWriter::m_dirName = wxEmptyString; std::string CDVTOOLFileWriter::m_dirName = std::string();
CDVTOOLFileWriter::CDVTOOLFileWriter() : CDVTOOLFileWriter::CDVTOOLFileWriter() :
m_fileName(), m_fileName(),
m_file(), m_file(nullptr),
m_count(0U), m_count(0U),
m_sequence(0U), m_sequence(0U),
m_offset(0) m_offset(0)
@ -56,54 +59,59 @@ CDVTOOLFileWriter::~CDVTOOLFileWriter()
{ {
} }
void CDVTOOLFileWriter::setDirectory(const wxString& dirName) void CDVTOOLFileWriter::setDirectory(const std::string& dirName)
{ {
m_dirName = dirName; m_dirName = dirName;
} }
wxString CDVTOOLFileWriter::getFileName() const std::string CDVTOOLFileWriter::getFileName() const
{ {
return m_fileName; return m_fileName;
} }
bool CDVTOOLFileWriter::open(const wxString& filename, const CHeaderData& header) bool CDVTOOLFileWriter::open(const std::string& filename, const CHeaderData& header)
{ {
if (m_file.IsOpened()) if (m_file != nullptr)
close(); close();
wxString name = filename; std::string name = filename;
#if !defined(__WINDOWS__) // Replace spaces with underscores
name.Replace(wxT(" "), wxT("_")); size_t pos = 0;
#endif while ((pos = name.find(' ', pos)) != std::string::npos) {
name.replace(pos, 1, "_");
pos += 1;
}
wxFileName fileName(m_dirName, name, wxT("dvtool")); m_fileName = m_dirName + "/" + name + ".dvtool";
m_fileName = fileName.GetFullPath();
bool res = m_file.Open(m_fileName, wxT("wb")); m_file = ::fopen(m_fileName.c_str(), "wb");
if (!res) if (m_file == nullptr)
return false; return false;
size_t n = m_file.Write(DVTOOL_SIGNATURE, DVTOOL_SIGNATURE_LENGTH); size_t n = ::fwrite(DVTOOL_SIGNATURE, 1U, DVTOOL_SIGNATURE_LENGTH, m_file);
if (n != DVTOOL_SIGNATURE_LENGTH) { if (n != DVTOOL_SIGNATURE_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
m_offset = m_file.Tell(); m_offset = ::ftell(m_file);
wxUint32 dummy = 0U; uint32_t dummy = 0U;
n = m_file.Write(&dummy, sizeof(wxUint32)); n = ::fwrite(&dummy, 1U, sizeof(uint32_t), m_file);
if (n != sizeof(wxUint32)) { if (n != sizeof(uint32_t)) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
m_sequence = 0U; m_sequence = 0U;
m_count = 0U; m_count = 0U;
res = writeHeader(header); bool res = writeHeader(header);
if (!res) { if (!res) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
@ -112,53 +120,75 @@ bool CDVTOOLFileWriter::open(const wxString& filename, const CHeaderData& header
bool CDVTOOLFileWriter::open(const CHeaderData& header) bool CDVTOOLFileWriter::open(const CHeaderData& header)
{ {
if (m_file.IsOpened()) if (m_file != nullptr)
close(); close();
wxDateTime time; // Format timestamp
time.SetToCurrent(); time_t now = ::time(nullptr);
#if defined(_WIN32)
wxString name = time.Format(wxT("%Y%m%d-%H%M%S-")); struct tm tm_buf;
localtime_s(&tm_buf, &now);
name.Append(header.getRptCall1()); struct tm* tm_info = &tm_buf;
name.Append(header.getRptCall2()); #else
name.Append(header.getYourCall()); struct tm tm_buf;
name.Append(header.getMyCall1()); struct tm* tm_info = localtime_r(&now, &tm_buf);
name.Append(header.getMyCall2());
#if !defined(__WINDOWS__)
name.Replace(wxT(" "), wxT("_"));
#endif #endif
name.Replace(wxT("/"), wxT("-")); char timeBuf[32];
::strftime(timeBuf, sizeof(timeBuf), "%Y%m%d-%H%M%S-", tm_info);
std::string name = std::string(timeBuf);
name += header.getRptCall1();
name += header.getRptCall2();
name += header.getYourCall();
name += header.getMyCall1();
name += header.getMyCall2();
// Replace spaces with underscores
size_t pos = 0;
while ((pos = name.find(' ', pos)) != std::string::npos) {
name.replace(pos, 1, "_");
pos += 1;
}
// Sanitize the filename to prevent path traversal from crafted callsigns
for (size_t i = 0; i < name.size(); i++) {
if (name[i] == '/' || name[i] == '\\')
name[i] = '-';
}
// Remove any ".." sequences
while ((pos = name.find("..")) != std::string::npos)
name.replace(pos, 2, "__");
wxFileName fileName(m_dirName, name, wxT("dvtool")); m_fileName = m_dirName + "/" + name + ".dvtool";
m_fileName = fileName.GetFullPath();
bool res = m_file.Open(m_fileName, wxT("wb")); m_file = ::fopen(m_fileName.c_str(), "wb");
if (!res) if (m_file == nullptr)
return false; return false;
size_t n = m_file.Write(DVTOOL_SIGNATURE, DVTOOL_SIGNATURE_LENGTH); size_t n = ::fwrite(DVTOOL_SIGNATURE, 1U, DVTOOL_SIGNATURE_LENGTH, m_file);
if (n != DVTOOL_SIGNATURE_LENGTH) { if (n != DVTOOL_SIGNATURE_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
m_offset = m_file.Tell(); m_offset = ::ftell(m_file);
wxUint32 dummy = 0U; uint32_t dummy = 0U;
n = m_file.Write(&dummy, sizeof(wxUint32)); n = ::fwrite(&dummy, 1U, sizeof(uint32_t), m_file);
if (n != sizeof(wxUint32)) { if (n != sizeof(uint32_t)) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
m_sequence = 0U; m_sequence = 0U;
m_count = 0U; m_count = 0U;
res = writeHeader(header); bool res = writeHeader(header);
if (!res) { if (!res) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
@ -167,45 +197,52 @@ bool CDVTOOLFileWriter::open(const CHeaderData& header)
bool CDVTOOLFileWriter::write(const unsigned char* buffer, unsigned int length) bool CDVTOOLFileWriter::write(const unsigned char* buffer, unsigned int length)
{ {
wxASSERT(buffer != 0); assert(buffer != nullptr);
wxASSERT(length > 0U); assert(length > 0U);
wxUint16 len = wxUINT16_SWAP_ON_BE(length + 15U); // wxUINT16_SWAP_ON_BE produces a little-endian value
size_t n = m_file.Write(&len, sizeof(wxUint16)); uint16_t len = htole16(length + 15U);
if (n != sizeof(wxUint16)) { size_t n = ::fwrite(&len, 1U, sizeof(uint16_t), m_file);
m_file.Close(); if (n != sizeof(uint16_t)) {
::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(DSVT_SIGNATURE, DSVT_SIGNATURE_LENGTH); n = ::fwrite(DSVT_SIGNATURE, 1U, DSVT_SIGNATURE_LENGTH, m_file);
if (n != DSVT_SIGNATURE_LENGTH) { if (n != DSVT_SIGNATURE_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
char byte = DATA_FLAG; char byte = DATA_FLAG;
n = m_file.Write(&byte, 1U); n = ::fwrite(&byte, 1U, 1U, m_file);
if (n != 1U) { if (n != 1U) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(FIXED_DATA, FIXED_DATA_LENGTH); n = ::fwrite(FIXED_DATA, 1U, FIXED_DATA_LENGTH, m_file);
if (n != FIXED_DATA_LENGTH) { if (n != FIXED_DATA_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
byte = m_sequence; byte = (char)m_sequence;
n = m_file.Write(&byte, 1U); n = ::fwrite(&byte, 1U, 1U, m_file);
if (n != 1U) { if (n != 1U) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(buffer, length); n = ::fwrite(buffer, 1U, length, m_file);
if (n != length) { if (n != length) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
@ -221,12 +258,14 @@ void CDVTOOLFileWriter::close()
{ {
writeTrailer(); writeTrailer();
m_file.Seek(m_offset); ::fseek(m_file, m_offset, SEEK_SET);
wxUint32 count = wxUINT32_SWAP_ON_LE(m_count); // wxUINT32_SWAP_ON_LE produces a big-endian value
m_file.Write(&count, sizeof(wxUint32)); uint32_t count = htobe32(m_count);
::fwrite(&count, 1U, sizeof(uint32_t), m_file);
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
} }
bool CDVTOOLFileWriter::writeHeader(const CHeaderData& header) bool CDVTOOLFileWriter::writeHeader(const CHeaderData& header)
@ -238,61 +277,67 @@ bool CDVTOOLFileWriter::writeHeader(const CHeaderData& header)
buffer[2] = header.getFlag3(); buffer[2] = header.getFlag3();
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[3 + i] = header.getRptCall1().GetChar(i); buffer[3 + i] = header.getRptCall1()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[11 + i] = header.getRptCall2().GetChar(i); buffer[11 + i] = header.getRptCall2()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[19 + i] = header.getYourCall().GetChar(i); buffer[19 + i] = header.getYourCall()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[27 + i] = header.getMyCall1().GetChar(i); buffer[27 + i] = header.getMyCall1()[i];
for (unsigned int i = 0U; i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < SHORT_CALLSIGN_LENGTH; i++)
buffer[35 + i] = header.getMyCall2().GetChar(i); buffer[35 + i] = header.getMyCall2()[i];
// Get the checksum for the header // Get the checksum for the header
CCCITTChecksumReverse csum; CCCITTChecksumReverse csum;
csum.update(buffer, RADIO_HEADER_LENGTH_BYTES - 2U); csum.update(buffer, RADIO_HEADER_LENGTH_BYTES - 2U);
csum.result(buffer + 39U); csum.result(buffer + 39U);
wxUint16 len = wxUINT16_SWAP_ON_BE(RADIO_HEADER_LENGTH_BYTES + 15U); uint16_t len = htole16(RADIO_HEADER_LENGTH_BYTES + 15U);
size_t n = m_file.Write(&len, sizeof(wxUint16)); size_t n = ::fwrite(&len, 1U, sizeof(uint16_t), m_file);
if (n != sizeof(wxUint16)) { if (n != sizeof(uint16_t)) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(DSVT_SIGNATURE, DSVT_SIGNATURE_LENGTH); n = ::fwrite(DSVT_SIGNATURE, 1U, DSVT_SIGNATURE_LENGTH, m_file);
if (n != DSVT_SIGNATURE_LENGTH) { if (n != DSVT_SIGNATURE_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
char byte = HEADER_FLAG; char byte = HEADER_FLAG;
n = m_file.Write(&byte, 1U); n = ::fwrite(&byte, 1U, 1U, m_file);
if (n != 1U) { if (n != 1U) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(FIXED_DATA, FIXED_DATA_LENGTH); n = ::fwrite(FIXED_DATA, 1U, FIXED_DATA_LENGTH, m_file);
if (n != FIXED_DATA_LENGTH) { if (n != FIXED_DATA_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
byte = HEADER_MASK; byte = HEADER_MASK;
n = m_file.Write(&byte, 1U); n = ::fwrite(&byte, 1U, 1U, m_file);
if (n != 1U) { if (n != 1U) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(buffer, RADIO_HEADER_LENGTH_BYTES); n = ::fwrite(buffer, 1U, RADIO_HEADER_LENGTH_BYTES, m_file);
if (n != RADIO_HEADER_LENGTH_BYTES) { if (n != RADIO_HEADER_LENGTH_BYTES) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
@ -303,42 +348,48 @@ bool CDVTOOLFileWriter::writeHeader(const CHeaderData& header)
bool CDVTOOLFileWriter::writeTrailer() bool CDVTOOLFileWriter::writeTrailer()
{ {
wxUint16 len = wxUINT16_SWAP_ON_BE(27U); uint16_t len = htole16(27U);
size_t n = m_file.Write(&len, sizeof(wxUint16)); size_t n = ::fwrite(&len, 1U, sizeof(uint16_t), m_file);
if (n != sizeof(wxUint16)) { if (n != sizeof(uint16_t)) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(DSVT_SIGNATURE, DSVT_SIGNATURE_LENGTH); n = ::fwrite(DSVT_SIGNATURE, 1U, DSVT_SIGNATURE_LENGTH, m_file);
if (n != DSVT_SIGNATURE_LENGTH) { if (n != DSVT_SIGNATURE_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
char byte = DATA_FLAG; char byte = DATA_FLAG;
n = m_file.Write(&byte, 1U); n = ::fwrite(&byte, 1U, 1U, m_file);
if (n != 1U) { if (n != 1U) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(FIXED_DATA, FIXED_DATA_LENGTH); n = ::fwrite(FIXED_DATA, 1U, FIXED_DATA_LENGTH, m_file);
if (n != FIXED_DATA_LENGTH) { if (n != FIXED_DATA_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
byte = TRAILER_MASK | m_sequence; byte = (char)(TRAILER_MASK | m_sequence);
n = m_file.Write(&byte, 1U); n = ::fwrite(&byte, 1U, 1U, m_file);
if (n != 1U) { if (n != 1U) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }
n = m_file.Write(TRAILER_DATA, TRAILER_DATA_LENGTH); n = ::fwrite(TRAILER_DATA, 1U, TRAILER_DATA_LENGTH, m_file);
if (n != TRAILER_DATA_LENGTH) { if (n != TRAILER_DATA_LENGTH) {
m_file.Close(); ::fclose(m_file);
m_file = nullptr;
return false; return false;
} }

@ -21,31 +21,52 @@
#include "HeaderData.h" #include "HeaderData.h"
#include <wx/wx.h> #include "StdCompat.h"
#include <wx/ffile.h> #include <cstdio>
#include <cstdint>
/*
* Sequential writer for .dvtool recording files (see DVTOOLFileReader.h for
* the file format description).
*
* Usage:
* open() writes the "DVTOOL" signature and a placeholder record count,
* then writes the header record.
* write() appends each DV data frame as a DSVT data record.
* close() writes the trailer record, then seeks back to the placeholder
* and fills in the final record count (big-endian uint32).
*
* Two open() overloads are provided:
* open(filename, header) uses a caller-supplied base filename.
* open(header) auto-generates a filename from a timestamp and
* the callsigns embedded in the header.
*
* setDirectory() is a class-level (static) setting that prepends a directory
* to all generated filenames.
*/
class CDVTOOLFileWriter { class CDVTOOLFileWriter {
public: public:
CDVTOOLFileWriter(); CDVTOOLFileWriter();
~CDVTOOLFileWriter(); ~CDVTOOLFileWriter();
static void setDirectory(const wxString& dirName); // Sets the output directory used by all CDVTOOLFileWriter instances.
static void setDirectory(const std::string& dirName);
wxString getFileName() const; std::string getFileName() const;
bool open(const CHeaderData& header); bool open(const CHeaderData& header);
bool open(const wxString& filename, const CHeaderData& header); bool open(const std::string& filename, const CHeaderData& header);
bool write(const unsigned char* buffer, unsigned int length); bool write(const unsigned char* buffer, unsigned int length);
void close(); void close();
private: private:
static wxString m_dirName; static std::string m_dirName;
wxString m_fileName; std::string m_fileName;
wxFFile m_file; FILE* m_file;
wxUint32 m_count; uint32_t m_count; // Number of DSVT records written (header + data + trailer).
unsigned int m_sequence; unsigned int m_sequence; // Per-frame sequence counter (020, mirrors DSRP wire format).
wxFileOffset m_offset; long m_offset; // File offset of the record-count field, for back-patching in close().
bool writeHeader(const CHeaderData& header); bool writeHeader(const CHeaderData& header);
bool writeTrailer(); bool writeTrailer();

@ -13,8 +13,6 @@
#include "DummyController.h" #include "DummyController.h"
#include <wx/wx.h>
CDummyController::CDummyController() CDummyController::CDummyController()
{ {
} }

@ -0,0 +1,35 @@
/*
* Cross-platform byte-swap functions for the D-Star wire format.
*
* D-Star protocol fields are little-endian on the wire. This header provides
* htole16/le16toh/htole32/le32toh/htobe32/be32toh using the appropriate
* platform API (POSIX <endian.h> on Linux, OSByteOrder on macOS, and
* _byteswap intrinsics on Windows x86). Include this wherever protocol
* buffers are serialised or deserialised.
*/
#ifndef EndianCompat_H
#define EndianCompat_H
#if defined(_WIN32)
#include <cstdlib>
// Windows: use _byteswap intrinsics
static inline uint16_t htole16(uint16_t x) { return x; } // x86 is little-endian
static inline uint16_t le16toh(uint16_t x) { return x; }
static inline uint32_t htole32(uint32_t x) { return x; }
static inline uint32_t le32toh(uint32_t x) { return x; }
static inline uint32_t htobe32(uint32_t x) { return _byteswap_ulong(x); }
static inline uint32_t be32toh(uint32_t x) { return _byteswap_ulong(x); }
#elif defined(__APPLE__)
#include <libkern/OSByteOrder.h>
#define htole16(x) OSSwapHostToLittleInt16(x)
#define le16toh(x) OSSwapLittleToHostInt16(x)
#define htole32(x) OSSwapHostToLittleInt32(x)
#define le32toh(x) OSSwapLittleToHostInt32(x)
#define htobe32(x) OSSwapHostToBigInt32(x)
#define be32toh(x) OSSwapBigToHostInt32(x)
#else
#include <endian.h>
#endif
#endif

@ -15,8 +15,10 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include <chrono>
#include <thread>
CExternalController::CExternalController(IHardwareController* controller, bool pttInvert) : CExternalController::CExternalController(IHardwareController* controller, bool pttInvert) :
wxThread(wxTHREAD_JOINABLE),
m_controller(controller), m_controller(controller),
m_pttInvert(pttInvert), m_pttInvert(pttInvert),
m_disable(false), m_disable(false),
@ -27,10 +29,11 @@ m_out1(false),
m_out2(false), m_out2(false),
m_out3(false), m_out3(false),
m_out4(false), m_out4(false),
m_kill(false) m_kill(false),
m_thread()
{ {
// wxASSERT(controller != NULL); // When PTT is active-low the idle state is high (i.e. inverted true), so
// pre-set m_radioTX accordingly before the thread starts writing to hardware.
if (m_pttInvert) if (m_pttInvert)
m_radioTX = true; m_radioTX = true;
} }
@ -46,42 +49,45 @@ bool CExternalController::open()
if (!res) if (!res)
return false; return false;
Create(); m_thread = std::thread(&CExternalController::entry, this);
Run();
return true; return true;
} }
void* CExternalController::Entry() void CExternalController::entry()
{ {
wxASSERT(m_controller != NULL); bool dummy1, dummy2, dummy3, dummy4, disableIn;
bool dummy1, dummy2, dummy3, dummy4;
// Poll the hardware at half the D-Star frame rate (every 10 ms).
// Apply desired output states, then latch the disable input for the repeater.
while (!m_kill) { while (!m_kill) {
m_controller->setDigitalOutputs(m_radioTX, false, m_heartbeat, m_active, m_out1, m_out2, m_out3, m_out4); m_controller->setDigitalOutputs(m_radioTX, false, m_heartbeat, m_active, m_out1, m_out2, m_out3, m_out4);
m_controller->getDigitalInputs(dummy1, dummy2, dummy3, dummy4, m_disable); m_controller->getDigitalInputs(dummy1, dummy2, dummy3, dummy4, disableIn);
m_disable = disableIn;
Sleep(DSTAR_FRAME_TIME_MS / 2U); std::this_thread::sleep_for(std::chrono::milliseconds(DSTAR_FRAME_TIME_MS / 2U));
} }
// Shutdown sequence: de-assert PTT (accounting for inversion), wait for the
// transmitter to drop (3 frame periods = 60 ms), then confirm the idle state
// a second time before closing the hardware device.
if (m_pttInvert) if (m_pttInvert)
m_controller->setDigitalOutputs(true, false, false, false, false, false, false, false); m_controller->setDigitalOutputs(true, false, false, false, false, false, false, false);
else else
m_controller->setDigitalOutputs(false, false, false, false, false, false, false, false); m_controller->setDigitalOutputs(false, false, false, false, false, false, false, false);
m_controller->getDigitalInputs(dummy1, dummy2, dummy3, dummy4, m_disable); m_controller->getDigitalInputs(dummy1, dummy2, dummy3, dummy4, disableIn);
m_disable = disableIn;
Sleep(DSTAR_FRAME_TIME_MS * 3U); std::this_thread::sleep_for(std::chrono::milliseconds(DSTAR_FRAME_TIME_MS * 3U));
if (m_pttInvert) if (m_pttInvert)
m_controller->setDigitalOutputs(true, false, false, false, false, false, false, false); m_controller->setDigitalOutputs(true, false, false, false, false, false, false, false);
else else
m_controller->setDigitalOutputs(false, false, false, false, false, false, false, false); m_controller->setDigitalOutputs(false, false, false, false, false, false, false, false);
m_controller->getDigitalInputs(dummy1, dummy2, dummy3, dummy4, m_disable); m_controller->getDigitalInputs(dummy1, dummy2, dummy3, dummy4, disableIn);
m_disable = disableIn;
m_controller->close(); m_controller->close();
return NULL;
} }
bool CExternalController::getDisable() const bool CExternalController::getDisable() const
@ -99,6 +105,7 @@ void CExternalController::setRadioTransmit(bool value)
void CExternalController::setHeartbeat() void CExternalController::setHeartbeat()
{ {
// Toggle on each call so any periodic caller drives a visible LED blink.
m_heartbeat = !m_heartbeat; m_heartbeat = !m_heartbeat;
} }
@ -131,5 +138,6 @@ void CExternalController::close()
{ {
m_kill = true; m_kill = true;
Wait(); if (m_thread.joinable())
m_thread.join();
} }

@ -11,46 +11,83 @@
* GNU General Public License for more details. * GNU General Public License for more details.
*/ */
/*
* CExternalController wraps an IHardwareController (serial port, Arduino,
* GPIO board, etc.) and drives it from a dedicated background thread.
*
* The repeater thread calls the set* methods to express the desired output
* state (PTT, heartbeat LED, active indicator, auxiliary outputs). The
* internal thread polls the hardware at half the D-Star frame rate (every
* 10 ms) and applies those values to the physical I/O pins. Input pins
* (the "disable" line) are read on the same schedule and stored atomically
* so the repeater thread can check them without blocking.
*
* PTT inversion
* -------------
* Some hardware asserts PTT by pulling a line low rather than high. When
* pttInvert is true the radioTX sense is flipped before being written to the
* hardware, and the idle state is initialised to the inverted (asserted) value
* so the line is de-asserted once the thread starts.
*
* Shutdown sequence
* -----------------
* close() sets m_kill and joins the thread. The thread then de-asserts PTT,
* waits three frame periods (60 ms) to let the transmitter drop, asserts the
* final idle state once more, then calls m_controller->close().
*/
#ifndef ExternalController_H #ifndef ExternalController_H
#define ExternalController_H #define ExternalController_H
#include "HardwareController.h" #include "HardwareController.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <atomic>
#include <thread>
class CExternalController : public wxThread { class CExternalController {
public: public:
CExternalController(IHardwareController* controller, bool pttInvert); CExternalController(IHardwareController* controller, bool pttInvert);
virtual ~CExternalController(); virtual ~CExternalController();
// Open the hardware device and start the polling thread.
virtual bool open(); virtual bool open();
// True when the hardware disable input is asserted (repeater should not TX).
virtual bool getDisable() const; virtual bool getDisable() const;
// Set the desired PTT state; inverted before applying if pttInvert is set.
virtual void setRadioTransmit(bool value); virtual void setRadioTransmit(bool value);
// Toggle the heartbeat output each call (caller drives the toggle rate).
virtual void setHeartbeat(); virtual void setHeartbeat();
// Assert or de-assert the "active" status output.
virtual void setActive(bool value); virtual void setActive(bool value);
virtual void setOutput1(bool value); virtual void setOutput1(bool value);
virtual void setOutput2(bool value); virtual void setOutput2(bool value);
virtual void setOutput3(bool value); virtual void setOutput3(bool value);
virtual void setOutput4(bool value); virtual void setOutput4(bool value);
// Signal the polling thread to stop, then close the hardware device.
virtual void close(); virtual void close();
virtual void* Entry();
private: private:
IHardwareController* m_controller; void entry(); // Polling thread body
bool m_pttInvert;
bool m_disable; IHardwareController* m_controller; // Concrete hardware driver (owned)
bool m_heartbeat; bool m_pttInvert; // True if PTT sense is active-low
bool m_active;
bool m_radioTX; // Desired output states written by the repeater thread and read by entry().
bool m_out1; // All are atomic so no mutex is needed for the shared flag pattern.
bool m_out2; std::atomic<bool> m_disable; // Latched from the hardware disable input
bool m_out3; std::atomic<bool> m_heartbeat; // Heartbeat/LED toggle
bool m_out4; std::atomic<bool> m_active; // Repeater-active indicator
bool m_kill; std::atomic<bool> m_radioTX; // PTT output (post-inversion)
std::atomic<bool> m_out1; // Auxiliary outputs 14
std::atomic<bool> m_out2;
std::atomic<bool> m_out3;
std::atomic<bool> m_out4;
std::atomic<bool> m_kill; // Set by close() to stop the poll loop
std::thread m_thread;
}; };
#endif #endif

@ -14,27 +14,30 @@
#include "FIRFilter.h" #include "FIRFilter.h"
CFIRFilter::CFIRFilter(const wxFloat32* taps, unsigned int length) : #include <cassert>
m_taps(NULL), #include <cstring>
CFIRFilter::CFIRFilter(const float* taps, unsigned int length) :
m_taps(nullptr),
m_length(length), m_length(length),
m_buffer(NULL), m_buffer(nullptr),
m_bufLen(20U * m_length), m_bufLen(20U * m_length),
m_pointer(length) m_pointer(length)
{ {
wxASSERT(taps != NULL); assert(taps != nullptr);
wxASSERT(length > 0U); assert(length > 0U);
m_taps = new wxFloat32[m_length]; m_taps = new float[m_length];
m_buffer = new wxFloat32[m_bufLen]; m_buffer = new float[m_bufLen];
::memcpy(m_taps, taps, m_length * sizeof(wxFloat32)); ::memcpy(m_taps, taps, m_length * sizeof(float));
::memset(m_buffer, 0x00, m_bufLen * sizeof(wxFloat32)); ::memset(m_buffer, 0x00, m_bufLen * sizeof(float));
} }
CFIRFilter::CFIRFilter() : CFIRFilter::CFIRFilter() :
m_taps(NULL), m_taps(nullptr),
m_length(0U), m_length(0U),
m_buffer(NULL), m_buffer(nullptr),
m_bufLen(0U), m_bufLen(0U),
m_pointer(0U) m_pointer(0U)
{ {
@ -46,10 +49,10 @@ CFIRFilter::~CFIRFilter()
delete[] m_buffer; delete[] m_buffer;
} }
void CFIRFilter::setTaps(const wxFloat32* taps, unsigned int length) void CFIRFilter::setTaps(const float* taps, unsigned int length)
{ {
wxASSERT(taps != NULL); assert(taps != nullptr);
wxASSERT(length > 0U); assert(length > 0U);
delete[] m_taps; delete[] m_taps;
delete[] m_buffer; delete[] m_buffer;
@ -58,46 +61,46 @@ void CFIRFilter::setTaps(const wxFloat32* taps, unsigned int length)
m_pointer = length; m_pointer = length;
m_bufLen = 20U * m_length; m_bufLen = 20U * m_length;
m_taps = new wxFloat32[m_length]; m_taps = new float[m_length];
m_buffer = new wxFloat32[m_bufLen]; m_buffer = new float[m_bufLen];
::memcpy(m_taps, taps, m_length * sizeof(wxFloat32)); ::memcpy(m_taps, taps, m_length * sizeof(float));
::memset(m_buffer, 0x00, m_bufLen * sizeof(wxFloat32)); ::memset(m_buffer, 0x00, m_bufLen * sizeof(float));
} }
wxFloat32 CFIRFilter::process(wxFloat32 val) float CFIRFilter::process(float val)
{ {
wxFloat32* ptr = m_buffer + m_pointer++; float* ptr = m_buffer + m_pointer++;
*ptr = val; *ptr = val;
wxFloat32* a = ptr - m_length; float* a = ptr - m_length;
wxFloat32* b = m_taps; float* b = m_taps;
wxFloat32 out = 0.0F; float out = 0.0F;
for (unsigned int i = 0U; i < m_length; i++) for (unsigned int i = 0U; i < m_length; i++)
out += (*a++) * (*b++); out += (*a++) * (*b++);
if (m_pointer == m_bufLen) { if (m_pointer == m_bufLen) {
::memcpy(m_buffer, m_buffer + m_bufLen - m_length, m_length * sizeof(wxFloat32)); ::memcpy(m_buffer, m_buffer + m_bufLen - m_length, m_length * sizeof(float));
m_pointer = m_length; m_pointer = m_length;
} }
return out; return out;
} }
void CFIRFilter::process(wxFloat32* inOut, unsigned int length) void CFIRFilter::process(float* inOut, unsigned int length)
{ {
wxASSERT(inOut != NULL); assert(inOut != nullptr);
for (unsigned int i = 0U; i < length; i++) for (unsigned int i = 0U; i < length; i++)
inOut[i] = process(inOut[i]); inOut[i] = process(inOut[i]);
} }
void CFIRFilter::process(const wxFloat32* in, wxFloat32* out, unsigned int length) void CFIRFilter::process(const float* in, float* out, unsigned int length)
{ {
wxASSERT(in != NULL); assert(in != nullptr);
wxASSERT(out != NULL); assert(out != nullptr);
for (unsigned int i = 0U; i < length; i++) for (unsigned int i = 0U; i < length; i++)
out[i] = process(in[i]); out[i] = process(in[i]);
@ -105,5 +108,5 @@ void CFIRFilter::process(const wxFloat32* in, wxFloat32* out, unsigned int lengt
void CFIRFilter::reset() void CFIRFilter::reset()
{ {
::memset(m_buffer, 0x00, m_bufLen * sizeof(wxFloat32));; ::memset(m_buffer, 0x00, m_bufLen * sizeof(float));
} }

@ -14,26 +14,33 @@
#ifndef FIRFilter_H #ifndef FIRFilter_H
#define FIRFilter_H #define FIRFilter_H
#include <wx/wx.h> /*
* Direct-form FIR (Finite Impulse Response) filter operating on float samples.
*
* Used by the GMSK modulator and demodulator to implement the Gaussian pulse-
* shaping filter required by the D-Star GMSK modulation scheme (BT=0.5).
*
* process() convolves input samples with the tap coefficients using a circular
* delay buffer. reset() clears the delay line without changing the tap set.
*/
class CFIRFilter { class CFIRFilter {
public: public:
CFIRFilter(const wxFloat32* taps, unsigned int length); CFIRFilter(const float* taps, unsigned int length);
CFIRFilter(); CFIRFilter();
~CFIRFilter(); ~CFIRFilter();
void setTaps(const wxFloat32* taps, unsigned int length); void setTaps(const float* taps, unsigned int length);
wxFloat32 process(wxFloat32 val); float process(float val);
void process(wxFloat32* inOut, unsigned int length); void process(float* inOut, unsigned int length);
void process(const wxFloat32* in, wxFloat32* out, unsigned int length); void process(const float* in, float* out, unsigned int length);
void reset(); void reset();
private: private:
wxFloat32* m_taps; float* m_taps;
unsigned int m_length; unsigned int m_length;
wxFloat32* m_buffer; float* m_buffer; // Circular delay line for the filter state.
unsigned int m_bufLen; unsigned int m_bufLen;
unsigned int m_pointer; unsigned int m_pointer;
}; };

@ -17,11 +17,13 @@
*/ */
#include "GMSKController.h" #include "GMSKController.h"
#if defined(__WINDOWS__)
#include "GMSKModemWinUSB.h"
#endif
#include "GMSKModemLibUsb.h" #include "GMSKModemLibUsb.h"
#include "Timer.h" #include "Timer.h"
#include "Logger.h"
#include <cassert>
#include <chrono>
#include <thread>
const unsigned char DVRPTR_HEADER_LENGTH = 5U; const unsigned char DVRPTR_HEADER_LENGTH = 5U;
@ -31,30 +33,16 @@ const unsigned int CYCLE_TIME = 15U;
CGMSKController::CGMSKController(USB_INTERFACE iface, unsigned int address, bool duplex) : CGMSKController::CGMSKController(USB_INTERFACE iface, unsigned int address, bool duplex) :
CModem(), CModem(),
m_modem(NULL), m_modem(nullptr),
m_duplex(duplex), m_duplex(duplex),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U) m_txData(1000U)
{ {
wxASSERT(address > 0U); assert(address > 0U);
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
#if defined(__WINDOWS__)
switch (iface) {
case UI_LIBUSB:
m_modem = new CGMSKModemLibUsb(address);
break;
case UI_WINUSB:
m_modem = new CGMSKModemWinUSB(address);
break;
default:
wxLogError(wxT("Unknown GMSM modem driver type - %d"), int(iface));
break;
}
#else
m_modem = new CGMSKModemLibUsb(address); m_modem = new CGMSKModemLibUsb(address);
#endif
} }
CGMSKController::~CGMSKController() CGMSKController::~CGMSKController()
@ -64,7 +52,7 @@ CGMSKController::~CGMSKController()
bool CGMSKController::start() bool CGMSKController::start()
{ {
if (m_modem == NULL) if (m_modem == nullptr)
return false; return false;
bool ret = m_modem->open(); bool ret = m_modem->open();
@ -73,16 +61,14 @@ bool CGMSKController::start()
return false; return false;
} }
Create(); m_thread = std::thread(&CGMSKController::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CGMSKController::Entry() void CGMSKController::entry()
{ {
wxLogMessage(wxT("Starting GMSK Modem Controller thread")); wxLogMessage("Starting GMSK Modem Controller thread");
CTimer hdrTimer(1000U, 0U, 100U); CTimer hdrTimer(1000U, 0U, 100U);
hdrTimer.start(); hdrTimer.start();
@ -98,10 +84,8 @@ void* CGMSKController::Entry()
unsigned char readLength = 0U; unsigned char readLength = 0U;
unsigned char* readBuffer = new unsigned char[DV_FRAME_LENGTH_BYTES]; unsigned char* readBuffer = new unsigned char[DV_FRAME_LENGTH_BYTES];
wxStopWatch stopWatch;
while (!m_stopped) { while (!m_stopped) {
stopWatch.Start(); auto loopStart = std::chrono::steady_clock::now();
// Only receive when not transmitting or when in duplex mode // Only receive when not transmitting or when in duplex mode
if (!m_tx || m_duplex) { if (!m_tx || m_duplex) {
@ -110,10 +94,10 @@ void* CGMSKController::Entry()
bool end; bool end;
int ret = m_modem->readData(buffer, GMSK_MODEM_DATA_LENGTH, end); int ret = m_modem->readData(buffer, GMSK_MODEM_DATA_LENGTH, end);
if (ret >= 0) { if (ret >= 0) {
// CUtils::dump(wxT("Read Data"), buffer, ret); // CUtils::dump("Read Data", buffer, ret);
if (end) { if (end) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -129,7 +113,7 @@ void* CGMSKController::Entry()
readLength++; readLength++;
if (readLength >= DV_FRAME_LENGTH_BYTES) { if (readLength >= DV_FRAME_LENGTH_BYTES) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -152,9 +136,9 @@ void* CGMSKController::Entry()
unsigned char buffer[90U]; unsigned char buffer[90U];
bool ret = m_modem->readHeader(buffer, 90U); bool ret = m_modem->readHeader(buffer, 90U);
if (ret) { if (ret) {
// CUtils::dump(wxT("Read Header"), buffer, RADIO_HEADER_LENGTH_BYTES); // CUtils::dump("Read Header", buffer, RADIO_HEADER_LENGTH_BYTES);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -176,7 +160,7 @@ void* CGMSKController::Entry()
// Only transmit when not receiving or when in duplex mode // Only transmit when not receiving or when in duplex mode
if (!rx || m_duplex) { if (!rx || m_duplex) {
if (writeLength == 0U && m_txData.hasData()) { if (writeLength == 0U && m_txData.hasData()) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_NONE; unsigned char type = DSMTT_NONE;
m_txData.getData(&type, 1U); m_txData.getData(&type, 1U);
@ -192,7 +176,7 @@ void* CGMSKController::Entry()
// Check that the modem isn't still transmitting before sending the new header // Check that the modem isn't still transmitting before sending the new header
if (tx == STATE_FALSE) { if (tx == STATE_FALSE) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
m_modem->writeHeader(writeBuffer, writeLength); m_modem->writeHeader(writeBuffer, writeLength);
m_modem->setPTT(true); m_modem->setPTT(true);
dataTimer.start(); dataTimer.start();
@ -212,7 +196,7 @@ void* CGMSKController::Entry()
// Check that there is space in the modem buffer // Check that there is space in the modem buffer
if (ret == STATE_TRUE) { if (ret == STATE_TRUE) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength); // CUtils::dump("Write Data", writeBuffer, writeLength);
int ret = m_modem->writeData(writeBuffer, writeLength); int ret = m_modem->writeData(writeBuffer, writeLength);
if (ret > 0) { if (ret > 0) {
writeLength -= ret; writeLength -= ret;
@ -236,14 +220,16 @@ void* CGMSKController::Entry()
} }
} }
unsigned long ms = stopWatch.Time(); auto loopEnd = std::chrono::steady_clock::now();
unsigned long ms = std::chrono::duration_cast<std::chrono::milliseconds>(loopEnd - loopStart).count();
// Don't sleep when reading from the modem // Don't sleep when reading from the modem
if (!rx) { if (!rx) {
if (ms < CYCLE_TIME) if (ms < CYCLE_TIME)
Sleep(CYCLE_TIME - ms); std::this_thread::sleep_for(std::chrono::milliseconds(CYCLE_TIME - ms));
ms = stopWatch.Time(); loopEnd = std::chrono::steady_clock::now();
ms = std::chrono::duration_cast<std::chrono::milliseconds>(loopEnd - loopStart).count();
} }
// Catch up with the clock // Catch up with the clock
@ -251,24 +237,22 @@ void* CGMSKController::Entry()
hdrTimer.clock(ms); hdrTimer.clock(ms);
} }
wxLogMessage(wxT("Stopping GMSK Modem Controller thread")); wxLogMessage("Stopping GMSK Modem Controller thread");
if (m_modem != NULL) { if (m_modem != nullptr) {
m_modem->close(); m_modem->close();
delete m_modem; delete m_modem;
} }
delete[] writeBuffer; delete[] writeBuffer;
delete[] readBuffer; delete[] readBuffer;
return NULL;
} }
bool CGMSKController::writeHeader(const CHeaderData& header) bool CGMSKController::writeHeader(const CHeaderData& header)
{ {
bool ret = m_txData.hasSpace(RADIO_HEADER_LENGTH_BYTES); bool ret = m_txData.hasSpace(RADIO_HEADER_LENGTH_BYTES);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -280,27 +264,27 @@ bool CGMSKController::writeHeader(const CHeaderData& header)
buffer[1U] = header.getFlag2(); buffer[1U] = header.getFlag2();
buffer[2U] = header.getFlag3(); buffer[2U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 3U] = rpt2.GetChar(i); buffer[i + 3U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 11U] = rpt1.GetChar(i); buffer[i + 11U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 19U] = your.GetChar(i); buffer[i + 19U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 27U] = my1.GetChar(i); buffer[i + 27U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 35U] = my2.GetChar(i); buffer[i + 35U] = my2[i];
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -316,11 +300,11 @@ bool CGMSKController::writeData(const unsigned char* data, unsigned int, bool en
{ {
bool ret = m_txData.hasSpace(DV_FRAME_LENGTH_BYTES + 2U); bool ret = m_txData.hasSpace(DV_FRAME_LENGTH_BYTES + 2U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char buffer[2U]; unsigned char buffer[2U];
buffer[0U] = end ? DSMTT_EOT : DSMTT_DATA; buffer[0U] = end ? DSMTT_EOT : DSMTT_DATA;
@ -347,7 +331,7 @@ bool CGMSKController::isTXReady()
bool CGMSKController::reopenModem() bool CGMSKController::reopenModem()
{ {
wxLogMessage(wxT("Connection to the GMSK modem has been lost")); wxLogMessage("Connection to the GMSK modem has been lost");
m_modem->close(); m_modem->close();
@ -357,18 +341,18 @@ bool CGMSKController::reopenModem()
return true; return true;
// Reset the drivers state // Reset the drivers state
m_mutex.Lock(); {
std::lock_guard<std::mutex> lock(m_mutex);
m_txData.clear(); m_txData.clear();
m_mutex.Unlock(); }
m_tx = false; m_tx = false;
Sleep(1000UL); std::this_thread::sleep_for(std::chrono::milliseconds(1000));
} }
delete m_modem; delete m_modem;
m_modem = NULL; m_modem = nullptr;
return false; return false;
} }

@ -24,16 +24,36 @@
#include "GMSKModem.h" #include "GMSKModem.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> /*
* CGMSKController - CModem driver that bridges the repeater to an IGMSKModem.
*
* Owns an IGMSKModem* (currently always a CGMSKModemLibUsb instance) and
* presents the standard CModem interface to the repeater thread.
*
* entry() loop (runs ~15ms cycle):
* Receive side: polls readHeader() every 100ms when idle. On a valid header,
* switches to data mode and calls readData() every loop until end is signalled.
* Received bytes are accumulated in a local buffer until a full DV_FRAME_LENGTH
* (12 bytes) is available, then forwarded to m_rxData.
*
* Transmit side: reads type/length/data from m_txData ring buffer. For headers,
* waits until the hardware PTT is deasserted before calling writeHeader() +
* setPTT(true). For data frames, waits 100ms after the header (dataTimer) then
* polls hasSpace() before each writeData(). On EOT, calls setPTT(false).
*
* duplex: when true, RX and TX can occur simultaneously. When false, TX
* suppresses RX polling and vice versa.
*
* reopenModem(): called on any negative return from an IGMSKModem method.
* Closes the modem, clears m_txData, and retries open() every 1s indefinitely.
*/
class CGMSKController : public CModem { class CGMSKController : public CModem {
public: public:
CGMSKController(USB_INTERFACE iface, unsigned int address, bool duplex); CGMSKController(USB_INTERFACE iface, unsigned int address, bool duplex);
virtual ~CGMSKController(); virtual ~CGMSKController();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -43,6 +63,8 @@ public:
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
private: private:
void entry();
IGMSKModem* m_modem; IGMSKModem* m_modem;
bool m_duplex; bool m_duplex;
unsigned char* m_buffer; unsigned char* m_buffer;

@ -21,8 +21,20 @@
#include "Utils.h" #include "Utils.h"
#include <wx/wx.h> /*
* IGMSKModem - Abstract interface for GMSK modem hardware.
*
* Implemented by CGMSKModemLibUsb (libusb) and potentially other backends.
* CGMSKController owns an IGMSKModem* and drives it through this interface,
* so the controller is independent of the USB transport details.
*
* The interface divides into three functional groups:
* Lifecycle: open(), close()
* Receive: readHeader(), readData() - poll-based; called from controller loop
* Transmit: writeHeader(), writeData(), setPTT() - push-based from controller
* Status: getPTT(), hasSpace() - return TRISTATE (TRUE/FALSE/UNKNOWN)
* UNKNOWN indicates a hardware error requiring modem reconnection.
*/
class IGMSKModem { class IGMSKModem {
public: public:
virtual ~IGMSKModem() = 0; virtual ~IGMSKModem() = 0;
@ -46,4 +58,3 @@ private:
}; };
#endif #endif

@ -18,6 +18,12 @@
#include "GMSKModemLibUsb.h" #include "GMSKModemLibUsb.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include <cassert>
#include <chrono>
#include <thread>
#include <string>
const unsigned int VENDOR_ID = 0x04D8U; const unsigned int VENDOR_ID = 0x04D8U;
@ -46,118 +52,48 @@ const char PTT_OnOff = 0x20;
const int PTT_ON = 1; const int PTT_ON = 1;
const int PTT_OFF = 0; const int PTT_OFF = 0;
#if defined(WIN32)
const wxString LIBNAME = wxT("libusb0");
wxDynamicLibrary* CGMSKModemLibUsb::m_library = NULL;
bool CGMSKModemLibUsb::m_loaded = false;
void (*CGMSKModemLibUsb::m_usbInit)() = NULL;
int (*CGMSKModemLibUsb::m_usbFindBusses)() = NULL;
int (*CGMSKModemLibUsb::m_usbFindDevices)() = NULL;
usb_bus* (*CGMSKModemLibUsb::m_usbGetBusses)() = NULL;
usb_dev_handle* (*CGMSKModemLibUsb::m_usbOpen)(struct usb_device*) = NULL;
int (*CGMSKModemLibUsb::m_usbSetConfiguration)(usb_dev_handle*, int) = NULL;
int (*CGMSKModemLibUsb::m_usbControlMsg)(usb_dev_handle*, int, int, int, int, unsigned char*, int, int) = NULL;
char* (*CGMSKModemLibUsb::m_usbStrerror)() = NULL;
int (*CGMSKModemLibUsb::m_usbClose)(usb_dev_handle*) = NULL;
#endif
static void libUsbLogError(int ret, const char *message) { static void libUsbLogError(int ret, const char *message) {
#if defined(WIN32) std::string errorText(libusb_error_name(ret));
wxString errorText(CGMSKModemLibUsb::m_usbStrerror(), wxConvLocal); wxLogMessage("%s, ret: %d, err=%s", message, ret, errorText.c_str());
#else
wxString errorText(libusb_error_name(ret), wxConvLocal);
#endif
wxLogMessage(wxT("%s, ret: %d, err=%s"), message, ret, errorText.c_str());
} }
CGMSKModemLibUsb::CGMSKModemLibUsb(unsigned int address) : CGMSKModemLibUsb::CGMSKModemLibUsb(unsigned int address) :
m_address(address), m_address(address),
#if !defined(WIN32) m_context(nullptr),
m_context(NULL), m_dev(nullptr),
#endif
m_dev(NULL),
m_brokenSpace(false) m_brokenSpace(false)
{ {
#if defined(WIN32)
if (m_library == NULL)
m_library = new wxDynamicLibrary(LIBNAME);
if (!m_library->IsLoaded()) {
wxLogError(wxT("Unable to load shared library %s"), LIBNAME.c_str());
return;
}
void* ptr1 = m_library->GetSymbol(wxT("usb_init"));
void* ptr2 = m_library->GetSymbol(wxT("usb_find_busses"));
void* ptr3 = m_library->GetSymbol(wxT("usb_find_devices"));
void* ptr4 = m_library->GetSymbol(wxT("usb_get_busses"));
void* ptr5 = m_library->GetSymbol(wxT("usb_open"));
void* ptr6 = m_library->GetSymbol(wxT("usb_set_configuration"));
void* ptr7 = m_library->GetSymbol(wxT("usb_control_msg"));
void* ptr8 = m_library->GetSymbol(wxT("usb_strerror"));
void* ptr9 = m_library->GetSymbol(wxT("usb_close"));
if (ptr1 == NULL || ptr2 == NULL || ptr3 == NULL ||
ptr4 == NULL || ptr5 == NULL || ptr6 == NULL ||
ptr7 == NULL || ptr8 == NULL || ptr9 == NULL) {
wxLogError(wxT("Unable to get symbols from %s"), LIBNAME.c_str());
return;
}
m_usbInit = (void (*)())ptr1;
m_usbFindBusses = (int (*)())ptr2;
m_usbFindDevices = (int (*)())ptr3;
m_usbGetBusses = (usb_bus* (*)())ptr4;
m_usbOpen = (usb_dev_handle* (*)(struct usb_device*))ptr5;
m_usbSetConfiguration = (int (*)(usb_dev_handle*, int))ptr6;
m_usbControlMsg = (int (*)(usb_dev_handle*, int, int, int, int, unsigned char*, int, int))ptr7;
m_usbStrerror = (char* (*)())ptr8;
m_usbClose = (int (*)(usb_dev_handle*))ptr9;
wxLogMessage(wxT("Successfully loaded library %s"), LIBNAME.c_str());
m_loaded = true;
#else
::libusb_init(&m_context); ::libusb_init(&m_context);
#endif
} }
CGMSKModemLibUsb::~CGMSKModemLibUsb() CGMSKModemLibUsb::~CGMSKModemLibUsb()
{ {
#if !defined(WIN32) assert(m_context != nullptr);
wxASSERT(m_context != NULL);
::libusb_exit(m_context); ::libusb_exit(m_context);
#endif
} }
bool CGMSKModemLibUsb::open() bool CGMSKModemLibUsb::open()
{ {
#if !defined(WIN32) assert(m_context != nullptr);
wxASSERT(m_context != NULL); assert(m_dev == nullptr);
#endif
wxASSERT(m_dev == NULL);
bool ret1 = openModem(); bool ret1 = openModem();
if (!ret1) { if (!ret1) {
wxLogError(wxT("Cannot find the GMSK Modem with address: 0x%04X"), m_address); wxLogError("Cannot find the GMSK Modem with address: 0x%04X", m_address);
return false; return false;
} }
wxLogInfo(wxT("Found the GMSK Modem with address: 0x%04X"), m_address); wxLogInfo("Found the GMSK Modem with address: 0x%04X", m_address);
wxString version; std::string version;
int ret2; int ret2;
do { do {
unsigned char buffer[GMSK_MODEM_DATA_LENGTH]; unsigned char buffer[GMSK_MODEM_DATA_LENGTH];
ret2 = io(0xC0, GET_VERSION, 0, 0, buffer, GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT); ret2 = io(0xC0, GET_VERSION, 0, 0, buffer, GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT);
if (ret2 > 0) { if (ret2 > 0) {
wxString text((char *) buffer, wxConvLocal, ret2); version.append((char*)buffer, ret2);
version.Append(text);
} else if (ret2 < 0) { } else if (ret2 < 0) {
::libUsbLogError(ret2, "GET_VERSION"); ::libUsbLogError(ret2, "GET_VERSION");
close(); close();
@ -165,27 +101,34 @@ bool CGMSKModemLibUsb::open()
} }
} while (ret2 == int(GMSK_MODEM_DATA_LENGTH)); } while (ret2 == int(GMSK_MODEM_DATA_LENGTH));
wxLogInfo(wxT("Firmware version: %s"), version.c_str()); wxLogInfo("Firmware version: %s", version.c_str());
// Trap firmware version 0.1.00 of DUTCH*Star and complain loudly // Trap firmware version 0.1.00 of DUTCH*Star and complain loudly
if (version.Find(wxT("DUTCH*Star")) != wxNOT_FOUND && version.Find(wxT("0.1.00")) != wxNOT_FOUND) { if (version.find("DUTCH*Star") != std::string::npos && version.find("0.1.00") != std::string::npos) {
wxLogWarning(wxT("This modem firmware is not supported by the repeater")); wxLogWarning("This modem firmware is not supported by the repeater");
wxLogWarning(wxT("Please upgrade to a newer version")); wxLogWarning("Please upgrade to a newer version");
close(); close();
return false; return false;
} }
// DUTCH*Star firmware has a broken concept of free space // DUTCH*Star firmware has a broken concept of free space
if (version.Find(wxT("DUTCH*Star")) != wxNOT_FOUND) if (version.find("DUTCH*Star") != std::string::npos)
m_brokenSpace = true; m_brokenSpace = true;
return true; return true;
} }
// Polls the modem for a received D-Star header via repeated GET_HEADER transfers.
// Each transfer returns up to 8 bytes. The loop accumulates bytes until
// RADIO_HEADER_LENGTH_BYTES have been received.
// If GET_HEADER returns 0 bytes mid-header and the COS (carrier-operated squelch)
// status bit is still set, the header counter is reset to zero because the signal
// has restarted (e.g. a quick key-up/key-down) and the modem's header buffer has
// been overwritten. Returns false on CRC error (GET_AD_STATUS CRC_ERROR bit set).
bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length) bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
{ {
wxASSERT(header != NULL); assert(header != nullptr);
wxASSERT(length > (RADIO_HEADER_LENGTH_BYTES * 2U)); assert(length > (RADIO_HEADER_LENGTH_BYTES * 2U));
unsigned int offset = 0U; unsigned int offset = 0U;
@ -197,12 +140,12 @@ bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
if (ret == -19) // -ENODEV if (ret == -19) // -ENODEV
return false; return false;
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
} else if (ret == 0) { } else if (ret == 0) {
if (offset == 0U) if (offset == 0U)
return false; return false;
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
unsigned char status; unsigned char status;
int ret = io(0xC0, GET_AD_STATUS, 0, 0, &status, 1, USB_TIMEOUT); int ret = io(0xC0, GET_AD_STATUS, 0, 0, &status, 1, USB_TIMEOUT);
@ -212,7 +155,7 @@ bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
if (ret == -19) // -ENODEV if (ret == -19) // -ENODEV
return false; return false;
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
} else if (ret > 0) { } else if (ret > 0) {
if ((status & COS_OnOff) == COS_OnOff) if ((status & COS_OnOff) == COS_OnOff)
offset = 0U; offset = 0U;
@ -230,7 +173,7 @@ bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
} }
if ((status & CRC_ERROR) == CRC_ERROR) { if ((status & CRC_ERROR) == CRC_ERROR) {
wxLogMessage(wxT("Header - CRC Error")); wxLogMessage("Header - CRC Error");
return false; return false;
} }
@ -239,8 +182,8 @@ bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
int CGMSKModemLibUsb::readData(unsigned char* data, unsigned int length, bool& end) int CGMSKModemLibUsb::readData(unsigned char* data, unsigned int length, bool& end)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
wxASSERT(length > 0U); assert(length > 0U);
end = false; end = false;
@ -273,8 +216,8 @@ int CGMSKModemLibUsb::readData(unsigned char* data, unsigned int length, bool& e
void CGMSKModemLibUsb::writeHeader(unsigned char* header, unsigned int length) void CGMSKModemLibUsb::writeHeader(unsigned char* header, unsigned int length)
{ {
wxASSERT(header != NULL); assert(header != nullptr);
wxASSERT(length >= (RADIO_HEADER_LENGTH_BYTES - 2U)); assert(length >= (RADIO_HEADER_LENGTH_BYTES - 2U));
io(0x40, SET_MyCALL2, 0, 0, (header + 35U), SHORT_CALLSIGN_LENGTH, USB_TIMEOUT); io(0x40, SET_MyCALL2, 0, 0, (header + 35U), SHORT_CALLSIGN_LENGTH, USB_TIMEOUT);
io(0x40, SET_MyCALL, 0, 0, (header + 27U), LONG_CALLSIGN_LENGTH, USB_TIMEOUT); io(0x40, SET_MyCALL, 0, 0, (header + 27U), LONG_CALLSIGN_LENGTH, USB_TIMEOUT);
@ -322,8 +265,8 @@ void CGMSKModemLibUsb::setPTT(bool on)
int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length) int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
wxASSERT(length > 0U && length <= DV_FRAME_LENGTH_BYTES); assert(length > 0U && length <= DV_FRAME_LENGTH_BYTES);
if (length > GMSK_MODEM_DATA_LENGTH) { if (length > GMSK_MODEM_DATA_LENGTH) {
int ret = io(0x40, PUT_DATA, 0, 0, data, GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT); int ret = io(0x40, PUT_DATA, 0, 0, data, GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT);
@ -337,7 +280,7 @@ int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length)
} }
// Give libUSB some recovery time // Give libUSB some recovery time
::wxMilliSleep(3UL); std::this_thread::sleep_for(std::chrono::milliseconds(3));
ret = io(0x40, PUT_DATA, 0, 0, (data + GMSK_MODEM_DATA_LENGTH), length - GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT); ret = io(0x40, PUT_DATA, 0, 0, (data + GMSK_MODEM_DATA_LENGTH), length - GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT);
if (ret < 0) { if (ret < 0) {
@ -367,46 +310,19 @@ int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length)
void CGMSKModemLibUsb::close() void CGMSKModemLibUsb::close()
{ {
wxASSERT(m_dev != NULL); assert(m_dev != nullptr);
#if defined(WIN32)
m_usbClose(m_dev);
#else
libusb_close(m_dev); libusb_close(m_dev);
#endif m_dev = nullptr;
m_dev = NULL;
} }
bool CGMSKModemLibUsb::openModem() bool CGMSKModemLibUsb::openModem()
{ {
#if defined(WIN32)
if (!m_loaded)
return false;
m_usbInit();
m_usbFindBusses();
m_usbFindDevices();
for (struct usb_bus* bus = m_usbGetBusses(); bus != NULL; bus = bus->next) {
for (struct usb_device* dev = bus->devices; dev != NULL; dev = dev->next) {
if (dev->descriptor.idVendor == VENDOR_ID && dev->descriptor.idProduct == m_address) {
m_dev = m_usbOpen(dev);
break;
}
}
}
if (m_dev == NULL)
return false;
m_usbSetConfiguration(m_dev, 1);
#else
m_dev = ::libusb_open_device_with_vid_pid(m_context, VENDOR_ID, m_address); m_dev = ::libusb_open_device_with_vid_pid(m_context, VENDOR_ID, m_address);
if (m_dev == NULL) if (m_dev == nullptr)
return false; return false;
::libusb_set_configuration(m_dev, 1); ::libusb_set_configuration(m_dev, 1);
#endif
unsigned char c; unsigned char c;
io(0x40, SET_AD_INIT, 0, 0, &c, 0, USB_TIMEOUT); io(0x40, SET_AD_INIT, 0, 0, &c, 0, USB_TIMEOUT);
@ -420,23 +336,19 @@ int CGMSKModemLibUsb::io(uint8_t requestType, uint8_t request, uint16_t value,
uint16_t index, unsigned char* data, uint16_t length, uint16_t index, unsigned char* data, uint16_t length,
unsigned int timeout) unsigned int timeout)
{ {
wxASSERT(m_dev != NULL); assert(m_dev != nullptr);
wxASSERT(data != NULL); assert(data != nullptr);
int ret = 0; int ret = 0;
for (unsigned int i = 0U; i < 4U; i++) { for (unsigned int i = 0U; i < 4U; i++) {
#if defined(WIN32)
ret = m_usbControlMsg(m_dev, requestType, request, value, index, data, length, timeout);
#else
ret = ::libusb_control_transfer(m_dev, requestType, request, value, index, data, length, timeout); ret = ::libusb_control_transfer(m_dev, requestType, request, value, index, data, length, timeout);
#endif
if (ret >= 0) if (ret >= 0)
return ret; return ret;
if (ret == -19) // ENODEV if (ret == -19) // ENODEV
return ret; return ret;
::wxMilliSleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
} }
return ret; return ret;

@ -22,19 +22,34 @@
#include "GMSKModem.h" #include "GMSKModem.h"
#include "Utils.h" #include "Utils.h"
#include <wx/wx.h>
#if defined(WIN32)
#include <wx/dynlib.h>
#if _MSC_VER == 1900
#undef __USB_H__
#include <lusb0_usb.h>
#else
#include "lusb0_usb.h"
#endif
#else
#include <libusb-1.0/libusb.h> #include <libusb-1.0/libusb.h>
#endif
/*
* CGMSKModemLibUsb - libusb implementation of IGMSKModem for GMSK USB modems.
*
* USB vendor ID: 0x04D8 (Microchip). The address parameter is the USB product
* ID used to distinguish between multiple modems on the same host.
*
* All modem operations use USB control transfers (libusb_control_transfer).
* Request direction encoding:
* 0xC0 = device-to-host (IN): GET_VERSION, GET_HEADER, GET_DATA,
* GET_AD_STATUS, GET_REMAINSPACE
* 0x40 = host-to-device (OUT): SET_AD_INIT, SET_PTT, PUT_DATA,
* SET_MyCALL, SET_YourCALL, SET_RPT1CALL,
* SET_RPT2CALL, SET_FLAGS, SET_MyCALL2
*
* The io() helper retries each control transfer up to 4 times on transient
* errors, returning -ENODEV (-19) immediately on device disconnect.
*
* DUTCH*Star firmware quirk (m_brokenSpace):
* Some versions report an incorrect remaining-space value from GET_REMAINSPACE.
* When detected by the firmware version string, the space check is bypassed
* by treating hasSpace() as always returning STATE_TRUE.
*
* Data is transferred in GMSK_MODEM_DATA_LENGTH chunks. Frames larger than
* one chunk are split across two sequential PUT_DATA transfers with a 3ms gap
* to give libusb recovery time between transfers.
*/
class CGMSKModemLibUsb : public IGMSKModem { class CGMSKModemLibUsb : public IGMSKModem {
public: public:
CGMSKModemLibUsb(unsigned int address); CGMSKModemLibUsb(unsigned int address);
@ -54,32 +69,15 @@ public:
virtual int writeData(unsigned char* data, unsigned int length); virtual int writeData(unsigned char* data, unsigned int length);
virtual void close(); virtual void close();
#if defined(WIN32)
static char* (*m_usbStrerror)();
#endif
private: private:
unsigned int m_address; unsigned int m_address;
#if defined(WIN32)
struct usb_dev_handle* m_dev;
static wxDynamicLibrary* m_library;
static bool m_loaded;
static void (*m_usbInit)();
static int (*m_usbFindBusses)();
static int (*m_usbFindDevices)();
static struct usb_bus* (*m_usbGetBusses)();
static usb_dev_handle* (*m_usbOpen)(struct usb_device*);
static int (*m_usbSetConfiguration)(usb_dev_handle*, int);
static int (*m_usbControlMsg)(usb_dev_handle*, int, int, int, int, unsigned char*, int, int);
static int (*m_usbClose)(usb_dev_handle*);
#else
libusb_context* m_context; libusb_context* m_context;
libusb_device_handle* m_dev; libusb_device_handle* m_dev;
#endif // Executes a USB control transfer, retrying up to 4 times on transient errors.
// requestType: 0xC0 = device-to-host, 0x40 = host-to-device.
// Returns byte count on success, negative libusb error code on failure.
int io(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, unsigned char* data, uint16_t length, unsigned int timeout); int io(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, unsigned char* data, uint16_t length, unsigned int timeout);
bool m_brokenSpace; bool m_brokenSpace;

@ -1,381 +0,0 @@
/*
* Copyright (C) 2010-2014 by Jonathan Naylor G4KLX
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "GMSKModemWinUSB.h"
#include "DStarDefines.h"
#include <Setupapi.h>
const unsigned char SET_AD_INIT = 0x00U;
const unsigned char SET_PTT = 0x05U;
const unsigned char PUT_DATA = 0x10U;
const unsigned char GET_DATA = 0x11U;
const unsigned char GET_HEADER = 0x21U;
const unsigned char GET_AD_STATUS = 0x30U;
const unsigned char SET_MyCALL = 0x40U;
const unsigned char SET_MyCALL2 = 0x41U;
const unsigned char SET_YourCALL = 0x42U;
const unsigned char SET_RPT1CALL = 0x43U;
const unsigned char SET_RPT2CALL = 0x44U;
const unsigned char SET_FLAGS = 0x45U;
const unsigned char GET_REMAINSPACE = 0x50U;
const unsigned char GET_VERSION = 0xFFU;
const unsigned char COS_OnOff = 0x02U;
const unsigned char CRC_ERROR = 0x04U;
const unsigned char LAST_FRAME = 0x08U;
const unsigned char PTT_OnOff = 0x20U;
const unsigned char PTT_ON = 1U;
const unsigned char PTT_OFF = 0U;
CGMSKModemWinUSB::CGMSKModemWinUSB(unsigned int address) :
m_address(address),
m_file(INVALID_HANDLE_VALUE),
m_handle(INVALID_HANDLE_VALUE),
m_brokenSpace(false)
{
}
CGMSKModemWinUSB::~CGMSKModemWinUSB()
{
}
bool CGMSKModemWinUSB::open()
{
wxASSERT(m_handle == INVALID_HANDLE_VALUE);
bool res = openModem();
if (!res) {
wxLogError(wxT("Cannot find the GMSK Modem with address: 0x%04X"), m_address);
return false;
}
wxLogInfo(wxT("Found the GMSK Modem with address: 0x%04X"), m_address);
wxString version;
int ret;
do {
unsigned char buffer[GMSK_MODEM_DATA_LENGTH];
ret = io(GET_VERSION, 0xC0U, 0U, buffer, GMSK_MODEM_DATA_LENGTH);
if (ret > 0) {
wxString text((char*)buffer, wxConvLocal, ret);
version.Append(text);
} else if (ret < 0) {
wxLogError(wxT("GET_VERSION returned %d"), -ret);
close();
return false;
}
} while (ret == int(GMSK_MODEM_DATA_LENGTH));
wxLogInfo(wxT("Firmware version: %s"), version.c_str());
// Trap firmware version 0.1.00 of DUTCH*Star and complain loudly
if (version.Find(wxT("DUTCH*Star")) != wxNOT_FOUND && version.Find(wxT("0.1.00")) != wxNOT_FOUND) {
wxLogWarning(wxT("This modem firmware is not supported by the repeater"));
wxLogWarning(wxT("Please upgrade to a newer version"));
close();
return false;
}
// DUTCH*Star firmware has a broken concept of free space
if (version.Find(wxT("DUTCH*Star")) != wxNOT_FOUND)
m_brokenSpace = true;
return true;
}
bool CGMSKModemWinUSB::readHeader(unsigned char* header, unsigned int length)
{
wxASSERT(header != NULL);
wxASSERT(length > (RADIO_HEADER_LENGTH_BYTES * 2U));
unsigned int offset = 0U;
while (offset < RADIO_HEADER_LENGTH_BYTES) {
int ret = io(GET_HEADER, 0xC0U, 0U, header + offset, GMSK_MODEM_DATA_LENGTH);
if (ret < 0) {
wxLogError(wxT("GET_HEADER returned %d"), -ret);
return false;
} else if (ret == 0) {
if (offset == 0U)
return false;
::wxMilliSleep(10UL);
unsigned char status;
ret = io(GET_AD_STATUS, 0xC0U, 0U, &status, 1U);
if (ret < 0) {
wxLogError(wxT("GET_COS returned %d"), -ret);
return false;
} else if (ret > 0) {
if ((status & COS_OnOff) == COS_OnOff)
offset = 0U;
}
} else {
offset += ret;
}
}
unsigned char status;
int ret = io(GET_AD_STATUS, 0xC0U, 0U, &status, 1U);
if (ret < 0) {
wxLogError(wxT("GET_CRC returned %d"), -ret);
return false;
}
if ((status & CRC_ERROR) == CRC_ERROR) {
wxLogMessage(wxT("Invalid CRC on header"));
return false;
}
return true;
}
int CGMSKModemWinUSB::readData(unsigned char* data, unsigned int length, bool& end)
{
wxASSERT(data != NULL);
wxASSERT(length > 0U);
end = false;
int ret = io(GET_DATA, 0xC0U, 0U, data, GMSK_MODEM_DATA_LENGTH);
if (ret < 0) {
wxLogError(wxT("GET_DATA returned %d"), -ret);
return ret;
} else if (ret == 0) {
unsigned char status;
int ret = io(GET_AD_STATUS, 0xC0U, 0U, &status, 1U);
if (ret < 0) {
wxLogError(wxT("LAST_FRAME returned %d"), -ret);
return ret;
}
if ((status & LAST_FRAME) == LAST_FRAME)
end = true;
}
return ret;
}
void CGMSKModemWinUSB::writeHeader(unsigned char* header, unsigned int length)
{
wxASSERT(header != NULL);
wxASSERT(length >= (RADIO_HEADER_LENGTH_BYTES - 2U));
io(SET_MyCALL2, 0x40U, 0U, header + 35U, SHORT_CALLSIGN_LENGTH);
io(SET_MyCALL, 0x40U, 0U, header + 27U, LONG_CALLSIGN_LENGTH);
io(SET_YourCALL, 0x40U, 0U, header + 19U, LONG_CALLSIGN_LENGTH);
io(SET_RPT1CALL, 0x40U, 0U, header + 11U, LONG_CALLSIGN_LENGTH);
io(SET_RPT2CALL, 0x40U, 0U, header + 3U, LONG_CALLSIGN_LENGTH);
io(SET_FLAGS, 0x40U, 0U, header + 0U, 3U);
}
TRISTATE CGMSKModemWinUSB::getPTT()
{
unsigned char status;
int ret = io(GET_AD_STATUS, 0xC0U, 0U, &status, 1U);
if (ret != 1) {
wxLogError(wxT("GET_PTT returned %d"), -ret);
return STATE_UNKNOWN;
}
if ((status & PTT_OnOff) == PTT_OnOff)
return STATE_TRUE;
else
return STATE_FALSE;
}
void CGMSKModemWinUSB::setPTT(bool on)
{
unsigned char c;
io(SET_PTT, 0x40U, on ? PTT_ON : PTT_OFF, &c, 0U);
}
TRISTATE CGMSKModemWinUSB::hasSpace()
{
unsigned char space;
int ret = io(GET_REMAINSPACE, 0xC0U, 0U, &space, 1U);
if (ret != 1) {
wxLogError(wxT("GET_REMAINSPACE returned %d"), -ret);
return STATE_UNKNOWN;
}
if (space >= DV_FRAME_LENGTH_BYTES)
return STATE_TRUE;
else
return STATE_FALSE;
}
int CGMSKModemWinUSB::writeData(unsigned char* data, unsigned int length)
{
wxASSERT(data != NULL);
wxASSERT(length > 0U && length <= DV_FRAME_LENGTH_BYTES);
if (length > GMSK_MODEM_DATA_LENGTH) {
int ret = io(PUT_DATA, 0x40U, 0U, data, GMSK_MODEM_DATA_LENGTH);
if (ret < 0) {
if (ret == -22) {
wxLogError(wxT("PUT_DATA 1, returned %d"), -ret);
return ret;
}
return 0;
}
ret = io(PUT_DATA, 0x40U, 0U, data + GMSK_MODEM_DATA_LENGTH, length - GMSK_MODEM_DATA_LENGTH);
if (ret < 0) {
if (ret == -22) {
wxLogError(wxT("PUT_DATA 2, returned %d"), -ret);
return ret;
}
return int(GMSK_MODEM_DATA_LENGTH);
}
return length;
} else {
int ret = io(PUT_DATA, 0x40U, 0U, data, length);
if (ret < 0) {
if (ret == -22) {
wxLogError(wxT("PUT_DATA returned %d"), -ret);
return ret;
}
return 0;
}
return length;
}
}
void CGMSKModemWinUSB::close()
{
wxASSERT(m_file != INVALID_HANDLE_VALUE);
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
::CloseHandle(m_file);
::WinUsb_Free(m_handle);
m_file = INVALID_HANDLE_VALUE;
m_handle = INVALID_HANDLE_VALUE;
}
bool CGMSKModemWinUSB::openModem()
{
WCHAR id1[15U], id2[15U];
::swprintf(id1, L"pid_%04x", m_address);
::swprintf(id2, L"pid_%04X", m_address);
wxString wxId1(id1, wxConvLocal);
wxString wxId2(id2, wxConvLocal);
CLSID clsId;
LPOLESTR str = OLESTR("{136C76EF-3F4E-4030-A7E3-E1003EF0A715}");
HRESULT result = ::CLSIDFromString(str, &clsId);
if (result != NOERROR) {
wxLogError(wxT("Error from CLSIDFromString: err=%lu"), ::GetLastError());
return false;
}
HDEVINFO devInfo = ::SetupDiGetClassDevs(&clsId, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%lu"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (unsigned int index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &clsId, index, &devInfoData); index++) {
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
DWORD required;
BOOL ret1 = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!ret1) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%lu"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
// Check the name to see if it's the correct vendor id and address
if (::wcsstr(detailData->DevicePath, id1) == NULL && ::wcsstr(detailData->DevicePath, id2) == NULL) {
::free(detailData);
continue;
}
m_file = ::CreateFile(detailData->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (m_file == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from CreateFile: err=%lu"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
ret1 = ::WinUsb_Initialize(m_file, &m_handle);
if (!ret1) {
wxLogError(wxT("Error from WinUsb_Initialize: err=%lu"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::CloseHandle(m_file);
::free(detailData);
return false;
}
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
unsigned char c;
io(SET_AD_INIT, 0x40U, 0U, &c, 0U);
setPTT(false);
return true;
}
::SetupDiDestroyDeviceInfoList(devInfo);
return false;
}
int CGMSKModemWinUSB::io(unsigned char type, unsigned char n1, unsigned char n2, unsigned char* buffer, unsigned int length)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
WINUSB_SETUP_PACKET packet;
packet.RequestType = n1;
packet.Request = type;
packet.Value = n2;
packet.Index = 0U;
packet.Length = length;
ULONG transferred;
BOOL ret = ::WinUsb_ControlTransfer(m_handle, packet, buffer, length, &transferred, 0);
if (!ret) {
long error = ::GetLastError();
return -error;
}
return transferred;
}

@ -1,60 +0,0 @@
/*
* Copyright (C) 2010-2014 by Jonathan Naylor G4KLX
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifndef GMSKModemWinUSB_H
#define GMSKModemWinUSB_H
#include "GMSKModem.h"
#include "Utils.h"
#include <wx/wx.h>
#include "winusb.h"
class CGMSKModemWinUSB : public IGMSKModem {
public:
CGMSKModemWinUSB(unsigned int address);
virtual ~CGMSKModemWinUSB();
virtual bool open();
virtual bool readHeader(unsigned char* header, unsigned int length);
virtual int readData(unsigned char* data, unsigned int length, bool& end);
virtual TRISTATE getPTT();
virtual void setPTT(bool on);
virtual TRISTATE hasSpace();
virtual void writeHeader(unsigned char* data, unsigned int length);
virtual int writeData(unsigned char* data, unsigned int length);
virtual void close();
private:
unsigned int m_address;
HANDLE m_file;
WINUSB_INTERFACE_HANDLE m_handle;
bool m_brokenSpace;
bool openModem();
int io(unsigned char type, unsigned char n1, unsigned char n2, unsigned char* buffer, unsigned int length);
};
#endif

@ -15,6 +15,8 @@
#include "GPIOController.h" #include "GPIOController.h"
#include <cstdio>
CGPIOController::CGPIOController(unsigned int config) : CGPIOController::CGPIOController(unsigned int config) :
m_config(config), m_config(config),
m_outp1(false), m_outp1(false),
@ -38,7 +40,7 @@ bool CGPIOController::open()
{ {
bool ret = ::wiringPiSetup() != -1; bool ret = ::wiringPiSetup() != -1;
if (!ret) { if (!ret) {
wxLogError(wxT("Unable to initialise wiringPi")); ::fprintf(stderr, "Unable to initialise wiringPi\n");
return false; return false;
} }
@ -172,4 +174,3 @@ void CGPIOController::close()
} }
#endif #endif

@ -18,8 +18,6 @@
#include "HardwareController.h" #include "HardwareController.h"
#include <wx/wx.h>
class CGPIOController : public IHardwareController { class CGPIOController : public IHardwareController {
public: public:
CGPIOController(unsigned int config); CGPIOController(unsigned int config);
@ -48,4 +46,3 @@ private:
#endif #endif
#endif #endif

@ -21,20 +21,22 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Utils.h" #include "Utils.h"
#include <cstring>
// Uncomment to hex-dump every outbound DSRP packet to stdout for debugging.
// #define DUMP_TX // #define DUMP_TX
const unsigned int BUFFER_LENGTH = 255U; const unsigned int BUFFER_LENGTH = 255U;
CGatewayProtocolHandler::CGatewayProtocolHandler(const wxString& localAddress, unsigned int localPort) : CGatewayProtocolHandler::CGatewayProtocolHandler(const std::string& localAddress, unsigned int localPort) :
m_socket(localAddress, localPort), m_socket(localAddress, localPort),
m_type(NETWORK_NONE), m_type(NETWORK_NONE),
m_buffer(NULL), m_buffer(nullptr),
m_length(0U) m_length(0U)
{ {
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
wxDateTime now = wxDateTime::UNow(); ::srand((unsigned int)::time(nullptr));
::srand(now.GetMillisecond());
} }
CGatewayProtocolHandler::~CGatewayProtocolHandler() CGatewayProtocolHandler::~CGatewayProtocolHandler()
@ -47,7 +49,7 @@ bool CGatewayProtocolHandler::open()
return m_socket.open(); return m_socket.open();
} }
bool CGatewayProtocolHandler::writeHeader(const unsigned char* header, wxUint16 id, const in_addr& address, unsigned int port) bool CGatewayProtocolHandler::writeHeader(const unsigned char* header, uint16_t id, const in_addr& address, unsigned int port)
{ {
unsigned char buffer[50U]; unsigned char buffer[50U];
@ -71,9 +73,11 @@ bool CGatewayProtocolHandler::writeHeader(const unsigned char* header, wxUint16
csum.result(buffer + 8U + RADIO_HEADER_LENGTH_BYTES - 2U); csum.result(buffer + 8U + RADIO_HEADER_LENGTH_BYTES - 2U);
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Header"), buffer, 49U); CUtils::dump("Sending Header", buffer, 49U);
#endif #endif
// Send the header four times to compensate for UDP packet loss.
// The repeater will deduplicate on the session ID.
for (unsigned int i = 0U; i < 4U; i++) { for (unsigned int i = 0U; i < 4U; i++) {
bool ret = m_socket.write(buffer, 49U, address, port); bool ret = m_socket.write(buffer, 49U, address, port);
if (!ret) if (!ret)
@ -83,10 +87,10 @@ bool CGatewayProtocolHandler::writeHeader(const unsigned char* header, wxUint16
return true; return true;
} }
bool CGatewayProtocolHandler::writeData(const unsigned char* data, unsigned int length, wxUint16 id, wxUint8 seqNo, const in_addr& address, unsigned int port) bool CGatewayProtocolHandler::writeData(const unsigned char* data, unsigned int length, uint16_t id, uint8_t seqNo, const in_addr& address, unsigned int port)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
wxASSERT(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES); assert(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
unsigned char buffer[30U]; unsigned char buffer[30U];
@ -107,13 +111,13 @@ bool CGatewayProtocolHandler::writeData(const unsigned char* data, unsigned int
::memcpy(buffer + 9U, data, length); ::memcpy(buffer + 9U, data, length);
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Data"), buffer, length + 9U); CUtils::dump("Sending Data", buffer, length + 9U);
#endif #endif
return m_socket.write(buffer, length + 9U, address, port); return m_socket.write(buffer, length + 9U, address, port);
} }
NETWORK_TYPE CGatewayProtocolHandler::read(wxUint16& id, in_addr& address, unsigned int& port) NETWORK_TYPE CGatewayProtocolHandler::read(uint16_t& id, in_addr& address, unsigned int& port)
{ {
bool res = true; bool res = true;
@ -124,7 +128,7 @@ NETWORK_TYPE CGatewayProtocolHandler::read(wxUint16& id, in_addr& address, unsig
return m_type; return m_type;
} }
bool CGatewayProtocolHandler::readPackets(wxUint16& id, in_addr& address, unsigned int& port) bool CGatewayProtocolHandler::readPackets(uint16_t& id, in_addr& address, unsigned int& port)
{ {
m_type = NETWORK_NONE; m_type = NETWORK_NONE;
@ -158,7 +162,7 @@ bool CGatewayProtocolHandler::readPackets(wxUint16& id, in_addr& address, unsign
} }
} }
CUtils::dump(wxT("Unknown packet from the Repeater"), m_buffer, m_length); CUtils::dump("Unknown packet from the Repeater", m_buffer, m_length);
return true; return true;
} }
@ -180,7 +184,7 @@ unsigned int CGatewayProtocolHandler::readHeader(unsigned char* buffer, unsigned
bool check = csum.check(m_buffer + 8U + RADIO_HEADER_LENGTH_BYTES - 2U); bool check = csum.check(m_buffer + 8U + RADIO_HEADER_LENGTH_BYTES - 2U);
if (!check) { if (!check) {
CUtils::dump(wxT("Header checksum failure from the Repeater"), m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES); CUtils::dump("Header checksum failure from the Repeater", m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES);
return 0U; return 0U;
} }
@ -189,11 +193,14 @@ unsigned int CGatewayProtocolHandler::readHeader(unsigned char* buffer, unsigned
return RADIO_HEADER_LENGTH_BYTES; return RADIO_HEADER_LENGTH_BYTES;
} }
unsigned int CGatewayProtocolHandler::readData(unsigned char* buffer, unsigned int length, wxUint8& seqNo, unsigned int& errors) unsigned int CGatewayProtocolHandler::readData(unsigned char* buffer, unsigned int length, uint8_t& seqNo, unsigned int& errors)
{ {
if (m_type != NETWORK_DATA) if (m_type != NETWORK_DATA)
return 0U; return 0U;
if (m_length < 9U)
return 0U;
unsigned int dataLen = m_length - 9U; unsigned int dataLen = m_length - 9U;
// Is our buffer too small? // Is our buffer too small?
@ -222,12 +229,13 @@ unsigned int CGatewayProtocolHandler::readData(unsigned char* buffer, unsigned i
return dataLen; return dataLen;
} }
unsigned int CGatewayProtocolHandler::readRegister(wxString& name) unsigned int CGatewayProtocolHandler::readRegister(std::string& name)
{ {
if (m_type != NETWORK_REGISTER) if (m_type != NETWORK_REGISTER)
return 0U; return 0U;
name = wxString((char*)(m_buffer + 5U), wxConvLocal); unsigned int maxLen = (m_length > 5U) ? (m_length - 5U) : 0U;
name = std::string((char*)(m_buffer + 5U), ::strnlen((char*)(m_buffer + 5U), maxLen));
return m_length - 6U; return m_length - 6U;
} }

@ -21,24 +21,38 @@
#include "UDPReaderWriter.h" #include "UDPReaderWriter.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "StdCompat.h"
#include <wx/wx.h> /*
#include <wx/datetime.h> * Implements the D-Star Repeater Protocol (DSRP) from the gateway side.
*
* This is the mirror of CRepeaterProtocolHandler it is used by the gateway
* process (ircDDBGateway/DStarGateway) to communicate with one or more
* repeater daemons. Unlike the repeater side, there is no fixed peer address:
* each packet carries the sender's address and port, which are passed through
* to the caller so it can route replies back to the correct repeater.
*
* The same DSRP packet types are used as on the repeater side (0x20 header,
* 0x21 data, 0x0B register). The key difference is that the gateway sends
* headers four times for redundancy (vs two on the repeater side).
*/
class CGatewayProtocolHandler { class CGatewayProtocolHandler {
public: public:
CGatewayProtocolHandler(const wxString& localAddress, unsigned int localPort); CGatewayProtocolHandler(const std::string& localAddress, unsigned int localPort);
~CGatewayProtocolHandler(); ~CGatewayProtocolHandler();
bool open(); bool open();
bool writeHeader(const unsigned char* header, wxUint16 id, const in_addr& address, unsigned int port); // Sends the header packet four times to reduce the impact of UDP packet loss.
bool writeData(const unsigned char* data, unsigned int length, wxUint16 id, wxUint8 seqNo, const in_addr& address, unsigned int port); bool writeHeader(const unsigned char* header, uint16_t id, const in_addr& address, unsigned int port);
bool writeData(const unsigned char* data, unsigned int length, uint16_t id, uint8_t seqNo, const in_addr& address, unsigned int port);
NETWORK_TYPE read(wxUint16& id, in_addr& address, unsigned int& port); // Drains pending datagrams; fills address/port with the sender for routing.
NETWORK_TYPE read(uint16_t& id, in_addr& address, unsigned int& port);
unsigned int readHeader(unsigned char* data, unsigned int length); unsigned int readHeader(unsigned char* data, unsigned int length);
unsigned int readData(unsigned char* data, unsigned int length, wxUint8& seqNo, unsigned int& errors); unsigned int readData(unsigned char* data, unsigned int length, uint8_t& seqNo, unsigned int& errors);
unsigned int readRegister(wxString& name); // Reads a Register packet; name is the repeater's self-reported name string.
unsigned int readRegister(std::string& name);
void close(); void close();
@ -48,7 +62,7 @@ private:
unsigned char* m_buffer; unsigned char* m_buffer;
unsigned int m_length; unsigned int m_length;
bool readPackets(wxUint16& id, in_addr& address, unsigned int& port); bool readPackets(uint16_t& id, in_addr& address, unsigned int& port);
}; };
#endif #endif

@ -19,6 +19,16 @@
#ifndef Golay_H #ifndef Golay_H
#define Golay_H #define Golay_H
/*
* Golay error-correcting code used by the AMBE FEC layer.
*
* Provides two variants:
* Golay(23,12) encodes 12 data bits into 23 bits; corrects up to 3 errors.
* Golay(24,12) adds a parity bit to the above for even-parity detection.
*
* encode*() returns the full codeword (data + parity bits).
* decode*() returns the corrected 12-bit data word, or ~0U on uncorrectable error.
*/
class CGolay { class CGolay {
public: public:
static unsigned int encode23127(unsigned int data); static unsigned int encode23127(unsigned int data);

@ -14,6 +14,18 @@
#ifndef HardwareController_H #ifndef HardwareController_H
#define HardwareController_H #define HardwareController_H
/*
* Abstract interface for hardware I/O controllers.
*
* Concrete implementations (CSerialLineController, CGPIOController,
* CArduinoController, CK8055Controller, etc.) allow the repeater to
* drive external PTT lines, squelch outputs, and read COR/COS inputs
* through whatever physical interface is attached to the host machine.
*
* Up to five digital inputs and eight digital outputs are supported;
* the meaning of each pin is determined by the configured controller
* type and the hardware wiring.
*/
class IHardwareController { class IHardwareController {
public: public:
virtual ~IHardwareController() = 0; virtual ~IHardwareController() = 0;

@ -20,8 +20,11 @@
#include "HeaderData.h" #include "HeaderData.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include <cassert>
#include <ctime>
CHeaderData::CHeaderData() : CHeaderData::CHeaderData() :
m_time(), m_time(0),
m_myCall1(), m_myCall1(),
m_myCall2(), m_myCall2(),
m_yourCall(), m_yourCall(),
@ -49,7 +52,7 @@ m_valid(header.m_valid)
} }
CHeaderData::CHeaderData(const unsigned char* data, unsigned int length, bool check) : CHeaderData::CHeaderData(const unsigned char* data, unsigned int length, bool check) :
m_time(), m_time(0),
m_myCall1(), m_myCall1(),
m_myCall2(), m_myCall2(),
m_yourCall(), m_yourCall(),
@ -60,29 +63,29 @@ m_flag2(0x00),
m_flag3(0x00), m_flag3(0x00),
m_valid(true) m_valid(true)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
wxASSERT(length >= (RADIO_HEADER_LENGTH_BYTES - 2U)); assert(length >= (RADIO_HEADER_LENGTH_BYTES - 2U));
const unsigned char* p = data; const unsigned char* p = data;
m_flag1 = *p++; m_flag1 = *p++;
m_flag2 = *p++; m_flag2 = *p++;
m_flag3 = *p++; m_flag3 = *p++;
m_rptCall2 = wxString((const char*)p, wxConvLocal, LONG_CALLSIGN_LENGTH); m_rptCall2 = std::string((const char*)p, LONG_CALLSIGN_LENGTH);
p += LONG_CALLSIGN_LENGTH; p += LONG_CALLSIGN_LENGTH;
m_rptCall1 = wxString((const char*)p, wxConvLocal, LONG_CALLSIGN_LENGTH); m_rptCall1 = std::string((const char*)p, LONG_CALLSIGN_LENGTH);
p += LONG_CALLSIGN_LENGTH; p += LONG_CALLSIGN_LENGTH;
m_yourCall = wxString((const char*)p, wxConvLocal, LONG_CALLSIGN_LENGTH); m_yourCall = std::string((const char*)p, LONG_CALLSIGN_LENGTH);
p += LONG_CALLSIGN_LENGTH; p += LONG_CALLSIGN_LENGTH;
m_myCall1 = wxString((const char*)p, wxConvLocal, LONG_CALLSIGN_LENGTH); m_myCall1 = std::string((const char*)p, LONG_CALLSIGN_LENGTH);
p += LONG_CALLSIGN_LENGTH; p += LONG_CALLSIGN_LENGTH;
m_myCall2 = wxString((const char*)p, wxConvLocal, SHORT_CALLSIGN_LENGTH); m_myCall2 = std::string((const char*)p, SHORT_CALLSIGN_LENGTH);
m_time.SetToCurrent(); m_time = ::time(nullptr);
// We have a checksum, check it if asked // We have a checksum, check it if asked
if (length >= RADIO_HEADER_LENGTH_BYTES && check) { if (length >= RADIO_HEADER_LENGTH_BYTES && check) {
@ -94,10 +97,10 @@ m_valid(true)
} }
} }
CHeaderData::CHeaderData(const wxString& myCall1, const wxString& myCall2, const wxString& yourCall, CHeaderData::CHeaderData(const std::string& myCall1, const std::string& myCall2, const std::string& yourCall,
const wxString& rptCall1, const wxString& rptCall2, unsigned char flag1, const std::string& rptCall1, const std::string& rptCall2, unsigned char flag1,
unsigned char flag2, unsigned char flag3) : unsigned char flag2, unsigned char flag3) :
m_time(), m_time(0),
m_myCall1(myCall1), m_myCall1(myCall1),
m_myCall2(myCall2), m_myCall2(myCall2),
m_yourCall(yourCall), m_yourCall(yourCall),
@ -108,51 +111,51 @@ m_flag2(flag2),
m_flag3(flag3), m_flag3(flag3),
m_valid(true) m_valid(true)
{ {
m_time.SetToCurrent(); m_time = ::time(nullptr);
m_myCall1.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_myCall1.resize(LONG_CALLSIGN_LENGTH, ' ');
m_myCall2.Append(wxT(' '), SHORT_CALLSIGN_LENGTH); m_myCall2.resize(SHORT_CALLSIGN_LENGTH, ' ');
m_yourCall.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_yourCall.resize(LONG_CALLSIGN_LENGTH, ' ');
m_rptCall1.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_rptCall1.resize(LONG_CALLSIGN_LENGTH, ' ');
m_rptCall2.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_rptCall2.resize(LONG_CALLSIGN_LENGTH, ' ');
m_myCall1.Truncate(LONG_CALLSIGN_LENGTH); m_myCall1.resize(LONG_CALLSIGN_LENGTH);
m_myCall2.Truncate(SHORT_CALLSIGN_LENGTH); m_myCall2.resize(SHORT_CALLSIGN_LENGTH);
m_yourCall.Truncate(LONG_CALLSIGN_LENGTH); m_yourCall.resize(LONG_CALLSIGN_LENGTH);
m_rptCall1.Truncate(LONG_CALLSIGN_LENGTH); m_rptCall1.resize(LONG_CALLSIGN_LENGTH);
m_rptCall2.Truncate(LONG_CALLSIGN_LENGTH); m_rptCall2.resize(LONG_CALLSIGN_LENGTH);
} }
CHeaderData::~CHeaderData() CHeaderData::~CHeaderData()
{ {
} }
wxDateTime CHeaderData::getTime() const time_t CHeaderData::getTime() const
{ {
return m_time; return m_time;
} }
wxString CHeaderData::getMyCall1() const std::string CHeaderData::getMyCall1() const
{ {
return m_myCall1; return m_myCall1;
} }
wxString CHeaderData::getMyCall2() const std::string CHeaderData::getMyCall2() const
{ {
return m_myCall2; return m_myCall2;
} }
wxString CHeaderData::getYourCall() const std::string CHeaderData::getYourCall() const
{ {
return m_yourCall; return m_yourCall;
} }
wxString CHeaderData::getRptCall1() const std::string CHeaderData::getRptCall1() const
{ {
return m_rptCall1; return m_rptCall1;
} }
wxString CHeaderData::getRptCall2() const std::string CHeaderData::getRptCall2() const
{ {
return m_rptCall2; return m_rptCall2;
} }
@ -172,39 +175,39 @@ unsigned char CHeaderData::getFlag3() const
return m_flag3; return m_flag3;
} }
void CHeaderData::setMyCall1(const wxString& callsign) void CHeaderData::setMyCall1(const std::string& callsign)
{ {
m_myCall1 = callsign; m_myCall1 = callsign;
m_myCall1.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_myCall1.resize(LONG_CALLSIGN_LENGTH, ' ');
m_myCall1.Truncate(LONG_CALLSIGN_LENGTH); m_myCall1.resize(LONG_CALLSIGN_LENGTH);
} }
void CHeaderData::setMyCall2(const wxString& callsign) void CHeaderData::setMyCall2(const std::string& callsign)
{ {
m_myCall2 = callsign; m_myCall2 = callsign;
m_myCall2.Append(wxT(' '), SHORT_CALLSIGN_LENGTH); m_myCall2.resize(SHORT_CALLSIGN_LENGTH, ' ');
m_myCall2.Truncate(SHORT_CALLSIGN_LENGTH); m_myCall2.resize(SHORT_CALLSIGN_LENGTH);
} }
void CHeaderData::setYourCall(const wxString& callsign) void CHeaderData::setYourCall(const std::string& callsign)
{ {
m_yourCall = callsign; m_yourCall = callsign;
m_yourCall.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_yourCall.resize(LONG_CALLSIGN_LENGTH, ' ');
m_yourCall.Truncate(LONG_CALLSIGN_LENGTH); m_yourCall.resize(LONG_CALLSIGN_LENGTH);
} }
void CHeaderData::setRptCall1(const wxString& callsign) void CHeaderData::setRptCall1(const std::string& callsign)
{ {
m_rptCall1 = callsign; m_rptCall1 = callsign;
m_rptCall1.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_rptCall1.resize(LONG_CALLSIGN_LENGTH, ' ');
m_rptCall1.Truncate(LONG_CALLSIGN_LENGTH); m_rptCall1.resize(LONG_CALLSIGN_LENGTH);
} }
void CHeaderData::setRptCall2(const wxString& callsign) void CHeaderData::setRptCall2(const std::string& callsign)
{ {
m_rptCall2 = callsign; m_rptCall2 = callsign;
m_rptCall2.Append(wxT(' '), LONG_CALLSIGN_LENGTH); m_rptCall2.resize(LONG_CALLSIGN_LENGTH, ' ');
m_rptCall2.Truncate(LONG_CALLSIGN_LENGTH); m_rptCall2.resize(LONG_CALLSIGN_LENGTH);
} }
bool CHeaderData::isAck() const bool CHeaderData::isAck() const
@ -320,11 +323,11 @@ bool CHeaderData::isValid() const
void CHeaderData::reset() void CHeaderData::reset()
{ {
m_myCall1 = wxT(" "); m_myCall1 = " ";
m_myCall2 = wxT(" "); m_myCall2 = " ";
m_yourCall = wxT("CQCQCQ "); m_yourCall = "CQCQCQ ";
m_rptCall1 = wxT("DIRECT "); m_rptCall1 = "DIRECT ";
m_rptCall2 = wxT("DIRECT "); m_rptCall2 = "DIRECT ";
m_flag1 = 0x00; m_flag1 = 0x00;
m_flag2 = 0x00; m_flag2 = 0x00;

@ -19,25 +19,52 @@
#ifndef HeaderData_H #ifndef HeaderData_H
#define HeaderData_H #define HeaderData_H
#include <wx/wx.h> #include <ctime>
#include <wx/datetime.h> #include <string>
/*
* Represents a D-Star radio header the block transmitted before every voice
* or data transmission that identifies the participants and routing.
*
* Wire format (41 bytes, all callsigns space-padded to fixed width):
* [0] Flag 1 repeater/data/control/urgent/BK/EMR bits
* [1] Flag 2 reserved (always 0x00 in practice)
* [2] Flag 3 reserved (always 0x00 in practice)
* [3..10] RPT2 outgoing repeater callsign (8 chars, LONG_CALLSIGN_LENGTH)
* [11..18] RPT1 incoming repeater callsign (8 chars)
* [19..26] YOUR destination callsign or "CQCQCQ " for a general call
* [27..34] MY1 originating station callsign (8 chars)
* [35..38] MY2 4-character callsign suffix (SHORT_CALLSIGN_LENGTH)
* [39..40] Checksum CCITT-16 (reversed bit order) over bytes [0..38]
*
* The checksum is computed by CCCITTChecksumReverse. A stored checksum of
* 0xFFFF is treated as "unchecked" and is accepted without verification; this
* is used when the header arrives from a trusted local source.
*
* Flag 1 bit meanings (see DStarDefines.h for mask constants):
* Bit 6 Repeater mode set when relayed through a repeater
* Bit 5 Data packet DV data vs. voice
* Bit 4 Interrupted previous transmission was interrupted (BK)
* Bit 2 Control signal auto-control frame
* Bit 0 Urgent emergency (EMR)
*/
class CHeaderData { class CHeaderData {
public: public:
CHeaderData(); CHeaderData();
CHeaderData(const CHeaderData& header); CHeaderData(const CHeaderData& header);
// Deserialise from raw wire bytes; set check=true to validate the checksum.
CHeaderData(const unsigned char* data, unsigned int length, bool check); CHeaderData(const unsigned char* data, unsigned int length, bool check);
CHeaderData(const wxString& myCall1, const wxString& myCall2, const wxString& yourCall, CHeaderData(const std::string& myCall1, const std::string& myCall2, const std::string& yourCall,
const wxString& rptCall1, const wxString& rptCall2, unsigned char flag1 = 0x00, const std::string& rptCall1, const std::string& rptCall2, unsigned char flag1 = 0x00,
unsigned char flag2 = 0x00, unsigned char flag3 = 0x00); unsigned char flag2 = 0x00, unsigned char flag3 = 0x00);
~CHeaderData(); ~CHeaderData();
wxDateTime getTime() const; time_t getTime() const;
wxString getMyCall1() const; std::string getMyCall1() const;
wxString getMyCall2() const; std::string getMyCall2() const;
wxString getYourCall() const; std::string getYourCall() const;
wxString getRptCall1() const; std::string getRptCall1() const;
wxString getRptCall2() const; std::string getRptCall2() const;
unsigned char getFlag1() const; unsigned char getFlag1() const;
unsigned char getFlag2() const; unsigned char getFlag2() const;
@ -57,11 +84,11 @@ public:
void setFlag2(unsigned char flag); void setFlag2(unsigned char flag);
void setFlag3(unsigned char flag); void setFlag3(unsigned char flag);
void setMyCall1(const wxString& callsign); void setMyCall1(const std::string& callsign);
void setMyCall2(const wxString& callsign); void setMyCall2(const std::string& callsign);
void setYourCall(const wxString& callsign); void setYourCall(const std::string& callsign);
void setRptCall1(const wxString& callsign); void setRptCall1(const std::string& callsign);
void setRptCall2(const wxString& callsign); void setRptCall2(const std::string& callsign);
void setRepeaterMode(bool set); void setRepeaterMode(bool set);
void setDataPacket(bool set); void setDataPacket(bool set);
@ -70,6 +97,7 @@ public:
void setUrgent(bool set); void setUrgent(bool set);
void setRepeaterFlags(unsigned char set); void setRepeaterFlags(unsigned char set);
// Returns false if the checksum was checked and failed during deserialisation.
bool isValid() const; bool isValid() const;
void reset(); void reset();
@ -77,16 +105,16 @@ public:
CHeaderData& operator=(const CHeaderData& header); CHeaderData& operator=(const CHeaderData& header);
private: private:
wxDateTime m_time; time_t m_time;
wxString m_myCall1; std::string m_myCall1;
wxString m_myCall2; std::string m_myCall2;
wxString m_yourCall; std::string m_yourCall;
wxString m_rptCall1; std::string m_rptCall1;
wxString m_rptCall2; std::string m_rptCall2;
unsigned char m_flag1; unsigned char m_flag1;
unsigned char m_flag2; unsigned char m_flag2;
unsigned char m_flag3; unsigned char m_flag3;
bool m_valid; bool m_valid; // Set to false if checksum validation failed.
}; };
#endif #endif

@ -20,12 +20,12 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Timer.h" #include "Timer.h"
#include "Utils.h" #include "Utils.h"
#include "Logger.h"
#if defined(__WINDOWS__) #include <cassert>
#include <setupapi.h> #include <chrono>
#else #include <thread>
#include <wx/dir.h> #include <string>
#endif
enum STATE_ICOM { enum STATE_ICOM {
SI_NONE, SI_NONE,
@ -37,7 +37,7 @@ const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CIcomController::CIcomController(const wxString& port) : CIcomController::CIcomController(const std::string& port) :
CModem(), CModem(),
m_port(port), m_port(port),
m_serial(port, SERIAL_38400, true), m_serial(port, SERIAL_38400, true),
@ -45,7 +45,7 @@ m_txData(2000U),
m_txCounter(0U), m_txCounter(0U),
m_pktCounter(0U) m_pktCounter(0U)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
} }
CIcomController::~CIcomController() CIcomController::~CIcomController()
@ -58,16 +58,14 @@ bool CIcomController::start()
if (!ret) if (!ret)
return false; return false;
Create(); m_thread = std::thread(&CIcomController::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CIcomController::Entry() void CIcomController::entry()
{ {
wxLogMessage(wxT("Starting Icom Controller thread")); wxLogMessage("Starting Icom Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U); CTimer pollTimer(200U, 0U, 100U);
@ -113,13 +111,13 @@ void* CIcomController::Entry()
break; break;
case RTI_ERROR: case RTI_ERROR:
wxLogMessage(wxT("Stopping Icom Controller thread")); wxLogMessage("Stopping Icom Controller thread");
return NULL; return;
case RTI_HEADER: { case RTI_HEADER: {
// CUtils::dump(wxT("RTI_HEADER"), buffer, length); // CUtils::dump("RTI_HEADER", buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -134,9 +132,9 @@ void* CIcomController::Entry()
break; break;
case RTI_DATA: { case RTI_DATA: {
// CUtils::dump(wxT("RTI_DATA"), buffer, length); // CUtils::dump("RTI_DATA", buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -151,9 +149,9 @@ void* CIcomController::Entry()
break; break;
case RTI_EOT: { case RTI_EOT: {
// wxLogMessage(wxT("RTI_EOT")); // wxLogMessage("RTI_EOT");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -166,23 +164,23 @@ void* CIcomController::Entry()
break; break;
case RTI_PONG: case RTI_PONG:
// wxLogMessage(wxT("RTI_PONG")); // wxLogMessage("RTI_PONG");
if (!connected) if (!connected)
wxLogMessage(wxT("Connected to the Icom radio")); wxLogMessage("Connected to the Icom radio");
lostTimer.start(); lostTimer.start();
connected = true; connected = true;
break; break;
case RTI_HEADER_ACK: case RTI_HEADER_ACK:
if (buffer[2U] == 0x00U) { if (buffer[2U] == 0x00U) {
// wxLogMessage(wxT("RTI_HEADER_ACK")); // wxLogMessage("RTI_HEADER_ACK");
if (state == SI_HEADER) { if (state == SI_HEADER) {
storeLength = 0U; storeLength = 0U;
retryTimer.stop(); retryTimer.stop();
txSpace = true; txSpace = true;
} }
} else { } else {
wxLogMessage(wxT("RTI_HEADER_NAK")); wxLogMessage("RTI_HEADER_NAK");
} }
lostTimer.start(); lostTimer.start();
@ -190,33 +188,33 @@ void* CIcomController::Entry()
case RTI_DATA_ACK: case RTI_DATA_ACK:
if (buffer[3U] == 0x00U) { if (buffer[3U] == 0x00U) {
// wxLogMessage(wxT("RTI_DATA_ACK - %02X"), buffer[2U]); // wxLogMessage("RTI_DATA_ACK - %02X", buffer[2U]);
if (state == SI_DATA && seqNo == buffer[2U]) { if (state == SI_DATA && seqNo == buffer[2U]) {
storeLength = 0U; storeLength = 0U;
retryTimer.stop(); retryTimer.stop();
txSpace = true; txSpace = true;
} }
} else { } else {
wxLogMessage(wxT("RTI_DATA_NAK - %02X"), buffer[2U]); wxLogMessage("RTI_DATA_NAK - %02X", buffer[2U]);
} }
lostTimer.start(); lostTimer.start();
break; break;
default: default:
wxLogMessage(wxT("Unknown message, type: %02X"), buffer[1U]); wxLogMessage("Unknown message, type: %02X", buffer[1U]);
CUtils::dump(wxT("Buffer dump"), buffer, length); CUtils::dump("Buffer dump", buffer, length);
break; break;
} }
if (retryTimer.isRunning() && retryTimer.hasExpired()) { if (retryTimer.isRunning() && retryTimer.hasExpired()) {
assert(storeLength > 0U); assert(storeLength > 0U);
// CUtils::dump(wxT("Re-Sending"), storeData, storeLength + 1U); // CUtils::dump("Re-Sending", storeData, storeLength + 1U);
int ret = m_serial.write(storeData, storeLength + 1U); int ret = m_serial.write(storeData, storeLength + 1U);
if (ret != int(storeLength + 1U)) if (ret != int(storeLength + 1U))
wxLogWarning(wxT("Error when writing to the Icom radio")); wxLogWarning("Error when writing to the Icom radio");
retryTimer.start(); retryTimer.start();
pollTimer.start(); pollTimer.start();
@ -228,7 +226,7 @@ void* CIcomController::Entry()
storeLength = storeData[0U]; storeLength = storeData[0U];
m_txData.getData(storeData + 1U, storeLength); m_txData.getData(storeData + 1U, storeLength);
// CUtils::dump(wxT("Sending"), storeData, storeLength + 1U); // CUtils::dump("Sending", storeData, storeLength + 1U);
if (storeData[1U] == 0x20U) { if (storeData[1U] == 0x20U) {
state = SI_HEADER; state = SI_HEADER;
@ -240,7 +238,7 @@ void* CIcomController::Entry()
int ret = m_serial.write(storeData, storeLength + 1U); int ret = m_serial.write(storeData, storeLength + 1U);
if (ret != int(storeLength + 1U)) if (ret != int(storeLength + 1U))
wxLogWarning(wxT("Error when writing to the Icom radio")); wxLogWarning("Error when writing to the Icom radio");
retryTimer.start(); retryTimer.start();
pollTimer.start(); pollTimer.start();
@ -248,7 +246,7 @@ void* CIcomController::Entry()
txSpace = false; txSpace = false;
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
lostTimer.clock(); lostTimer.clock();
@ -256,7 +254,7 @@ void* CIcomController::Entry()
if (lostTimer.hasExpired()) { if (lostTimer.hasExpired()) {
if (connected) if (connected)
wxLogWarning(wxT("Lost connection to the Icom radio")); wxLogWarning("Lost connection to the Icom radio");
pollTimer.setTimeout(0U, 100U); pollTimer.setTimeout(0U, 100U);
pollTimer.start(); pollTimer.start();
@ -276,16 +274,14 @@ void* CIcomController::Entry()
m_serial.close(); m_serial.close();
wxLogMessage(wxT("Stopping Icom Controller thread")); wxLogMessage("Stopping Icom Controller thread");
return NULL;
} }
bool CIcomController::writeHeader(const CHeaderData& header) bool CIcomController::writeHeader(const CHeaderData& header)
{ {
bool ret = m_txData.hasSpace(43U); bool ret = m_txData.hasSpace(43U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -301,47 +297,52 @@ bool CIcomController::writeHeader(const CHeaderData& header)
buffer[3U] = header.getFlag2(); buffer[3U] = header.getFlag2();
buffer[4U] = header.getFlag3(); buffer[4U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 5U] = rpt2.GetChar(i); buffer[i + 5U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 13U] = rpt1.GetChar(i); buffer[i + 13U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 21U] = your.GetChar(i); buffer[i + 21U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 29U] = my1.GetChar(i); buffer[i + 29U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 37U] = my2.GetChar(i); buffer[i + 37U] = my2[i];
buffer[41U] = 0xFFU; buffer[41U] = 0xFFU;
m_txCounter = 0U; m_txCounter = 0U;
m_pktCounter = 0U; m_pktCounter = 0U;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.addData(buffer, 42U); m_txData.addData(buffer, 42U);
return true; return true;
} }
// Builds an Icom data packet (type 0x22).
// m_txCounter is the stream sequence (increments per packet, never wraps to 0).
// m_pktCounter counts frames within a super-frame (0-20), resetting either when
// a data-sync pattern is detected in the AMBE payload or at 21 frames.
// The end-of-transmission flag is bit 6 of the pktCounter byte (0x40).
bool CIcomController::writeData(const unsigned char* data, unsigned int, bool end) bool CIcomController::writeData(const unsigned char* data, unsigned int, bool end)
{ {
bool ret = m_txData.hasSpace(18U); bool ret = m_txData.hasSpace(18U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
unsigned char buffer[20U]; unsigned char buffer[20U] = {};
if (end) { if (end) {
buffer[0U] = 0x10U; buffer[0U] = 0x10U;
@ -355,7 +356,7 @@ bool CIcomController::writeData(const unsigned char* data, unsigned int, bool en
buffer[16U] = 0xFFU; buffer[16U] = 0xFFU;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.addData(buffer, 17U); m_txData.addData(buffer, 17U);
@ -371,14 +372,14 @@ bool CIcomController::writeData(const unsigned char* data, unsigned int, bool en
m_txCounter++; m_txCounter++;
m_pktCounter++; m_pktCounter++;
if (::memcmp(buffer + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES) == 0 || m_pktCounter == 21U) if (::memcmp(data + VOICE_FRAME_LENGTH_BYTES, DATA_SYNC_BYTES, DATA_FRAME_LENGTH_BYTES) == 0 || m_pktCounter == 21U)
m_pktCounter = 0U; m_pktCounter = 0U;
::memcpy(buffer + 4U, data, DV_FRAME_LENGTH_BYTES); ::memcpy(buffer + 4U, data, DV_FRAME_LENGTH_BYTES);
buffer[16U] = 0xFFU; buffer[16U] = 0xFFU;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.addData(buffer, 17U); m_txData.addData(buffer, 17U);
@ -395,17 +396,23 @@ bool CIcomController::isTXReady()
return true; return true;
} }
wxString CIcomController::getPath() const std::string CIcomController::getPath() const
{ {
return wxEmptyString; return std::string();
} }
// Reads one Icom serial frame.
// byte[0] = length of the remaining payload (valid values: 0x03, 0x04, 0x10, 0x2C).
// byte[1] = message type (0x03 pong, 0x10 header, 0x12 data/EOT, 0x21 hdr-ack, 0x23 data-ack).
// A value of 0xFF in byte[0] is a line-idle fill byte; treated as RT_TIMEOUT.
// All reads on bytes[2..] use a 40ms timeout to avoid blocking the main loop
// indefinitely if the radio stops mid-frame.
RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int& length) RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int& length)
{ {
// Get the start of the frame or nothing at all // Get the start of the frame or nothing at all
int ret = m_serial.read(buffer, 1U); int ret = m_serial.read(buffer, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading the length from the Icom radio")); wxLogError("Error when reading the length from the Icom radio");
return RTI_ERROR; return RTI_ERROR;
} }
@ -419,13 +426,13 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
// Validate the message lengths // Validate the message lengths
if (buffer[0U] != 0x03U && buffer[0U] != 0x04U && buffer[0U] != 0x10U && buffer[0U] != 0x2CU) { if (buffer[0U] != 0x03U && buffer[0U] != 0x04U && buffer[0U] != 0x10U && buffer[0U] != 0x2CU) {
wxLogError(wxT("Invalid data length received from the Icom radio - 0x%02X"), length); wxLogError("Invalid data length received from the Icom radio - 0x%02X", length);
return RTI_TIMEOUT; return RTI_TIMEOUT;
} }
ret = m_serial.read(buffer + 1U, 1U, 40U); ret = m_serial.read(buffer + 1U, 1U, 40U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading the type from the Icom radio")); wxLogError("Error when reading the type from the Icom radio");
return RTI_ERROR; return RTI_ERROR;
} }
@ -434,7 +441,7 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
// Validate the message types // Validate the message types
if (buffer[1U] != 0x03U && buffer[1U] != 0x10U && buffer[1U] != 0x12U && buffer[1U] != 0x21U && buffer[1U] != 0x23U) { if (buffer[1U] != 0x03U && buffer[1U] != 0x10U && buffer[1U] != 0x12U && buffer[1U] != 0x21U && buffer[1U] != 0x23U) {
wxLogError(wxT("Invalid data type received from the Icom radio - 0x%02X"), buffer[1U]); wxLogError("Invalid data type received from the Icom radio - 0x%02X", buffer[1U]);
return RTI_TIMEOUT; return RTI_TIMEOUT;
} }
@ -443,7 +450,7 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
while (offset < length) { while (offset < length) {
ret = m_serial.read(buffer + offset, length - offset, 40U); ret = m_serial.read(buffer + offset, length - offset, 40U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading data from the Icom radio")); wxLogError("Error when reading data from the Icom radio");
return RTI_ERROR; return RTI_ERROR;
} }
@ -451,12 +458,12 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
offset += ret; offset += ret;
if (ret == 0) { if (ret == 0) {
// CUtils::dump(wxT("Receive timed out"), buffer, offset); // CUtils::dump("Receive timed out", buffer, offset);
return RTI_TIMEOUT; return RTI_TIMEOUT;
} }
} }
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
switch (buffer[1U]) { switch (buffer[1U]) {
case 0x03U: case 0x03U:

@ -23,8 +23,9 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
enum RESP_TYPE_ICOM { enum RESP_TYPE_ICOM {
RTI_TIMEOUT, RTI_TIMEOUT,
@ -38,13 +39,46 @@ enum RESP_TYPE_ICOM {
RTI_PONG RTI_PONG
}; };
/*
* CIcomController - Driver for Icom radios in terminal/access-point mode.
*
* Hardware interface: Serial at 38400 baud with RTS/CTS flow control.
*
* Protocol: Length-prefixed binary frames. The first byte of each frame is
* the payload length; valid lengths are 0x03, 0x04, 0x10, and 0x2C.
* The second byte is the message type:
* 0x03 - PONG (keepalive response)
* 0x10 - D-Star header received (44 bytes payload)
* 0x12 - D-Star data frame received (12 bytes payload)
* bit 6 of byte[3] set = end-of-transmission
* 0x21 - Header ACK/NAK (byte[2] == 0x00 is ACK)
* 0x23 - Data ACK/NAK (byte[2] = sequence, byte[3] == 0x00 is ACK)
*
* Packet counters:
* m_txCounter - monotonically incrementing TX stream ID (rolls at 255 -> 1).
* m_pktCounter - per-packet sequence within a stream, resets on data-sync
* pattern (0x55 0x2D 0x16) or at 21. The radio uses this to
* detect lost frames and request retransmission.
*
* Connection management:
* The driver sends poll frames (0xFF 0xFF 0xFF) initially and ping frames
* (0x02 0x02 0xFF) after the 18th poll. A 5-second lostTimer triggers
* reconnection if no response (PONG or data) is received.
*
* ACK/retry:
* Each transmitted packet is held in storeData until the matching ACK is
* received (matched by sequence number for data frames). A 50ms retryTimer
* re-sends the packet if no ACK arrives. txSpace gates the send queue to
* ensure only one unacknowledged packet is in flight at a time.
*
* getPath() returns an empty string - Icom radios do not use the sysfs USB
* path tracking used by the MMDVM/DV-RPTR family.
*/
class CIcomController : public CModem { class CIcomController : public CModem {
public: public:
CIcomController(const wxString& port); CIcomController(const std::string& port);
virtual ~CIcomController(); virtual ~CIcomController();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -53,19 +87,22 @@ public:
virtual bool writeHeader(const CHeaderData& header); virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const; virtual std::string getPath() const;
private: private:
wxString m_port; void entry();
std::string m_port;
CSerialDataController m_serial; CSerialDataController m_serial;
CRingBuffer<unsigned char> m_txData; CRingBuffer<unsigned char> m_txData;
unsigned char m_txCounter; unsigned char m_txCounter;
unsigned char m_pktCounter; unsigned char m_pktCounter;
RESP_TYPE_ICOM getResponse(unsigned char* buffer, unsigned int& length); RESP_TYPE_ICOM getResponse(unsigned char* buffer, unsigned int& length);
// Sends 0xFF 0xFF 0xFF - null keepalive before the radio responds to pings.
bool writePoll(); bool writePoll();
// Sends 0x02 0x02 0xFF - ping request; elicits a PONG response from the radio.
bool writePing(); bool writePing();
}; };
#endif #endif

@ -13,6 +13,11 @@
#include "K8055Controller.h" #include "K8055Controller.h"
#include <cassert>
#include <cstdio>
#if !defined(_WIN32)
const unsigned int VELLEMAN_VENDOR_ID = 0x10CFU; const unsigned int VELLEMAN_VENDOR_ID = 0x10CFU;
const unsigned int VELLEMAN_PRODUCT_ID = 0x5500U; const unsigned int VELLEMAN_PRODUCT_ID = 0x5500U;
@ -36,208 +41,6 @@ const char OUT_PORT6 = 0x20U;
const char OUT_PORT7 = 0x40U; const char OUT_PORT7 = 0x40U;
const char OUT_PORT8 = 0x80U; const char OUT_PORT8 = 0x80U;
#if defined(__WINDOWS__)
#include <Setupapi.h>
#include <hidsdi.h>
const DWORD USB_BUFSIZE = 9UL;
CK8055Controller::CK8055Controller(unsigned int address) :
m_address(address),
m_outp1(false),
m_outp2(false),
m_outp3(false),
m_outp4(false),
m_outp5(false),
m_outp6(false),
m_outp7(false),
m_outp8(false),
m_handle(INVALID_HANDLE_VALUE)
{
}
CK8055Controller::~CK8055Controller()
{
}
bool CK8055Controller::open()
{
wxASSERT(m_handle == INVALID_HANDLE_VALUE);
GUID guid;
::HidD_GetHidGuid(&guid);
HDEVINFO devInfo = ::SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%u"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &guid, index, &devInfoData); index++) {
// Find the required length of the device structure
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// Get the detailed data into the newly allocated device structure
DWORD required;
BOOL res = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!res) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
// Get the handle for getting the attributes
HANDLE handle = ::CreateFile(detailData->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (handle == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from CreateFile: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
HIDD_ATTRIBUTES attributes;
attributes.Size = sizeof(HIDD_ATTRIBUTES);
res = ::HidD_GetAttributes(handle, &attributes);
if (!res) {
wxLogError(wxT("Error from HidD_GetAttributes: err=%u"), ::GetLastError());
::CloseHandle(handle);
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::CloseHandle(handle);
// Is this a Velleman K8055 and the right one?
if (attributes.VendorID == VELLEMAN_VENDOR_ID && attributes.ProductID == (VELLEMAN_PRODUCT_ID + m_address)) {
m_handle = ::CreateFile(detailData->DevicePath, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (m_handle == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from CreateFile: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
setDigitalOutputs(false, false, false, false, false, false, false, false);
return true;
}
::free(detailData);
}
::SetupDiDestroyDeviceInfoList(devInfo);
return false;
}
void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
char buffer[USB_BUFSIZE];
buffer[0] = REPORT_ID;
buffer[1] = 0x00U;
buffer[2] = 0x00U;
buffer[3] = 0x00U;
buffer[4] = 0x00U;
buffer[5] = 0x00U;
buffer[6] = 0x00U;
buffer[7] = 0x00U;
buffer[8] = 0x00U;
DWORD read;
BOOL res = ::ReadFile(m_handle, buffer, USB_BUFSIZE, &read, NULL);
if (!res) {
wxLogError(wxT("Error from ReadFile: err=%u, read=%u"), ::GetLastError(), read);
return;
}
// Do we have data?
if (buffer[2] != 0x00) {
inp1 = (buffer[1] & IN_PORT1) == IN_PORT1;
inp2 = (buffer[1] & IN_PORT2) == IN_PORT2;
inp3 = (buffer[1] & IN_PORT3) == IN_PORT3;
inp4 = (buffer[1] & IN_PORT4) == IN_PORT4;
inp5 = (buffer[1] & IN_PORT5) == IN_PORT5;
}
}
void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, bool outp4, bool outp5, bool outp6, bool outp7, bool outp8)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
if (outp1 == m_outp1 && outp2 == m_outp2 && outp3 == m_outp3 && outp4 == m_outp4 &&
outp5 == m_outp5 && outp6 == m_outp6 && outp7 == m_outp7 && outp8 == m_outp8)
return;
char buffer[USB_BUFSIZE];
buffer[0] = REPORT_ID;
buffer[1] = CMD_SET_ANALOG_DIGITAL;
buffer[2] = 0x00U;
buffer[3] = 0x00U;
buffer[4] = 0x00U;
buffer[5] = 0x00U;
buffer[6] = 0x00U;
buffer[7] = 0x00U;
buffer[8] = 0x00U;
if (outp1)
buffer[2] |= OUT_PORT1;
if (outp2)
buffer[2] |= OUT_PORT2;
if (outp3)
buffer[2] |= OUT_PORT3;
if (outp4)
buffer[2] |= OUT_PORT4;
if (outp5)
buffer[2] |= OUT_PORT5;
if (outp6)
buffer[2] |= OUT_PORT6;
if (outp7)
buffer[2] |= OUT_PORT7;
if (outp8)
buffer[2] |= OUT_PORT8;
DWORD written;
BOOL res = ::WriteFile(m_handle, buffer, USB_BUFSIZE, &written, NULL);
if (!res || written != USB_BUFSIZE)
wxLogError(wxT("Error from WriteFile: err=%u, written=%u"), ::GetLastError(), written);
m_outp1 = outp1;
m_outp2 = outp2;
m_outp3 = outp3;
m_outp4 = outp4;
m_outp5 = outp5;
m_outp6 = outp6;
m_outp7 = outp7;
m_outp8 = outp8;
}
void CK8055Controller::close()
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
setDigitalOutputs(false, false, false, false, false, false, false, false);
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
}
#else
const unsigned int VELLEMAN_HID_INTERFACE = 0U; const unsigned int VELLEMAN_HID_INTERFACE = 0U;
const unsigned int USB_OUTPUT_ENDPOINT = 0x01U; const unsigned int USB_OUTPUT_ENDPOINT = 0x01U;
@ -257,27 +60,27 @@ m_outp5(false),
m_outp6(false), m_outp6(false),
m_outp7(false), m_outp7(false),
m_outp8(false), m_outp8(false),
m_context(NULL), m_context(nullptr),
m_handle(NULL) m_handle(nullptr)
{ {
::libusb_init(&m_context); ::libusb_init(&m_context);
} }
CK8055Controller::~CK8055Controller() CK8055Controller::~CK8055Controller()
{ {
wxASSERT(m_context != NULL); assert(m_context != nullptr);
::libusb_exit(m_context); ::libusb_exit(m_context);
} }
bool CK8055Controller::open() bool CK8055Controller::open()
{ {
wxASSERT(m_context != NULL); assert(m_context != nullptr);
wxASSERT(m_handle == NULL); assert(m_handle == nullptr);
m_handle = ::libusb_open_device_with_vid_pid(m_context, VELLEMAN_VENDOR_ID, VELLEMAN_PRODUCT_ID + m_address); m_handle = ::libusb_open_device_with_vid_pid(m_context, VELLEMAN_VENDOR_ID, VELLEMAN_PRODUCT_ID + m_address);
if (m_handle == NULL) { if (m_handle == nullptr) {
wxLogError(wxT("Could not open the Velleman K8055")); ::fprintf(stderr, "Could not open the Velleman K8055\n");
return false; return false;
} }
@ -285,17 +88,17 @@ bool CK8055Controller::open()
if (res != 0) { if (res != 0) {
res = ::libusb_detach_kernel_driver(m_handle, VELLEMAN_HID_INTERFACE); res = ::libusb_detach_kernel_driver(m_handle, VELLEMAN_HID_INTERFACE);
if (res != 0) { if (res != 0) {
wxLogError(wxT("Error from libusb_detach_kernel_driver: err=%d"), res); ::fprintf(stderr, "Error from libusb_detach_kernel_driver: err=%d\n", res);
::libusb_close(m_handle); ::libusb_close(m_handle);
m_handle = NULL; m_handle = nullptr;
return false; return false;
} }
res = ::libusb_claim_interface(m_handle, VELLEMAN_HID_INTERFACE); res = ::libusb_claim_interface(m_handle, VELLEMAN_HID_INTERFACE);
if (res != 0) { if (res != 0) {
wxLogError(wxT("Error from libusb_claim_interface: err=%d"), res); ::fprintf(stderr, "Error from libusb_claim_interface: err=%d\n", res);
::libusb_close(m_handle); ::libusb_close(m_handle);
m_handle = NULL; m_handle = nullptr;
return false; return false;
} }
} }
@ -309,7 +112,7 @@ bool CK8055Controller::open()
void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5) void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5)
{ {
wxASSERT(m_handle != NULL); assert(m_handle != nullptr);
unsigned char buffer[USB_BUFSIZE]; unsigned char buffer[USB_BUFSIZE];
buffer[0] = 0x00; buffer[0] = 0x00;
@ -324,7 +127,7 @@ void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool
int written; int written;
int res = ::libusb_interrupt_transfer(m_handle, USB_INPUT_ENDPOINT, buffer, USB_BUFSIZE, &written, USB_TIMEOUT); int res = ::libusb_interrupt_transfer(m_handle, USB_INPUT_ENDPOINT, buffer, USB_BUFSIZE, &written, USB_TIMEOUT);
if (res != 0) { if (res != 0) {
wxLogError(wxT("Error from libusb_interrupt_transfer: err=%d"), res); ::fprintf(stderr, "Error from libusb_interrupt_transfer: err=%d\n", res);
return; return;
} }
@ -343,7 +146,7 @@ void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool
void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, bool outp4, bool outp5, bool outp6, bool outp7, bool outp8) void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, bool outp4, bool outp5, bool outp6, bool outp7, bool outp8)
{ {
wxASSERT(m_handle != NULL); assert(m_handle != nullptr);
if (outp1 == m_outp1 && outp2 == m_outp2 && outp3 == m_outp3 && outp4 == m_outp4 && if (outp1 == m_outp1 && outp2 == m_outp2 && outp3 == m_outp3 && outp4 == m_outp4 &&
outp5 == m_outp5 && outp6 == m_outp6 && outp7 == m_outp7 && outp8 == m_outp8) outp5 == m_outp5 && outp6 == m_outp6 && outp7 == m_outp7 && outp8 == m_outp8)
@ -379,7 +182,7 @@ void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, boo
int written; int written;
int res = ::libusb_interrupt_transfer(m_handle, USB_OUTPUT_ENDPOINT, buffer, USB_BUFSIZE, &written, USB_TIMEOUT); int res = ::libusb_interrupt_transfer(m_handle, USB_OUTPUT_ENDPOINT, buffer, USB_BUFSIZE, &written, USB_TIMEOUT);
if (res != 0) { if (res != 0) {
wxLogError(wxT("Error from libusb_interrupt_transfer: err=%d"), res); ::fprintf(stderr, "Error from libusb_interrupt_transfer: err=%d\n", res);
return; return;
} }
@ -398,15 +201,46 @@ void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, boo
void CK8055Controller::close() void CK8055Controller::close()
{ {
wxASSERT(m_handle != NULL); assert(m_handle != nullptr);
setDigitalOutputs(false, false, false, false, false, false, false, false); setDigitalOutputs(false, false, false, false, false, false, false, false);
::libusb_release_interface(m_handle, VELLEMAN_HID_INTERFACE); ::libusb_release_interface(m_handle, VELLEMAN_HID_INTERFACE);
::libusb_close(m_handle); ::libusb_close(m_handle);
m_handle = NULL; m_handle = nullptr;
} }
#endif #else
// Windows stub implementation
CK8055Controller::CK8055Controller(unsigned int address) :
m_address(address)
{
}
CK8055Controller::~CK8055Controller()
{
}
bool CK8055Controller::open()
{
::fprintf(stderr, "K8055 not supported on Windows without Velleman DLL\n");
return false;
}
void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5)
{
inp1 = inp2 = inp3 = inp4 = inp5 = false;
}
void CK8055Controller::setDigitalOutputs(bool, bool, bool, bool, bool, bool, bool, bool)
{
}
void CK8055Controller::close()
{
}
#endif

@ -16,13 +16,9 @@
#include "HardwareController.h" #include "HardwareController.h"
#include <wx/wx.h> #if !defined(_WIN32)
#if defined(__WINDOWS__)
#include <windows.h>
#else
#include <libusb-1.0/libusb.h> #include <libusb-1.0/libusb.h>
#endif
class CK8055Controller : public IHardwareController { class CK8055Controller : public IHardwareController {
public: public:
@ -47,12 +43,32 @@ private:
bool m_outp6; bool m_outp6;
bool m_outp7; bool m_outp7;
bool m_outp8; bool m_outp8;
#if defined(__WINDOWS__)
HANDLE m_handle;
#else
libusb_context* m_context; libusb_context* m_context;
libusb_device_handle* m_handle; libusb_device_handle* m_handle;
#endif
}; };
#else
// Windows stub — K8055 requires the Velleman K8055D.dll which is not bundled.
// Instantiating this class on Windows will log an error and open() will return false.
class CK8055Controller : public IHardwareController {
public:
CK8055Controller(unsigned int address);
virtual ~CK8055Controller();
virtual bool open();
virtual void getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5);
virtual void setDigitalOutputs(bool outp1, bool outp2, bool outp3, bool outp4, bool outp5, bool outp6, bool outp7, bool outp8);
virtual void close();
private:
unsigned int m_address;
};
#endif
#endif #endif

@ -1,45 +0,0 @@
/*
* Copyright (C) 2010 by Jonathan Naylor G4KLX
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "LogEvent.h"
CLogEvent::CLogEvent(const wxString& text, wxEventType type, int id) :
wxEvent(id, type),
m_text(text)
{
}
CLogEvent::CLogEvent(const CLogEvent& event) :
wxEvent(event),
m_text(event.m_text)
{
}
CLogEvent::~CLogEvent()
{
}
wxString CLogEvent::getText() const
{
return m_text;
}
wxEvent* CLogEvent::Clone() const
{
return new CLogEvent(*this);
}

@ -1,40 +0,0 @@
/*
* Copyright (C) 2010 by Jonathan Naylor G4KLX
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#ifndef LogEvent_H
#define LogEvent_H
#include <wx/wx.h>
class CLogEvent : public wxEvent {
public:
CLogEvent(const wxString& text, wxEventType type, int id = 0);
virtual ~CLogEvent();
virtual wxString getText() const;
virtual wxEvent* Clone() const;
protected:
CLogEvent(const CLogEvent& event);
private:
wxString m_text;
};
#endif

@ -18,121 +18,162 @@
#include "Logger.h" #include "Logger.h"
#include <cstdarg>
#include <ctime>
#include <cstdlib>
static struct tm* gmtime_safe(const time_t* t, struct tm* result) {
#if defined(_WIN32)
gmtime_s(result, t);
#else
gmtime_r(t, result);
#endif
return result;
}
#if defined(MQTT) #if defined(MQTT)
#include "MQTTConnection.h" #include "MQTTConnection.h"
CMQTTConnection* g_mqtt = NULL; CMQTTConnection* g_mqtt = nullptr;
unsigned int g_mqttLevel = 2U; unsigned int g_mqttLevel = 2U;
#endif #endif
CLogger::CLogger(const wxString& directory, const wxString& name) : std::atomic<CLogger*> CLogger::s_instance{nullptr};
wxLog(),
CLogger::CLogger(const std::string& directory, const std::string& name,
unsigned int fileLevel, unsigned int displayLevel) :
m_name(name), m_name(name),
m_file(NULL), m_directory(directory),
m_fileName(), m_file(nullptr),
m_day(0) m_day(0),
m_mutex(),
m_fileLevel(fileLevel),
m_displayLevel(displayLevel)
{ {
m_file = new wxFFile; if (m_fileLevel == 0U)
return; // File logging disabled; do not create a log file.
m_fileName.SetPath(directory);
m_fileName.SetExt(wxT("log"));
time_t timestamp; time_t timestamp;
::time(&timestamp); ::time(&timestamp);
struct tm* tm = ::gmtime(&timestamp); struct tm tmBuf;
struct tm* tm = gmtime_safe(&timestamp, &tmBuf);
wxString text;
text.Printf(wxT("%s-%04d-%02d-%02d"), m_name.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
m_day = tm->tm_yday; m_day = tm->tm_yday;
m_fileName.SetName(text); openFile(tm);
}
bool ret = m_file->Open(m_fileName.GetFullPath(), wxT("a+t")); CLogger::~CLogger()
if (!ret) { {
wxLogError(wxT("Cannot open %s file for appending"), m_fileName.GetFullPath().c_str()); if (m_file != nullptr) {
return; ::fclose(m_file);
m_file = nullptr;
} }
} }
CLogger::~CLogger() CLogger* CLogger::getInstance()
{ {
wxASSERT(m_file != NULL); return s_instance;
}
m_file->Close(); void CLogger::setInstance(CLogger* logger)
delete m_file; {
s_instance = logger;
} }
void CLogger::DoLogRecord(wxLogLevel level, const wxString& msg, const wxLogRecordInfo& info) void CLogger::openFile(const struct tm* tm)
{ {
wxASSERT(m_file != NULL); if (m_file != nullptr)
wxASSERT(m_file->IsOpened()); ::fclose(m_file);
wxString letter; char filename[512];
::snprintf(filename, sizeof(filename), "%s/%s-%04d-%02d-%02d.log",
m_directory.c_str(), m_name.c_str(),
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
switch (level) { m_file = ::fopen(filename, "a+t");
case wxLOG_FatalError: letter = wxT("F"); break; if (m_file == nullptr)
case wxLOG_Error: letter = wxT("E"); break; ::fprintf(stderr, "Cannot open %s for appending\n", filename);
case wxLOG_Warning: letter = wxT("W"); break; }
case wxLOG_Info: letter = wxT("I"); break;
case wxLOG_Message: letter = wxT("M"); break; void CLogger::log(LogLevel level, const char* fmt, ...)
case wxLOG_Status: letter = wxT("M"); break; {
case wxLOG_Trace: letter = wxT("T"); break; // Fast-path: drop the message if neither sink would accept it.
case wxLOG_Debug: letter = wxT("D"); break; const unsigned int numLevel = static_cast<unsigned int>(level);
default: letter = wxT("U"); break; const bool toFile = (m_fileLevel != 0U && numLevel >= m_fileLevel);
} const bool toDisplay = (m_displayLevel != 0U && numLevel >= m_displayLevel);
#if defined(MQTT)
const bool toMQTT = (g_mqtt != nullptr && g_mqttLevel != 0U && numLevel >= g_mqttLevel);
#else
const bool toMQTT = false;
#endif
if (!toFile && !toDisplay && !toMQTT && level != LOG_FATAL)
return;
struct tm* tm = ::gmtime(&info.timestamp); char buffer[1024];
wxString message; va_list ap;
message.Printf(wxT("%s: %04d-%02d-%02d %02d:%02d:%02d: %s\n"), letter.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, msg.c_str()); va_start(ap, fmt);
::vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);
writeLog(message.c_str(), info.timestamp); time_t timestamp;
::time(&timestamp);
#if defined(MQTT) const char* letter;
if (g_mqtt != NULL && g_mqttLevel != 0U) {
unsigned int numLevel = 0U;
switch (level) { switch (level) {
case wxLOG_FatalError: numLevel = 6U; break; case LOG_FATAL: letter = "F"; break;
case wxLOG_Error: numLevel = 5U; break; case LOG_ERROR: letter = "E"; break;
case wxLOG_Warning: numLevel = 4U; break; case LOG_WARNING: letter = "W"; break;
case wxLOG_Info: numLevel = 3U; break; case LOG_INFO: letter = "I"; break;
case wxLOG_Message: numLevel = 2U; break; case LOG_MESSAGE: letter = "M"; break;
default: numLevel = 1U; break; case LOG_DEBUG: letter = "D"; break;
default: letter = "U"; break;
}
struct tm tmBuf;
struct tm* tm = gmtime_safe(&timestamp, &tmBuf);
char message[1280];
::snprintf(message, sizeof(message), "%s: %04d-%02d-%02d %02d:%02d:%02d: %s\n",
letter,
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec,
buffer);
if (toFile || toDisplay) {
std::lock_guard<std::mutex> lock(m_mutex);
if (toFile)
writeLog(message, timestamp);
if (toDisplay) {
::fputs(message, stdout);
::fflush(stdout);
} }
if (numLevel >= g_mqttLevel)
g_mqtt->publish("log", (const char*)message.mb_str());
} }
// MQTT publish happens outside the lock: CMQTTConnection has its own
// internal locking and mosquitto_publish() must not be called while
// holding m_mutex (risk of priority inversion / deadlock).
#if defined(MQTT)
if (toMQTT)
g_mqtt->publish("log", message);
#endif #endif
if (level == wxLOG_FatalError) if (level == LOG_FATAL)
::abort(); ::abort();
} }
void CLogger::writeLog(const wxChar* msg, time_t timestamp) void CLogger::writeLog(const char* msg, time_t timestamp)
{ {
wxASSERT(m_file != NULL); struct tm tmBuf;
wxASSERT(m_file->IsOpened()); struct tm* tm = gmtime_safe(&timestamp, &tmBuf);
wxASSERT(msg != NULL);
struct tm* tm = ::gmtime(&timestamp);
int day = tm->tm_yday; int day = tm->tm_yday;
if (day != m_day) { if (day != m_day) {
wxString text;
text.Printf(wxT("%s-%04d-%02d-%02d"), m_name.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
m_day = day; m_day = day;
m_fileName.SetName(text); openFile(tm);
m_file->Close();
bool ret = m_file->Open(m_fileName.GetFullPath(), wxT("a+t"));
if (!ret) {
wxLogError(wxT("Cannot open %s file for appending"), m_fileName.GetFullPath().c_str());
return;
}
} }
m_file->Write(wxString(msg)); if (m_file != nullptr) {
m_file->Flush(); ::fputs(msg, m_file);
::fflush(m_file);
}
} }

@ -16,34 +16,96 @@
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/ */
/*
* Singleton, thread-safe file logger with daily log rotation.
*
* One CLogger instance is created at startup and registered via setInstance().
* All threads call the static getInstance() accessor or, more commonly, use the
* wxLogXxx macros below. The macros are compatibility shims that preserve the
* original wxWidgets wxLogMessage/wxLogError call sites without pulling in the
* wxWidgets library.
*
* Log levels follow the MMDVMHost convention so that config file values map
* directly to enum values without translation:
*
* Config value 0 no logging (disabled)
* Config value 1 LOG_DEBUG (most verbose)
* Config value 2 LOG_MESSAGE
* Config value 3 LOG_INFO
* Config value 4 LOG_WARNING
* Config value 5 LOG_ERROR
* Config value 6 LOG_FATAL (least verbose)
*
* Both fileLevel and displayLevel use these values: a message is written to
* the respective sink only when its level >= the configured threshold. Setting
* a threshold to 0 disables that sink entirely; 6 passes only LOG_FATAL.
*
* When built with MQTT=1, log lines are also forwarded to the MQTT broker on
* the "{name}/log" topic via the global g_mqtt connection, filtered by the
* minimum level stored in g_mqttLevel.
*/
#ifndef Logger_H #ifndef Logger_H
#define Logger_H #define Logger_H
#include <wx/wx.h> #include <atomic>
#include <wx/ffile.h> #include <string>
#include <wx/filename.h> #include <cstdio>
#include <mutex>
#if defined(MQTT) #if defined(MQTT)
class CMQTTConnection; class CMQTTConnection;
extern CMQTTConnection* g_mqtt; extern CMQTTConnection* g_mqtt; // Global MQTT connection (set in main())
extern unsigned int g_mqttLevel; extern unsigned int g_mqttLevel; // Minimum LogLevel to forward over MQTT
#endif #endif
class CLogger : public wxLog { // Ordered to match MMDVMHost: enum value == config file level.
// Level 0 ("no logging") has no corresponding enum constant.
enum LogLevel {
LOG_DEBUG = 1,
LOG_MESSAGE = 2,
LOG_INFO = 3,
LOG_WARNING = 4,
LOG_ERROR = 5,
LOG_FATAL = 6
};
class CLogger {
public: public:
CLogger(const wxString& directory, const wxString& name); // directory — where daily log files are written; may be empty when fileLevel == 0.
virtual ~CLogger(); // name — base filename stem (e.g. "dstarrepeaterd").
// fileLevel — minimum LogLevel to write to the log file (0 = disabled).
// displayLevel — minimum LogLevel to write to stdout (0 = disabled).
CLogger(const std::string& directory, const std::string& name,
unsigned int fileLevel = 2U, unsigned int displayLevel = 2U);
~CLogger();
void log(LogLevel level, const char* fmt, ...) __attribute__((format(printf, 3, 4)));
virtual void DoLogRecord(wxLogLevel level, const wxString& msg, const wxLogRecordInfo& info); static CLogger* getInstance();
static void setInstance(CLogger* logger);
private: private:
wxString m_name; std::string m_name; // Base filename stem (e.g. "dstarrepeater")
wxFFile* m_file; std::string m_directory; // Directory where daily log files are written
wxFileName m_fileName; FILE* m_file; // Currently open log file handle (nullptr when disabled)
int m_day; int m_day; // Day-of-year when m_file was opened; triggers rotation
std::mutex m_mutex; // Serialises concurrent log() calls from multiple threads
unsigned int m_fileLevel; // Minimum level to write to file (0 = disabled)
unsigned int m_displayLevel; // Minimum level to write to stdout (0 = disabled)
void writeLog(const wxChar* msg, time_t timestamp); static std::atomic<CLogger*> s_instance; // The singleton logger
void openFile(const struct tm* tm);
void writeLog(const char* msg, time_t timestamp);
}; };
#endif // Compatibility macros matching the wxWidgets wxLogXxx API. Existing call
// sites throughout the codebase use these names and can remain unchanged.
#define wxLogFatalError(fmt, ...) do { if (CLogger::getInstance()) CLogger::getInstance()->log(LOG_FATAL, fmt, ##__VA_ARGS__); } while(0)
#define wxLogError(fmt, ...) do { if (CLogger::getInstance()) CLogger::getInstance()->log(LOG_ERROR, fmt, ##__VA_ARGS__); } while(0)
#define wxLogWarning(fmt, ...) do { if (CLogger::getInstance()) CLogger::getInstance()->log(LOG_WARNING, fmt, ##__VA_ARGS__); } while(0)
#define wxLogMessage(fmt, ...) do { if (CLogger::getInstance()) CLogger::getInstance()->log(LOG_MESSAGE, fmt, ##__VA_ARGS__); } while(0)
#define wxLogInfo(fmt, ...) do { if (CLogger::getInstance()) CLogger::getInstance()->log(LOG_INFO, fmt, ##__VA_ARGS__); } while(0)
#endif

@ -19,12 +19,16 @@
#include "CCITTChecksumReverse.h" #include "CCITTChecksumReverse.h"
#include "MMDVMController.h" #include "MMDVMController.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h" #include "Timer.h"
#if defined(__WINDOWS__) #include <chrono>
#include <setupapi.h> #include <thread>
#else #include <cassert>
#include <wx/dir.h> #include <cstring>
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif #endif
const unsigned char MMDVM_FRAME_START = 0xE0U; const unsigned char MMDVM_FRAME_START = 0xE0U;
@ -52,7 +56,7 @@ const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CMMDVMController::CMMDVMController(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int rxLevel, unsigned int txLevel) : CMMDVMController::CMMDVMController(const std::string& port, const std::string& path, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int rxLevel, unsigned int txLevel) :
CModem(), CModem(),
m_port(port), m_port(port),
m_path(path), m_path(path),
@ -63,11 +67,11 @@ m_txDelay(txDelay),
m_rxLevel(rxLevel), m_rxLevel(rxLevel),
m_txLevel(txLevel), m_txLevel(txLevel),
m_serial(port, SERIAL_115200, true), m_serial(port, SERIAL_115200, true),
m_buffer(NULL), m_buffer(nullptr),
m_txData(1000U), m_txData(1000U),
m_rx(false) m_rx(false)
{ {
wxASSERT(!port.IsEmpty()); assert(!port.empty());
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
} }
@ -87,16 +91,14 @@ bool CMMDVMController::start()
findPath(); findPath();
Create(); m_thread = std::thread(&CMMDVMController::entry, this);
SetPriority(100U);
Run();
return true; return true;
} }
void* CMMDVMController::Entry() void CMMDVMController::entry()
{ {
wxLogMessage(wxT("Starting MMDVM Controller thread")); wxLogMessage("Starting MMDVM Controller thread");
// Clock every 5ms-ish // Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U); CTimer pollTimer(200U, 0U, 100U);
@ -115,8 +117,9 @@ void* CMMDVMController::Entry()
if (!ret) { if (!ret) {
ret = findModem(); ret = findModem();
if (!ret) { if (!ret) {
wxLogError(wxT("Stopping MMDVM Controller thread")); wxLogError("Stopping MMDVM Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
} }
@ -133,15 +136,16 @@ void* CMMDVMController::Entry()
case RTDVM_ERROR: { case RTDVM_ERROR: {
bool ret = findModem(); bool ret = findModem();
if (!ret) { if (!ret) {
wxLogError(wxT("Stopping MMDVM Controller thread")); wxLogError("Stopping MMDVM Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
} }
break; break;
case RTDVM_DSTAR_HEADER: { case RTDVM_DSTAR_HEADER: {
// CUtils::dump(wxT("RT_DSTAR_HEADER"), m_buffer, length); // CUtils::dump("RT_DSTAR_HEADER", m_buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_HEADER; data[0U] = DSMTT_HEADER;
@ -155,8 +159,8 @@ void* CMMDVMController::Entry()
break; break;
case RTDVM_DSTAR_DATA: { case RTDVM_DSTAR_DATA: {
// CUtils::dump(wxT("RT_DSTAR_DATA"), m_buffer, length); // CUtils::dump("RT_DSTAR_DATA", m_buffer, length);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_DATA; data[0U] = DSMTT_DATA;
@ -170,8 +174,8 @@ void* CMMDVMController::Entry()
break; break;
case RTDVM_DSTAR_EOT: { case RTDVM_DSTAR_EOT: {
// wxLogMessage(wxT("RT_DSTAR_EOT")); // wxLogMessage("RT_DSTAR_EOT");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -183,8 +187,8 @@ void* CMMDVMController::Entry()
break; break;
case RTDVM_DSTAR_LOST: { case RTDVM_DSTAR_LOST: {
// wxLogMessage(wxT("RT_DSTAR_LOST")); // wxLogMessage("RT_DSTAR_LOST");
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_LOST; data[0U] = DSMTT_LOST;
@ -198,20 +202,21 @@ void* CMMDVMController::Entry()
case RTDVM_GET_STATUS: { case RTDVM_GET_STATUS: {
bool dstar = (m_buffer[3U] & 0x01U) == 0x01U; bool dstar = (m_buffer[3U] & 0x01U) == 0x01U;
if (!dstar) { if (!dstar) {
wxLogError(wxT("D-Star not enabled in the MMDVM!!!")); wxLogError("D-Star not enabled in the MMDVM!!!");
wxLogError(wxT("Stopping MMDVM Controller thread")); wxLogError("Stopping MMDVM Controller thread");
return NULL; delete[] writeBuffer;
return;
} }
m_tx = (m_buffer[5U] & 0x01U) == 0x01U; m_tx = (m_buffer[5U] & 0x01U) == 0x01U;
bool adcOverflow = (m_buffer[5U] & 0x02U) == 0x02U; bool adcOverflow = (m_buffer[5U] & 0x02U) == 0x02U;
if (adcOverflow) if (adcOverflow)
wxLogWarning(wxT("MMDVM ADC levels have overflowed")); wxLogWarning("MMDVM ADC levels have overflowed");
space = m_buffer[6U]; space = m_buffer[6U];
// CUtils::dump(wxT("GET_STATUS"), m_buffer, length); // CUtils::dump("GET_STATUS", m_buffer, length);
// wxLogMessage(wxT("PTT=%d space=%u"), int(m_tx), space); // wxLogMessage("PTT=%d space=%u", int(m_tx), space);
} }
break; break;
@ -223,23 +228,23 @@ void* CMMDVMController::Entry()
case RTDVM_NAK: { case RTDVM_NAK: {
switch (m_buffer[3U]) { switch (m_buffer[3U]) {
case MMDVM_DSTAR_HEADER: case MMDVM_DSTAR_HEADER:
wxLogWarning(wxT("Received a header NAK from the MMDVM, reason = %u"), m_buffer[4U]); wxLogWarning("Received a header NAK from the MMDVM, reason = %u", m_buffer[4U]);
break; break;
case MMDVM_DSTAR_DATA: case MMDVM_DSTAR_DATA:
wxLogWarning(wxT("Received a data NAK from the MMDVM, reason = %u"), m_buffer[4U]); wxLogWarning("Received a data NAK from the MMDVM, reason = %u", m_buffer[4U]);
break; break;
case MMDVM_DSTAR_EOT: case MMDVM_DSTAR_EOT:
wxLogWarning(wxT("Received an EOT NAK from the MMDVM, reason = %u"), m_buffer[4U]); wxLogWarning("Received an EOT NAK from the MMDVM, reason = %u", m_buffer[4U]);
break; break;
default: default:
wxLogWarning(wxT("Received a NAK from the MMDVM, command = 0x%02X, reason = %u"), m_buffer[3U], m_buffer[4U]); wxLogWarning("Received a NAK from the MMDVM, command = 0x%02X, reason = %u", m_buffer[3U], m_buffer[4U]);
break; break;
} }
} }
break; break;
case RTDVM_DUMP: case RTDVM_DUMP:
CUtils::dump(wxT("Modem dump"), m_buffer + 3U, length - 3U); CUtils::dump("Modem dump", m_buffer + 3U, length - 3U);
break; break;
case RTDVM_DEBUG1: case RTDVM_DEBUG1:
@ -251,13 +256,13 @@ void* CMMDVMController::Entry()
break; break;
default: default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[2U]); wxLogMessage("Unknown message, type: %02X", m_buffer[2U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length); CUtils::dump("Buffer dump", m_buffer, length);
break; break;
} }
if (writeType == DSMTT_NONE && m_txData.hasData()) { if (writeType == DSMTT_NONE && m_txData.hasData()) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&writeType, 1U); m_txData.getData(&writeType, 1U);
m_txData.getData(&writeLength, 1U); m_txData.getData(&writeLength, 1U);
@ -265,46 +270,44 @@ void* CMMDVMController::Entry()
} }
if (space > 4U && writeType == DSMTT_HEADER) { if (space > 4U && writeType == DSMTT_HEADER) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength); // CUtils::dump("Write Header", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing the header to the MMDVM")); wxLogWarning("Error when writing the header to the MMDVM");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space -= 4U; space -= 4U;
} }
if (space > 1U && (writeType == DSMTT_DATA || writeType == DSMTT_EOT)) { if (space > 1U && (writeType == DSMTT_DATA || writeType == DSMTT_EOT)) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength); // CUtils::dump("Write Data", writeBuffer, writeLength);
int ret = m_serial.write(writeBuffer, writeLength); int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength)) if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the MMDVM")); wxLogWarning("Error when writing data to the MMDVM");
writeType = DSMTT_NONE; writeType = DSMTT_NONE;
space--; space--;
} }
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock(); pollTimer.clock();
} }
wxLogMessage(wxT("Stopping MMDVM Controller thread")); wxLogMessage("Stopping MMDVM Controller thread");
delete[] writeBuffer; delete[] writeBuffer;
m_serial.close(); m_serial.close();
return NULL;
} }
bool CMMDVMController::writeHeader(const CHeaderData& header) bool CMMDVMController::writeHeader(const CHeaderData& header)
{ {
bool ret = m_txData.hasSpace(46U); bool ret = m_txData.hasSpace(46U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write the header")); wxLogWarning("No space to write the header");
return false; return false;
} }
@ -320,31 +323,31 @@ bool CMMDVMController::writeHeader(const CHeaderData& header)
buffer[4U] = header.getFlag2(); buffer[4U] = header.getFlag2();
buffer[5U] = header.getFlag3(); buffer[5U] = header.getFlag3();
wxString rpt2 = header.getRptCall2(); std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 6U] = rpt2.GetChar(i); buffer[i + 6U] = rpt2[i];
wxString rpt1 = header.getRptCall1(); std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 14U] = rpt1.GetChar(i); buffer[i + 14U] = rpt1[i];
wxString your = header.getYourCall(); std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 22U] = your.GetChar(i); buffer[i + 22U] = your[i];
wxString my1 = header.getMyCall1(); std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 30U] = my1.GetChar(i); buffer[i + 30U] = my1[i];
wxString my2 = header.getMyCall2(); std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 38U] = my2.GetChar(i); buffer[i + 38U] = my2[i];
CCCITTChecksumReverse cksum1; CCCITTChecksumReverse cksum1;
cksum1.update(buffer + 3U, RADIO_HEADER_LENGTH_BYTES - 2U); cksum1.update(buffer + 3U, RADIO_HEADER_LENGTH_BYTES - 2U);
cksum1.result(buffer + 42U); cksum1.result(buffer + 42U);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_HEADER; unsigned char type = DSMTT_HEADER;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -361,7 +364,7 @@ bool CMMDVMController::writeData(const unsigned char* data, unsigned int length,
{ {
bool ret = m_txData.hasSpace(17U); bool ret = m_txData.hasSpace(17U);
if (!ret) { if (!ret) {
wxLogWarning(wxT("No space to write data")); wxLogWarning("No space to write data");
return false; return false;
} }
@ -372,7 +375,7 @@ bool CMMDVMController::writeData(const unsigned char* data, unsigned int length,
buffer[1U] = 3U; buffer[1U] = 3U;
buffer[2U] = MMDVM_DSTAR_EOT; buffer[2U] = MMDVM_DSTAR_EOT;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_EOT; unsigned char type = DSMTT_EOT;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -390,7 +393,7 @@ bool CMMDVMController::writeData(const unsigned char* data, unsigned int length,
buffer[2U] = MMDVM_DSTAR_DATA; buffer[2U] = MMDVM_DSTAR_DATA;
::memcpy(buffer + 3U, data, DV_FRAME_LENGTH_BYTES); ::memcpy(buffer + 3U, data, DV_FRAME_LENGTH_BYTES);
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_DATA; unsigned char type = DSMTT_DATA;
m_txData.addData(&type, 1U); m_txData.addData(&type, 1U);
@ -416,9 +419,13 @@ bool CMMDVMController::isTXReady()
return m_txData.isEmpty(); return m_txData.isEmpty();
} }
// Waits 2s for the MMDVM firmware to finish booting, then retries GET_VERSION
// up to 6 times (with 1s between attempts) before giving up. The 2s initial
// wait is required because the Arduino bootloader holds the serial port for
// approximately that period after a USB connection is established.
bool CMMDVMController::readVersion() bool CMMDVMController::readVersion()
{ {
::wxSleep(2); std::this_thread::sleep_for(std::chrono::seconds(2));
for (unsigned int i = 0U; i < 6U; i++) { for (unsigned int i = 0U; i < 6U; i++) {
unsigned char buffer[3U]; unsigned char buffer[3U];
@ -427,30 +434,30 @@ bool CMMDVMController::readVersion()
buffer[1U] = 3U; buffer[1U] = 3U;
buffer[2U] = MMDVM_GET_VERSION; buffer[2U] = MMDVM_GET_VERSION;
// CUtils::dump(wxT("Written"), buffer, 3U); // CUtils::dump("Written", buffer, 3U);
int ret = m_serial.write(buffer, 3U); int ret = m_serial.write(buffer, 3U);
if (ret != 3) if (ret != 3)
return false; return false;
for (unsigned int count = 0U; count < MAX_RESPONSES; count++) { for (unsigned int count = 0U; count < MAX_RESPONSES; count++) {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
unsigned int length; unsigned int length;
RESP_TYPE_MMDVM resp = getResponse(m_buffer, length); RESP_TYPE_MMDVM resp = getResponse(m_buffer, length);
if (resp == RTDVM_GET_VERSION) { if (resp == RTDVM_GET_VERSION) {
wxString description((char*)(m_buffer + 4U), wxConvLocal, length - 4U); std::string description((char*)(m_buffer + 4U), length - 4U);
wxLogInfo(wxT("MMDVM protocol version: %u, description: %s"), m_buffer[3U], description.c_str()); wxLogInfo("MMDVM protocol version: %u, description: %s", m_buffer[3U], description.c_str());
return true; return true;
} }
} }
::wxSleep(1); std::this_thread::sleep_for(std::chrono::seconds(1));
} }
wxLogError(wxT("Unable to read the firmware version after six attempts")); wxLogError("Unable to read the firmware version after six attempts");
return false; return false;
} }
@ -466,6 +473,14 @@ bool CMMDVMController::readStatus()
return m_serial.write(buffer, 3U) == 3; return m_serial.write(buffer, 3U) == 3;
} }
// Sends SET_CONFIG (20-byte frame) to configure the MMDVM for D-Star only.
// Key fields:
// byte[3] invert flags: bit0=rxInvert, bit1=txInvert, bit2=pttInvert
// byte[4] mode mask: 0x01 = D-Star enabled, all other modes off
// byte[5] txDelay: value / 10ms units (e.g. txDelay=100 -> 10)
// byte[6] initial state: 1 = STATE_DSTAR
// byte[7] rxLevel, byte[8] txLevel: 0-100% scaled to 0-255
// bytes[12-15,18] txLevel repeated for DMR/YSF/P25/NXDN (unused but required)
bool CMMDVMController::setConfig() bool CMMDVMController::setConfig()
{ {
unsigned char buffer[25U]; unsigned char buffer[25U];
@ -511,7 +526,7 @@ bool CMMDVMController::setConfig()
buffer[19U] = 0U; buffer[19U] = 0U;
// CUtils::dump(wxT("Written"), buffer, 20U); // CUtils::dump("Written", buffer, 20U);
int ret = m_serial.write(buffer, 20U); int ret = m_serial.write(buffer, 20U);
if (ret != 20U) if (ret != 20U)
@ -521,23 +536,23 @@ bool CMMDVMController::setConfig()
unsigned int length; unsigned int length;
RESP_TYPE_MMDVM resp; RESP_TYPE_MMDVM resp;
do { do {
::wxMilliSleep(10UL); std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length); resp = getResponse(m_buffer, length);
if (resp != RTDVM_ACK && resp != RTDVM_NAK) { if (resp != RTDVM_ACK && resp != RTDVM_NAK) {
count++; count++;
if (count >= MAX_RESPONSES) { if (count >= MAX_RESPONSES) {
wxLogError(wxT("The MMDVM is not responding to the SET_CONFIG command")); wxLogError("The MMDVM is not responding to the SET_CONFIG command");
return false; return false;
} }
} }
} while (resp != RTDVM_ACK && resp != RTDVM_NAK); } while (resp != RTDVM_ACK && resp != RTDVM_NAK);
// CUtils::dump(wxT("Response"), m_buffer, length); // CUtils::dump("Response", m_buffer, length);
if (resp == RTDVM_NAK) { if (resp == RTDVM_NAK) {
wxLogError(wxT("Received a NAK to the SET_CONFIG command from the modem")); wxLogError("Received a NAK to the SET_CONFIG command from the modem");
return false; return false;
} }
@ -549,7 +564,7 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
// Get the start of the frame or nothing at all // Get the start of the frame or nothing at all
int ret = m_serial.read(buffer + 0U, 1U); int ret = m_serial.read(buffer + 0U, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the MMDVM")); wxLogError("Error when reading from the MMDVM");
return RTDVM_ERROR; return RTDVM_ERROR;
} }
@ -561,7 +576,7 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
ret = m_serial.read(buffer + 1U, 1U); ret = m_serial.read(buffer + 1U, 1U);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the MMDVM")); wxLogError("Error when reading from the MMDVM");
return RTDVM_ERROR; return RTDVM_ERROR;
} }
@ -571,8 +586,8 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
length = buffer[1U]; length = buffer[1U];
if (length >= BUFFER_LENGTH) { if (length >= BUFFER_LENGTH) {
wxLogError(wxT("Invalid data received from the MMDVM")); wxLogError("Invalid data received from the MMDVM");
CUtils::dump(wxT("Data"), buffer, 2U); CUtils::dump("Data", buffer, 2U);
return RTDVM_TIMEOUT; return RTDVM_TIMEOUT;
} }
@ -581,18 +596,21 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
while (offset < length) { while (offset < length) {
int ret = m_serial.read(buffer + offset, length - offset); int ret = m_serial.read(buffer + offset, length - offset);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error when reading from the MMDVM")); wxLogError("Error when reading from the MMDVM");
return RTDVM_ERROR; return RTDVM_ERROR;
} }
if (ret > 0) if (ret > 0)
offset += ret; offset += ret;
if (ret == 0) if (ret == 0) {
Sleep(5UL); std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (m_stopped)
return RTDVM_TIMEOUT;
}
} }
// CUtils::dump(wxT("Received"), buffer, length); // CUtils::dump("Received", buffer, length);
switch (buffer[2U]) { switch (buffer[2U]) {
case MMDVM_GET_STATUS: case MMDVM_GET_STATUS:
@ -628,151 +646,109 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
} }
} }
wxString CMMDVMController::getPath() const std::string CMMDVMController::getPath() const
{ {
return m_path; return m_path;
} }
bool CMMDVMController::findPort() bool CMMDVMController::findPort()
{ {
if (m_path.IsEmpty()) #if !defined(_WIN32)
if (m_path.empty())
return false; return false;
#if defined(__WINDOWS__) DIR* dir = ::opendir("/sys/class/tty");
#else if (dir == nullptr) {
wxDir dir; wxLogError("Cannot open directory /sys/class/tty");
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
return false; return false;
} }
wxString fileName; struct dirent* entry;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*")); while ((entry = ::readdir(dir)) != nullptr) {
while (ret1) { std::string fileName(entry->d_name);
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
char cpath[255U]; // Match ttyACM* entries
::memset(cpath, 0x00U, 255U); if (fileName.substr(0, 6) != "ttyACM")
continue;
std::string path = "/sys/class/tty/" + fileName;
for (unsigned int i = 0U; i < path.Len(); i++) char cpath[255U];
cpath[i] = path.GetChar(i); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U]; char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U); int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, 255U); ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) { if (ret2 < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
::closedir(dir);
return false; return false;
} }
symlink[ret2] = '\0';
path = wxString(symlink, wxConvLocal, ret2); path = std::string(symlink, ret2);
} else { } else {
// Get all but the last section symlink[ret2] = '\0';
wxString fullPath = wxString(symlink, wxConvLocal, ret2); std::string fullPath(symlink, ret2);
path = fullPath.BeforeLast(wxT('/')); size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (path.IsSameAs(m_path)) { if (path == m_path) {
m_port.Printf(wxT("/dev/%s"), fileName.c_str()); m_port = "/dev/" + fileName;
wxLogMessage(wxT("Found modem port of %s based on the path"), m_port.c_str()); wxLogMessage("Found modem port of %s based on the path", m_port.c_str());
::closedir(dir);
return true; return true;
} }
ret1 = dir.GetNext(&fileName);
} }
#endif
::closedir(dir);
return false; return false;
#else
return true;
#endif
} }
bool CMMDVMController::findPath() bool CMMDVMController::findPath()
{ {
#if defined(__WINDOWS__) #if !defined(_WIN32)
#ifdef notdef std::string path = "/sys/class/tty/" + m_port.substr(5U);
GUID guids[5U];
DWORD count;
BOOL res = ::SetupDiClassGuidsFromName(L"Multifunction", guids, 5U, &count);
if (!res) {
wxLogError(wxT("Error from SetupDiClassGuidsFromName: err=%u"), ::GetLastError());
return false;
}
for (DWORD i = 0U; i < count; i++) {
HDEVINFO devInfo = ::SetupDiGetClassDevs(&guids[i], NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devInfo == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Error from SetupDiGetClassDevs: err=%u"), ::GetLastError());
return false;
}
SP_DEVICE_INTERFACE_DATA devInfoData;
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD index = 0U; ::SetupDiEnumDeviceInterfaces(devInfo, NULL, &guids[i], index, &devInfoData); index++) {
// Find the required length of the device structure
DWORD length;
::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, NULL, 0U, &length, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = PSP_DEVICE_INTERFACE_DETAIL_DATA(::malloc(length));
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// Get the detailed data into the newly allocated device structure
DWORD required;
res = ::SetupDiGetDeviceInterfaceDetail(devInfo, &devInfoData, detailData, length, &required, NULL);
if (!res) {
wxLogError(wxT("Error from SetupDiGetDeviceInterfaceDetail: err=%u"), ::GetLastError());
::SetupDiDestroyDeviceInfoList(devInfo);
::free(detailData);
return false;
}
::free(detailData);
}
::SetupDiDestroyDeviceInfoList(devInfo);
}
return false;
#endif
#else
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), m_port.Mid(5U).c_str());
char cpath[255U]; char cpath[255U];
::memset(cpath, 0x00U, 255U); ::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char symlink[255U]; char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U); int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
::strcat(cpath, "/device"); ::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, 255U); ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) { if (ret < 0) {
wxLogError(wxT("Error from readlink()")); wxLogError("Error from readlink()");
return false; return false;
} }
symlink[ret] = '\0';
path = wxString(symlink, wxConvLocal, ret); path = std::string(symlink, ret);
} else { } else {
wxString fullPath = wxString(symlink, wxConvLocal, ret); symlink[ret] = '\0';
path = fullPath.BeforeLast(wxT('/')); std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
} }
if (m_path.IsEmpty()) if (m_path.empty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str()); wxLogMessage("Found modem path of %s", path.c_str());
m_path = path; m_path = path;
#endif
return true; return true;
#else
return true;
#endif
} }
bool CMMDVMController::findModem() bool CMMDVMController::findModem()
@ -781,7 +757,7 @@ bool CMMDVMController::findModem()
// Tell the repeater that the signal has gone away // Tell the repeater that the signal has gone away
if (m_rx) { if (m_rx) {
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U]; unsigned char data[2U];
data[0U] = DSMTT_EOT; data[0U] = DSMTT_EOT;
@ -797,7 +773,7 @@ bool CMMDVMController::findModem()
while (!m_stopped) { while (!m_stopped) {
count++; count++;
if (count >= 4U) { if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem")); wxLogMessage("Trying to reopen the modem");
bool ret = findPort(); bool ret = findPort();
if (ret) { if (ret) {
@ -809,7 +785,7 @@ bool CMMDVMController::findModem()
count = 0U; count = 0U;
} }
Sleep(500UL); std::this_thread::sleep_for(std::chrono::milliseconds(500));
} }
return false; return false;
@ -839,30 +815,30 @@ bool CMMDVMController::openModem()
void CMMDVMController::printDebug() void CMMDVMController::printDebug()
{ {
unsigned int length = m_buffer[1U]; unsigned int length = m_buffer[1U];
if (m_buffer[2U] == 0xF1U) { if (m_buffer[2U] == 0xF1U && length >= 4U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 3U); std::string message((char*)(m_buffer + 3U), length - 3U);
wxLogMessage(wxT("Debug: %s"), message.c_str()); wxLogMessage("Debug: %s", message.c_str());
} else if (m_buffer[2U] == 0xF2U) { } else if (m_buffer[2U] == 0xF2U && length >= 5U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 5U); std::string message((char*)(m_buffer + 3U), length - 5U);
short val1 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U]; short val1 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d"), message.c_str(), val1); wxLogMessage("Debug: %s %d", message.c_str(), val1);
} else if (m_buffer[2U] == 0xF3U) { } else if (m_buffer[2U] == 0xF3U && length >= 7U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 7U); std::string message((char*)(m_buffer + 3U), length - 7U);
short val1 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U]; short val1 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U];
short val2 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U]; short val2 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d %d"), message.c_str(), val1, val2); wxLogMessage("Debug: %s %d %d", message.c_str(), val1, val2);
} else if (m_buffer[2U] == 0xF4U) { } else if (m_buffer[2U] == 0xF4U && length >= 9U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 9U); std::string message((char*)(m_buffer + 3U), length - 9U);
short val1 = (m_buffer[length - 6U] << 8) | m_buffer[length - 5U]; short val1 = (m_buffer[length - 6U] << 8) | m_buffer[length - 5U];
short val2 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U]; short val2 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U];
short val3 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U]; short val3 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d %d %d"), message.c_str(), val1, val2, val3); wxLogMessage("Debug: %s %d %d %d", message.c_str(), val1, val2, val3);
} else if (m_buffer[2U] == 0xF5U) { } else if (m_buffer[2U] == 0xF5U && length >= 11U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 11U); std::string message((char*)(m_buffer + 3U), length - 11U);
short val1 = (m_buffer[length - 8U] << 8) | m_buffer[length - 7U]; short val1 = (m_buffer[length - 8U] << 8) | m_buffer[length - 7U];
short val2 = (m_buffer[length - 6U] << 8) | m_buffer[length - 5U]; short val2 = (m_buffer[length - 6U] << 8) | m_buffer[length - 5U];
short val3 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U]; short val3 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U];
short val4 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U]; short val4 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d %d %d %d"), message.c_str(), val1, val2, val3, val4); wxLogMessage("Debug: %s %d %d %d %d", message.c_str(), val1, val2, val3, val4);
} }
} }

@ -23,8 +23,9 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "Modem.h" #include "Modem.h"
#include "Utils.h" #include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <string>
enum RESP_TYPE_MMDVM { enum RESP_TYPE_MMDVM {
RTDVM_TIMEOUT, RTDVM_TIMEOUT,
@ -46,13 +47,52 @@ enum RESP_TYPE_MMDVM {
RTDVM_DEBUG5 RTDVM_DEBUG5
}; };
/*
* CMMDVMController - Driver for MMDVM (Multi-Mode Digital Voice Modem) boards.
*
* Hardware interface: USB CDC-ACM serial at 115200 baud with RTS/CTS flow control.
*
* Protocol: Binary, 3-byte minimum frame.
* byte[0] = 0xE0 (MMDVM_FRAME_START, always)
* byte[1] = total frame length (including this 3-byte header)
* byte[2] = frame type (command/response code)
* byte[3..] = payload
*
* Frame types used for D-Star:
* 0x00 GET_VERSION - request firmware version string
* 0x01 GET_STATUS - request modem status (TX state, space, ADC overflow)
* 0x02 SET_CONFIG - configure modem (invert flags, levels, txDelay, mode)
* 0x10 DSTAR_HEADER - D-Star header (TX from host, RX from modem)
* 0x11 DSTAR_DATA - D-Star voice frame
* 0x12 DSTAR_LOST - RX sync lost (modem -> host)
* 0x13 DSTAR_EOT - end of transmission
* 0x70 ACK - positive acknowledgement
* 0x7F NAK - negative acknowledgement (with reason byte)
* 0xF0 DUMP - raw hex dump from firmware
* 0xF1-0xF5 DEBUG1-5 - firmware debug messages with 0-4 int16 parameters
*
* Startup sequence (readVersion waits 2s for the firmware to boot):
* readVersion() -> setConfig() -> launch entry() thread
*
* During operation (entry() thread):
* - Sends GET_STATUS every 100ms (readStatus()), which triggers a status
* response containing the current TX flag and remaining TX buffer space.
* - Buffer space is used to gate outbound header and data writes:
* space > 4 required to send a header (4 frames reserved)
* space > 1 required to send a data frame
*
* pttInvert: inverts the PTT output polarity (useful when the radio uses
* active-low PTT keying).
* rxLevel / txLevel: 0-100%, scaled to 0-255 for the modem's ADC/DAC ranges.
*
* Auto-detection (findPort / findPath): same sysfs USB path mechanism as
* CDVMegaController; allows the modem to be found after a USB re-enumeration.
*/
class CMMDVMController : public CModem { class CMMDVMController : public CModem {
public: public:
CMMDVMController(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int rxLevel, unsigned int txLevel); CMMDVMController(const std::string& port, const std::string& path, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int rxLevel, unsigned int txLevel);
virtual ~CMMDVMController(); virtual ~CMMDVMController();
virtual void* Entry();
virtual bool start(); virtual bool start();
virtual unsigned int getSpace(); virtual unsigned int getSpace();
@ -61,11 +101,13 @@ public:
virtual bool writeHeader(const CHeaderData& header); virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end); virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const; virtual std::string getPath() const;
private: private:
wxString m_port; void entry();
wxString m_path;
std::string m_port;
std::string m_path;
bool m_rxInvert; bool m_rxInvert;
bool m_txInvert; bool m_txInvert;
bool m_pttInvert; bool m_pttInvert;
@ -89,8 +131,9 @@ private:
bool findModem(); bool findModem();
bool openModem(); bool openModem();
// Decodes and logs a DEBUG1-DEBUG5 firmware debug frame.
// DEBUG1 carries a string; DEBUG2-5 append 1-4 big-endian int16 values.
void printDebug(); void printDebug();
}; };
#endif #endif

@ -23,9 +23,9 @@
#include <cassert> #include <cassert>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#if defined(_WIN32)
#if defined(_WIN32) || defined(_WIN64)
#include <process.h> #include <process.h>
#define getpid _getpid
#else #else
#include <unistd.h> #include <unistd.h>
#endif #endif
@ -60,11 +60,7 @@ CMQTTConnection::~CMQTTConnection()
bool CMQTTConnection::open() bool CMQTTConnection::open()
{ {
char name[50U]; char name[50U];
#if defined(_WIN32) || defined(_WIN64) ::snprintf(name, sizeof(name), "DStarRepeater.%u", (unsigned)::getpid());
::sprintf(name, "DStarRepeater.%u", (unsigned)::_getpid());
#else
::sprintf(name, "DStarRepeater.%u", (unsigned)::getpid());
#endif
::fprintf(stdout, "DStarRepeater (%s) connecting to MQTT as %s\n", m_name.c_str(), name); ::fprintf(stdout, "DStarRepeater (%s) connecting to MQTT as %s\n", m_name.c_str(), name);
@ -127,7 +123,7 @@ bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsi
if (::strchr(topic, '/') == nullptr) { if (::strchr(topic, '/') == nullptr) {
char topicEx[100U]; char topicEx[100U];
::sprintf(topicEx, "%s/%s", m_name.c_str(), topic); ::snprintf(topicEx, sizeof(topicEx), "%s/%s", m_name.c_str(), topic);
int rc = ::mosquitto_publish(m_mosq, nullptr, topicEx, len, data, static_cast<int>(m_qos), false); int rc = ::mosquitto_publish(m_mosq, nullptr, topicEx, len, data, static_cast<int>(m_qos), false);
if (rc != MOSQ_ERR_SUCCESS) { if (rc != MOSQ_ERR_SUCCESS) {
@ -174,7 +170,7 @@ void CMQTTConnection::onConnect(mosquitto* mosq, void* obj, int rc)
if (topic.find_first_of('/') == std::string::npos) { if (topic.find_first_of('/') == std::string::npos) {
char topicEx[100U]; char topicEx[100U];
::sprintf(topicEx, "%s/%s", p->m_name.c_str(), topic.c_str()); ::snprintf(topicEx, sizeof(topicEx), "%s/%s", p->m_name.c_str(), topic.c_str());
rc = ::mosquitto_subscribe(mosq, nullptr, topicEx, static_cast<int>(p->m_qos)); rc = ::mosquitto_subscribe(mosq, nullptr, topicEx, static_cast<int>(p->m_qos));
if (rc != MOSQ_ERR_SUCCESS) { if (rc != MOSQ_ERR_SUCCESS) {
@ -213,7 +209,7 @@ void CMQTTConnection::onMessage(mosquitto* mosq, void* obj, const mosquitto_mess
std::string topic = (*it).first; std::string topic = (*it).first;
char topicEx[100U]; char topicEx[100U];
::sprintf(topicEx, "%s/%s", p->m_name.c_str(), topic.c_str()); ::snprintf(topicEx, sizeof(topicEx), "%s/%s", p->m_name.c_str(), topic.c_str());
if (::strcmp(topicEx, message->topic) == 0) { if (::strcmp(topicEx, message->topic) == 0) {
(*it).second((unsigned char*)message->payload, message->payloadlen); (*it).second((unsigned char*)message->payload, message->payloadlen);

@ -23,9 +23,27 @@
#include <mosquitto.h> #include <mosquitto.h>
#include <atomic>
#include <vector> #include <vector>
#include <string> #include <string>
/*
* Thin wrapper around the libmosquitto client for publish-only MQTT telemetry.
*
* open() initialises the mosquitto client, registers callbacks, and starts the
* internal mosquitto network loop thread (mosquitto_loop_start). After open()
* returns, publish() can be called from any thread; mosquitto handles its own
* thread safety internally.
*
* Topics without a '/' are automatically prefixed with the configured name
* (e.g. "log" becomes "MyRepeater/log"). Topics containing '/' are used as-is.
*
* Optional subscriptions can be provided via the subs vector; each entry pairs
* a topic string with a callback function invoked on incoming messages.
*
* This class is compiled only when MQTT=1 is passed to make.
*/
enum class MQTT_QOS : int { enum class MQTT_QOS : int {
AT_MODE_ONCE = 0, AT_MODE_ONCE = 0,
AT_LEAST_ONCE = 1, AT_LEAST_ONCE = 1,
@ -48,7 +66,7 @@ public:
private: private:
std::string m_host; std::string m_host;
unsigned short m_port; unsigned short m_port;
std::string m_name; std::string m_name; // Prefix prepended to bare topic strings.
bool m_authEnabled; bool m_authEnabled;
std::string m_username; std::string m_username;
std::string m_password; std::string m_password;
@ -56,8 +74,9 @@ private:
unsigned int m_keepalive; unsigned int m_keepalive;
MQTT_QOS m_qos; MQTT_QOS m_qos;
mosquitto* m_mosq; mosquitto* m_mosq;
bool m_connected; std::atomic<bool> m_connected; // Set true in onConnect, false in onDisconnect.
// Mosquitto event callbacks (called on the internal mosquitto thread).
static void onConnect(mosquitto* mosq, void* obj, int rc); static void onConnect(mosquitto* mosq, void* obj, int rc);
static void onSubscribe(mosquitto* mosq, void* obj, int mid, int qosCount, const int* grantedQOS); static void onSubscribe(mosquitto* mosq, void* obj, int mid, int qosCount, const int* grantedQOS);
static void onMessage(mosquitto* mosq, void* obj, const mosquitto_message* message); static void onMessage(mosquitto* mosq, void* obj, const mosquitto_message* message);

@ -22,8 +22,14 @@
#if defined(MQTT) #if defined(MQTT)
/* /*
* Display-Driver-compatible MQTT JSON publishing for DStarRepeater. * Inline helper functions that format and publish D-Star events as JSON to the
* MQTT broker via the global g_mqtt (CMQTTConnection*) instance.
* *
* All helpers are no-ops when g_mqtt is nullptr (MQTT not configured) or when
* the broker is not connected. They publish to the "json" sub-topic, which is
* automatically prefixed with the configured repeater name by CMQTTConnection.
*
* Display-Driver-compatible MQTT JSON publishing for DStarRepeater:
* Publishes event-driven JSON messages to the "json" topic in the format * Publishes event-driven JSON messages to the "json" topic in the format
* expected by Display-Driver, enabling LCD/OLED display output. * expected by Display-Driver, enabling LCD/OLED display output.
* *
@ -35,27 +41,55 @@
#include "MQTTConnection.h" #include "MQTTConnection.h"
#include "Logger.h" #include "Logger.h"
#include <wx/wx.h> #include <string>
#include <cstdio> #include <cstdio>
// Escape a string for safe embedding in a JSON value.
// Handles the characters that would break JSON string syntax or silently
// corrupt the document: backslash, double-quote, and the common C0 controls.
// Non-printable control characters other than \n/\r/\t are dropped entirely
// so that malformed radio headers cannot produce invalid JSON.
static inline std::string jsonEscape(const std::string& s)
{
std::string out;
out.reserve(s.size());
for (char c : s) {
if (c == '"') out += "\\\"";
else if (c == '\\') out += "\\\\";
else if (c == '\n') out += "\\n";
else if (c == '\r') out += "\\r";
else if (c == '\t') out += "\\t";
else if (c >= 0x20) out += c;
// drop other non-printable control characters
}
return out;
}
// Publish a D-Star call start event (RF or network) // Publish a D-Star call start event (RF or network)
static inline void mqttPublishDStarStart(const wxString& myCall1, const wxString& myCall2, static inline void mqttPublishDStarStart(const std::string& myCall1, const std::string& myCall2,
const wxString& yourCall, const wxString& rptCall2, const char* source) const std::string& yourCall, const std::string& rptCall2, const char* source)
{ {
if (g_mqtt == NULL) if (g_mqtt == nullptr)
return; return;
// Escape all callsign strings: over-the-air headers are untrusted input
// and may contain characters (e.g. '"' or '\') that break JSON syntax.
// 'source' is a string literal supplied by our own code, not radio data.
const std::string cs1 = jsonEscape(myCall1);
const std::string cs2 = jsonEscape(myCall2);
const std::string yc = jsonEscape(yourCall);
const std::string rc2 = jsonEscape(rptCall2);
char buf[512]; char buf[512];
::snprintf(buf, sizeof(buf), ::snprintf(buf, sizeof(buf),
"{\"D-Star\":{\"action\":\"start\"," "{\"D-Star\":{\"action\":\"start\","
"\"source_cs\":\"%s\",\"source_ext\":\"%s\"," "\"source_cs\":\"%s\",\"source_ext\":\"%s\","
"\"destination_cs\":\"%s\",\"reflector\":\"%s\"," "\"destination_cs\":\"%s\",\"reflector\":\"%s\","
"\"source\":\"%s\"}}", "\"source\":\"%s\"}}",
(const char*)myCall1.mb_str(), cs1.c_str(),
(const char*)myCall2.mb_str(), cs2.c_str(),
(const char*)yourCall.mb_str(), yc.c_str(),
(const char*)rptCall2.mb_str(), rc2.c_str(),
source); source);
g_mqtt->publish("json", buf); g_mqtt->publish("json", buf);
@ -64,7 +98,7 @@ static inline void mqttPublishDStarStart(const wxString& myCall1, const wxString
// Publish a D-Star call end event (normal termination) // Publish a D-Star call end event (normal termination)
static inline void mqttPublishDStarEnd() static inline void mqttPublishDStarEnd()
{ {
if (g_mqtt == NULL) if (g_mqtt == nullptr)
return; return;
g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"end\"}}"); g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"end\"}}");
@ -73,7 +107,7 @@ static inline void mqttPublishDStarEnd()
// Publish a D-Star call lost event (watchdog timeout / abnormal) // Publish a D-Star call lost event (watchdog timeout / abnormal)
static inline void mqttPublishDStarLost() static inline void mqttPublishDStarLost()
{ {
if (g_mqtt == NULL) if (g_mqtt == nullptr)
return; return;
g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"lost\"}}"); g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"lost\"}}");
@ -82,16 +116,30 @@ static inline void mqttPublishDStarLost()
// Publish idle mode (no traffic) // Publish idle mode (no traffic)
static inline void mqttPublishIdle() static inline void mqttPublishIdle()
{ {
if (g_mqtt == NULL) if (g_mqtt == nullptr)
return; return;
g_mqtt->publish("json", "{\"MMDVM\":{\"mode\":\"idle\"}}"); g_mqtt->publish("json", "{\"MMDVM\":{\"mode\":\"idle\"}}");
} }
// Publish D-Star RSSI value (DVAP signal strength in dBm)
static inline void mqttPublishRSSI(int rssi)
{
if (g_mqtt == nullptr)
return;
char buf[128];
::snprintf(buf, sizeof(buf),
"{\"RSSI\":{\"mode\":\"D-Star\",\"value\":%d}}",
rssi);
g_mqtt->publish("json", buf);
}
// Publish D-Star BER value // Publish D-Star BER value
static inline void mqttPublishBER(float ber) static inline void mqttPublishBER(float ber)
{ {
if (g_mqtt == NULL) if (g_mqtt == nullptr)
return; return;
char buf[128]; char buf[128];
@ -103,15 +151,19 @@ static inline void mqttPublishBER(float ber)
} }
// Publish D-Star slow data text // Publish D-Star slow data text
static inline void mqttPublishText(const wxString& text) static inline void mqttPublishText(const std::string& text)
{ {
if (g_mqtt == NULL) if (g_mqtt == nullptr)
return; return;
// Slow-data text arrives over the air and is untrusted; escape it so
// that a crafted transmission cannot produce malformed JSON.
const std::string escaped = jsonEscape(text);
char buf[256]; char buf[256];
::snprintf(buf, sizeof(buf), ::snprintf(buf, sizeof(buf),
"{\"Text\":{\"mode\":\"D-Star\",\"value\":\"%s\"}}", "{\"Text\":{\"mode\":\"D-Star\",\"value\":\"%s\"}}",
(const char*)text.mb_str()); escaped.c_str());
g_mqtt->publish("json", buf); g_mqtt->publish("json", buf);
} }

@ -2,7 +2,7 @@ OBJECTS = AMBEFEC.o AnnouncementUnit.o ArduinoController.o BeaconUnit.o Callsign
DStarGMSKDemodulator.o DStarGMSKModulator.o DStarRepeaterConfig.o DStarScrambler.o DummyController.o DVAPController.o \ DStarGMSKDemodulator.o DStarGMSKModulator.o DStarRepeaterConfig.o DStarScrambler.o DummyController.o DVAPController.o \
DVMegaController.o DVRPTRV1Controller.o DVRPTRV2Controller.o DVRPTRV3Controller.o DVTOOLFileReader.o DVTOOLFileWriter.o \ DVMegaController.o DVRPTRV1Controller.o DVRPTRV2Controller.o DVRPTRV3Controller.o DVTOOLFileReader.o DVTOOLFileWriter.o \
ExternalController.o FIRFilter.o GatewayProtocolHandler.o GMSKController.o GMSKModem.o GMSKModemLibUsb.o Golay.o \ ExternalController.o FIRFilter.o GatewayProtocolHandler.o GMSKController.o GMSKModem.o GMSKModemLibUsb.o Golay.o \
GPIOController.o HardwareController.o HeaderData.o IcomController.o K8055Controller.o LogEvent.o Logger.o MMDVMController.o \ GPIOController.o HardwareController.o HeaderData.o IcomController.o K8055Controller.o Logger.o MMDVMController.o \
Modem.o OutputQueue.o RepeaterProtocolHandler.o SerialDataController.o SerialLineController.o SerialPortSelector.o \ Modem.o OutputQueue.o RepeaterProtocolHandler.o SerialDataController.o SerialLineController.o SerialPortSelector.o \
SlowDataDecoder.o SlowDataEncoder.o SoundCardController.o SoundCardReaderWriter.o SplitController.o TCPReaderWriter.o \ SlowDataDecoder.o SlowDataEncoder.o SoundCardController.o SoundCardReaderWriter.o SplitController.o TCPReaderWriter.o \
MQTTConnection.o Timer.o UDPReaderWriter.o UDRCController.o URIUSBController.o Utils.o MQTTConnection.o Timer.o UDPReaderWriter.o UDRCController.o URIUSBController.o Utils.o
@ -16,10 +16,15 @@ Common.a: $(OBJECTS)
-include $(OBJECTS:.o=.d) -include $(OBJECTS:.o=.d)
# BeaconUnit needs DATA_DIR at compile time to locate AMBE voice files in the
# system data directory when they are not found under $HOME.
BeaconUnit.o: BeaconUnit.cpp
$(CXX) $(CFLAGS) -DDATA_DIR='"$(DATADIR)"' -c -o $@ $<
$(CXX) -MM $(CFLAGS) -DDATA_DIR='"$(DATADIR)"' $< > $*.d
%.o: %.cpp %.o: %.cpp
$(CXX) -DwxUSE_GUI=0 $(CFLAGS) -c -o $@ $< $(CXX) $(CFLAGS) -c -o $@ $<
$(CXX) -MM -DwxUSE_GUI=0 $(CFLAGS) $< > $*.d $(CXX) -MM $(CFLAGS) $< > $*.d
clean: clean:
$(RM) Common.a *.o *.d *.bak *~ $(RM) Common.a *.o *.d *.bak *~

@ -19,17 +19,19 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Modem.h" #include "Modem.h"
// Internal scratch buffer used by read() to hold one decoded message payload.
// 200 bytes is large enough for RADIO_HEADER_LENGTH_BYTES (41) and a max DV frame.
const unsigned int BUFFER_LENGTH = 200U; const unsigned int BUFFER_LENGTH = 200U;
CModem::CModem() : CModem::CModem() :
wxThread(wxTHREAD_JOINABLE),
m_rxData(1000U), m_rxData(1000U),
m_mutex(), m_mutex(),
m_tx(false), m_tx(false),
m_stopped(false), m_stopped(false),
m_thread(),
m_readType(DSMTT_NONE), m_readType(DSMTT_NONE),
m_readLength(0U), m_readLength(0U),
m_readBuffer(NULL) m_readBuffer(nullptr)
{ {
m_readBuffer = new unsigned char[BUFFER_LENGTH]; m_readBuffer = new unsigned char[BUFFER_LENGTH];
} }
@ -51,8 +53,9 @@ DSMT_TYPE CModem::read()
if (m_rxData.isEmpty()) if (m_rxData.isEmpty())
return DSMTT_NONE; return DSMTT_NONE;
wxMutexLocker locker(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
// Each ring-buffer message is framed as [type:1][length:1][payload:length].
unsigned char hdr[2U]; unsigned char hdr[2U];
m_rxData.getData(hdr, 2U); m_rxData.getData(hdr, 2U);
@ -66,7 +69,7 @@ DSMT_TYPE CModem::read()
CHeaderData* CModem::readHeader() CHeaderData* CModem::readHeader()
{ {
if (m_readType != DSMTT_HEADER) if (m_readType != DSMTT_HEADER)
return NULL; return nullptr;
return new CHeaderData(m_readBuffer, RADIO_HEADER_LENGTH_BYTES, false); return new CHeaderData(m_readBuffer, RADIO_HEADER_LENGTH_BYTES, false);
} }
@ -89,6 +92,6 @@ void CModem::stop()
{ {
m_stopped = true; m_stopped = true;
Wait(); if (m_thread.joinable())
m_thread.join();
} }

@ -16,50 +16,93 @@
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/ */
/*
* CModem is the abstract base class for all hardware modem drivers
* (DVAP, MMDVM, DVMega, GMSK USB, etc.).
*
* Threading model
* ---------------
* Each concrete driver runs its own I/O thread (started by start(), stopped by
* stop()). That thread reads received frames from the hardware and pushes them
* into the m_rxData ring buffer. The repeater thread then calls read() /
* readHeader() / readData() to drain the buffer. The mutex guards the ring
* buffer against concurrent access between the two threads.
*
* Read protocol
* -------------
* The repeater thread calls read() each tick. If a message is available,
* read() returns the DSMT_TYPE tag and latches the payload into an internal
* scratch buffer. The caller then calls the matching accessor:
* - DSMTT_HEADER -> readHeader() (returns a heap-allocated CHeaderData)
* - DSMTT_DATA -> readData() (copies bytes into the caller's buffer)
* Other types (START, EOT, LOST) carry no payload and need no further call.
*/
#ifndef Modem_H #ifndef Modem_H
#define Modem_H #define Modem_H
#include "HeaderData.h" #include "HeaderData.h"
#include "RingBuffer.h" #include "RingBuffer.h"
#include "StdCompat.h"
#include <wx/wx.h> #include <atomic>
#include <thread>
#include <mutex>
// Message type tags pushed by the driver thread into the ring buffer.
// Each message is prefixed with [type:1][length:1][payload:length] bytes.
enum DSMT_TYPE { enum DSMT_TYPE {
DSMTT_NONE, DSMTT_NONE, // No message waiting
DSMTT_START, DSMTT_START, // Hardware has started (carrier / sync detected)
DSMTT_HEADER, DSMTT_HEADER, // A valid D-Star header has been decoded from the air
DSMTT_DATA, DSMTT_DATA, // One DV frame of voice + slow-data payload
DSMTT_EOT, DSMTT_EOT, // End-of-transmission detected on air
DSMTT_LOST DSMTT_LOST, // Signal lost mid-frame (carrier dropped unexpectedly)
}; };
class CModem : public wxThread { class CModem {
public: public:
CModem(); CModem();
virtual ~CModem(); virtual ~CModem();
// Start the driver I/O thread and open the hardware device.
virtual bool start() = 0; virtual bool start() = 0;
// Transmit a D-Star header (called before the first writeData()).
virtual bool writeHeader(const CHeaderData& header) = 0; virtual bool writeHeader(const CHeaderData& header) = 0;
// Transmit one DV frame; set end=true for the final frame of a transmission.
virtual bool writeData(const unsigned char* data, unsigned int length, bool end) = 0; virtual bool writeData(const unsigned char* data, unsigned int length, bool end) = 0;
// Number of DV frames the TX buffer can still accept.
virtual unsigned int getSpace() = 0; virtual unsigned int getSpace() = 0;
// True when the hardware is ready to accept TX frames.
virtual bool isTXReady() = 0; virtual bool isTXReady() = 0;
// True while the hardware is actively transmitting.
virtual bool isTX(); virtual bool isTX();
// Drain the next message from the ring buffer; returns its type tag.
// Must be called before readHeader() or readData().
virtual DSMT_TYPE read(); virtual DSMT_TYPE read();
// Returns the header decoded by the most recent read() == DSMTT_HEADER call.
// Caller owns the returned object.
virtual CHeaderData* readHeader(); virtual CHeaderData* readHeader();
// Copies DV frame bytes from the most recent read() == DSMTT_DATA call.
virtual unsigned int readData(unsigned char* data, unsigned int length); virtual unsigned int readData(unsigned char* data, unsigned int length);
// Signal the driver thread to exit and wait for it to join.
virtual void stop(); virtual void stop();
protected: protected:
// SPSC ring buffer: driver thread writes, repeater thread reads.
CRingBuffer<unsigned char> m_rxData; CRingBuffer<unsigned char> m_rxData;
wxMutex m_mutex; // Guards m_rxData against concurrent access.
bool m_tx; std::mutex m_mutex;
bool m_stopped; std::atomic<bool> m_tx; // Current TX state of the hardware
std::atomic<bool> m_stopped; // Set by stop() to terminate the thread
std::thread m_thread; // The driver I/O thread
private: private:
// Scratch state populated by read() and consumed by readHeader()/readData().
DSMT_TYPE m_readType; DSMT_TYPE m_readType;
unsigned int m_readLength; unsigned int m_readLength;
unsigned char* m_readBuffer; unsigned char* m_readBuffer;

@ -18,14 +18,16 @@
#include "OutputQueue.h" #include "OutputQueue.h"
#include <cassert>
COutputQueue::COutputQueue(unsigned int space, unsigned int threshold) : COutputQueue::COutputQueue(unsigned int space, unsigned int threshold) :
m_data(space), m_data(space),
m_threshold(threshold), m_threshold(threshold),
m_header(NULL), m_header(nullptr),
m_count(0U) m_count(0U)
{ {
wxASSERT(space > 0U); assert(space > 0U);
wxASSERT(threshold > 0U); assert(threshold > 0U);
} }
COutputQueue::~COutputQueue() COutputQueue::~COutputQueue()
@ -35,7 +37,7 @@ COutputQueue::~COutputQueue()
void COutputQueue::setHeader(CHeaderData* header) void COutputQueue::setHeader(CHeaderData* header)
{ {
wxASSERT(header != NULL); assert(header != nullptr);
delete m_header; delete m_header;
@ -46,14 +48,14 @@ CHeaderData* COutputQueue::getHeader()
{ {
CHeaderData* header = m_header; CHeaderData* header = m_header;
m_header = NULL; m_header = nullptr;
return header; return header;
} }
unsigned int COutputQueue::getData(unsigned char *data, unsigned int length, bool& end) unsigned int COutputQueue::getData(unsigned char *data, unsigned int length, bool& end)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
if (m_data.isEmpty()) { if (m_data.isEmpty()) {
end = false; end = false;
@ -66,8 +68,6 @@ unsigned int COutputQueue::getData(unsigned char *data, unsigned int length, boo
end = hdr[0U] == 1U; end = hdr[0U] == 1U;
if (length < hdr[1U]) { if (length < hdr[1U]) {
wxLogWarning(wxT("Output buffer is too short, %u < %u"), length, hdr[1U]);
unsigned int len = m_data.getData(data, length); unsigned int len = m_data.getData(data, length);
// Purge the excess data // Purge the excess data
@ -84,11 +84,10 @@ unsigned int COutputQueue::getData(unsigned char *data, unsigned int length, boo
unsigned int COutputQueue::addData(const unsigned char *data, unsigned int length, bool end) unsigned int COutputQueue::addData(const unsigned char *data, unsigned int length, bool end)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
bool ret = m_data.hasSpace(length + 2U); bool ret = m_data.hasSpace(length + 2U);
if (!ret) { if (!ret) {
// XXX wxLogWarning(wxT("Not enough space in the output queue"));
return 0U; return 0U;
} }
@ -107,17 +106,17 @@ unsigned int COutputQueue::addData(const unsigned char *data, unsigned int lengt
bool COutputQueue::headerReady() const bool COutputQueue::headerReady() const
{ {
return m_header != NULL && m_count >= m_threshold; return m_header != nullptr && m_count >= m_threshold;
} }
bool COutputQueue::dataReady() const bool COutputQueue::dataReady() const
{ {
return m_header == NULL && m_count >= m_threshold; return m_header == nullptr && m_count >= m_threshold;
} }
bool COutputQueue::isEmpty() bool COutputQueue::isEmpty()
{ {
return m_header == NULL && m_data.isEmpty(); return m_header == nullptr && m_data.isEmpty();
} }
void COutputQueue::reset() void COutputQueue::reset()
@ -125,14 +124,14 @@ void COutputQueue::reset()
m_data.clear(); m_data.clear();
delete m_header; delete m_header;
m_header = NULL; m_header = nullptr;
m_count = 0U; m_count = 0U;
} }
void COutputQueue::setThreshold(unsigned int threshold) void COutputQueue::setThreshold(unsigned int threshold)
{ {
wxASSERT(threshold > 0U); assert(threshold > 0U);
m_threshold = threshold; m_threshold = threshold;
} }

@ -22,20 +22,39 @@
#include "RingBuffer.h" #include "RingBuffer.h"
#include "HeaderData.h" #include "HeaderData.h"
#include <wx/wx.h> /*
* FIFO queue for outbound D-Star frames with type/length/data encoding.
*
* Each frame is stored in the ring buffer as a 2-byte header followed by the
* frame payload:
* byte 0: end flag 1 if this is the last frame of the transmission, else 0
* byte 1: payload length in bytes
* [payload bytes]
*
* A separate m_header pointer holds the pending CHeaderData for the next
* transmission. Playback is gated by a threshold: neither the header nor data
* frames are released to the transmitter until m_count >= m_threshold, which
* ensures there is a small buffer of frames pre-queued before TX begins (to
* absorb jitter in frame delivery from the network). Receiving an end-of-
* stream frame immediately raises the count to the threshold so the remaining
* frames drain promptly.
*/
class COutputQueue { class COutputQueue {
public: public:
COutputQueue(unsigned int space, unsigned int threshold); COutputQueue(unsigned int space, unsigned int threshold);
~COutputQueue(); ~COutputQueue();
// Takes ownership of header; replaces any previously pending header.
void setHeader(CHeaderData* header); void setHeader(CHeaderData* header);
// Returns the pending header and relinquishes ownership (caller must delete).
CHeaderData* getHeader(); CHeaderData* getHeader();
unsigned int getData(unsigned char* data, unsigned int length, bool& end); unsigned int getData(unsigned char* data, unsigned int length, bool& end);
unsigned int addData(const unsigned char* data, unsigned int length, bool end); unsigned int addData(const unsigned char* data, unsigned int length, bool end);
// True when a header is queued and the pre-fill threshold has been reached.
bool headerReady() const; bool headerReady() const;
// True when data frames are queued and no header is pending (mid-stream).
bool dataReady() const; bool dataReady() const;
bool isEmpty(); bool isEmpty();
@ -48,7 +67,7 @@ private:
CRingBuffer<unsigned char> m_data; CRingBuffer<unsigned char> m_data;
unsigned int m_threshold; unsigned int m_threshold;
CHeaderData* m_header; CHeaderData* m_header;
unsigned int m_count; unsigned int m_count; // Frames added since the last header; triggers playback start.
}; };
#endif #endif

@ -21,11 +21,12 @@
#include "DStarDefines.h" #include "DStarDefines.h"
#include "Utils.h" #include "Utils.h"
// Uncomment to hex-dump every outbound DSRP packet to stdout for debugging.
// #define DUMP_TX // #define DUMP_TX
const unsigned int BUFFER_LENGTH = 255U; const unsigned int BUFFER_LENGTH = 255U;
CRepeaterProtocolHandler::CRepeaterProtocolHandler(const wxString& gatewayAddress, unsigned int gatewayPort, const wxString& localAddress, unsigned int localPort, const wxString& name) : CRepeaterProtocolHandler::CRepeaterProtocolHandler(const std::string& gatewayAddress, unsigned int gatewayPort, const std::string& localAddress, unsigned int localPort, const std::string& name) :
m_socket(localAddress, localPort), m_socket(localAddress, localPort),
m_address(), m_address(),
m_port(gatewayPort), m_port(gatewayPort),
@ -34,15 +35,14 @@ m_outId(0U),
m_outSeq(0U), m_outSeq(0U),
m_type(NETWORK_NONE), m_type(NETWORK_NONE),
m_inId(0U), m_inId(0U),
m_buffer(NULL), m_buffer(nullptr),
m_length(0U) m_length(0U)
{ {
m_address = CUDPReaderWriter::lookup(gatewayAddress); m_address = CUDPReaderWriter::lookup(gatewayAddress);
m_buffer = new unsigned char[BUFFER_LENGTH]; m_buffer = new unsigned char[BUFFER_LENGTH];
wxDateTime now = wxDateTime::UNow(); ::srand((unsigned int)::time(nullptr));
::srand(now.GetMillisecond());
} }
CRepeaterProtocolHandler::~CRepeaterProtocolHandler() CRepeaterProtocolHandler::~CRepeaterProtocolHandler()
@ -82,19 +82,19 @@ bool CRepeaterProtocolHandler::writeHeader(const CHeaderData& header)
buffer[10] = header.getFlag3(); buffer[10] = header.getFlag3();
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[11 + i] = header.getRptCall1().GetChar(i); buffer[11 + i] = header.getRptCall1()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[19 + i] = header.getRptCall2().GetChar(i); buffer[19 + i] = header.getRptCall2()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[27 + i] = header.getYourCall().GetChar(i); buffer[27 + i] = header.getYourCall()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[35 + i] = header.getMyCall1().GetChar(i); buffer[35 + i] = header.getMyCall1()[i];
for (unsigned int i = 0U; i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < SHORT_CALLSIGN_LENGTH; i++)
buffer[43 + i] = header.getMyCall2().GetChar(i); buffer[43 + i] = header.getMyCall2()[i];
// Get the checksum for the header // Get the checksum for the header
CCCITTChecksumReverse csum; CCCITTChecksumReverse csum;
@ -104,7 +104,7 @@ bool CRepeaterProtocolHandler::writeHeader(const CHeaderData& header)
m_outSeq = 0U; m_outSeq = 0U;
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Header"), buffer, 49U); CUtils::dump("Sending Header", buffer, 49U);
#endif #endif
for (unsigned int i = 0U; i < 2U; i++) { for (unsigned int i = 0U; i < 2U; i++) {
@ -118,8 +118,8 @@ bool CRepeaterProtocolHandler::writeHeader(const CHeaderData& header)
bool CRepeaterProtocolHandler::writeData(const unsigned char* data, unsigned int length, unsigned int errors, bool end) bool CRepeaterProtocolHandler::writeData(const unsigned char* data, unsigned int length, unsigned int errors, bool end)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
wxASSERT(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES); assert(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
unsigned char buffer[30U]; unsigned char buffer[30U];
@ -150,7 +150,7 @@ bool CRepeaterProtocolHandler::writeData(const unsigned char* data, unsigned int
::memcpy(buffer + 9U, data, length); ::memcpy(buffer + 9U, data, length);
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Data"), buffer, length + 9U); CUtils::dump("Sending Data", buffer, length + 9U);
#endif #endif
return m_socket.write(buffer, length + 9U, m_address, m_port); return m_socket.write(buffer, length + 9U, m_address, m_port);
@ -180,19 +180,19 @@ bool CRepeaterProtocolHandler::writeBusyHeader(const CHeaderData& header)
buffer[10] = header.getFlag3(); buffer[10] = header.getFlag3();
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[11 + i] = header.getRptCall1().GetChar(i); buffer[11 + i] = header.getRptCall1()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[19 + i] = header.getRptCall2().GetChar(i); buffer[19 + i] = header.getRptCall2()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[27 + i] = header.getYourCall().GetChar(i); buffer[27 + i] = header.getYourCall()[i];
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH; i++)
buffer[35 + i] = header.getMyCall1().GetChar(i); buffer[35 + i] = header.getMyCall1()[i];
for (unsigned int i = 0U; i < SHORT_CALLSIGN_LENGTH; i++) for (unsigned int i = 0U; i < SHORT_CALLSIGN_LENGTH; i++)
buffer[43 + i] = header.getMyCall2().GetChar(i); buffer[43 + i] = header.getMyCall2()[i];
// Get the checksum for the header // Get the checksum for the header
CCCITTChecksumReverse csum; CCCITTChecksumReverse csum;
@ -202,7 +202,7 @@ bool CRepeaterProtocolHandler::writeBusyHeader(const CHeaderData& header)
m_outSeq = 0U; m_outSeq = 0U;
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Busy Header"), buffer, 49U); CUtils::dump("Sending Busy Header", buffer, 49U);
#endif #endif
return m_socket.write(buffer, 49U, m_address, m_port); return m_socket.write(buffer, 49U, m_address, m_port);
@ -210,8 +210,8 @@ bool CRepeaterProtocolHandler::writeBusyHeader(const CHeaderData& header)
bool CRepeaterProtocolHandler::writeBusyData(const unsigned char* data, unsigned int length, unsigned int errors, bool end) bool CRepeaterProtocolHandler::writeBusyData(const unsigned char* data, unsigned int length, unsigned int errors, bool end)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
wxASSERT(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES); assert(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
unsigned char buffer[30U]; unsigned char buffer[30U];
@ -242,13 +242,13 @@ bool CRepeaterProtocolHandler::writeBusyData(const unsigned char* data, unsigned
::memcpy(buffer + 9U, data, length); ::memcpy(buffer + 9U, data, length);
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Busy Data"), buffer, length + 9U); CUtils::dump("Sending Busy Data", buffer, length + 9U);
#endif #endif
return m_socket.write(buffer, length + 9U, m_address, m_port); return m_socket.write(buffer, length + 9U, m_address, m_port);
} }
bool CRepeaterProtocolHandler::writePoll(const wxString& text) bool CRepeaterProtocolHandler::writePoll(const std::string& text)
{ {
unsigned char buffer[40U]; unsigned char buffer[40U];
@ -259,15 +259,16 @@ bool CRepeaterProtocolHandler::writePoll(const wxString& text)
buffer[4] = 0x0A; // Poll with text buffer[4] = 0x0A; // Poll with text
unsigned int length = text.Length(); unsigned int length = text.length();
if (length > 34U) length = 34U; // buffer[40] - offset[5] - null[1]
for (unsigned int i = 0U; i < length; i++) for (unsigned int i = 0U; i < length; i++)
buffer[5U + i] = text.GetChar(i); buffer[5U + i] = text[i];
buffer[5U + length] = 0x00; buffer[5U + length] = 0x00;
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Poll"), buffer, 6U + length); CUtils::dump("Sending Poll", buffer, 6U + length);
#endif #endif
return m_socket.write(buffer, 6U + length, m_address, m_port); return m_socket.write(buffer, 6U + length, m_address, m_port);
@ -284,15 +285,16 @@ bool CRepeaterProtocolHandler::writeRegister()
buffer[4] = 0x0B; // Register with name buffer[4] = 0x0B; // Register with name
unsigned int length = m_name.Length(); unsigned int length = m_name.length();
if (length > 34U) length = 34U; // buffer[40] - offset[5] - null[1]
for (unsigned int i = 0U; i < length; i++) for (unsigned int i = 0U; i < length; i++)
buffer[5U + i] = m_name.GetChar(i); buffer[5U + i] = m_name[i];
buffer[5U + length] = 0x00; buffer[5U + length] = 0x00;
#if defined(DUMP_TX) #if defined(DUMP_TX)
CUtils::dump(wxT("Sending Register"), buffer, 6U + length); CUtils::dump("Sending Register", buffer, 6U + length);
#endif #endif
return m_socket.write(buffer, 6U + length, m_address, m_port); return m_socket.write(buffer, 6U + length, m_address, m_port);
@ -320,10 +322,12 @@ bool CRepeaterProtocolHandler::readPackets()
if (length <= 0) if (length <= 0)
return false; return false;
// Check if the data is for us // Validate the source IP and port against the configured gateway address.
// Any packet arriving from a different host is silently rejected to prevent
// a third party from injecting audio or control packets into the repeater.
if (m_address.s_addr != address.s_addr || m_port != port) { if (m_address.s_addr != address.s_addr || m_port != port) {
wxLogMessage(wxT("Packet received from an invalid source, %08X != %08X and/or %u != %u"), m_address.s_addr, address.s_addr, m_port, port); ::fprintf(stderr, "Packet received from an invalid source, %08X != %08X and/or %u != %u\n", m_address.s_addr, address.s_addr, m_port, port);
CUtils::dump(wxT("Data"), m_buffer, length); CUtils::dump("Data", m_buffer, length);
return false; return false;
} }
@ -373,7 +377,7 @@ bool CRepeaterProtocolHandler::readPackets()
// Header data // Header data
else if (m_buffer[4] == 0x20U) { else if (m_buffer[4] == 0x20U) {
wxUint16 id = m_buffer[5] * 256U + m_buffer[6]; uint16_t id = m_buffer[5] * 256U + m_buffer[6];
// Are we listening for headers? // Are we listening for headers?
if (m_inId != 0U) if (m_inId != 0U)
@ -386,7 +390,7 @@ bool CRepeaterProtocolHandler::readPackets()
// User data // User data
else if (m_buffer[4] == 0x21U) { else if (m_buffer[4] == 0x21U) {
wxUint16 id = m_buffer[5] * 256U + m_buffer[6]; uint16_t id = m_buffer[5] * 256U + m_buffer[6];
// Check that the stream id matches the valid header, reject otherwise // Check that the stream id matches the valid header, reject otherwise
if (id != m_inId) if (id != m_inId)
@ -405,7 +409,7 @@ bool CRepeaterProtocolHandler::readPackets()
} }
} }
CUtils::dump(wxT("Unknown packet from the Gateway"), m_buffer, m_length); CUtils::dump("Unknown packet from the Gateway", m_buffer, m_length);
return true; return true;
} }
@ -413,7 +417,7 @@ bool CRepeaterProtocolHandler::readPackets()
CHeaderData* CRepeaterProtocolHandler::readHeader() CHeaderData* CRepeaterProtocolHandler::readHeader()
{ {
if (m_type != NETWORK_HEADER) if (m_type != NETWORK_HEADER)
return NULL; return nullptr;
// If the checksum is 0xFFFF then we accept the header without testing the checksum // If the checksum is 0xFFFF then we accept the header without testing the checksum
if (m_buffer[47U] == 0xFFU && m_buffer[48U] == 0xFFU) if (m_buffer[47U] == 0xFFU && m_buffer[48U] == 0xFFU)
@ -423,9 +427,9 @@ CHeaderData* CRepeaterProtocolHandler::readHeader()
CHeaderData* header = new CHeaderData(m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES, true); CHeaderData* header = new CHeaderData(m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES, true);
if (!header->isValid()) { if (!header->isValid()) {
CUtils::dump(wxT("Header checksum failure from the Gateway"), m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES); CUtils::dump("Header checksum failure from the Gateway", m_buffer + 8U, RADIO_HEADER_LENGTH_BYTES);
delete header; delete header;
return NULL; return nullptr;
} }
return header; return header;
@ -436,6 +440,9 @@ unsigned int CRepeaterProtocolHandler::readData(unsigned char* buffer, unsigned
if (m_type != NETWORK_DATA) if (m_type != NETWORK_DATA)
return 0U; return 0U;
if (m_length < 9U)
return 0U;
unsigned int dataLen = m_length - 9U; unsigned int dataLen = m_length - 9U;
// Is our buffer too small? // Is our buffer too small?
@ -462,70 +469,70 @@ unsigned int CRepeaterProtocolHandler::readData(unsigned char* buffer, unsigned
return dataLen; return dataLen;
} }
void CRepeaterProtocolHandler::readText(wxString& text, LINK_STATUS& status, wxString& reflector) void CRepeaterProtocolHandler::readText(std::string& text, LINK_STATUS& status, std::string& reflector)
{ {
if (m_type != NETWORK_TEXT) { if (m_type != NETWORK_TEXT) {
text = wxT(" "); text = " ";
reflector = wxT(" "); reflector = " ";
status = LS_NONE; status = LS_NONE;
return; return;
} }
text = wxString((char*)(m_buffer + 5U), wxConvLocal, 20U); text = std::string((char*)(m_buffer + 5U), 20U);
status = LINK_STATUS(m_buffer[25U]); status = LINK_STATUS(m_buffer[25U]);
reflector = wxString((char*)(m_buffer + 26U), wxConvLocal, 8U); reflector = std::string((char*)(m_buffer + 26U), 8U);
} }
void CRepeaterProtocolHandler::readTempText(wxString& text) void CRepeaterProtocolHandler::readTempText(std::string& text)
{ {
if (m_type != NETWORK_TEMPTEXT) { if (m_type != NETWORK_TEMPTEXT) {
text = wxT(" "); text = " ";
return; return;
} }
text = wxString((char*)(m_buffer + 5U), wxConvLocal, 20U); text = std::string((char*)(m_buffer + 5U), 20U);
} }
wxString CRepeaterProtocolHandler::readStatus1() std::string CRepeaterProtocolHandler::readStatus1()
{ {
if (m_type != NETWORK_STATUS1) if (m_type != NETWORK_STATUS1)
return wxEmptyString; return std::string();
return wxString((char*)(m_buffer + 6U), wxConvLocal, 20U); return std::string((char*)(m_buffer + 6U), 20U);
} }
wxString CRepeaterProtocolHandler::readStatus2() std::string CRepeaterProtocolHandler::readStatus2()
{ {
if (m_type != NETWORK_STATUS2) if (m_type != NETWORK_STATUS2)
return wxEmptyString; return std::string();
return wxString((char*)(m_buffer + 6U), wxConvLocal, 20U); return std::string((char*)(m_buffer + 6U), 20U);
} }
wxString CRepeaterProtocolHandler::readStatus3() std::string CRepeaterProtocolHandler::readStatus3()
{ {
if (m_type != NETWORK_STATUS3) if (m_type != NETWORK_STATUS3)
return wxEmptyString; return std::string();
return wxString((char*)(m_buffer + 6U), wxConvLocal, 20U); return std::string((char*)(m_buffer + 6U), 20U);
} }
wxString CRepeaterProtocolHandler::readStatus4() std::string CRepeaterProtocolHandler::readStatus4()
{ {
if (m_type != NETWORK_STATUS4) if (m_type != NETWORK_STATUS4)
return wxEmptyString; return std::string();
return wxString((char*)(m_buffer + 6U), wxConvLocal, 20U); return std::string((char*)(m_buffer + 6U), 20U);
} }
wxString CRepeaterProtocolHandler::readStatus5() std::string CRepeaterProtocolHandler::readStatus5()
{ {
if (m_type != NETWORK_STATUS5) if (m_type != NETWORK_STATUS5)
return wxEmptyString; return std::string();
return wxString((char*)(m_buffer + 6U), wxConvLocal, 20U); return std::string((char*)(m_buffer + 6U), 20U);
} }
void CRepeaterProtocolHandler::reset() void CRepeaterProtocolHandler::reset()

@ -22,50 +22,70 @@
#include "UDPReaderWriter.h" #include "UDPReaderWriter.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include "HeaderData.h" #include "HeaderData.h"
#include "StdCompat.h"
#include <wx/wx.h> /*
* Implements the D-Star Repeater Protocol (DSRP) from the repeater side.
*
* DSRP is the UDP-based protocol used between the repeater daemon and
* ircDDBGateway/DStarGateway. All packets begin with the four-byte magic
* "DSRP", followed by a one-byte type code:
*
* 0x0A Poll keepalive with a human-readable text string
* 0x0B Register sent on startup to name this repeater to the gateway
* 0x20 Header D-Star radio header (callsigns + flags)
* 0x21 Data one DV frame (voice + slow-data)
* 0x22 Busy Header header received while repeater is already busy
* 0x23 Busy Data data received while repeater is already busy
*
* The repeater daemon calls write*() to push outbound audio toward the
* gateway, and calls read() each timer tick to drain inbound packets.
*/
class CRepeaterProtocolHandler { class CRepeaterProtocolHandler {
public: public:
CRepeaterProtocolHandler(const wxString& gatewayAddress, unsigned int gatewayPort, const wxString& localAddress, unsigned int localPort, const wxString& name); CRepeaterProtocolHandler(const std::string& gatewayAddress, unsigned int gatewayPort, const std::string& localAddress, unsigned int localPort, const std::string& name);
~CRepeaterProtocolHandler(); ~CRepeaterProtocolHandler();
bool open(); bool open();
// Sends the header packet twice to reduce the impact of UDP packet loss.
bool writeHeader(const CHeaderData& header); bool writeHeader(const CHeaderData& header);
bool writeBusyHeader(const CHeaderData& header); bool writeBusyHeader(const CHeaderData& header);
bool writeData(const unsigned char* data, unsigned int length, unsigned int errors, bool end); bool writeData(const unsigned char* data, unsigned int length, unsigned int errors, bool end);
bool writeBusyData(const unsigned char* data, unsigned int length, unsigned int errors, bool end); bool writeBusyData(const unsigned char* data, unsigned int length, unsigned int errors, bool end);
bool writePoll(const wxString& text); bool writePoll(const std::string& text);
bool writeRegister(); bool writeRegister();
// Drains all pending UDP datagrams; returns the type of the last useful one.
NETWORK_TYPE read(); NETWORK_TYPE read();
void readText(wxString& text, LINK_STATUS& status, wxString& reflector); void readText(std::string& text, LINK_STATUS& status, std::string& reflector);
void readTempText(wxString& text); void readTempText(std::string& text);
wxString readStatus1(); std::string readStatus1();
wxString readStatus2(); std::string readStatus2();
wxString readStatus3(); std::string readStatus3();
wxString readStatus4(); std::string readStatus4();
wxString readStatus5(); std::string readStatus5();
CHeaderData* readHeader(); CHeaderData* readHeader();
unsigned int readData(unsigned char* data, unsigned int length, unsigned char& seqNo); unsigned int readData(unsigned char* data, unsigned int length, unsigned char& seqNo);
// Clears the current inbound session ID so a new stream can be accepted.
void reset(); void reset();
void close(); void close();
private: private:
CUDPReaderWriter m_socket; CUDPReaderWriter m_socket;
in_addr m_address; in_addr m_address; // Resolved gateway address; packets from any other source are dropped.
unsigned int m_port; unsigned int m_port;
wxString m_name; std::string m_name;
wxUint16 m_outId; uint16_t m_outId; // Random session ID assigned at the start of each transmission.
wxUint8 m_outSeq; uint8_t m_outSeq; // Per-frame sequence number (020, resets on each sync frame).
NETWORK_TYPE m_type; NETWORK_TYPE m_type;
wxUint16 m_inId; uint16_t m_inId; // Session ID of the stream currently being received (0 = idle).
unsigned char* m_buffer; unsigned char* m_buffer;
unsigned int m_length; unsigned int m_length;
// Reads one UDP datagram and sets m_type; returns true if more may be waiting.
bool readPackets(); bool readPackets();
}; };

@ -16,20 +16,40 @@
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/ */
/*
* Single-producer / single-consumer (SPSC) lock-free ring buffer.
*
* Used primarily for modem-to-repeater data transfer: the modem driver thread
* writes received frames via addData(), while the repeater thread reads them
* via getData() or peek(). The atomic m_iPtr (write index) and m_oPtr (read
* index) make the empty/full checks safe across exactly two threads without a
* mutex, provided each pointer is only advanced by its respective thread.
*
* Important: this is NOT safe for multiple concurrent producers or consumers.
* CModem wraps accesses with a mutex when both threads need stronger ordering
* guarantees (e.g. when writing the 2-byte framing header atomically with the
* payload).
*
* The buffer wastes one slot so that iPtr == oPtr always means empty rather
* than ambiguously full. Capacity is therefore (length - 1) elements.
*/
#ifndef RingBuffer_H #ifndef RingBuffer_H
#define RingBuffer_H #define RingBuffer_H
#include <wx/wx.h> #include <atomic>
#include <cassert>
#include <cstring>
template<class T> class CRingBuffer { template<class T> class CRingBuffer {
public: public:
CRingBuffer(unsigned int length) : CRingBuffer(unsigned int length) :
m_length(length), m_length(length),
m_buffer(NULL), m_buffer(nullptr),
m_iPtr(0U), m_iPtr(0U),
m_oPtr(0U) m_oPtr(0U)
{ {
wxASSERT(length > 0U); assert(length > 0U);
m_buffer = new T[length]; m_buffer = new T[length];
@ -41,6 +61,8 @@ public:
delete[] m_buffer; delete[] m_buffer;
} }
// Write nSamples elements from buffer. Returns 0 if there is not enough
// free space; the caller must retry or discard.
unsigned int addData(const T* buffer, unsigned int nSamples) unsigned int addData(const T* buffer, unsigned int nSamples)
{ {
if (nSamples > freeSpace()) if (nSamples > freeSpace())
@ -56,6 +78,8 @@ public:
return nSamples; return nSamples;
} }
// Read up to nSamples elements into buffer. Returns the actual count read
// (may be less than requested if the buffer does not have enough data).
unsigned int getData(T* buffer, unsigned int nSamples) unsigned int getData(T* buffer, unsigned int nSamples)
{ {
unsigned int data = dataSize(); unsigned int data = dataSize();
@ -73,6 +97,7 @@ public:
return nSamples; return nSamples;
} }
// Like getData() but does not advance the read pointer (non-destructive).
unsigned int peek(T* buffer, unsigned int nSamples) unsigned int peek(T* buffer, unsigned int nSamples)
{ {
unsigned int data = dataSize(); unsigned int data = dataSize();
@ -112,7 +137,7 @@ public:
bool hasSpace(unsigned int length) const bool hasSpace(unsigned int length) const
{ {
return freeSpace() > length; return freeSpace() >= length;
} }
bool hasData() const bool hasData() const
@ -126,10 +151,10 @@ public:
} }
private: private:
unsigned int m_length; unsigned int m_length; // Allocated slot count (capacity + 1)
T* m_buffer; T* m_buffer;
volatile unsigned int m_iPtr; std::atomic<unsigned int> m_iPtr; // Write index; advanced only by the producer
volatile unsigned int m_oPtr; std::atomic<unsigned int> m_oPtr; // Read index; advanced only by the consumer
unsigned int dataSize() const unsigned int dataSize() const
{ {

@ -19,12 +19,8 @@
#include "SerialDataController.h" #include "SerialDataController.h"
#if !defined(_WIN32)
#include <sys/types.h> #include <sys/types.h>
#if defined(__WINDOWS__)
#include <setupapi.h>
#include <winioctl.h>
#else
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <cerrno> #include <cerrno>
@ -34,253 +30,15 @@
#endif #endif
#if defined(__WINDOWS__) #if !defined(_WIN32)
const unsigned int BUFFER_LENGTH = 1000U;
CSerialDataController::CSerialDataController(const wxString& device, SERIAL_SPEED speed, bool assertRTS) :
m_device(device),
m_speed(speed),
m_assertRTS(assertRTS),
m_handle(INVALID_HANDLE_VALUE),
m_readOverlapped(),
m_writeOverlapped(),
m_readBuffer(NULL),
m_readLength(0U),
m_readPending(false)
{
wxASSERT(!device.IsEmpty());
m_readBuffer = new unsigned char[BUFFER_LENGTH];
}
CSerialDataController::~CSerialDataController()
{
delete[] m_readBuffer;
}
bool CSerialDataController::open()
{
wxASSERT(m_handle == INVALID_HANDLE_VALUE);
DWORD errCode;
wxString baseName = m_device.Mid(4U); // Convert "\\.\COM10" to "COM10"
m_handle = ::CreateFile(m_device.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (m_handle == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Cannot open device - %s, err=%04lx"), m_device.c_str(), ::GetLastError());
return false;
}
DCB dcb;
if (::GetCommState(m_handle, &dcb) == 0) {
wxLogError(wxT("Cannot get the attributes for %s, err=%04lx"), m_device.c_str(), ::GetLastError());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
return false;
}
dcb.BaudRate = DWORD(m_speed);
dcb.ByteSize = 8;
dcb.Parity = NOPARITY;
dcb.fParity = FALSE;
dcb.StopBits = ONESTOPBIT;
dcb.fInX = FALSE;
dcb.fOutX = FALSE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
if (::SetCommState(m_handle, &dcb) == 0) {
wxLogError(wxT("Cannot set the attributes for %s, err=%04lx"), m_device.c_str(), ::GetLastError());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
return false;
}
COMMTIMEOUTS timeouts;
if (!::GetCommTimeouts(m_handle, &timeouts)) {
wxLogError(wxT("Cannot get the timeouts for %s, err=%04lx"), m_device.c_str(), ::GetLastError());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
return false;
}
timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0UL;
timeouts.ReadTotalTimeoutConstant = 0UL;
if (!::SetCommTimeouts(m_handle, &timeouts)) {
wxLogError(wxT("Cannot set the timeouts for %s, err=%04lx"), m_device.c_str(), ::GetLastError());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
return false;
}
if (::EscapeCommFunction(m_handle, CLRDTR) == 0) {
wxLogError(wxT("Cannot clear DTR for %s, err=%04lx"), m_device.c_str(), ::GetLastError());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
return false;
}
if (::EscapeCommFunction(m_handle, m_assertRTS ? SETRTS : CLRRTS) == 0) {
wxLogError(wxT("Cannot set/clear RTS for %s, err=%04lx"), m_device.c_str(), ::GetLastError());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
return false;
}
::ClearCommError(m_handle, &errCode, NULL);
::memset(&m_readOverlapped, 0x00U, sizeof(OVERLAPPED));
::memset(&m_writeOverlapped, 0x00U, sizeof(OVERLAPPED));
m_readOverlapped.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
m_writeOverlapped.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
m_readLength = 0U;
m_readPending = false;
::memset(m_readBuffer, 0x00U, BUFFER_LENGTH);
return true;
}
int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsigned int timeout)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
wxASSERT(buffer != NULL);
unsigned int ptr = 0U;
while (ptr < length) {
int ret = readNonblock(buffer + ptr, length - ptr, timeout);
if (ret < 0) {
return ret;
} else if (ret == 0) {
if (ptr == 0U)
return 0;
} else {
ptr += ret;
}
}
return int(length);
}
int CSerialDataController::readNonblock(unsigned char* buffer, unsigned int length, unsigned int timeout)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
wxASSERT(buffer != NULL);
if (length > BUFFER_LENGTH)
length = BUFFER_LENGTH;
if (m_readPending && length != m_readLength) {
::CancelIo(m_handle);
m_readPending = false;
}
m_readLength = length;
if (length == 0U)
return 0;
if (!m_readPending) {
DWORD bytes = 0UL;
BOOL res = ::ReadFile(m_handle, m_readBuffer, m_readLength, &bytes, &m_readOverlapped);
if (res) {
::memcpy(buffer, m_readBuffer, bytes);
return int(bytes);
}
DWORD error = ::GetLastError();
if (error != ERROR_IO_PENDING) {
wxLogError(wxT("Error from ReadFile: %04lx"), error);
return -1;
}
m_readPending = true;
}
DWORD bytes = 0UL;
DWORD millis = timeout;
BOOL res = ::GetOverlappedResultEx(m_handle, &m_readOverlapped, &bytes, millis, FALSE);
if (!res) {
DWORD error = ::GetLastError();
if (timeout == 0U && error == ERROR_IO_INCOMPLETE) {
return 0;
} else if (timeout > 0U && error == WAIT_TIMEOUT) {
return 0;
} else if (timeout > 0U && error == WAIT_IO_COMPLETION) {
return 0;
} else {
wxLogError(wxT("Error from GetOverlappedResultEx (ReadFile): %04lx"), error);
return -1;
}
}
::memcpy(buffer, m_readBuffer, bytes);
m_readPending = false;
return int(bytes);
}
int CSerialDataController::write(const unsigned char* buffer, unsigned int length)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
wxASSERT(buffer != NULL);
if (length == 0U)
return 0;
unsigned int ptr = 0U;
while (ptr < length) {
DWORD bytes = 0UL;
BOOL res = ::WriteFile(m_handle, buffer + ptr, length - ptr, &bytes, &m_writeOverlapped);
if (!res) {
DWORD error = ::GetLastError();
if (error != ERROR_IO_PENDING) {
wxLogError(wxT("Error from WriteFile: %04lx"), error);
return -1;
}
res = ::GetOverlappedResult(m_handle, &m_writeOverlapped, &bytes, TRUE);
if (!res) {
wxLogError(wxT("Error from GetOverlappedResult (WriteFile): %04lx"), ::GetLastError());
return -1;
}
}
ptr += bytes;
}
return int(length);
}
void CSerialDataController::close()
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
::CloseHandle(m_readOverlapped.hEvent);
::CloseHandle(m_writeOverlapped.hEvent);
}
#else
CSerialDataController::CSerialDataController(const wxString& device, SERIAL_SPEED speed, bool assertRTS) : CSerialDataController::CSerialDataController(const std::string& device, SERIAL_SPEED speed, bool assertRTS) :
m_device(device), m_device(device),
m_speed(speed), m_speed(speed),
m_assertRTS(assertRTS), m_assertRTS(assertRTS),
m_fd(-1) m_fd(-1)
{ {
wxASSERT(!device.IsEmpty()); assert(!device.empty());
} }
CSerialDataController::~CSerialDataController() CSerialDataController::~CSerialDataController()
@ -289,23 +47,23 @@ CSerialDataController::~CSerialDataController()
bool CSerialDataController::open() bool CSerialDataController::open()
{ {
wxASSERT(m_fd == -1); assert(m_fd == -1);
m_fd = ::open(m_device.mb_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0); m_fd = ::open(m_device.c_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0);
if (m_fd < 0) { if (m_fd < 0) {
wxLogError(wxT("Cannot open device - %s"), m_device.c_str()); ::fprintf(stderr, "Cannot open device - %s\n", m_device.c_str());
return false; return false;
} }
if (::isatty(m_fd) == 0) { if (::isatty(m_fd) == 0) {
wxLogError(wxT("%s is not a TTY device"), m_device.c_str()); ::fprintf(stderr, "%s is not a TTY device\n", m_device.c_str());
::close(m_fd); ::close(m_fd);
return false; return false;
} }
termios termios; termios termios;
if (::tcgetattr(m_fd, &termios) < 0) { if (::tcgetattr(m_fd, &termios) < 0) {
wxLogError(wxT("Cannot get the attributes for %s"), m_device.c_str()); ::fprintf(stderr, "Cannot get the attributes for %s\n", m_device.c_str());
::close(m_fd); ::close(m_fd);
return false; return false;
} }
@ -352,13 +110,13 @@ bool CSerialDataController::open()
::cfsetispeed(&termios, B230400); ::cfsetispeed(&termios, B230400);
break; break;
default: default:
wxLogError(wxT("Unsupported serial port speed - %d"), int(m_speed)); ::fprintf(stderr, "Unsupported serial port speed - %d\n", int(m_speed));
::close(m_fd); ::close(m_fd);
return false; return false;
} }
if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) {
wxLogError(wxT("Cannot set the attributes for %s"), m_device.c_str()); ::fprintf(stderr, "Cannot set the attributes for %s\n", m_device.c_str());
::close(m_fd); ::close(m_fd);
return false; return false;
} }
@ -366,7 +124,7 @@ bool CSerialDataController::open()
if (m_assertRTS) { if (m_assertRTS) {
unsigned int y; unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) { if (::ioctl(m_fd, TIOCMGET, &y) < 0) {
wxLogError(wxT("Cannot get the control attributes for %s"), m_device.c_str()); ::fprintf(stderr, "Cannot get the control attributes for %s\n", m_device.c_str());
::close(m_fd); ::close(m_fd);
return false; return false;
} }
@ -374,7 +132,7 @@ bool CSerialDataController::open()
y |= TIOCM_RTS; y |= TIOCM_RTS;
if (::ioctl(m_fd, TIOCMSET, &y) < 0) { if (::ioctl(m_fd, TIOCMSET, &y) < 0) {
wxLogError(wxT("Cannot set the control attributes for %s"), m_device.c_str()); ::fprintf(stderr, "Cannot set the control attributes for %s\n", m_device.c_str());
::close(m_fd); ::close(m_fd);
return false; return false;
} }
@ -385,8 +143,8 @@ bool CSerialDataController::open()
int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsigned int timeout) int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsigned int timeout)
{ {
wxASSERT(buffer != NULL); assert(buffer != nullptr);
wxASSERT(m_fd != -1); assert(m_fd != -1);
if (length == 0U) if (length == 0U)
return 0; return 0;
@ -404,15 +162,15 @@ int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsi
tv.tv_sec = timeout / 1000U; tv.tv_sec = timeout / 1000U;
tv.tv_usec = (timeout % 1000U) * 1000U; tv.tv_usec = (timeout % 1000U) * 1000U;
n = ::select(m_fd + 1, &fds, NULL, NULL, &tv); n = ::select(m_fd + 1, &fds, nullptr, nullptr, &tv);
if (n == 0) if (n == 0)
return 0; return 0;
} else { } else {
n = ::select(m_fd + 1, &fds, NULL, NULL, NULL); n = ::select(m_fd + 1, &fds, nullptr, nullptr, nullptr);
} }
if (n < 0) { if (n < 0) {
wxLogError(wxT("Error from select(), errno=%d"), errno); ::fprintf(stderr, "Error from select(), errno=%d\n", errno);
return -1; return -1;
} }
@ -420,7 +178,7 @@ int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsi
ssize_t len = ::read(m_fd, buffer + offset, length - offset); ssize_t len = ::read(m_fd, buffer + offset, length - offset);
if (len < 0) { if (len < 0) {
if (errno != EAGAIN) { if (errno != EAGAIN) {
wxLogError(wxT("Error from read(), errno=%d"), errno); ::fprintf(stderr, "Error from read(), errno=%d\n", errno);
return -1; return -1;
} }
} }
@ -435,8 +193,8 @@ int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsi
int CSerialDataController::write(const unsigned char* buffer, unsigned int length) int CSerialDataController::write(const unsigned char* buffer, unsigned int length)
{ {
wxASSERT(buffer != NULL); assert(buffer != nullptr);
wxASSERT(m_fd != -1); assert(m_fd != -1);
if (length == 0U) if (length == 0U)
return 0; return 0;
@ -447,7 +205,7 @@ int CSerialDataController::write(const unsigned char* buffer, unsigned int lengt
ssize_t n = ::write(m_fd, buffer + ptr, length - ptr); ssize_t n = ::write(m_fd, buffer + ptr, length - ptr);
if (n < 0) { if (n < 0) {
if (errno != EAGAIN) { if (errno != EAGAIN) {
wxLogError(wxT("Error returned from write(), errno=%d"), errno); ::fprintf(stderr, "Error returned from write(), errno=%d\n", errno);
return -1; return -1;
} }
} }
@ -461,10 +219,184 @@ int CSerialDataController::write(const unsigned char* buffer, unsigned int lengt
void CSerialDataController::close() void CSerialDataController::close()
{ {
wxASSERT(m_fd != -1); assert(m_fd != -1);
::close(m_fd); ::close(m_fd);
m_fd = -1; m_fd = -1;
} }
#endif #else // _WIN32
CSerialDataController::CSerialDataController(const std::string& device, SERIAL_SPEED speed, bool assertRTS) :
m_device(device),
m_speed(speed),
m_assertRTS(assertRTS),
m_handle(INVALID_HANDLE_VALUE)
{
assert(!device.empty());
}
CSerialDataController::~CSerialDataController()
{
}
bool CSerialDataController::open()
{
assert(m_handle == INVALID_HANDLE_VALUE);
// On Windows, ports above COM9 require the \\.\COMn prefix.
std::string path = "\\\\.\\" + m_device;
m_handle = ::CreateFileA(path.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (m_handle == INVALID_HANDLE_VALUE) {
::fprintf(stderr, "Cannot open device - %s\n", m_device.c_str());
return false;
}
DCB dcb;
::memset(&dcb, 0, sizeof(DCB));
dcb.DCBlength = sizeof(DCB);
if (!::GetCommState(m_handle, &dcb)) {
::fprintf(stderr, "Cannot get the attributes for %s\n", m_device.c_str());
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
return false;
}
dcb.BaudRate = static_cast<DWORD>(m_speed);
dcb.ByteSize = 8;
dcb.Parity = NOPARITY;
dcb.StopBits = ONESTOPBIT;
dcb.fBinary = TRUE;
dcb.fParity = FALSE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
if (!::SetCommState(m_handle, &dcb)) {
::fprintf(stderr, "Cannot set the attributes for %s\n", m_device.c_str());
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
return false;
}
// Use infinite inter-character timeout; per-read timeout set in read().
COMMTIMEOUTS timeouts;
::memset(&timeouts, 0, sizeof(COMMTIMEOUTS));
timeouts.ReadIntervalTimeout = 0;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;
if (!::SetCommTimeouts(m_handle, &timeouts)) {
::fprintf(stderr, "Cannot set the timeouts for %s\n", m_device.c_str());
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
return false;
}
if (m_assertRTS) {
if (!::EscapeCommFunction(m_handle, SETRTS)) {
::fprintf(stderr, "Cannot set RTS for %s\n", m_device.c_str());
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
return false;
}
}
return true;
}
int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsigned int timeout)
{
assert(buffer != nullptr);
assert(m_handle != INVALID_HANDLE_VALUE);
if (length == 0U)
return 0;
unsigned int offset = 0U;
// Apply the caller's timeout to the first byte only.
COMMTIMEOUTS timeouts;
::memset(&timeouts, 0, sizeof(COMMTIMEOUTS));
timeouts.ReadIntervalTimeout = 0;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = (offset == 0U) ? static_cast<DWORD>(timeout) : MAXDWORD;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;
::SetCommTimeouts(m_handle, &timeouts);
while (offset < length) {
if (offset == 1U) {
// After the first byte arrives, block indefinitely for the rest.
timeouts.ReadTotalTimeoutConstant = MAXDWORD;
::SetCommTimeouts(m_handle, &timeouts);
}
DWORD bytesRead = 0U;
BOOL ok = ::ReadFile(m_handle,
buffer + offset,
length - offset,
&bytesRead,
nullptr);
if (!ok) {
::fprintf(stderr, "Error from ReadFile(), error=%lu\n", ::GetLastError());
return -1;
}
if (bytesRead == 0U && offset == 0U)
return 0; // timeout on first byte
offset += bytesRead;
}
return static_cast<int>(length);
}
int CSerialDataController::write(const unsigned char* buffer, unsigned int length)
{
assert(buffer != nullptr);
assert(m_handle != INVALID_HANDLE_VALUE);
if (length == 0U)
return 0;
unsigned int ptr = 0U;
while (ptr < length) {
DWORD bytesWritten = 0U;
BOOL ok = ::WriteFile(m_handle,
buffer + ptr,
length - ptr,
&bytesWritten,
nullptr);
if (!ok) {
::fprintf(stderr, "Error returned from WriteFile(), error=%lu\n", ::GetLastError());
return -1;
}
ptr += bytesWritten;
}
return static_cast<int>(length);
}
void CSerialDataController::close()
{
assert(m_handle != INVALID_HANDLE_VALUE);
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
}
#endif // _WIN32

@ -20,9 +20,9 @@
#ifndef SerialDataController_H #ifndef SerialDataController_H
#define SerialDataController_H #define SerialDataController_H
#include <wx/wx.h> #include "StdCompat.h"
#if defined(__WINDOWS__) #if defined(_WIN32)
#include <windows.h> #include <windows.h>
#endif #endif
@ -38,36 +38,38 @@ enum SERIAL_SPEED {
SERIAL_230400 = 230400 SERIAL_230400 = 230400
}; };
/*
* Binary data I/O over a serial port.
*
* Used to communicate with modem hardware (DVAP, DV-RPTR, MMDVM, etc.) that
* is attached via a serial or USB-serial interface. Provides a simple
* read/write interface with optional read timeout (milliseconds).
*
* If assertRTS is true, the RTS line is asserted after open() some modems
* use this as a hardware flow-control or power signal.
*/
class CSerialDataController { class CSerialDataController {
public: public:
CSerialDataController(const wxString& device, SERIAL_SPEED speed, bool assertRTS = false); CSerialDataController(const std::string& device, SERIAL_SPEED speed, bool assertRTS = false);
~CSerialDataController(); ~CSerialDataController();
bool open(); bool open();
// Returns bytes read, 0 on timeout, or -1 on error. timeout=0 is non-blocking.
int read(unsigned char* buffer, unsigned int length, unsigned int timeout = 0U); int read(unsigned char* buffer, unsigned int length, unsigned int timeout = 0U);
int write(const unsigned char* buffer, unsigned int length); int write(const unsigned char* buffer, unsigned int length);
void close(); void close();
private: private:
wxString m_device; std::string m_device;
SERIAL_SPEED m_speed; SERIAL_SPEED m_speed;
bool m_assertRTS; bool m_assertRTS;
#if defined(__WINDOWS__) #if defined(_WIN32)
HANDLE m_handle; HANDLE m_handle;
OVERLAPPED m_readOverlapped;
OVERLAPPED m_writeOverlapped;
unsigned char* m_readBuffer;
unsigned int m_readLength;
bool m_readPending;
#else #else
int m_fd; int m_fd;
#endif #endif
#if defined(__WINDOWS__)
int readNonblock(unsigned char* buffer, unsigned int length, unsigned int timeout);
#endif
}; };
#endif #endif

@ -19,12 +19,8 @@
#include "SerialLineController.h" #include "SerialLineController.h"
#if !defined(_WIN32)
#include <sys/types.h> #include <sys/types.h>
#if defined(__WINDOWS__)
#include <setupapi.h>
#include <winioctl.h>
#else
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <cerrno> #include <cerrno>
@ -34,17 +30,17 @@
#endif #endif
#if defined(__WINDOWS__) #if !defined(_WIN32)
CSerialLineController::CSerialLineController(const wxString& device, unsigned int config) : CSerialLineController::CSerialLineController(const std::string& device, unsigned int config) :
m_device(device), m_device(device),
m_config(config), m_config(config),
m_rts(false), m_rts(false),
m_dtr(false), m_dtr(false),
m_handle(INVALID_HANDLE_VALUE) m_fd(-1)
{ {
wxASSERT(!device.IsEmpty()); assert(!device.empty());
wxASSERT(config == 1U || config == 2U || config == 3U); assert(config == 1U || config == 2U || config == 3U);
} }
CSerialLineController::~CSerialLineController() CSerialLineController::~CSerialLineController()
@ -53,111 +49,111 @@ CSerialLineController::~CSerialLineController()
bool CSerialLineController::open() bool CSerialLineController::open()
{ {
wxASSERT(m_handle == INVALID_HANDLE_VALUE); assert(m_fd == -1);
DWORD errCode; m_fd = ::open(m_device.c_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0);
if (m_fd < 0) {
::fprintf(stderr, "Cannot open device - %s\n", m_device.c_str());
return false;
}
m_handle = ::CreateFile(m_device.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (::isatty(m_fd) == 0) {
if (m_handle == INVALID_HANDLE_VALUE) { ::fprintf(stderr, "%s is not a TTY device\n", m_device.c_str());
wxLogError(wxT("Cannot open device - %s"), m_device.c_str()); ::close(m_fd);
return false; return false;
} }
DCB dcb; termios termios;
if (::GetCommState(m_handle, &dcb) == 0) { if (::tcgetattr(m_fd, &termios) < 0) {
wxLogError(wxT("Cannot get the attributes for %s"), m_device.c_str()); ::fprintf(stderr, "Cannot get the attributes for %s\n", m_device.c_str());
::ClearCommError(m_handle, &errCode, NULL); ::close(m_fd);
::CloseHandle(m_handle);
return false; return false;
} }
dcb.fOutxCtsFlow = FALSE; // FIXME XXX unfinished
dcb.fOutxDsrFlow = FALSE; termios.c_cflag |= (CLOCAL | CREAD);
dcb.fDtrControl = DTR_CONTROL_DISABLE; termios.c_cflag &= ~CRTSCTS;
dcb.fRtsControl = RTS_CONTROL_DISABLE; termios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
termios.c_oflag &= ~OPOST;
termios.c_cc[VMIN] = 0;
termios.c_cc[VTIME] = 10;
if (::SetCommState(m_handle, &dcb) == 0) { if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) {
wxLogError(wxT("Cannot set the attributes for %s"), m_device.c_str()); ::fprintf(stderr, "Cannot set the attributes for %s\n", m_device.c_str());
::ClearCommError(m_handle, &errCode, NULL); ::close(m_fd);
::CloseHandle(m_handle);
return false; return false;
} }
if (::EscapeCommFunction(m_handle, CLRDTR) == 0) { unsigned int y;
wxLogError(wxT("Cannot clear DTR for %s"), m_device.c_str()); if (::ioctl(m_fd, TIOCMGET, &y) < 0) {
::ClearCommError(m_handle, &errCode, NULL); ::fprintf(stderr, "Cannot get the modem status bits for %s\n", m_device.c_str());
::CloseHandle(m_handle); ::close(m_fd);
return false; return false;
} }
if (::EscapeCommFunction(m_handle, CLRRTS) == 0) { y &= ~TIOCM_DTR;
wxLogError(wxT("Cannot clear RTS for %s"), m_device.c_str()); y &= ~TIOCM_RTS;
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle); if (::ioctl(m_fd, TIOCMSET, &y) < 0) {
::fprintf(stderr, "Cannot set the modem status bits for %s\n", m_device.c_str());
::close(m_fd);
return false; return false;
} }
::ClearCommError(m_handle, &errCode, NULL);
return true; return true;
} }
bool CSerialLineController::getCD() const bool CSerialLineController::getCD() const
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
DWORD status; unsigned int y;
DWORD errCode; if (::ioctl(m_fd, TIOCMGET, &y) < 0)
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return false; return false;
}
return (status & MS_RLSD_ON) == MS_RLSD_ON; return (y & TIOCM_CD) == TIOCM_CD;
} }
bool CSerialLineController::getCTS() const bool CSerialLineController::getCTS() const
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
DWORD status; unsigned int y;
DWORD errCode; if (::ioctl(m_fd, TIOCMGET, &y) < 0)
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return false; return false;
}
return (status & MS_CTS_ON) == MS_CTS_ON; return (y & TIOCM_CTS) == TIOCM_CTS;
} }
bool CSerialLineController::getDSR() const bool CSerialLineController::getDSR() const
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
DWORD status; unsigned int y;
DWORD errCode; if (::ioctl(m_fd, TIOCMGET, &y) < 0)
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return false; return false;
}
return (status & MS_DSR_ON) == MS_DSR_ON; return (y & TIOCM_DSR) == TIOCM_DSR;
} }
bool CSerialLineController::setRTS(bool set) bool CSerialLineController::setRTS(bool set)
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
if (set == m_rts) if (set == m_rts)
return true; return true;
DWORD rts = (set) ? SETRTS : CLRRTS; unsigned int y;
DWORD errCode; if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
if (set)
y |= TIOCM_RTS;
else
y &= ~TIOCM_RTS;
if (::EscapeCommFunction(m_handle, rts) == 0) { if (::ioctl(m_fd, TIOCMSET, &y) < 0)
::ClearCommError(m_handle, &errCode, NULL);
return false; return false;
}
m_rts = set; m_rts = set;
@ -166,18 +162,22 @@ bool CSerialLineController::setRTS(bool set)
bool CSerialLineController::setDTR(bool set) bool CSerialLineController::setDTR(bool set)
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
if (set == m_dtr) if (set == m_dtr)
return true; return true;
DWORD dtr = (set) ? SETDTR : CLRDTR; unsigned int y;
DWORD errCode; if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
if (::EscapeCommFunction(m_handle, dtr) == 0) { if (set)
::ClearCommError(m_handle, &errCode, NULL); y |= TIOCM_DTR;
else
y &= ~TIOCM_DTR;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return false; return false;
}
m_dtr = set; m_dtr = set;
@ -186,116 +186,106 @@ bool CSerialLineController::setDTR(bool set)
void CSerialLineController::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5) void CSerialLineController::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5)
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
inp1 = inp2 = inp3 = inp4 = inp5 = false; inp1 = inp2 = inp3 = inp4 = inp5 = false;
DWORD status; unsigned int y;
DWORD errCode; if (::ioctl(m_fd, TIOCMGET, &y) < 0)
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return; return;
}
switch (m_config) { switch (m_config) {
case 1U: case 1U:
inp1 = (status & MS_DSR_ON) == MS_DSR_ON; inp1 = (y & TIOCM_DSR) == TIOCM_DSR;
inp2 = (status & MS_CTS_ON) == MS_CTS_ON; inp2 = (y & TIOCM_CTS) == TIOCM_CTS;
break; break;
case 2U: case 2U:
inp1 = (status & MS_RLSD_ON) == MS_RLSD_ON; inp1 = (y & TIOCM_CD) == TIOCM_CD;
inp2 = (status & MS_RLSD_ON) == MS_RLSD_ON; inp2 = (y & TIOCM_CD) == TIOCM_CD;
break; break;
case 3U: case 3U:
inp1 = (status & MS_RLSD_ON) == MS_RLSD_ON; inp1 = (y & TIOCM_CD) == TIOCM_CD;
break; break;
default: default:
wxLogError(wxT("Unknown serial config - %u"), m_config); ::fprintf(stderr, "Unknown serial config - %u\n", m_config);
break; break;
} }
} }
void CSerialLineController::setDigitalOutputs(bool outp1, bool, bool outp3, bool, bool, bool, bool, bool) void CSerialLineController::setDigitalOutputs(bool outp1, bool, bool outp3, bool, bool, bool, bool, bool)
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
switch (m_config) { if (m_config == 1U) {
case 1U: if (outp1 == m_dtr && outp3 == m_rts)
if (outp1 != m_dtr) { return;
DWORD dtr = (outp1) ? SETDTR : CLRDTR;
DWORD errCode;
if (::EscapeCommFunction(m_handle, dtr) == 0) { unsigned int y;
::ClearCommError(m_handle, &errCode, NULL); if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return; return;
}
m_dtr = outp1; if (outp1)
} y |= TIOCM_DTR;
if (outp3 != m_rts) { else
DWORD rts = (outp3) ? SETRTS : CLRRTS; y &= ~TIOCM_DTR;
DWORD errCode;
if (outp3)
y |= TIOCM_RTS;
else
y &= ~TIOCM_RTS;
if (::EscapeCommFunction(m_handle, rts) == 0) { if (::ioctl(m_fd, TIOCMSET, &y) < 0)
::ClearCommError(m_handle, &errCode, NULL);
return; return;
}
m_dtr = outp1;
m_rts = outp3; m_rts = outp3;
} } else if (m_config == 2U || m_config == 3U) {
break; if (outp1 == m_rts && outp3 == m_dtr)
return;
case 2U:
case 3U:
if (outp1 != m_rts) {
DWORD rts = (outp1) ? SETRTS : CLRRTS;
DWORD errCode;
if (::EscapeCommFunction(m_handle, rts) == 0) { unsigned int y;
::ClearCommError(m_handle, &errCode, NULL); if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return; return;
}
m_rts = outp1; if (outp1)
} y |= TIOCM_RTS;
if (outp3 != m_dtr) { else
DWORD dtr = (outp3) ? SETDTR : CLRDTR; y &= ~TIOCM_RTS;
DWORD errCode;
if (::EscapeCommFunction(m_handle, dtr) == 0) { if (outp3)
::ClearCommError(m_handle, &errCode, NULL); y |= TIOCM_DTR;
else
y &= ~TIOCM_DTR;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return; return;
}
m_rts = outp1;
m_dtr = outp3; m_dtr = outp3;
} } else {
break; ::fprintf(stderr, "Unknown serial config - %u\n", m_config);
default:
wxLogError(wxT("Unknown serial config - %u"), m_config);
break;
} }
} }
void CSerialLineController::close() void CSerialLineController::close()
{ {
wxASSERT(m_handle != INVALID_HANDLE_VALUE); assert(m_fd != -1);
::CloseHandle(m_handle); ::close(m_fd);
m_handle = INVALID_HANDLE_VALUE; m_fd = -1;
} }
#else #else // _WIN32
CSerialLineController::CSerialLineController(const wxString& device, unsigned int config) : CSerialLineController::CSerialLineController(const std::string& device, unsigned int config) :
m_device(device), m_device(device),
m_config(config), m_config(config),
m_rts(false), m_rts(false),
m_dtr(false), m_dtr(false),
m_fd(-1) m_handle(INVALID_HANDLE_VALUE)
{ {
wxASSERT(!device.IsEmpty()); assert(!device.empty());
wxASSERT(config == 1U || config == 2U || config == 3U); assert(config == 1U || config == 2U || config == 3U);
} }
CSerialLineController::~CSerialLineController() CSerialLineController::~CSerialLineController()
@ -304,110 +294,74 @@ CSerialLineController::~CSerialLineController()
bool CSerialLineController::open() bool CSerialLineController::open()
{ {
wxASSERT(m_fd == -1); assert(m_handle == INVALID_HANDLE_VALUE);
m_fd = ::open(m_device.mb_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0); // On Windows, ports above COM9 require the \\.\COMn prefix.
if (m_fd < 0) { std::string path = "\\\\.\\" + m_device;
wxLogError(wxT("Cannot open device - %s"), m_device.c_str());
return false; m_handle = ::CreateFileA(path.c_str(),
} GENERIC_READ | GENERIC_WRITE,
0,
if (::isatty(m_fd) == 0) { nullptr,
wxLogError(wxT("%s is not a TTY device"), m_device.c_str()); OPEN_EXISTING,
::close(m_fd); FILE_ATTRIBUTE_NORMAL,
return false; nullptr);
} if (m_handle == INVALID_HANDLE_VALUE) {
::fprintf(stderr, "Cannot open device - %s\n", m_device.c_str());
termios termios;
if (::tcgetattr(m_fd, &termios) < 0) {
wxLogError(wxT("Cannot get the attributes for %s"), m_device.c_str());
::close(m_fd);
return false;
}
// FIXME XXX unfinished
termios.c_cflag |= (CLOCAL | CREAD);
termios.c_cflag &= ~CRTSCTS;
termios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
termios.c_oflag &= ~OPOST;
termios.c_cc[VMIN] = 0;
termios.c_cc[VTIME] = 10;
if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) {
wxLogError(wxT("Cannot set the attributes for %s"), m_device.c_str());
::close(m_fd);
return false;
}
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) {
wxLogError(wxT("Cannot get the modem status bits for %s"), m_device.c_str());
::close(m_fd);
return false; return false;
} }
y &= ~TIOCM_DTR; // Deassert both DTR and RTS on open, matching the POSIX behaviour.
y &= ~TIOCM_RTS; ::EscapeCommFunction(m_handle, CLRDTR);
::EscapeCommFunction(m_handle, CLRRTS);
if (::ioctl(m_fd, TIOCMSET, &y) < 0) { m_rts = false;
wxLogError(wxT("Cannot set the modem status bits for %s"), m_device.c_str()); m_dtr = false;
::close(m_fd);
return false;
}
return true; return true;
} }
bool CSerialLineController::getCD() const bool CSerialLineController::getCD() const
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
unsigned int y; DWORD status = 0U;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) if (!::GetCommModemStatus(m_handle, &status))
return false; return false;
return (y & TIOCM_CD) == TIOCM_CD; return (status & MS_RLSD_ON) != 0U;
} }
bool CSerialLineController::getCTS() const bool CSerialLineController::getCTS() const
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
unsigned int y; DWORD status = 0U;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) if (!::GetCommModemStatus(m_handle, &status))
return false; return false;
return (y & TIOCM_CTS) == TIOCM_CTS; return (status & MS_CTS_ON) != 0U;
} }
bool CSerialLineController::getDSR() const bool CSerialLineController::getDSR() const
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
unsigned int y; DWORD status = 0U;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) if (!::GetCommModemStatus(m_handle, &status))
return false; return false;
return (y & TIOCM_DSR) == TIOCM_DSR; return (status & MS_DSR_ON) != 0U;
} }
bool CSerialLineController::setRTS(bool set) bool CSerialLineController::setRTS(bool set)
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
if (set == m_rts) if (set == m_rts)
return true; return true;
unsigned int y; if (!::EscapeCommFunction(m_handle, set ? SETRTS : CLRRTS))
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
if (set)
y |= TIOCM_RTS;
else
y &= ~TIOCM_RTS;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return false; return false;
m_rts = set; m_rts = set;
@ -417,21 +371,12 @@ bool CSerialLineController::setRTS(bool set)
bool CSerialLineController::setDTR(bool set) bool CSerialLineController::setDTR(bool set)
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
if (set == m_dtr) if (set == m_dtr)
return true; return true;
unsigned int y; if (!::EscapeCommFunction(m_handle, set ? SETDTR : CLRDTR))
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
if (set)
y |= TIOCM_DTR;
else
y &= ~TIOCM_DTR;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return false; return false;
m_dtr = set; m_dtr = set;
@ -441,56 +386,42 @@ bool CSerialLineController::setDTR(bool set)
void CSerialLineController::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5) void CSerialLineController::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool& inp4, bool& inp5)
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
inp1 = inp2 = inp3 = inp4 = inp5 = false; inp1 = inp2 = inp3 = inp4 = inp5 = false;
unsigned int y; DWORD status = 0U;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) if (!::GetCommModemStatus(m_handle, &status))
return; return;
switch (m_config) { switch (m_config) {
case 1U: case 1U:
inp1 = (y & TIOCM_DSR) == TIOCM_DSR; inp1 = (status & MS_DSR_ON) != 0U;
inp2 = (y & TIOCM_CTS) == TIOCM_CTS; inp2 = (status & MS_CTS_ON) != 0U;
break; break;
case 2U: case 2U:
inp1 = (y & TIOCM_CD) == TIOCM_CD; inp1 = (status & MS_RLSD_ON) != 0U;
inp2 = (y & TIOCM_CD) == TIOCM_CD; inp2 = (status & MS_RLSD_ON) != 0U;
break; break;
case 3U: case 3U:
inp1 = (y & TIOCM_CD) == TIOCM_CD; inp1 = (status & MS_RLSD_ON) != 0U;
break; break;
default: default:
wxLogError(wxT("Unknown serial config - %u"), m_config); ::fprintf(stderr, "Unknown serial config - %u\n", m_config);
break; break;
} }
} }
void CSerialLineController::setDigitalOutputs(bool outp1, bool, bool outp3, bool, bool, bool, bool, bool) void CSerialLineController::setDigitalOutputs(bool outp1, bool, bool outp3, bool, bool, bool, bool, bool)
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
if (m_config == 1U) { if (m_config == 1U) {
if (outp1 == m_dtr && outp3 == m_rts) if (outp1 == m_dtr && outp3 == m_rts)
return; return;
unsigned int y; ::EscapeCommFunction(m_handle, outp1 ? SETDTR : CLRDTR);
if (::ioctl(m_fd, TIOCMGET, &y) < 0) ::EscapeCommFunction(m_handle, outp3 ? SETRTS : CLRRTS);
return;
if (outp1)
y |= TIOCM_DTR;
else
y &= ~TIOCM_DTR;
if (outp3)
y |= TIOCM_RTS;
else
y &= ~TIOCM_RTS;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return;
m_dtr = outp1; m_dtr = outp1;
m_rts = outp3; m_rts = outp3;
@ -498,36 +429,22 @@ void CSerialLineController::setDigitalOutputs(bool outp1, bool, bool outp3, bool
if (outp1 == m_rts && outp3 == m_dtr) if (outp1 == m_rts && outp3 == m_dtr)
return; return;
unsigned int y; ::EscapeCommFunction(m_handle, outp1 ? SETRTS : CLRRTS);
if (::ioctl(m_fd, TIOCMGET, &y) < 0) ::EscapeCommFunction(m_handle, outp3 ? SETDTR : CLRDTR);
return;
if (outp1)
y |= TIOCM_RTS;
else
y &= ~TIOCM_RTS;
if (outp3)
y |= TIOCM_DTR;
else
y &= ~TIOCM_DTR;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return;
m_rts = outp1; m_rts = outp1;
m_dtr = outp3; m_dtr = outp3;
} else { } else {
wxLogError(wxT("Unknown serial config - %u"), m_config); ::fprintf(stderr, "Unknown serial config - %u\n", m_config);
} }
} }
void CSerialLineController::close() void CSerialLineController::close()
{ {
wxASSERT(m_fd != -1); assert(m_handle != INVALID_HANDLE_VALUE);
::close(m_fd); ::CloseHandle(m_handle);
m_fd = -1; m_handle = INVALID_HANDLE_VALUE;
} }
#endif #endif // _WIN32

@ -21,27 +21,39 @@
#define SerialLineController_H #define SerialLineController_H
#include "HardwareController.h" #include "HardwareController.h"
#include "StdCompat.h"
#include <wx/wx.h> #if defined(_WIN32)
#if defined(__WINDOWS__)
#include <windows.h> #include <windows.h>
#endif #endif
// Serial modem control lines used for PTT / COR signalling.
enum SERIALPIN { enum SERIALPIN {
SERIAL_CD, SERIAL_CD, // Carrier Detect — input, indicates received signal.
SERIAL_CTS, SERIAL_CTS, // Clear To Send — input.
SERIAL_DSR, SERIAL_DSR, // Data Set Ready — input.
SERIAL_DTR, SERIAL_DTR, // Data Terminal Ready — output, used for PTT on some hardware.
SERIAL_RTS, SERIAL_RTS, // Request To Send — output, most common PTT line.
SERIAL_ECHOLINK SERIAL_ECHOLINK // EchoLink-compatible pin assignment variant.
}; };
const unsigned int MAX_DEVICE_NAME = 255U; const unsigned int MAX_DEVICE_NAME = 255U;
/*
* Serial port line-level controller for PTT and COR/COS I/O.
*
* Implements IHardwareController using the modem-control lines of an RS-232
* (or USB-serial) port. RTS and/or DTR are used as PTT outputs; CD, CTS,
* and DSR are used as COR/squelch inputs. The config parameter selects which
* pin assignment scheme is used (see the SERIALPIN enum and the controller
* config documentation).
*
* This controller does not transfer any data it only toggles the control
* lines. For serial data transfer use CSerialDataController.
*/
class CSerialLineController : public IHardwareController { class CSerialLineController : public IHardwareController {
public: public:
CSerialLineController(const wxString& device, unsigned int config = 1U); CSerialLineController(const std::string& device, unsigned int config = 1U);
virtual ~CSerialLineController(); virtual ~CSerialLineController();
virtual bool open(); virtual bool open();
@ -59,11 +71,11 @@ public:
virtual void close(); virtual void close();
private: private:
wxString m_device; std::string m_device;
unsigned int m_config; unsigned int m_config; // Pin assignment scheme index.
bool m_rts; bool m_rts;
bool m_dtr; bool m_dtr;
#if defined(__WINDOWS__) #if defined(_WIN32)
HANDLE m_handle; HANDLE m_handle;
#else #else
int m_fd; int m_fd;

@ -18,7 +18,12 @@
#include "SerialPortSelector.h" #include "SerialPortSelector.h"
#if !defined(_WIN32)
#include <sys/types.h> #include <sys/types.h>
#include <dirent.h>
#include <cstring>
#include <algorithm>
#endif
#if defined(__APPLE__) && defined(__MACH__) #if defined(__APPLE__) && defined(__MACH__)
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
@ -29,19 +34,12 @@
#include <sys/param.h> #include <sys/param.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <termios.h> #include <termios.h>
#elif defined(__WINDOWS__)
#include <setupapi.h>
#include <winioctl.h>
#else
#include <wx/dir.h>
#endif #endif
wxArrayString CSerialPortSelector::getDevices() std::vector<std::string> CSerialPortSelector::getDevices()
{ {
wxArrayString devices; std::vector<std::string> devices;
devices.Alloc(10);
#if defined(__APPLE__) && defined(__MACH__) #if defined(__APPLE__) && defined(__MACH__)
mach_port_t masterPort; mach_port_t masterPort;
@ -50,8 +48,8 @@ wxArrayString CSerialPortSelector::getDevices()
return devices; return devices;
CFMutableDictionaryRef match = ::IOServiceMatching(kIOSerialBSDServiceValue); CFMutableDictionaryRef match = ::IOServiceMatching(kIOSerialBSDServiceValue);
if (match == NULL) if (match == nullptr)
wxLogWarning(wxT("IOServiceMatching() returned NULL")); ::fprintf(stderr, "IOServiceMatching() returned nullptr\n");
else else
::CFDictionarySetValue(match, CFSTR(kIOSerialBSDTypeKey), CFSTR(kIOSerialBSDRS232Type)); ::CFDictionarySetValue(match, CFSTR(kIOSerialBSDTypeKey), CFSTR(kIOSerialBSDRS232Type));
@ -63,80 +61,60 @@ wxArrayString CSerialPortSelector::getDevices()
io_object_t modem; io_object_t modem;
while ((modem = ::IOIteratorNext(services))) { while ((modem = ::IOIteratorNext(services))) {
CFTypeRef filePath = ::IORegistryEntryCreateCFProperty(modem, CFSTR(kIOCalloutDeviceKey), kCFAllocatorDefault, 0); CFTypeRef filePath = ::IORegistryEntryCreateCFProperty(modem, CFSTR(kIOCalloutDeviceKey), kCFAllocatorDefault, 0);
if (filePath != NULL) { if (filePath != nullptr) {
char port[MAXPATHLEN]; char port[MAXPATHLEN];
Boolean result = ::CFStringGetCString((const __CFString*)filePath, port, MAXPATHLEN, kCFStringEncodingASCII); Boolean result = ::CFStringGetCString((const __CFString*)filePath, port, MAXPATHLEN, kCFStringEncodingASCII);
::CFRelease(filePath); ::CFRelease(filePath);
if (result) if (result)
devices.Add(port); devices.push_back(port);
::IOObjectRelease(modem); ::IOObjectRelease(modem);
} }
} }
::IOObjectRelease(services); ::IOObjectRelease(services);
#elif defined(__WINDOWS__) #elif defined(_WIN32)
devices.Add(wxT("\\\\.\\COM1")); // Windows: probe COM1 through COM32 with CreateFile.
devices.Add(wxT("\\\\.\\COM2")); for (int i = 1; i <= 32; ++i) {
devices.Add(wxT("\\\\.\\COM3")); char portName[16];
devices.Add(wxT("\\\\.\\COM4")); ::sprintf_s(portName, sizeof(portName), "\\\\.\\COM%d", i);
devices.Add(wxT("\\\\.\\COM5"));
devices.Add(wxT("\\\\.\\COM6")); HANDLE h = ::CreateFileA(portName,
devices.Add(wxT("\\\\.\\COM7")); GENERIC_READ | GENERIC_WRITE,
devices.Add(wxT("\\\\.\\COM8")); 0,
devices.Add(wxT("\\\\.\\COM9")); nullptr,
devices.Add(wxT("\\\\.\\COM10")); OPEN_EXISTING,
devices.Add(wxT("\\\\.\\COM11")); FILE_ATTRIBUTE_NORMAL,
devices.Add(wxT("\\\\.\\COM12")); nullptr);
devices.Add(wxT("\\\\.\\COM13")); if (h != INVALID_HANDLE_VALUE) {
devices.Add(wxT("\\\\.\\COM14")); ::CloseHandle(h);
devices.Add(wxT("\\\\.\\COM15")); char name[8];
devices.Add(wxT("\\\\.\\COM16")); ::sprintf_s(name, sizeof(name), "COM%d", i);
devices.Add(wxT("\\\\.\\COM17")); devices.push_back(name);
devices.Add(wxT("\\\\.\\COM18"));
devices.Add(wxT("\\\\.\\COM19"));
#else
wxDir devDir;
bool ret = devDir.Open(wxT("/dev"));
if (ret) {
wxString fileName;
ret = devDir.GetFirst(&fileName, wxT("ttyACM*"), wxDIR_FILES);
while (ret) {
fileName.Prepend(wxT("/dev/"));
devices.Add(fileName);
ret = devDir.GetNext(&fileName);
} }
ret = devDir.GetFirst(&fileName, wxT("ttyAMA*"), wxDIR_FILES);
while (ret) {
fileName.Prepend(wxT("/dev/"));
devices.Add(fileName);
ret = devDir.GetNext(&fileName);
} }
#else
ret = devDir.GetFirst(&fileName, wxT("ttyS*"), wxDIR_FILES); // Linux: enumerate /dev for common serial port patterns
static const char* const patterns[] = {
while (ret) { "ttyACM", "ttyAMA", "ttyS", "ttyUSB", nullptr
fileName.Prepend(wxT("/dev/")); };
devices.Add(fileName);
DIR* devDir = ::opendir("/dev");
ret = devDir.GetNext(&fileName); if (devDir != nullptr) {
struct dirent* entry;
while ((entry = ::readdir(devDir)) != nullptr) {
if (entry->d_type != DT_CHR && entry->d_type != DT_LNK && entry->d_type != DT_UNKNOWN)
continue;
for (int i = 0; patterns[i] != nullptr; ++i) {
if (::strncmp(entry->d_name, patterns[i], ::strlen(patterns[i])) == 0) {
devices.push_back(std::string("/dev/") + entry->d_name);
break;
}
} }
ret = devDir.GetFirst(&fileName, wxT("ttyUSB*"), wxDIR_FILES);
while (ret) {
fileName.Prepend(wxT("/dev/"));
devices.Add(fileName);
ret = devDir.GetNext(&fileName);
} }
::closedir(devDir);
std::sort(devices.begin(), devices.end());
} }
#endif #endif

@ -19,11 +19,11 @@
#ifndef SerialPortSelector_H #ifndef SerialPortSelector_H
#define SerialPortSelector_H #define SerialPortSelector_H
#include <wx/wx.h> #include "StdCompat.h"
class CSerialPortSelector { class CSerialPortSelector {
public: public:
static wxArrayString getDevices(); static std::vector<std::string> getDevices();
private: private:
}; };

@ -15,16 +15,19 @@
#include "SlowDataDecoder.h" #include "SlowDataDecoder.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include <cassert>
#include <cstring>
const unsigned int SLOW_DATA_BLOCK_SIZE = 6U; const unsigned int SLOW_DATA_BLOCK_SIZE = 6U;
const unsigned int SLOW_DATA_FULL_BLOCK_SIZE = (SLOW_DATA_BLOCK_SIZE - 1U) * 10U; const unsigned int SLOW_DATA_FULL_BLOCK_SIZE = (SLOW_DATA_BLOCK_SIZE - 1U) * 10U;
CSlowDataDecoder::CSlowDataDecoder() : CSlowDataDecoder::CSlowDataDecoder() :
m_buffer(NULL), m_buffer(nullptr),
m_state(SDD_FIRST), m_state(SDD_FIRST),
m_headerData(NULL), m_headerData(nullptr),
m_headerPtr(0U), m_headerPtr(0U),
m_header(NULL) m_header(nullptr)
{ {
m_buffer = new unsigned char[SLOW_DATA_BLOCK_SIZE]; m_buffer = new unsigned char[SLOW_DATA_BLOCK_SIZE];
m_headerData = new unsigned char[SLOW_DATA_FULL_BLOCK_SIZE]; m_headerData = new unsigned char[SLOW_DATA_FULL_BLOCK_SIZE];
@ -40,7 +43,7 @@ CSlowDataDecoder::~CSlowDataDecoder()
void CSlowDataDecoder::addData(const unsigned char* data) void CSlowDataDecoder::addData(const unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
switch (m_state) { switch (m_state) {
case SDD_FIRST: case SDD_FIRST:
@ -62,8 +65,8 @@ void CSlowDataDecoder::addData(const unsigned char* data)
CHeaderData* CSlowDataDecoder::getHeaderData() CHeaderData* CSlowDataDecoder::getHeaderData()
{ {
if (m_header == NULL) if (m_header == nullptr)
return NULL; return nullptr;
CHeaderData* temp = new CHeaderData(*m_header); CHeaderData* temp = new CHeaderData(*m_header);
@ -88,13 +91,13 @@ void CSlowDataDecoder::reset()
::memset(m_headerData, 0x00, SLOW_DATA_FULL_BLOCK_SIZE * sizeof(unsigned char)); ::memset(m_headerData, 0x00, SLOW_DATA_FULL_BLOCK_SIZE * sizeof(unsigned char));
delete m_header; delete m_header;
m_header = NULL; m_header = nullptr;
} }
void CSlowDataDecoder::processHeader() void CSlowDataDecoder::processHeader()
{ {
// Do we have a complete and valid header already? // Do we have a complete and valid header already?
if (m_header != NULL) if (m_header != nullptr)
return; return;
for (unsigned int i = 1U; i <= (SLOW_DATA_BLOCK_SIZE - 1U); i++, m_headerPtr++) { for (unsigned int i = 1U; i <= (SLOW_DATA_BLOCK_SIZE - 1U); i++, m_headerPtr++) {

@ -16,11 +16,28 @@
#include "HeaderData.h" #include "HeaderData.h"
#include <wx/wx.h> /*
* Decodes D-Star slow data embedded in the 3-byte data field of each DV frame.
*
* D-Star slow data is multiplexed with voice at 1200 bps by stealing 3 bytes
* from every 12-byte DV frame. These 3 bytes are XOR-scrambled before
* transmission using a repeating 3-byte key (SCRAMBLER_BYTE1/2/3 from
* DStarDefines.h) to keep the bit pattern from looking like sync patterns.
*
* Two consecutive 3-byte slow-data fields are collected into a 6-byte block.
* The first byte of each block is a type/length tag; the remaining 5 bytes
* carry payload. Blocks can carry a re-transmitted copy of the radio header
* (type SLOW_DATA_TYPE_HEADER) or free-text (type SLOW_DATA_TYPE_TEXT).
*
* addData() is called once per DV frame with the 3-byte slow-data field (after
* stripping the AMBE bytes). It alternates between SDD_FIRST and SDD_SECOND
* states to accumulate complete 6-byte blocks. When a valid header checksum
* is found, the result is available via getHeaderData().
*/
enum SDD_STATE { enum SDD_STATE {
SDD_FIRST, SDD_FIRST, // Waiting for the first 3-byte half of a slow-data block.
SDD_SECOND SDD_SECOND // Waiting for the second half to complete the block.
}; };
class CSlowDataDecoder { class CSlowDataDecoder {
@ -28,19 +45,23 @@ public:
CSlowDataDecoder(); CSlowDataDecoder();
~CSlowDataDecoder(); ~CSlowDataDecoder();
// Feed one 3-byte slow-data field (raw, still scrambled).
void addData(const unsigned char* data); void addData(const unsigned char* data);
// Returns a newly allocated CHeaderData if a valid header was decoded,
// or nullptr if none has been found yet. Caller takes ownership.
CHeaderData* getHeaderData(); CHeaderData* getHeaderData();
// Called on each data-sync frame to re-align the two-phase accumulator.
void sync(); void sync();
void reset(); void reset();
private: private:
unsigned char* m_buffer; unsigned char* m_buffer; // Accumulates the current 6-byte block.
SDD_STATE m_state; SDD_STATE m_state;
unsigned char* m_headerData; unsigned char* m_headerData; // Circular buffer of decoded payload bytes.
unsigned int m_headerPtr; unsigned int m_headerPtr;
CHeaderData* m_header; CHeaderData* m_header; // Decoded header, held until getHeaderData() is called.
void processHeader(); void processHeader();
bool processHeader(const unsigned char* bytes); bool processHeader(const unsigned char* bytes);

@ -15,13 +15,16 @@
#include "SlowDataEncoder.h" #include "SlowDataEncoder.h"
#include "DStarDefines.h" #include "DStarDefines.h"
#include <cassert>
#include <cstring>
const unsigned int SLOW_DATA_BLOCK_SIZE = 6U; const unsigned int SLOW_DATA_BLOCK_SIZE = 6U;
const unsigned int SLOW_DATA_FULL_BLOCK_SIZE = SLOW_DATA_BLOCK_SIZE * 10U; const unsigned int SLOW_DATA_FULL_BLOCK_SIZE = SLOW_DATA_BLOCK_SIZE * 10U;
CSlowDataEncoder::CSlowDataEncoder() : CSlowDataEncoder::CSlowDataEncoder() :
m_headerData(NULL), m_headerData(nullptr),
m_textData(NULL), m_textData(nullptr),
m_headerPtr(0U), m_headerPtr(0U),
m_textPtr(0U) m_textPtr(0U)
{ {
@ -40,7 +43,7 @@ CSlowDataEncoder::~CSlowDataEncoder()
void CSlowDataEncoder::getHeaderData(unsigned char* data) void CSlowDataEncoder::getHeaderData(unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
data[0U] = m_headerData[m_headerPtr++] ^ SCRAMBLER_BYTE1; data[0U] = m_headerData[m_headerPtr++] ^ SCRAMBLER_BYTE1;
data[1U] = m_headerData[m_headerPtr++] ^ SCRAMBLER_BYTE2; data[1U] = m_headerData[m_headerPtr++] ^ SCRAMBLER_BYTE2;
@ -52,7 +55,7 @@ void CSlowDataEncoder::getHeaderData(unsigned char* data)
void CSlowDataEncoder::getTextData(unsigned char* data) void CSlowDataEncoder::getTextData(unsigned char* data)
{ {
wxASSERT(data != NULL); assert(data != nullptr);
data[0U] = m_textData[m_textPtr++] ^ SCRAMBLER_BYTE1; data[0U] = m_textData[m_textPtr++] ^ SCRAMBLER_BYTE1;
data[1U] = m_textData[m_textPtr++] ^ SCRAMBLER_BYTE2; data[1U] = m_textData[m_textPtr++] ^ SCRAMBLER_BYTE2;
@ -85,56 +88,56 @@ void CSlowDataEncoder::setHeaderData(const CHeaderData& header)
m_headerData[1U] = header.getFlag1(); m_headerData[1U] = header.getFlag1();
m_headerData[2U] = header.getFlag2(); m_headerData[2U] = header.getFlag2();
m_headerData[3U] = header.getFlag3(); m_headerData[3U] = header.getFlag3();
m_headerData[4U] = header.getRptCall2().GetChar(0); m_headerData[4U] = header.getRptCall2()[0];
m_headerData[5U] = header.getRptCall2().GetChar(1); m_headerData[5U] = header.getRptCall2()[1];
m_headerData[6U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[6U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[7U] = header.getRptCall2().GetChar(2); m_headerData[7U] = header.getRptCall2()[2];
m_headerData[8U] = header.getRptCall2().GetChar(3); m_headerData[8U] = header.getRptCall2()[3];
m_headerData[9U] = header.getRptCall2().GetChar(4); m_headerData[9U] = header.getRptCall2()[4];
m_headerData[10U] = header.getRptCall2().GetChar(5); m_headerData[10U] = header.getRptCall2()[5];
m_headerData[11U] = header.getRptCall2().GetChar(6); m_headerData[11U] = header.getRptCall2()[6];
m_headerData[12U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[12U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[13U] = header.getRptCall2().GetChar(7); m_headerData[13U] = header.getRptCall2()[7];
m_headerData[14U] = header.getRptCall1().GetChar(0); m_headerData[14U] = header.getRptCall1()[0];
m_headerData[15U] = header.getRptCall1().GetChar(1); m_headerData[15U] = header.getRptCall1()[1];
m_headerData[16U] = header.getRptCall1().GetChar(2); m_headerData[16U] = header.getRptCall1()[2];
m_headerData[17U] = header.getRptCall1().GetChar(3); m_headerData[17U] = header.getRptCall1()[3];
m_headerData[18U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[18U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[19U] = header.getRptCall1().GetChar(4); m_headerData[19U] = header.getRptCall1()[4];
m_headerData[20U] = header.getRptCall1().GetChar(5); m_headerData[20U] = header.getRptCall1()[5];
m_headerData[21U] = header.getRptCall1().GetChar(6); m_headerData[21U] = header.getRptCall1()[6];
m_headerData[22U] = header.getRptCall1().GetChar(7); m_headerData[22U] = header.getRptCall1()[7];
m_headerData[23U] = header.getYourCall().GetChar(0); m_headerData[23U] = header.getYourCall()[0];
m_headerData[24U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[24U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[25U] = header.getYourCall().GetChar(1); m_headerData[25U] = header.getYourCall()[1];
m_headerData[26U] = header.getYourCall().GetChar(2); m_headerData[26U] = header.getYourCall()[2];
m_headerData[27U] = header.getYourCall().GetChar(3); m_headerData[27U] = header.getYourCall()[3];
m_headerData[28U] = header.getYourCall().GetChar(4); m_headerData[28U] = header.getYourCall()[4];
m_headerData[29U] = header.getYourCall().GetChar(5); m_headerData[29U] = header.getYourCall()[5];
m_headerData[30U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[30U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[31U] = header.getYourCall().GetChar(6); m_headerData[31U] = header.getYourCall()[6];
m_headerData[32U] = header.getYourCall().GetChar(7); m_headerData[32U] = header.getYourCall()[7];
m_headerData[33U] = header.getMyCall1().GetChar(0); m_headerData[33U] = header.getMyCall1()[0];
m_headerData[34U] = header.getMyCall1().GetChar(1); m_headerData[34U] = header.getMyCall1()[1];
m_headerData[35U] = header.getMyCall1().GetChar(2); m_headerData[35U] = header.getMyCall1()[2];
m_headerData[36U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[36U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[37U] = header.getMyCall1().GetChar(3); m_headerData[37U] = header.getMyCall1()[3];
m_headerData[38U] = header.getMyCall1().GetChar(4); m_headerData[38U] = header.getMyCall1()[4];
m_headerData[39U] = header.getMyCall1().GetChar(5); m_headerData[39U] = header.getMyCall1()[5];
m_headerData[40U] = header.getMyCall1().GetChar(6); m_headerData[40U] = header.getMyCall1()[6];
m_headerData[41U] = header.getMyCall1().GetChar(7); m_headerData[41U] = header.getMyCall1()[7];
m_headerData[42U] = SLOW_DATA_TYPE_HEADER | 5U; m_headerData[42U] = SLOW_DATA_TYPE_HEADER | 5U;
m_headerData[43U] = header.getMyCall2().GetChar(0); m_headerData[43U] = header.getMyCall2()[0];
m_headerData[44U] = header.getMyCall2().GetChar(1); m_headerData[44U] = header.getMyCall2()[1];
m_headerData[45U] = header.getMyCall2().GetChar(2); m_headerData[45U] = header.getMyCall2()[2];
m_headerData[46U] = header.getMyCall2().GetChar(3); m_headerData[46U] = header.getMyCall2()[3];
CCCITTChecksumReverse cksum; CCCITTChecksumReverse cksum;
cksum.update(m_headerData + 1U, 5U); cksum.update(m_headerData + 1U, 5U);
@ -157,41 +160,40 @@ void CSlowDataEncoder::setHeaderData(const CHeaderData& header)
m_headerPtr = 0U; m_headerPtr = 0U;
} }
void CSlowDataEncoder::setTextData(const wxString& text) void CSlowDataEncoder::setTextData(const std::string& text)
{ {
if (text.IsEmpty()) if (text.empty())
return; return;
::memset(m_textData, 'f', SLOW_DATA_FULL_BLOCK_SIZE); ::memset(m_textData, 'f', SLOW_DATA_FULL_BLOCK_SIZE);
m_textData[0U] = SLOW_DATA_TYPE_TEXT | 0U; m_textData[0U] = SLOW_DATA_TYPE_TEXT | 0U;
m_textData[1U] = text.GetChar(0); m_textData[1U] = text[0];
m_textData[2U] = text.GetChar(1); m_textData[2U] = text[1];
m_textData[3U] = text.GetChar(2); m_textData[3U] = text[2];
m_textData[4U] = text.GetChar(3); m_textData[4U] = text[3];
m_textData[5U] = text.GetChar(4); m_textData[5U] = text[4];
m_textData[6U] = SLOW_DATA_TYPE_TEXT | 1U; m_textData[6U] = SLOW_DATA_TYPE_TEXT | 1U;
m_textData[7U] = text.GetChar(5); m_textData[7U] = text[5];
m_textData[8U] = text.GetChar(6); m_textData[8U] = text[6];
m_textData[9U] = text.GetChar(7); m_textData[9U] = text[7];
m_textData[10U] = text.GetChar(8); m_textData[10U] = text[8];
m_textData[11U] = text.GetChar(9); m_textData[11U] = text[9];
m_textData[12U] = SLOW_DATA_TYPE_TEXT | 2U; m_textData[12U] = SLOW_DATA_TYPE_TEXT | 2U;
m_textData[13U] = text.GetChar(10); m_textData[13U] = text[10];
m_textData[14U] = text.GetChar(11); m_textData[14U] = text[11];
m_textData[15U] = text.GetChar(12); m_textData[15U] = text[12];
m_textData[16U] = text.GetChar(13); m_textData[16U] = text[13];
m_textData[17U] = text.GetChar(14); m_textData[17U] = text[14];
m_textData[18U] = SLOW_DATA_TYPE_TEXT | 3U; m_textData[18U] = SLOW_DATA_TYPE_TEXT | 3U;
m_textData[19U] = text.GetChar(15); m_textData[19U] = text[15];
m_textData[20U] = text.GetChar(16); m_textData[20U] = text[16];
m_textData[21U] = text.GetChar(17); m_textData[21U] = text[17];
m_textData[22U] = text.GetChar(18); m_textData[22U] = text[18];
m_textData[23U] = text.GetChar(19); m_textData[23U] = text[19];
m_textPtr = 0U; m_textPtr = 0U;
} }

@ -16,27 +16,48 @@
#include "HeaderData.h" #include "HeaderData.h"
#include <wx/wx.h> #include <string>
/*
* Encodes D-Star slow data into the 3-byte data field of each DV frame.
*
* Two independent payloads can be encoded:
* Header a copy of the radio header, split across 8 tagged 6-byte blocks
* with a CCITT-16 checksum appended. Used by the repeater to
* re-broadcast the header to late-joining receivers.
* Text a 20-character free-text message split across 4 tagged blocks.
*
* Each block is 6 bytes: 1-byte type/sequence tag followed by 5 payload bytes.
* getHeaderData() and getTextData() each return 3 bytes (half a block) per
* call, cycling back to the start after all blocks have been emitted.
*
* Before writing to the DV frame the 3 bytes are XOR-scrambled with the
* repeating key SCRAMBLER_BYTE1/SCRAMBLER_BYTE2/SCRAMBLER_BYTE3. sync()
* resets the read pointer to the beginning of the sequence so that the
* encoder is aligned with the data-sync frame position in the stream.
*/
class CSlowDataEncoder { class CSlowDataEncoder {
public: public:
CSlowDataEncoder(); CSlowDataEncoder();
~CSlowDataEncoder(); ~CSlowDataEncoder();
void setHeaderData(const CHeaderData& header); void setHeaderData(const CHeaderData& header);
void setTextData(const wxString& text); void setTextData(const std::string& text);
// Returns the next 3 scrambled bytes of the header slow-data sequence.
void getHeaderData(unsigned char* data); void getHeaderData(unsigned char* data);
// Returns the next 3 scrambled bytes of the text slow-data sequence.
void getTextData(unsigned char* data); void getTextData(unsigned char* data);
void reset(); void reset();
// Resets read pointers to the start of the sequence (call on each sync frame).
void sync(); void sync();
private: private:
unsigned char* m_headerData; unsigned char* m_headerData; // Pre-packed, unscrambled header slow-data bytes.
unsigned char* m_textData; unsigned char* m_textData; // Pre-packed, unscrambled text slow-data bytes.
unsigned int m_headerPtr; unsigned int m_headerPtr; // Current read offset into m_headerData.
unsigned int m_textPtr; unsigned int m_textPtr; // Current read offset into m_textData.
}; };
#endif #endif

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save

Powered by TurnKey Linux.