feat(companion): implement ble
@@ -1,22 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
|
||||
import { Container } from "@/components/Container";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
export default function BleScreen() {
|
||||
return (
|
||||
<View className={styles.container}>
|
||||
<Container>
|
||||
<Text variant="h3">BLE</Text>
|
||||
<Text className={styles.subtitle}>
|
||||
Port of SwiftUI BleStatusView will live here.
|
||||
</Text>
|
||||
</Container>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: "flex flex-1 bg-white",
|
||||
subtitle: "mt-2 text-sm text-muted-foreground",
|
||||
};
|
||||
24
aris/apps/companion/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
# expo router
|
||||
expo-env.d.ts
|
||||
|
||||
|
||||
|
||||
ios
|
||||
android
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
@@ -31,7 +31,11 @@
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "sh.nym.aris"
|
||||
"bundleIdentifier": "sh.nym.aris",
|
||||
"backgroundModes": ["bluetooth-peripheral"],
|
||||
"infoPlist": {
|
||||
"NSBluetoothAlwaysUsageDescription": "Allow Bluetooth to connect to Iris Glass."
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"package": "sh.nym.aris",
|
||||
255
aris/apps/companion/app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
|
||||
import { Container } from "@/components/Container";
|
||||
import { SafeAreaScrollView } from "@/components/safe-area-scroll-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { Ble } from "@aris/ble";
|
||||
import { useBleStore } from "@/store/ble";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
const formatTime = (timestamp: number | null) => {
|
||||
if (!timestamp) {
|
||||
return "Never";
|
||||
}
|
||||
return new Date(timestamp).toLocaleTimeString();
|
||||
};
|
||||
|
||||
export default function BleScreen() {
|
||||
const initialize = useBleStore((state) => state.initialize);
|
||||
const isSupported = useBleStore((state) => state.isSupported);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<View className="flex flex-1 bg-white">
|
||||
<Container className="px-8">
|
||||
<View className="gap-1">
|
||||
<Text variant="h3">BLE</Text>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
Bluetooth peripheral status and controls.
|
||||
</Text>
|
||||
</View>
|
||||
<View className="mt-4 rounded-xl border border-muted bg-muted/20 px-4 py-3">
|
||||
<Text className="text-sm font-medium">BLE Unavailable</Text>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
This page is currently supported on iOS only.
|
||||
</Text>
|
||||
</View>
|
||||
</Container>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex flex-1 bg-white">
|
||||
<View>
|
||||
<SafeAreaScrollView>
|
||||
<View className="gap-4" style={{ paddingBottom: insets.bottom + 24 }}>
|
||||
<View className="gap-1 px-8">
|
||||
<Text variant="h3">BLE</Text>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
Bluetooth peripheral status and controls.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full h-px bg-muted" />
|
||||
<ControlPanel />
|
||||
|
||||
<View className="w-full h-px bg-muted" />
|
||||
<BleStatusSection />
|
||||
|
||||
<View className="w-full h-px bg-muted" />
|
||||
<TelemetrySection />
|
||||
|
||||
<View className="w-full h-px bg-muted" />
|
||||
<UuidsSection />
|
||||
</View>
|
||||
</SafeAreaScrollView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ValueRow({ label, value }: { label: ReactNode; value: ReactNode }) {
|
||||
return (
|
||||
<View className="flex flex-row items-center justify-between">
|
||||
{typeof label === "string" ? (
|
||||
<Text className="text-sm text-muted-foreground">{label}</Text>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
{typeof value === "string" || typeof value === "number" ? (
|
||||
<Text className="text-sm">{value}</Text>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function BleStatusSection() {
|
||||
const bluetoothState = useBleStore((state) => state.bluetoothState);
|
||||
const isSubscribed = useBleStore((state) => state.isSubscribed);
|
||||
const subscribedCount = useBleStore((state) => state.subscribedCount);
|
||||
const connectionLabel = isSubscribed ? "Connected" : "Not Connected";
|
||||
|
||||
return (
|
||||
<View className="px-8">
|
||||
<Text variant="h4" className="mb-2 font-medium">
|
||||
Status
|
||||
</Text>
|
||||
<View className="gap-1">
|
||||
<ValueRow label="State" value={connectionLabel} />
|
||||
<ValueRow label="Bluetooth" value={bluetoothState} />
|
||||
<ValueRow label="Subscribers" value={subscribedCount} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ControlPanel() {
|
||||
const advertisingEnabled = useBleStore((state) => state.advertisingEnabled);
|
||||
const setAdvertisingEnabled = useBleStore(
|
||||
(state) => state.setAdvertisingEnabled,
|
||||
);
|
||||
const wifiRequested = useBleStore((state) => state.wifiRequested);
|
||||
const setWifiRequested = useBleStore((state) => state.setWifiRequested);
|
||||
const sendFixtureFeedNow = useBleStore((state) => state.sendFixtureFeedNow);
|
||||
const handleCopyUUIDs = async () => {
|
||||
const uuids = Ble.getUuids();
|
||||
const text = [
|
||||
`SERVICE_UUID=${uuids.serviceUUID}`,
|
||||
`FEED_TX_UUID=${uuids.feedTxUUID}`,
|
||||
`CONTROL_RX_UUID=${uuids.controlRxUUID}`,
|
||||
`WIFI_REQUEST_TX_UUID=${uuids.wifiRequestTxUUID}`,
|
||||
].join("\n");
|
||||
await Clipboard.setStringAsync(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="px-8">
|
||||
<Text variant="h4" className="mb-3 font-medium">
|
||||
Control Panel
|
||||
</Text>
|
||||
|
||||
<View className="flex-row gap-3 w-full mb-3">
|
||||
<ControlTile
|
||||
icon={<Ionicons name="radio-outline" size={20} />}
|
||||
label="Advertising"
|
||||
checked={advertisingEnabled}
|
||||
onCheckChange={setAdvertisingEnabled}
|
||||
/>
|
||||
<ControlTile
|
||||
icon={<Ionicons name="wifi-outline" size={20} />}
|
||||
label="WiFi"
|
||||
checked={wifiRequested}
|
||||
onCheckChange={setWifiRequested}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="gap-2">
|
||||
<Button onPress={sendFixtureFeedNow} variant="outline">
|
||||
<Text>Send Fixture Feed Now</Text>
|
||||
</Button>
|
||||
<Button onPress={handleCopyUUIDs} variant="outline">
|
||||
<Text>Copy UUIDs</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ControlTile({
|
||||
icon,
|
||||
label,
|
||||
checked,
|
||||
onCheckChange,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: ReactNode;
|
||||
checked: boolean;
|
||||
onCheckChange: (checked: boolean) => void;
|
||||
}) {
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
return (
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
setIsPressed(true);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
setIsPressed(false);
|
||||
}}
|
||||
onPress={() => {
|
||||
onCheckChange(!checked);
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 items-start justify-center rounded-md px-3 py-2 gap-1",
|
||||
checked ? "border-2 border-primary" : "border border-border",
|
||||
{ "bg-accent": isPressed },
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<Text className={cn({ "font-bold": checked })}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function TelemetrySection() {
|
||||
const lastMsgIdSent = useBleStore((state) => state.lastMsgIdSent);
|
||||
const lastPingAt = useBleStore((state) => state.lastPingAt);
|
||||
const lastDataAt = useBleStore((state) => state.lastDataAt);
|
||||
const lastNotifyAt = useBleStore((state) => state.lastNotifyAt);
|
||||
const notifyQueueDepth = useBleStore((state) => state.notifyQueueDepth);
|
||||
const droppedNotifyPackets = useBleStore(
|
||||
(state) => state.droppedNotifyPackets,
|
||||
);
|
||||
const lastCommand = useBleStore((state) => state.lastCommand);
|
||||
|
||||
return (
|
||||
<View className="px-8">
|
||||
<Text variant="h4" className="mb-2 font-medium">
|
||||
Telemetry
|
||||
</Text>
|
||||
<View className="gap-1">
|
||||
<ValueRow label="Last msgId" value={lastMsgIdSent} />
|
||||
<ValueRow label="Last ping" value={formatTime(lastPingAt)} />
|
||||
<ValueRow label="Last data" value={formatTime(lastDataAt)} />
|
||||
<ValueRow label="Notify queue" value={notifyQueueDepth} />
|
||||
{droppedNotifyPackets > 0 ? (
|
||||
<ValueRow
|
||||
label="Dropped notify packets"
|
||||
value={droppedNotifyPackets}
|
||||
/>
|
||||
) : null}
|
||||
<ValueRow label="Last notify" value={formatTime(lastNotifyAt)} />
|
||||
{lastCommand ? (
|
||||
<ValueRow label="Last control" value={lastCommand} />
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function UuidsSection() {
|
||||
const uuids = Ble.getUuids();
|
||||
|
||||
return (
|
||||
<View className="px-8">
|
||||
<Text variant="h4" className="mb-2 font-medium">
|
||||
UUIDs
|
||||
</Text>
|
||||
<Text selectable className="text-xs text-muted-foreground">
|
||||
{`SERVICE_UUID=${uuids.serviceUUID}\nFEED_TX_UUID=${uuids.feedTxUUID}\nCONTROL_RX_UUID=${uuids.controlRxUUID}\nWIFI_REQUEST_TX_UUID=${uuids.wifiRequestTxUUID}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
2062
aris/apps/companion/bun.lock
Normal file
16
aris/apps/companion/components/Container.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export const Container = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<SafeAreaView className={cn("flex flex-1", className)}>
|
||||
{children}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
19
aris/apps/companion/components/safe-area-scroll-view.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ScrollView } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
export function SafeAreaScrollView({ children }: React.PropsWithChildren) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
36
aris/apps/companion/components/ui/switch.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as SwitchPrimitives from '@rn-primitives/switch';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: SwitchPrimitives.RootProps & React.RefAttributes<SwitchPrimitives.RootRef>) {
|
||||
return (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
'flex h-[1.15rem] w-8 shrink-0 flex-row items-center rounded-full border border-transparent shadow-sm shadow-black/5',
|
||||
Platform.select({
|
||||
web: 'focus-visible:border-ring focus-visible:ring-ring/50 peer inline-flex outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed',
|
||||
}),
|
||||
props.checked ? 'bg-primary' : 'bg-input dark:bg-input/80',
|
||||
props.disabled && 'opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
'bg-background size-4 rounded-full transition-transform',
|
||||
Platform.select({
|
||||
web: 'pointer-events-none block ring-0',
|
||||
}),
|
||||
props.checked
|
||||
? 'dark:bg-primary-foreground translate-x-3.5'
|
||||
: 'dark:bg-foreground translate-x-0'
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch };
|
||||
51
aris/apps/companion/lib/ble/fixtures.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export const fullFeedFixture = {
|
||||
schema: 1,
|
||||
generated_at: 1767716400,
|
||||
feed: [
|
||||
{
|
||||
id: "demo:welcome",
|
||||
type: "INFO",
|
||||
title: "Glass Now online",
|
||||
subtitle: "Connected to iPhone",
|
||||
priority: 0.8,
|
||||
ttl_sec: 86400,
|
||||
bucket: "RIGHT_NOW",
|
||||
actions: ["DISMISS"],
|
||||
},
|
||||
{
|
||||
id: "cal:demo:1767717000",
|
||||
type: "CALENDAR_EVENT",
|
||||
title: "Team Sync",
|
||||
subtitle: "",
|
||||
priority: 0.7,
|
||||
ttl_sec: 5400,
|
||||
starts_at: 1767717000,
|
||||
bucket: "FYI",
|
||||
actions: ["DISMISS"],
|
||||
},
|
||||
{
|
||||
id: "demo:next",
|
||||
type: "INFO",
|
||||
title: "Next: Calendar",
|
||||
subtitle: "Then Weather + POI",
|
||||
priority: 0.4,
|
||||
ttl_sec: 86400,
|
||||
bucket: "FYI",
|
||||
actions: ["DISMISS"],
|
||||
},
|
||||
{
|
||||
id: "music:now:demo",
|
||||
type: "NOW_PLAYING",
|
||||
title: "Midnight City",
|
||||
subtitle: "M83 • Hurry Up, We're Dreaming",
|
||||
priority: 0.35,
|
||||
ttl_sec: 30,
|
||||
bucket: "FYI",
|
||||
actions: ["DISMISS"],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
winner_id: "demo:welcome",
|
||||
unread_count: 4,
|
||||
},
|
||||
} as const;
|
||||
24
aris/apps/companion/metro.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||
const path = require("path");
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
/** @type {import('expo/metro-config').MetroConfig} */
|
||||
|
||||
const projectRoot = __dirname;
|
||||
const workspaceRoot = path.resolve(projectRoot, "../..");
|
||||
|
||||
const config = getDefaultConfig(projectRoot);
|
||||
|
||||
config.watchFolders = [workspaceRoot];
|
||||
config.resolver.nodeModulesPaths = [
|
||||
path.resolve(projectRoot, "node_modules"),
|
||||
path.resolve(workspaceRoot, "node_modules"),
|
||||
];
|
||||
config.resolver.disableHierarchicalLookup = true;
|
||||
|
||||
module.exports = withNativeWind(config, {
|
||||
input: "./global.css",
|
||||
inlineRem: 16,
|
||||
});
|
||||
57
aris/apps/companion/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "aris",
|
||||
"version": "1.0.0",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"start": "expo start",
|
||||
"prebuild": "expo prebuild",
|
||||
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"",
|
||||
"format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aris/ble": "*",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@rn-primitives/portal": "^1.3.0",
|
||||
"@rn-primitives/slot": "^1.2.0",
|
||||
"@rn-primitives/switch": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"expo": "^54.0.0",
|
||||
"expo-clipboard": "~8.0.8",
|
||||
"expo-constants": "~18.0.9",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-router": "~6.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-web-browser": "~15.0.7",
|
||||
"nativewind": "latest",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~19.1.10",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
61
aris/apps/companion/store/ble.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { EventSubscription } from "expo-modules-core";
|
||||
import { create } from "zustand";
|
||||
|
||||
import { fullFeedFixture } from "@/lib/ble/fixtures";
|
||||
import { Ble, defaultBleState } from "@aris/ble";
|
||||
import type { BleStatePayload } from "@aris/ble";
|
||||
|
||||
export type BleStoreState = BleStatePayload & {
|
||||
isSupported: boolean;
|
||||
isReady: boolean;
|
||||
initialize: () => void;
|
||||
setAdvertisingEnabled: (enabled: boolean) => void;
|
||||
setWifiRequested: (requested: boolean) => void;
|
||||
sendFixtureFeedNow: () => void;
|
||||
};
|
||||
|
||||
let subscription: EventSubscription | null = null;
|
||||
|
||||
const noopSubscription: EventSubscription = {
|
||||
remove: () => undefined,
|
||||
};
|
||||
|
||||
export const useBleStore = create<BleStoreState>((set, get) => ({
|
||||
...defaultBleState,
|
||||
isSupported: Ble.isSupported,
|
||||
isReady: false,
|
||||
initialize: () => {
|
||||
if (!Ble.isSupported || get().isReady) {
|
||||
return;
|
||||
}
|
||||
const state = Ble.getState();
|
||||
set({ ...state, isReady: true });
|
||||
subscription?.remove();
|
||||
subscription =
|
||||
Ble.addStateListener?.((nextState) => {
|
||||
set({ ...nextState });
|
||||
}) ?? noopSubscription;
|
||||
Ble.start();
|
||||
},
|
||||
setAdvertisingEnabled: (enabled) => {
|
||||
if (!Ble.isSupported) {
|
||||
return;
|
||||
}
|
||||
set({ advertisingEnabled: enabled });
|
||||
Ble.setAdvertisingEnabled(enabled);
|
||||
},
|
||||
setWifiRequested: (requested) => {
|
||||
if (!Ble.isSupported) {
|
||||
return;
|
||||
}
|
||||
set({ wifiRequested: requested });
|
||||
Ble.setWifiRequested(requested);
|
||||
},
|
||||
sendFixtureFeedNow: () => {
|
||||
if (!Ble.isSupported) {
|
||||
return;
|
||||
}
|
||||
const payload = JSON.stringify(fullFeedFixture);
|
||||
Ble.sendOpaque(payload, 1);
|
||||
},
|
||||
}));
|
||||
668
aris/bun.lock
@@ -1,9 +0,0 @@
|
||||
import { SafeAreaView } from 'react-native';
|
||||
|
||||
export const Container = ({ children }: { children: React.ReactNode }) => {
|
||||
return <SafeAreaView className={styles.container}>{children}</SafeAreaView>;
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: 'flex flex-1 m-6',
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
/** @type {import('expo/metro-config').MetroConfig} */
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
module.exports = withNativeWind(config, {
|
||||
input: "./global.css",
|
||||
inlineRem: 16,
|
||||
});
|
||||
@@ -1,54 +1,5 @@
|
||||
{
|
||||
"name": "aris",
|
||||
"version": "1.0.0",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"start": "expo start",
|
||||
"prebuild": "expo prebuild",
|
||||
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"",
|
||||
"format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@rn-primitives/portal": "^1.3.0",
|
||||
"@rn-primitives/slot": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"expo": "^54.0.0",
|
||||
"expo-constants": "~18.0.9",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-router": "~6.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-web-browser": "~15.0.7",
|
||||
"nativewind": "latest",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~19.1.10",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
"name": "iris",
|
||||
"private": true,
|
||||
"workspaces": ["apps/*", "packages/*"]
|
||||
}
|
||||
|
||||
2
aris/packages/ble/.eslintrc.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// @generated by expo-module-scripts
|
||||
module.exports = require('expo-module-scripts/eslintrc.base.js');
|
||||
32
aris/packages/ble/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @aris/ble
|
||||
|
||||
BLE peripheral module for the Aris companion app.
|
||||
|
||||
# API documentation
|
||||
|
||||
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/example.com/)
|
||||
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/example.com/)
|
||||
|
||||
# Installation in managed Expo projects
|
||||
|
||||
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
|
||||
|
||||
# Installation in bare React Native projects
|
||||
|
||||
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
|
||||
|
||||
### Add the package to your npm dependencies
|
||||
|
||||
```
|
||||
npm install @aris/ble
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Configure for iOS
|
||||
|
||||
Run `npx pod-install` after installing the npm package.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
|
||||
7
aris/packages/ble/expo-module.config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"platforms": ["apple"],
|
||||
"apple": {
|
||||
"modules": ["ArisBleModule"],
|
||||
"appDelegateSubscribers": ["ArisBleAppDelegateSubscriber"]
|
||||
}
|
||||
}
|
||||
35
aris/packages/ble/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@aris/ble",
|
||||
"version": "0.1.0",
|
||||
"description": "BLE peripheral module for the Aris companion app.",
|
||||
"author": "Iris",
|
||||
"homepage": "https://example.com",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "expo-module build",
|
||||
"clean": "expo-module clean",
|
||||
"lint": "expo-module lint",
|
||||
"test": "expo-module test",
|
||||
"prepare": "expo-module prepare",
|
||||
"prepublishOnly": "expo-module prepublishOnly",
|
||||
"expo-module": "expo-module"
|
||||
},
|
||||
"keywords": ["react-native", "expo", "ble"],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"expo-module-scripts": "^5.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
||||
7
aris/packages/ble/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { Ble, defaultBleState } from "./native";
|
||||
export { BleBluetoothState } from "./types";
|
||||
export type {
|
||||
BleNativeModuleEvents,
|
||||
BleStatePayload,
|
||||
BleUuids,
|
||||
} from "./types";
|
||||
90
aris/packages/ble/src/native.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NativeModule, requireNativeModule } from "expo";
|
||||
import { Platform } from "react-native";
|
||||
import type { EventSubscription } from "expo-modules-core";
|
||||
|
||||
import {
|
||||
BleBluetoothState,
|
||||
type BleNativeModuleEvents,
|
||||
type BleStatePayload,
|
||||
type BleUuids,
|
||||
} from "./types";
|
||||
|
||||
declare class ArisBleNativeModule extends NativeModule<BleNativeModuleEvents> {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
setAdvertisingEnabled: (enabled: boolean) => void;
|
||||
setWifiRequested: (requested: boolean) => void;
|
||||
sendOpaque: (payload: string, msgType?: number) => void;
|
||||
getState: () => BleStatePayload;
|
||||
serviceUUID: string;
|
||||
feedTxUUID: string;
|
||||
controlRxUUID: string;
|
||||
wifiRequestTxUUID: string;
|
||||
}
|
||||
|
||||
const isSupported = Platform.OS === "ios";
|
||||
|
||||
const nativeModule = isSupported
|
||||
? requireNativeModule<ArisBleNativeModule>("ArisBle")
|
||||
: null;
|
||||
|
||||
export const defaultBleState: BleStatePayload = {
|
||||
bluetoothState: BleBluetoothState.Unknown,
|
||||
advertisingEnabled: true,
|
||||
isAdvertising: false,
|
||||
isSubscribed: false,
|
||||
subscribedCount: 0,
|
||||
lastMsgIdSent: 0,
|
||||
lastPingAt: null,
|
||||
lastCommand: null,
|
||||
notifyQueueDepth: 0,
|
||||
droppedNotifyPackets: 0,
|
||||
lastNotifyAt: null,
|
||||
lastDataAt: null,
|
||||
wifiRequested: false,
|
||||
};
|
||||
|
||||
const emptyUuids: BleUuids = {
|
||||
serviceUUID: "",
|
||||
feedTxUUID: "",
|
||||
controlRxUUID: "",
|
||||
wifiRequestTxUUID: "",
|
||||
};
|
||||
|
||||
type BleApi = {
|
||||
isSupported: boolean;
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
setAdvertisingEnabled: (enabled: boolean) => void;
|
||||
setWifiRequested: (requested: boolean) => void;
|
||||
sendOpaque: (payload: string, msgType?: number) => void;
|
||||
getState: () => BleStatePayload;
|
||||
addStateListener: (
|
||||
listener: (state: BleStatePayload) => void,
|
||||
) => EventSubscription | undefined;
|
||||
getUuids: () => BleUuids;
|
||||
};
|
||||
|
||||
export const Ble: BleApi = {
|
||||
isSupported,
|
||||
start: () => nativeModule?.start?.(),
|
||||
stop: () => nativeModule?.stop?.(),
|
||||
setAdvertisingEnabled: (enabled: boolean) =>
|
||||
nativeModule?.setAdvertisingEnabled?.(enabled),
|
||||
setWifiRequested: (requested: boolean) =>
|
||||
nativeModule?.setWifiRequested?.(requested),
|
||||
sendOpaque: (payload: string, msgType?: number) =>
|
||||
nativeModule?.sendOpaque?.(payload, msgType),
|
||||
getState: () => nativeModule?.getState?.() ?? { ...defaultBleState },
|
||||
addStateListener: (listener: (state: BleStatePayload) => void) =>
|
||||
nativeModule?.addListener("onStateChange", listener),
|
||||
getUuids: (): BleUuids =>
|
||||
nativeModule
|
||||
? {
|
||||
serviceUUID: nativeModule.serviceUUID,
|
||||
feedTxUUID: nativeModule.feedTxUUID,
|
||||
controlRxUUID: nativeModule.controlRxUUID,
|
||||
wifiRequestTxUUID: nativeModule.wifiRequestTxUUID,
|
||||
}
|
||||
: emptyUuids,
|
||||
};
|
||||
39
aris/packages/ble/src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export const BleBluetoothState = {
|
||||
Unknown: "Unknown",
|
||||
Resetting: "Resetting",
|
||||
Unsupported: "Unsupported",
|
||||
Unauthorized: "Unauthorized",
|
||||
PoweredOff: "Powered Off",
|
||||
PoweredOn: "Powered On",
|
||||
Other: "Other",
|
||||
} as const;
|
||||
|
||||
export type BleBluetoothState =
|
||||
(typeof BleBluetoothState)[keyof typeof BleBluetoothState];
|
||||
|
||||
export type BleStatePayload = {
|
||||
bluetoothState: BleBluetoothState;
|
||||
advertisingEnabled: boolean;
|
||||
isAdvertising: boolean;
|
||||
isSubscribed: boolean;
|
||||
subscribedCount: number;
|
||||
lastMsgIdSent: number;
|
||||
lastPingAt: number | null;
|
||||
lastCommand: string | null;
|
||||
notifyQueueDepth: number;
|
||||
droppedNotifyPackets: number;
|
||||
lastNotifyAt: number | null;
|
||||
lastDataAt: number | null;
|
||||
wifiRequested: boolean;
|
||||
};
|
||||
|
||||
export type BleUuids = {
|
||||
serviceUUID: string;
|
||||
feedTxUUID: string;
|
||||
controlRxUUID: string;
|
||||
wifiRequestTxUUID: string;
|
||||
};
|
||||
|
||||
export type BleNativeModuleEvents = {
|
||||
onStateChange: (state: BleStatePayload) => void;
|
||||
};
|
||||
9
aris/packages/ble/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
// @generated by expo-module-scripts
|
||||
{
|
||||
"extends": "expo-module-scripts/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "./build"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
|
||||
}
|
||||