Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Troubleshooting and setup
url: https://github.com/su-engineering/react-native-nearby-interaction/blob/main/docs/TROUBLESHOOTING.md
about: Check common discovery, configuration, and measurement problems.
- name: Report a security vulnerability privately
url: https://github.com/su-engineering/react-native-nearby-interaction/security/advisories/new
about: Use private reporting instead of a public issue for vulnerabilities.
27 changes: 27 additions & 0 deletions .github/ISSUE_TEMPLATE/hardware.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Hardware validation
description: Share reproducible results for an iPhone and UWB accessory combination
body:
- type: markdown
attributes:
value: "Report observed results, including failures and unavailable readings. Remove device identifiers and raw accessory configuration tokens."
- type: textarea
id: environment
attributes:
label: Hardware and software
description: Package version or commit, iPhone model, iOS, React Native/Expo, accessory model, and firmware revision (or unknown).
validations:
required: true
- type: textarea
id: setup
attributes:
label: Setup and reproduction
description: Example app or minimal reproduction, transport/profile changes, and relevant public protocol documentation.
validations:
required: true
- type: textarea
id: results
attributes:
label: Observed results
description: Report distance, angle/direction availability, Stop/Start, foreground resume, tag power-cycle, stale-reading cleanup, and any untested checks. Include approximate distances and conditions without claiming calibrated accuracy.
validations:
required: true
28 changes: 27 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ Open an issue before substantial API changes. Bug reports should include package
React Native/Expo, iOS and Xcode versions, tag model/firmware, lifecycle state and
a minimal reproduction. Remove credentials and private accessory tokens.

Use Node.js 22 for parity with CI. Native checks require macOS and Xcode; CI uses
Xcode 26.2. Start with the fast checks, then run consumer checks for integration changes.

```sh
npm ci
npm run check
```

Before changing packaging or native integration:

```sh
npm ci
npm run release:check
Expand All @@ -17,8 +27,24 @@ On macOS, also run `swift test` and compile generated consumers as CI does.

Add tests for changed behavior, update the changelog and document API changes.
Changes to NI lifecycle, BLE negotiation or framing require physical test results
with hardware/firmware versions. The first beta's physical validation is pending.
with hardware/firmware versions. See the [first hardware report](docs/validation/2026-09-21.md) for the validated
configuration and remaining coverage. Report additional hardware using the
hardware validation issue form.

Preserve explicit session ownership/stop, nullable measurements, main-queue native
isolation and bounded recovery. Keep application-specific behavior out of the
library. Contributions must be yours to submit and distributable under MIT.

## Pull requests

Keep each PR focused on one problem. Explain the observed behavior, the resulting
behavior, and the checks you ran. Link the relevant issue when there is one.
For hardware-dependent changes, distinguish measurements observed on a device
from simulator or mocked tests; record any checks you could not run.

Documentation-only changes need accurate examples and working links. Changes to
public behavior need appropriate regression coverage. Do not include signing
profiles, device identifiers, raw nearby-device scans, or private accessory data.

Use respectful, constructive discussion. For vulnerabilities, follow
[SECURITY.md](SECURITY.md) instead of filing a public bug.
110 changes: 89 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
# React Native Nearby Interaction

iPhone → UWB accessory ranging through Apple's Nearby Interaction framework.
This standalone MVP has a configurable Truesense T-TAG BLE profile, an external
transport API for other accessories, a typed event API, and a React hook.
**Measure the distance from an iPhone to a UWB accessory in React Native and Expo.**

Current release: **0.1.0**, published under the `latest` npm tag. Initial physical distance ranging, stop/restart, foreground resume and
tag reconnection passed on an iPhone 16 Pro with a Truesense T-TAG. See
[validation](docs/VALIDATION.md) for the evidence and remaining acceptance tests.
[![CI](https://github.com/su-engineering/react-native-nearby-interaction/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/su-engineering/react-native-nearby-interaction/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@su-engineering/react-native-nearby-interaction)](https://www.npmjs.com/package/@su-engineering/react-native-nearby-interaction)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Apple's Nearby Interaction framework provides the measurements. This library
handles the native session, Truesense T-TAG Bluetooth negotiation, and recovery,
and exposes a typed event API and a React hook.

- **T-TAG support:** configurable BLE services, characteristics, and command bytes.
- **React state:** distance, session state, accessory details, and errors through one hook.
- **Session recovery:** bounded retries, foreground resume, and stale-reading cleanup.
- **Other accessories:** bring your own transport for an Apple-compatible NI protocol.
- **Native integration:** React Native New Architecture and an Expo config plugin.

Current release: **[0.1.0](https://github.com/su-engineering/react-native-nearby-interaction/releases/tag/v0.1.0)**.
Distance ranging, stop/restart, foreground resume, and tag reconnection passed on
an iPhone 16 Pro with a Truesense T-TAG. Direction and angle were unavailable in
those sessions. See the [hardware report](docs/validation/2026-09-21.md) for the
exact test scope; other accessories remain unverified.

[Quick start](#installation) · [API reference](docs/API.md) ·
[Troubleshooting](docs/TROUBLESHOOTING.md) · [Validation](docs/VALIDATION.md) ·
[Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)

## Scope

| Capability | MVP |
| Capability | Support in 0.1.0 |
| --- | --- |
| iOS UWB accessory sessions | Implemented |
| Truesense demo firmware handshake | Device-tested with a T-TAG; firmware revision not recorded |
Expand All @@ -20,8 +38,8 @@ tag reconnection passed on an iPhone 16 Pro with a Truesense T-TAG. See
| Bounded retries, deadlines, write acknowledgements, stale reading cleanup | Implemented |
| React Native New Architecture | TurboModule + generated event bindings |
| Expo | Info.plist config plugin; native development build required |
| Android, iPhone-to-iPhone, multiple simultaneous tags | Outside this MVP |
| Background ranging | Outside this MVP; disconnects and resumes in foreground |
| Android, iPhone-to-iPhone, multiple simultaneous tags | Outside this release |
| Background ranging | Outside this release; disconnects and resumes in foreground |

Compatibility target: React Native 0.83.x (baseline 0.83.1), React 19.2.x,
Expo SDK 55 (baseline 55.0.0), iOS 15.1+, Xcode 26.2+ for Expo builds. Other versions are not claimed by this
Expand All @@ -36,15 +54,16 @@ Install from npm:
npm install @su-engineering/react-native-nearby-interaction
```

Before release, install the tarball produced by `npm run pack:check`, or clone
the repository and install locally:
For a bare React Native app, install the iOS pods after adding the package:

```sh
npm install /absolute/path/to/react-native-nearby-interaction
cd ios
bundle exec pod install
```

You need a UWB-capable iPhone and an Apple-compatible UWB accessory for physical
ranging. A simulator can validate UI and unsupported-device handling only.

Add nonempty `NSNearbyInteractionUsageDescription` and
`NSBluetoothAlwaysUsageDescription` strings to your app's Info.plist. The library
checks their presence before starting a built-in BLE session.
Expand All @@ -68,22 +87,54 @@ not add background modes. Expo Go cannot load this native module.
## Basic usage

```tsx
import {Text} from 'react-native';
import {useEffect, useState} from 'react';
import {Button, Text, View} from 'react-native';
import {nearbyInteraction, useNearbyInteraction} from '@su-engineering/react-native-nearby-interaction';

function Distance() {
// This screen owns the shared session. Mount only one session owner at a time.
export function DistanceScreen() {
const {state, measurement, error} = useNearbyInteraction();
return <Text>{error?.message ?? `${state}: ${measurement?.distance ?? '—'} m`}</Text>;
}
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState<string | null>(null);

useEffect(() => () => {
void nearbyInteraction.stop().catch(console.error);
}, []);

async function run(action: 'start' | 'stop') {
setBusy(true);
setMessage(null);
try {
if (action === 'start') {
const capabilities = await nearbyInteraction.getCapabilities();
if (!capabilities.supported) {
setMessage('This device does not support accessory UWB ranging.');
return;
}
await nearbyInteraction.start();
} else {
await nearbyInteraction.stop();
}
} catch (cause) {
setMessage(cause instanceof Error ? cause.message : String(cause));
} finally {
setBusy(false);
}
}

const capabilities = await nearbyInteraction.getCapabilities();
if (capabilities.supported) {
await nearbyInteraction.start({namePrefix: 'T-TAG', maxRetries: 3});
return (
<View>
<Text>{state}</Text>
<Text>{measurement?.distance == null ? '—' : `${measurement.distance.toFixed(2)} m`}</Text>
<Text>{message ?? error?.message}</Text>
<Button title="Start" disabled={busy || state !== 'idle'} onPress={() => void run('start')} />
<Button title="Stop / reset" disabled={busy} onPress={() => void run('stop')} />
</View>
);
}
await nearbyInteraction.stop();
```

Omit the name filter if your firmware advertises another name. By default, the
The example uses the default T-TAG profile without a name filter. By default, the
first matching peripheral is selected. Use `deviceId` (CoreBluetooth UUID) to
target a particular unit. The default scan timeout is 15 seconds.

Expand Down Expand Up @@ -210,3 +261,20 @@ and [source lineage](docs/SOURCE.md). This package is MIT licensed; the example
retains the official template's MIT license in `example/TEMPLATE-LICENSE`.
See [contributing](CONTRIBUTING.md), [security](SECURITY.md) and the
[release guide](docs/RELEASING.md).

## Help and contributions

Use the [bug report form](https://github.com/su-engineering/react-native-nearby-interaction/issues/new?template=bug.yml)
for reproducible problems and the [hardware validation form](https://github.com/su-engineering/react-native-nearby-interaction/issues/new?template=hardware.yml)
to share results from another device or firmware. Include versions, session states,
and a minimal reproduction; remove private identifiers and accessory configuration tokens.
Read the [troubleshooting guide](docs/TROUBLESHOOTING.md) before collecting logs.

Small fixes, regression tests, documentation improvements, and reproducible hardware
reports are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md). Report security
issues through the private channel in [SECURITY.md](SECURITY.md).

## License

[MIT](LICENSE), copyright SU Engineering. The example also retains the official
React Native template's [MIT license](example/TEMPLATE-LICENSE).
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Security

Only the latest published beta receives fixes during the pre-1.0 period.
Only the latest published release receives fixes during the pre-1.0 period.

Use GitHub's private **Report a vulnerability** feature when available. Otherwise,
open an issue requesting a private reporting channel without exploit details.
[Report a vulnerability privately](https://github.com/su-engineering/react-native-nearby-interaction/security/advisories/new)
through GitHub Security Advisories. Do not include exploit details in public issues.
Include version, affected path, impact and reproduction, with real credentials
and private accessory tokens removed.

Expand Down
84 changes: 84 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# API reference

Import the shared `nearbyInteraction` client, `useNearbyInteraction` hook,
`NearbyInteractionError`, and `TTAG_PROFILE` from
`@su-engineering/react-native-nearby-interaction`. Public interfaces are exported
as TypeScript types; see [types.ts](../src/types.ts) for their full definitions.

## Session ownership

One client manages one accessory session. Subscribe before calling `start()`.
Mounting or unmounting the hook does not start or stop the session; the owning
screen or service must call `stop()` when finished. Stop before replacing an
active or failed run. See the [complete screen example](../README.md#basic-usage).

| Method | Result and behavior |
| --- | --- |
| `getCapabilities()` | `Promise<Capabilities>` with `supported`, `preciseDistance`, `direction`, and `platform`. Capability does not guarantee that every measurement contains direction. |
| `start(options?)` | `Promise<void>`; accepts a run. Observe state and measurement events for ranging progress. |
| `stop()` | `Promise<void>`; tears down the native session and built-in BLE transport, clears the snapshot, and returns to `idle`. External transport cleanup remains the caller's responsibility. |
| `configureAccessory(base64, bluetoothPeerId?)` | Supplies Apple accessory configuration during an external session. The optional peer ID has [specific pairing requirements](../README.md#other-accessory-protocols). |
| `sendData(base64)` | Sends application data through the built-in BLE transport; resolves after write acknowledgements. Stop cancels pending writes. |
| `on(event, listener)` | Returns a subscription with `remove()`. Listener payloads are typed by event. |
| `getSnapshot()` | Returns the current read-only snapshot. |
| `subscribe(listener)` | Observes snapshot changes; returns an unsubscribe function. React applications normally use the hook. |

Methods that accept options or encoded data can throw during validation; wrap
the call itself in `try`/`catch`, including when using `await`. Native/session
failures can also reject a returned promise or arrive through the `error` event.

## Start options

| Option | Default | Meaning |
| --- | --- | --- |
| `transport` | `'ttag'` | Built-in T-TAG BLE transport or `'external'`. |
| `deviceId` | Unset | Target CoreBluetooth peripheral UUID, not a MAC address. |
| `namePrefix` | Unset | Restrict discovery by advertised name prefix. Leave unset if the firmware name is unknown. |
| `profile` | `TTAG_PROFILE` | BLE UUIDs, command bytes, and optional chunking. See [profile configuration](../README.md#configure-the-accessory-profile). |
| `maxRetries` | `3` | Additional attempts per run; integer `0–10`. |
| `timeoutMs` | `15000` | Scan, connection, and handshake deadline; integer `1000–120000`. |
| `updateIntervalMs` | `100` | Native measurement delivery throttle; integer `0–5000`. |
| `staleAfterMs` | `2000` | Clear the snapshot reading after this many milliseconds without an update; integer `100–30000`, greater than `updateIntervalMs`. |

Base64 arguments must be nonempty canonical base64 and decode to at most 64 KiB.
BLE payload limits can be smaller; chunking requires explicit firmware support.

## Hook and measurements

`useNearbyInteraction()` returns `{state, accessory, measurement, error}`.
Accessory, measurement, and error are nullable. A measurement contains:

| Field | Unit / shape |
| --- | --- |
| `distance` | Metres, or `null`. |
| `horizontalAngle` | Radians, or `null`. |
| `direction` | Apple's direction vector `{x, y, z}`, or `null`. |
| `timestamp` | Unix timestamp in milliseconds. |

Use a placeholder for unavailable data; do not convert `null` to zero. Distance,
angle, and direction can be unavailable independently. Direction and angle were
not available during the initial T-TAG hardware validation.

The snapshot clears measurements on stop, disconnection, non-ranging states,
and stale timeout. Applications maintaining their own event history must apply
their own display and retention policy.

## Events

| Event | Payload |
| --- | --- |
| `state` | `{state}`: `idle`, `waitingForBluetooth`, `scanning`, `connecting`, `configuring`, `ranging`, `suspended`, `reconnecting`, or `failed`. |
| `connected` | `{id, name}`. |
| `disconnected` | `{reason}`. |
| `measurement` | The measurement described above. |
| `configuration` | `{data}`: base64 Apple shareable configuration to deliver using an external transport. |
| `data` | `{data, source}`: opaque base64 data; source is `'nus'` or `'configuration'`. |
| `error` | `{code, message, recoverable}`. |

Remove event subscriptions when their owner is disposed. External transports
must renegotiate when the session returns to `configuring`, including after
retry or foreground resume. A recoverable error can be followed by recovery;
`failed` requires correcting the cause, stopping, and starting again.

See [troubleshooting](TROUBLESHOOTING.md) for common failure stages and
[architecture](ARCHITECTURE.md) for the native/JavaScript boundary.
Loading
Loading