spectacles-ble
SKILL.md
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.
Weekly Installs
1
Repository
rolandsmeenk/le…ioagentsGitHub Stars
3
First Seen
11 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1