spectacles-ble
Spectacles BLE — Reference Guide
Spectacles can act as a BLE Central (scanner + connector) to communicate bidirectionally with any BLE Peripheral — an Arduino, a game controller, a phone app, or any custom BLE device.
Official docs: Spectacles Home · Experimental APIs (BLE enablement)
Enable BLE: Project Settings → Spectacles → Experimental APIs → ✅ Bluetooth Low Energy
Core Concepts
| Term | Meaning |
|---|---|
| Central | The device that initiates connections (Spectacles) |
| Peripheral | The device that advertises and accepts connections (Arduino, phone, etc.) |
| Service | A logical grouping of related functionality, identified by a UUID |
| Characteristic | A specific data value within a service; readable, writable, and/or notifiable |
BLE Module API
const bleModule = require('LensStudio:BleModule')
Scanning for peripherals
const TARGET_SERVICE_UUID = '12345678-1234-1234-1234-1234567890ab'
bleModule.startScan([TARGET_SERVICE_UUID], (peripheral: BlePeripheral) => {
print('Found device: ' + peripheral.name + ' (' + peripheral.id + ')')
bleModule.stopScan() // stop immediately — scanning drains battery
connectToPeripheral(peripheral)
})
Connecting
function connectToPeripheral(peripheral: BlePeripheral): void {
peripheral.connect((success: boolean, error: string) => {
if (success) {
print('Connected to ' + peripheral.name)
discoverServices(peripheral)
} else {
print('Connection failed: ' + error)
}
})
}
Discovering services and characteristics
const SERVICE_UUID = '12345678-1234-1234-1234-1234567890ab'
const CHAR_UUID = 'abcdef01-1234-1234-1234-1234567890ab'
function discoverServices(peripheral: BlePeripheral): void {
peripheral.discoverServices([SERVICE_UUID], (services: BleService[]) => {
const service = services.find(s => s.uuid === SERVICE_UUID)
if (!service) return
service.discoverCharacteristics([CHAR_UUID], (characteristics: BleCharacteristic[]) => {
const char = characteristics.find(c => c.uuid === CHAR_UUID)
if (char) {
activeCharacteristic = char
subscribeToNotifications(char)
}
})
})
}
Reading & Writing Characteristics
Read once
activeCharacteristic.read((data: Uint8Array, error: string) => {
if (error) { print('Read error: ' + error); return }
const value = new DataView(data.buffer).getFloat32(0, true) // little-endian float
print('Sensor value: ' + value)
})
Subscribe to notifications (streaming data)
function subscribeToNotifications(char: BleCharacteristic): void {
char.setNotifyValue(true, (data: Uint8Array, error: string) => {
if (error) { print('Notify error: ' + error); return }
parseIncomingData(data)
})
}
function parseIncomingData(data: Uint8Array): void {
if (data.byteLength < 12) {
print('[BLE] Unexpected packet length: ' + data.byteLength)
return
}
const view = new DataView(data.buffer)
const roll = view.getFloat32(0, true) // bytes 0–3, little-endian
const pitch = view.getFloat32(4, true) // bytes 4–7
const yaw = view.getFloat32(8, true) // bytes 8–11
print(`Roll: ${roll}, Pitch: ${pitch}, Yaw: ${yaw}`)
}
Write to a characteristic
// Send a single byte command
function sendCommand(command: number): void {
const buffer = new Uint8Array([command])
activeCharacteristic.write(buffer, true, (success: boolean, error: string) => {
if (!success) print('Write error: ' + error)
})
}
// Send a float value
function sendFloat(value: number): void {
const buffer = new ArrayBuffer(4)
new DataView(buffer).setFloat32(0, value, true) // little-endian
activeCharacteristic.write(new Uint8Array(buffer), false, () => {})
}
MTU Negotiation
The default BLE MTU is 20 bytes per write. For larger payloads, negotiate a higher MTU first:
peripheral.requestMtu(512, (negotiatedMtu: number, error: string) => {
if (error) {
print('MTU negotiation failed: ' + error)
return
}
print('Negotiated MTU: ' + negotiatedMtu + ' bytes')
// Now you can write payloads up to (negotiatedMtu - 3) bytes at once
// (3 bytes reserved for ATT overhead)
})
If MTU negotiation isn't possible, split large payloads into 20-byte chunks and send sequentially.
Arduino Pattern
Arduino side (C++):
#include <ArduinoBLE.h>
#include <Arduino_LSM9DS1.h> // IMU
BLEService imuService("12345678-...");
BLECharacteristic imuChar("abcdef01-...", BLERead | BLENotify, 12); // 3 floats = 12 bytes
void loop() {
float roll, pitch, yaw;
IMU.readGyroscope(roll, pitch, yaw);
byte data[12];
memcpy(data, &roll, 4);
memcpy(data + 4, &pitch, 4);
memcpy(data + 8, &yaw, 4);
imuChar.writeValue(data, 12);
delay(20); // 50 Hz
}
Lens Studio TypeScript reads the three Euler angles and applies them to a scene object's rotation to mirror the physical board's orientation.
Game Controller Pattern
function parseControllerInput(data: Uint8Array): void {
const buttons = data[0] // bitmask: bit 0 = A, bit 1 = B, etc.
const joyX = (data[1] - 128) / 128 // signed normalised [-1, 1]
const joyY = (data[2] - 128) / 128
const buttonA = (buttons & 0x01) !== 0
if (buttonA) onJump()
moveCharacter(joyX, joyY)
}
Haptic feedback:
function rumble(durationMs: number): void {
sendCommand(0x01) // start rumble
const timeout = this.createEvent('DelayedCallbackEvent')
timeout.bind(() => sendCommand(0x00)) // stop
timeout.reset(durationMs / 1000)
}
Spectacles Mobile Kit
const mobileKit = require('LensStudio:SpectaclesMobileKit')
mobileKit.onMessage.add((message: string) => {
const data = JSON.parse(message)
print('From phone: ' + JSON.stringify(data))
})
mobileKit.sendMessage(JSON.stringify({ event: 'scoreUpdate', score: 42 }))
Connection Lifecycle
let retryCount = 0
const MAX_RETRIES = 5
peripheral.onDisconnected.add(() => {
print('Device disconnected — retrying in 3s')
hideConnectedUI()
if (retryCount >= MAX_RETRIES) {
print('[BLE] Max retries reached. Stopping reconnect.')
return
}
retryCount++
const retry = this.createEvent('DelayedCallbackEvent')
retry.bind(() => connectToPeripheral(peripheral))
retry.reset(3)
})
Common Gotchas
- Enable via: Project Settings → Spectacles → Experimental APIs → Bluetooth Low Energy. Lenses using Experimental APIs cannot be published to a wider audience — BLE is development and sideloading only.
- Scan drains battery — always call
stopScan()once you find your target device. - Service UUID must match exactly between lens and peripheral (case-insensitive, dashes required).
- Data endianness: Arduino's
memcpyandDataView.setFloat32must agree on byte order (true= little-endian on most MCUs). - Always check packet length before reading with
DataView— a peripheral sending fewer bytes than expected causes silent out-of-bounds reads. - Default MTU is 20 bytes — negotiate with
requestMtu()for larger payloads; expect up to 3 bytes of ATT overhead. - Cap the reconnect loop to avoid infinite retries against a device that keeps dropping (e.g., a spoofed peripheral).
- iOS background mode: if building a companion mobile app, enable CoreBluetooth background mode in the iOS app's entitlements.
Reference Examples
- BLEArduino.ts - Shows scanning, connecting, and reading/writing GATT UUIDs from a generic embedded device.
More from rolandsmeenk/lensstudioagents
lens-studio-scripting
Reference guide for the Lens Studio TypeScript component system — covering the @component, @input, @hint, @allowUndefined, and @label decorators, the BaseScriptComponent lifecycle (onAwake vs OnStartEvent, UpdateEvent, DelayedCallbackEvent one-shot and repeating timers, TurnOnEvent/TurnOffEvent, onDestroy), accessing components with getComponent (plus null-check patterns to fix 'cannot read property of null' errors), cross-TypeScript imports with getTypeName(), NativeLogger vs print, prefab instantiation (sync and async), SceneObject hierarchy queries, and enabling/disabling objects. Use this skill whenever writing or debugging any Lens Studio TypeScript script, wiring up scene objects, or fixing 'this is undefined' or null-reference errors — platform-agnostic (works for Spectacles and phone lenses).
12spectacles-cloud
Reference guide for Snap Cloud (Supabase-powered backend) in Spectacles lenses — covering Fetch API setup (requires Internet Access capability in Project Settings), Postgres REST queries with the anon key, Row Level Security policies, Realtime WebSocket subscriptions with correct postgres_changes event format and reconnect-on-sleep patterns, cloud storage uploads of base64 images captured by Spectacles, serverless Edge Functions, and companion web dashboard architecture. Use this skill whenever a lens needs persistent cloud data, needs to share data with a web app in real time, uploads captured images to a bucket, or calls a cloud function — covering Snap Cloud and World Kindness Day samples. Use spectacles-networking for plain REST calls to non-Snap backends, and spectacles-connected-lenses for in-session multiplayer state.
7lens-studio-camera-texture
Learn how to programmatically request and manage raw camera textures and crops.
3spectacles-mocopi-integration
Connect Sony Mocopi motion capture hardware to Spectacles using WebSockets for real-time skeletal tracking.
2