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()setIngressSupplementupdateFrame → 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