Skip to content

maximumWriteValueLengthForType returns a hardcoded 20 on JVM/btleplug, ignoring the negotiated MTU #1248

Description

@bkhall

Summary

On the JVM (btleplug) target, Peripheral.maximumWriteValueLengthForType always returns 20, regardless of the
MTU actually negotiated with the peripheral. It also ignores its writeType argument.

This contradicts the documented contract of the function, and unlike the JavaScript target — where the same
limitation exists and is documented — there is nothing in the KDoc telling a caller that JVM behaves this way.

Environment

  • Kable 0.42.0, and unchanged on main as of 0.44.3 (2026-07-22)
  • Target: JVM (btleplug), observed on Windows

Detail

kable-core/src/jvmMain/kotlin/com/juul/kable/btleplug/BtleplugPeripheral.kt:

private const val DEFAULT_ATT_MTU = 23
private const val ATT_MTU_HEADER_SIZE = 3

// ...

override suspend fun maximumWriteValueLengthForType(writeType: WriteType): Int =
    DEFAULT_ATT_MTU - ATT_MTU_HEADER_SIZE

The declared contract in kable-core/src/commonMain/kotlin/Peripheral.kt is:

Return the current ATT MTU size, minus the size of the ATT headers (3 bytes).

On Android, this will be the default (23 - 3) unless you called requestMtu when connecting.
For iOS, this is automatically negotiated, and can also vary depending on the writeType.
On JavaScript, this will return the default (23 - 3) every time as there is no ATT MTU property available.

JVM is absent from that list. Since three of the four targets are described and the JS caveat is called out
explicitly, the omission reads as "JVM behaves like Android/iOS" — which is what makes this quiet rather than
obvious.

Expected: either the current negotiated MTU minus 3, or a documented statement that JVM returns the default,
as JavaScript already has.

Actual: 20, always.

Why this is fixable rather than an inherent btleplug limitation

btleplug's Peripheral trait carries the value already — fn mtu(&self) -> u16, "Returns the currently
negotiated mtu size"
— and it is a required trait method as of btleplug 0.12.0, which Kable already tracks
(#1120). So this looks like surfacing one existing FFI call rather than adding capability:

override suspend fun maximumWriteValueLengthForType(writeType: WriteType): Int =
    ffi.mtu().toInt() - ATT_MTU_HEADER_SIZE

(kable-btleplug-ffi 0.42.0 does not expose mtu at all, so the FFI side needs the binding too.)

Repro

val peripheral = Peripheral(identifier)
peripheral.connect()
// Prints 20 on JVM regardless of what the OS and peripheral negotiated.
println(peripheral.maximumWriteValueLengthForType(WriteType.WithResponse))

Why it matters to us

We use maximumWriteValueLengthForType to decide whether to fragment outbound writes. The peripheral we talk to
prefers an MTU of 247, and the very first client write of every session is a ~132-byte authentication packet.

A hardcoded 20 is worse than no answer here: it is indistinguishable from a genuinely negotiated 20, so a caller
that trusts it fragments messages that never needed fragmenting. In our case whole writes of ~141 bytes were
observed succeeding on this path against real hardware, while splitting to the reported 20 did not work — the
reported value was not merely conservative, it was actively misleading. We currently special-case the JVM target
to treat the number as unknown and skip fragmentation entirely, which is the only safe reading of it.

This also interacts with #298 (throw when a write exceeds the MTU): implemented against the current JVM value,
that would reject every write over 20 bytes on a link that can carry far more.

Secondary, related: no way to request an MTU on JVM

There is no requestMtu on the JVM path — ServicesDiscoveredPeripheral.requestMtu is Android-only — so a JVM
caller can neither ask for a larger MTU nor find out what it got. Whether that belongs in this issue or in the
builder-configuration work already under way in #867 / #811 is yours to decide; happy to split it out.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions