diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d61c6c6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -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. diff --git a/.gitignore b/.gitignore index 6f31401..eea1972 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.DS_Store +build/* +build_*/* build/ .vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 38c89cc..67901c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -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 @@ -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) @@ -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 ) diff --git a/DroidStar.code-workspace b/DroidStar.code-workspace new file mode 100644 index 0000000..fd3b45f --- /dev/null +++ b/DroidStar.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "git.ignoreLimitWarning": true + } +} \ No newline at end of file diff --git a/Info.plist b/Info.plist index 450494d..f053a6f 100755 --- a/Info.plist +++ b/Info.plist @@ -8,15 +8,32 @@ DroidStar CFBundleIconFile droidstar.icns + CFBundleIconName + AppIcon CFBundleIdentifier - com.yourcompany.90d5dd37.DroidStar + org.dudetronics.droidstar CFBundleName DroidStar CFBundlePackageType APPL + CFBundleShortVersionString + 0.90.0 + CFBundleVersion + 94 + ITSAppUsesNonExemptEncryption + + NSCameraUsageDescription + DroidStar does not use the camera, but a linked library references the Camera API. NSMicrophoneUsageDescription You know you want to... UILaunchStoryboardName LaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + diff --git a/README.md b/README.md index 8841ad4..8de63d9 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/audioengine.cpp b/audioengine.cpp index 381e3c1..f384886 100644 --- a/audioengine.cpp +++ b/audioengine.cpp @@ -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){ diff --git a/codec2/quantise.cpp b/codec2/quantise.cpp index dd3e4ef..61ff07b 100644 --- a/codec2/quantise.cpp +++ b/codec2/quantise.cpp @@ -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]; @@ -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); } } diff --git a/dcs.cpp b/dcs.cpp index f88aa37..e472e20 100755 --- a/dcs.cpp +++ b/dcs.cpp @@ -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(); diff --git a/dmr.cpp b/dmr.cpp index eeb6852..ece13be 100755 --- a/dmr.cpp +++ b/dmr.cpp @@ -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){ @@ -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); diff --git a/httpmanager.cpp b/httpmanager.cpp index 27cda83..c2bd832 100644 --- a/httpmanager.cpp +++ b/httpmanager.cpp @@ -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()); diff --git a/m17.cpp b/m17.cpp index 744ba28..512c8f3 100755 --- a/m17.cpp +++ b/m17.cpp @@ -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; diff --git a/ysf.cpp b/ysf.cpp index b3977a5..631e1fe 100755 --- a/ysf.cpp +++ b/ysf.cpp @@ -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);