Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copilot Instructions for DroidStar

## Project Overview

DroidStar is a cross-platform amateur radio digital voice client that connects to M17, D-STAR (REF/XRF/DCS), DMR, YSF/FCS, P25, NXDN reflectors and AllStar nodes (IAX2). Built with C++/Qt6 Quick (QML) and targets Linux, Windows, macOS, Android, and iOS.

Licensed under GPL v3.

## Build

Requires **Qt6 >= 6.5** with modules: Core, Gui, Multimedia, Network, Quick, QuickControls2, SerialPort (except iOS).

```bash
mkdir build && cd build
cmake ..
make
```

### Compile-time feature flags (in CMakeLists.txt)

- `VOCODER_PLUGIN` — build without internal AMBE vocoder (plugin-only mode)
- `USE_FLITE` — enable Flite text-to-speech for TX
- `USE_EXTERNAL_CODEC2` — link system codec2 instead of bundled C++ port
- `USE_MD380_VOCODER` — enable md380 vocoder (ARM platforms, requires external lib)

Toggle these by changing `if(TRUE)` / `if(FALSE)` blocks or uncommenting `DEFINES+=` lines in CMakeLists.txt.

## Architecture

### Core layers

1. **QML UI** (`Main.qml`, `MainTab.qml`, `SettingsTab.qml`, `LogTab.qml`, `HostsTab.qml`, `AboutTab.qml`) — Qt Quick tabbed interface. The `DroidStar` C++ type is registered as `org.dudetronics.droidstar` and instantiated directly in QML.

2. **DroidStar** (`droidstar.h/.cpp`) — Application controller (QObject). Manages settings, host lists, device discovery, and user interactions. Creates protocol `Mode` instances and moves them to a worker `QThread`.

3. **Mode** (`mode.h/.cpp`) — Abstract base class for all digital voice protocols. Owns the `QUdpSocket`, audio engine, vocoder state, and MMDVM modem connection. Each protocol subclass implements `process_udp()`, `transmit()`, `send_ping()`, `send_disconnect()`, and `hostname_lookup()`.

4. **Protocol subclasses** — One per protocol, all inheriting `Mode`:
- `REF`, `XRF`, `DCS` — D-STAR reflector protocols
- `DMR` — DMR with Brandmeister/TGIF/DMR+ auth
- `YSF` — Yaesu System Fusion (also used for FCS)
- `P25`, `NXDN` — P25 and NXDN reflectors
- `M17` — M17 protocol with Codec2 vocoder
- `IAX` — AllStar IAX2 client

5. **AudioEngine** (`audioengine.h/.cpp`) — Wraps Qt audio I/O with AGC, capture/playback queues, and 8kHz PCM processing.

### Factory pattern

`Mode::create_mode(QString)` in `mode.cpp` is the factory that instantiates protocol subclasses by name string (e.g., `"DMR"`, `"M17"`). The `DroidStar` controller calls this, then moves the `Mode` to its own thread and wires up Qt signal/slot connections.

### Vocoder system

Three vocoder paths exist:
- **Hardware**: USB AMBE devices via `SerialAMBE` (not available on iOS)
- **Software plugin**: Dynamic loading (`dlopen`/`LoadLibrary`) via the `Vocoder` interface in `vocoder_plugin.h`
- **Bundled**: `codec2/` (C++ port for M17), `imbe_vocoder/` (P25), `mbe/` (DMR/D-STAR/YSF/NXDN)

### Threading model

Each active `Mode` runs on its own `QThread`. Communication between the QML UI ↔ `DroidStar` controller ↔ `Mode` worker is entirely via Qt signals and slots.

## Key Conventions

- **Flat source layout**: All protocol implementations and helpers are in the project root (no `src/` directory).
- **Platform conditionals**: Heavy use of `#ifdef Q_OS_ANDROID`, `Q_OS_IOS`, `Q_OS_WIN` and CMake `if(IOS)` / `if(ANDROID)` / `if(WIN32)` blocks. iOS excludes serial port support entirely.
- **No test suite**: There are no automated tests in this project.
- **Settings persistence**: `QSettings` stores all user config; `save_settings()` is called from nearly every setter in `DroidStar`.
- **Host files**: Reflector/server lists (DMRHosts.txt, DMRIDs.data, etc.) are downloaded at runtime and stored in the platform's config directory (`~/.config/dudetronics` on Linux).
- **Signal/slot wiring**: `DroidStar::process_connect()` contains extensive signal/slot connections between the controller and the active `Mode`. When adding new protocol features, connections must be added here.
- **QML registration**: The `DroidStar` type is registered via `qmlRegisterType` in `main.cpp` and accessed from QML as a singleton-like instance.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.DS_Store
build/*
build_*/*
build/
.vscode/
20 changes: 17 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(DroidStar VERSION 1.0 LANGUAGES C CXX)
project(DroidStar VERSION 0.90.0 LANGUAGES C CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

Expand Down Expand Up @@ -103,7 +103,7 @@ qt_add_qml_module(DroidStar
)

execute_process(
COMMAND git rev-parse --short HEAD
COMMAND git describe --tags --always
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
OUTPUT_VARIABLE VERSION_NUMBER
OUTPUT_STRIP_TRAILING_WHITESPACE
Expand Down Expand Up @@ -145,9 +145,23 @@ endif()

if(APPLE)
set_target_properties(DroidStar PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
set_target_properties(DroidStar PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER org.dudetronics.droidstar
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER org.dudetronics.droidstar
XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym"
)
target_link_libraries(DroidStar PRIVATE
"-framework AVFoundation"
)
if(IOS)
set_target_properties(DroidStar PROPERTIES
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
)
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Images.xcassets PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
target_sources(DroidStar PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Images.xcassets")
include(${Qt6Multimedia_DIR}/Qt6MultimediaMacros.cmake)
qt_add_ios_ffmpeg_libraries(DroidStar)
endif()
endif()

if(ANDROID)
Expand Down Expand Up @@ -210,7 +224,7 @@ if(FALSE) # set TRUE for flite
)
endif()

if(TRUE) # set TRUE for md380_vocoder
if(NOT IOS) # md380_vocoder is not available on iOS
target_compile_definitions(DroidStar PRIVATE
USE_MD380_VOCODER
)
Expand Down
10 changes: 10 additions & 0 deletions DroidStar.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"git.ignoreLimitWarning": true
}
}
19 changes: 18 additions & 1 deletion Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,32 @@
<string>DroidStar</string>
<key>CFBundleIconFile</key>
<string>droidstar.icns</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.yourcompany.90d5dd37.DroidStar</string>
<string>org.dudetronics.droidstar</string>
<key>CFBundleName</key>
<string>DroidStar</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.90.0</string>
<key>CFBundleVersion</key>
<string>94</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>DroidStar does not use the camera, but a linked library references the Camera API.</string>
<key>NSMicrophoneUsageDescription</key>
<string>You know you want to...</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
44 changes: 32 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,13 @@ I added Flite TTS TX capability so I didn't have to talk to myself all of the ti
# Usage
Linux users with USB AMBE and/or MMDVM dongles will need to make sure they have permission to use the USB serial device, and disable the archaic ModeManager service that still exists on many Linux systems. On most systems this means adding your user to the 'dialout' group, and running 'sudo systemctl disable ModemManager.service' and rebooting. This is a requirement for any serial device to be accessed.

Host/Mod: Select the desired host and module (for D-STAR and M17) from the selections.

Callsign: Enter your amateur radio callsign. A valid license is required to use this software.

DMRID: A valid DMR ID is required to connect to DMR servers.
Latitude/Longitude/Location/Description: These are DMR config options, sent to the DMR server during connect. Some servers require specific values here, some do not. This is specific to the server you are connecting to, so please dont ask what these values should be.

DMR+ IPSC2 hosts: The format for the DMR+ options string is the complete string including 'Options='. Create your options string and check 'Send DMR+ options on connect' before connecting. A description of the DMR+ options string can be found here: https://github.com/g4klx/MMDVMHost/blob/master/DMRplus_startup_options.md .

Talkgroup: For DMR, enter the talkgroup ID number. A very active TG for testing functionality on Brandmeister is 91 (Brandmeister Worldwide). You must TX with a talkgroup entered to link to that talkgroup, just like a real radio. Any statics you have defined in BM selfcare will work the same way they do if you were using a hotspot/radio.

MYCALL/URCALL/RPTR1/RPTR2 are for Dstar modes REF/DCS/XRF. These fields need to be entered correctly before attempting to TX on any DSTAR reflector. All fields are populated with suggested values upon connect, but can still be modified for advanced users. RPT2 is always overwritten with the current reflector upon connected.
* Host/Mod: Select the desired host and module (for D-STAR and M17) from the selections.
* Callsign: Enter your amateur radio callsign. A valid license is required to use this software.
* DMRID: A valid DMR ID is required to connect to DMR servers.
* Latitude/Longitude/Location/Description: These are DMR config options, sent to the DMR server during connect. Some servers require specific values here, some do not. This is specific to the server you are connecting to, so please dont ask what these values should be.
* DMR+ IPSC2 hosts: The format for the DMR+ options string is the complete string including 'Options='. Create your options string and check 'Send DMR+ options on connect' before connecting. A description of the DMR+ options string can be found here: https://github.com/g4klx/MMDVMHost/blob/master/DMRplus_startup_options.md .
* Talkgroup: For DMR, enter the talkgroup ID number. A very active TG for testing functionality on Brandmeister is 91 (Brandmeister Worldwide). You must TX with a talkgroup entered to link to that talkgroup, just like a real radio. Any statics you have defined in BM selfcare will work the same way they do if you were using a hotspot/radio.
* MYCALL/URCALL/RPTR1/RPTR2 are for Dstar modes REF/DCS/XRF. These fields need to be entered correctly before attempting to TX on any DSTAR reflector. All fields are populated with suggested values upon connect, but can still be modified for advanced users. RPT2 is always overwritten with the current reflector upon connected.

# IAX Client for AllStar
DroidStar can connect to an AllStar node as an IAX(2) client. See the AllStar wiki and other AllStar, Asterisk, and IAX2 protocal related websites for the technical details of IAX2 for AllStar. This is a basic client and currently only uLaw audio codec is supported. This is the default codec on most AllStar nodes.
Expand Down Expand Up @@ -100,6 +95,31 @@ My primary development platform is Fedora Linux. With a proper build environmen

All of the gradle build files are provided to create an APK file ready to be installed on an Android device. A proper Android build system including the Android NDK is required and beyond the scope of this document.

## Note for building for iOS
Building for iOS is only possible on a mac with xcode installed. In a first step you have install a recent QT version; a convenient way
to do this is using aqtinstall as follows:
```
pip3 install aqtinstall
QT_VERSION=6.6.1

mkdir build_ios
cd build_ios

aqt install-qt mac desktop $QT_VERSION clang_64
aqt install-qt mac ios $QT_VERSION
aqt install-qt mac ios $QT_VERSION -m qtmultimedia
```
Afterwards you can create the xcode project using the qmake version for iOS, i.e.:
```
./$QT_VERSION/ios/bin/qmake ../DroidStar.pro
make
```
Once this is done, you can open the xcode project
```
open DroidStar.xcodeproj
```
and build (or deploy to Testflight) as with any other iOS app.

# No builds are available on Github
No builds for any platform are available on this Github site. This is and always will be an open source project, to be used for educational and development purposes only. I am currently providing a Windows build which is *not* to be confused as any sort of official release of any kind. No support will be provided for any build at any time.

Expand Down
2 changes: 1 addition & 1 deletion audioengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ uint16_t AudioEngine::read(int16_t *pcm)
s = 160;
}
else{
s = m_audioinq.size();
s = (int)m_audioinq.size();
}

for(int i = 0; i < s; ++i){
Expand Down
4 changes: 2 additions & 2 deletions codec2/quantise.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ void CQuantize::encode_lspds_scalar(int indexes[], float lsp[], int order)
k = lsp_cbd[i].k;
m = lsp_cbd[i].m;
cb = lsp_cbd[i].cb;
indexes[i] = quantise(cb, &dlsp[i], wt, k, m, &se);
indexes[i] = (int)quantise(cb, &dlsp[i], wt, k, m, &se);
dlsp_[i] = cb[indexes[i]*k];


Expand Down Expand Up @@ -572,7 +572,7 @@ void CQuantize::encode_lsps_scalar(int indexes[], float lsp[], int order)
k = lsp_cb[i].k;
m = lsp_cb[i].m;
cb = lsp_cb[i].cb;
indexes[i] = quantise(cb, &lsp_hz[i], wt, k, m, &se);
indexes[i] = (int)quantise(cb, &lsp_hz[i], wt, k, m, &se);
}
}

Expand Down
2 changes: 1 addition & 1 deletion dcs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ void DCS::process_udp()
static int sd_seq = 0;
static char user_data[21];
buf.resize(200);
int size = m_udp->readDatagram(buf.data(), buf.size(), &sender, &senderPort);
int size = (int)m_udp->readDatagram(buf.data(), buf.size(), &sender, &senderPort);

if(m_debug){
QDebug debug = qDebug();
Expand Down
6 changes: 3 additions & 3 deletions dmr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ void DMR::process_udp()
char latitude[20U];
char longitude[20U];

sprintf(latitude, "%08f", m_lat.toFloat());
sprintf(longitude, "%09f", m_lon.toFloat());
snprintf(latitude, sizeof(latitude), "%08f", m_lat.toFloat());
snprintf(longitude, sizeof(longitude), "%09f", m_lon.toFloat());

char *p;
if((p = strchr(latitude, ',')) != NULL){
Expand All @@ -185,7 +185,7 @@ void DMR::process_udp()
if((p = strchr(longitude, ',')) != NULL){
*p = '.';
}
::sprintf(buffer + 8U, "%-8.8s%09u%09u%02u%02u%8.8s%9.9s%03d%-20.20s%-19.19s%c%-124.124s%-40.40s%-40.40s", m_modeinfo.callsign.toStdString().c_str(),
::snprintf(buffer + 8U, sizeof(buffer) - 8U, "%-8.8s%09u%09u%02u%02u%8.8s%9.9s%03d%-20.20s%-19.19s%c%-124.124s%-40.40s%-40.40s", m_modeinfo.callsign.toStdString().c_str(),
m_freq.toUInt(), m_freq.toUInt(), 1, 1, latitude, longitude, 0, m_location.toStdString().c_str(), m_desc.toStdString().c_str(), '4',
m_url.toStdString().c_str(), m_swid.toStdString().c_str(), m_pkid.toStdString().c_str());
out.append(buffer, 302);
Expand Down
2 changes: 1 addition & 1 deletion httpmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ void HttpManager::http_finished(QNetworkReply *reply)
QStringList l = m_filename.split('_');
if((l.at(0) != "/vocoder") && (l.size() > 1)) return;
QFile *hosts_file = new QFile(m_config_path + m_filename);
hosts_file->open(QIODevice::WriteOnly);
(void)hosts_file->open(QIODevice::WriteOnly);
QFileInfo fileInfo(hosts_file->fileName());
QString filename(fileInfo.fileName());
hosts_file->write(reply->readAll());
Expand Down
2 changes: 1 addition & 1 deletion m17.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ void M17::encode_callsign(uint8_t *callsign)
memset(cs, 0, sizeof(cs));
memcpy(cs, callsign, strlen((char *)callsign));
uint64_t encoded = 0;
for(int i = std::strlen((char *)callsign)-1; i >= 0; i--) {
for(int i = (int)std::strlen((char *)callsign)-1; i >= 0; i--) {
auto pos = m17_alphabet.find(cs[i]);
if (pos == std::string::npos) {
pos = 0;
Expand Down
2 changes: 1 addition & 1 deletion ysf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ void YSF::process_udp()

if(m_refname.left(3) == "FCS"){
char info[100U];
::sprintf(info, "%9u%9u%-6.6s%-12.12s%7u", 438000000, 438000000, "AA00AA", "MMDVM", 1234567);
::snprintf(info, sizeof(info), "%9u%9u%-6.6s%-12.12s%7u", 438000000, 438000000, "AA00AA", "MMDVM", 1234567);
::memset(info + 43U, ' ', 57U);
out.append(info, 100);
m_udp->writeDatagram(out, m_address, m_modeinfo.port);
Expand Down