Design → Contract → Build

Ski Spot
Architecture Blueprint

From requirements to running code. This site captures every design decision, contract, and architectural choice — the single source of truth before a line of implementation code is written.

01

Project Overview

A cross-platform mobile app that locates lost skis using BLE body-blocking direction finding.

Hardware

Custom "Ski Spot V1.0.0" PCB by Southern Composite Engineering. ESP32-class BLE transmitter mounted on each ski.

Mobile App

React Native (Expo bare workflow). Cross-platform Android + iOS. TypeScript strict mode throughout.

Core Technique

BLE body-blocking: user rotates 360°, app finds lowest RSSI bearing, points arrow in opposite direction toward lost ski.

How Body-Blocking Direction Finding Works

1

Start Scan

User presses START, begins slow 360° rotation

2

Collect Samples

App records RSSI + compass heading at ~10 Hz simultaneously

3

Find Minimum

Bucket into 5° bins, smooth, find weakest signal bearing (body blocking)

4

Point Arrow

Arrow points opposite to minimum = toward the ski. Updates live with compass.

02

Design Lenses

Seven behavioral lenses applied to every design decision. The lenses inform the design. The design informs the build.

R

Reframe

Solve for psychological reality, not logical assumptions.

Insight

A user who lost a ski is stressed and cold. "Scanning..." with no feedback feels broken. A polar chart filling in real-time reframes waiting as progress.

Decision

Show live polar plot during rotation, not a spinner. The act of rotating becomes purposeful rather than uncertain.

F

Friction

Map friction across discovery, cognitive, execution, emotional, and protective dimensions.

Cognitive

User shouldn't need to understand RSSI or BLE. Arrow points at ski. That's it.

Execution

One-tap START. No settings to configure before scanning. Default device auto-selected if only one paired.

Protective

Calibration prompt only when magnetometer accuracy is actually low, not every time.

B

Behaviour

B = MAP. Motivation + Ability + Prompt must converge.

Motivation

High — user has lost an expensive ski. No motivation design needed.

Ability

Must work with gloves on. Large tap targets. No typing required during scan flow.

Prompt

Clear instruction overlay: "Hold phone at chest height and turn slowly in a circle."

H

Heuristic

Universal quality patterns. Design for user goals, not tasks.

Visibility

System status always visible: signal strength bar, compass heading readout, scan progress arc.

Error Recovery

"Incomplete rotation" warning with clear retry — not a dead end. Scan data preserved for partial results.

T

AI Trust

Automation spectrum: from suggestion to autonomous decision.

Confidence

Show confidence indicator with result. Strong null = high confidence arrow. Weak null = "Try moving closer" suggestion.

Transparency

Polar chart shows the raw data behind the arrow. User can visually verify the algorithm's decision.

S

Systems

Second-order effects and feedback loops.

Battery Loop

BLE scanning drains phone battery. Cold weather reduces it further. Show battery warning if below 20%.

Transmitter Life

ESP32 transmitter has finite battery. If no signal found, guide user: "Is the Ski Spot turned on? Battery may be depleted."

E

Emotion & Meaning

Functional → Reliable → Usable → Pleasurable.

Relief

Success state: satisfying animation when ski is found. Haptic feedback on result. The moment of relief should feel earned.

Encouragement

Weak signal state: "Getting warmer..." as RSSI improves on approach. Gamify the walk toward the ski.

03

Architecture

Three-layer architecture: Surface (interface), Core (logic), Edge (hardware).

Surface Layer

Interface

React Native App
Three-tab Navigation
Animated Direction Arrow
Real-time Polar Chart
Device Management UI
Core Layer

Platform Logic

Bearing Algorithm
Signal Smoothing (EMA)
Circular Statistics
Rotation Validation
Zustand State Management
Edge Layer

Hardware Bridge

BLE Scanner (react-native-ble-plx)
Magnetometer (react-native-sensors)
Platform Permission Layer
Device Filtering (UUID + Name)
AsyncStorage Persistence

Source Structure

src/
app/                        # Navigation + screens
  (tabs)/
    FindScreen.tsx            # Main scan/rotate/result
    DevicesScreen.tsx          # Paired device management
    SettingsScreen.tsx
  _layout.tsx

components/                 # Reusable UI
  DirectionArrow.tsx          # Animated compass arrow
  RSSIPolarChart.tsx          # Real-time polar plot
  ScanButton.tsx              # START/STOP toggle
  DeviceCard.tsx              # BLE device list item
  SignalStrengthBar.tsx
core/                       # Business logic
  algorithm.ts                # Bearing calculation
  ble.ts                      # BLE scanning + RSSI
  compass.ts                  # Magnetometer heading
  types.ts                    # Shared interfaces

stores/                     # Zustand state
  scanStore.ts                # Scan state + samples
  deviceStore.ts              # Paired devices

utils/                      # Pure functions
  angles.ts                   # Circular angle math
  filters.ts                  # Signal smoothing

constants.ts                  # UUIDs, timing, thresholds
04

Contracts

Machine-readable specifications that bridge design intent and implementation. No code is written until contracts are approved.

Click any contract to expand the specification.

Core TypeScript Interfaces

These types form the contract between all modules. Every function signature references these.

interface RSSISample {
  heading: number;     // 0-360 degrees from magnetic north
  rssi: number;        // dBm, typically -30 to -100
  timestamp: number;   // ms since epoch
}

interface BinnedRSSI {
  centreHeading: number;  // Centre of the 5-degree bin
  avgRSSI: number;        // Mean RSSI in this bin
  sampleCount: number;    // Number of samples in bin
}

interface ScanResult {
  deviceBearing: number;   // Computed direction to device
  confidence: number;      // 0-1 based on null depth
  coverage: number;        // Fraction of bins covered (0-1)
  bins: BinnedRSSI[];        // For polar chart display
  rawSamples: RSSISample[];  // Full dataset
}

interface PairedDevice {
  id: string;            // BLE device ID (platform-specific)
  name: string;          // User-assigned name ("Left Ski")
  hardwareName: string;  // Original BLE advertised name
  lastSeen: number;      // Timestamp of last detection
  lastRSSI: number;      // Most recent signal strength
}

type ScanPhase = 'idle' | 'scanning' | 'computing' | 'result';

interface AlgorithmConfig {
  binSizeDegrees: number;      // Default: 5
  smoothingWindow: number;     // Default: 3 bins
  emaAlpha: number;            // Default: 0.3
  coverageThreshold: number;   // Default: 0.85
  outlierThresholdDb: number;  // Default: 20
}

Component Tree

Every UI component, its props, and parent-child relationships.

App
├── NavigationContainer
│   └── BottomTabs
│       ├── FindScreen
│       │   ├── DeviceSelector        props: { devices, selected, onSelect }
│       │   ├── ScanButton            props: { phase, onStart, onStop }
│       │   ├── RSSIPolarChart        props: { bins, currentHeading }
│       │   ├── DirectionArrow        props: { bearing, compassHeading }
│       │   ├── SignalStrengthBar     props: { rssi, min, max }
│       │   ├── CoverageArc           props: { coverage, threshold }
│       │   └── ScanInstructions      props: { phase }
│       ├── DevicesScreen
│       │   ├── DeviceCard[]          props: { device, onRename, onRemove }
│       │   ├── AddDeviceButton       props: { onPress }
│       │   └── ScanModal             props: { visible, found[], onSelect }
│       └── SettingsScreen
│           ├── FirmwareInfo          props: { version }
│           └── ResetButton           props: { onReset }

Zustand State Schemas

Two stores: scan state (ephemeral) and device state (persisted).

// ─── scanStore.ts ───
interface ScanStore {
  // State
  phase: ScanPhase;
  targetDeviceId: string | null;
  samples: RSSISample[];
  currentHeading: number;
  currentRSSI: number | null;
  result: ScanResult | null;
  error: string | null;

  // Actions
  startScan: (deviceId: string) => void;
  stopScan: () => void;
  addSample: (sample: RSSISample) => void;
  updateHeading: (heading: number) => void;
  updateRSSI: (rssi: number) => void;
  reset: () => void;
}

// ─── deviceStore.ts ───
interface DeviceStore {
  // State (persisted to AsyncStorage)
  pairedDevices: PairedDevice[];
  selectedDeviceId: string | null;

  // Actions
  addDevice: (device: PairedDevice) => void;
  removeDevice: (id: string) => void;
  renameDevice: (id: string, name: string) => void;
  selectDevice: (id: string) => void;
  updateLastSeen: (id: string, rssi: number) => void;
  resetAll: () => void;
}

Data Flow

How data moves through the system during a scan cycle.

1

BLE Advertisement Received

ble.ts → scanStore.updateRSSI(rssi)

2

Magnetometer Reading

compass.ts → scanStore.updateHeading(heading)

3

Sample Created (merged at ~10Hz tick)

scanStore.addSample({ heading, rssi, timestamp })

4

EMA Applied Per-Sample

filters.ts → exponentialMovingAverage(rssi, α=0.3)

5

User Stops Scan

scanStore.stopScan() → algorithm.calculateDeviceBearing(samples)

6

Bucket → Smooth → Find Minimum → Invert

algorithm.ts → ScanResult { deviceBearing, confidence, bins }

7

Result Rendered

DirectionArrow rotation = deviceBearing - currentCompassHeading

Platform Permissions Matrix

Required permissions and platform-specific behaviour.

Permission iOS Android Notes
Bluetooth NSBluetoothAlwaysUsageDescription BLUETOOTH_SCAN + BLUETOOTH_CONNECT Android 12+ requires both
Location Not required for BLE ACCESS_FINE_LOCATION Android requires for BLE scanning
Motion NSMotionUsageDescription None required Magnetometer access
Background Foreground only Foreground only Scan requires active user
05

Build Phases

Three-phase cadence: Concept → Contract → Cut. We are currently in the Contract phase.

Complete
Phase 1

Concept

Requirements decomposition, architecture specification, stack selection.

  • Brief decomposition
  • Architecture specification
  • Tech stack selection
  • Core algorithm design
  • Platform constraints mapped
Active
Phase 2

Contract

Formal blueprints as reviewable specifications. No code until approved.

  • TypeScript interfaces defined
  • Component tree mapped
  • State schemas specified
  • Data flow documented
  • Permission matrix complete
  • Design lens review approved
  • Algorithm edge cases documented
  • UX flow prototyped
Pending
Phase 3

Cut

AI-driven code generation against approved contracts. Operator review at each milestone.

  • Core algorithm implementation
  • BLE + compass modules
  • Zustand stores
  • Screen components
  • Platform integration + testing
  • Field testing with hardware
06

Design Decisions

Open questions and architectural decisions that must be resolved before the Cut phase. Add notes to refine the final build.

Open

BLE Service UUID

The actual service UUID from the Ski Spot V1.0.0 firmware must be obtained via nRF Connect scan or firmware source code.

Impact: High Blocks: BLE scanning implementation
Open

Optimal Rotation Speed

Target 10-15 seconds for 360°. Too fast = sparse data, too slow = RSSI drift. Needs field testing to calibrate.

Impact: Medium Blocks: UX instruction text
Open

Multi-Ski Mode

Scan for both skis simultaneously or one at a time? Simultaneous is better UX but doubles processing and may confuse the polar chart.

Impact: High Blocks: Find screen design, store schema
Open

Range Limits & Confidence Threshold

At what RSSI threshold is the body-blocking null too faint to detect? Estimated around -85 dBm. Below this, results are unreliable.

Impact: High Blocks: Confidence scoring, "move closer" UX
Open

Snow & Weather Effects

Wet clothing may change attenuation pattern. Buried ski under snow may have different BLE propagation characteristics.

Impact: Medium Blocks: Algorithm tuning
Open

ESP32 Advertising Interval

Must be fast enough for ~10 Hz sampling. Configure firmware to advertise every 50-100 ms. Faster = more battery drain on transmitter.

Impact: High Blocks: Sampling rate, algorithm accuracy
Open

Confidence Scoring Algorithm

How to quantify result confidence? Options: null depth (dB difference between min and max RSSI), null sharpness (angular width of the dip), or combined metric.

Impact: Medium Blocks: Result UI, "try again" threshold
07

Tech Stack

Opinionated defaults to reduce decision overhead. Each choice has a rationale.

Framework
React Native (Expo bare)

Single codebase, native BLE access via bare workflow

BLE
react-native-ble-plx ^3.x

Mature lib, supports RSSI scanning without connection

Compass
react-native-sensors ^7.x

Raw magnetometer at high sample rate

State
Zustand ^4.x

Lightweight, no boilerplate, works with RN

Navigation
React Navigation ^6.x

Stack + bottom tabs, standard RN navigation

Storage
AsyncStorage ^1.x

Persist paired device list across sessions

Language
TypeScript (strict)

No any. Type safety across BLE data structures

Testing
Jest + RNTL

Unit tests for algorithm, component tests for UI

Hardware
ESP32 BLE (Ski Spot V1.0.0)

Custom PCB by Southern Composite Engineering