Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@ Release/
x64/
x86/
out/

# iOS build trees and Qt installer logs
/build-ios*/
aqtinstall.log
149 changes: 139 additions & 10 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,26 @@ if(ANDROID)
endif()

# Core Qt components (GuiPrivate handled separately for cross-platform compatibility)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network Multimedia ShaderTools Gui SerialPort Svg)
# QtSerialPort is used only by the desktop serial keyer path; the HaliKey/KPOD
# serial code is stubbed on both iOS and Android, so require SerialPort on
# desktop only (the mobile Qt kits may not ship it).
set(QK4_QT_COMPONENTS Core Widgets Network Multimedia ShaderTools Gui Svg)
if(NOT IOS AND NOT ANDROID)
list(APPEND QK4_QT_COMPONENTS SerialPort)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${QK4_QT_COMPONENTS})

# TLS-PSK backend for the K4 link. Qt's iOS kit ships only the Secure
# Transport TLS plugin, which has no PSK support, so iOS drives a statically
# linked OpenSSL directly (see src/network/psktlssocket_openssl.cpp).
if(IOS)
set(_qk4_psk_openssl_default ON)
else()
set(_qk4_psk_openssl_default OFF)
endif()
option(QK4_PSK_TLS_OPENSSL "Use OpenSSL directly for TLS-PSK instead of QSslSocket" ${_qk4_psk_openssl_default})
unset(_qk4_psk_openssl_default)
set(QK4_OPENSSL_ROOT "" CACHE PATH "OpenSSL install prefix (include/, lib/) used when QK4_PSK_TLS_OPENSSL is ON")

# GuiPrivate is needed for QRhi access but may not have CMake config on all platforms
if(APPLE)
Expand Down Expand Up @@ -130,6 +149,26 @@ if(ANDROID)
"Android build requires Opus. Provide -DQK4_OPUS_INCLUDE_DIR and -DQK4_OPUS_LIBRARY, "
"or install an Opus package that exports Opus::opus.")
endif()
elseif(IOS)
# iOS: HID/serial/MIDI hardware is stubbed. Opus and OpenSSL are static
# fat libraries (x86_64 simulator + arm64 device) under third_party/ios;
# see third_party/ios/README.md for how they are built.
set(HIDAPI_INCLUDE_DIRS "")
set(HIDAPI_LIBRARIES "")
if(NOT QK4_OPUS_INCLUDE_DIR OR NOT QK4_OPUS_LIBRARY)
set(QK4_OPUS_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/ios/opus/include")
set(QK4_OPUS_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/third_party/ios/opus/lib/libopus.a")
endif()
if(NOT EXISTS "${QK4_OPUS_INCLUDE_DIR}/opus/opus.h" OR NOT EXISTS "${QK4_OPUS_LIBRARY}")
message(FATAL_ERROR
"iOS build requires a static Opus. Expected ${QK4_OPUS_INCLUDE_DIR}/opus/opus.h and "
"${QK4_OPUS_LIBRARY}; see third_party/ios/README.md.")
endif()
set(OPUS_INCLUDE_DIRS "${QK4_OPUS_INCLUDE_DIR}")
set(OPUS_LIBRARIES "${QK4_OPUS_LIBRARY}")
if(NOT QK4_OPENSSL_ROOT)
set(QK4_OPENSSL_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/third_party/ios/openssl")
endif()
elseif(APPLE)
# macOS with Homebrew
set(OPUS_PREFIX "/opt/homebrew/opt/opus")
Expand Down Expand Up @@ -157,6 +196,7 @@ set(SOURCES
src/main.cpp
src/mainwindow.cpp
src/network/tcpclient.cpp
src/network/psktlssocket.h
src/network/protocol.cpp
src/network/kpa1500client.cpp
src/network/catserver.cpp
Expand Down Expand Up @@ -228,18 +268,47 @@ set(SOURCES
src/ui/k4popupbase.cpp
src/ui/kpa1500panel.cpp
src/ui/kpa1500window.cpp
src/ui/adjustoverlay.cpp
src/ui/sidecontroloverlay.cpp
src/ui/monoverlay.cpp
src/ui/baloverlay.cpp
src/ui/wheelaccumulator.cpp
)

if(QK4_PSK_TLS_OPENSSL)
list(APPEND SOURCES src/network/psktlssocket_openssl.cpp)
else()
list(APPEND SOURCES src/network/psktlssocket_qssl.cpp)
endif()

if(IOS)
# iOS-only AVAudioSession backend (Objective-C++). Needed so the mic can be
# captured for TX; RX works without it. Built with ARC.
list(APPEND SOURCES
src/ios/iosaudiosession.mm src/ios/iosaudiosession.h
src/ios/iosorientation.mm src/ios/iosorientation.h
src/ios/iossecurecredentials.mm src/ios/iossecurecredentials.h)
set_source_files_properties(
src/ios/iosaudiosession.mm src/ios/iosorientation.mm src/ios/iossecurecredentials.mm
PROPERTIES COMPILE_FLAGS "-fobjc-arc")
endif()

if(ANDROID)
# Keep class availability on Android via no-op implementations.
# Android: KPOD/HaliKey serial+MIDI keying is stubbed (no-op implementations).
list(APPEND SOURCES
src/hardware/kpoddevice.cpp
src/hardware/halikeydevice.cpp
)
elseif(IOS)
# iOS: MIDI keyer only (CoreMIDI via RtMidi). No serial/HID V14 worker; KPOD
# stays stubbed inside kpoddevice.cpp.
list(APPEND SOURCES
src/hardware/kpoddevice.cpp
src/hardware/halikeydevice.cpp
src/hardware/halikeyworkerbase.cpp
src/hardware/halikeymidiworker.cpp
third_party/rtmidi/RtMidi.cpp
)
else()
list(APPEND SOURCES
src/hardware/kpoddevice.cpp
Expand Down Expand Up @@ -322,6 +391,7 @@ set(HEADERS
src/ui/inwindowdialog.h
src/ui/kpa1500panel.h
src/ui/kpa1500window.h
src/ui/adjustoverlay.h
src/ui/sidecontroloverlay.h
src/ui/monoverlay.h
src/ui/baloverlay.h
Expand All @@ -333,6 +403,13 @@ if(ANDROID)
src/hardware/kpoddevice.h
src/hardware/halikeydevice.h
)
elseif(IOS)
list(APPEND HEADERS
src/hardware/kpoddevice.h
src/hardware/halikeydevice.h
src/hardware/halikeyworkerbase.h
src/hardware/halikeymidiworker.h
)
else()
list(APPEND HEADERS
src/hardware/kpoddevice.h
Expand All @@ -346,6 +423,30 @@ endif()
# On Android, create Qt executable package target
if(ANDROID)
qt_add_executable(${PROJECT_NAME} MANUAL_FINALIZATION ${SOURCES} ${HEADERS})
elseif(IOS)
# Qt for iOS is static and needs qt_add_executable for plugin import.
# Build with the Xcode generator (qt-cmake defaults to it) and pick the
# SDK at build time: cmake --build . -- -sdk iphonesimulator
set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
set(MACOSX_BUNDLE_BUNDLE_VERSION ${QK4_VERSION_FULL})
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${QK4_VERSION_FULL})
set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.w9wdx.qk4phone")
# App icon: compile the iOS asset catalog (AppIcon) into the bundle.
set(QK4_IOS_ASSETS "${CMAKE_SOURCE_DIR}/resources/ios/Assets.xcassets")
set_source_files_properties(${QK4_IOS_ASSETS} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
qt_add_executable(${PROJECT_NAME} MANUAL_FINALIZATION MACOSX_BUNDLE ${SOURCES} ${HEADERS} ${QK4_IOS_ASSETS})
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/resources/Info.plist.ios.in"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.w9wdx.qk4phone"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
)
# AVAudioSession (src/ios/iosaudiosession.mm) lives in AVFoundation.
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AVFoundation" "-framework Foundation" "-framework Security" "-framework UIKit")
# Static Qt auto-imports every Multimedia plugin. QK4 only uses
# QAudioSink/QAudioSource/QMediaDevices, which live in QtMultimedia itself,
# so drop the FFmpeg plugin and its five xcframeworks.
qt_import_plugins(${PROJECT_NAME} EXCLUDE Qt6::QFFmpegMediaPlugin)
# On macOS, create an app bundle
elseif(APPLE)
set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME})
Expand Down Expand Up @@ -416,26 +517,56 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
Qt6::Multimedia
Qt6::Gui
Qt6::Svg
Qt6::SerialPort
${OPUS_LIBRARIES}
${HIDAPI_LIBRARIES}
)
if(NOT IOS AND NOT ANDROID)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::SerialPort)
endif()

if(QK4_PSK_TLS_OPENSSL)
if(NOT QK4_OPENSSL_ROOT)
message(FATAL_ERROR "QK4_PSK_TLS_OPENSSL requires QK4_OPENSSL_ROOT (prefix with include/ and lib/).")
endif()
foreach(_qk4_ssl_file "include/openssl/ssl.h" "lib/libssl.a" "lib/libcrypto.a")
if(NOT EXISTS "${QK4_OPENSSL_ROOT}/${_qk4_ssl_file}")
message(FATAL_ERROR "Missing ${QK4_OPENSSL_ROOT}/${_qk4_ssl_file}; see third_party/ios/README.md.")
endif()
endforeach()
unset(_qk4_ssl_file)
target_compile_definitions(${PROJECT_NAME} PRIVATE QK4_PSK_TLS_OPENSSL)
target_include_directories(${PROJECT_NAME} PRIVATE "${QK4_OPENSSL_ROOT}/include")
target_link_libraries(${PROJECT_NAME} PRIVATE
"${QK4_OPENSSL_ROOT}/lib/libssl.a"
"${QK4_OPENSSL_ROOT}/lib/libcrypto.a"
)
endif()

# Link GuiPrivate if found via CMake, otherwise private headers are included manually
if(QT_GUI_PRIVATE_FOUND)
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::GuiPrivate)
endif()

# macOS frameworks for KPOD hotplug detection (IOKit)
if(APPLE)
if(APPLE AND NOT IOS)
target_link_libraries(${PROJECT_NAME} PRIVATE
"-framework IOKit"
"-framework CoreFoundation"
)
endif()

# RtMidi platform-specific MIDI backend
if(APPLE)
if(ANDROID)
# No RtMidi backend on Android (HaliKey/KPOD serial+MIDI stubbed).
elseif(IOS)
# iOS uses the CoreMIDI backend (RtMidi guards the bits unavailable on iOS).
target_compile_definitions(${PROJECT_NAME} PRIVATE __MACOSX_CORE__)
target_link_libraries(${PROJECT_NAME} PRIVATE
"-framework CoreMIDI"
"-framework CoreAudio"
"-framework CoreFoundation"
)
elseif(APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE __MACOSX_CORE__)
target_link_libraries(${PROJECT_NAME} PRIVATE
"-framework CoreMIDI"
Expand All @@ -444,8 +575,6 @@ if(APPLE)
elseif(WIN32)
target_compile_definitions(${PROJECT_NAME} PRIVATE __WINDOWS_MM__)
target_link_libraries(${PROJECT_NAME} PRIVATE winmm)
elseif(ANDROID)
# No RtMidi backend on Android (HaliKey/KPOD features disabled).
else()
target_compile_definitions(${PROJECT_NAME} PRIVATE __LINUX_ALSA__)
target_link_libraries(${PROJECT_NAME} PRIVATE asound)
Expand Down Expand Up @@ -528,7 +657,7 @@ endif()
# Deployment / Bundling - Create self-contained distributable app
# =============================================================================

if(APPLE)
if(APPLE AND NOT IOS)
# Find macdeployqt
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${Qt6_DIR}/../../../bin")

Expand Down Expand Up @@ -618,11 +747,11 @@ elseif(WIN32)
)
endif()

if(ANDROID)
if(ANDROID OR IOS)
qt_finalize_executable(${PROJECT_NAME})
endif()

if(BUILD_TESTING AND NOT ANDROID)
if(BUILD_TESTING AND NOT ANDROID AND NOT IOS)
find_package(Qt6 REQUIRED COMPONENTS Test)
qt_add_executable(test_frequencyentryparser
tests/test_frequencyentryparser.cpp
Expand Down
40 changes: 40 additions & 0 deletions docs/BUILD_IOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Build QK4 Mobile for iOS

The iOS target must be configured and built on macOS with Xcode and a Qt iOS kit. The Windows and Android scripts do not produce an iOS application.

## Requirements

- Xcode with the iOS 16 or newer SDK
- Qt 6.11.1 for iOS, matching the Qt version used by the project
- CMake through Qt's `qt-cmake` wrapper
- The checked-in iOS OpenSSL and Opus headers and static libraries under `third_party/ios`

The K4 remote connection uses TLS 1.2 PSK. Qt's iOS TLS backend does not support PSK, so this target links OpenSSL and uses `PskTlsSocket` directly. QRZ API keys are stored in the iOS Keychain with the `AfterFirstUnlockThisDeviceOnly` accessibility class.

## Configure and build

From the repository root on the Mac:

```bash
/path/to/Qt/6.11.1/ios/bin/qt-cmake -S . -B build-ios -G Xcode
cmake --build build-ios --config RelWithDebInfo -- -sdk iphoneos
```

For an Intel Simulator build:

```bash
cmake --build build-ios --config RelWithDebInfo -- \
-sdk iphonesimulator -arch x86_64 CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES
```

The bundled archives contain device `arm64` and Intel Simulator `x86_64` slices. An Apple Silicon Simulator needs separately built simulator-arm64 dependencies packaged as XCFrameworks.

Set the Apple development team and signing identity in the generated Xcode project before installing on a physical device. The bundle identifier is `com.w9wdx.qk4phone`.

## Screen behavior

The radio console opens in landscape. FT8 and FT4 request portrait. SSTV and a logbook opened from the radio or SSTV allow portrait and landscape, then return to the landscape radio console when closed.

## Validation boundary

Windows tests and an Android build verify shared C++ and the Qt TLS adapter. The iOS target, Keychain, orientation requests, direct OpenSSL PSK handshake, audio routes, and physical-device UI require validation on a Mac and iPhone/iPad before release.
58 changes: 58 additions & 0 deletions resources/Info.plist.ios.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleDisplayName</key>
<string>QK4 Mobile</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>QK4 needs microphone access for transmit audio.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>QK4 needs local network access to connect to your Elecraft K4 radio.</string>
</dict>
</plist>
14 changes: 14 additions & 0 deletions resources/ios/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "icon_1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions resources/ios/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading