Skip to content

Add iOS support (iPhone and iPad: native build, phone/tablet layout, touch RC, BLE, Wi-Fi MAVLink) - #16

Draft
SebastianKumor wants to merge 22 commits into
b14ckyy:developmentfrom
SebastianKumor:ipados-port
Draft

Add iOS support (iPhone and iPad: native build, phone/tablet layout, touch RC, BLE, Wi-Fi MAVLink)#16
SebastianKumor wants to merge 22 commits into
b14ckyy:developmentfrom
SebastianKumor:ipados-port

Conversation

@SebastianKumor

@SebastianKumor SebastianKumor commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

iOS support (iPhone and iPad)

Builds on the merged macOS work (#8) to run Kite Ground Control natively on iPhone and iPad. Desktop, Windows and Linux behavior is unchanged. Everything mobile is gated behind runtime platform checks (isMobile, plus isPhone and isTablet for the two form factors) so nothing here affects the desktop builds, and phone-only layout never touches the tablet or the other way around.

Now merged up to the latest master (video and native-capture rework), builds clean for aarch64-apple-ios.

What this adds

  • Compiles the Rust core for aarch64-apple-ios so the app runs as a real iOS binary, not a webview shim.
  • App container data paths and the Local Network permission so telemetry over Wi-Fi works inside the iOS sandbox.
  • Native CoreBluetooth BLE transport, mirroring the BLE capability the desktop builds already have.
  • On screen touch RC sticks that send stick input to the flight controller as RC_CHANNELS_OVERRIDE over the active MAVLink link. This rides the same connection as telemetry, so it needs a live link to the aircraft (for example the ELRS backpack Wi-Fi), it is not a standalone transmitter.
  • Native location: the position that centers the map and places the ground-station marker now comes from CoreLocation via tauri-plugin-geolocation on mobile, so the permission prompt shows the app name and usage string rather than the WebView origin ("localhost"). Desktop keeps navigator.geolocation. The plugin is a mobile only dependency, registered under cfg(mobile). The vehicle GPS stays the fallback source, which matters on Wi-Fi only tablets with no GPS chip.
  • Minimum deployment target set to iOS 18.6, plus scripts/run-ipad.sh and scripts/build-ios.sh for the dev and release flows.
  • Safe area padding driven by env(safe-area-inset-*) so the iOS status bar and notch do not cover the toolbar and the home indicator and map controls do not cover the HUD or the Leaflet attribution.

Phone and tablet layout

The interface adapts to each form factor at runtime:

  • Phone: a collapsible toolbar that shows a connection, protocol and arming status chip when collapsed, a two row HUD with larger tiles, panels that overlay the dock so their content stays reachable, and window controls dropped where the OS owns them.
  • Tablet: the nav rail and panels track the taller mobile toolbar height so they clear it, and the small HUD and side dock tiles are enlarged to use the extra screen space while the round instruments keep their size.
  • Both: single finger map pan plus pinch handed to Leaflet, and a one time recenter on the first location fix (cancelled the moment you pan or zoom).

Tested on hardware

Ran on a physical iPad and iPhone against a live aircraft. MAVLink telemetry over an ELRS TX backpack Wi-Fi link (INAV 9, ELRS link in MAVLink mode, UDP 14550) came through with full attitude, GPS, battery and flight mode, plus live RC channels. Connection stayed stable across the session. MSP is also available on iOS over Wi-Fi (TCP/UDP) and BLE; only the USB serial transport is unavailable there, since iOS has no raw serial access.

Notes

  • The iOS Xcode project under gen/apple stays gitignored and regenerable, same as upstream Tauri mobile convention.
  • Signing uses a personal Apple development team via the APPLE_DEVELOPMENT_TEAM env var, nothing team specific is committed.
  • Still a draft since this is a large surface. Happy to split into smaller PRs if you prefer to land it in stages.

Future work

Android would slot into the same layout tiers: the isMobile, isPhone and isTablet flags and their html classes are named by form factor rather than by OS for exactly that reason. Out of scope for this PR, noting it as a follow up.

@b14ckyy

b14ckyy commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Hey man. You are really on fire now :D

But please be aware. I will not add iOS, iPad OS or Android support for the 1.0 release. So please take your time. Additionally we need to have a chat anyway since although its cool to have so many platforms supported, I personally can only build and test on Win, Linux and Android but I don't have apple devices.

So if we extend the support to Apple Devices I will rely on your help in the future for testing and development.

@SebastianKumor

Copy link
Copy Markdown
Contributor Author

@b14ckyy For me personally ipad version is also quite interesting as its super easy to take to the field and works pretty ok now with wifi and backpack. I do not have crossfire with ble to test but I could make a testflight for this so people who have other hardware can try. This is a draft and can be merged in whatever release you want ,I will be updating the branch with master every once in a while and fixing bugs. Of course I am happy to help you maintain this.

@SebastianKumor
SebastianKumor force-pushed the ipados-port branch 2 times, most recently from 813c043 to cd017c8 Compare July 9, 2026 12:43
Gate the desktop-only pieces behind cfg(target_os = "ios") so the backend
compiles for iOS while every desktop target (Windows/Linux/macOS) builds byte
-for-byte as before:

- Cargo.toml: move serialport, btleplug and starship-battery to a
  cfg(not(target_os = "ios")) dependency table (no iOS backend for any).
- transport: compile out the serial and BLE modules on iOS; keep TCP/UDP.
- connection: gate the serial/BLE commands and connect() match arms; iOS
  rejects those transports and connects over Wi-Fi (TCP/UDP MAVLink) only.
- telemetry_forward + radar: gate the serial/BLE relay sinks and the serial
  ADS-B / FormationFlight sources; hoist the shared NodePos alias into
  radar::source so it stays available on iOS.
- system_on_battery: iOS stub returns true (no starship-battery backend).
- lib.rs: gate the serial/BLE command registrations and the desktop
  window-state plugin.

Both aarch64-apple-ios and the native desktop lib build with zero warnings.
- flightlog/logging/terrain: resolve the DB, logs and terrain cache to
  Library/Application Support/kite-gc on iOS (shares the macOS branch; HOME is
  the app's sandbox container root there). Desktop paths unchanged.
- Info.ios.plist: add NSLocalNetworkUsageDescription. iOS 14+ blocks every
  local-network connection until the user grants Local Network access, and only
  prompts when this key is present - required for Wi-Fi (TCP/UDP) MAVLink.
Phase 3 of the iPad port. All gated behind isMobile (from $lib/platform), so
the Windows/Linux/macOS UI is unchanged.

- platform.ts: add isIOS / isMobile. iPadOS (and the iOS Simulator) report a
  desktop "Macintosh" user-agent, so disambiguate with maxTouchPoints; also
  exclude iPadOS from isMacOS so it no longer gets the mac traffic-light controls.
- Toolbar: hide the Serial and BLE transport options on mobile (Wi-Fi only).
- +page.svelte: default the connection to UDP MAVLink (port 14550) on mobile,
  and ignore a serial/BLE transport carried over from synced desktop settings;
  hide the Video and RC-control nav tabs on mobile.
- widgetRegistry / WidgetPanel: drop the video widget from the mobile catalog
  and skip rendering it from any stale saved layout.
- LogbookPanel: hide the blackbox import button on mobile (it runs the native
  blackbox_decode binary, which can't execute on iOS).

Verified on the iPad simulator: launches, defaults to UDP with Serial/BLE gone,
Video and RC tabs hidden. svelte-check passes with 0 errors.
Adds a Mode-2 virtual RC controller for iPad, since the mobile build has no
physical joystick. It reuses the existing RC-injection pipeline end to end:

- mobileRc store: writes the fixed AETR channel frame (CH1 roll, CH2 pitch,
  CH3 throttle, CH4 yaw, CH5 arm) into channelValues and the PX4 MANUAL_CONTROL
  setpoint into manualOutput, then engages rcEngaged so the shared rcStream pump
  handles the wire format (INAV MSP / ArduPilot override / PX4 manual), send rate
  and the 500 ms deadman. Nothing here persists to the desktop RC profiles.
- VirtualSticks component: two large touch pads as a bottom overlay. Left =
  throttle (holds) + yaw (springs to centre); right = pitch + roll (spring).
  Inert until Engage; arming is a separate explicit toggle; releasing a spring
  axis re-centres it; tearing down the panel disengages.
- +page.svelte: on mobile, offer the RC tab whenever a control-capable FC is
  connected and render VirtualSticks instead of the desktop RcControlPanel.

Desktop is untouched (all mobile-only paths behind isMobile). Verified in the
iPad simulator; live control still needs a field test. svelte-check: 0 errors.
btleplug has no iOS backend, so BLE on iOS is implemented in Rust directly
against CoreBluetooth via the objc2 bindings, kept in-crate (no Swift, no Tauri
plugin) so it survives cargo tauri ios init:

- transport/ble_ios.rs: a Rust CBCentralManager/CBPeripheral delegate feeding a
  byte ring, exposing the same surface as the desktop btleplug module
  (scan_ble_devices / run_scan_session / connect_ble / connect_ble_listen and a
  BleTransport: ByteTransport). Matches the same serial GATT profiles.
- connection.rs: BLE commands + the connect() BLE arm now route through a
  per-platform ble_backend alias (btleplug on desktop, CoreBluetooth on iOS).
  Serial stays desktop-only.
- lib.rs: register the BLE scan commands on iOS too (list_serial_ports stays
  desktop-only).
- Toolbar / +page.svelte: re-enable the BLE transport option on mobile.
- Info.ios.plist: NSBluetoothAlwaysUsageDescription for the BLE permission.

Compiles cleanly for aarch64-apple-ios and links (CoreBluetooth), desktop
unchanged (0 warnings). Runtime is not yet validated against a real BLE flight
controller - the delegate handshake needs an on-device pass.
Pin bundle.iOS.minimumSystemVersion so it survives gen/apple regen, and
add scripts/run-ipad.sh (dev live-reload on device) and scripts/build-ios.sh
(release ipa) that run the full web + Rust + Xcode flow in one command.
…ap UI

On mobile the iOS status bar overlapped the toolbar and the bottom edge crowded the
map zoom/compass buttons, the map attribution label, the HUD readouts and the RC sticks.
Tag the document with is-mobile and expose --safe-top / --safe-bottom (env safe-area
insets, viewport-fit=cover), then: grow the toolbar row and offset the top-anchored map,
video and toast layers by --safe-top; lift the map control cluster and move the Leaflet
attribution off it; and raise the HUD dock above the on-screen sticks on the rc-control tab.
The WebView Geolocation API labels its permission prompt with the page
origin, which for a Tauri app reads "localhost". Route the ground-station
position through tauri-plugin-geolocation on mobile (iOS/Android) so the OS
prompt shows the app name and the Info.plist usage string instead. Desktop
keeps navigator.geolocation. The plugin is a mobile-only dependency and is
registered under cfg(mobile); the vehicle GPS remains the fallback source.
Adapt the interface for iPhone and iPad separately. Phone: a collapsible
toolbar with a connection/protocol/arming status chip, a two-row HUD with
larger tiles, and panels that overlay the dock so their content stays
reachable. Tablet: track the taller mobile toolbar height so the nav rail
and panels clear it, and enlarge the small HUD and side-dock tiles to use
the extra space. Both: hand map touch gestures to Leaflet (single-finger
pan plus pinch) and recenter once on the first location fix.

Layout tiers are gated on OS-agnostic flags (isPhone, isTablet, isMobile,
with matching html classes) so a future Android build reuses them, and so
phone-only rules never affect the tablet and vice versa.
@SebastianKumor SebastianKumor changed the title Add iPadOS support (native iOS build, touch RC, BLE, Wi-Fi MAVLink) Add iOS support (iPhone and iPad: native build, phone/tablet layout, touch RC, BLE, Wi-Fi MAVLink) Jul 27, 2026
@b14ckyy

b14ckyy commented Jul 28, 2026

Copy link
Copy Markdown
Owner

I see you are closely tracking.
I am preparing the branching now for further development. Master will for now stay a production ready branch until 1.0 is final released (Highly appreciate if you can thoroughly test the video input and RTSP stuff on macOS. Its entirely untested after the full refactor. )

Then we can point your iPad PR to a new development branch to get it on track while I work on the android Tablet side.

@b14ckyy
b14ckyy changed the base branch from master to development July 28, 2026 19:35
@b14ckyy

b14ckyy commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Hey mate.
so I have looked closer into your PR and it looks very promising. There are a few concerns about the phone screen layout but I have an idea to solve that properly without giving you extra work.

But there are a few things I would like to discuss with your regarding available features on mobile OS devices (iOS and Andorid). Maybe we can chat somewhere 1:1 for easier communication?

A few bullet points:

  • we can keep the Flight Logbook but it should be slightly simplified. No Dataflash or INAV Blackbox import. Its already difficult to get the files on the device but the size, hundreds of MB for Ardu Dataflash, would blow up the DB so much that its not really good use on a mobile device. Telemetry logging can stay for later replay. Export as well to move recordings to a PC (in the far future I already have ideas to run a Database centralized and remote)
  • Disabling the video feature on phone makes sense but on tablet it should be doable via RTSP. Android can even do USB OTG and USB Serial. but iOS could easily do wifi streaming.
  • for Phone widgets I will work on a custom Widget dock with redesigned minimal space widgets so we can probably fit 8 of them on the space where you have 4 now. But that can be on my end after we merged your PR and I build the Android side on top of yours.

If we discuss that and you can make the necessary changes on what we agree on, I will do the full review and we can merge that as a base so I can work on the Android side. Alternatively if you can separate the PR into 2 and split the front end from the iOS specific code, we could merge the front end and we build on that while each of us sticks with the back end part for each OS.

@b14ckyy b14ckyy added this to the 1.1 milestone Jul 29, 2026
@b14ckyy b14ckyy added enhancement New feature or request platform changes to specific platforms that have to be thoroughly checked for cross compatibility gating. labels Jul 29, 2026
b14ckyy and others added 6 commits July 29, 2026 10:40
The toolbar is the title bar, and nothing in it gave way. Narrowing the window
— worst with a UAV connected, where the sensor bar adds ~200 px — pushed the
minimise/maximise/close buttons out of the frame, so the window could no longer
be closed.

The structural guarantee is that only the left and centre groups yield: the
right-hand group keeps its size, and the window controls no longer shrink on
either placement (the macOS traffic lights could be squeezed under the logo).
On top of that the bar now sheds content in a fixed order as it runs out of
room: version, then wordmark to icon, then the sensor bar, then the connection
controls onto a strip below the bar.

Fit is measured against the summed natural group widths rather than fixed
breakpoints, because the requirement swings widely with the sensor count, the
selected transport and the locale. Restoring a level uses the width that level
actually measured, so dragging across a boundary cannot flicker.

The second row floats out of flow deliberately: the app grid pins the toolbar
row to 53 px and the map, panel and video overlays offset against that same
number, so a taller header would drag four unrelated surfaces with it.

Also drops the empty sensor bar that rendered as a small bordered box while
disconnected.

Co-Authored-By: Claude Opus 4.8
macOS was missing from the platform badge and the supported-setups list, even
though the release workflow has been building a universal bundle for a while.
Alongside that:

- status badge said "public beta"; the tree is at 1.0.0-rc1
- Linux is x86-64 AND ARM64, both built by the release workflow
- Android read "on hold"; work on it has started for a post-1.0 release
- live video also covers local capture devices, not just RTSP
- the build section omitted `just build-macos`, which the justfile has
- CI runs clippy and the Rust tests too, on all three platforms
- the feature-freeze note predates the branching model: new features are welcome
  against `development` now, they just don't reach `master` before 1.0

Documentation site links verified live.

Co-Authored-By: Claude Opus 4.8
The mobile groundwork covers both — form-factor detection, safe-area handling
and a BLE transport for iOS all exist — so listing only Android understated it.

Co-Authored-By: Claude Opus 4.8
fix(toolbar): keep the window buttons reachable in a narrow window
…d state

Two defects behind issue b14ckyy#26, both specific to replaying a passively recorded
flight.

**3D track buried underground.** The replay places its track at
`startMslGps + geoid + relative altitude`, and took the anchor from the first
fix's `alt_m`. That is only a true MSL for protocols that report one. CRSF and
LTM report altitude relative to the arming point — the backend already says so
(`msl = !matches!(p, Protocol::Ltm | Protocol::Crsf)`) — so their `alt_m` starts
near zero and the whole track was placed at sea level, i.e. buried by the local
terrain elevation. The reporter's flight sits at ~550 m, which is exactly how far
under the ground it rendered. For those protocols the anchor now comes from
terrain at the start point, the same reference the live path captures at the arm
edge. A missing terrain sample falls back to the previous behaviour rather than
inventing a height.

The geoid fallback ground was reading the same raw `alt_m`, which would have
skewed the offset by the whole local elevation on exactly these flights whenever
the Copernicus lookup came back empty. It now takes the resolved anchor.

**Aircraft always disarmed in replay.** The adapter read `armingFlags` from
`state_flags`, which is INAV's stateFlags bitfield and only blackbox imports
populate it — the live recorder has no source and stores NULL, so every replayed
live flight looked disarmed regardless of protocol. Recovered from the recording
itself: the recorder opens a flight on the arm edge and finalises it on disarm,
so a sample carrying a flight mode was taken while armed.

Both work on flights already in the database — the reporter's log included. Which
protocols are relative is decided in one place (`helpers/altReference.ts`) by
matching the stored protocol label; that makes the label part of a data contract,
noted for replacement by a real column at the next schema change.

Fixes b14ckyy#26

Co-Authored-By: Claude Opus 4.8
fix(replay): anchor relative-altitude tracks to terrain, restore armed state
b14ckyy and others added 5 commits August 1, 2026 14:48
Ubuntu 26.04 starts the AppImage into an empty window and aborts at EGL display
creation. The installation page already warned about distributions being too
old; this adds the other end of the range, points at the .deb (which works on
the same system) and says plainly that we found no packaging fix — bundling less
makes it worse, bundling the graphics driver is not allowed, and a newer build
base would lock out everything below it.

Invites correction in issue b14ckyy#29 for anyone who knows the format better.

Co-Authored-By: Claude Opus 4.8
# Conflicts:
#	src/lib/components/Toolbar.svelte
SerialConnection::open() did a bare serialport open — the Rust serialport
crate leaves DTR low. USB-CDC devices gate their device→host stream on
DTR (tud_cdc_n_connected()), so against a virtual COM port (e.g. a
Kite-Link USB-OTG node) the link looked dead in exactly one direction:
host→device requests arrived, every response was dropped at the device.
INAV Configurator, Mission Planner and any terminal work because they
assert DTR on open.

The raise now lives inside open() itself, so the FC connect, passive
listen and FormationFlight paths all get it; the ADS-B source's existing
explicit call becomes a harmless no-op. Best-effort: ports without
control lines (BT-SPP) log a warning and continue.

Co-Authored-By: Claude Opus 4.8
fix(serial): raise DTR/RTS on open — USB-CDC devices gate their TX on it

Tested and fix confirmed with Kite-Link Virtual Com.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request platform changes to specific platforms that have to be thoroughly checked for cross compatibility gating.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants