The Vibration channel sends bits by buzzing the sender’s vibration motor and feeling the buzzes with the receiver’s accelerometer, while the two phones are pressed together. It is the slowest channel by far. The aim is to show that data can cross even a purely mechanical coupling, with the same packet protocol and CRC protection as the other channels.
Status: experimental, not reliable yet. This page describes the design. The bit codec passes its unit tests, but on real phones vibration transfers usually fail or never finish. The Simulation Lab doesn’t prove it works either: its
vibration-coupledscenario ends up sending over the simulated optical channel (Simulation Lab quirk 1). See Known Issues §2.8 for the likely causes. Use Light or Sound for real messages and demonstrations.
Back to the documentation index.
SENDER RECEIVER
packet bytes (24 B header + payload + CRC-32) packet bytes → PacketCodec.decode (CRC-32)
│ preamble 0 1 0 1 0 1 1 0 + bits MSB-first ▲ preamble search, header → length, full packet
▼ │
bit 0 → 80 ms buzz ; bit 1 → 180 ms buzz │ pulse length ≥ 130 ms → 1, else 0
gap 60 ms (+50 ms motor settle) │ pulse = |‖a‖ − baseline| ≥ 1.4 m/s²
▼ │
vibration motor ═════ phones pressed together ════ accelerometer (x, y, z)
| Property | Value |
|---|---|
| Topology | One-to-one (physical contact) |
| Data path | Protocol path (ReliableTransport + PacketCodec) |
| Packet payload (MTU) | 48 bytes |
| Status | Experimental: unreliable on real phones |
| Speed | ≈4.2 bit/s ≈ 0.5 B/s (nominal) |
| Platforms | Android and iOS (not web) |
| Area | File |
|---|---|
| Channel (motor TX, accelerometer RX) | lib/core/channels/vibration_channel.dart (HardwareVibrationChannel) |
| Bit codec | lib/core/physical/physical_codecs.dart (VibrationBitCodec) |
| UI state | lib/core/platform/vibration_transmitter_state.dart |
| Packet format | lib/core/protocol/packet_codec.dart |
| MTU | lib/core/physical/hardware_phy_config.dart (hardwareVibrationPacketSize = 48) |
Information is carried by the duration of each buzz, not its strength. Motor strength varies between phones and with how they’re held, but duration can be timed reliably.
Parameter (VibrationBitCodec) |
Default |
|---|---|
shortPulseMs (bit 0) |
80 ms |
longPulseMs (bit 1) |
180 ms |
gapMs |
60 ms |
preamble |
0 1 0 1 0 1 1 0 |
detectionThreshold (absolute) |
12.0 m/s² |
relativeThreshold (against baseline) |
1.4 m/s² |
| Decision boundary | (80 + 180) / 2 = 130 ms |
bytesToBits produces the preamble followed by each byte MSB-first.decodePulseDuration(d) = d ≥ 130 ? 1 : 0.HardwareVibrationChannel.transmit(packet):
start() to have run (otherwise it throws StateError), and skips on non-mobile platforms.bits = codec.bytesToBits(packet).vibrationTransmitterState.setVibrating(true), so the UI’s icon pulses._vibrateFor(duration):
Vibration.vibrate(pattern: [0, d], intensities: [0, 255]) (full intensity).Vibration.vibrate(duration: d).Vibration.vibrate(pattern: [0, d]).HapticFeedback.heavyImpact().setVibrating(false); wait gapMs = 60 ms.Vibration.cancel() and clear the transmitting state.Real bit periods: bit 0 = 80 + 50 + 60 = 190 ms, bit 1 = 180 + 50 + 60 = 290 ms.
HardwareVibrationChannel._processAccelerometer(event) runs for every accelerometer sample:
mag = sqrt(x² + y² + z²) (m/s², includes gravity ≈ 9.8)
signal% = clamp(|mag − baseline| / 6, 0, 1) → UI meter and confidence
active = |mag − baseline| ≥ 1.4
if !active and no pulse in progress:
baseline ← 0.92·baseline + 0.08·mag (tracks gravity and posture while quiet)
rising edge (active, no pulse) → pulseStart = now
falling edge (!active, pulse) → duration = now − pulseStart
if duration ≥ 25 ms: bit = duration ≥ 130 ? 1 : 0
once ≥ 24 bits are buffered: try to decode packets
Baseline filter. The update b ← 0.92 b + 0.08 m is an exponential moving average with α = 0.08. Its time constant is about 1/α ≈ 12.5 samples, which follows slow changes (tilting the phone) but not a 80–180 ms buzz. The baseline is frozen while a pulse is active, so the buzz doesn’t pull it upward. It starts at 9.8 m/s².
Noise rejection. Pulses shorter than 25 ms (knocks, taps) are ignored.
_tryDecodePackets)payloadLength (little-endian, offset 15). If it exceeds 512, the header is garbage: skip one bit past this preamble and retry later.24 + payloadLength + 4 bytes of bits are present.packetCodec.decode(bytes) checks the version and the CRC-32.
The packets are then collected by AppController’s 80 ms poll and reassembled (see Architecture §6).
Vibration always uses the protocol path:
ReliableTransport with the hardware config: ACK timeout 20 000 ms, 8 retries, window 4.See Data Formats §5 and Adaptive Engine.
Average bit period, assuming equally likely 0s and 1s: (190 + 290) / 2 = 240 ms, i.e. about 4.17 bit/s, or about 0.52 B/s raw.
| Message | Envelope | Packet on air | Bits (incl. 8 preamble) | Time |
|---|---|---|---|---|
| “hi” | 9 B | 9 + 28 = 37 B | 304 | ≈73 s |
| “ok” | 9 B | 37 B | 304 | ≈73 s |
| “hello” | 12 B | 40 B | 328 | ≈79 s |
| 48-byte payload (a full packet) | — | 76 B | 616 | ≈148 s |
These times are calculated, not measured; on real phones most transfers don’t complete (see the status note at the top). The per-packet overhead (28 bytes, 224 bits, about 54 s) dominates short messages, so even a working vibration channel would only suit a word or two.
Known timing conflict. A full vibration packet takes longer on air (up to about 148 s) than the 20 s hardware ACK timeout, so in unicast mode the sender may retransmit before the first copy finishes. See Known Issues.
| Method | Values |
|---|---|
capabilities |
name “Vibration (Hardware)”, maxThroughput 800, minLatency 300, binary, hardware-implemented |
discover(timeoutMs) |
Waits timeoutMs / 2, then reports present if the last magnitude ≥ 0.8 × 12.0 |
test(n) |
Samples every 30 ms. Counts “received” when magnitude ≥ 0.7 × 12.0; throughput = 800 × ratio; latency = 80 × 8 = 640 ms; stability 0.65 |
getMetrics() |
Loss from sent/received counts (default 0.08); throughput 800·(1 − loss); latency (180 + 60) × 8 = 1 920 ms; reliability 1 − loss; stability 0.65 |
These values let the Adaptive Engine rank vibration as the channel of last resort.
| Tip | Why |
|---|---|
| Press the phones back to back, firmly, on a soft surface (a hand or a cloth) | Maximises mechanical coupling and damps table resonance |
| Keep both phones still | Any movement shifts the baseline and can create false pulses |
| Send one short word | Each byte costs about 2 s; each packet about 54 s of overhead |
| Turn off keyboard haptics | Stray buzzes on the sender are harmless, but on the receiver they add noise |
Limitations: no forward error correction beyond the packet CRC; motor latency varies by model; iOS offers limited motor control (a haptic fallback is used); the web isn’t supported.