| title | BLE integration |
|---|
VeltoKit accepts bytes and optional rawX. For app code, start with Integration recipes; open this page when debugging packets or BLE modes.
Layered stack for firmware that ships filtered int16 packets at ~20–30 Hz:
| Layer | Type | Role |
|---|---|---|
| BLE | TrikiBLEManager |
Scan, connect, UUID cache, auto-reconnect, notify |
| Parser | TrikiParser |
parse(Data) → ParsedMotionData; preset v2 (int16 @2,4,6 ÷100), legacy + fallback |
| Motion | TrikiMotionEngine |
Velocity, direction, shake / tilt / swing |
| API | TrikiGameController |
TrikiGameInput, onMove / onShake / onAction |
let triki = TrikiGameController()
triki.inputMode = .game // or .smooth
triki.onMove { direction in /* -1…1 */ }
triki.onShake { }
triki.onAction { }
triki.connect()
// Game loop:
let pad = triki.tick(deltaTime: dt)
// pad.direction, pad.velocity, pad.isMoving — no raw X/Y/Z APIMotionSDK.connect() uses this pipeline internally and still publishes GameInput for existing games.
The stack measures Δt between notify packets and debounces mode changes (3 consecutive samples):
| Mode | Typical Δt | TrikiInputStrategy |
Game drivers |
|---|---|---|---|
fast |
< 30 ms | .velocity |
Δpos + position follow |
normal |
30 ms – 200 ms | .hybrid |
Δpos + tilt hold |
lowPower |
> 200 ms | .threshold |
Tilt edges + debounce |
Each pollInput enriches GameInput with bleMode, frameDeltaX/Y, trikiVelocity, tiltLeft/tiltRight.
Use SDK drivers (presets per mode):
var paddle = TrikiPaddleDriver()
var menu = TrikiMenuDriver()
var pointer = TrikiPointerDriver()
var lateral = TrikiLateralDriver()
let x = paddle.steer(current: paddleX, input: input, deltaTime: dt, courtCenter: center)
let menuStep = menu.step(input: input, deltaTime: dt, slots: 4, currentSelection: selected)| Driver | Sample games |
|---|---|
TrikiPaddleDriver |
Pong — bezpośrednio posX w grze (jak przed adaptive input) |
TrikiMenuDriver |
Quiz |
TrikiPointerDriver |
Dart aim |
TrikiLateralDriver |
Bowling aim |
FAST mode shaping: high-rate notify can spike Δpos — TrikiVelocityController applies deadzone → clamp → sensitivity per mode before drivers move gameplay. Source: VeltoKit/Triki/TrikiVelocityController.swift.
Per-game strategies on raw velocity = current − last (never clamped for events). Movement uses filtered signal only in Pong.
GameMode |
Strategy |
|---|---|
.pong |
TrikiControlStyle: .raw (×2.5, dz 0.3), .arcade (×3), .smooth (EMA) — default raw |
.quiz |
posX → slot A–D; przycisk BLE (edge + cooldown) = zatwierdzenie — bez hold / velocity |
.bowling |
Peak velocity + release detection, 0.7 s cooldown |
.dart |
Spike > 7 + 0.5 s cooldown |
var inputMgr = TrikiGameInputManager(mode: .pong)
inputMgr.config.pongControlStyle = .raw // .arcade | .smooth
let frame = inputMgr.process(input: input, deltaTime: dt)
inputMgr.applyPongMovement(to: &paddleX, frame: frame, minX: minX, maxX: maxX)Source: VeltoKit/Triki/TrikiGameInputManager.swift. Sample games wire this in app/Games/.
| Mode | deadzone | max Δ | sensitivity |
|---|---|---|---|
fast |
0.004 | 0.012 | 0.22 |
normal |
0.0018 | 0.028 | 0.42 |
lowPower |
0.005 | 0.045 | 0.62 |
let shaped = TrikiVelocityController.shape(input.frameDeltaX, mode: input.bleMode)triki.onModeChanged { mode in
switch mode {
case .fast: /* full UI */
case .lowPower: /* show triki.idleStatusMessage */
default: break
}
}
let mode = triki.getBLEMode() // or motion.trikiBLEMode
triki.debugBLEMonitorLogging = true // Δt + transitions in consoleimport VeltoKit
let motion = MotionSDK()
motion.setMode(.paddle)
motion.connect() // BLE scan; auto-connect when one likely device is found
// ~60 Hz in your game loop:
let input = motion.pollInput(deltaTime: dt)| API | Role |
|---|---|
connect() |
Start scan; auto-connect when a single likely match (name contains triki) |
disconnect() |
Drop session and reset motion state |
pollInput(deltaTime:) |
Drain parser + updateFrame → enriched GameInput |
isConnected / isReceiving |
GATT link + packets in the last ~350 ms |
liveInput |
Throttled @Published copy for SwiftUI HUD |
calibrateNeutralPose() |
calibrateCenter() + paddle reset (same as sample calibration) |
Requires NSBluetoothAlwaysUsageDescription in Info.plist. Test on a physical iPhone.
Documented from BLEGyroParser + BLEButtonDecoder:
- Repeated blocks:
0x22 0x00+ 6 bytes (3× int16 LE) - Normalized axis value: raw / 2000 (
BLEGyroParser.gyroDivisor) - Multi-block notify: first block → tilt (scaled
/80), last block → gyro used for motion
- Packet header
0x22onbytes[0] - Button state on
bytes[1](0/1) - Rising edge
0→1→ one-frame click impulse (ButtonDetector.consumeClick())
primaryAction mapping depends on MotionMode:
| Mode | Maps to primaryAction |
|---|---|
.paddle |
BLE click edge only |
.pointer, .gesture |
Click or throw or TrikiMotionEngine.isAction |
Triki gamepad velocity (onAction) is still available on GameInput.trikiVelocity / isMoving — it does not set primaryAction in paddle mode, so Quiz and menus are not auto-confirmed by fast tilts.
:::caution Unofficial Packet layout is reverse-engineered for education. Your peripheral may differ — log hex in DEV and adapt. :::
If you already have CBCentralManager notify callbacks:
let motion = MotionSDK()
motion.setMode(.paddle)
motion.enqueueBLE(bytes) // in notify handler
let input = motion.updateFrame(deltaTime: dt) // or read motion.inputPaddle mode may use BLEGyroParser.gyroRawFromPacket inside enqueueBLE without buffering full blocks.
Thin wrapper around MotionSDK for the gametriki demo (ObservableObject, HUD wiring):
let adapter = TrikiInputAdapter()
adapter.connect() // → motionSDK.connect()
adapter.setInputMode(.gesture)
let input = adapter.pollInput(deltaTime: dt)| API | Role |
|---|---|
connect() / disconnect() |
Forwards to motionSDK |
performCalibration() |
Manual neutral pose — Dev Mode ZERO or your own UI |
pollInput(deltaTime:) |
Forwards to motionSDK.pollInput |
motionSDK |
Escape hatch to low-level SDK |
Type alias: MotionInputProvider = TrikiInputAdapter. Source: app/Platform/ (not required for SPM-only apps).
For .paddle, adapter uses MotionParser tilt refresh + updateFrame without full gyro block drain — lower latency for Pong.
parser.flush() → setIngressSupplement → updateFrame → merges impulses (shake) into GameInput.
| You want… | Use |
|---|---|
| Fastest integration | motion.connect() + pollInput() |
| Existing BLE code | enqueueBLE + updateFrame |
| Sample app UX | Copy TrikiInputAdapter from app/Platform |
| Unit tests | ingestTrikiFrame or inject bytes without radio |
public protocol InputProvider: AnyObject {
func pollInput(deltaTime: TimeInterval?) -> GameInput
}Swap your own provider in tests; games stay on GameInput.