Skip to main content

BLE integration

VeltoKit accepts bytes and optional rawX. For app code, start with Integration recipes; open this page when debugging packets or BLE modes.

Gamepad pipeline (TrikiGameController)​

Layered stack for firmware that ships filtered int16 packets at ~20–30 Hz:

LayerTypeRole
BLETrikiBLEManagerScan, connect, UUID cache, auto-reconnect, notify
ParserTrikiParserparse(Data) → ParsedMotionData; preset v2 (int16 @2,4,6 ÷100), legacy + fallback
MotionTrikiMotionEngineVelocity, direction, shake / tilt / swing
APITrikiGameControllerTrikiGameInput, 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 API

MotionSDK.connect() uses this pipeline internally and still publishes GameInput for existing games.

Adaptive BLE mode (TrikiBLEMonitor)​

The stack measures Δt between notify packets and debounces mode changes (3 consecutive samples):

ModeTypical ΔtTrikiInputStrategyGame drivers
fast< 30 ms.velocityΔpos + position follow
normal30 ms – 200 ms.hybridΔpos + tilt hold
lowPower> 200 ms.thresholdTilt 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)
DriverSample games
TrikiPaddleDriverPong — bezpośrednio posX w grze (jak przed adaptive input)
TrikiMenuDriverQuiz
TrikiPointerDriverDart aim
TrikiLateralDriverBowling 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.

Game-specific input (TrikiGameInputManager)​

Per-game strategies on raw velocity = current − last (never clamped for events). Movement uses filtered signal only in Pong.

GameModeStrategy
.pongTrikiControlStyle: .raw (×2.5, dz 0.3), .arcade (×3), .smooth (EMA) — default raw
.quizposX → slot A–D; przycisk BLE (edge + cooldown) = zatwierdzenie — bez hold / velocity
.bowlingPeak velocity + release detection, 0.7 s cooldown
.dartSpike > 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/.

Modedeadzonemax Δsensitivity
fast0.0040.0120.22
normal0.00180.0280.42
lowPower0.0050.0450.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 console

Simple connection (built into MotionSDK)​

import 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)
APIRole
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 / isReceivingGATT link + packets in the last ~350 ms
liveInputThrottled @Published copy for SwiftUI HUD
calibrateNeutralPose()calibrateCenter() + paddle reset (same as sample calibration)

Requires NSBluetoothAlwaysUsageDescription in Info.plist. Test on a physical iPhone.

Packet shape (lab hardware)​

Documented from BLEGyroParser + BLEButtonDecoder:

Gyro / IMU blocks​

  • 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

Button​

  • Packet header 0x22 on bytes[0]
  • Button state on bytes[1] (0 / 1)
  • Rising edge 0→1 → one-frame click impulse (ButtonDetector.consumeClick())

primaryAction mapping depends on MotionMode:

ModeMaps to primaryAction
.paddleBLE click edge only
.pointer, .gestureClick 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.

Unofficial

Packet layout is reverse-engineered for education. Your peripheral may differ — log hex in DEV and adapt.

Your own BLE stack (no connect())​

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.input

Paddle mode may use BLEGyroParser.gyroRawFromPacket inside enqueueBLE without buffering full blocks.

TrikiInputAdapter (sample app — optional)​

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)
APIRole
connect() / disconnect()Forwards to motionSDK
performCalibration()Manual neutral pose — Dev Mode ZERO or your own UI
pollInput(deltaTime:)Forwards to motionSDK.pollInput
motionSDKEscape hatch to low-level SDK

Type alias: MotionInputProvider = TrikiInputAdapter. Source: app/Platform/ (not required for SPM-only apps).

Paddle path in adapter​

For .paddle, adapter uses MotionParser tilt refresh + updateFrame without full gyro block drain — lower latency for Pong.

Gesture / pointer path​

parser.flush() → setIngressSupplement → updateFrame → merges impulses (shake) into GameInput.

Choose a path​

You want…Use
Fastest integrationmotion.connect() + pollInput()
Existing BLE codeenqueueBLE + updateFrame
Sample app UXCopy TrikiInputAdapter from app/Platform
Unit testsingestTrikiFrame or inject bytes without radio

InputProvider protocol​

public protocol InputProvider: AnyObject {
func pollInput(deltaTime: TimeInterval?) -> GameInput
}

Swap your own provider in tests; games stay on GameInput.

Installation · Architecture