% ble-apple-ecosystem.tex — iBeacon (Core Location), AccessorySetupKit, UWB /
% Nearby Interaction, the Find My network, MFi / ExternalAccessory, Multipeer / Wi-Fi
% Aware, HomeKit / Matter — which framework, what each needs.
% Source: docs/memos/ble-apple-ecosystem.md (checked against
% docs/school/notes/knowledge-gaps-2026-09-23.md: in the background ALL service UUIDs
% move to the overflow area). Not used from the source: Q8's "NI falls back to BLE-only
% distance on non-UWB peers" (NI needs UWB on both sides — capability check instead).
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/ble/ble-apple-ecosystem.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=ble kind=api level=senior platform=apple new=no round=missing-2026-09-25 topic=platform-apis
% @tags: ibeacon, clbeaconregion, core-location, region-monitoring, accessorysetupkit, nearby-interaction, uwb, nisession, find-my, mfi, externalaccessory, multipeerconnectivity
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{SwiftSheet}{
  morekeywords={protocol,class,final,struct,enum,func,var,let,weak,init,
    if,else,return,guard,self,nil,try,await,async,throws,private,some,static,
    true,false,in,AnyObject,Void,String,Bool},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/}, morestring=[b]"}

\tikzset{
  by/.style={draw=white, minimum height=5mm, inner sep=0pt, font=\ttfamily\tiny, anchor=west},
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=5mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
}
% byte segment: \bseg{x}{width-cm}{fill}{label}
\newcommand\bseg[4]{\node[by, minimum width=#2cm, fill=#3] at (#1,2.75) {#4};}

\begin{document}

\sheettitle{BLE in the Apple ecosystem — iBeacon, ASK, UWB, Find My}{ble · memo}

\oneliner{Apple's proximity features are \textbf{different frameworks on the same
radio}: iBeacon is a BLE advert read by \textbf{Core Location} (not CoreBluetooth);
\textbf{AccessorySetupKit} (iOS 18) pairs one accessory without the broad Bluetooth
permission; \textbf{Nearby Interaction} measures distance + direction over \textbf{UWB}
after BLE hands over tokens; \textbf{Find My} is a crowd relay of encrypted locations
keyed by rotating BLE keys. Plain BLE GATT needs \textbf{no MFi}.}

\vspace{3pt}
\noindent\begin{tikzpicture}[sheet]
  % ── iBeacon advert, byte by byte ──
  \node[font=\bfseries\small, anchor=west] at (-0.1,3.35) {an iBeacon advert — 30 of the 31 bytes};
  \bseg{0.0}{1.2}{black!10}{02 01 06}
  \bseg{1.2}{0.45}{sheetBrown!25}{1A}
  \bseg{1.65}{0.45}{sheetBrown!25}{FF}
  \bseg{2.1}{0.9}{sheetBlue!25}{4C 00}
  \bseg{3.0}{0.45}{sheetBlue!25}{02}
  \bseg{3.45}{0.45}{sheetBlue!25}{15}
  \bseg{3.9}{6.4}{sheetGreen!25}{proximity UUID — 16 B}
  \bseg{10.3}{1.6}{sheetOrange!30}{major 2 B}
  \bseg{11.9}{1.6}{sheetOrange!20}{minor 2 B}
  \bseg{13.5}{1.4}{sheetRed!25}{TX 1 B}
  \node[lbl] at (0.6,2.25) {flags AD};
  \node[lbl] at (1.425,2.25) {len 26};
  \node[lbl] at (1.875,2.25) {mfr};
  \node[lbl] at (2.55,2.25) {Apple\\(LE)};
  \node[lbl] at (3.225,2.25) {type\\beacon};
  \node[lbl] at (3.675,2.25) {len\\21};
  \node[lbl] at (7.1,2.25) {company / app — \emph{what} to look for};
  \node[lbl] at (11.1,2.25) {building / store};
  \node[lbl] at (12.7,2.25) {spot / shelf};
  \node[lbl] at (14.2,2.25) {RSSI at 1 m\\(signed)};
  \node[lbl, anchor=west, align=left] at (15.0,2.75) {no room\\for a name};
  \draw[sheetGrey!40] (-0.1,1.85) -- (16.5,1.85);
  % ── Nearby Interaction ──
  \node[font=\bfseries\small, anchor=west] at (-0.1,1.6) {Nearby Interaction: BLE bootstraps, UWB measures};
  \node[sb, minimum width=16mm] (a) at (0.9,0.55) {iPhone\\(U1/U2)};
  \node[sb, minimum width=16mm] (b) at (6.6,0.55) {peer / UWB\\accessory};
  \draw[->, thick, dashed, sheetBlue] ([yshift=5pt]a.east) -- node[lbl, above=1pt, text=sheetBlue]{\textbf{1} tokens over \emph{your} channel (BLE, network)} ([yshift=5pt]b.west);
  \draw[<->, very thick, sheetOrange] ([yshift=-5pt]a.east) -- node[lbl, below=1pt, text=sheetOrange]{\textbf{2} \texttt{NISession.run(config)}: UWB} ([yshift=-5pt]b.west);
  \node[lbl, anchor=west, align=left] at (-0.1,-0.35) {\textbf{3} \texttt{NINearbyObject}: \texttt{distance} (m) + \texttt{direction} (unit vector, may be nil)};
  \draw[sheetGrey!40] (8.0,1.75) -- (8.0,-0.55);
  % ── Find My ──
  \node[font=\bfseries\small, anchor=west] at (8.1,1.6) {Find My: offline finding, end-to-end encrypted};
  \node[sb, minimum width=12mm] (tag) at (8.8,0.55) {AirTag /\\lost item};
  \node[sb, minimum width=14mm] (pass) at (11.2,0.55) {passing\\Apple device};
  \node[sb, minimum width=11mm, draw=sheetGrey, fill=black!4] (srv) at (13.45,0.55) {Apple\\servers};
  \node[sb, minimum width=11mm, draw=sheetGreen, fill=sheetGreen!10] (own) at (15.65,0.55) {owner\\(private key)};
  \draw[->, thick, sheetBlue] (tag) -- node[lbl, above]{rotating} node[lbl, below]{public key} (pass);
  \draw[->, thick] (pass) -- node[lbl, above]{location} node[lbl, below]{encrypted} (srv);
  \draw[->, thick, sheetGreen] (srv) -- node[lbl, above]{fetch +} node[lbl, below]{decrypt} (own);
  \node[lbl, anchor=west, align=left] at (8.1,-0.35) {Apple and the finder cannot read it; rotation stops anyone linking adverts to one tag};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}
  \item \textbf{iBeacon = Core Location.} \texttt{CLBeaconRegion} +
        \texttt{CLBeaconIdentityConstraint(uuid:major:minor:)} (iOS 13; iOS 17:
        \texttt{CLMonitor}). \textbf{Monitoring} = enter/exit, OS-level, relaunches a
        terminated app (needs \textbf{Always}); max \textbf{20} regions per app, geofences
        included. \textbf{Ranging} = \texttt{startRangingBeacons(satisfying:)}, foreground:
        \texttt{proximity} \texttt{.immediate}/\texttt{.near}/\texttt{.far} + \texttt{accuracy}
        (m, from RSSI). CoreBluetooth hides iBeacon adverts. \textbf{Sending} one:
        \texttt{peripheralData(withMeasuredPower:)} $\to$ \texttt{CBPeripheralManager},
        foreground only.
  \item \textbf{AccessorySetupKit} (iOS 18) — \texttt{ASAccessorySession}
        \texttt{.showPicker(for:)} with \texttt{ASPickerDisplayItem} (name, image,
        \texttt{ASDiscoveryDescriptor}: service UUID / company id / name). The
        \textbf{system} scans and shows the sheet; the app gets an \texttt{ASAccessory}
        (\texttt{bluetoothIdentifier}) and uses CoreBluetooth for \emph{that} accessory
        only — no global Bluetooth prompt. Info.plist: \texttt{NSAccessorySetupKitSupports}
        + descriptors.
  \item \textbf{Nearby Interaction} (iOS 14) — U1 (iPhone 11--14) / U2 (iPhone 15+),
        AirTag, some Watches; \textbf{both} sides need UWB. Tokens are
        \texttt{NSSecureCoding}: archive, send, \texttt{NINearbyPeerConfiguration(peerToken:)}.
        Third-party UWB chips: \texttt{NINearbyAccessoryConfiguration} (iOS 15). Check
        \texttt{NISession.deviceCapabilities} (iOS 16) — direction may be missing.
        Foreground-oriented; a paused session needs fresh tokens.
  \item \textbf{Find My network} — third-party trackers join through the \textbf{MFi}
        Find My network accessory program (Apple's spec). No public app API to read
        other people's reports.
  \item \textbf{MFi} — only for \texttt{ExternalAccessory} (iAP2 over Classic BT /
        Lightning / USB, \texttt{UISupportedExternalAccessoryProtocols}), Find My and
        similar programs. A BLE GATT peripheral (our ESP32s) needs \textbf{none}.
  \item \textbf{Beyond BLE} — MultipeerConnectivity (\texttt{MCSession}: Wi-Fi,
        peer-to-peer Wi-Fi, Bluetooth; local-network usage string +
        \texttt{NSBonjourServices}); Wi-Fi Aware (iOS 26). HomeKit: entitlement + usage
        string; Matter (iOS 16.1+): \texttt{MatterSupport}, CSA-certified devices.
\end{itemize}

\section{Remember}
\textbf{Beacon = Location, Accessory = ASK then CB, Precise = UWB after BLE, Lost = Find
My relay, MFi = Classic/Find My only.}

\columnbreak

\section{Which framework, what it needs}
{\footnotesize
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{19mm}>{\raggedright\arraybackslash}p{19mm}>{\raggedright\arraybackslash}p{36mm}@{}}
\toprule
\textbf{Job} & \textbf{Framework} & \textbf{Needs} \\
\midrule
GATT to a peripheral & CoreBluetooth & Bluetooth usage string; background modes \\
detect iBeacon & Core Location & location auth (Always to relaunch); no BT prompt \\
be an iBeacon & CL payload + CB & BT permission, app in foreground \\
onboard 1 accessory & AccessorySetupKit & iOS 18, Info.plist descriptors; then CB \\
distance + direction & NearbyInteraction & UWB both sides, NI usage string, a token channel \\
lost-item relay & Find My network & MFi program; hardware \\
classic / Lightning & ExternalAccessory & MFi + protocol strings \\
\bottomrule
\end{tabular}\par}

\section{Example}
\begin{lstlisting}[language=SwiftSheet]
let c = CLBeaconIdentityConstraint(uuid: hubUUID, major: 1)
let region = CLBeaconRegion(beaconIdentityConstraint: c,
                            identifier: "hub")
manager.requestAlwaysAuthorization()  // relaunch on entry
manager.startMonitoring(for: region)  // background, 20 max
func locationManager(_ m: CLLocationManager,
                     didEnterRegion r: CLRegion) {
  m.startRangingBeacons(satisfying: c) } // detail, fg
// UWB: my token out, the peer's token in, over GATT
let blob = try NSKeyedArchiver.archivedData(
  withRootObject: ni.discoveryToken!, requiringSecureCoding: true)
ni.run(NINearbyPeerConfiguration(peerToken: peerToken))
\end{lstlisting}

\section{Interview traps}
\begin{itemize}
  \trap{Ranging iBeacons with \texttt{CBCentralManager} — Core Location's job; CB hides them.}
  \trap{``iBeacon gives me distance'' — RSSI buckets, metres of error. Precision = UWB.}
  \trap{Ranging in the background — monitoring only; ranging is foreground.}
  \trap{``ASK replaces CoreBluetooth'' — it replaces \emph{discovery + permission};
        GATT is still CoreBluetooth.}
  \trap{UWB without a side channel — it has no discovery; tokens travel over BLE/network.}
  \trap{``MFi for my BLE sensor'' — no. And an iOS advert in the background drops the
        name and moves \textbf{all} service UUIDs to the overflow area.}
\end{itemize}

\section{Likely questions}
\begin{enumerate}
  \item Wake an app near a hub? — beacon region monitoring (Always).
  \item Onboard without the BT prompt? — AccessorySetupKit.
  \item Find My privacy? — rotating keys; encrypted to the owner.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} BLE advertising \& scanning
(31 bytes) · CoreBluetooth central/peripheral · CoreBluetooth background · BLE security
\& privacy (RPA) · deep links (App Clips)}

\end{document}
