Merge pull request #17 from MW0MWZ/remove-wxwidgets-cpp17

master
Jonathan Naylor 4 months ago committed by GitHub
commit 84a33449c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitignore vendored

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

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

@ -21,12 +21,23 @@
#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 {
public:
CAMBEFEC();
~CAMBEFEC();
// Attempts in-place error correction; returns number of errors corrected.
unsigned int regenerate(unsigned char* bytes) const;
// Counts bit errors without modifying the data.
unsigned int count(const unsigned char* bytes) const;
private:

@ -16,6 +16,8 @@
#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 {
public:
virtual void transmitAnnouncementHeader(CHeaderData* header) = 0;

@ -18,12 +18,20 @@
#include "AnnouncementUnit.h"
#include <wx/filename.h>
#include <wx/file.h>
#include <cassert>
#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_localFileName(),
m_reader(),
@ -32,14 +40,23 @@ m_time(),
m_out(0U),
m_sending(false)
{
wxASSERT(handler != NULL);
assert(handler != nullptr);
m_localFileName.Printf(wxT("Announce %s"), callsign.c_str());
#if !defined(__WINDOWS__)
m_localFileName.Replace(wxT(" "), wxT("_"));
#endif
m_localFileName = "Announce " + callsign;
// Replace spaces with underscores in the filename
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()
@ -65,23 +82,33 @@ bool CAnnouncementUnit::writeData(const unsigned char* data, unsigned int length
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()))
::wxRemoveFile(fileName.GetFullPath());
if (access(filePath.c_str(), F_OK) == 0)
::remove(filePath.c_str());
}
void CAnnouncementUnit::startAnnouncement()
{
wxFileName fileName1(wxFileName::GetHomeDir(), m_localFileName, wxT("dvtool"));
wxFileName fileName2(wxFileName::GetHomeDir(), GLOBAL_FILE_NAME, wxT("dvtool"));
#if defined(_WIN32)
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())) {
bool ret = m_reader.open(fileName1.GetFullPath());
if (access(filePath1.c_str(), F_OK) == 0) {
bool ret = m_reader.open(filePath1);
if (!ret)
return;
} else if (wxFile::Exists(fileName2.GetFullPath())) {
bool ret = m_reader.open(fileName2.GetFullPath());
} else if (access(filePath2.c_str(), F_OK) == 0) {
bool ret = m_reader.open(filePath2);
if (!ret)
return;
} else {
@ -90,14 +117,12 @@ void CAnnouncementUnit::startAnnouncement()
DVTFR_TYPE type = m_reader.read();
if (type != DVTFR_HEADER) {
wxLogError(wxT("Invalid header element in the file - %d"), int(type));
m_reader.close();
return;
}
CHeaderData* header = m_reader.readHeader();
if (header == NULL) {
wxLogError(wxT("NULL header element in the file"));
if (header == nullptr) {
m_reader.close();
return;
}
@ -107,7 +132,7 @@ void CAnnouncementUnit::startAnnouncement()
m_handler->transmitAnnouncementHeader(header);
m_time.Start();
m_time = std::chrono::steady_clock::now();
m_out = 0U;
m_sending = true;
@ -118,12 +143,11 @@ void CAnnouncementUnit::clock()
if (!m_sending)
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) {
DVTFR_TYPE type = m_reader.read();
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_reader.close();
m_sending = false;

@ -25,11 +25,29 @@
#include "DStarDefines.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 {
public:
CAnnouncementUnit(IAnnouncementCallback* handler, const wxString& callsign);
CAnnouncementUnit(IAnnouncementCallback* handler, const std::string& callsign);
~CAnnouncementUnit();
bool writeHeader(const CHeaderData& header);
@ -39,15 +57,16 @@ public:
void startAnnouncement();
// Called every repeater tick; releases frames to the transmitter at air rate.
void clock();
private:
IAnnouncementCallback* m_handler;
wxString m_localFileName;
std::string m_localFileName; // Callsign-specific filename (no extension).
CDVTOOLFileReader m_reader;
CDVTOOLFileWriter m_writer;
wxStopWatch m_time;
unsigned int m_out;
std::chrono::steady_clock::time_point m_time;
unsigned int m_out; // Frames dispatched so far during playback.
bool m_sending;
};

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

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

@ -14,12 +14,13 @@
#ifndef 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 {
public:
virtual void readCallback(const wxFloat32* input, unsigned int nSamples, int id) = 0;
virtual void writeCallback(wxFloat32* output, int& nSamples, int id) = 0;
virtual void readCallback(const float* input, unsigned int nSamples, int id) = 0;
virtual void writeCallback(float* output, int& nSamples, int id) = 0;
private:
};

@ -14,6 +14,8 @@
#ifndef 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 {
public:
virtual void transmitBeaconHeader() = 0;

@ -18,19 +18,31 @@
#include "BeaconUnit.h"
#include <wx/filename.h>
#include <wx/textfile.h>
#include <wx/tokenzr.h>
#include <wx/ffile.h>
#include <fstream>
#include <sstream>
#include <cstdio>
#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;
// 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;
CBeaconUnit::CBeaconUnit(IBeaconCallback* handler, const wxString& callsign, const wxString& text, bool voice, TEXT_LANG language) :
m_ambe(NULL),
CBeaconUnit::CBeaconUnit(IBeaconCallback* handler, const std::string& callsign, const std::string& text, bool voice, TEXT_LANG language) :
m_ambe(nullptr),
m_ambeLength(0U),
m_data(NULL),
m_data(nullptr),
m_dataLength(0U),
m_index(),
m_language(language),
@ -43,69 +55,69 @@ m_seqNo(0U),
m_time(),
m_sending(false)
{
wxASSERT(handler != NULL);
assert(handler != nullptr);
wxString slowData = text;
slowData.resize(20U, wxT(' '));
std::string slowData = text;
slowData.resize(20U, ' ');
m_encoder.setTextData(slowData);
m_data = new unsigned char[MAX_FRAMES * VOICE_FRAME_LENGTH_BYTES];
::memset(m_data, 0x00U, MAX_FRAMES * VOICE_FRAME_LENGTH_BYTES);
m_data = new unsigned char[MAX_FRAMES * DV_FRAME_LENGTH_BYTES];
::memset(m_data, 0x00U, MAX_FRAMES * DV_FRAME_LENGTH_BYTES);
if (!voice)
return;
wxString ambeFileName;
wxString indxFileName;
std::string ambeFileName;
std::string indxFileName;
switch (m_language) {
case TL_DEUTSCH:
ambeFileName = wxT("de_DE.ambe");
indxFileName = wxT("de_DE.indx");
ambeFileName = "de_DE.ambe";
indxFileName = "de_DE.indx";
break;
case TL_DANSK:
ambeFileName = wxT("dk_DK.ambe");
indxFileName = wxT("dk_DK.indx");
ambeFileName = "dk_DK.ambe";
indxFileName = "dk_DK.indx";
break;
case TL_ITALIANO:
ambeFileName = wxT("it_IT.ambe");
indxFileName = wxT("it_IT.indx");
ambeFileName = "it_IT.ambe";
indxFileName = "it_IT.indx";
break;
case TL_FRANCAIS:
ambeFileName = wxT("fr_FR.ambe");
indxFileName = wxT("fr_FR.indx");
ambeFileName = "fr_FR.ambe";
indxFileName = "fr_FR.indx";
break;
case TL_ESPANOL:
ambeFileName = wxT("es_ES.ambe");
indxFileName = wxT("es_ES.indx");
ambeFileName = "es_ES.ambe";
indxFileName = "es_ES.indx";
break;
case TL_SVENSKA:
ambeFileName = wxT("se_SE.ambe");
indxFileName = wxT("se_SE.indx");
ambeFileName = "se_SE.ambe";
indxFileName = "se_SE.indx";
break;
case TL_POLSKI:
ambeFileName = wxT("pl_PL.ambe");
indxFileName = wxT("pl_PL.indx");
ambeFileName = "pl_PL.ambe";
indxFileName = "pl_PL.indx";
break;
case TL_ENGLISH_US:
ambeFileName = wxT("en_US.ambe");
indxFileName = wxT("en_US.indx");
ambeFileName = "en_US.ambe";
indxFileName = "en_US.indx";
break;
case TL_NORSK:
ambeFileName = wxT("no_NO.ambe");
indxFileName = wxT("no_NO.indx");
ambeFileName = "no_NO.ambe";
indxFileName = "no_NO.indx";
break;
// case TL_NEDERLANDS_NL:
// ambeFileName = wxT("nl_NL.ambe");
// indxFileName = wxT("nl_NL.indx");
// ambeFileName = "nl_NL.ambe";
// indxFileName = "nl_NL.indx";
// break;
// case TL_NEDERLANDS_BE:
// ambeFileName = wxT("nl_BE.ambe");
// indxFileName = wxT("nl_BE.indx");
// ambeFileName = "nl_BE.ambe";
// indxFileName = "nl_BE.indx";
// break;
default:
ambeFileName = wxT("en_GB.ambe");
indxFileName = wxT("en_GB.indx");
ambeFileName = "en_GB.ambe";
indxFileName = "en_GB.indx";
break;
}
@ -131,14 +143,14 @@ void CBeaconUnit::sendBeacon()
m_sending = true;
m_time.Start();
m_time = std::chrono::steady_clock::now();
m_in = 0U;
m_out = 0U;
m_seqNo = 0U;
m_dataLength = 0U;
if (m_ambe == NULL) {
if (m_ambe == nullptr) {
for (unsigned int i = 0U; i < 21U; i++) {
unsigned char buffer[DV_FRAME_LENGTH_BYTES];
@ -160,17 +172,17 @@ void CBeaconUnit::sendBeacon()
m_in++;
}
} else {
lookup(wxT(" "));
lookup(wxT(" "));
lookup(wxT(" "));
lookup(wxT(" "));
lookup(" ");
lookup(" ");
lookup(" ");
lookup(" ");
spellCallsign(m_callsign);
lookup(wxT(" "));
lookup(wxT(" "));
lookup(wxT(" "));
lookup(wxT(" "));
lookup(" ");
lookup(" ");
lookup(" ");
lookup(" ");
}
}
@ -179,7 +191,10 @@ void CBeaconUnit::clock()
if (!m_sending)
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) {
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];
if (info == NULL) {
// wxLogError(wxT("Cannot find the AMBE index for *%s*"), id.c_str());
CIndexList_t::iterator it = m_index.find(id);
if (it == m_index.end() || it->second == nullptr) {
// Cannot find the AMBE index for this id
return false;
}
CIndexRecord* info = it->second;
unsigned int start = info->getStart();
unsigned int length = info->getLength();
@ -232,82 +249,81 @@ bool CBeaconUnit::lookup(const wxString &id)
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++) {
wxString c = callsign.Mid(i, 1U);
std::string c = callsign.substr(i, 1U);
if (!c.IsSameAs(wxT(" ")))
if (c != " ")
lookup(c);
}
wxChar c = callsign.GetChar(length - 1U);
char c = callsign[length - 1U];
switch (c) {
case wxT('A'):
lookup(wxT("alpha"));
case 'A':
lookup("alpha");
break;
case wxT('B'):
lookup(wxT("bravo"));
case 'B':
lookup("bravo");
break;
case wxT('C'):
lookup(wxT("charlie"));
case 'C':
lookup("charlie");
break;
case wxT('D'):
lookup(wxT("delta"));
case 'D':
lookup("delta");
break;
default:
lookup(wxString(c));
lookup(std::string(1, c));
break;
}
}
bool CBeaconUnit::readAMBE(const wxString& name)
bool CBeaconUnit::readAMBE(const std::string& name)
{
wxFileName fileName(wxFileName::GetHomeDir(), name);
if (!fileName.IsFileReadable()) {
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str());
#if defined(__WINDOWS__)
fileName.Assign(::wxGetCwd(), name);
#if defined(_WIN32)
const char* home = getenv("USERPROFILE");
#else
fileName.Assign(wxT(DATA_DIR), name);
const char* home = getenv("HOME");
#endif
if (!fileName.IsFileReadable()) {
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str());
std::string homePath = std::string(home != nullptr ? home : "") + "/" + name;
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;
}
}
wxFFile file;
bool ret = file.Open(fileName.GetFullPath().c_str(), wxT("rb"));
if (!ret) {
wxLogMessage(wxT("Cannot open %s for reading"), fileName.GetFullPath().c_str());
FILE* file = ::fopen(filePath.c_str(), "rb");
if (file == nullptr)
return false;
}
wxLogMessage(wxT("Reading %s"), fileName.GetFullPath().c_str());
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) {
wxLogMessage(wxT("Unable to read the header from %s"), fileName.GetFullPath().c_str());
file.Close();
::fclose(file);
return false;
}
if (::memcmp(buffer, "AMBE", 4U) != 0) {
wxLogMessage(wxT("Invalid header from %s"), fileName.GetFullPath().c_str());
file.Close();
::fclose(file);
return false;
}
// Length of the file minus the header
unsigned int length = file.Length() - 4U;
// Determine file length minus the 4-byte header
::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
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)
::memcpy(p, NULL_AMBE_DATA_BYTES, VOICE_FRAME_LENGTH_BYTES);
n = file.Read(p, length);
n = ::fread(p, 1U, length, file);
if (n != length) {
wxLogMessage(wxT("Unable to read the AMBE data from %s"), fileName.GetFullPath().c_str());
file.Close();
::fclose(file);
delete[] m_ambe;
m_ambe = NULL;
m_ambe = nullptr;
return false;
}
file.Close();
::fclose(file);
return true;
}
bool CBeaconUnit::readIndex(const wxString& name)
bool CBeaconUnit::readIndex(const std::string& name)
{
wxFileName fileName(wxFileName::GetHomeDir(), name);
if (!fileName.IsFileReadable()) {
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str());
#if defined(__WINDOWS__)
fileName.Assign(::wxGetCwd(), name);
#if defined(_WIN32)
const char* home = getenv("USERPROFILE");
#else
fileName.Assign(wxT(DATA_DIR), name);
const char* home = getenv("HOME");
#endif
if (!fileName.IsFileReadable()) {
wxLogMessage(wxT("File %s not readable"), fileName.GetFullPath().c_str());
std::string homePath = std::string(home != nullptr ? home : "") + "/" + name;
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;
}
}
wxTextFile file;
bool ret = file.Open(fileName.GetFullPath());
if (!ret) {
wxLogMessage(wxT("Cannot open %s for reading"), fileName.GetFullPath().c_str());
std::ifstream file(filePath);
if (!file.is_open())
return false;
}
// Add a silence entry at the beginning
m_index[wxT(" ")] = new CIndexRecord(wxT(" "), 0U, SILENCE_LENGTH);
wxLogMessage(wxT("Reading %s"), fileName.GetFullPath().c_str());
unsigned int nLines = file.GetLineCount();
for (unsigned int i = 0; i < nLines; i++) {
wxString line = file.GetLine(i);
if (line.length() > 0 && line.GetChar(0) != wxT('#')) {
wxStringTokenizer t(line, wxT(" \t\r\n"), wxTOKEN_STRTOK);
wxString name = t.GetNextToken();
wxString startTxt = t.GetNextToken();
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);
m_index[" "] = new CIndexRecord(" ", 0U, SILENCE_LENGTH);
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;
if (!entryName.empty() && !startTxt.empty() && !lengthTxt.empty()) {
unsigned long start = std::stoul(startTxt);
unsigned long length = std::stoul(lengthTxt);
if (start >= m_ambeLength || (start + length) >= m_ambeLength) {
// Out of range entry — skip silently
} else {
m_index[entryName] = new CIndexRecord(entryName, start + SILENCE_LENGTH, length);
}
}
}
}
file.Close();
file.close();
return true;
}

@ -23,18 +23,25 @@
#include "BeaconCallback.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 {
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_start(start),
m_length(length)
{
}
wxString getName() const
std::string getName() const
{
return m_name;
}
@ -50,43 +57,65 @@ public:
}
private:
wxString m_name;
unsigned int m_start;
unsigned int m_length;
std::string m_name;
unsigned int m_start; // Frame offset into the AMBE buffer.
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 {
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();
// Assembles the beacon frames and signals the repeater thread to begin TX.
void sendBeacon();
// Called every repeater tick; releases frames to the transmitter at air rate.
void clock();
private:
unsigned char* m_ambe;
unsigned int m_ambeLength;
unsigned char* m_data;
unsigned int m_dataLength;
CIndexList_t m_index;
unsigned char* m_ambe; // Raw AMBE frames loaded from the .ambe file.
unsigned int m_ambeLength; // Total frames available in m_ambe.
unsigned char* m_data; // Assembled DV frames ready for transmission.
unsigned int m_dataLength; // Bytes written into m_data so far.
CIndexList_t m_index; // Word/character -> AMBE frame mapping.
TEXT_LANG m_language;
IBeaconCallback* m_handler;
wxString m_callsign;
CSlowDataEncoder m_encoder;
unsigned int m_in;
unsigned int m_out;
unsigned int m_seqNo;
wxStopWatch m_time;
std::string m_callsign;
CSlowDataEncoder m_encoder; // Encodes the beacon text into slow-data fields.
unsigned int m_in; // Total frames queued into m_data.
unsigned int m_out; // Frames dispatched to the transmitter so far.
unsigned int m_seqNo; // Slow-data sequence counter (020).
std::chrono::steady_clock::time_point m_time; // Wall-clock start time of the beacon.
bool m_sending;
bool lookup(const wxString& id);
void spellCallsign(const wxString& callsign);
// Appends AMBE frames for a word/character from the index into m_data.
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 readIndex(const wxString& name);
bool readAMBE(const std::string& name);
bool readIndex(const std::string& name);
};
#endif

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

@ -14,8 +14,18 @@
#ifndef 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 {
public:
CCCITTChecksum();
@ -31,8 +41,8 @@ public:
private:
union {
wxUint16 m_crc16;
wxUint8 m_crc8[2U];
uint16_t m_crc16;
uint8_t m_crc8[2U];
};
};

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

@ -14,8 +14,17 @@
#ifndef 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 {
public:
CCCITTChecksumReverse();
@ -31,8 +40,8 @@ public:
private:
union {
wxUint16 m_crc16;
wxUint8 m_crc8[2U];
uint16_t m_crc16;
uint8_t m_crc8[2U];
};
};

@ -19,9 +19,10 @@
#include "CallsignList.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_callsigns()
{
@ -34,43 +35,41 @@ CCallsignList::~CCallsignList()
bool CCallsignList::load()
{
wxTextFile file;
bool res = file.Open(m_filename);
if (!res)
std::ifstream file(m_filename);
if (!file.is_open())
return false;
unsigned int lines = file.GetLineCount();
if (lines == 0U) {
file.Close();
return true;
}
m_callsigns.Alloc(lines);
std::string callsign;
while (std::getline(file, callsign)) {
// Strip trailing CR so CRLF files (Windows line endings) are handled correctly
if (!callsign.empty() && callsign.back() == '\r')
callsign.pop_back();
wxString callsign = file.GetFirstLine();
// Convert to uppercase
std::transform(callsign.begin(), callsign.end(), callsign.begin(), ::toupper);
while (!file.Eof()) {
callsign.MakeUpper();
callsign.Append(wxT(" "));
callsign.Truncate(LONG_CALLSIGN_LENGTH);
// Pad to LONG_CALLSIGN_LENGTH with spaces, then truncate
callsign.append(8U, ' ');
callsign.resize(LONG_CALLSIGN_LENGTH);
m_callsigns.Add(callsign);
callsign = file.GetNextLine();
m_callsigns.push_back(callsign);
}
file.Close();
file.close();
return true;
}
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
#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 {
public:
CCallsignList(const wxString& filename);
CCallsignList(const std::string& filename);
~CCallsignList();
bool load();
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:
wxString m_filename;
wxArrayString m_callsigns;
std::string m_filename;
std::vector<std::string> m_callsigns;
};
#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.
*/
/*
* 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
#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 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 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
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;
// Each DV frame also carries 3 bytes of slow data (24 bits at 1200 bps).
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;
// 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;
// Length of the FEC (forward error correction) section in the radio header.
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;
// 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;
// --- 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;
// Samples per processing block handed to the modem (20 ms at 48 kHz).
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;
// --- Callsign / locator field widths ---
// Maidenhead grid locator string length (e.g. "IO91wm").
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;
// Suffix / RPT2 suffix field (e.g. " G" for gateway, " A" for port A).
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_GPSDATA = 0x30U;
const unsigned char SLOW_DATA_TYPE_TEXT = 0x40U;
const unsigned char SLOW_DATA_TYPE_HEADER = 0x50U;
const unsigned char SLOW_DATA_TYPE_SQUELCH = 0xC0U;
const unsigned char SLOW_DATA_TYPE_GPSDATA = 0x30U; // APRS/GPS position string
const unsigned char SLOW_DATA_TYPE_TEXT = 0x40U; // Free-text message (TX message)
const unsigned char SLOW_DATA_TYPE_HEADER = 0x50U; // Repeated radio header
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;
// Maximum length of a slow-data text message in characters.
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_BYTE2 = 0x4FU;
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;
// Set when a repeater is involved in the QSO.
const unsigned char REPEATER_MASK = 0x40U;
// Set when the transmission was interrupted (incomplete header received).
const unsigned char INTERRUPTED_MASK = 0x20U;
// Set for control-channel signalling frames.
const unsigned char CONTROL_SIGNAL_MASK = 0x10U;
// Set to request priority / urgent handling.
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 = 0x07U;
const unsigned char AUTO_REPLY = 0x06U;
const unsigned char RESEND_REQUESTED = 0x04U;
const unsigned char ACK_FLAG = 0x03U;
const unsigned char NO_RESPONSE = 0x02U;
const unsigned char RELAY_UNAVAILABLE = 0x01U;
const unsigned char REPEATER_CONTROL = 0x07U; // Normal repeater frame
const unsigned char AUTO_REPLY = 0x06U; // Automatic acknowledgement
const unsigned char RESEND_REQUESTED = 0x04U; // Request retransmission
const unsigned char ACK_FLAG = 0x03U; // Positive acknowledgement
const unsigned char NO_RESPONSE = 0x02U; // Destination not responding
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;
// 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_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;
// --- Network linking ports ---
// UDP port used by the DExtra reflector linking protocol.
const unsigned int DEXTRA_PORT = 30001U;
// UDP port used by the DCS reflector linking protocol.
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;
// 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 LOCAL_RUN_FRAME_COUNT = 1U;
const unsigned int NETWORK_RUN_FRAME_COUNT = 25U;
const unsigned int DSTAR_BLEEP_FREQ = 2000U;
const unsigned int DSTAR_BLEEP_LENGTH = 100U;
const float DSTAR_BLEEP_AMPL = 0.5F;
// Parameters for the end-of-transmission "bleep" courtesy tone.
const unsigned int DSTAR_BLEEP_FREQ = 2000U; // Hz
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 SPLIT_RX_GUI_COUNT = 5U;
const unsigned int SPLIT_RX_COUNT = 25U;
const unsigned int SPLIT_TX_GUI_COUNT = 3U;
const unsigned int SPLIT_TX_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_TX_COUNT = 5U;
// --- Enums ---
// Repeater main state machine. Drives the TX/RX loop in the thread classes.
enum DSTAR_RPT_STATE {
DSRS_SHUTDOWN,
DSRS_LISTENING,
DSRS_VALID,
DSRS_VALID_WAIT,
DSRS_INVALID,
DSRS_INVALID_WAIT,
DSRS_TIMEOUT,
DSRS_TIMEOUT_WAIT,
DSRS_NETWORK
DSRS_SHUTDOWN, // Repeater is administratively shut down
DSRS_LISTENING, // Idle, waiting for a valid D-Star header
DSRS_VALID, // Receiving a transmission from a known/allowed callsign
DSRS_VALID_WAIT, // Transmission ended; waiting for the tail to clear
DSRS_INVALID, // Receiving from an unknown or blocked callsign
DSRS_INVALID_WAIT, // Invalid transmission ended; clearing tail
DSRS_TIMEOUT, // Transmission exceeded the configured timeout
DSRS_TIMEOUT_WAIT, // Timeout tail clearing
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 {
NETWORK_NONE,
NETWORK_HEADER,
NETWORK_DATA,
NETWORK_TEXT,
NETWORK_TEMPTEXT,
NETWORK_STATUS1,
NETWORK_NONE, // No packet available
NETWORK_HEADER, // D-Star link header (start of a network QSO)
NETWORK_DATA, // DV frame payload
NETWORK_TEXT, // Slow-data text message from the gateway
NETWORK_TEMPTEXT, // Temporary/override text message
NETWORK_STATUS1, // ircDDB user status slots 15
NETWORK_STATUS2,
NETWORK_STATUS3,
NETWORK_STATUS4,
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 {
MODE_DUPLEX,
MODE_SIMPLEX,
MODE_GATEWAY,
MODE_TXONLY,
MODE_RXONLY,
MODE_TXANDRX
MODE_DUPLEX, // Full duplex: separate RX and TX frequencies
MODE_SIMPLEX, // Simplex: share a single frequency for RX and TX
MODE_GATEWAY, // Gateway-only: no over-air receive, network traffic only
MODE_TXONLY, // Transmit only (e.g. beacon node)
MODE_RXONLY, // Receive only (e.g. logging node)
MODE_TXANDRX // Independent TX and RX paths (split site)
};
// Current state of a reflector or gateway link.
enum LINK_STATUS {
LS_NONE,
LS_PENDING_IRCDDB,
LS_LINKING_LOOPBACK,
LS_NONE, // Not linked
LS_PENDING_IRCDDB, // Waiting for ircDDB callsign lookup to complete
LS_LINKING_LOOPBACK, // Handshake in progress for each protocol type:
LS_LINKING_DEXTRA,
LS_LINKING_DPLUS,
LS_LINKING_DCS,
LS_LINKING_CCS,
LS_LINKED_LOOPBACK,
LS_LINKED_LOOPBACK, // Fully linked on each protocol:
LS_LINKED_DEXTRA,
LS_LINKED_DPLUS,
LS_LINKED_DCS,
LS_LINKED_CCS
};
// What the repeater sends back to the calling station after a transmission.
enum ACK_TYPE {
AT_NONE,
AT_BER,
AT_STATUS
AT_NONE, // No acknowledgement
AT_BER, // Bit-error-rate report
AT_STATUS // Repeater status message
};
// Language used when generating spoken or text acknowledgement messages.
enum TEXT_LANG {
TL_ENGLISH_UK,
TL_DEUTSCH,
@ -178,21 +258,24 @@ enum TEXT_LANG {
TL_NORSK
};
// How the modem hardware is connected to the host.
enum CONNECTION_TYPE {
CT_USB,
CT_NETWORK
};
// Which USB backend to use for USB-connected modems.
enum USB_INTERFACE {
UI_LIBUSB,
UI_WINUSB
};
// DVMega radio module variant; determines which band(s) are active.
enum DVMEGA_VARIANT {
DVMV_MODEM,
DVMV_RADIO_2M,
DVMV_RADIO_70CM,
DVMV_RADIO_2M_70CM
DVMV_MODEM, // Modem-only (external radio)
DVMV_RADIO_2M, // Built-in 2 m radio
DVMV_RADIO_70CM, // Built-in 70 cm radio
DVMV_RADIO_2M_70CM // Dual-band 2 m / 70 cm radio
};
#endif

@ -14,7 +14,7 @@
#include "DStarGMSKDemodulator.h"
// 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.000000001829471F, 0.000000016627294F, 0.000000131062698F, 0.000000895979719F, 0.000005312253663F, 0.000027316243802F,
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;
@ -60,7 +60,7 @@ TRISTATE CDStarGMSKDemodulator::decode(wxFloat32 val)
m_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;
@ -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;
@ -112,7 +112,7 @@ void CDStarGMSKDemodulator::setInvert(bool set)
void CDStarGMSKDemodulator::lock(bool on)
{
// Debugging only XXX
wxLogMessage(wxT("Current DC offset: %f"), m_offset);
// (wxLogMessage removed - was: "Current DC offset: %f", m_offset)
m_locked = on;

@ -18,29 +18,45 @@
#include "FIRFilter.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 {
public:
CDStarGMSKDemodulator();
~CDStarGMSKDemodulator();
TRISTATE decode(wxFloat32 val);
TRISTATE decode(float val);
void setInvert(bool set);
void reset();
// Freezes (on=true) or enables (on=false) PLL frequency adaptation.
void lock(bool on);
private:
CFIRFilter m_filter;
bool m_invert;
unsigned int m_pll;
bool m_prev;
unsigned int m_inc;
bool m_locked;
wxFloat32 m_offset;
wxFloat32 m_accum;
unsigned int m_count;
CFIRFilter m_filter;
bool m_invert;
unsigned int m_pll; // Current PLL phase accumulator.
bool m_prev; // Previous bit value, used for GMSK differential decoding.
unsigned int m_inc; // PLL phase increment per sample.
bool m_locked;
float m_offset;
float m_accum;
unsigned int m_count;
};
#endif

@ -15,8 +15,10 @@
#include "DStarDefines.h"
#include <cassert>
// 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.000000001829471F, 0.000000016627294F, 0.000000131062698F, 0.000000895979719F, 0.000005312253663F, 0.000027316243802F,
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);
wxASSERT(length == DSTAR_RADIO_BIT_LENGTH);
assert(buffer != nullptr);
assert(length == DSTAR_RADIO_BIT_LENGTH);
if (m_invert)
bit = !bit;

@ -16,12 +16,21 @@
#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 {
public:
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);

File diff suppressed because it is too large Load Diff

@ -21,198 +21,248 @@
#include "DStarDefines.h"
#include <wx/wx.h>
#include <wx/config.h>
#include <wx/filename.h>
#include <stdexcept>
#include <string>
#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 {
public:
#if defined(__WINDOWS__)
CDStarRepeaterConfig(wxConfigBase* config, const wxString& dir, const wxString& configName, const wxString& name);
#else
CDStarRepeaterConfig(const wxString& dir, const wxString& configName, const wxString& name, const bool mustExist=false);
#endif
/*
* Opens and parses filePath. Throws std::runtime_error if the file
* does not exist or cannot be opened. All members are initialised to
* built-in defaults before parsing, so any key absent from the file
* silently retains its default value.
*/
explicit CDStarRepeaterConfig(const std::string& filePath);
~CDStarRepeaterConfig();
void getCallsign(wxString& callsign, wxString& gateway, DSTAR_MODE& mode, ACK_TYPE& ack, bool& restriction, bool& rpt1Validation, bool& dtmfBlanking, bool& errorReply) const;
void setCallsign(const wxString& callsign, const wxString& gateway, DSTAR_MODE mode, ACK_TYPE ack, bool restriction, bool rpt1Validation, bool dtmfBlanking, bool errorReply);
// [General]
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;
void setNetwork(const wxString& gatewayAddress, unsigned int gatewayPort, const wxString& localAddress, unsigned int localPort, const wxString& name);
// [Log]
void getLog(std::string& filePath, unsigned int& fileLevel, unsigned int& displayLevel, unsigned int& mqttLevel) const;
void getModem(wxString& type) const;
void setModem(const wxString& type);
// [Paths]
void getPaths(std::string& dataDir, std::string& audioDir) const;
void getTimes(unsigned int& timeout, unsigned int& ackTime) const;
void setTimes(unsigned int timeout, unsigned int ackTime);
// [Network]
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;
void setBeacon(unsigned int time, const wxString& text, bool voice, TEXT_LANG language);
// [Times]
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;
void setAnnouncement(bool enabled, unsigned int time, const wxString& recordRPT1, const wxString& recordRPT2, const wxString& deleteRPT1, const wxString& deleteRPT2);
// [Beacon]
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;
void setController(const wxString& type, unsigned int serialConfig, bool pttInvert, unsigned int activeHangTime);
// [Controller]
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 setOutputs(bool out1, bool out2, bool out3, bool out4);
// [Frame Logging]
void getLogging(bool& logging) const;
void setLogging(bool logging);
void getPosition(int& x, int& y) const;
void setPosition(int x, int y);
// [Whitelist] / [Blacklist] / [Greylist] (empty string = not configured)
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;
void setDVAP(const wxString& port, unsigned int frequency, int power, int squelch);
// [DVAP]
void getDVAP(std::string& port, unsigned int& frequency, int& power, int& squelch) const;
// [GMSK]
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;
void setDVRPTR1(const wxString& port, bool rxInvert, bool txInvert, bool channel, unsigned int modLevel, unsigned int txDelay);
// [DV-RPTR V1]
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;
void setDVRPTR2(CONNECTION_TYPE connectionType, const wxString& usbPort, const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, unsigned int txDelay);
// [DV-RPTR V2]
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;
void setDVRPTR3(CONNECTION_TYPE connectionType, const wxString& usbPort, const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, unsigned int txDelay);
// [DV-RPTR V3]
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;
void setDVMEGA(const wxString& port, DVMEGA_VARIANT variant, bool rxInvert, bool txInvert, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power);
// [DVMEGA]
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;
void setMMDVM(const wxString& port, bool rxInvert, bool txInvert, bool pttInvert, unsigned int txDelay, unsigned int rxLevel, unsigned int txLevel);
// [MMDVM]
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;
void setSoundCard(const wxString& rxDevice, const wxString& txDevice, bool rxInvert, bool txInvert, wxFloat32 rxLevel, wxFloat32 txLevel, unsigned int txDelay, unsigned int txTail);
// [Sound Card]
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;
void setSplit(const wxString& localAddress, unsigned int localPort, const wxArrayString& transmitterNames, const wxArrayString& receiverNames, unsigned int timeout);
// [Split]
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;
void setIcom(const wxString& port);
// [Icom]
void getIcom(std::string& port) const;
#if defined(MQTT)
void getMQTT(wxString& host, unsigned int& port, bool& auth, wxString& username, wxString& password, unsigned int& keepalive, wxString& name) const;
void setMQTT(const wxString& host, unsigned int port, bool auth, const wxString& username, const wxString& password, unsigned int keepalive, const wxString& name);
// [MQTT]
void getMQTT(std::string& host, unsigned int& port, bool& auth, std::string& username, std::string& password, unsigned int& keepalive, std::string& name) const;
#endif
bool write();
private:
#if defined(__WINDOWS__)
wxConfigBase* m_config;
wxString m_name;
#endif
wxFileName m_fileName;
wxString m_callsign;
wxString m_gateway;
// [General]
std::string m_callsign;
std::string m_gateway;
DSTAR_MODE m_mode;
ACK_TYPE m_ack;
bool m_restriction;
bool m_rpt1Validation;
bool m_dtmfBlanking;
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;
wxString m_localAddress;
std::string m_localAddress;
unsigned int m_localPort;
wxString m_networkName;
wxString m_modemType;
std::string m_networkName;
// [Modem]
std::string m_modemType;
// [Times]
unsigned int m_timeout;
unsigned int m_ackTime;
// [Beacon]
unsigned int m_beaconTime;
wxString m_beaconText;
std::string m_beaconText;
bool m_beaconVoice;
TEXT_LANG m_language;
// [Announcement]
bool m_announcementEnabled;
unsigned int m_announcementTime;
wxString m_announcementRecordRPT1;
wxString m_announcementRecordRPT2;
wxString m_announcementDeleteRPT1;
wxString m_announcementDeleteRPT2;
std::string m_announcementRecordRPT1;
std::string m_announcementRecordRPT2;
std::string m_announcementDeleteRPT1;
std::string m_announcementDeleteRPT2;
// [Control]
bool m_controlEnabled;
wxString m_controlRpt1Callsign;
wxString m_controlRpt2Callsign;
wxString m_controlShutdown;
wxString m_controlStartup;
wxString m_controlStatus1;
wxString m_controlStatus2;
wxString m_controlStatus3;
wxString m_controlStatus4;
wxString m_controlStatus5;
wxString m_controlCommand1;
wxString m_controlCommand1Line;
wxString m_controlCommand2;
wxString m_controlCommand2Line;
wxString m_controlCommand3;
wxString m_controlCommand3Line;
wxString m_controlCommand4;
wxString m_controlCommand4Line;
wxString m_controlCommand5;
wxString m_controlCommand5Line;
wxString m_controlCommand6;
wxString m_controlCommand6Line;
wxString m_controlOutput1;
wxString m_controlOutput2;
wxString m_controlOutput3;
wxString m_controlOutput4;
wxString m_controllerType;
std::string m_controlRpt1Callsign;
std::string m_controlRpt2Callsign;
std::string m_controlShutdown;
std::string m_controlStartup;
std::string m_controlStatus1;
std::string m_controlStatus2;
std::string m_controlStatus3;
std::string m_controlStatus4;
std::string m_controlStatus5;
std::string m_controlCommand1;
std::string m_controlCommand1Line;
std::string m_controlCommand2;
std::string m_controlCommand2Line;
std::string m_controlCommand3;
std::string m_controlCommand3Line;
std::string m_controlCommand4;
std::string m_controlCommand4Line;
std::string m_controlCommand5;
std::string m_controlCommand5Line;
std::string m_controlCommand6;
std::string m_controlCommand6Line;
std::string m_controlOutput1;
std::string m_controlOutput2;
std::string m_controlOutput3;
std::string m_controlOutput4;
// [Controller]
std::string m_controllerType;
unsigned int m_serialConfig;
bool m_pttInvert;
unsigned int m_activeHangTime;
// [Outputs]
bool m_output1;
bool m_output2;
bool m_output3;
bool m_output4;
// [Frame Logging]
bool m_logging;
int m_x;
int m_y;
// DVAP
wxString m_dvapPort;
// [Whitelist] / [Blacklist] / [Greylist] (empty = not configured)
std::string m_whitelistFile;
std::string m_blacklistFile;
std::string m_greylistFile;
// [DVAP]
std::string m_dvapPort;
unsigned int m_dvapFrequency;
int m_dvapPower;
int m_dvapSquelch;
// GMSK
// [GMSK]
USB_INTERFACE m_gmskInterface;
unsigned int m_gmskAddress;
// DV-RPTR 1
wxString m_dvrptr1Port;
// [DV-RPTR V1]
std::string m_dvrptr1Port;
bool m_dvrptr1RXInvert;
bool m_dvrptr1TXInvert;
bool m_dvrptr1Channel;
unsigned int m_dvrptr1ModLevel;
unsigned int m_dvrptr1TXDelay;
// DV-RPTR 2
// [DV-RPTR V2]
CONNECTION_TYPE m_dvrptr2Connection;
wxString m_dvrptr2USBPort;
wxString m_dvrptr2Address;
std::string m_dvrptr2USBPort;
std::string m_dvrptr2Address;
unsigned int m_dvrptr2Port;
bool m_dvrptr2TXInvert;
unsigned int m_dvrptr2ModLevel;
unsigned int m_dvrptr2TXDelay;
// DV-RPTR 3
// [DV-RPTR V3]
CONNECTION_TYPE m_dvrptr3Connection;
wxString m_dvrptr3USBPort;
wxString m_dvrptr3Address;
std::string m_dvrptr3USBPort;
std::string m_dvrptr3Address;
unsigned int m_dvrptr3Port;
bool m_dvrptr3TXInvert;
unsigned int m_dvrptr3ModLevel;
unsigned int m_dvrptr3TXDelay;
// DVMEGA
wxString m_dvmegaPort;
// [DVMEGA]
std::string m_dvmegaPort;
DVMEGA_VARIANT m_dvmegaVariant;
bool m_dvmegaRXInvert;
bool m_dvmegaTXInvert;
@ -221,8 +271,8 @@ private:
unsigned int m_dvmegaTXFrequency;
unsigned int m_dvmegaPower;
// MMDVM
wxString m_mmdvmPort;
// [MMDVM]
std::string m_mmdvmPort;
bool m_mmdvmRXInvert;
bool m_mmdvmTXInvert;
bool m_mmdvmPTTInvert;
@ -230,35 +280,35 @@ private:
unsigned int m_mmdvmRXLevel;
unsigned int m_mmdvmTXLevel;
// Sound Card
wxString m_soundCardRXDevice;
wxString m_soundCardTXDevice;
// [Sound Card]
std::string m_soundCardRXDevice;
std::string m_soundCardTXDevice;
bool m_soundCardRXInvert;
bool m_soundCardTXInvert;
wxFloat32 m_soundCardRXLevel;
wxFloat32 m_soundCardTXLevel;
float m_soundCardRXLevel;
float m_soundCardTXLevel;
unsigned int m_soundCardTXDelay;
unsigned int m_soundCardTXTail;
// Split
wxString m_splitLocalAddress;
unsigned int m_splitLocalPort;
wxArrayString m_splitTXNames;
wxArrayString m_splitRXNames;
unsigned int m_splitTimeout;
// [Split]
std::string m_splitLocalAddress;
unsigned int m_splitLocalPort;
std::vector<std::string> m_splitTXNames;
std::vector<std::string> m_splitRXNames;
unsigned int m_splitTimeout;
// Icom Access Point/Terminal Mode
wxString m_icomPort;
// [Icom]
std::string m_icomPort;
#if defined(MQTT)
// MQTT
wxString m_mqttHost;
// [MQTT]
std::string m_mqttHost;
unsigned int m_mqttPort;
bool m_mqttAuth;
wxString m_mqttUsername;
wxString m_mqttPassword;
std::string m_mqttUsername;
std::string m_mqttPassword;
unsigned int m_mqttKeepalive;
wxString m_mqttName;
std::string m_mqttName;
#endif
};

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

@ -14,6 +14,18 @@
#ifndef 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 {
public:
CDStarScrambler();
@ -28,7 +40,7 @@ public:
void reset();
private:
unsigned int m_count;
unsigned int m_count; // Current position in the LFSR sequence.
};
#endif

@ -19,8 +19,15 @@
#include "CCITTChecksumReverse.h"
#include "DVAPController.h"
#include "DStarDefines.h"
#include "Logger.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 int DVAP_REQ_NAME_LEN = 4U;
@ -125,7 +132,7 @@ const unsigned int BUFFER_LENGTH = 200U;
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(),
m_serial(port, SERIAL_230400),
m_frequency(frequency),
@ -133,24 +140,24 @@ m_power(power),
m_squelch(squelch),
m_squelchOpen(false),
m_signal(0),
m_buffer(NULL),
m_buffer(nullptr),
m_streamId(0U),
m_framePos(0U),
m_seq(0U),
m_txData(1000U)
#if defined(DVAP_DUMP)
,
m_dvapData(NULL),
m_dvapLength(NULL),
m_dvapData(nullptr),
m_dvapLength(nullptr),
m_dvapIndex(0U)
#endif
{
wxASSERT(!port.IsEmpty());
wxASSERT((frequency >= 144000000U && frequency <= 148000000U) ||
(frequency >= 220000000U && frequency <= 225000000U) ||
(frequency >= 420000000U && frequency <= 450000000U));
wxASSERT(power >= -12 && power <= 10);
wxASSERT(squelch >= -128 && squelch <= -45);
assert(!port.empty());
assert((frequency >= 144000000U && frequency <= 148000000U) ||
(frequency >= 220000000U && frequency <= 225000000U) ||
(frequency >= 420000000U && frequency <= 450000000U));
assert(power >= -12 && power <= 10);
assert(squelch >= -128 && squelch <= -45);
m_buffer = new unsigned char[BUFFER_LENGTH];
@ -238,16 +245,19 @@ bool CDVAPController::start()
return false;
}
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CDVAPController::entry, this);
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
CTimer pollTimer(200U, 2U);
@ -272,31 +282,32 @@ void* CDVAPController::Entry()
case RT_TIMEOUT:
break;
case RT_ERROR:
wxLogMessage(wxT("Stopping DVAP Controller thread"));
wxLogMessage("Stopping DVAP Controller thread");
#if defined(DVAP_DUMP)
dumpPackets();
#endif
m_serial.close();
return NULL;
delete[] writeBuffer;
return;
case RT_STATE:
m_signal = int(m_buffer[4U]) - 256;
m_squelchOpen = m_buffer[5U] == 0x01U;
space = m_buffer[6U];
break;
case RT_PTT:
m_tx = m_buffer[4U] == 0x01U;
m_tx = m_buffer[4U] == 0x01U;
break;
case RT_START:
break;
case RT_STOP:
wxLogWarning(wxT("DVAP has stopped, restarting"));
wxLogWarning("DVAP has stopped, restarting");
#if defined(DVAP_DUMP)
dumpPackets();
#endif
startDVAP();
break;
case RT_HEADER: {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char hdr[2U];
hdr[0U] = DSMTT_HEADER;
@ -309,7 +320,7 @@ void* CDVAPController::Entry()
case RT_HEADER_ACK:
break;
case RT_GMSK_DATA: {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
bool end = (m_buffer[4U] & 0x40U) == 0x40U;
if (end) {
@ -328,7 +339,7 @@ void* CDVAPController::Entry()
}
break;
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)
dumpPackets();
#endif
@ -337,8 +348,8 @@ void* CDVAPController::Entry()
startDVAP();
break;
default:
wxLogMessage(wxT("Unknown message"));
CUtils::dump(wxT("Buffer dump"), m_buffer, length);
wxLogMessage("Unknown message");
CUtils::dump("Buffer dump", m_buffer, length);
#if defined(DVAP_DUMP)
dumpPackets();
#endif
@ -348,7 +359,7 @@ void* CDVAPController::Entry()
// Use the status packet every 20ms to trigger the sending of data to the DVAP
if (space > 0U && type == RT_STATE) {
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(writeBuffer, writeLength);
@ -356,55 +367,53 @@ void* CDVAPController::Entry()
// Only send the header when the TX is off
if (!m_tx && writeLength == DVAP_HEADER_LEN) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength);
// CUtils::dump("Write Header", writeBuffer, writeLength);
#if defined(DVAP_DUMP)
storePacket(writeBuffer, writeLength);
#endif
int ret = m_serial.write(writeBuffer, 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;
space--;
}
if (writeLength == DVAP_GMSK_DATA_LEN) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength);
// CUtils::dump("Write Data", writeBuffer, writeLength);
#if defined(DVAP_DUMP)
storePacket(writeBuffer, writeLength);
#endif
int ret = m_serial.write(writeBuffer, writeLength);
if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the DVAP"));
wxLogWarning("Error when writing data to the DVAP");
writeLength = 0U;
space--;
}
}
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock();
}
wxLogMessage(wxT("Stopping DVAP Controller thread"));
wxLogMessage("Stopping DVAP Controller thread");
stopDVAP();
delete[] writeBuffer;
m_serial.close();
return NULL;
}
bool CDVAPController::writeHeader(const CHeaderData& header)
{
bool ret = m_txData.hasSpace(DVAP_HEADER_LEN + 1U);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -414,8 +423,8 @@ bool CDVAPController::writeHeader(const CHeaderData& header)
::memcpy(buffer, DVAP_HEADER, DVAP_HEADER_LEN);
wxUint16 sid = wxUINT16_SWAP_ON_BE(m_streamId);
::memcpy(buffer + 2U, &sid, sizeof(wxUint16));
uint16_t sid = htole16(m_streamId);
::memcpy(buffer + 2U, &sid, sizeof(uint16_t));
buffer[4U] = 0x80U;
buffer[5U] = 0U;
@ -426,25 +435,25 @@ bool CDVAPController::writeHeader(const CHeaderData& header)
buffer[7U] = header.getFlag2();
buffer[8U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 9U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 9U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 17U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 17U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 25U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 25U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 33U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 33U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 41U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 41U] = my2[i];
CCCITTChecksumReverse cksum;
cksum.update(buffer + 6U, RADIO_HEADER_LENGTH_BYTES - 2U);
@ -453,7 +462,7 @@ bool CDVAPController::writeHeader(const CHeaderData& header)
m_framePos = 0U;
m_seq = 0U;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = DVAP_HEADER_LEN;
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);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
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)
m_framePos = 0U;
wxUint16 sid = wxUINT16_SWAP_ON_BE(m_streamId);
::memcpy(buffer + 2U, &sid, sizeof(wxUint16));
uint16_t sid = htole16(m_streamId);
::memcpy(buffer + 2U, &sid, sizeof(uint16_t));
buffer[4U] = m_framePos;
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);
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = DVAP_GMSK_DATA_LEN;
m_txData.addData(&len, 1U);
@ -545,14 +554,14 @@ bool CDVAPController::getName()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_NAME) {
count++;
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;
}
}
@ -560,7 +569,7 @@ bool CDVAPController::getName()
bool cmp = ::memcmp(m_buffer, DVAP_RESP_NAME, length) == 0;
if (!cmp) {
wxLogError(wxT("The Dongle is not responding as a DVAP"));
wxLogError("The Dongle is not responding as a DVAP");
return false;
}
@ -579,21 +588,21 @@ bool CDVAPController::getFirmware()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_FIRMWARE) {
count++;
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;
}
}
} while (resp != RT_FIRMWARE);
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;
}
@ -610,21 +619,21 @@ bool CDVAPController::getSerial()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_SERIAL) {
count++;
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;
}
}
} while (resp != RT_SERIAL);
wxString serial((char*)(m_buffer + 4U), wxConvLocal, length - 5U);
wxLogInfo(wxT("DVAP Serial number: %s"), serial.c_str());
std::string serial((char*)(m_buffer + 4U), length - 5U);
wxLogInfo("DVAP Serial number: %s", serial.c_str());
return true;
}
@ -641,14 +650,14 @@ bool CDVAPController::startDVAP()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_START) {
count++;
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;
}
}
@ -669,14 +678,14 @@ bool CDVAPController::stopDVAP()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_STOP) {
count++;
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;
}
}
@ -697,14 +706,14 @@ bool CDVAPController::setModulation()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_MODULATION) {
count++;
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;
}
}
@ -725,14 +734,14 @@ bool CDVAPController::setMode()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_MODE) {
count++;
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;
}
}
@ -749,7 +758,7 @@ bool CDVAPController::setSquelch()
do {
unsigned char buffer[10U];
::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);
if (ret != int(DVAP_REQ_SQUELCH_LEN)) {
@ -757,14 +766,14 @@ bool CDVAPController::setSquelch()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_SQUELCH) {
count++;
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;
}
}
@ -782,8 +791,8 @@ bool CDVAPController::setPower()
unsigned char buffer[10U];
::memcpy(buffer, DVAP_REQ_POWER, DVAP_REQ_POWER_LEN);
wxInt16 power = wxINT16_SWAP_ON_BE(m_power);
::memcpy(buffer + 4U, &power, sizeof(wxInt16));
int16_t power = htole16(m_power);
::memcpy(buffer + 4U, &power, sizeof(int16_t));
int ret = m_serial.write(buffer, DVAP_REQ_POWER_LEN);
if (ret != int(DVAP_REQ_POWER_LEN)) {
@ -791,14 +800,14 @@ bool CDVAPController::setPower()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_POWER) {
count++;
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;
}
}
@ -819,29 +828,29 @@ bool CDVAPController::setFrequency()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_FREQLIMITS) {
count++;
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;
}
}
} while (resp != RT_FREQLIMITS);
wxUint32* pFreq1 = (wxUint32*)(m_buffer + 4U);
wxUint32* pFreq2 = (wxUint32*)(m_buffer + 8U);
uint32_t* pFreq1 = (uint32_t*)(m_buffer + 4U);
uint32_t* pFreq2 = (uint32_t*)(m_buffer + 8U);
wxUint32 lower = wxUINT32_SWAP_ON_BE(*pFreq1);
wxUint32 upper = wxUINT32_SWAP_ON_BE(*pFreq2);
uint32_t lower = le32toh(*pFreq1);
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) {
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();
return false;
}
@ -851,8 +860,8 @@ bool CDVAPController::setFrequency()
unsigned char buffer[10U];
::memcpy(buffer, DVAP_REQ_FREQUENCY, DVAP_REQ_FREQUENCY_LEN);
wxUint32 frequency = wxUINT32_SWAP_ON_BE(m_frequency);
::memcpy(buffer + 4U, &frequency, sizeof(wxUint32));
uint32_t frequency = htole32(m_frequency);
::memcpy(buffer + 4U, &frequency, sizeof(uint32_t));
int ret = m_serial.write(buffer, DVAP_REQ_FREQUENCY_LEN);
if (ret != int(DVAP_REQ_FREQUENCY_LEN)) {
@ -860,14 +869,14 @@ bool CDVAPController::setFrequency()
return false;
}
::wxMilliSleep(50UL);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
resp = getResponse(m_buffer, length);
if (resp != RT_FREQUENCY) {
count++;
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;
}
}
@ -876,6 +885,16 @@ bool CDVAPController::setFrequency()
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)
{
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
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)
dumpPackets();
#endif
@ -912,11 +931,14 @@ RESP_TYPE CDVAPController::getResponse(unsigned char *buffer, unsigned int& leng
return RT_ERROR;
if (ret > 0)
offset += ret;
if (ret == 0)
Sleep(5UL);
if (ret == 0) {
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)
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)
return RT_SQUELCH;
else {
CUtils::dump(wxT("Bad DVAP data"), buffer, length);
CUtils::dump("Bad DVAP data", buffer, length);
#if defined(DVAP_DUMP)
dumpPackets();
#endif
@ -964,30 +986,41 @@ 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()
{
wxLogWarning(wxT("Resynchronising the DVAP data stream"));
wxLogWarning("Resynchronising the DVAP data stream");
unsigned char data[DVAP_STATUS_LEN];
::memset(data, 0x00U, DVAP_STATUS_LEN);
while (::memcmp(data, DVAP_STATUS, 4U) != 0) {
if (m_stopped)
return;
unsigned char c;
int n = m_serial.read(&c, 1U);
if (n > 0) {
data[0U] = data[1U];
data[1U] = data[2U];
data[2U] = data[3U];
data[3U] = data[4U];
data[4U] = data[5U];
data[5U] = data[6U];
data[6U] = c;
// CUtils::dump(wxT("Resync buffer"), data, DVAP_STATUS_LEN);
if (n < 0)
return;
if (n == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
continue;
}
data[0U] = data[1U];
data[1U] = data[2U];
data[2U] = data[3U];
data[3U] = data[4U];
data[4U] = data[5U];
data[5U] = data[6U];
data[6U] = c;
// CUtils::dump("Resync buffer", data, DVAP_STATUS_LEN);
}
wxLogMessage(wxT("End resynchronising"));
wxLogMessage("End resynchronising");
}
#if defined(DVAP_DUMP)
@ -1004,22 +1037,15 @@ void CDVAPController::storePacket(const unsigned char* data, unsigned int length
void CDVAPController::dumpPackets()
{
unsigned int n = m_dvapIndex;
unsigned int i = 0U;
while (n != m_dvapIndex) {
unsigned int n = (m_dvapIndex + 1U) % DVAP_DUMP_LENGTH;
for (unsigned int i = 0U; i < DVAP_DUMP_LENGTH; i++) {
if (m_dvapLength[n] > 0U) {
wxString text;
text.Printf(wxT("Packet: %u"), i);
char text[32];
::snprintf(text, sizeof(text), "Packet: %u", i);
CUtils::dump(text, m_dvapData[n], m_dvapLength[n]);
}
i++;
n++;
if (n >= DVAP_DUMP_LENGTH)
n = 0U;
n = (n + 1U) % DVAP_DUMP_LENGTH;
}
m_dvapIndex = 0U;
}

@ -26,8 +26,10 @@
#include "HeaderData.h"
#include "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
#include <cstdint>
enum RESP_TYPE {
RT_TIMEOUT,
@ -53,13 +55,44 @@ enum RESP_TYPE {
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 {
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 void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -72,16 +105,18 @@ public:
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
private:
void entry();
CSerialDataController m_serial;
wxUint32 m_frequency;
wxInt16 m_power;
wxInt8 m_squelch;
bool m_squelchOpen;
int m_signal;
uint32_t m_frequency;
int16_t m_power;
int8_t m_squelch;
std::atomic<bool> m_squelchOpen;
std::atomic<int> m_signal;
unsigned char* m_buffer;
wxUint16 m_streamId;
wxUint8 m_framePos;
wxUint8 m_seq;
uint16_t m_streamId;
uint8_t m_framePos;
uint8_t m_seq;
CRingBuffer<unsigned char> m_txData;
#if defined(DVAP_DUMP)
unsigned char** m_dvapData;
@ -103,10 +138,17 @@ private:
bool startDVAP();
bool stopDVAP();
// Sends a 3-byte keepalive ACK; DVAP requires periodic traffic to stay active.
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();
// 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);
#if defined(DVAP_DUMP)

@ -20,13 +20,19 @@
#include "DVMegaController.h"
#include "CCITTChecksum.h"
#include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h"
#include "Utils.h"
#if defined(__WINDOWS__)
#include <setupapi.h>
#else
#include <wx/dir.h>
#include <chrono>
#include <thread>
#include <cassert>
#include <cstring>
#include <cstdio>
#include "EndianCompat.h"
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif
const unsigned char DVRPTR_HEADER_LENGTH = 5U;
@ -57,7 +63,7 @@ const unsigned int MAX_RESPONSES = 30U;
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(),
m_port(port),
m_path(path),
@ -68,7 +74,7 @@ m_rxFrequency(0U),
m_txFrequency(0U),
m_power(0U),
m_serial(port, SERIAL_115200, true),
m_buffer(NULL),
m_buffer(nullptr),
m_txData(1000U),
m_txCounter(0U),
m_pktCounter(0U),
@ -77,12 +83,12 @@ m_txSpace(0U),
m_txEnabled(false),
m_checksum(false)
{
wxASSERT(!port.IsEmpty());
assert(!port.empty());
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(),
m_port(port),
m_path(path),
@ -93,7 +99,7 @@ m_rxFrequency(rxFrequency),
m_txFrequency(txFrequency),
m_power(power),
m_serial(port, SERIAL_115200, true),
m_buffer(NULL),
m_buffer(nullptr),
m_txData(1000U),
m_txCounter(0U),
m_pktCounter(0U),
@ -102,11 +108,11 @@ m_txSpace(0U),
m_txEnabled(false),
m_checksum(false)
{
wxASSERT(!port.IsEmpty());
wxASSERT((rxFrequency >= 144000000U && rxFrequency <= 148000000U) ||
(rxFrequency >= 420000000U && rxFrequency <= 450000000U));
wxASSERT((txFrequency >= 144000000U && txFrequency <= 148000000U) ||
(txFrequency >= 420000000U && txFrequency <= 450000000U));
assert(!port.empty());
assert((rxFrequency >= 144000000U && rxFrequency <= 148000000U) ||
(rxFrequency >= 420000000U && rxFrequency <= 450000000U));
assert((txFrequency >= 144000000U && txFrequency <= 148000000U) ||
(txFrequency >= 420000000U && txFrequency <= 450000000U));
m_buffer = new unsigned char[BUFFER_LENGTH];
}
@ -126,16 +132,14 @@ bool CDVMegaController::start()
findPath();
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CDVMegaController::entry, this);
return true;
}
void* CDVMegaController::Entry()
void CDVMegaController::entry()
{
wxLogMessage(wxT("Starting DVMEGA Controller thread"));
wxLogMessage("Starting DVMEGA Controller thread");
// Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U);
@ -154,8 +158,9 @@ void* CDVMegaController::Entry()
if (!ret) {
ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DVMEGA Controller thread"));
return NULL;
wxLogMessage("Stopping DVMEGA Controller thread");
delete[] writeBuffer;
return;
}
}
@ -172,29 +177,30 @@ void* CDVMegaController::Entry()
case RTM_ERROR: {
bool ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DVMEGA Controller thread"));
return NULL;
wxLogMessage("Stopping DVMEGA Controller thread");
delete[] writeBuffer;
return;
}
}
break;
case RTM_RXPREAMBLE:
// wxLogMessage(wxT("RT_PREAMBLE"));
// wxLogMessage("RT_PREAMBLE");
break;
case RTM_START:
// wxLogMessage(wxT("RT_START"));
// wxLogMessage("RT_START");
break;
case RTM_HEADER:
CUtils::dump(wxT("RT_HEADER"), m_buffer, length);
CUtils::dump("RT_HEADER", m_buffer, length);
if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a header NAK from the DVMEGA"));
wxLogWarning("Received a header NAK from the DVMEGA");
} else {
bool correct = (m_buffer[5U] & 0x80U) == 0x00U;
if (correct) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_HEADER;
@ -209,16 +215,16 @@ void* CDVMegaController::Entry()
break;
case RTM_RXSYNC:
// wxLogMessage(wxT("RT_RXSYNC"));
// wxLogMessage("RT_RXSYNC");
break;
case RTM_DATA:
CUtils::dump(wxT("RT_DATA"), m_buffer, length);
CUtils::dump("RT_DATA", m_buffer, length);
if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a data NAK from the DVMEGA"));
wxLogWarning("Received a data NAK from the DVMEGA");
} else {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_DATA;
@ -232,8 +238,8 @@ void* CDVMegaController::Entry()
break;
case RTM_EOT: {
// wxLogMessage(wxT("RT_EOT"));
wxMutexLocker locker(m_mutex);
// wxLogMessage("RT_EOT");
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -245,8 +251,8 @@ void* CDVMegaController::Entry()
break;
case RTM_RXLOST: {
// wxLogMessage(wxT("RT_LOST"));
wxMutexLocker locker(m_mutex);
// wxLogMessage("RT_LOST");
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_LOST;
@ -263,8 +269,8 @@ void* CDVMegaController::Entry()
m_tx = (m_buffer[5U] & 0x02U) == 0x02U;
m_txSpace = m_buffer[8U];
space = m_txSpace - m_buffer[9U];
// CUtils::dump(wxT("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));
// CUtils::dump("GET_STATUS", m_buffer, length);
// 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;
@ -275,14 +281,14 @@ void* CDVMegaController::Entry()
break;
default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[3U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length);
wxLogMessage("Unknown message, type: %02X", m_buffer[3U]);
CUtils::dump("Buffer dump", m_buffer, length);
break;
}
if (space > 0U) {
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(&writeLength, 1U);
@ -291,53 +297,51 @@ void* CDVMegaController::Entry()
// Only send the start when the TX is off
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);
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;
space--;
}
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);
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;
space -= 4U;
}
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);
if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the DVMEGA"));
wxLogWarning("Error when writing data to the DVMEGA");
writeType = DSMTT_NONE;
space--;
}
}
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock();
}
wxLogMessage(wxT("Stopping DVMEGA Controller thread"));
wxLogMessage("Stopping DVMEGA Controller thread");
setEnabled(false);
delete[] writeBuffer;
m_serial.close();
return NULL;
}
bool CDVMegaController::writeHeader(const CHeaderData& header)
@ -347,7 +351,7 @@ bool CDVMegaController::writeHeader(const CHeaderData& header)
bool ret = m_txData.hasSpace(64U);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -397,25 +401,25 @@ bool CDVMegaController::writeHeader(const CHeaderData& header)
buffer2[9U] = header.getFlag2();
buffer2[10U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2[i];
CCCITTChecksumReverse cksum1;
cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U);
@ -434,7 +438,7 @@ bool CDVMegaController::writeHeader(const CHeaderData& header)
m_pktCounter = 0U;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type1 = DSMTT_START;
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);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
return false;
}
@ -488,7 +492,7 @@ bool CDVMegaController::writeData(const unsigned char* data, unsigned int, bool
buffer[7U] = 0x0BU;
}
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_EOT;
m_txData.addData(&type, 1U);
@ -532,7 +536,7 @@ bool CDVMegaController::writeData(const unsigned char* data, unsigned int, bool
buffer[23U] = 0x0BU;
}
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_DATA;
m_txData.addData(&type, 1U);
@ -579,36 +583,43 @@ bool CDVMegaController::readVersion()
buffer[5U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 6U);
// CUtils::dump("Written", buffer, 6U);
int ret = m_serial.write(buffer, 6U);
if (ret != 6)
return false;
for (unsigned int count = 0U; count < MAX_RESPONSES; count++) {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
unsigned int length;
RESP_TYPE_MEGA resp = getResponse(m_buffer, length);
if (resp == RTM_GET_VERSION) {
wxString firmware;
char firmware[32];
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
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;
}
}
::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;
}
@ -657,8 +668,8 @@ bool CDVMegaController::setConfig()
if (m_txInvert)
buffer[6U] |= 0x02U;
wxUint16* txDelay = (wxUint16*)(buffer + 8U);
*txDelay = wxUINT16_SWAP_ON_BE(m_txDelay);
uint16_t txDelay = htole16((uint16_t)m_txDelay);
::memcpy(buffer + 8U, &txDelay, sizeof(uint16_t));
if (m_checksum) {
CCCITTChecksum cksum;
@ -669,7 +680,7 @@ bool CDVMegaController::setConfig()
buffer[11U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 12U);
// CUtils::dump("Written", buffer, 12U);
int ret = m_serial.write(buffer, 12U);
if (ret != 12)
@ -680,24 +691,24 @@ bool CDVMegaController::setConfig()
RESP_TYPE_MEGA resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RTM_SET_CONFIG) {
count++;
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;
}
}
} while (resp != RTM_SET_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length);
// CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U];
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;
}
@ -721,11 +732,11 @@ bool CDVMegaController::setFrequencyAndPower()
buffer[5U] = 0x0CU; // Block length
wxUint32 rxFreq = wxUINT32_SWAP_ON_BE(wxUint32(m_rxFrequency));
wxUint32 txFreq = wxUINT32_SWAP_ON_BE(wxUint32(m_txFrequency));
uint32_t rxFreq = htole32((uint32_t)m_rxFrequency);
uint32_t txFreq = htole32((uint32_t)m_txFrequency);
::memcpy(buffer + 7U, &rxFreq, sizeof(wxUint32));
::memcpy(buffer + 11U, &txFreq, sizeof(wxUint32));
::memcpy(buffer + 7U, &rxFreq, sizeof(uint32_t));
::memcpy(buffer + 11U, &txFreq, sizeof(uint32_t));
buffer[16U] = (m_power * 64U) / 100U;
@ -738,7 +749,7 @@ bool CDVMegaController::setFrequencyAndPower()
buffer[20U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 21U);
// CUtils::dump("Written", buffer, 21U);
int ret = m_serial.write(buffer, 21U);
if (ret != 21)
@ -749,24 +760,24 @@ bool CDVMegaController::setFrequencyAndPower()
RESP_TYPE_MEGA resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RTM_SET_CONFIG) {
count++;
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;
}
}
} while (resp != RTM_SET_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length);
// CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U];
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;
}
@ -799,7 +810,7 @@ bool CDVMegaController::setEnabled(bool enable)
buffer[6U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 7U);
// CUtils::dump("Written", buffer, 7U);
int ret = m_serial.write(buffer, 7U);
if (ret != 7)
@ -809,36 +820,41 @@ bool CDVMegaController::setEnabled(bool enable)
unsigned int length;
RESP_TYPE_MEGA resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RTM_GET_STATUS) {
count++;
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;
}
}
} while (resp != RTM_GET_STATUS);
// CUtils::dump(wxT("Response"), m_buffer, length);
// CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U];
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 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)
{
// Get the start of the frame or nothing at all
int ret = m_serial.read(buffer, 1U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DVMEGA"));
wxLogError("Error when reading from the DVMEGA");
return RTM_ERROR;
}
@ -853,21 +869,24 @@ RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned in
while (offset < DVRPTR_HEADER_LENGTH) {
ret = m_serial.read(buffer + offset, DVRPTR_HEADER_LENGTH - offset);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DVMEGA"));
wxLogError("Error when reading from the DVMEGA");
return RTM_ERROR;
}
if (ret > 0)
offset += ret;
if (ret == 0)
Sleep(5UL);
if (ret == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (m_stopped)
return RTM_TIMEOUT;
}
}
length = buffer[1U] + buffer[2U] * 256U;
if (length >= 100U) {
wxLogError(wxT("Invalid data received from the DVMEGA"));
wxLogError("Invalid data received from the DVMEGA");
return RTM_ERROR;
}
@ -879,20 +898,23 @@ RESP_TYPE_MEGA CDVMegaController::getResponse(unsigned char *buffer, unsigned in
while (offset < length) {
ret = m_serial.read(buffer + offset + DVRPTR_HEADER_LENGTH, length - offset);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DVMEGA"));
wxLogError("Error when reading from the DVMEGA");
return RTM_ERROR;
}
if (ret > 0)
offset += ret;
if (ret == 0)
Sleep(5UL);
if (ret == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (m_stopped)
return RTM_TIMEOUT;
}
}
length += DVRPTR_HEADER_LENGTH;
// CUtils::dump(wxT("Received"), buffer, length);
// CUtils::dump("Received", buffer, length);
switch (type) {
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;
}
bool CDVMegaController::findPort()
{
if (m_path.IsEmpty())
if (m_path.empty())
return false;
#if defined(__WINDOWS__)
#else
wxDir dir;
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
DIR* dir = ::opendir("/sys/class/tty");
if (dir == nullptr) {
wxLogError("Cannot open directory /sys/class/tty");
return false;
}
wxString fileName;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*"));
while (ret1) {
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
struct dirent* entry;
while ((entry = ::readdir(dir)) != nullptr) {
std::string fileName(entry->d_name);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
// Match ttyACM* entries
if (fileName.substr(0, 6) != "ttyACM")
continue;
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
std::string path = "/sys/class/tty/" + fileName;
char cpath[255U];
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U);
int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
::strcat(cpath, "/device");
ret2 = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
::closedir(dir);
return false;
}
path = wxString(symlink, wxConvLocal, ret2);
symlink[ret2] = '\0';
path = std::string(symlink, ret2);
} else {
symlink[ret2] = '\0';
std::string fullPath(symlink, ret2);
// Get all but the last section
wxString fullPath = wxString(symlink, wxConvLocal, 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)) {
m_port.Printf(wxT("/dev/%s"), fileName.c_str());
if (path == m_path) {
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;
}
ret1 = dir.GetNext(&fileName);
}
#endif
::closedir(dir);
return false;
}
bool CDVMegaController::findPath()
{
#if defined(__WINDOWS__)
#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());
std::string path = "/sys/class/tty/" + m_port.substr(5U);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U);
int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
::strcat(cpath, "/device");
ret = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
return false;
}
path = wxString(symlink, wxConvLocal, ret);
symlink[ret] = '\0';
path = std::string(symlink, ret);
} else {
wxString fullPath = wxString(symlink, wxConvLocal, ret);
path = fullPath.BeforeLast(wxT('/'));
symlink[ret] = '\0';
std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
}
if (m_path.IsEmpty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str());
if (m_path.empty())
wxLogMessage("Found modem path of %s", path.c_str());
m_path = path;
#endif
return true;
}
@ -1079,7 +1052,7 @@ bool CDVMegaController::findModem()
// Tell the repeater that the signal has gone away
if (m_rx) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -1095,7 +1068,7 @@ bool CDVMegaController::findModem()
while (!m_stopped) {
count++;
if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem"));
wxLogMessage("Trying to reopen the modem");
bool ret = findPort();
if (ret) {
@ -1107,7 +1080,7 @@ bool CDVMegaController::findModem()
count = 0U;
}
Sleep(500UL);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return false;
@ -1147,4 +1120,3 @@ bool CDVMegaController::openModem()
return true;
}

@ -23,8 +23,9 @@
#include "RingBuffer.h"
#include "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
enum RESP_TYPE_MEGA {
RTM_TIMEOUT,
@ -45,14 +46,47 @@ enum RESP_TYPE_MEGA {
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 {
public:
CDVMegaController(const wxString& port, const wxString& path, bool rxInvert, bool txInvert, unsigned int txDelay);
CDVMegaController(const wxString& port, const wxString& path, unsigned int txDelay, unsigned int rxFrequency, unsigned int txFrequency, unsigned int power);
// Modem-only variant: no integrated RF, host radio provides the signal.
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 void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -61,11 +95,13 @@ public:
virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const;
virtual std::string getPath() const;
private:
wxString m_port;
wxString m_path;
void entry();
std::string m_port;
std::string m_path;
bool m_rxInvert;
bool m_txInvert;
unsigned int m_txDelay;
@ -93,9 +129,12 @@ private:
bool findPort();
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();
// Opens the serial port, sends GET_VERSION, SET_CONFIG (physical layer),
// optionally SET_CONFIG (RF layer for radio variant), then setEnabled(true).
bool openModem();
};
#endif

@ -20,12 +20,18 @@
#include "DVRPTRV1Controller.h"
#include "CCITTChecksum.h"
#include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h"
#if defined(__WINDOWS__)
#include <setupapi.h>
#else
#include <wx/dir.h>
#include <chrono>
#include <thread>
#include <cassert>
#include <cstring>
#include <cstdio>
#include "EndianCompat.h"
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif
const unsigned char DVRPTR_HEADER_LENGTH = 5U;
@ -56,7 +62,7 @@ const unsigned int MAX_RESPONSES = 30U;
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(),
m_port(port),
m_path(path),
@ -66,7 +72,7 @@ m_channel(channel),
m_modLevel(modLevel),
m_txDelay(txDelay),
m_serial(port, SERIAL_115200),
m_buffer(NULL),
m_buffer(nullptr),
m_txData(1000U),
m_txCounter(0U),
m_pktCounter(0U),
@ -75,7 +81,7 @@ m_txSpace(0U),
m_txEnabled(false),
m_checksum(false)
{
wxASSERT(!port.IsEmpty());
assert(!port.empty());
m_buffer = new unsigned char[BUFFER_LENGTH];
}
@ -95,16 +101,14 @@ bool CDVRPTRV1Controller::start()
findPath();
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CDVRPTRV1Controller::entry, this);
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
CTimer pollTimer(200U, 0U, 100U);
@ -123,8 +127,9 @@ void* CDVRPTRV1Controller::Entry()
if (!ret) {
ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR1 Modem Controller thread"));
return NULL;
wxLogMessage("Stopping DV-RPTR1 Modem Controller thread");
delete[] writeBuffer;
return;
}
}
@ -141,29 +146,30 @@ void* CDVRPTRV1Controller::Entry()
case RT1_ERROR: {
bool ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR1 Modem Controller thread"));
return NULL;
wxLogMessage("Stopping DV-RPTR1 Modem Controller thread");
delete[] writeBuffer;
return;
}
}
break;
case RT1_RXPREAMBLE:
// wxLogMessage(wxT("RT_PREAMBLE"));
// wxLogMessage("RT_PREAMBLE");
break;
case RT1_START:
// wxLogMessage(wxT("RT_START"));
// wxLogMessage("RT_START");
break;
case RT1_HEADER:
// CUtils::dump(wxT("RT_HEADER"), m_buffer, length);
// CUtils::dump("RT_HEADER", m_buffer, length);
if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a header NAK from the modem"));
wxLogWarning("Received a header NAK from the modem");
} else {
bool correct = (m_buffer[5U] & 0x80U) == 0x00U;
if (correct) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_HEADER;
@ -178,16 +184,16 @@ void* CDVRPTRV1Controller::Entry()
break;
case RT1_RXSYNC:
// wxLogMessage(wxT("RT_RXSYNC"));
// wxLogMessage("RT_RXSYNC");
break;
case RT1_DATA:
// CUtils::dump(wxT("RT_DATA"), m_buffer, length);
// CUtils::dump("RT_DATA", m_buffer, length);
if (length == 7U) {
if (m_buffer[4U] == DVRPTR_NAK)
wxLogWarning(wxT("Received a data NAK from the modem"));
wxLogWarning("Received a data NAK from the modem");
} else {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_DATA;
@ -201,8 +207,8 @@ void* CDVRPTRV1Controller::Entry()
break;
case RT1_EOT: {
// wxLogMessage(wxT("RT_EOT"));
wxMutexLocker locker(m_mutex);
// wxLogMessage("RT_EOT");
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -214,8 +220,8 @@ void* CDVRPTRV1Controller::Entry()
break;
case RT1_RXLOST: {
// wxLogMessage(wxT("RT_LOST"));
wxMutexLocker locker(m_mutex);
// wxLogMessage("RT_LOST");
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_LOST;
@ -232,8 +238,8 @@ void* CDVRPTRV1Controller::Entry()
m_tx = (m_buffer[5U] & 0x02U) == 0x02U;
m_txSpace = m_buffer[8U];
space = m_txSpace - m_buffer[9U];
// CUtils::dump(wxT("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));
// CUtils::dump("GET_STATUS", m_buffer, length);
// 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;
@ -244,14 +250,14 @@ void* CDVRPTRV1Controller::Entry()
break;
default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[3U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length);
wxLogMessage("Unknown message, type: %02X", m_buffer[3U]);
CUtils::dump("Buffer dump", m_buffer, length);
break;
}
if (space > 0U) {
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(&writeLength, 1U);
@ -260,53 +266,51 @@ void* CDVRPTRV1Controller::Entry()
// Only send the start when the TX is off
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);
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;
space--;
}
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);
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;
space -= 4U;
}
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);
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;
space--;
}
}
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock();
}
wxLogMessage(wxT("Stopping DV-RPTR1 Modem Controller thread"));
wxLogMessage("Stopping DV-RPTR1 Modem Controller thread");
setEnabled(false);
delete[] writeBuffer;
m_serial.close();
return NULL;
}
bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
@ -316,7 +320,7 @@ bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
bool ret = m_txData.hasSpace(64U);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -366,25 +370,25 @@ bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
buffer2[9U] = header.getFlag2();
buffer2[10U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 11U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 19U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 27U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer2[i + 35U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer2[i + 43U] = my2[i];
CCCITTChecksumReverse cksum1;
cksum1.update(buffer2 + 8U, RADIO_HEADER_LENGTH_BYTES - 2U);
@ -403,7 +407,7 @@ bool CDVRPTRV1Controller::writeHeader(const CHeaderData& header)
m_pktCounter = 0U;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type1 = DSMTT_START;
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);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
return false;
}
@ -457,7 +461,7 @@ bool CDVRPTRV1Controller::writeData(const unsigned char* data, unsigned int, boo
buffer[7U] = 0x0BU;
}
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_EOT;
m_txData.addData(&type, 1U);
@ -501,7 +505,7 @@ bool CDVRPTRV1Controller::writeData(const unsigned char* data, unsigned int, boo
buffer[23U] = 0x0BU;
}
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_DATA;
m_txData.addData(&type, 1U);
@ -547,7 +551,7 @@ bool CDVRPTRV1Controller::readVersion()
buffer[5U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 6U);
// CUtils::dump("Written", buffer, 6U);
int ret = m_serial.write(buffer, 6U);
if (ret != 6)
@ -557,28 +561,35 @@ bool CDVRPTRV1Controller::readVersion()
unsigned int length;
RESP_TYPE_V1 resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RT1_GET_VERSION) {
count++;
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;
}
}
} while (resp != RT1_GET_VERSION);
wxString firmware;
char firmware[32];
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
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;
}
@ -606,6 +617,10 @@ bool CDVRPTRV1Controller::readStatus()
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()
{
unsigned char buffer[20U];
@ -631,8 +646,8 @@ bool CDVRPTRV1Controller::setConfig()
buffer[7U] = (m_modLevel * 256U) / 100U;
wxUint16* txDelay = (wxUint16*)(buffer + 8U);
*txDelay = wxUINT16_SWAP_ON_BE(m_txDelay);
uint16_t txDelay = htole16((uint16_t)m_txDelay);
::memcpy(buffer + 8U, &txDelay, sizeof(uint16_t));
if (m_checksum) {
CCCITTChecksum cksum;
@ -643,7 +658,7 @@ bool CDVRPTRV1Controller::setConfig()
buffer[11U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 12U);
// CUtils::dump("Written", buffer, 12U);
int ret = m_serial.write(buffer, 12U);
if (ret != 12)
@ -654,24 +669,24 @@ bool CDVRPTRV1Controller::setConfig()
RESP_TYPE_V1 resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RT1_SET_CONFIG) {
count++;
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;
}
}
} while (resp != RT1_SET_CONFIG);
// CUtils::dump(wxT("Response"), m_buffer, length);
// CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U];
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;
}
@ -704,7 +719,7 @@ bool CDVRPTRV1Controller::setEnabled(bool enable)
buffer[6U] = 0x0BU;
}
// CUtils::dump(wxT("Written"), buffer, 7U);
// CUtils::dump("Written", buffer, 7U);
int ret = m_serial.write(buffer, 7U);
if (ret != 7)
@ -714,24 +729,24 @@ bool CDVRPTRV1Controller::setEnabled(bool enable)
unsigned int length;
RESP_TYPE_V1 resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RT1_GET_STATUS) {
count++;
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;
}
}
} while (resp != RT1_GET_STATUS);
// CUtils::dump(wxT("Response"), m_buffer, length);
// CUtils::dump("Response", m_buffer, length);
unsigned char type = m_buffer[4U];
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;
}
@ -743,7 +758,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
// Get the start of the frame or nothing at all
int ret = m_serial.read(buffer, 1U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR"));
wxLogError("Error when reading from the DV-RPTR");
return RT1_ERROR;
}
@ -758,7 +773,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < DVRPTR_HEADER_LENGTH) {
ret = m_serial.read(buffer + offset, DVRPTR_HEADER_LENGTH - offset);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR"));
wxLogError("Error when reading from the DV-RPTR");
return RT1_ERROR;
}
@ -766,7 +781,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret;
if (ret == 0)
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
length = buffer[1U] + buffer[2U] * 256U;
@ -779,7 +794,7 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < length) {
ret = m_serial.read(buffer + offset + DVRPTR_HEADER_LENGTH, length - offset);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR"));
wxLogError("Error when reading from the DV-RPTR");
return RT1_ERROR;
}
@ -787,12 +802,12 @@ RESP_TYPE_V1 CDVRPTRV1Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret;
if (ret == 0)
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
length += DVRPTR_HEADER_LENGTH;
// CUtils::dump(wxT("Received"), buffer, length);
// CUtils::dump("Received", buffer, length);
switch (type) {
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;
}
bool CDVRPTRV1Controller::findPort()
{
if (m_path.IsEmpty())
if (m_path.empty())
return false;
#if defined(__WINDOWS__)
#else
wxDir dir;
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
DIR* dir = ::opendir("/sys/class/tty");
if (dir == nullptr) {
wxLogError("Cannot open directory /sys/class/tty");
return false;
}
wxString fileName;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*"));
while (ret1) {
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
struct dirent* entry;
while ((entry = ::readdir(dir)) != nullptr) {
std::string fileName(entry->d_name);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
// Match ttyACM* entries
if (fileName.substr(0, 6) != "ttyACM")
continue;
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
std::string path = "/sys/class/tty/" + fileName;
char cpath[255U];
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U);
int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
::strcat(cpath, "/device");
ret2 = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
::closedir(dir);
return false;
}
path = wxString(symlink, wxConvLocal, ret2);
symlink[ret2] = '\0';
path = std::string(symlink, ret2);
} else {
symlink[ret2] = '\0';
std::string fullPath(symlink, ret2);
// Get all but the last section
wxString fullPath = wxString(symlink, wxConvLocal, 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)) {
m_port.Printf(wxT("/dev/%s"), fileName.c_str());
if (path == m_path) {
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;
}
ret1 = dir.GetNext(&fileName);
}
#endif
::closedir(dir);
return false;
}
bool CDVRPTRV1Controller::findPath()
{
#if defined(__WINDOWS__)
#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());
std::string path = "/sys/class/tty/" + m_port.substr(5U);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U);
int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
::strcat(cpath, "/device");
ret = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
return false;
}
path = wxString(symlink, wxConvLocal, ret);
symlink[ret] = '\0';
path = std::string(symlink, ret);
} else {
wxString fullPath = wxString(symlink, wxConvLocal, ret);
path = fullPath.BeforeLast(wxT('/'));
symlink[ret] = '\0';
std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
}
if (m_path.IsEmpty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str());
if (m_path.empty())
wxLogMessage("Found modem path of %s", path.c_str());
m_path = path;
#endif
return true;
}
@ -979,7 +945,7 @@ bool CDVRPTRV1Controller::findModem()
// Tell the repeater that the signal has gone away
if (m_rx) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -995,7 +961,7 @@ bool CDVRPTRV1Controller::findModem()
while (!m_stopped) {
count++;
if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem"));
wxLogMessage("Trying to reopen the modem");
bool ret = findPort();
if (ret) {
@ -1007,7 +973,7 @@ bool CDVRPTRV1Controller::findModem()
count = 0U;
}
Sleep(500UL);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return false;
@ -1039,4 +1005,3 @@ bool CDVRPTRV1Controller::openModem()
return true;
}

@ -23,8 +23,9 @@
#include "RingBuffer.h"
#include "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
enum RESP_TYPE_V1 {
RT1_TIMEOUT,
@ -45,13 +46,28 @@ enum RESP_TYPE_V1 {
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 {
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 void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -60,11 +76,13 @@ public:
virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const;
virtual std::string getPath() const;
private:
wxString m_port;
wxString m_path;
void entry();
std::string m_port;
std::string m_path;
bool m_rxInvert;
bool m_txInvert;
bool m_channel;
@ -95,4 +113,3 @@ private:
};
#endif

@ -18,19 +18,23 @@
#include "DVRPTRV2Controller.h"
#include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h"
#if defined(__WINDOWS__)
#include <setupapi.h>
#else
#include <wx/dir.h>
#include <chrono>
#include <thread>
#include <cassert>
#include <cstring>
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif
const unsigned int MAX_RESPONSES = 30U;
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(),
m_connection(CT_USB),
m_usbPort(port),
@ -42,20 +46,20 @@ m_modLevel(modLevel),
m_duplex(duplex),
m_callsign(callsign),
m_txDelay(txDelay),
m_usb(NULL),
m_network(NULL),
m_buffer(NULL),
m_usb(nullptr),
m_network(nullptr),
m_buffer(nullptr),
m_txData(1000U),
m_rx(false)
{
wxASSERT(!port.IsEmpty());
assert(!port.empty());
m_usb = new CSerialDataController(port, SERIAL_115200);
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(),
m_connection(CT_NETWORK),
m_usbPort(),
@ -67,14 +71,14 @@ m_modLevel(modLevel),
m_duplex(duplex),
m_callsign(callsign),
m_txDelay(txDelay),
m_usb(NULL),
m_network(NULL),
m_buffer(NULL),
m_usb(nullptr),
m_network(nullptr),
m_buffer(nullptr),
m_txData(1000U),
m_rx(false)
{
wxASSERT(!address.IsEmpty());
wxASSERT(port > 0U);
assert(!address.empty());
assert(port > 0U);
m_network = new CTCPReaderWriter(address, port);
@ -99,16 +103,14 @@ bool CDVRPTRV2Controller::start()
findPath();
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CDVRPTRV2Controller::entry, this);
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
CTimer pollTimer(200U, 0U, 250U);
@ -123,8 +125,8 @@ void* CDVRPTRV2Controller::Entry()
if (!ret) {
ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread"));
return NULL;
wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
return;
}
}
@ -141,15 +143,15 @@ void* CDVRPTRV2Controller::Entry()
case RT2_ERROR: {
bool ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread"));
return NULL;
wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
return;
}
}
break;
case RT2_HEADER: {
// CUtils::dump(wxT("RT2_HEADER"), m_buffer, length);
wxMutexLocker locker(m_mutex);
// CUtils::dump("RT2_HEADER", m_buffer, length);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_HEADER;
@ -174,8 +176,8 @@ void* CDVRPTRV2Controller::Entry()
break;
case RT2_DATA: {
// CUtils::dump(wxT("RT2_DATA"), m_buffer, length);
wxMutexLocker locker(m_mutex);
// CUtils::dump("RT2_DATA", m_buffer, length);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_DATA;
@ -200,7 +202,7 @@ void* CDVRPTRV2Controller::Entry()
case RT2_SPACE:
space = m_buffer[9U];
// CUtils::dump(wxT("RT2_SPACE"), m_buffer, length);
// CUtils::dump("RT2_SPACE", m_buffer, length);
break;
// These should not be received in this loop, but don't complain if we do
@ -209,8 +211,8 @@ void* CDVRPTRV2Controller::Entry()
break;
default:
wxLogMessage(wxT("Unknown DV-RPTR2 message, type"));
CUtils::dump(wxT("Buffer dump"), m_buffer, length);
wxLogMessage("Unknown DV-RPTR2 message, type");
CUtils::dump("Buffer dump", m_buffer, length);
break;
}
@ -220,20 +222,20 @@ void* CDVRPTRV2Controller::Entry()
unsigned char data[200U];
{
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_txData.getData(&len, 1U);
m_txData.getData(data, len);
}
// CUtils::dump(wxT("Write"), data, len);
// CUtils::dump("Write", data, len);
bool ret = writeModem(data, len);
if (!ret) {
ret = findModem();
if (!ret) {
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread"));
return NULL;
wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
return;
}
} else {
if (len > 100U)
@ -244,23 +246,21 @@ void* CDVRPTRV2Controller::Entry()
}
}
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock();
}
wxLogMessage(wxT("Stopping DV-RPTR2 Modem Controller thread"));
wxLogMessage("Stopping DV-RPTR2 Modem Controller thread");
closeModem();
return NULL;
}
bool CDVRPTRV2Controller::writeHeader(const CHeaderData& header)
{
bool ret = m_txData.hasSpace(106U);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -284,27 +284,27 @@ bool CDVRPTRV2Controller::writeHeader(const CHeaderData& header)
buffer[10U] = header.getFlag2();
buffer[11U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 12U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 12U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 20U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 20U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 28U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 28U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 36U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 36U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 44U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 44U] = my2[i];
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = 105U;
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);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
return false;
}
@ -344,7 +344,7 @@ bool CDVRPTRV2Controller::writeData(const unsigned char* data, unsigned int, boo
m_tx = false;
}
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char len = 17U;
m_txData.addData(&len, 1U);
@ -380,7 +380,7 @@ bool CDVRPTRV2Controller::readSerial()
buffer[7U] = '0';
buffer[8U] = '0';
// CUtils::dump(wxT("Written"), buffer, 105U);
// CUtils::dump("Written", buffer, 105U);
bool ret = writeModem(buffer, 105U);
if (!ret)
@ -390,20 +390,20 @@ bool CDVRPTRV2Controller::readSerial()
unsigned int length;
RESP_TYPE_V2 resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RT2_QUERY) {
count++;
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;
}
}
} 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;
}
@ -423,7 +423,7 @@ bool CDVRPTRV2Controller::readSpace()
buffer[8U] = '1';
buffer[9U] = 0x00U;
// CUtils::dump(wxT("Written"), buffer, 10U);
// CUtils::dump("Written", buffer, 10U);
return writeModem(buffer, 10U);
}
@ -445,8 +445,8 @@ bool CDVRPTRV2Controller::setConfig()
buffer[8U] = '1';
::memset(buffer + 9U, ' ', LONG_CALLSIGN_LENGTH);
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH && i < m_callsign.Len(); i++)
buffer[9U + i] = m_callsign.GetChar(i);
for (unsigned int i = 0U; i < LONG_CALLSIGN_LENGTH && i < m_callsign.size(); i++)
buffer[9U + i] = m_callsign[i];
buffer[65U] = m_duplex ? 0x03U : 0x00U;
@ -466,7 +466,7 @@ bool CDVRPTRV2Controller::setConfig()
buffer[89U] = m_txDelay / 10U;
// CUtils::dump(wxT("Written"), buffer, 105U);
// CUtils::dump("Written", buffer, 105U);
bool ret = writeModem(buffer, 105U);
if (!ret)
@ -476,34 +476,41 @@ bool CDVRPTRV2Controller::setConfig()
unsigned int length;
RESP_TYPE_V2 resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RT2_CONFIG) {
count++;
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;
}
}
} 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;
}
// 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)
{
// Get the start of the frame or nothing at all
int ret = readModem(buffer + 0U, 5U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR"));
wxLogError("Error when reading from the DV-RPTR");
return RT2_ERROR;
}
@ -518,7 +525,7 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
ret = readModem(buffer + 4U, 1U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR"));
wxLogError("Error when reading from the DV-RPTR");
return RT2_ERROR;
}
@ -537,7 +544,7 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
length = 20U;
break;
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;
}
@ -546,7 +553,7 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
while (offset < length) {
ret = readModem(buffer + offset, length - offset);
if (ret < 0) {
wxLogError(wxT("Error when reading from the DV-RPTR"));
wxLogError("Error when reading from the DV-RPTR");
return RT2_ERROR;
}
@ -554,10 +561,10 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
offset += ret;
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) {
return RT2_DATA;
@ -577,157 +584,115 @@ RESP_TYPE_V2 CDVRPTRV2Controller::getResponse(unsigned char *buffer, unsigned in
return RT2_UNKNOWN;
}
wxString CDVRPTRV2Controller::getPath() const
std::string CDVRPTRV2Controller::getPath() const
{
return m_usbPath;
}
bool CDVRPTRV2Controller::findPort()
{
#if !defined(_WIN32)
if (m_connection != CT_USB)
return true;
if (m_usbPath.IsEmpty())
if (m_usbPath.empty())
return false;
#if defined(__WINDOWS__)
#else
wxDir dir;
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
DIR* dir = ::opendir("/sys/class/tty");
if (dir == nullptr) {
wxLogError("Cannot open directory /sys/class/tty");
return false;
}
wxString fileName;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*"));
while (ret1) {
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
struct dirent* entry;
while ((entry = ::readdir(dir)) != nullptr) {
std::string fileName(entry->d_name);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
// Match ttyACM* entries
if (fileName.substr(0, 6) != "ttyACM")
continue;
std::string path = "/sys/class/tty/" + fileName;
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char cpath[255U];
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U);
int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
::strcat(cpath, "/device");
ret2 = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
::closedir(dir);
return false;
}
path = wxString(symlink, wxConvLocal, ret2);
symlink[ret2] = '\0';
path = std::string(symlink, ret2);
} else {
// Get all but the last section
wxString fullPath = wxString(symlink, wxConvLocal, ret2);
path = fullPath.BeforeLast(wxT('/'));
symlink[ret2] = '\0';
std::string fullPath(symlink, ret2);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
}
if (path.IsSameAs(m_usbPath)) {
m_usbPort.Printf(wxT("/dev/%s"), fileName.c_str());
if (path == m_usbPath) {
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;
}
ret1 = dir.GetNext(&fileName);
}
#endif
::closedir(dir);
return false;
#else
return true;
#endif
}
bool CDVRPTRV2Controller::findPath()
{
#if !defined(_WIN32)
if (m_connection != CT_USB)
return true;
#if defined(__WINDOWS__)
#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());
std::string path = "/sys/class/tty/" + m_usbPort.substr(5U);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U);
int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
::strcat(cpath, "/device");
ret = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
return false;
}
path = wxString(symlink, wxConvLocal, ret);
symlink[ret] = '\0';
path = std::string(symlink, ret);
} else {
wxString fullPath = wxString(symlink, wxConvLocal, ret);
path = fullPath.BeforeLast(wxT('/'));
symlink[ret] = '\0';
std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
}
if (m_usbPath.IsEmpty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str());
if (m_usbPath.empty())
wxLogMessage("Found modem path of %s", path.c_str());
m_usbPath = path;
#endif
return true;
#else
return true;
#endif
}
bool CDVRPTRV2Controller::findModem()
@ -736,7 +701,7 @@ bool CDVRPTRV2Controller::findModem()
// Tell the repeater that the signal has gone away
if (m_rx) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -752,7 +717,7 @@ bool CDVRPTRV2Controller::findModem()
while (!m_stopped) {
count++;
if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem"));
wxLogMessage("Trying to reopen the modem");
bool ret = findPort();
if (ret) {
@ -764,7 +729,7 @@ bool CDVRPTRV2Controller::findModem()
count = 0U;
}
Sleep(500UL);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return false;
@ -782,7 +747,7 @@ bool CDVRPTRV2Controller::openModem()
ret = m_network->open();
break;
default:
wxLogError(wxT("Invalid connection type: %d"), int(m_connection));
wxLogError("Invalid connection type: %d", int(m_connection));
break;
}

@ -25,8 +25,9 @@
#include "RingBuffer.h"
#include "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
enum RESP_TYPE_V2 {
RT2_TIMEOUT,
@ -39,14 +40,43 @@ enum RESP_TYPE_V2 {
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 {
public:
CDVRPTRV2Controller(const wxString& port, const wxString& path, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay);
CDVRPTRV2Controller(const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay);
// USB serial connection.
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 void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -55,18 +85,20 @@ public:
virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const;
virtual std::string getPath() const;
private:
void entry();
CONNECTION_TYPE m_connection;
wxString m_usbPort;
wxString m_usbPath;
wxString m_address;
std::string m_usbPort;
std::string m_usbPath;
std::string m_address;
unsigned int m_port;
bool m_txInvert;
unsigned int m_modLevel;
bool m_duplex;
wxString m_callsign;
std::string m_callsign;
unsigned int m_txDelay;
CSerialDataController* m_usb;
CTCPReaderWriter* m_network;

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

@ -25,8 +25,9 @@
#include "RingBuffer.h"
#include "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
enum RESP_TYPE_V3 {
RT3_TIMEOUT,
@ -39,14 +40,22 @@ enum RESP_TYPE_V3 {
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 {
public:
CDVRPTRV3Controller(const wxString& port, const wxString& path, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay);
CDVRPTRV3Controller(const wxString& address, unsigned int port, bool txInvert, unsigned int modLevel, bool duplex, const wxString& callsign, unsigned int txDelay);
// USB serial connection.
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 void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -55,18 +64,20 @@ public:
virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const;
virtual std::string getPath() const;
private:
void entry();
CONNECTION_TYPE m_connection;
wxString m_usbPort;
wxString m_usbPath;
wxString m_address;
std::string m_usbPort;
std::string m_usbPath;
std::string m_address;
unsigned int m_port;
bool m_txInvert;
unsigned int m_modLevel;
bool m_duplex;
wxString m_callsign;
std::string m_callsign;
unsigned int m_txDelay;
CSerialDataController* m_usb;
CTCPReaderWriter* m_network;

@ -20,7 +20,10 @@
#include "DStarDefines.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 unsigned int DVTOOL_SIGNATURE_LENGTH = 6U;
@ -41,10 +44,10 @@ const unsigned int BUFFER_LENGTH = 255U;
CDVTOOLFileReader::CDVTOOLFileReader() :
m_fileName(),
m_file(),
m_file(nullptr),
m_records(0U),
m_type(DVTFR_NONE),
m_buffer(NULL),
m_buffer(nullptr),
m_length(0U),
m_end(false)
{
@ -56,7 +59,7 @@ CDVTOOLFileReader::~CDVTOOLFileReader()
delete[] m_buffer;
}
wxString CDVTOOLFileReader::getFileName() const
std::string CDVTOOLFileReader::getFileName() const
{
return m_fileName;
}
@ -66,34 +69,38 @@ unsigned int CDVTOOLFileReader::getRecords() const
return m_records;
}
bool CDVTOOLFileReader::open(const wxString& fileName)
bool CDVTOOLFileReader::open(const std::string& fileName)
{
m_fileName = fileName;
bool res = m_file.Open(fileName, wxT("rb"));
if (!res)
m_file = ::fopen(fileName.c_str(), "rb");
if (m_file == nullptr)
return false;
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) {
m_file.Close();
::fclose(m_file);
m_file = nullptr;
return false;
}
if (::memcmp(buffer, DVTOOL_SIGNATURE, DVTOOL_SIGNATURE_LENGTH) != 0) {
m_file.Close();
::fclose(m_file);
m_file = nullptr;
return false;
}
wxUint32 uint32;
n = m_file.Read(&uint32, sizeof(wxUint32));
if (n != sizeof(wxUint32)) {
m_file.Close();
uint32_t uint32;
n = ::fread(&uint32, 1U, sizeof(uint32_t), m_file);
if (n != sizeof(uint32_t)) {
::fclose(m_file);
m_file = nullptr;
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;
return true;
@ -101,15 +108,16 @@ bool CDVTOOLFileReader::open(const wxString& fileName)
DVTFR_TYPE CDVTOOLFileReader::read()
{
wxUint16 uint16;
size_t n = m_file.Read(&uint16, sizeof(wxUint16));
if (n != sizeof(wxUint16))
uint16_t uint16;
size_t n = ::fread(&uint16, 1U, sizeof(uint16_t), m_file);
if (n != sizeof(uint16_t))
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];
n = m_file.Read(bytes, DSVT_SIGNATURE_LENGTH);
n = ::fread(bytes, 1U, DSVT_SIGNATURE_LENGTH, m_file);
if (n != DSVT_SIGNATURE_LENGTH)
return DVTFR_NONE;
@ -117,17 +125,17 @@ DVTFR_TYPE CDVTOOLFileReader::read()
return DVTFR_NONE;
char flag;
n = m_file.Read(&flag, 1U);
n = ::fread(&flag, 1U, 1U, m_file);
if (n != 1U)
return DVTFR_NONE;
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)
return DVTFR_NONE;
n = m_file.Read(&flag, 1U);
n = ::fread(&flag, 1U, 1U, m_file);
if (n != 1U)
return DVTFR_NONE;
@ -136,7 +144,7 @@ DVTFR_TYPE CDVTOOLFileReader::read()
m_end = true;
}
n = m_file.Read(m_buffer, m_length);
n = ::fread(m_buffer, 1U, m_length, m_file);
if (n != m_length)
return DVTFR_NONE;
@ -146,7 +154,7 @@ DVTFR_TYPE CDVTOOLFileReader::read()
CHeaderData* CDVTOOLFileReader::readHeader()
{
if (m_type != DVTFR_HEADER)
return NULL;
return nullptr;
if (m_buffer[39U] == 0xFFU && m_buffer[40U] == 0xFFU)
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);
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;
return NULL;
return nullptr;
}
return header;
@ -165,8 +173,8 @@ CHeaderData* CDVTOOLFileReader::readHeader()
unsigned int CDVTOOLFileReader::readData(unsigned char* buffer, unsigned int length, bool& end)
{
wxASSERT(buffer != NULL);
wxASSERT(length > 0U);
assert(buffer != nullptr);
assert(length > 0U);
if (m_type != DVTFR_DATA)
return 0U;
@ -183,5 +191,8 @@ unsigned int CDVTOOLFileReader::readData(unsigned char* buffer, unsigned int len
void CDVTOOLFileReader::close()
{
m_file.Close();
if (m_file != nullptr) {
::fclose(m_file);
m_file = nullptr;
}
}

@ -21,37 +21,62 @@
#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 {
DVTFR_NONE,
DVTFR_HEADER,
DVTFR_DATA
DVTFR_NONE, // No record read yet, or end of file.
DVTFR_HEADER, // Current record contains a radio header.
DVTFR_DATA // Current record contains a DV voice/data frame.
};
#include <wx/wx.h>
#include <wx/ffile.h>
#include "StdCompat.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 {
public:
CDVTOOLFileReader();
~CDVTOOLFileReader();
wxString getFileName() const;
std::string getFileName() 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();
// Returns a newly allocated CHeaderData; caller takes ownership.
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);
void close();
private:
wxString m_fileName;
wxFFile m_file;
wxUint32 m_records;
std::string m_fileName;
FILE* m_file;
uint32_t m_records; // Total record count from the file header.
DVTFR_TYPE m_type;
unsigned char* m_buffer;
unsigned int m_length;
bool m_end;
unsigned int m_length; // Payload length of the current record.
bool m_end; // True if the trailer bit was set in the current record.
};
#endif

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

@ -21,31 +21,52 @@
#include "HeaderData.h"
#include <wx/wx.h>
#include <wx/ffile.h>
#include "StdCompat.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 {
public:
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 wxString& filename, const CHeaderData& header);
bool open(const std::string& filename, const CHeaderData& header);
bool write(const unsigned char* buffer, unsigned int length);
void close();
private:
static wxString m_dirName;
static std::string m_dirName;
wxString m_fileName;
wxFFile m_file;
wxUint32 m_count;
unsigned int m_sequence;
wxFileOffset m_offset;
std::string m_fileName;
FILE* m_file;
uint32_t m_count; // Number of DSVT records written (header + data + trailer).
unsigned int m_sequence; // Per-frame sequence counter (020, mirrors DSRP wire format).
long m_offset; // File offset of the record-count field, for back-patching in close().
bool writeHeader(const CHeaderData& header);
bool writeTrailer();

@ -13,8 +13,6 @@
#include "DummyController.h"
#include <wx/wx.h>
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 <chrono>
#include <thread>
CExternalController::CExternalController(IHardwareController* controller, bool pttInvert) :
wxThread(wxTHREAD_JOINABLE),
m_controller(controller),
m_pttInvert(pttInvert),
m_disable(false),
@ -27,10 +29,11 @@ m_out1(false),
m_out2(false),
m_out3(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)
m_radioTX = true;
}
@ -46,42 +49,45 @@ bool CExternalController::open()
if (!res)
return false;
Create();
Run();
m_thread = std::thread(&CExternalController::entry, this);
return true;
}
void* CExternalController::Entry()
void CExternalController::entry()
{
wxASSERT(m_controller != NULL);
bool dummy1, dummy2, dummy3, dummy4;
bool dummy1, dummy2, dummy3, dummy4, disableIn;
// 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) {
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)
m_controller->setDigitalOutputs(true, false, false, false, false, false, false, false);
else
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)
m_controller->setDigitalOutputs(true, false, false, false, false, false, false, false);
else
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();
return NULL;
}
bool CExternalController::getDisable() const
@ -99,6 +105,7 @@ void CExternalController::setRadioTransmit(bool value)
void CExternalController::setHeartbeat()
{
// Toggle on each call so any periodic caller drives a visible LED blink.
m_heartbeat = !m_heartbeat;
}
@ -131,5 +138,6 @@ void CExternalController::close()
{
m_kill = true;
Wait();
if (m_thread.joinable())
m_thread.join();
}

@ -11,46 +11,83 @@
* 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
#define ExternalController_H
#include "HardwareController.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <atomic>
#include <thread>
class CExternalController : public wxThread {
class CExternalController {
public:
CExternalController(IHardwareController* controller, bool pttInvert);
virtual ~CExternalController();
// Open the hardware device and start the polling thread.
virtual bool open();
// True when the hardware disable input is asserted (repeater should not TX).
virtual bool getDisable() const;
// Set the desired PTT state; inverted before applying if pttInvert is set.
virtual void setRadioTransmit(bool value);
// Toggle the heartbeat output each call (caller drives the toggle rate).
virtual void setHeartbeat();
// Assert or de-assert the "active" status output.
virtual void setActive(bool value);
virtual void setOutput1(bool value);
virtual void setOutput2(bool value);
virtual void setOutput3(bool value);
virtual void setOutput4(bool value);
// Signal the polling thread to stop, then close the hardware device.
virtual void close();
virtual void* Entry();
private:
IHardwareController* m_controller;
bool m_pttInvert;
bool m_disable;
bool m_heartbeat;
bool m_active;
bool m_radioTX;
bool m_out1;
bool m_out2;
bool m_out3;
bool m_out4;
bool m_kill;
void entry(); // Polling thread body
IHardwareController* m_controller; // Concrete hardware driver (owned)
bool m_pttInvert; // True if PTT sense is active-low
// Desired output states written by the repeater thread and read by entry().
// All are atomic so no mutex is needed for the shared flag pattern.
std::atomic<bool> m_disable; // Latched from the hardware disable input
std::atomic<bool> m_heartbeat; // Heartbeat/LED toggle
std::atomic<bool> m_active; // Repeater-active indicator
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

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

@ -17,11 +17,13 @@
*/
#include "GMSKController.h"
#if defined(__WINDOWS__)
#include "GMSKModemWinUSB.h"
#endif
#include "GMSKModemLibUsb.h"
#include "Timer.h"
#include "Logger.h"
#include <cassert>
#include <chrono>
#include <thread>
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) :
CModem(),
m_modem(NULL),
m_modem(nullptr),
m_duplex(duplex),
m_buffer(NULL),
m_buffer(nullptr),
m_txData(1000U)
{
wxASSERT(address > 0U);
assert(address > 0U);
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);
#endif
}
CGMSKController::~CGMSKController()
@ -64,7 +52,7 @@ CGMSKController::~CGMSKController()
bool CGMSKController::start()
{
if (m_modem == NULL)
if (m_modem == nullptr)
return false;
bool ret = m_modem->open();
@ -73,16 +61,14 @@ bool CGMSKController::start()
return false;
}
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CGMSKController::entry, this);
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);
hdrTimer.start();
@ -98,10 +84,8 @@ void* CGMSKController::Entry()
unsigned char readLength = 0U;
unsigned char* readBuffer = new unsigned char[DV_FRAME_LENGTH_BYTES];
wxStopWatch stopWatch;
while (!m_stopped) {
stopWatch.Start();
auto loopStart = std::chrono::steady_clock::now();
// Only receive when not transmitting or when in duplex mode
if (!m_tx || m_duplex) {
@ -110,10 +94,10 @@ void* CGMSKController::Entry()
bool end;
int ret = m_modem->readData(buffer, GMSK_MODEM_DATA_LENGTH, end);
if (ret >= 0) {
// CUtils::dump(wxT("Read Data"), buffer, ret);
// CUtils::dump("Read Data", buffer, ret);
if (end) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -129,7 +113,7 @@ void* CGMSKController::Entry()
readLength++;
if (readLength >= DV_FRAME_LENGTH_BYTES) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_DATA;
@ -152,9 +136,9 @@ void* CGMSKController::Entry()
unsigned char buffer[90U];
bool ret = m_modem->readHeader(buffer, 90U);
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];
data[0U] = DSMTT_HEADER;
@ -176,7 +160,7 @@ void* CGMSKController::Entry()
// Only transmit when not receiving or when in duplex mode
if (!rx || m_duplex) {
if (writeLength == 0U && m_txData.hasData()) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_NONE;
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
if (tx == STATE_FALSE) {
// CUtils::dump(wxT("Write Header"), writeBuffer, writeLength);
// CUtils::dump("Write Header", writeBuffer, writeLength);
m_modem->writeHeader(writeBuffer, writeLength);
m_modem->setPTT(true);
dataTimer.start();
@ -212,7 +196,7 @@ void* CGMSKController::Entry()
// Check that there is space in the modem buffer
if (ret == STATE_TRUE) {
// CUtils::dump(wxT("Write Data"), writeBuffer, writeLength);
// CUtils::dump("Write Data", writeBuffer, writeLength);
int ret = m_modem->writeData(writeBuffer, writeLength);
if (ret > 0) {
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
if (!rx) {
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
@ -251,24 +237,22 @@ void* CGMSKController::Entry()
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();
delete m_modem;
}
delete[] writeBuffer;
delete[] readBuffer;
return NULL;
}
bool CGMSKController::writeHeader(const CHeaderData& header)
{
bool ret = m_txData.hasSpace(RADIO_HEADER_LENGTH_BYTES);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -280,27 +264,27 @@ bool CGMSKController::writeHeader(const CHeaderData& header)
buffer[1U] = header.getFlag2();
buffer[2U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 3U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 3U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 11U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 11U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 19U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 19U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 27U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 27U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 35U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 35U] = my2[i];
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
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);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
return false;
}
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char buffer[2U];
buffer[0U] = end ? DSMTT_EOT : DSMTT_DATA;
@ -347,7 +331,7 @@ bool CGMSKController::isTXReady()
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();
@ -357,18 +341,18 @@ bool CGMSKController::reopenModem()
return true;
// Reset the drivers state
m_mutex.Lock();
m_txData.clear();
m_mutex.Unlock();
{
std::lock_guard<std::mutex> lock(m_mutex);
m_txData.clear();
}
m_tx = false;
Sleep(1000UL);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
delete m_modem;
m_modem = NULL;
m_modem = nullptr;
return false;
}

@ -24,16 +24,36 @@
#include "GMSKModem.h"
#include "Modem.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 {
public:
CGMSKController(USB_INTERFACE iface, unsigned int address, bool duplex);
virtual ~CGMSKController();
virtual void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -43,6 +63,8 @@ public:
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
private:
void entry();
IGMSKModem* m_modem;
bool m_duplex;
unsigned char* m_buffer;

@ -21,8 +21,20 @@
#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 {
public:
virtual ~IGMSKModem() = 0;
@ -46,4 +58,3 @@ private:
};
#endif

@ -18,6 +18,12 @@
#include "GMSKModemLibUsb.h"
#include "DStarDefines.h"
#include "Logger.h"
#include <cassert>
#include <chrono>
#include <thread>
#include <string>
const unsigned int VENDOR_ID = 0x04D8U;
@ -46,118 +52,48 @@ const char PTT_OnOff = 0x20;
const int PTT_ON = 1;
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) {
#if defined(WIN32)
wxString errorText(CGMSKModemLibUsb::m_usbStrerror(), wxConvLocal);
#else
wxString errorText(libusb_error_name(ret), wxConvLocal);
#endif
wxLogMessage(wxT("%s, ret: %d, err=%s"), message, ret, errorText.c_str());
std::string errorText(libusb_error_name(ret));
wxLogMessage("%s, ret: %d, err=%s", message, ret, errorText.c_str());
}
CGMSKModemLibUsb::CGMSKModemLibUsb(unsigned int address) :
m_address(address),
#if !defined(WIN32)
m_context(NULL),
#endif
m_dev(NULL),
m_context(nullptr),
m_dev(nullptr),
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);
#endif
}
CGMSKModemLibUsb::~CGMSKModemLibUsb()
{
#if !defined(WIN32)
wxASSERT(m_context != NULL);
assert(m_context != nullptr);
::libusb_exit(m_context);
#endif
}
bool CGMSKModemLibUsb::open()
{
#if !defined(WIN32)
wxASSERT(m_context != NULL);
#endif
wxASSERT(m_dev == NULL);
assert(m_context != nullptr);
assert(m_dev == nullptr);
bool ret1 = openModem();
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;
}
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;
do {
unsigned char buffer[GMSK_MODEM_DATA_LENGTH];
ret2 = io(0xC0, GET_VERSION, 0, 0, buffer, GMSK_MODEM_DATA_LENGTH, USB_TIMEOUT);
if (ret2 > 0) {
wxString text((char *) buffer, wxConvLocal, ret2);
version.Append(text);
version.append((char*)buffer, ret2);
} else if (ret2 < 0) {
::libUsbLogError(ret2, "GET_VERSION");
close();
@ -165,27 +101,34 @@ bool CGMSKModemLibUsb::open()
}
} 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
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"));
if (version.find("DUTCH*Star") != std::string::npos && version.find("0.1.00") != std::string::npos) {
wxLogWarning("This modem firmware is not supported by the repeater");
wxLogWarning("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)
if (version.find("DUTCH*Star") != std::string::npos)
m_brokenSpace = 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)
{
wxASSERT(header != NULL);
wxASSERT(length > (RADIO_HEADER_LENGTH_BYTES * 2U));
assert(header != nullptr);
assert(length > (RADIO_HEADER_LENGTH_BYTES * 2U));
unsigned int offset = 0U;
@ -197,12 +140,12 @@ bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
if (ret == -19) // -ENODEV
return false;
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} else if (ret == 0) {
if (offset == 0U)
return false;
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
unsigned char status;
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
return false;
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} else if (ret > 0) {
if ((status & COS_OnOff) == COS_OnOff)
offset = 0U;
@ -230,7 +173,7 @@ bool CGMSKModemLibUsb::readHeader(unsigned char* header, unsigned int length)
}
if ((status & CRC_ERROR) == CRC_ERROR) {
wxLogMessage(wxT("Header - CRC Error"));
wxLogMessage("Header - CRC Error");
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)
{
wxASSERT(data != NULL);
wxASSERT(length > 0U);
assert(data != nullptr);
assert(length > 0U);
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)
{
wxASSERT(header != NULL);
wxASSERT(length >= (RADIO_HEADER_LENGTH_BYTES - 2U));
assert(header != nullptr);
assert(length >= (RADIO_HEADER_LENGTH_BYTES - 2U));
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);
@ -322,8 +265,8 @@ void CGMSKModemLibUsb::setPTT(bool on)
int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length)
{
wxASSERT(data != NULL);
wxASSERT(length > 0U && length <= DV_FRAME_LENGTH_BYTES);
assert(data != nullptr);
assert(length > 0U && length <= DV_FRAME_LENGTH_BYTES);
if (length > GMSK_MODEM_DATA_LENGTH) {
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
::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);
if (ret < 0) {
@ -348,7 +291,7 @@ int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length)
return int(GMSK_MODEM_DATA_LENGTH);
}
return length;
} else {
int ret = io(0x40, PUT_DATA, 0, 0, data, length, USB_TIMEOUT);
@ -367,46 +310,19 @@ int CGMSKModemLibUsb::writeData(unsigned char* data, unsigned int length)
void CGMSKModemLibUsb::close()
{
wxASSERT(m_dev != NULL);
assert(m_dev != nullptr);
#if defined(WIN32)
m_usbClose(m_dev);
#else
libusb_close(m_dev);
#endif
m_dev = NULL;
m_dev = nullptr;
}
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);
if (m_dev == NULL)
if (m_dev == nullptr)
return false;
::libusb_set_configuration(m_dev, 1);
#endif
unsigned char c;
io(0x40, SET_AD_INIT, 0, 0, &c, 0, USB_TIMEOUT);
@ -416,27 +332,23 @@ bool CGMSKModemLibUsb::openModem()
return true;
}
int CGMSKModemLibUsb::io(uint8_t requestType, uint8_t request, uint16_t value,
uint16_t index, unsigned char* data, uint16_t length,
int CGMSKModemLibUsb::io(uint8_t requestType, uint8_t request, uint16_t value,
uint16_t index, unsigned char* data, uint16_t length,
unsigned int timeout)
{
wxASSERT(m_dev != NULL);
wxASSERT(data != NULL);
assert(m_dev != nullptr);
assert(data != nullptr);
int ret = 0;
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);
#endif
if (ret >= 0)
return ret;
if (ret == -19) // ENODEV
return ret;
::wxMilliSleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return ret;

@ -22,19 +22,34 @@
#include "GMSKModem.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>
#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 {
public:
CGMSKModemLibUsb(unsigned int address);
@ -54,32 +69,15 @@ public:
virtual int writeData(unsigned char* data, unsigned int length);
virtual void close();
#if defined(WIN32)
static char* (*m_usbStrerror)();
#endif
private:
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_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);
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 <cstdio>
CGPIOController::CGPIOController(unsigned int config) :
m_config(config),
m_outp1(false),
@ -38,7 +40,7 @@ bool CGPIOController::open()
{
bool ret = ::wiringPiSetup() != -1;
if (!ret) {
wxLogError(wxT("Unable to initialise wiringPi"));
::fprintf(stderr, "Unable to initialise wiringPi\n");
return false;
}
@ -172,4 +174,3 @@ void CGPIOController::close()
}
#endif

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

@ -21,20 +21,22 @@
#include "DStarDefines.h"
#include "Utils.h"
#include <cstring>
// Uncomment to hex-dump every outbound DSRP packet to stdout for debugging.
// #define DUMP_TX
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_type(NETWORK_NONE),
m_buffer(NULL),
m_buffer(nullptr),
m_length(0U)
{
m_buffer = new unsigned char[BUFFER_LENGTH];
wxDateTime now = wxDateTime::UNow();
::srand(now.GetMillisecond());
::srand((unsigned int)::time(nullptr));
}
CGatewayProtocolHandler::~CGatewayProtocolHandler()
@ -47,7 +49,7 @@ bool CGatewayProtocolHandler::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];
@ -71,9 +73,11 @@ bool CGatewayProtocolHandler::writeHeader(const unsigned char* header, wxUint16
csum.result(buffer + 8U + RADIO_HEADER_LENGTH_BYTES - 2U);
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Header"), buffer, 49U);
CUtils::dump("Sending Header", buffer, 49U);
#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++) {
bool ret = m_socket.write(buffer, 49U, address, port);
if (!ret)
@ -83,10 +87,10 @@ bool CGatewayProtocolHandler::writeHeader(const unsigned char* header, wxUint16
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);
wxASSERT(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
assert(data != nullptr);
assert(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
unsigned char buffer[30U];
@ -107,13 +111,13 @@ bool CGatewayProtocolHandler::writeData(const unsigned char* data, unsigned int
::memcpy(buffer + 9U, data, length);
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Data"), buffer, length + 9U);
CUtils::dump("Sending Data", buffer, length + 9U);
#endif
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;
@ -124,7 +128,7 @@ NETWORK_TYPE CGatewayProtocolHandler::read(wxUint16& id, in_addr& address, unsig
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;
@ -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;
}
@ -180,7 +184,7 @@ unsigned int CGatewayProtocolHandler::readHeader(unsigned char* buffer, unsigned
bool check = csum.check(m_buffer + 8U + RADIO_HEADER_LENGTH_BYTES - 2U);
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;
}
@ -189,11 +193,14 @@ unsigned int CGatewayProtocolHandler::readHeader(unsigned char* buffer, unsigned
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)
return 0U;
if (m_length < 9U)
return 0U;
unsigned int dataLen = m_length - 9U;
// Is our buffer too small?
@ -222,12 +229,13 @@ unsigned int CGatewayProtocolHandler::readData(unsigned char* buffer, unsigned i
return dataLen;
}
unsigned int CGatewayProtocolHandler::readRegister(wxString& name)
unsigned int CGatewayProtocolHandler::readRegister(std::string& name)
{
if (m_type != NETWORK_REGISTER)
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;
}

@ -21,24 +21,38 @@
#include "UDPReaderWriter.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 {
public:
CGatewayProtocolHandler(const wxString& localAddress, unsigned int localPort);
CGatewayProtocolHandler(const std::string& localAddress, unsigned int localPort);
~CGatewayProtocolHandler();
bool open();
bool writeHeader(const unsigned char* header, wxUint16 id, const in_addr& address, unsigned int port);
bool writeData(const unsigned char* data, unsigned int length, wxUint16 id, wxUint8 seqNo, const in_addr& address, unsigned int port);
// Sends the header packet four times to reduce the impact of UDP packet loss.
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 readData(unsigned char* data, unsigned int length, wxUint8& seqNo, unsigned int& errors);
unsigned int readRegister(wxString& name);
unsigned int readData(unsigned char* data, unsigned int length, uint8_t& seqNo, unsigned int& errors);
// Reads a Register packet; name is the repeater's self-reported name string.
unsigned int readRegister(std::string& name);
void close();
@ -48,7 +62,7 @@ private:
unsigned char* m_buffer;
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

@ -19,6 +19,16 @@
#ifndef 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 {
public:
static unsigned int encode23127(unsigned int data);

@ -14,6 +14,18 @@
#ifndef 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 {
public:
virtual ~IHardwareController() = 0;

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

@ -19,25 +19,52 @@
#ifndef HeaderData_H
#define HeaderData_H
#include <wx/wx.h>
#include <wx/datetime.h>
#include <ctime>
#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 {
public:
CHeaderData();
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 wxString& myCall1, const wxString& myCall2, const wxString& yourCall,
const wxString& rptCall1, const wxString& rptCall2, unsigned char flag1 = 0x00,
CHeaderData(const std::string& myCall1, const std::string& myCall2, const std::string& yourCall,
const std::string& rptCall1, const std::string& rptCall2, unsigned char flag1 = 0x00,
unsigned char flag2 = 0x00, unsigned char flag3 = 0x00);
~CHeaderData();
wxDateTime getTime() const;
wxString getMyCall1() const;
wxString getMyCall2() const;
wxString getYourCall() const;
wxString getRptCall1() const;
wxString getRptCall2() const;
time_t getTime() const;
std::string getMyCall1() const;
std::string getMyCall2() const;
std::string getYourCall() const;
std::string getRptCall1() const;
std::string getRptCall2() const;
unsigned char getFlag1() const;
unsigned char getFlag2() const;
@ -57,11 +84,11 @@ public:
void setFlag2(unsigned char flag);
void setFlag3(unsigned char flag);
void setMyCall1(const wxString& callsign);
void setMyCall2(const wxString& callsign);
void setYourCall(const wxString& callsign);
void setRptCall1(const wxString& callsign);
void setRptCall2(const wxString& callsign);
void setMyCall1(const std::string& callsign);
void setMyCall2(const std::string& callsign);
void setYourCall(const std::string& callsign);
void setRptCall1(const std::string& callsign);
void setRptCall2(const std::string& callsign);
void setRepeaterMode(bool set);
void setDataPacket(bool set);
@ -70,6 +97,7 @@ public:
void setUrgent(bool set);
void setRepeaterFlags(unsigned char set);
// Returns false if the checksum was checked and failed during deserialisation.
bool isValid() const;
void reset();
@ -77,16 +105,16 @@ public:
CHeaderData& operator=(const CHeaderData& header);
private:
wxDateTime m_time;
wxString m_myCall1;
wxString m_myCall2;
wxString m_yourCall;
wxString m_rptCall1;
wxString m_rptCall2;
time_t m_time;
std::string m_myCall1;
std::string m_myCall2;
std::string m_yourCall;
std::string m_rptCall1;
std::string m_rptCall2;
unsigned char m_flag1;
unsigned char m_flag2;
unsigned char m_flag3;
bool m_valid;
bool m_valid; // Set to false if checksum validation failed.
};
#endif

@ -20,12 +20,12 @@
#include "DStarDefines.h"
#include "Timer.h"
#include "Utils.h"
#include "Logger.h"
#if defined(__WINDOWS__)
#include <setupapi.h>
#else
#include <wx/dir.h>
#endif
#include <cassert>
#include <chrono>
#include <thread>
#include <string>
enum STATE_ICOM {
SI_NONE,
@ -37,7 +37,7 @@ const unsigned int MAX_RESPONSES = 30U;
const unsigned int BUFFER_LENGTH = 200U;
CIcomController::CIcomController(const wxString& port) :
CIcomController::CIcomController(const std::string& port) :
CModem(),
m_port(port),
m_serial(port, SERIAL_38400, true),
@ -45,7 +45,7 @@ m_txData(2000U),
m_txCounter(0U),
m_pktCounter(0U)
{
wxASSERT(!port.IsEmpty());
assert(!port.empty());
}
CIcomController::~CIcomController()
@ -58,16 +58,14 @@ bool CIcomController::start()
if (!ret)
return false;
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CIcomController::entry, this);
return true;
}
void* CIcomController::Entry()
void CIcomController::entry()
{
wxLogMessage(wxT("Starting Icom Controller thread"));
wxLogMessage("Starting Icom Controller thread");
// Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U);
@ -77,14 +75,14 @@ void* CIcomController::Entry()
CTimer lostTimer(200U, 5U);
lostTimer.start();
unsigned char storeData[BUFFER_LENGTH];
unsigned int storeLength = 0U;
unsigned int pollCount = 0U;
bool connected = false;
STATE_ICOM state = SI_NONE;
unsigned char seqNo = 0U;
bool txSpace = true;
@ -113,13 +111,13 @@ void* CIcomController::Entry()
break;
case RTI_ERROR:
wxLogMessage(wxT("Stopping Icom Controller thread"));
return NULL;
wxLogMessage("Stopping Icom Controller thread");
return;
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];
data[0U] = DSMTT_HEADER;
@ -134,9 +132,9 @@ void* CIcomController::Entry()
break;
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];
data[0U] = DSMTT_DATA;
@ -151,9 +149,9 @@ void* CIcomController::Entry()
break;
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];
data[0U] = DSMTT_EOT;
@ -166,23 +164,23 @@ void* CIcomController::Entry()
break;
case RTI_PONG:
// wxLogMessage(wxT("RTI_PONG"));
// wxLogMessage("RTI_PONG");
if (!connected)
wxLogMessage(wxT("Connected to the Icom radio"));
wxLogMessage("Connected to the Icom radio");
lostTimer.start();
connected = true;
break;
case RTI_HEADER_ACK:
if (buffer[2U] == 0x00U) {
// wxLogMessage(wxT("RTI_HEADER_ACK"));
// wxLogMessage("RTI_HEADER_ACK");
if (state == SI_HEADER) {
storeLength = 0U;
retryTimer.stop();
txSpace = true;
}
} else {
wxLogMessage(wxT("RTI_HEADER_NAK"));
wxLogMessage("RTI_HEADER_NAK");
}
lostTimer.start();
@ -190,33 +188,33 @@ void* CIcomController::Entry()
case RTI_DATA_ACK:
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]) {
storeLength = 0U;
retryTimer.stop();
txSpace = true;
}
} else {
wxLogMessage(wxT("RTI_DATA_NAK - %02X"), buffer[2U]);
wxLogMessage("RTI_DATA_NAK - %02X", buffer[2U]);
}
lostTimer.start();
break;
default:
wxLogMessage(wxT("Unknown message, type: %02X"), buffer[1U]);
CUtils::dump(wxT("Buffer dump"), buffer, length);
wxLogMessage("Unknown message, type: %02X", buffer[1U]);
CUtils::dump("Buffer dump", buffer, length);
break;
}
if (retryTimer.isRunning() && retryTimer.hasExpired()) {
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);
if (ret != int(storeLength + 1U))
wxLogWarning(wxT("Error when writing to the Icom radio"));
wxLogWarning("Error when writing to the Icom radio");
retryTimer.start();
pollTimer.start();
@ -228,7 +226,7 @@ void* CIcomController::Entry()
storeLength = storeData[0U];
m_txData.getData(storeData + 1U, storeLength);
// CUtils::dump(wxT("Sending"), storeData, storeLength + 1U);
// CUtils::dump("Sending", storeData, storeLength + 1U);
if (storeData[1U] == 0x20U) {
state = SI_HEADER;
@ -240,7 +238,7 @@ void* CIcomController::Entry()
int ret = m_serial.write(storeData, 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();
pollTimer.start();
@ -248,15 +246,15 @@ void* CIcomController::Entry()
txSpace = false;
}
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock();
lostTimer.clock();
retryTimer.clock();
if (lostTimer.hasExpired()) {
if (connected)
wxLogWarning(wxT("Lost connection to the Icom radio"));
wxLogWarning("Lost connection to the Icom radio");
pollTimer.setTimeout(0U, 100U);
pollTimer.start();
@ -276,16 +274,14 @@ void* CIcomController::Entry()
m_serial.close();
wxLogMessage(wxT("Stopping Icom Controller thread"));
return NULL;
wxLogMessage("Stopping Icom Controller thread");
}
bool CIcomController::writeHeader(const CHeaderData& header)
{
bool ret = m_txData.hasSpace(43U);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -301,47 +297,52 @@ bool CIcomController::writeHeader(const CHeaderData& header)
buffer[3U] = header.getFlag2();
buffer[4U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 5U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 5U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 13U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 13U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 21U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 21U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 29U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 29U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 37U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 37U] = my2[i];
buffer[41U] = 0xFFU;
m_txCounter = 0U;
m_pktCounter = 0U;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_txData.addData(buffer, 42U);
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 ret = m_txData.hasSpace(18U);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
return false;
}
unsigned char buffer[20U];
unsigned char buffer[20U] = {};
if (end) {
buffer[0U] = 0x10U;
@ -355,7 +356,7 @@ bool CIcomController::writeData(const unsigned char* data, unsigned int, bool en
buffer[16U] = 0xFFU;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_txData.addData(buffer, 17U);
@ -371,14 +372,14 @@ bool CIcomController::writeData(const unsigned char* data, unsigned int, bool en
m_txCounter++;
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;
::memcpy(buffer + 4U, data, DV_FRAME_LENGTH_BYTES);
buffer[16U] = 0xFFU;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
m_txData.addData(buffer, 17U);
@ -395,17 +396,23 @@ bool CIcomController::isTXReady()
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)
{
// Get the start of the frame or nothing at all
int ret = m_serial.read(buffer, 1U);
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;
}
@ -419,13 +426,13 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
// Validate the message lengths
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;
}
ret = m_serial.read(buffer + 1U, 1U, 40U);
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;
}
@ -434,7 +441,7 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
// Validate the message types
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;
}
@ -443,7 +450,7 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
while (offset < length) {
ret = m_serial.read(buffer + offset, length - offset, 40U);
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;
}
@ -451,12 +458,12 @@ RESP_TYPE_ICOM CIcomController::getResponse(unsigned char *buffer, unsigned int&
offset += ret;
if (ret == 0) {
// CUtils::dump(wxT("Receive timed out"), buffer, offset);
// CUtils::dump("Receive timed out", buffer, offset);
return RTI_TIMEOUT;
}
}
// CUtils::dump(wxT("Received"), buffer, length);
// CUtils::dump("Received", buffer, length);
switch (buffer[1U]) {
case 0x03U:

@ -23,8 +23,9 @@
#include "RingBuffer.h"
#include "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
enum RESP_TYPE_ICOM {
RTI_TIMEOUT,
@ -38,13 +39,46 @@ enum RESP_TYPE_ICOM {
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 {
public:
CIcomController(const wxString& port);
CIcomController(const std::string& port);
virtual ~CIcomController();
virtual void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -53,19 +87,22 @@ public:
virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const;
virtual std::string getPath() const;
private:
wxString m_port;
void entry();
std::string m_port;
CSerialDataController m_serial;
CRingBuffer<unsigned char> m_txData;
unsigned char m_txCounter;
unsigned char m_pktCounter;
RESP_TYPE_ICOM getResponse(unsigned char* buffer, unsigned int& length);
// Sends 0xFF 0xFF 0xFF - null keepalive before the radio responds to pings.
bool writePoll();
// Sends 0x02 0x02 0xFF - ping request; elicits a PONG response from the radio.
bool writePing();
};
#endif

@ -13,6 +13,11 @@
#include "K8055Controller.h"
#include <cassert>
#include <cstdio>
#if !defined(_WIN32)
const unsigned int VELLEMAN_VENDOR_ID = 0x10CFU;
const unsigned int VELLEMAN_PRODUCT_ID = 0x5500U;
@ -36,208 +41,6 @@ const char OUT_PORT6 = 0x20U;
const char OUT_PORT7 = 0x40U;
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 USB_OUTPUT_ENDPOINT = 0x01U;
@ -257,27 +60,27 @@ m_outp5(false),
m_outp6(false),
m_outp7(false),
m_outp8(false),
m_context(NULL),
m_handle(NULL)
m_context(nullptr),
m_handle(nullptr)
{
::libusb_init(&m_context);
}
CK8055Controller::~CK8055Controller()
{
wxASSERT(m_context != NULL);
assert(m_context != nullptr);
::libusb_exit(m_context);
}
bool CK8055Controller::open()
{
wxASSERT(m_context != NULL);
wxASSERT(m_handle == NULL);
assert(m_context != nullptr);
assert(m_handle == nullptr);
m_handle = ::libusb_open_device_with_vid_pid(m_context, VELLEMAN_VENDOR_ID, VELLEMAN_PRODUCT_ID + m_address);
if (m_handle == NULL) {
wxLogError(wxT("Could not open the Velleman K8055"));
if (m_handle == nullptr) {
::fprintf(stderr, "Could not open the Velleman K8055\n");
return false;
}
@ -285,17 +88,17 @@ bool CK8055Controller::open()
if (res != 0) {
res = ::libusb_detach_kernel_driver(m_handle, VELLEMAN_HID_INTERFACE);
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);
m_handle = NULL;
m_handle = nullptr;
return false;
}
res = ::libusb_claim_interface(m_handle, VELLEMAN_HID_INTERFACE);
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);
m_handle = NULL;
m_handle = nullptr;
return false;
}
}
@ -309,7 +112,7 @@ bool CK8055Controller::open()
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];
buffer[0] = 0x00;
@ -324,7 +127,7 @@ void CK8055Controller::getDigitalInputs(bool& inp1, bool& inp2, bool& inp3, bool
int written;
int res = ::libusb_interrupt_transfer(m_handle, USB_INPUT_ENDPOINT, buffer, USB_BUFSIZE, &written, USB_TIMEOUT);
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;
}
@ -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)
{
wxASSERT(m_handle != NULL);
assert(m_handle != nullptr);
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)
@ -379,7 +182,7 @@ void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, boo
int written;
int res = ::libusb_interrupt_transfer(m_handle, USB_OUTPUT_ENDPOINT, buffer, USB_BUFSIZE, &written, USB_TIMEOUT);
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;
}
@ -398,15 +201,46 @@ void CK8055Controller::setDigitalOutputs(bool outp1, bool outp2, bool outp3, boo
void CK8055Controller::close()
{
wxASSERT(m_handle != NULL);
assert(m_handle != nullptr);
setDigitalOutputs(false, false, false, false, false, false, false, false);
::libusb_release_interface(m_handle, VELLEMAN_HID_INTERFACE);
::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 <wx/wx.h>
#if !defined(_WIN32)
#if defined(__WINDOWS__)
#include <windows.h>
#else
#include <libusb-1.0/libusb.h>
#endif
class CK8055Controller : public IHardwareController {
public:
@ -47,12 +43,32 @@ private:
bool m_outp6;
bool m_outp7;
bool m_outp8;
#if defined(__WINDOWS__)
HANDLE m_handle;
#else
libusb_context* m_context;
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

@ -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 <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)
#include "MQTTConnection.h"
CMQTTConnection* g_mqtt = NULL;
CMQTTConnection* g_mqtt = nullptr;
unsigned int g_mqttLevel = 2U;
#endif
CLogger::CLogger(const wxString& directory, const wxString& name) :
wxLog(),
std::atomic<CLogger*> CLogger::s_instance{nullptr};
CLogger::CLogger(const std::string& directory, const std::string& name,
unsigned int fileLevel, unsigned int displayLevel) :
m_name(name),
m_file(NULL),
m_fileName(),
m_day(0)
m_directory(directory),
m_file(nullptr),
m_day(0),
m_mutex(),
m_fileLevel(fileLevel),
m_displayLevel(displayLevel)
{
m_file = new wxFFile;
m_fileName.SetPath(directory);
m_fileName.SetExt(wxT("log"));
if (m_fileLevel == 0U)
return; // File logging disabled; do not create a log file.
time_t timestamp;
::time(&timestamp);
struct tm* tm = ::gmtime(&timestamp);
wxString text;
text.Printf(wxT("%s-%04d-%02d-%02d"), m_name.c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
struct tm tmBuf;
struct tm* tm = gmtime_safe(&timestamp, &tmBuf);
m_day = tm->tm_yday;
m_fileName.SetName(text);
openFile(tm);
}
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;
CLogger::~CLogger()
{
if (m_file != nullptr) {
::fclose(m_file);
m_file = nullptr;
}
}
CLogger::~CLogger()
CLogger* CLogger::getInstance()
{
wxASSERT(m_file != NULL);
return s_instance;
}
m_file->Close();
delete m_file;
void CLogger::setInstance(CLogger* logger)
{
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);
wxASSERT(m_file->IsOpened());
if (m_file != nullptr)
::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) {
case wxLOG_FatalError: letter = wxT("F"); break;
case wxLOG_Error: letter = wxT("E"); break;
case wxLOG_Warning: letter = wxT("W"); break;
case wxLOG_Info: letter = wxT("I"); break;
case wxLOG_Message: letter = wxT("M"); break;
case wxLOG_Status: letter = wxT("M"); break;
case wxLOG_Trace: letter = wxT("T"); break;
case wxLOG_Debug: letter = wxT("D"); break;
default: letter = wxT("U"); break;
}
m_file = ::fopen(filename, "a+t");
if (m_file == nullptr)
::fprintf(stderr, "Cannot open %s for appending\n", filename);
}
void CLogger::log(LogLevel level, const char* fmt, ...)
{
// Fast-path: drop the message if neither sink would accept it.
const unsigned int numLevel = static_cast<unsigned int>(level);
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;
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_list ap;
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)
if (g_mqtt != NULL && g_mqttLevel != 0U) {
unsigned int numLevel = 0U;
switch (level) {
case wxLOG_FatalError: numLevel = 6U; break;
case wxLOG_Error: numLevel = 5U; break;
case wxLOG_Warning: numLevel = 4U; break;
case wxLOG_Info: numLevel = 3U; break;
case wxLOG_Message: numLevel = 2U; break;
default: numLevel = 1U; break;
const char* letter;
switch (level) {
case LOG_FATAL: letter = "F"; break;
case LOG_ERROR: letter = "E"; break;
case LOG_WARNING: letter = "W"; break;
case LOG_INFO: letter = "I"; break;
case LOG_MESSAGE: letter = "M"; 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
if (level == wxLOG_FatalError)
if (level == LOG_FATAL)
::abort();
}
void CLogger::writeLog(const wxChar* msg, time_t timestamp)
void CLogger::writeLog(const char* msg, time_t timestamp)
{
wxASSERT(m_file != NULL);
wxASSERT(m_file->IsOpened());
wxASSERT(msg != NULL);
struct tm* tm = ::gmtime(&timestamp);
struct tm tmBuf;
struct tm* tm = gmtime_safe(&timestamp, &tmBuf);
int day = tm->tm_yday;
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_fileName.SetName(text);
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;
}
openFile(tm);
}
m_file->Write(wxString(msg));
m_file->Flush();
if (m_file != nullptr) {
::fputs(msg, m_file);
::fflush(m_file);
}
}

@ -16,34 +16,96 @@
* 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
#define Logger_H
#include <wx/wx.h>
#include <wx/ffile.h>
#include <wx/filename.h>
#include <atomic>
#include <string>
#include <cstdio>
#include <mutex>
#if defined(MQTT)
class CMQTTConnection;
extern CMQTTConnection* g_mqtt;
extern unsigned int g_mqttLevel;
extern CMQTTConnection* g_mqtt; // Global MQTT connection (set in main())
extern unsigned int g_mqttLevel; // Minimum LogLevel to forward over MQTT
#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:
CLogger(const wxString& directory, const wxString& name);
virtual ~CLogger();
// directory — where daily log files are written; may be empty when fileLevel == 0.
// 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:
wxString m_name;
wxFFile* m_file;
wxFileName m_fileName;
int m_day;
std::string m_name; // Base filename stem (e.g. "dstarrepeater")
std::string m_directory; // Directory where daily log files are written
FILE* m_file; // Currently open log file handle (nullptr when disabled)
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 "MMDVMController.h"
#include "DStarDefines.h"
#include "Logger.h"
#include "Timer.h"
#if defined(__WINDOWS__)
#include <setupapi.h>
#else
#include <wx/dir.h>
#include <chrono>
#include <thread>
#include <cassert>
#include <cstring>
#if !defined(_WIN32)
#include <dirent.h>
#include <unistd.h>
#endif
const unsigned char MMDVM_FRAME_START = 0xE0U;
@ -52,7 +56,7 @@ const unsigned int MAX_RESPONSES = 30U;
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(),
m_port(port),
m_path(path),
@ -63,11 +67,11 @@ m_txDelay(txDelay),
m_rxLevel(rxLevel),
m_txLevel(txLevel),
m_serial(port, SERIAL_115200, true),
m_buffer(NULL),
m_buffer(nullptr),
m_txData(1000U),
m_rx(false)
{
wxASSERT(!port.IsEmpty());
assert(!port.empty());
m_buffer = new unsigned char[BUFFER_LENGTH];
}
@ -87,16 +91,14 @@ bool CMMDVMController::start()
findPath();
Create();
SetPriority(100U);
Run();
m_thread = std::thread(&CMMDVMController::entry, this);
return true;
}
void* CMMDVMController::Entry()
void CMMDVMController::entry()
{
wxLogMessage(wxT("Starting MMDVM Controller thread"));
wxLogMessage("Starting MMDVM Controller thread");
// Clock every 5ms-ish
CTimer pollTimer(200U, 0U, 100U);
@ -115,8 +117,9 @@ void* CMMDVMController::Entry()
if (!ret) {
ret = findModem();
if (!ret) {
wxLogError(wxT("Stopping MMDVM Controller thread"));
return NULL;
wxLogError("Stopping MMDVM Controller thread");
delete[] writeBuffer;
return;
}
}
@ -133,15 +136,16 @@ void* CMMDVMController::Entry()
case RTDVM_ERROR: {
bool ret = findModem();
if (!ret) {
wxLogError(wxT("Stopping MMDVM Controller thread"));
return NULL;
wxLogError("Stopping MMDVM Controller thread");
delete[] writeBuffer;
return;
}
}
break;
case RTDVM_DSTAR_HEADER: {
// CUtils::dump(wxT("RT_DSTAR_HEADER"), m_buffer, length);
wxMutexLocker locker(m_mutex);
// CUtils::dump("RT_DSTAR_HEADER", m_buffer, length);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_HEADER;
@ -155,8 +159,8 @@ void* CMMDVMController::Entry()
break;
case RTDVM_DSTAR_DATA: {
// CUtils::dump(wxT("RT_DSTAR_DATA"), m_buffer, length);
wxMutexLocker locker(m_mutex);
// CUtils::dump("RT_DSTAR_DATA", m_buffer, length);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_DATA;
@ -170,8 +174,8 @@ void* CMMDVMController::Entry()
break;
case RTDVM_DSTAR_EOT: {
// wxLogMessage(wxT("RT_DSTAR_EOT"));
wxMutexLocker locker(m_mutex);
// wxLogMessage("RT_DSTAR_EOT");
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -183,8 +187,8 @@ void* CMMDVMController::Entry()
break;
case RTDVM_DSTAR_LOST: {
// wxLogMessage(wxT("RT_DSTAR_LOST"));
wxMutexLocker locker(m_mutex);
// wxLogMessage("RT_DSTAR_LOST");
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_LOST;
@ -198,20 +202,21 @@ void* CMMDVMController::Entry()
case RTDVM_GET_STATUS: {
bool dstar = (m_buffer[3U] & 0x01U) == 0x01U;
if (!dstar) {
wxLogError(wxT("D-Star not enabled in the MMDVM!!!"));
wxLogError(wxT("Stopping MMDVM Controller thread"));
return NULL;
wxLogError("D-Star not enabled in the MMDVM!!!");
wxLogError("Stopping MMDVM Controller thread");
delete[] writeBuffer;
return;
}
m_tx = (m_buffer[5U] & 0x01U) == 0x01U;
bool adcOverflow = (m_buffer[5U] & 0x02U) == 0x02U;
if (adcOverflow)
wxLogWarning(wxT("MMDVM ADC levels have overflowed"));
wxLogWarning("MMDVM ADC levels have overflowed");
space = m_buffer[6U];
// CUtils::dump(wxT("GET_STATUS"), m_buffer, length);
// wxLogMessage(wxT("PTT=%d space=%u"), int(m_tx), space);
// CUtils::dump("GET_STATUS", m_buffer, length);
// wxLogMessage("PTT=%d space=%u", int(m_tx), space);
}
break;
@ -223,23 +228,23 @@ void* CMMDVMController::Entry()
case RTDVM_NAK: {
switch (m_buffer[3U]) {
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;
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;
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;
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;
case RTDVM_DUMP:
CUtils::dump(wxT("Modem dump"), m_buffer + 3U, length - 3U);
CUtils::dump("Modem dump", m_buffer + 3U, length - 3U);
break;
case RTDVM_DEBUG1:
@ -251,13 +256,13 @@ void* CMMDVMController::Entry()
break;
default:
wxLogMessage(wxT("Unknown message, type: %02X"), m_buffer[2U]);
CUtils::dump(wxT("Buffer dump"), m_buffer, length);
wxLogMessage("Unknown message, type: %02X", m_buffer[2U]);
CUtils::dump("Buffer dump", m_buffer, length);
break;
}
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(&writeLength, 1U);
@ -265,46 +270,44 @@ void* CMMDVMController::Entry()
}
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);
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;
space -= 4U;
}
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);
if (ret != int(writeLength))
wxLogWarning(wxT("Error when writing data to the MMDVM"));
wxLogWarning("Error when writing data to the MMDVM");
writeType = DSMTT_NONE;
space--;
}
Sleep(5UL);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
pollTimer.clock();
}
wxLogMessage(wxT("Stopping MMDVM Controller thread"));
wxLogMessage("Stopping MMDVM Controller thread");
delete[] writeBuffer;
m_serial.close();
return NULL;
}
bool CMMDVMController::writeHeader(const CHeaderData& header)
{
bool ret = m_txData.hasSpace(46U);
if (!ret) {
wxLogWarning(wxT("No space to write the header"));
wxLogWarning("No space to write the header");
return false;
}
@ -320,31 +323,31 @@ bool CMMDVMController::writeHeader(const CHeaderData& header)
buffer[4U] = header.getFlag2();
buffer[5U] = header.getFlag3();
wxString rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 6U] = rpt2.GetChar(i);
std::string rpt2 = header.getRptCall2();
for (unsigned int i = 0U; i < rpt2.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 6U] = rpt2[i];
wxString rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 14U] = rpt1.GetChar(i);
std::string rpt1 = header.getRptCall1();
for (unsigned int i = 0U; i < rpt1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 14U] = rpt1[i];
wxString your = header.getYourCall();
for (unsigned int i = 0U; i < your.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 22U] = your.GetChar(i);
std::string your = header.getYourCall();
for (unsigned int i = 0U; i < your.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 22U] = your[i];
wxString my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.Len() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 30U] = my1.GetChar(i);
std::string my1 = header.getMyCall1();
for (unsigned int i = 0U; i < my1.size() && i < LONG_CALLSIGN_LENGTH; i++)
buffer[i + 30U] = my1[i];
wxString my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.Len() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 38U] = my2.GetChar(i);
std::string my2 = header.getMyCall2();
for (unsigned int i = 0U; i < my2.size() && i < SHORT_CALLSIGN_LENGTH; i++)
buffer[i + 38U] = my2[i];
CCCITTChecksumReverse cksum1;
cksum1.update(buffer + 3U, RADIO_HEADER_LENGTH_BYTES - 2U);
cksum1.result(buffer + 42U);
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_HEADER;
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);
if (!ret) {
wxLogWarning(wxT("No space to write data"));
wxLogWarning("No space to write data");
return false;
}
@ -372,7 +375,7 @@ bool CMMDVMController::writeData(const unsigned char* data, unsigned int length,
buffer[1U] = 3U;
buffer[2U] = MMDVM_DSTAR_EOT;
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char type = DSMTT_EOT;
m_txData.addData(&type, 1U);
@ -390,7 +393,7 @@ bool CMMDVMController::writeData(const unsigned char* data, unsigned int length,
buffer[2U] = MMDVM_DSTAR_DATA;
::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;
m_txData.addData(&type, 1U);
@ -416,9 +419,13 @@ bool CMMDVMController::isTXReady()
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()
{
::wxSleep(2);
std::this_thread::sleep_for(std::chrono::seconds(2));
for (unsigned int i = 0U; i < 6U; i++) {
unsigned char buffer[3U];
@ -427,30 +434,30 @@ bool CMMDVMController::readVersion()
buffer[1U] = 3U;
buffer[2U] = MMDVM_GET_VERSION;
// CUtils::dump(wxT("Written"), buffer, 3U);
// CUtils::dump("Written", buffer, 3U);
int ret = m_serial.write(buffer, 3U);
if (ret != 3)
return false;
for (unsigned int count = 0U; count < MAX_RESPONSES; count++) {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
unsigned int length;
RESP_TYPE_MMDVM resp = getResponse(m_buffer, length);
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;
}
}
::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;
}
@ -466,6 +473,14 @@ bool CMMDVMController::readStatus()
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()
{
unsigned char buffer[25U];
@ -503,7 +518,7 @@ bool CMMDVMController::setConfig()
buffer[13U] = (m_txLevel * 255U) / 100U;
buffer[14U] = (m_txLevel * 255U) / 100U;
buffer[15U] = (m_txLevel * 255U) / 100U;
buffer[16U] = 128U;
buffer[17U] = 128U;
@ -511,7 +526,7 @@ bool CMMDVMController::setConfig()
buffer[19U] = 0U;
// CUtils::dump(wxT("Written"), buffer, 20U);
// CUtils::dump("Written", buffer, 20U);
int ret = m_serial.write(buffer, 20U);
if (ret != 20U)
@ -521,23 +536,23 @@ bool CMMDVMController::setConfig()
unsigned int length;
RESP_TYPE_MMDVM resp;
do {
::wxMilliSleep(10UL);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
resp = getResponse(m_buffer, length);
if (resp != RTDVM_ACK && resp != RTDVM_NAK) {
count++;
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;
}
}
} while (resp != RTDVM_ACK && resp != RTDVM_NAK);
// CUtils::dump(wxT("Response"), m_buffer, length);
// CUtils::dump("Response", m_buffer, length);
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;
}
@ -549,7 +564,7 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
// Get the start of the frame or nothing at all
int ret = m_serial.read(buffer + 0U, 1U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the MMDVM"));
wxLogError("Error when reading from the MMDVM");
return RTDVM_ERROR;
}
@ -561,7 +576,7 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
ret = m_serial.read(buffer + 1U, 1U);
if (ret < 0) {
wxLogError(wxT("Error when reading from the MMDVM"));
wxLogError("Error when reading from the MMDVM");
return RTDVM_ERROR;
}
@ -571,8 +586,8 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
length = buffer[1U];
if (length >= BUFFER_LENGTH) {
wxLogError(wxT("Invalid data received from the MMDVM"));
CUtils::dump(wxT("Data"), buffer, 2U);
wxLogError("Invalid data received from the MMDVM");
CUtils::dump("Data", buffer, 2U);
return RTDVM_TIMEOUT;
}
@ -581,18 +596,21 @@ RESP_TYPE_MMDVM CMMDVMController::getResponse(unsigned char *buffer, unsigned in
while (offset < length) {
int ret = m_serial.read(buffer + offset, length - offset);
if (ret < 0) {
wxLogError(wxT("Error when reading from the MMDVM"));
wxLogError("Error when reading from the MMDVM");
return RTDVM_ERROR;
}
if (ret > 0)
offset += ret;
if (ret == 0)
Sleep(5UL);
if (ret == 0) {
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]) {
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;
}
bool CMMDVMController::findPort()
{
if (m_path.IsEmpty())
#if !defined(_WIN32)
if (m_path.empty())
return false;
#if defined(__WINDOWS__)
#else
wxDir dir;
bool ret1 = dir.Open(wxT("/sys/class/tty"));
if (!ret1) {
wxLogError(wxT("Cannot open directory /sys/class/tty"));
DIR* dir = ::opendir("/sys/class/tty");
if (dir == nullptr) {
wxLogError("Cannot open directory /sys/class/tty");
return false;
}
wxString fileName;
ret1 = dir.GetFirst(&fileName, wxT("ttyACM*"));
while (ret1) {
wxString path;
path.Printf(wxT("/sys/class/tty/%s"), fileName.c_str());
struct dirent* entry;
while ((entry = ::readdir(dir)) != nullptr) {
std::string fileName(entry->d_name);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
// Match ttyACM* entries
if (fileName.substr(0, 6) != "ttyACM")
continue;
std::string path = "/sys/class/tty/" + fileName;
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
char cpath[255U];
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret2 = ::readlink(cpath, symlink, 255U);
int ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
::strcat(cpath, "/device");
ret2 = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret2 = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret2 < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
::closedir(dir);
return false;
}
path = wxString(symlink, wxConvLocal, ret2);
symlink[ret2] = '\0';
path = std::string(symlink, ret2);
} else {
// Get all but the last section
wxString fullPath = wxString(symlink, wxConvLocal, ret2);
path = fullPath.BeforeLast(wxT('/'));
symlink[ret2] = '\0';
std::string fullPath(symlink, ret2);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
}
if (path.IsSameAs(m_path)) {
m_port.Printf(wxT("/dev/%s"), fileName.c_str());
if (path == m_path) {
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;
}
ret1 = dir.GetNext(&fileName);
}
#endif
::closedir(dir);
return false;
#else
return true;
#endif
}
bool CMMDVMController::findPath()
{
#if defined(__WINDOWS__)
#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());
#if !defined(_WIN32)
std::string path = "/sys/class/tty/" + m_port.substr(5U);
char cpath[255U];
::memset(cpath, 0x00U, 255U);
for (unsigned int i = 0U; i < path.Len(); i++)
cpath[i] = path.GetChar(i);
::strncpy(cpath, path.c_str(), sizeof(cpath) - 1);
cpath[sizeof(cpath) - 1] = '\0';
char symlink[255U];
int ret = ::readlink(cpath, symlink, 255U);
int ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
::strcat(cpath, "/device");
ret = ::readlink(cpath, symlink, 255U);
::strncat(cpath, "/device", sizeof(cpath) - ::strlen(cpath) - 1);
ret = ::readlink(cpath, symlink, sizeof(symlink) - 1);
if (ret < 0) {
wxLogError(wxT("Error from readlink()"));
wxLogError("Error from readlink()");
return false;
}
path = wxString(symlink, wxConvLocal, ret);
symlink[ret] = '\0';
path = std::string(symlink, ret);
} else {
wxString fullPath = wxString(symlink, wxConvLocal, ret);
path = fullPath.BeforeLast(wxT('/'));
symlink[ret] = '\0';
std::string fullPath(symlink, ret);
size_t pos = fullPath.rfind('/');
path = (pos != std::string::npos) ? fullPath.substr(0, pos) : fullPath;
}
if (m_path.IsEmpty())
wxLogMessage(wxT("Found modem path of %s"), path.c_str());
if (m_path.empty())
wxLogMessage("Found modem path of %s", path.c_str());
m_path = path;
#endif
return true;
#else
return true;
#endif
}
bool CMMDVMController::findModem()
@ -781,7 +757,7 @@ bool CMMDVMController::findModem()
// Tell the repeater that the signal has gone away
if (m_rx) {
wxMutexLocker locker(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
unsigned char data[2U];
data[0U] = DSMTT_EOT;
@ -797,7 +773,7 @@ bool CMMDVMController::findModem()
while (!m_stopped) {
count++;
if (count >= 4U) {
wxLogMessage(wxT("Trying to reopen the modem"));
wxLogMessage("Trying to reopen the modem");
bool ret = findPort();
if (ret) {
@ -809,7 +785,7 @@ bool CMMDVMController::findModem()
count = 0U;
}
Sleep(500UL);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
return false;
@ -839,30 +815,30 @@ bool CMMDVMController::openModem()
void CMMDVMController::printDebug()
{
unsigned int length = m_buffer[1U];
if (m_buffer[2U] == 0xF1U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 3U);
wxLogMessage(wxT("Debug: %s"), message.c_str());
} else if (m_buffer[2U] == 0xF2U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 5U);
if (m_buffer[2U] == 0xF1U && length >= 4U) {
std::string message((char*)(m_buffer + 3U), length - 3U);
wxLogMessage("Debug: %s", message.c_str());
} else if (m_buffer[2U] == 0xF2U && length >= 5U) {
std::string message((char*)(m_buffer + 3U), length - 5U);
short val1 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d"), message.c_str(), val1);
} else if (m_buffer[2U] == 0xF3U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 7U);
wxLogMessage("Debug: %s %d", message.c_str(), val1);
} else if (m_buffer[2U] == 0xF3U && length >= 7U) {
std::string message((char*)(m_buffer + 3U), length - 7U);
short val1 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U];
short val2 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d %d"), message.c_str(), val1, val2);
} else if (m_buffer[2U] == 0xF4U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 9U);
wxLogMessage("Debug: %s %d %d", message.c_str(), val1, val2);
} else if (m_buffer[2U] == 0xF4U && length >= 9U) {
std::string message((char*)(m_buffer + 3U), length - 9U);
short val1 = (m_buffer[length - 6U] << 8) | m_buffer[length - 5U];
short val2 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U];
short val3 = (m_buffer[length - 2U] << 8) | m_buffer[length - 1U];
wxLogMessage(wxT("Debug: %s %d %d %d"), message.c_str(), val1, val2, val3);
} else if (m_buffer[2U] == 0xF5U) {
wxString message((char*)(m_buffer + 3U), wxConvLocal, length - 11U);
wxLogMessage("Debug: %s %d %d %d", message.c_str(), val1, val2, val3);
} else if (m_buffer[2U] == 0xF5U && length >= 11U) {
std::string message((char*)(m_buffer + 3U), length - 11U);
short val1 = (m_buffer[length - 8U] << 8) | m_buffer[length - 7U];
short val2 = (m_buffer[length - 6U] << 8) | m_buffer[length - 5U];
short val3 = (m_buffer[length - 4U] << 8) | m_buffer[length - 3U];
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 "Modem.h"
#include "Utils.h"
#include "StdCompat.h"
#include <wx/wx.h>
#include <string>
enum RESP_TYPE_MMDVM {
RTDVM_TIMEOUT,
@ -46,13 +47,52 @@ enum RESP_TYPE_MMDVM {
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 {
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 void* Entry();
virtual bool start();
virtual unsigned int getSpace();
@ -61,11 +101,13 @@ public:
virtual bool writeHeader(const CHeaderData& header);
virtual bool writeData(const unsigned char* data, unsigned int length, bool end);
virtual wxString getPath() const;
virtual std::string getPath() const;
private:
wxString m_port;
wxString m_path;
void entry();
std::string m_port;
std::string m_path;
bool m_rxInvert;
bool m_txInvert;
bool m_pttInvert;
@ -89,8 +131,9 @@ private:
bool findModem();
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();
};
#endif

@ -23,9 +23,9 @@
#include <cassert>
#include <cstdio>
#include <cstring>
#if defined(_WIN32) || defined(_WIN64)
#if defined(_WIN32)
#include <process.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif
@ -60,11 +60,7 @@ CMQTTConnection::~CMQTTConnection()
bool CMQTTConnection::open()
{
char name[50U];
#if defined(_WIN32) || defined(_WIN64)
::sprintf(name, "DStarRepeater.%u", (unsigned)::_getpid());
#else
::sprintf(name, "DStarRepeater.%u", (unsigned)::getpid());
#endif
::snprintf(name, sizeof(name), "DStarRepeater.%u", (unsigned)::getpid());
::fprintf(stdout, "DStarRepeater (%s) connecting to MQTT as %s\n", m_name.c_str(), name);
@ -75,7 +71,7 @@ bool CMQTTConnection::open()
}
if (m_authEnabled)
::mosquitto_username_pw_set(m_mosq, m_username.c_str(), m_password.c_str());
::mosquitto_username_pw_set(m_mosq, m_username.c_str(), m_password.c_str());
::mosquitto_connect_callback_set(m_mosq, onConnect);
::mosquitto_subscribe_callback_set(m_mosq, onSubscribe);
@ -127,7 +123,7 @@ bool CMQTTConnection::publish(const char* topic, const unsigned char* data, unsi
if (::strchr(topic, '/') == nullptr) {
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);
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) {
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));
if (rc != MOSQ_ERR_SUCCESS) {
@ -213,7 +209,7 @@ void CMQTTConnection::onMessage(mosquitto* mosq, void* obj, const mosquitto_mess
std::string topic = (*it).first;
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) {
(*it).second((unsigned char*)message->payload, message->payloadlen);

@ -23,9 +23,27 @@
#include <mosquitto.h>
#include <atomic>
#include <vector>
#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 {
AT_MODE_ONCE = 0,
AT_LEAST_ONCE = 1,
@ -48,7 +66,7 @@ public:
private:
std::string m_host;
unsigned short m_port;
std::string m_name;
std::string m_name; // Prefix prepended to bare topic strings.
bool m_authEnabled;
std::string m_username;
std::string m_password;
@ -56,8 +74,9 @@ private:
unsigned int m_keepalive;
MQTT_QOS m_qos;
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 onSubscribe(mosquitto* mosq, void* obj, int mid, int qosCount, const int* grantedQOS);
static void onMessage(mosquitto* mosq, void* obj, const mosquitto_message* message);

@ -22,8 +22,14 @@
#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
* expected by Display-Driver, enabling LCD/OLED display output.
*
@ -35,27 +41,55 @@
#include "MQTTConnection.h"
#include "Logger.h"
#include <wx/wx.h>
#include <string>
#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)
static inline void mqttPublishDStarStart(const wxString& myCall1, const wxString& myCall2,
const wxString& yourCall, const wxString& rptCall2, const char* source)
static inline void mqttPublishDStarStart(const std::string& myCall1, const std::string& myCall2,
const std::string& yourCall, const std::string& rptCall2, const char* source)
{
if (g_mqtt == NULL)
if (g_mqtt == nullptr)
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];
::snprintf(buf, sizeof(buf),
"{\"D-Star\":{\"action\":\"start\","
"\"source_cs\":\"%s\",\"source_ext\":\"%s\","
"\"destination_cs\":\"%s\",\"reflector\":\"%s\","
"\"source\":\"%s\"}}",
(const char*)myCall1.mb_str(),
(const char*)myCall2.mb_str(),
(const char*)yourCall.mb_str(),
(const char*)rptCall2.mb_str(),
cs1.c_str(),
cs2.c_str(),
yc.c_str(),
rc2.c_str(),
source);
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)
static inline void mqttPublishDStarEnd()
{
if (g_mqtt == NULL)
if (g_mqtt == nullptr)
return;
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)
static inline void mqttPublishDStarLost()
{
if (g_mqtt == NULL)
if (g_mqtt == nullptr)
return;
g_mqtt->publish("json", "{\"D-Star\":{\"action\":\"lost\"}}");
@ -82,16 +116,30 @@ static inline void mqttPublishDStarLost()
// Publish idle mode (no traffic)
static inline void mqttPublishIdle()
{
if (g_mqtt == NULL)
if (g_mqtt == nullptr)
return;
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
static inline void mqttPublishBER(float ber)
{
if (g_mqtt == NULL)
if (g_mqtt == nullptr)
return;
char buf[128];
@ -103,15 +151,19 @@ static inline void mqttPublishBER(float ber)
}
// 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;
// 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];
::snprintf(buf, sizeof(buf),
"{\"Text\":{\"mode\":\"D-Star\",\"value\":\"%s\"}}",
(const char*)text.mb_str());
escaped.c_str());
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 \
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 \
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 \
SlowDataDecoder.o SlowDataEncoder.o SoundCardController.o SoundCardReaderWriter.o SplitController.o TCPReaderWriter.o \
MQTTConnection.o Timer.o UDPReaderWriter.o UDRCController.o URIUSBController.o Utils.o
@ -16,10 +16,15 @@ Common.a: $(OBJECTS)
-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
$(CXX) -DwxUSE_GUI=0 $(CFLAGS) -c -o $@ $<
$(CXX) -MM -DwxUSE_GUI=0 $(CFLAGS) $< > $*.d
$(CXX) $(CFLAGS) -c -o $@ $<
$(CXX) -MM $(CFLAGS) $< > $*.d
clean:
$(RM) Common.a *.o *.d *.bak *~

@ -19,17 +19,19 @@
#include "DStarDefines.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;
CModem::CModem() :
wxThread(wxTHREAD_JOINABLE),
m_rxData(1000U),
m_mutex(),
m_tx(false),
m_stopped(false),
m_thread(),
m_readType(DSMTT_NONE),
m_readLength(0U),
m_readBuffer(NULL)
m_readBuffer(nullptr)
{
m_readBuffer = new unsigned char[BUFFER_LENGTH];
}
@ -51,8 +53,9 @@ DSMT_TYPE CModem::read()
if (m_rxData.isEmpty())
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];
m_rxData.getData(hdr, 2U);
@ -66,7 +69,7 @@ DSMT_TYPE CModem::read()
CHeaderData* CModem::readHeader()
{
if (m_readType != DSMTT_HEADER)
return NULL;
return nullptr;
return new CHeaderData(m_readBuffer, RADIO_HEADER_LENGTH_BYTES, false);
}
@ -89,6 +92,6 @@ void CModem::stop()
{
m_stopped = true;
Wait();
if (m_thread.joinable())
m_thread.join();
}

@ -16,50 +16,93 @@
* 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
#define Modem_H
#include "HeaderData.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 {
DSMTT_NONE,
DSMTT_START,
DSMTT_HEADER,
DSMTT_DATA,
DSMTT_EOT,
DSMTT_LOST
DSMTT_NONE, // No message waiting
DSMTT_START, // Hardware has started (carrier / sync detected)
DSMTT_HEADER, // A valid D-Star header has been decoded from the air
DSMTT_DATA, // One DV frame of voice + slow-data payload
DSMTT_EOT, // End-of-transmission detected on air
DSMTT_LOST, // Signal lost mid-frame (carrier dropped unexpectedly)
};
class CModem : public wxThread {
class CModem {
public:
CModem();
virtual ~CModem();
// Start the driver I/O thread and open the hardware device.
virtual bool start() = 0;
// Transmit a D-Star header (called before the first writeData()).
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;
// Number of DV frames the TX buffer can still accept.
virtual unsigned int getSpace() = 0;
// True when the hardware is ready to accept TX frames.
virtual bool isTXReady() = 0;
// True while the hardware is actively transmitting.
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();
// Returns the header decoded by the most recent read() == DSMTT_HEADER call.
// Caller owns the returned object.
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);
// Signal the driver thread to exit and wait for it to join.
virtual void stop();
protected:
// SPSC ring buffer: driver thread writes, repeater thread reads.
CRingBuffer<unsigned char> m_rxData;
wxMutex m_mutex;
bool m_tx;
bool m_stopped;
// Guards m_rxData against concurrent access.
std::mutex m_mutex;
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:
// Scratch state populated by read() and consumed by readHeader()/readData().
DSMT_TYPE m_readType;
unsigned int m_readLength;
unsigned char* m_readBuffer;

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

@ -22,20 +22,39 @@
#include "RingBuffer.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 {
public:
COutputQueue(unsigned int space, unsigned int threshold);
~COutputQueue();
// Takes ownership of header; replaces any previously pending header.
void setHeader(CHeaderData* header);
// Returns the pending header and relinquishes ownership (caller must delete).
CHeaderData* getHeader();
unsigned int getData(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;
// True when data frames are queued and no header is pending (mid-stream).
bool dataReady() const;
bool isEmpty();
@ -48,7 +67,7 @@ private:
CRingBuffer<unsigned char> m_data;
unsigned int m_threshold;
CHeaderData* m_header;
unsigned int m_count;
unsigned int m_count; // Frames added since the last header; triggers playback start.
};
#endif

@ -21,11 +21,12 @@
#include "DStarDefines.h"
#include "Utils.h"
// Uncomment to hex-dump every outbound DSRP packet to stdout for debugging.
// #define DUMP_TX
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_address(),
m_port(gatewayPort),
@ -34,15 +35,14 @@ m_outId(0U),
m_outSeq(0U),
m_type(NETWORK_NONE),
m_inId(0U),
m_buffer(NULL),
m_buffer(nullptr),
m_length(0U)
{
m_address = CUDPReaderWriter::lookup(gatewayAddress);
m_buffer = new unsigned char[BUFFER_LENGTH];
wxDateTime now = wxDateTime::UNow();
::srand(now.GetMillisecond());
::srand((unsigned int)::time(nullptr));
}
CRepeaterProtocolHandler::~CRepeaterProtocolHandler()
@ -82,19 +82,19 @@ bool CRepeaterProtocolHandler::writeHeader(const CHeaderData& header)
buffer[10] = header.getFlag3();
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++)
buffer[19 + i] = header.getRptCall2().GetChar(i);
buffer[19 + i] = header.getRptCall2()[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++)
buffer[35 + i] = header.getMyCall1().GetChar(i);
buffer[35 + i] = header.getMyCall1()[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
CCCITTChecksumReverse csum;
@ -104,7 +104,7 @@ bool CRepeaterProtocolHandler::writeHeader(const CHeaderData& header)
m_outSeq = 0U;
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Header"), buffer, 49U);
CUtils::dump("Sending Header", buffer, 49U);
#endif
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)
{
wxASSERT(data != NULL);
wxASSERT(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
assert(data != nullptr);
assert(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
unsigned char buffer[30U];
@ -150,7 +150,7 @@ bool CRepeaterProtocolHandler::writeData(const unsigned char* data, unsigned int
::memcpy(buffer + 9U, data, length);
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Data"), buffer, length + 9U);
CUtils::dump("Sending Data", buffer, length + 9U);
#endif
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();
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++)
buffer[19 + i] = header.getRptCall2().GetChar(i);
buffer[19 + i] = header.getRptCall2()[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++)
buffer[35 + i] = header.getMyCall1().GetChar(i);
buffer[35 + i] = header.getMyCall1()[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
CCCITTChecksumReverse csum;
@ -202,7 +202,7 @@ bool CRepeaterProtocolHandler::writeBusyHeader(const CHeaderData& header)
m_outSeq = 0U;
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Busy Header"), buffer, 49U);
CUtils::dump("Sending Busy Header", buffer, 49U);
#endif
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)
{
wxASSERT(data != NULL);
wxASSERT(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
assert(data != nullptr);
assert(length == DV_FRAME_LENGTH_BYTES || length == DV_FRAME_MAX_LENGTH_BYTES);
unsigned char buffer[30U];
@ -242,13 +242,13 @@ bool CRepeaterProtocolHandler::writeBusyData(const unsigned char* data, unsigned
::memcpy(buffer + 9U, data, length);
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Busy Data"), buffer, length + 9U);
CUtils::dump("Sending Busy Data", buffer, length + 9U);
#endif
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];
@ -259,15 +259,16 @@ bool CRepeaterProtocolHandler::writePoll(const wxString& 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++)
buffer[5U + i] = text.GetChar(i);
buffer[5U + i] = text[i];
buffer[5U + length] = 0x00;
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Poll"), buffer, 6U + length);
CUtils::dump("Sending Poll", buffer, 6U + length);
#endif
return m_socket.write(buffer, 6U + length, m_address, m_port);
@ -284,15 +285,16 @@ bool CRepeaterProtocolHandler::writeRegister()
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++)
buffer[5U + i] = m_name.GetChar(i);
buffer[5U + i] = m_name[i];
buffer[5U + length] = 0x00;
#if defined(DUMP_TX)
CUtils::dump(wxT("Sending Register"), buffer, 6U + length);
CUtils::dump("Sending Register", buffer, 6U + length);
#endif
return m_socket.write(buffer, 6U + length, m_address, m_port);
@ -320,10 +322,12 @@ bool CRepeaterProtocolHandler::readPackets()
if (length <= 0)
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) {
wxLogMessage(wxT("Packet received from an invalid source, %08X != %08X and/or %u != %u"), m_address.s_addr, address.s_addr, m_port, port);
CUtils::dump(wxT("Data"), m_buffer, length);
::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("Data", m_buffer, length);
return false;
}
@ -373,7 +377,7 @@ bool CRepeaterProtocolHandler::readPackets()
// Header data
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?
if (m_inId != 0U)
@ -386,7 +390,7 @@ bool CRepeaterProtocolHandler::readPackets()
// User data
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
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;
}
@ -413,7 +417,7 @@ bool CRepeaterProtocolHandler::readPackets()
CHeaderData* CRepeaterProtocolHandler::readHeader()
{
if (m_type != NETWORK_HEADER)
return NULL;
return nullptr;
// If the checksum is 0xFFFF then we accept the header without testing the checksum
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);
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;
return NULL;
return nullptr;
}
return header;
@ -436,6 +440,9 @@ unsigned int CRepeaterProtocolHandler::readData(unsigned char* buffer, unsigned
if (m_type != NETWORK_DATA)
return 0U;
if (m_length < 9U)
return 0U;
unsigned int dataLen = m_length - 9U;
// Is our buffer too small?
@ -462,70 +469,70 @@ unsigned int CRepeaterProtocolHandler::readData(unsigned char* buffer, unsigned
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) {
text = wxT(" ");
reflector = wxT(" ");
text = " ";
reflector = " ";
status = LS_NONE;
return;
}
text = wxString((char*)(m_buffer + 5U), wxConvLocal, 20U);
text = std::string((char*)(m_buffer + 5U), 20U);
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) {
text = wxT(" ");
text = " ";
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)
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)
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)
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)
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)
return wxEmptyString;
return std::string();
return wxString((char*)(m_buffer + 6U), wxConvLocal, 20U);
return std::string((char*)(m_buffer + 6U), 20U);
}
void CRepeaterProtocolHandler::reset()

@ -22,50 +22,70 @@
#include "UDPReaderWriter.h"
#include "DStarDefines.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 {
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();
bool open();
// Sends the header packet twice to reduce the impact of UDP packet loss.
bool writeHeader(const CHeaderData& header);
bool writeBusyHeader(const CHeaderData& header);
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 writePoll(const wxString& text);
bool writePoll(const std::string& text);
bool writeRegister();
// Drains all pending UDP datagrams; returns the type of the last useful one.
NETWORK_TYPE read();
void readText(wxString& text, LINK_STATUS& status, wxString& reflector);
void readTempText(wxString& text);
wxString readStatus1();
wxString readStatus2();
wxString readStatus3();
wxString readStatus4();
wxString readStatus5();
void readText(std::string& text, LINK_STATUS& status, std::string& reflector);
void readTempText(std::string& text);
std::string readStatus1();
std::string readStatus2();
std::string readStatus3();
std::string readStatus4();
std::string readStatus5();
CHeaderData* readHeader();
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 close();
private:
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;
wxString m_name;
wxUint16 m_outId;
wxUint8 m_outSeq;
std::string m_name;
uint16_t m_outId; // Random session ID assigned at the start of each transmission.
uint8_t m_outSeq; // Per-frame sequence number (020, resets on each sync frame).
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 int m_length;
// Reads one UDP datagram and sets m_type; returns true if more may be waiting.
bool readPackets();
};

@ -16,20 +16,40 @@
* 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
#define RingBuffer_H
#include <wx/wx.h>
#include <atomic>
#include <cassert>
#include <cstring>
template<class T> class CRingBuffer {
public:
CRingBuffer(unsigned int length) :
m_length(length),
m_buffer(NULL),
m_buffer(nullptr),
m_iPtr(0U),
m_oPtr(0U)
{
wxASSERT(length > 0U);
assert(length > 0U);
m_buffer = new T[length];
@ -41,6 +61,8 @@ public:
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)
{
if (nSamples > freeSpace())
@ -56,6 +78,8 @@ public:
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 data = dataSize();
@ -73,6 +97,7 @@ public:
return nSamples;
}
// Like getData() but does not advance the read pointer (non-destructive).
unsigned int peek(T* buffer, unsigned int nSamples)
{
unsigned int data = dataSize();
@ -112,7 +137,7 @@ public:
bool hasSpace(unsigned int length) const
{
return freeSpace() > length;
return freeSpace() >= length;
}
bool hasData() const
@ -126,10 +151,10 @@ public:
}
private:
unsigned int m_length;
T* m_buffer;
volatile unsigned int m_iPtr;
volatile unsigned int m_oPtr;
unsigned int m_length; // Allocated slot count (capacity + 1)
T* m_buffer;
std::atomic<unsigned int> m_iPtr; // Write index; advanced only by the producer
std::atomic<unsigned int> m_oPtr; // Read index; advanced only by the consumer
unsigned int dataSize() const
{

@ -19,12 +19,8 @@
#include "SerialDataController.h"
#if !defined(_WIN32)
#include <sys/types.h>
#if defined(__WINDOWS__)
#include <setupapi.h>
#include <winioctl.h>
#else
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <cerrno>
@ -34,253 +30,15 @@
#endif
#if defined(__WINDOWS__)
const unsigned int BUFFER_LENGTH = 1000U;
#if !defined(_WIN32)
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_speed(speed),
m_assertRTS(assertRTS),
m_fd(-1)
{
wxASSERT(!device.IsEmpty());
assert(!device.empty());
}
CSerialDataController::~CSerialDataController()
@ -289,23 +47,23 @@ CSerialDataController::~CSerialDataController()
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) {
wxLogError(wxT("Cannot open device - %s"), m_device.c_str());
::fprintf(stderr, "Cannot open device - %s\n", m_device.c_str());
return false;
}
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);
return false;
}
termios termios;
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);
return false;
}
@ -352,13 +110,13 @@ bool CSerialDataController::open()
::cfsetispeed(&termios, B230400);
break;
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);
return false;
}
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);
return false;
}
@ -366,15 +124,15 @@ bool CSerialDataController::open()
if (m_assertRTS) {
unsigned int y;
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);
return false;
}
y |= TIOCM_RTS;
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);
return false;
}
@ -385,8 +143,8 @@ bool CSerialDataController::open()
int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsigned int timeout)
{
wxASSERT(buffer != NULL);
wxASSERT(m_fd != -1);
assert(buffer != nullptr);
assert(m_fd != -1);
if (length == 0U)
return 0;
@ -404,15 +162,15 @@ int CSerialDataController::read(unsigned char* buffer, unsigned int length, unsi
tv.tv_sec = timeout / 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)
return 0;
} else {
n = ::select(m_fd + 1, &fds, NULL, NULL, NULL);
n = ::select(m_fd + 1, &fds, nullptr, nullptr, nullptr);
}
if (n < 0) {
wxLogError(wxT("Error from select(), errno=%d"), errno);
::fprintf(stderr, "Error from select(), errno=%d\n", errno);
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);
if (len < 0) {
if (errno != EAGAIN) {
wxLogError(wxT("Error from read(), errno=%d"), errno);
::fprintf(stderr, "Error from read(), errno=%d\n", errno);
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)
{
wxASSERT(buffer != NULL);
wxASSERT(m_fd != -1);
assert(buffer != nullptr);
assert(m_fd != -1);
if (length == 0U)
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);
if (n < 0) {
if (errno != EAGAIN) {
wxLogError(wxT("Error returned from write(), errno=%d"), errno);
::fprintf(stderr, "Error returned from write(), errno=%d\n", errno);
return -1;
}
}
@ -461,10 +219,184 @@ int CSerialDataController::write(const unsigned char* buffer, unsigned int lengt
void CSerialDataController::close()
{
wxASSERT(m_fd != -1);
assert(m_fd != -1);
::close(m_fd);
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
#define SerialDataController_H
#include <wx/wx.h>
#include "StdCompat.h"
#if defined(__WINDOWS__)
#if defined(_WIN32)
#include <windows.h>
#endif
@ -38,36 +38,38 @@ enum SERIAL_SPEED {
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 {
public:
CSerialDataController(const wxString& device, SERIAL_SPEED speed, bool assertRTS = false);
CSerialDataController(const std::string& device, SERIAL_SPEED speed, bool assertRTS = false);
~CSerialDataController();
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 write(const unsigned char* buffer, unsigned int length);
void close();
private:
wxString m_device;
std::string m_device;
SERIAL_SPEED m_speed;
bool m_assertRTS;
#if defined(__WINDOWS__)
#if defined(_WIN32)
HANDLE m_handle;
OVERLAPPED m_readOverlapped;
OVERLAPPED m_writeOverlapped;
unsigned char* m_readBuffer;
unsigned int m_readLength;
bool m_readPending;
#else
int m_fd;
#endif
#if defined(__WINDOWS__)
int readNonblock(unsigned char* buffer, unsigned int length, unsigned int timeout);
#endif
};
#endif

@ -19,12 +19,8 @@
#include "SerialLineController.h"
#if !defined(_WIN32)
#include <sys/types.h>
#if defined(__WINDOWS__)
#include <setupapi.h>
#include <winioctl.h>
#else
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <cerrno>
@ -34,17 +30,17 @@
#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_config(config),
m_rts(false),
m_dtr(false),
m_handle(INVALID_HANDLE_VALUE)
m_fd(-1)
{
wxASSERT(!device.IsEmpty());
wxASSERT(config == 1U || config == 2U || config == 3U);
assert(!device.empty());
assert(config == 1U || config == 2U || config == 3U);
}
CSerialLineController::~CSerialLineController()
@ -53,111 +49,111 @@ CSerialLineController::~CSerialLineController()
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 (m_handle == INVALID_HANDLE_VALUE) {
wxLogError(wxT("Cannot open device - %s"), m_device.c_str());
if (::isatty(m_fd) == 0) {
::fprintf(stderr, "%s is not a TTY device\n", m_device.c_str());
::close(m_fd);
return false;
}
DCB dcb;
if (::GetCommState(m_handle, &dcb) == 0) {
wxLogError(wxT("Cannot get the attributes for %s"), m_device.c_str());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
termios termios;
if (::tcgetattr(m_fd, &termios) < 0) {
::fprintf(stderr, "Cannot get the attributes for %s\n", m_device.c_str());
::close(m_fd);
return false;
}
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
// 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 (::SetCommState(m_handle, &dcb) == 0) {
wxLogError(wxT("Cannot set the attributes for %s"), m_device.c_str());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) {
::fprintf(stderr, "Cannot set the attributes for %s\n", m_device.c_str());
::close(m_fd);
return false;
}
if (::EscapeCommFunction(m_handle, CLRDTR) == 0) {
wxLogError(wxT("Cannot clear DTR for %s"), m_device.c_str());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0) {
::fprintf(stderr, "Cannot get the modem status bits for %s\n", m_device.c_str());
::close(m_fd);
return false;
}
if (::EscapeCommFunction(m_handle, CLRRTS) == 0) {
wxLogError(wxT("Cannot clear RTS for %s"), m_device.c_str());
::ClearCommError(m_handle, &errCode, NULL);
::CloseHandle(m_handle);
y &= ~TIOCM_DTR;
y &= ~TIOCM_RTS;
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;
}
::ClearCommError(m_handle, &errCode, NULL);
return true;
}
bool CSerialLineController::getCD() const
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
DWORD status;
DWORD errCode;
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
}
return (status & MS_RLSD_ON) == MS_RLSD_ON;
return (y & TIOCM_CD) == TIOCM_CD;
}
bool CSerialLineController::getCTS() const
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
DWORD status;
DWORD errCode;
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
}
return (status & MS_CTS_ON) == MS_CTS_ON;
return (y & TIOCM_CTS) == TIOCM_CTS;
}
bool CSerialLineController::getDSR() const
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
DWORD status;
DWORD errCode;
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
}
return (status & MS_DSR_ON) == MS_DSR_ON;
return (y & TIOCM_DSR) == TIOCM_DSR;
}
bool CSerialLineController::setRTS(bool set)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
if (set == m_rts)
return true;
DWORD rts = (set) ? SETRTS : CLRRTS;
DWORD errCode;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
if (set)
y |= TIOCM_RTS;
else
y &= ~TIOCM_RTS;
if (::EscapeCommFunction(m_handle, rts) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return false;
}
m_rts = set;
@ -166,18 +162,22 @@ bool CSerialLineController::setRTS(bool set)
bool CSerialLineController::setDTR(bool set)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
if (set == m_dtr)
return true;
DWORD dtr = (set) ? SETDTR : CLRDTR;
DWORD errCode;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return false;
if (::EscapeCommFunction(m_handle, dtr) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
if (set)
y |= TIOCM_DTR;
else
y &= ~TIOCM_DTR;
if (::ioctl(m_fd, TIOCMSET, &y) < 0)
return false;
}
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)
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
inp1 = inp2 = inp3 = inp4 = inp5 = false;
DWORD status;
DWORD errCode;
if (::GetCommModemStatus(m_handle, &status) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return;
}
switch (m_config) {
case 1U:
inp1 = (status & MS_DSR_ON) == MS_DSR_ON;
inp2 = (status & MS_CTS_ON) == MS_CTS_ON;
inp1 = (y & TIOCM_DSR) == TIOCM_DSR;
inp2 = (y & TIOCM_CTS) == TIOCM_CTS;
break;
case 2U:
inp1 = (status & MS_RLSD_ON) == MS_RLSD_ON;
inp2 = (status & MS_RLSD_ON) == MS_RLSD_ON;
inp1 = (y & TIOCM_CD) == TIOCM_CD;
inp2 = (y & TIOCM_CD) == TIOCM_CD;
break;
case 3U:
inp1 = (status & MS_RLSD_ON) == MS_RLSD_ON;
inp1 = (y & TIOCM_CD) == TIOCM_CD;
break;
default:
wxLogError(wxT("Unknown serial config - %u"), m_config);
::fprintf(stderr, "Unknown serial config - %u\n", m_config);
break;
}
}
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) {
case 1U:
if (outp1 != m_dtr) {
DWORD dtr = (outp1) ? SETDTR : CLRDTR;
DWORD errCode;
if (::EscapeCommFunction(m_handle, dtr) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return;
}
m_dtr = outp1;
}
if (outp3 != m_rts) {
DWORD rts = (outp3) ? SETRTS : CLRRTS;
DWORD errCode;
if (::EscapeCommFunction(m_handle, rts) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return;
}
m_rts = outp3;
}
break;
if (m_config == 1U) {
if (outp1 == m_dtr && outp3 == m_rts)
return;
case 2U:
case 3U:
if (outp1 != m_rts) {
DWORD rts = (outp1) ? SETRTS : CLRRTS;
DWORD errCode;
if (::EscapeCommFunction(m_handle, rts) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return;
}
m_rts = outp1;
}
if (outp3 != m_dtr) {
DWORD dtr = (outp3) ? SETDTR : CLRDTR;
DWORD errCode;
if (::EscapeCommFunction(m_handle, dtr) == 0) {
::ClearCommError(m_handle, &errCode, NULL);
return;
}
m_dtr = outp3;
}
break;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
return;
default:
wxLogError(wxT("Unknown serial config - %u"), m_config);
break;
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_rts = outp3;
} else if (m_config == 2U || m_config == 3U) {
if (outp1 == m_rts && outp3 == m_dtr)
return;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
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_dtr = outp3;
} else {
::fprintf(stderr, "Unknown serial config - %u\n", m_config);
}
}
void CSerialLineController::close()
{
wxASSERT(m_handle != INVALID_HANDLE_VALUE);
assert(m_fd != -1);
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
::close(m_fd);
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_config(config),
m_rts(false),
m_dtr(false),
m_fd(-1)
m_handle(INVALID_HANDLE_VALUE)
{
wxASSERT(!device.IsEmpty());
wxASSERT(config == 1U || config == 2U || config == 3U);
assert(!device.empty());
assert(config == 1U || config == 2U || config == 3U);
}
CSerialLineController::~CSerialLineController()
@ -304,110 +294,74 @@ CSerialLineController::~CSerialLineController()
bool CSerialLineController::open()
{
wxASSERT(m_fd == -1);
m_fd = ::open(m_device.mb_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0);
if (m_fd < 0) {
wxLogError(wxT("Cannot open device - %s"), m_device.c_str());
return false;
}
if (::isatty(m_fd) == 0) {
wxLogError(wxT("%s is not a TTY device"), m_device.c_str());
::close(m_fd);
return false;
}
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);
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;
}
y &= ~TIOCM_DTR;
y &= ~TIOCM_RTS;
// Deassert both DTR and RTS on open, matching the POSIX behaviour.
::EscapeCommFunction(m_handle, CLRDTR);
::EscapeCommFunction(m_handle, CLRRTS);
if (::ioctl(m_fd, TIOCMSET, &y) < 0) {
wxLogError(wxT("Cannot set the modem status bits for %s"), m_device.c_str());
::close(m_fd);
return false;
}
m_rts = false;
m_dtr = false;
return true;
}
bool CSerialLineController::getCD() const
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
DWORD status = 0U;
if (!::GetCommModemStatus(m_handle, &status))
return false;
return (y & TIOCM_CD) == TIOCM_CD;
return (status & MS_RLSD_ON) != 0U;
}
bool CSerialLineController::getCTS() const
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
DWORD status = 0U;
if (!::GetCommModemStatus(m_handle, &status))
return false;
return (y & TIOCM_CTS) == TIOCM_CTS;
return (status & MS_CTS_ON) != 0U;
}
bool CSerialLineController::getDSR() const
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
DWORD status = 0U;
if (!::GetCommModemStatus(m_handle, &status))
return false;
return (y & TIOCM_DSR) == TIOCM_DSR;
return (status & MS_DSR_ON) != 0U;
}
bool CSerialLineController::setRTS(bool set)
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
if (set == m_rts)
return true;
unsigned int y;
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)
if (!::EscapeCommFunction(m_handle, set ? SETRTS : CLRRTS))
return false;
m_rts = set;
@ -417,21 +371,12 @@ bool CSerialLineController::setRTS(bool set)
bool CSerialLineController::setDTR(bool set)
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
if (set == m_dtr)
return true;
unsigned int y;
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)
if (!::EscapeCommFunction(m_handle, set ? SETDTR : CLRDTR))
return false;
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)
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
inp1 = inp2 = inp3 = inp4 = inp5 = false;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
DWORD status = 0U;
if (!::GetCommModemStatus(m_handle, &status))
return;
switch (m_config) {
case 1U:
inp1 = (y & TIOCM_DSR) == TIOCM_DSR;
inp2 = (y & TIOCM_CTS) == TIOCM_CTS;
inp1 = (status & MS_DSR_ON) != 0U;
inp2 = (status & MS_CTS_ON) != 0U;
break;
case 2U:
inp1 = (y & TIOCM_CD) == TIOCM_CD;
inp2 = (y & TIOCM_CD) == TIOCM_CD;
inp1 = (status & MS_RLSD_ON) != 0U;
inp2 = (status & MS_RLSD_ON) != 0U;
break;
case 3U:
inp1 = (y & TIOCM_CD) == TIOCM_CD;
inp1 = (status & MS_RLSD_ON) != 0U;
break;
default:
wxLogError(wxT("Unknown serial config - %u"), m_config);
::fprintf(stderr, "Unknown serial config - %u\n", m_config);
break;
}
}
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 (outp1 == m_dtr && outp3 == m_rts)
return;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
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;
::EscapeCommFunction(m_handle, outp1 ? SETDTR : CLRDTR);
::EscapeCommFunction(m_handle, outp3 ? SETRTS : CLRRTS);
m_dtr = outp1;
m_rts = outp3;
@ -498,36 +429,22 @@ void CSerialLineController::setDigitalOutputs(bool outp1, bool, bool outp3, bool
if (outp1 == m_rts && outp3 == m_dtr)
return;
unsigned int y;
if (::ioctl(m_fd, TIOCMGET, &y) < 0)
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;
::EscapeCommFunction(m_handle, outp1 ? SETRTS : CLRRTS);
::EscapeCommFunction(m_handle, outp3 ? SETDTR : CLRDTR);
m_rts = outp1;
m_dtr = outp3;
} else {
wxLogError(wxT("Unknown serial config - %u"), m_config);
::fprintf(stderr, "Unknown serial config - %u\n", m_config);
}
}
void CSerialLineController::close()
{
wxASSERT(m_fd != -1);
assert(m_handle != INVALID_HANDLE_VALUE);
::close(m_fd);
m_fd = -1;
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
}
#endif
#endif // _WIN32

@ -21,27 +21,39 @@
#define SerialLineController_H
#include "HardwareController.h"
#include "StdCompat.h"
#include <wx/wx.h>
#if defined(__WINDOWS__)
#if defined(_WIN32)
#include <windows.h>
#endif
// Serial modem control lines used for PTT / COR signalling.
enum SERIALPIN {
SERIAL_CD,
SERIAL_CTS,
SERIAL_DSR,
SERIAL_DTR,
SERIAL_RTS,
SERIAL_ECHOLINK
SERIAL_CD, // Carrier Detect — input, indicates received signal.
SERIAL_CTS, // Clear To Send — input.
SERIAL_DSR, // Data Set Ready — input.
SERIAL_DTR, // Data Terminal Ready — output, used for PTT on some hardware.
SERIAL_RTS, // Request To Send — output, most common PTT line.
SERIAL_ECHOLINK // EchoLink-compatible pin assignment variant.
};
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 {
public:
CSerialLineController(const wxString& device, unsigned int config = 1U);
CSerialLineController(const std::string& device, unsigned int config = 1U);
virtual ~CSerialLineController();
virtual bool open();
@ -59,11 +71,11 @@ public:
virtual void close();
private:
wxString m_device;
unsigned int m_config;
std::string m_device;
unsigned int m_config; // Pin assignment scheme index.
bool m_rts;
bool m_dtr;
#if defined(__WINDOWS__)
#if defined(_WIN32)
HANDLE m_handle;
#else
int m_fd;

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

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

@ -15,16 +15,19 @@
#include "SlowDataDecoder.h"
#include "DStarDefines.h"
#include <cassert>
#include <cstring>
const unsigned int SLOW_DATA_BLOCK_SIZE = 6U;
const unsigned int SLOW_DATA_FULL_BLOCK_SIZE = (SLOW_DATA_BLOCK_SIZE - 1U) * 10U;
CSlowDataDecoder::CSlowDataDecoder() :
m_buffer(NULL),
m_buffer(nullptr),
m_state(SDD_FIRST),
m_headerData(NULL),
m_headerData(nullptr),
m_headerPtr(0U),
m_header(NULL)
m_header(nullptr)
{
m_buffer = new unsigned char[SLOW_DATA_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)
{
wxASSERT(data != NULL);
assert(data != nullptr);
switch (m_state) {
case SDD_FIRST:
@ -57,13 +60,13 @@ void CSlowDataDecoder::addData(const unsigned char* data)
m_state = SDD_FIRST;
processHeader();
break;
}
}
}
CHeaderData* CSlowDataDecoder::getHeaderData()
{
if (m_header == NULL)
return NULL;
if (m_header == nullptr)
return nullptr;
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));
delete m_header;
m_header = NULL;
m_header = nullptr;
}
void CSlowDataDecoder::processHeader()
{
// Do we have a complete and valid header already?
if (m_header != NULL)
if (m_header != nullptr)
return;
for (unsigned int i = 1U; i <= (SLOW_DATA_BLOCK_SIZE - 1U); i++, m_headerPtr++) {

@ -16,11 +16,28 @@
#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 {
SDD_FIRST,
SDD_SECOND
SDD_FIRST, // Waiting for the first 3-byte half of a slow-data block.
SDD_SECOND // Waiting for the second half to complete the block.
};
class CSlowDataDecoder {
@ -28,19 +45,23 @@ public:
CSlowDataDecoder();
~CSlowDataDecoder();
// Feed one 3-byte slow-data field (raw, still scrambled).
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();
// Called on each data-sync frame to re-align the two-phase accumulator.
void sync();
void reset();
private:
unsigned char* m_buffer;
unsigned char* m_buffer; // Accumulates the current 6-byte block.
SDD_STATE m_state;
unsigned char* m_headerData;
unsigned char* m_headerData; // Circular buffer of decoded payload bytes.
unsigned int m_headerPtr;
CHeaderData* m_header;
CHeaderData* m_header; // Decoded header, held until getHeaderData() is called.
void processHeader();
bool processHeader(const unsigned char* bytes);

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

@ -16,27 +16,48 @@
#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 {
public:
CSlowDataEncoder();
~CSlowDataEncoder();
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);
// Returns the next 3 scrambled bytes of the text slow-data sequence.
void getTextData(unsigned char* data);
void reset();
// Resets read pointers to the start of the sequence (call on each sync frame).
void sync();
private:
unsigned char* m_headerData;
unsigned char* m_textData;
unsigned int m_headerPtr;
unsigned int m_textPtr;
unsigned char* m_headerData; // Pre-packed, unscrambled header slow-data bytes.
unsigned char* m_textData; // Pre-packed, unscrambled text slow-data bytes.
unsigned int m_headerPtr; // Current read offset into m_headerData.
unsigned int m_textPtr; // Current read offset into m_textData.
};
#endif

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

Loading…
Cancel
Save

Powered by TurnKey Linux.