# Fitbit OS SDK Reference for Sense 2 / Versa 4

Comprehensive reference for building and sideloading Fitbit OS apps on the Fitbit Sense 2
(and Versa 4). Covers unofficial SDK setup, build and sideload workflows, all available
device APIs, UI components and limitations, screen specs, and developer bridge configuration.

---

## Table of Contents

1. [Background: Why Unofficial?](#1-background-why-unofficial)
2. [Screen Dimensions and Hardware Specs](#2-screen-dimensions-and-hardware-specs)
3. [Project Setup and package.json Configuration](#3-project-setup-and-packagejson-configuration)
4. [Unofficial SDK Setup (cmengler approach)](#4-unofficial-sdk-setup-cmengler-approach)
5. [Alternative SDK Setup (yeohongred approach)](#5-alternative-sdk-setup-yeohongred-approach)
6. [Developer Bridge Setup](#6-developer-bridge-setup)
7. [Building and Sideloading Workflow](#7-building-and-sideloading-workflow)
8. [Fitbit OS Simulator (for testing)](#8-fitbit-os-simulator-for-testing)
9. [Device APIs Reference](#9-device-apis-reference)
10. [Companion API and HTTP Requests (fetch)](#10-companion-api-and-http-requests-fetch)
11. [UI Components and SVG Reference](#11-ui-components-and-svg-reference)
12. [UI Limitations and Constraints](#12-ui-limitations-and-constraints)
13. [Permissions Reference](#13-permissions-reference)
14. [Project Directory Structure](#14-project-directory-structure)
15. [Complete package.json Example](#15-complete-packagejson-example)
16. [Key Resources and Repositories](#16-key-resources-and-repositories)

---

## 1. Background: Why Unofficial?

The Fitbit Sense 2 and Versa 4 do **not** have an officially released SDK from Fitbit/Google.
They also do not officially support public third-party apps in the Fitbit App Gallery.
However, the community has reverse-engineered the process:

- The watches run Fitbit OS (based on JerryScript, a lightweight JS engine)
- They use the same core architecture as Versa 3 / original Sense but with a different
  build target identifier
- Unofficial SDK packages (pre-release versions of `@fitbit/sdk` and `@fitbit/sdk-cli`)
  can build and install apps on these devices
- The `FITBIT_QA_COMMANDS` environment flag enables the `hosts` command for USB debugging
- Apps can be sideloaded via the Developer Bridge or uploaded as private apps to the
  Fitbit App Gallery

**Code name**: The Sense 2 / Versa 4 build target is referred to as **`hera`** internally.

---

## 2. Screen Dimensions and Hardware Specs

### Fitbit Sense 2

| Property | Value |
|---|---|
| Display type | AMOLED (touchscreen) |
| Screen size | 1.58 inches |
| Resolution | **336 x 336 pixels** |
| Pixel density | ~299 ppi |
| Shape | Square ("squircle" with rounded corners) |
| Dimensions | 38.1 x 38.1 x 11.43 mm (or 40.5 x 40.5 x 11.2 mm depending on variant) |
| Weight | ~37-40g |
| Glass | Corning Gorilla Glass 3 |
| Memory (storage) | ~4 GB internal |
| Battery | 6+ days (3+ days with Always-On Display) |
| Water resistance | 5 ATM (50m) |
| Bluetooth | v5.2 |
| GPS | Built-in |

### Key UI Implications

- **336x336 square canvas**: Your entire app UI must fit in this space
- No bezel or button-area offsets are available in the SDK; all 336x336 pixels are
  addressable
- The display has rounded corners — keep critical content away from the extreme edges
  (safe area: roughly 16px inset on all sides)
- AMOLED means black backgrounds save battery
- Touch input only (no physical keyboard)
- One physical button on the side (maps to `document.onkeydown` / back navigation)

### Build Target Definition (from `fitbit-sdk-build-targets`)

```js
const extraBuildTargets = {
  hera: {
    displayName: 'Fitbit Versa 4',
    minSDKVersion: '7.1.0',
    platform: ['20001.1.1+'],
    resourceFilterTag: '336x336',
    specs: {
      screenSize: { width: 336, height: 336 },
    },
  },
};
```

The Sense 2 uses the same `hera` target with the same 336x336 resolution.

---

## 3. Project Setup and package.json Configuration

### Creating a New Project

```bash
npx create-fitbit-app hevy-workout
```

This scaffolds a project targeting Versa 3 / Sense by default. You then edit
`package.json` to target Sense 2 / Versa 4.

### Required package.json Changes for Sense 2

Two key sections must be modified:

#### A. devDependencies — Use Pre-release Versions

```json
"devDependencies": {
  "@fitbit/sdk": "~6.2.0-pre.1",
  "@fitbit/sdk-cli": "~1.8.0-pre.10"
}
```

These pre-release versions know about the `hera` build target. The official stable
versions (`~6.1.0` / `^1.7.3`) do not.

#### B. fitbit.buildTargets — Target `hera`

```json
"fitbit": {
  "appType": "app",
  "appDisplayName": "Hevy Workout",
  "iconFile": "resources/icon.png",
  "wipeColor": "#000000",
  "requestedPermissions": [
    "access_internet",
    "access_heart_rate",
    "access_activity",
    "access_exercise",
    "access_user_profile"
  ],
  "buildTargets": ["hera"],
  "i18n": {
    "en": { "name": "Hevy Workout" }
  }
}
```

**Critical**: `"buildTargets": ["hera"]` — this is what targets Sense 2 / Versa 4.

### The `enableProposedAPI` Flag

When building for `hera`, you will see a warning:
```
[warn][build] Targeting proposed API may cause your app to behave unexpectedly.
```
This is normal — the `hera` target uses proposed (pre-release) API. The cmengler
approach explicitly sets `enableProposedAPI` for installs.

### Important: Simulator vs. Device

- **Simulator**: Build for `atlas` (Versa 3) or `vulcan` (Sense) — the simulator
  does NOT support `hera`. Test your UI logic here first, then switch to `hera`
  for sideloading.
- **Device**: Build for `hera` to sideload onto Sense 2 / Versa 4.

---

## 4. Unofficial SDK Setup (cmengler approach)

**Repository**: https://github.com/cmengler/fitbit-app-versa4

This is the original and most battle-tested approach. It uses:

1. The `FITBIT_QA_COMMANDS=1` environment variable to enable QA-only CLI commands
   (specifically `hosts`)
2. An unofficial drop-in package `@fitbit/sdk-build-targets` that adds the `hera` target
3. `enableProposedAPI` set to true for installs

### Step-by-Step Setup

```bash
# 1. Clone the reference repo (or use it as a template)
git clone https://github.com/cmengler/fitbit-app-versa4.git
cd fitbit-app-versa4/app

# 2. Install dependencies
yarn install

# 3. Enable QA commands (CRITICAL for USB discovery)
export FITBIT_QA_COMMANDS=1

# 4. (Optional) Enable dev bridge dump for debugging
export FITBIT_DEVBRIDGE_DUMP=1

# 5. Build
yarn build
```

### The @fitbit/sdk-build-targets Drop-in

From https://github.com/cmengler/fitbit-sdk-build-targets — this package simply
exports an additional build target:

```js
const extraBuildTargets = {
  hera: {
    displayName: 'Fitbit Versa 4',
    minSDKVersion: '7.1.0',
    platform: ['20001.1.1+'],
    resourceFilterTag: '336x336',
    specs: {
      screenSize: { width: 336, height: 336 },
    },
  },
};
exports.default = extraBuildTargets;
```

The cmengler approach wires this into the build via a custom `fitbit-build` script
that imports these extra targets.

### Key Takeaway

If using the cmengler approach, clone/fork the repo and build from within the `app/`
subdirectory. The `package.json` there already has all the correct settings for `hera`.

---

## 5. Alternative SDK Setup (yeohongred approach)

**Repository**: https://github.com/yeohongred/fitbit-versa4-sense2-sdk

This approach stays closer to the official Fitbit SDK workflow — you use the standard
`npx create-fitbit-app` scaffolding and then manually edit `package.json`.

### Step-by-Step

```bash
# 1. Create project normally
npx create-fitbit-app hevy-workout

# 2. cd into project
cd hevy-workout

# 3. Edit package.json:
#    - Change devDependencies to pre-release versions
#    - Change buildTargets to ["hera"]
#    (See Section 3 above for exact values)

# 4. Install dependencies
npm install

# 5. Enable QA commands
export FITBIT_QA_COMMANDS=1

# 6. Build
npx fitbit-build
```

### Which Approach to Choose?

- **cmengler**: Simpler if you're starting fresh — clone and go. Fewer manual edits.
  The `yarn debug` command is pre-configured.
- **yeohongred**: Better if you want to understand the mechanism or integrate with
  existing Fitbit tooling. More manual but transparent.

Both produce the same result: an `.fba` file built for `hera` (Sense 2 / Versa 4).

---

## 6. Developer Bridge Setup

The Developer Bridge is the communication channel between your computer, your phone,
and your watch. It's required for sideloading.

### Phone Setup

1. Open the **Fitbit mobile app** on your phone
2. Go to the **Developer Menu** (you need a Fitbit account with developer access
   enabled at https://gam.fitbit.com/)
3. Toggle on **Developer Bridge**
4. Wait for status: "Waiting for Studio"

### Watch Setup (Sense 2)

1. Connect the watch to its **charger** (USB debugging requires external power)
2. Go to **Settings** on the watch
3. Navigate to **Developer Bridge**
4. Toggle **USB debugging** ON
5. Connect the watch to your computer via the **USB charging cable**

### Computer Setup

```bash
# REQUIRED: Enable QA commands
export FITBIT_QA_COMMANDS=1

# OPTIONAL: Dump all dev bridge protocol messages for debugging
export FITBIT_DEVBRIDGE_DUMP=1

# Login to your Fitbit developer account (prompted on first connect)
npx fitbit
```

### Verifying Connections

At the `fitbit$` prompt:

```
fitbit$ hosts
```

Expected output shows both device and phone hosts:

```
Device Hosts:
[
  {
    displayName: 'Sense 2',     # your watch name
    available: true,
    roles: [ 'APP_HOST' ],
    connect: [AsyncFunction: connect]
  }
]
Phone Hosts:
[
  {
    available: true,
    connect: [Function: connect],
    displayName: 'Samsung SM-G991B',   # your phone model
    roles: [ 'COMPANION_HOST' ]
  }
]
```

### Connecting

```
fitbit$ connect phone
fitbit$ connect device
```

---

## 7. Building and Sideloading Workflow

### Complete Workflow

```bash
# 1. Set environment
export FITBIT_QA_COMMANDS=1

# 2. Build the app
npx fitbit-build
# or: cd app && yarn build  (cmengler approach)

# 3. Launch Fitbit shell
npx fitbit
# or: yarn debug  (cmengler approach)

# 4. At fitbit$ prompt:
fitbit$ connect phone
fitbit$ connect device

# 5. Install
fitbit$ build-and-install
# or shortcut: fitbit$ bi
```

### What Happens During Install

- The app's `.fba` file (Fitbit Archive) is pushed to the phone
- The phone pushes it to the watch via Bluetooth
- The companion (if any) is installed on the phone
- The app launches automatically

### Build Output

```
[info][app] Building app for Fitbit Versa 4
[info][companion] Building companion
[info][build] App UUID: 2bcb9c0f-493b-4ec5-9fe0-07039a28ffa1, BuildID: 0x09256fc65528043a
```

### Commands Cheat Sheet

| Command | Description |
|---|---|
| `fitbit$ build` | Build only |
| `fitbit$ install` | Install previously built app |
| `fitbit$ bi` or `build-and-install` | Build + Install in one step |
| `fitbit$ hosts` | List connected devices and phones |
| `fitbit$ connect phone` | Connect to phone companion |
| `fitbit$ connect device` | Connect to watch |

---

## 8. Fitbit OS Simulator (for Testing)

Since the simulator does NOT support Sense 2 / Versa 4, you must test against the
previous generation:

1. Change `buildTargets` to `["atlas"]` (Versa 3) or `["vulcan"]` (Sense)
2. Build and run in the simulator
3. These devices are "basically interchangeable" for app development — if it works
   on Versa 3 / Sense in the simulator, it should work on Sense 2

### Simulator Downloads

- Windows: https://simulator-updates.fitbit.com/download/stable/win
- macOS: https://simulator-updates.fitbit.com/download/stable/mac

### Simulator Limitations

- The simulator does NOT enforce HTTPS-only for fetch (real devices do)
- Some sensor APIs may behave differently (simulated vs. real data)
- File system behavior may differ
- No Bluetooth or real phone companion communication

---

## 9. Device APIs Reference

All APIs run **on the watch** using the JerryScript JavaScript engine (ES5.1 subset
with some ES6 features).

### Sensor APIs

All sensors follow the same pattern: import, check availability, construct with
options (frequency), add `reading` event listener, call `start()`.

#### Accelerometer
```js
import { Accelerometer } from "accelerometer";
const accel = new Accelerometer({ frequency: 1 }); // Hz
accel.addEventListener("reading", () => {
  console.log(`${accel.x}, ${accel.y}, ${accel.z}`); // m/s^2
});
accel.start();
```

#### HeartRateSensor
```js
import { HeartRateSensor } from "heart-rate";
// Requires permission: access_heart_rate
const hrm = new HeartRateSensor();
hrm.addEventListener("reading", () => {
  console.log(`HR: ${hrm.heartRate}`); // beats per minute
});
hrm.start();
```

#### Gyroscope
```js
import { Gyroscope } from "gyroscope";
const gyro = new Gyroscope({ frequency: 1 });
gyro.addEventListener("reading", () => {
  console.log(`${gyro.x}, ${gyro.y}, ${gyro.z}`); // rad/s
});
gyro.start();
```

#### OrientationSensor
```js
import { OrientationSensor } from "orientation";
const orient = new OrientationSensor({ frequency: 1 });
orient.addEventListener("reading", () => {
  // quaternion[0] = scalar, [1],[2],[3] = vector components
  console.log(orient.quaternion);
});
orient.start();
```

#### Barometer
```js
import { Barometer } from "barometer";
const baro = new Barometer({ frequency: 1 });
baro.addEventListener("reading", () => {
  console.log(`Pressure: ${baro.pressure} Pa`);
});
baro.start();
```

#### BodyPresenceSensor
```js
import { BodyPresenceSensor } from "body-presence";
const body = new BodyPresenceSensor();
body.addEventListener("reading", () => {
  console.log(`On wrist: ${body.present}`); // boolean
});
body.start();
```
Note: Cannot set frequency on body presence sensor.

### System APIs

#### Device Info
```js
import { me as device } from "device";
console.log(device.type);           // "WATCH"
console.log(device.modelName);      // "Sense 2" (or "Versa 4")
console.log(device.modelId);        // device model identifier
console.log(device.firmwareVersion);
console.log(device.screen.width);   // 336
console.log(device.screen.height);  // 336
console.log(device.lastSyncTime);   // Date
```

#### App Lifecycle (Appbit)
```js
import { me as appbit } from "appbit";
console.log(appbit.applicationId);
console.log(appbit.buildId);

// Disable auto-timeout (app runs indefinitely)
appbit.appTimeoutEnabled = false; // careful with battery!

// Check permissions
if (appbit.permissions.granted("access_heart_rate")) { ... }

// Exit the app
appbit.exit();

// Handle unload
appbit.onunload = () => {
  // Clean up before app is killed
};
```

#### Clock
```js
import clock from "clock";
clock.granularity = "seconds"; // "off" | "seconds" | "minutes" | "hours"
clock.ontick = (evt) => {
  console.log(evt.date); // Date object
};
```

#### Display
```js
import { display } from "display";
display.addEventListener("change", () => {
  if (display.on) {
    // Display just turned on — start sensors
  } else {
    // Display turned off — stop sensors to save battery
  }
});

// Keep display on
display.autoOff = false; // Note: ignored on AMOLED (SDK 4.0+)
display.poke();          // Turn on + reset auto-off timer

// Brightness (SDK 4.0+)
display.brightnessOverride = "max"; // "dim" | "normal" | "max" | undefined

// AOD (SDK 4.1+)
display.aodAllowed = true; // requires access_aod permission
```

#### Exercise (SDK 3.0+)
```js
import { me as appbit } from "appbit";
import exercise from "exercise";

if (appbit.permissions.granted("access_exercise")) {
  exercise.start("weight", { gps: false }); // predefined or custom type

  if (exercise.state === "started") {
    console.log(exercise.stats.calories);
    console.log(exercise.stats.heartRate.current);
    console.log(exercise.stats.activeTime); // ms
    exercise.pause();
    exercise.resume();
    exercise.stop();
  }
}
```

Predefined exercise types: `run`, `treadmill`, `hiking`, `weight`, `cycling`,
`elliptical`, `spinning`, `yoga`, `stair-climber`, `circuit-training`, `bootcamp`,
`pilates`, `kickboxing`, `tennis`, `martial-arts`, `golf`, `walk`, `workout`, `swim`,
or a custom string.

#### File System
```js
import { readFileSync, writeFileSync, unlinkSync, statSync } from "fs";

// Write
writeFileSync("workout_data.txt", "hello", "utf-8");
writeFileSync("data.json", { key: "value" }, "json");
writeFileSync("data.cbor", { key: "value" }, "cbor");

// Read
const text = readFileSync("workout_data.txt", "utf-8");
const obj = readFileSync("data.json", "json");

// Check existence, delete, stat
const exists = fs.existsSync("file.txt");
fs.unlinkSync("file.txt");
const meta = fs.statSync("file.txt"); // { size: number }
```

File naming rules: Flat namespace, no directories, max 64 chars, only alphanumeric
and `!#$%&'()-@^_{}~.`.

#### Vibration
```js
import { vibration } from "haptics";
vibration.start("ping");     // short pulse
vibration.start("nudge");    // slightly longer
vibration.start("nudge-max");// strongest/longest
vibration.stop();
```

### All Known Device API Modules

| Module | Import | Permission Required |
|---|---|---|
| Accelerometer | `"accelerometer"` | — |
| Appbit | `"appbit"` | — |
| Barometer | `"barometer"` | — |
| Body Presence | `"body-presence"` | — |
| Clock | `"clock"` | — |
| Crypto | `"crypto"` | — |
| Device | `"device"` | — |
| Display | `"display"` | `access_aod` (for AOD features) |
| Document | `"document"` | — |
| Exercise | `"exercise"` | `access_exercise`, `access_heart_rate`, `access_location` |
| File System | `"fs"` | — |
| File Transfer | `"file-transfer"` | — |
| Gyroscope | `"gyroscope"` | — |
| Haptics (vibration) | `"haptics"` | — |
| Heart Rate | `"heart-rate"` | `access_heart_rate` |
| Messaging | `"messaging"` | — |
| Orientation | `"orientation"` | — |
| User Activity | `"user-activity"` | `access_activity` |
| User Profile | `"user-profile"` | `access_user_profile` |

---

## 10. Companion API and HTTP Requests (fetch)

### Architecture

The watch CANNOT make HTTP requests directly. All network access goes through the
**companion** — JavaScript code that runs inside the Fitbit mobile app on the phone.

```
Watch <-- Messaging API --> Companion (on phone) <-- fetch() --> Internet/API
```

### Companion fetch() API

The companion has access to the standard `fetch()` API:

```js
// companion/index.js
import { peerSocket } from "messaging";

// Listen for messages from the watch
peerSocket.addEventListener("message", (evt) => {
  const { url, options } = evt.data;

  fetch(url, options)
    .then(response => response.json())
    .then(data => {
      // Send result back to watch
      if (peerSocket.readyState === peerSocket.OPEN) {
        peerSocket.send(data);
      }
    })
    .catch(err => {
      console.log("Fetch error: " + err);
      if (peerSocket.readyState === peerSocket.OPEN) {
        peerSocket.send({ error: err.toString() });
      }
    });
});
```

### Critical fetch() Limitations

1. **HTTPS ONLY on real devices**: `fetch()` to `http://` endpoints works in the
   simulator but will FAIL on real devices. You MUST use HTTPS.

2. **Self-signed certificates are NOT accepted**: The companion will reject
   self-signed certs. Use Let's Encrypt or another trusted CA.

3. **HTTP on local IP works inconsistently**: Some reports that local network IPs
   (192.168.x.x) work with HTTP, but this is not reliable across Android versions.

4. **Requires `access_internet` permission** in package.json (not needed in simulator).

5. **Companion lifecycle is fragile**: The companion may be killed by the phone OS at
   any time. Long-running operations should be avoided. The companion is woken up by:
   - Peer app launched on device (`peerAppLaunched`)
   - Incoming file transfer (`fileTransfer`)
   - Periodic wake-up timer (`wokenUp`)
   - Settings changes (`settingsChanged`)
   - Location changes (`locationChanged`)

### Messaging API (Watch <-> Companion)

```js
// Both device AND companion use identical API
import { peerSocket } from "messaging";

// Send
peerSocket.send({ type: "fetch_request", url: "..." });

// Receive
peerSocket.addEventListener("message", (evt) => {
  const msg = evt.data;
  console.log("Received: " + JSON.stringify(msg));
});

// Connection state
peerSocket.addEventListener("open", () => { /* connected */ });
peerSocket.addEventListener("close", () => { /* disconnected */ });
peerSocket.addEventListener("error", (err) => { /* error handling */ });

// Buffer management
peerSocket.addEventListener("bufferedamountdecrease", () => {
  // Safe to send more data
});
```

**MAX_MESSAGE_SIZE**: Messages have a size limit. If you need to transfer large data,
use the **File Transfer API** instead:

```js
// Companion: send a file
import { outbox } from "file-transfer";
outbox.enqueue("workout.json", JSON.stringify(workoutData));

// Device: receive the file
import { inbox } from "file-transfer";
inbox.addEventListener("newfile", () => {
  let fileName;
  while ((fileName = inbox.nextFile())) {
    const data = fs.readFileSync(fileName, "json");
    // process data
  }
});
```

### Hevy API Integration Pattern

For a Hevy workout app, the companion would:

1. Receive a fetch request from watch (e.g., `{ action: "get_workouts" }`)
2. Call the Hevy API with appropriate auth headers
3. Return parsed data to the watch
4. The watch caches workout data locally using the File System API

```js
// companion/index.js - Hevy API fetch example
async function fetchHevyWorkouts(apiKey) {
  const response = await fetch("https://api.hevyapp.com/v1/workouts", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
  });

  if (!response.ok) {
    throw new Error(`Hevy API error: ${response.status}`);
  }

  return response.json();
}
```

---

## 11. UI Components and SVG Reference

Fitbit OS apps use a custom XML SVG dialect (`.view` files) for UI, combined with CSS
styling and JavaScript interaction.

### Core SVG Elements

| Element | Description |
|---|---|
| `<svg>` | Root container; can nest. Use `display="none"` to hide screens |
| `<rect>` | Rectangle (backgrounds, dividers, touch targets) |
| `<circle>` | Circle |
| `<image>` | Display a PNG/JPG from `/resources/` |
| `<textarea>` | Multi-line text display (primary text element) |
| `<use>` | Instantiate a `<symbol>` template with specific properties |
| `<defs>` | Definitions block (symbols, stylesheet imports) |
| `<symbol>` | Reusable template (like a component) |
| `<set>` | Set an attribute on a child element |
| `<var>` | Define a variable for child elements |
| `<animate>` | Declarative animation |
| `<link>` | Import stylesheets or widget defs |

### Text Elements

**`<textarea>`** — The primary text display element:
```xml
<textarea id="exercise-name" x="16" y="50" width="100%-32" height="30"
  fill="white" font-size="24" text-length="30">Bench Press</textarea>
```

Key attributes:
- `text-length`: Estimated max characters (affects rendering performance)
- `text-buffer`: Dynamic text content (set via `<set>` or JS)
- `fill`: Text color
- `text-overflow`: "ellipsis" or "clip"

**`<dynamic-textarea>`** — Auto-resizing text area via `<use href="#dynamic-textarea">`

**`<marquee-text>`** — Scrolling ticker text via `<use href="#marquee-text">`

### System Widgets (Pre-built Components)

Fitbit provides system widgets that can be imported:

```xml
<defs>
  <link rel="import" href="/mnt/sysassets/system_widget.defs" />
  <link rel="import" href="/mnt/sysassets/widgets_common.gui" />
  <link rel="import" href="/mnt/sysassets/widgets/baseview_widget.gui" />
  <link rel="import" href="/mnt/sysassets/widgets/combo_button_widget.gui" />
  <link rel="import" href="/mnt/sysassets/widgets/square_button_widget.gui" />
  <link rel="import" href="/mnt/sysassets/widgets/dynamic_textarea.gui" />
</defs>
```

### Virtual Tile List (Scrollable List)

The `#tile-list` is the standard scrollable list widget. It virtualizes items for
performance:

```xml
<use id="exercise-list" href="#tile-list">
  <var id="virtual" value="1" />
  <var id="reorder-enabled" value="0" />
  <var id="peek-enabled" value="0" />
  <var id="separator-height-bottom" value="2" />

  <!-- Pool of reusable items (need ~15 for reliable scrolling) -->
  <use id="item-pool" href="#tile-list-pool">
    <use id="item-pool[0]" href="#exercise-item" class="tile-list-item" />
    <use id="item-pool[1]" href="#exercise-item" class="tile-list-item" />
    <!-- ... up to 14+ items in pool -->
  </use>
</use>
```

**Important Tile List Gotchas**:
- You need 14-15 items in the item pool for reliable scrolling (even though only
  ~5-6 are visible)
- The `virtual` var enables virtualization
- Each item template is a `<symbol>` extending `#tile-list-item`
- Touch targets are handled via a transparent `<rect id="touch" pointer-events="all">`

### Example Workout Item Template

```xml
<symbol id="exercise-item" href="#tile-list-item" class="list-item"
  height="80" focusable="false" pointer-events="none" system-events="all" display="none">

  <rect id="item-bg" x="0" y="0" width="100%" height="100%" fill="fb-black" />

  <!-- Exercise name -->
  <textarea id="item-name" x="10" y="8" width="100%-20" height="24"
    fill="white" font-size="22" font-weight="bold" text-length="30" />

  <!-- Sets x Reps -->
  <textarea id="item-detail" x="10" y="32" width="100%-20" height="20"
    fill="fb-gray" font-size="18" text-length="40" />

  <!-- Weight -->
  <textarea id="item-weight" x="10" y="52" width="100%-20" height="20"
    fill="fb-cyan" font-size="18" text-length="20" />

  <rect id="tile-divider-bottom" class="tile-divider-bottom" fill="fb-dark-gray" />
  <rect id="touch-area" pointer-events="all" fill="transparent" />
</symbol>
```

### Touch/Button Interaction

There are no traditional `<button>` elements in Fitbit SVG. Touch interaction is
handled by:

```js
// Get a rect element used as touch target
const touchArea = document.getElementById("start-button-touch");
touchArea.onclick = (evt) => {
  console.log("Button pressed!");
  // Start workout logic
};
```

Or use the physical button:
```js
document.onkeydown = (evt) => {
  // Physical button pressed
};
```

### Screen Navigation Pattern

```xml
<svg id="main-screen">
  <!-- Main content -->
</svg>

<svg id="workout-screen" display="none">
  <!-- Workout content -->
</svg>

<svg id="rest-timer-screen" display="none">
  <!-- Rest timer overlay -->
</svg>
```

```js
// Switch screens
document.getElementById("main-screen").style.display = "none";
document.getElementById("workout-screen").style.display = "inline";
```

### CSS Styling

Fitbit apps support a subset of CSS:

```css
/* styles.css */
.list-item {
  /* Styling for tile list items */
}

.foreground-fill {
  fill: white;
}

.background-fill {
  fill: black;
}

#rest-timer {
  font-size: 60;
  font-weight: bold;
  text-anchor: middle;
}
```

**Predefined colors**: `fb-black`, `fb-white`, `fb-red`, `fb-cyan`, `fb-blue`,
`fb-green`, `fb-pink`, `fb-yellow`, `fb-orange`, `fb-purple`, `fb-gray`, `fb-dark-gray`.

### Animations

CSS animations and SVG `<animate>` work:
```xml
<animate attributeName="opacity" from="1" to="0" dur="0.3" begin="click" />
```

```css
.my-element {
  transition: opacity 0.3s;
}
```

---

## 12. UI Limitations and Constraints

### Size Limits
- **Install package**: Max 10 MB
- **Total filesystem**: Max 15 MB (including resources and written files)
- **JavaScript engine**: JerryScript (ES5.1 with some ES6). No `class`, limited
  `async/await`, no `Map`/`Set`, no `WeakRef`, limited `Proxy`
- **Filenames**: Flat namespace, max 64 chars, limited character set

### UI Performance Constraints
- **SVG rendering**: Keep DOM shallow — deeply nested elements are slow
- **Tile list pool**: 14-15 items minimum for reliable virtual scrolling
- **Text rendering**: Set `text-length` for best performance
- **No HTML/CSS web rendering**: This is SVG only, not a web browser
- **Image formats**: PNG and JPEG only; use `resourceFilterTag: "336x336"` for
  resolution-specific resources

### Runtime Constraints
- **App timeout**: 2 minutes of inactivity kills the app unless you set
  `appTimeoutEnabled = false` (battery drain risk)
- **Background execution**: Very limited. Apps generally do NOT run in background
  (clock faces are the exception)
- **No multi-threading**: Single-threaded JS event loop
- **Memory is tight**: Be frugal with allocations; avoid large in-memory data
  structures

### Companion Constraints
- **fetch() requires HTTPS** (enforced on real devices, not simulator)
- **No self-signed certs**: Must use CA-trusted certificates (Let's Encrypt works)
- **Companion can be killed anytime**: Save state frequently, use file transfer for
  reliability
- **Wake-up reasons**: Companion wakes on `peerAppLaunched`, `wokenUp` (periodic),
  `fileTransfer`, `settingsChanged`, `locationChanged`

### Sense 2 Specific
- **336x336 resolution** with rounded corners — safe area ~16px inset
- **AMOLED display**: black backgrounds save significant battery
- **Always-On Display (AOD)** possible but requires `access_aod` permission and
  special authorization from Fitbit

---

## 13. Permissions Reference

Permissions are declared in `package.json` under `fitbit.requestedPermissions`:

```json
"requestedPermissions": [
  "access_activity",
  "access_aod",
  "access_exercise",
  "access_heart_rate",
  "access_internet",
  "access_location",
  "access_user_profile",
  "run_background"
]
```

| Permission | Required For |
|---|---|
| `access_activity` | User activity stats (steps, calories, etc.) |
| `access_aod` | Always-On Display (requires Fitbit authorization) |
| `access_exercise` | Exercise API (start/stop workouts) |
| `access_heart_rate` | HeartRateSensor readings |
| `access_internet` | fetch() in companion (not needed in simulator) |
| `access_location` | GPS data in exercise API |
| `access_user_profile` | User height, weight, age, etc. |
| `run_background` | Background execution (limited) |

### Checking Permissions at Runtime

```js
import { me as appbit } from "appbit";

if (appbit.permissions.granted("access_heart_rate")) {
  // Use heart rate sensor
} else {
  // Gracefully handle lack of permission
}
```

---

## 14. Project Directory Structure

```
hevy-workout/
  package.json          # SDK deps + build targets + permissions
  app/
    index.js            # Main device app entry point
    index.view          # SVG UI layout
    styles.css          # Stylesheet
    widgets.defs        # Widget/template definitions (optional)
  companion/
    index.js            # Companion entry point (runs on phone)
  resources/
    icon.png            # App icon (80x80 recommended)
    *.png               # Other image assets
  settings/
    index.jsx           # Settings UI (React JSX, optional)
  common/
    *.js                # Shared code between app and companion
```

### Build Output
```
build/
  app.fba              # Fitbit Archive for the device
  companion.js         # Compiled companion
```

---

## 15. Complete package.json Example

```json
{
  "name": "hevy-workout",
  "version": "1.0.0",
  "description": "Hevy workout tracker for Fitbit Sense 2",
  "scripts": {
    "build": "fitbit-build",
    "debug": "fitbit"
  },
  "devDependencies": {
    "@fitbit/sdk": "~6.2.0-pre.1",
    "@fitbit/sdk-cli": "~1.8.0-pre.10"
  },
  "fitbit": {
    "appUUID": "YOUR-APP-UUID-HERE",
    "appType": "app",
    "appDisplayName": "Hevy Workout",
    "iconFile": "resources/icon.png",
    "wipeColor": "#000000",
    "requestedPermissions": [
      "access_activity",
      "access_exercise",
      "access_heart_rate",
      "access_internet",
      "access_user_profile"
    ],
    "buildTargets": ["hera"],
    "i18n": {
      "en": {
        "name": "Hevy Workout"
      }
    },
    "enableProposedAPI": true
  }
}
```

### Build Target Reference

| Target | Device | Screen | Notes |
|---|---|---|---|
| `hera` | Versa 4 / **Sense 2** | 336x336 | Requires pre-release SDK |
| `atlas` | Versa 3 | 336x336 | Officially supported; works in simulator |
| `vulcan` | Sense (1st gen) | 336x336 | Officially supported; works in simulator |
| `meson` | Versa | 300x300 | Legacy |
| `mira` | Versa 2 | 300x300 | Legacy |
| `higgs` | Ionic | 348x250 | Legacy |

---

## 16. Key Resources and Repositories

### Essential Repos
- **cmengler/fitbit-app-versa4**: https://github.com/cmengler/fitbit-app-versa4
  - Working example with sideloading approach for Versa 4 / Sense 2
- **cmengler/fitbit-sdk-build-targets**: https://github.com/cmengler/fitbit-sdk-build-targets
  - Drop-in package adding `hera` build target
- **yeohongred/fitbit-versa4-sense2-sdk**: https://github.com/yeohongred/fitbit-versa4-sense2-sdk
  - Unofficial SDK guide for Versa 4 / Sense 2

### Official Fitbit SDK Resources
- **SDK Getting Started**: https://dev.fitbit.com/getting-started/
- **Device API Reference**: https://dev.fitbit.com/build/reference/device-api/
- **Companion API Reference**: https://dev.fitbit.com/build/reference/companion-api/
- **UI Guide**: https://dev.fitbit.com/build/guides/user-interface/
- **Communications Guide**: https://dev.fitbit.com/build/guides/communications/
- **Gallery App Manager**: https://gam.fitbit.com/ (enable developer access)
- **Fitbit OS Simulator (Windows)**: https://simulator-updates.fitbit.com/download/stable/win
- **Fitbit OS Simulator (macOS)**: https://simulator-updates.fitbit.com/download/stable/mac

### Community Examples
- **Fitbit/sdk-app-demo**: https://github.com/Fitbit/sdk-app-demo
  - Official demo: tile list, multiple screens
- **Fitbit/ossapps**: https://github.com/Fitbit/ossapps
  - Curated list of open-source Fitbit apps, clocks, and modules
- **Fitbit/sdk-exercise**: https://github.com/Fitbit/sdk-exercise
  - Official exercise app example

### Reference Implementations
- **derekwilson.net**: Excellent blog posts on virtual tile lists and checkbox lists
  in Fitbit OS5 (see search results above for specific articles)

---

## Summary: Building for Hevy on Sense 2

The architecture for a Hevy workout tracker on Sense 2 would be:

```
Sense 2 Watch                    Phone (Fitbit App)              Internet
┌──────────────┐    Messaging    ┌─────────────────┐    HTTPS    ┌──────────┐
│  app/index.js │ ◄────────────► │ companion/       │ ◄─────────► │ Hevy API │
│  SVG UI       │                │   index.js       │    fetch()  │          │
│  Sensors (HR) │                │   fetch() proxy  │            │          │
│  FS cache     │                │                  │            │          │
└──────────────┘                └─────────────────┘            └──────────┘
```

1. Watch sends request via Messaging API to companion
2. Companion calls Hevy API via fetch() (HTTPS required, Let's Encrypt cert OK)
3. Companion sends response back to watch via Messaging
4. Watch renders workout data in tile-list UI
5. Watch caches data with File System API for offline use
6. User logs sets locally on watch
7. Sync back to Hevy API via companion when done
