Home

Chapter 04 / 8BLE · libmapper · UDP

Implemented

1221 words · 7 min read · Arsh Shah

Sensing

Many signals, one vocabulary

Devices disagree about transport, timing, and shape. RowSim’s input layer makes those differences disappear before they reach the simulation. Native libmapper, a Python UDP bridge, and Concept2 PM5 BLE all become InputEvents, so stroke arbitration and renderer updates can reason about meaning rather than packet formats.

Transport is not meaning

Transport is not meaning. InputEvent is a transport-agnostic enumeration covering coordinates, gestures, accelerometer, world acceleration, velocity, displacement, orientation, roll, azimuth, tilt, and PM5 data and drive events. InputRouter accepts multiple InputProviders, records every event through a UniversalLoggingCenter bucketed at fifty hertz into JSONL plus a merged CSV and a SHA-256–verified manifest, and forwards to the single InputEventSink. Logging is part of the vocabulary contract: the same fan-in that protects the renderer also produces an inspectable research trace.

Native libmapper is the default macOS path. A Python bridge can instead emit UDP packets when ROWSIM_INPUT_UDP equals one, while PM5 BLE contributes ergometer telemetry. Each producer becomes an InputEvent before it is allowed to touch stroke arbitration or renderer uniforms. This fan-in design means the PM5 BLE client, the native libmapper client, and the UDP bridge are interchangeable sources behind one interface—provider replaceability is an implementation claim only because every path already ends in InputEvent.

Stroke handling and renderer updates can therefore reason about meaning rather than packet formats. Adding a sensor becomes a provider problem, not a rewrite of the fluid pipeline. The publication treats that replaceability as an implementation claim grounded in the host code—RenderViewController provider selection, InputEvent.swift, and the bridge’s emit path—not as a speculative plugin marketplace promised without a typed boundary. Convergence before mutation is the sensing chapter’s version of the software chapter’s main-thread authority.

InRouteSinklibmapperNative · 2 msUDPPort 8080PM5 BLELocked snapPython bridgeInputRouterFan-in · 50 Hz logEvent sinkMain-thread meaning

Input vocabulary

Many transports, one event.

Producers disagree about cables and clocks. They agree once they become InputEvent. Dashed satellite: Python bridge → UDP.

01 · ProvidersThree transports, one vocabulary: libmapper, UDP (via the Python bridge), and PM5 BLE all emit InputEvent.

Fig. 04.1

libmapper, UDP (Python bridge dashed), and PM5 BLE converge on InputRouter, then the main-thread sink as InputEvent.

libmapper, UDP, and PM5 BLE

As noted in the normalization section, libmapper, UDP, and PM5 BLE are interchangeable behind InputEvent. The native libmapper provider runs an in-process Swiftmapper graph named fluid_sim_bridge that auto-creates maps from any ergometer.* device’s outputs—world-frame and body-frame acceleration, velocity, displacement, orientation, roll, azimuth, tilt—to the bridge’s inputs. It polls on a background DispatchSourceTimer at a two-millisecond period, translates signals into InputEvents, and delivers them on the main thread. PM5 data is wired in-process alongside the mapper so ergometer ownership does not require a second application process.

The UDP provider uses the Network framework’s NWListener on default port 8080 and parses prefix-based messages such as worldVel:, velocity:, displacement:, pm5:, pm5drive:, roll:, azimuth:, orientation:, legacy aliases, and a bracketed acceleration format. That is the path used when ROWSIM_INPUT_UDP equals one. The Python bridge remains a producer rather than a second renderer: it normalizes and emits sensing packets into the host vocabulary, then steps aside while Metal remains the only fluid authority.

The PM5 BLE client connects over CoreBluetooth to a Concept2 PM5 using service CE060030-43E5-11E4-916C-0800200C9A66 and subscribes to five characteristics—General Status, Additional Status 1 and 2, Stroke Data, and Additional Stroke Data. Each characteristic’s little-endian payload is parsed into a PM5DataSnapshot under an NSLock; drive events are de-duplicated by a signature of strokeState, driveTime, recoveryTime, driveLength, and peakForce. Locked snapshots, not live mutation of Metal state, are the PM5 contract.

Events converge before state changes

Mapper polling, BLE callbacks, and bridge activity occur on distinct queues. Events are copied, normalized, and delivered to the main-thread sink. PM5 snapshots move under a lock before they cross that boundary. UDP arrives on the main queue by construction. The native mapper’s emitOnMain helper is the documented bridge from the poll queue to the sink. This convergence is what keeps sensing concurrency from leaking into simulation ownership.

Timestamp normalization at the sink further protects stroke gating and cooldown paths from mixed-clock regressions when PM5 snapshot internals and emitted event timestamps disagree. The risk register in the architecture guide treats that heterogeneity as a mitigated guardrail rather than an open defect: event timestamps are normalized for stroke gating so cooldown logic does not inherit whatever clock domain a characteristic happened to use.

Python bridge timing depends on its poll-and-flush cadence and UDP scheduling; PM5 ingestion latency depends on BLE notification cadence and queue handoff to the main thread. The architecture does not pretend those tempos are identical. It only requires that they become the same vocabulary before they mutate state, and that experiments log which stack was active because native and Python PM5 paths are protocol-compatible but not timing-identical.

ProducersBoundarySinkMapper poll2 ms · backgroundPM5 BLENotify · lockedUDP listenerPort 8080Copy · normalizeCross to mainMain sinkClock normalize

Event timing

Converge before mutate.

Sensing stays concurrent on the edges. Meaning and cooldown clocks meet only at the sink.

01 · QueuesMapper, BLE, and UDP arrive on different tempos. None may touch Metal from their queue.

Fig. 04.3

Mapper, BLE, and UDP stay on separate queues; copy/normalize hands off to the main sink with clock normalization.

Arbitration before the wake

Stroke sources do not share one refractory. IMU admission tests a 0.4 s unifiedMinStrokeInterval; PM5 admission is a 0.25 s drive-edge cooldown. When the PM5 is connected, velocity drive detection is suppressed and IMU stroke firing is disabled, so the PM5 owns the stroke by design. That is the central stroke-ownership policy: Idle, IMU, Velocity, PM5, and Wizard-of-Oz exist in the source, and the study ran PM5.

Velocity and displacement handlers collect a baseline of forty samples each and subtract it so a stationary sensor reads near zero. The IMU accelerometer path implements a stillness and engagement state machine with slow baseline adaptation under low motion using imuBaselineAdaptAlpha 0.01, engaging after three consecutive samples exceed imuEngageThreshold 0.8 or the jerk threshold, and disengaging after five sub-threshold samples. Hysteresis prevents chatter that would otherwise pepper the wake with false drives.

Beyond ownership, the PM5 modulates the wake on the path that actually ran. Watts scaled trail ink; peak force scaled impulse; boat speed set the carrier; watts also blended into forceScale. The displacement path then attenuated what it inherited: ink × 0.42, force × 0.55, radius clamped to 95 so splat size did not vary with effort. Stroke-rate point density and sinusoidal oar sweep live only in the Wizard-of-Oz generator — flags true, path off in all seventeen sessions. A five-second disconnect timeout marks the connection dead, while a separate two-second timeout removes the PM5 target from the velocity controller.

01IdleReady02IMUAccel03VelocityDrive04PM5Owner05WoZWizardSource-specific refractoryIMU ≥ 0.4 s · PM5 ≥ 0.25 s

Stroke ownership

Source-specific gates.

IMU tests 0.4 s. PM5 admits at 0.25 s. When PM5 is live, IMU and velocity stay quiet.

01 · IdleNo stroke in flight. IMU waits 0.4 s; PM5 waits 0.25 s. The study ran PM5.

Fig. 04.4

Idle, IMU, Velocity, PM5, and WoZ. IMU tests 0.4 s; PM5 admits at 0.25 s. Live PM5 turns the other drive detectors off.

Displacement geometry, smoothed forcing

When sensor displacement trails are enabled—the default—the rower’s world-frame displacement is mapped through UnitMapping into screen-space oar positions. All stroke sources—PM5, velocity sensor, IMU, and the auto-row timer—funnel their trail geometry through beginDisplacementDrivenStroke. The system enforces a monotonically advancing handle X to prevent backward wakes, throttles splat injection by minimum spatial fourteen-pixel and temporal one-forty-fifth-second increments, and scales stroke progress by PM5 drive length when available through typicalDriveMetersForStroke.

Rowing effort reaches the fluid through two cooperating integrators in RenderViewController. A velocity-sensor integrator baseline-subtracts world-frame velocity, detects drive when calibrated velocity falls below defaultDriveThreshold 0.15, integrates propulsiveForce equals handleSpeed times forceScale against quadratic drag with defaultDragCoefficient 0.04, and clamps boat speed to the interval from −maxBoatSpeed to zero with maxBoatSpeed 15.0. A maxPhysicsDeltaTime of 0.1 prevents integration spikes after frame stalls.

A target-tracking controller then pulls boat velocity toward an active target selected from PM5 or IMU with a PD-style response rate, again under quadratic drag and the same speed clamp. PM5 takes priority when connected, when pm5UseForSpeed is enabled, and when within pm5TimeoutInterval 2.0 seconds; otherwise IMU may hold within imuVelocityHoldInterval 0.18 seconds. Two unflagged feel couplings still run: trail ink rises about 39% from stroke momentum alone, while bloom intensity and gold shift follow the product of momentum and interval consistency, up to +63%. Both reset after four seconds without a stroke; the boosted ink is then scaled by 0.42 on the displacement path.

InputForkResultSeeFeelStrokePM5 · IMU · Vel · WoZWake trail×0.42 ink · r≤95Boat speedIntegrate → smoothFloorSee + feel

Trails & physics

One stroke, two jobs.

A stroke paints the wake and drives boat speed on separate paths. They meet again under the athlete’s feet.

01 · StrokeEvery stroke source starts here — same gate as Fig. 4.4. Ownership is already decided.

Fig. 04.5

A stroke forks into two jobs: wake trail (ink ×0.42, force ×0.55, radius ≤95) and boat speed, then meets as floor feel.

Works cited

Numbers match the thesis bibliography. Locators such as ch. 4–5 name chapters of that argument.

  1. [040]

    Paul Dourish (2004).

    Where the Action Is: The Foundations of Embodied Interaction.

    MIT Press.

  2. [081]

    Paul Milgram and Fumio Kishino (1994).

    A taxonomy of mixed reality visual displays.

    IEICE Transactions on Information and Systems, E77-D(12), 1321–1329.

  3. [Th.]

    Arsh Shah (2026).

    RowSim: Designing and Evaluating Ambient Interaction in Mixed-Reality Rowing.

    Master’s thesis, Dalhousie University, Halifax, NS.

    ch. 3Thesis PDF