Skip to content

[PIN Lock] 2 - Enter PIN and unlock writes #359

Description

@KacperKozak

What to build

Let a rider supply their board's PIN while linking, and use it so tune writes actually land on a PIN-locked board.

Two halves of one path:

Entering the PIN. The PIN is a row in the existing linking timeline, not a new wizard step. BoardLinkTimeline already renders the vertical linking checklist (connect / handshake / scan / transport / bms / identity); PIN becomes one more row in that sequence, with the input rendered inline in the row's content slot.

Row states follow the existing vocabulary:

  • board has no pinlock firmware -> absent, caption says so, exactly like the bms row's "No smart BMS"
  • pinlock firmware, no PIN set on the board -> done, nothing to type
  • PIN set -> interactive: inline input in content, validated against the board before saving
  • rider declines -> absent, linking still completes

The typed PIN is validated with COMM_LOCK_STATUSpin_matches answers directly, so no write is attempted to find out. Declining leaves the Board Link outdated (telemetry works, firmware-dependent commands blocked) until the rider re-links.

This lands in both linking surfaces for free, since ScanStep and src/app/editBoard/link.tsx both render the same timeline.

There is deliberately no PIN management surface. Re-link is the whole recovery path: PIN changed in VESC Tool, PIN cleared by a firmware upgrade, or PIN skipped at link time all resolve by re-linking. EditBoardSettings already has a Re-link button and unlinking preserves the board record, so this costs the rider a probe, not their history.

Using the PIN. Refloat tune writes get wrapped in COMM_WRITE_UNLOCK_CMD (157), which unlocks for exactly one inner packet and re-locks after. Reads are untouched — a locked board answers telemetry normally, and only writes are dropped.

The failure mode is the hard part: a wrong PIN produces no reply at all, identical to a dropped packet. Blind retry is actively harmful, because each differing wrong PIN doubles a firmware-side cooldown that never resets until the board reboots. So a timed-out wrapped write must be classified with a COMM_LOCK_STATUS call, not retried.

Likely files

  • src/modules/board/components/BoardLinkTimeline.tsx - the linking checklist; STEP_KEYS, buildSteps, TransportPicker
  • src/components/base/StepTimeline.tsx - generic timeline; TimelineStep.content is the inline-UI slot, StepState the state vocabulary
  • modules/vescape-core/ios/config/ConfigRWController.swift - encodeAndSendWrite is the single write choke point
  • modules/vescape-core/ios/config/ConfigRWStates.kt peer + ConfigRWFsmTest - timeout classification and its tests
  • modules/vescape-core/ios/connection/BoardSessionController.swift - startLinkIntegrityProbe already gates on a persisted link capability (config.hasBms == true)
  • modules/vescape-core/ios/protocol/VescProtocol.swift - envelope encoding

Implementation hints

Envelope format, PIN as big-endian uint16:

[157][pin_hi][pin_lo][ ...inner packet... ]

The inner command's reply comes back through the same reply_func unchanged, so decoders need no changes — only the encoder wraps. Wrap at encodeAndSendWrite, not at the lower send(_ connection:_ payload:), or reads get wrapped too.

Prefer the 157 envelope over "unlock, write, re-lock" via COMM_WRITE_LOCK. The envelope leaves no window in which a second BLE link (boards with both internal and external BLE) can slip a write in.

startLinkIntegrityProbe is the right place to confirm lock state per session — lock_on_boot means a board comes up locked after every power cycle. It already has the shape:

_ = transport.sendPayload(config.transport.frame([UInt8(COMM_FW_VERSION)]))
_ = transport.sendPayload(RefloatConfigProtocol.buildGetInfo(transport: config.transport))
if config.hasBms == true { ...timeout... }

Probe with the saved PIN, never with 0, so the per-session check cannot bump the cooldown. markBmsMissing after a timeout is the template for concluding "no reply".

Cooldown detail worth knowing: resubmitting the same wrong PIN does not increase the cooldown (if (pin != writelock_pin_prev)), only a differing one does. Rider-facing copy should still hedge — "PIN rejected, wait a moment" — because a correct PIN also reports as non-matching while a cooldown is active, and the firmware exposes no remaining-cooldown value.

TransportPicker in BoardLinkTimeline.tsx is the pattern to copy for the inline input — it is attached via content on a row built by row(...), and lives in the picking phase, the one phase that waits for rider input. A PIN row that needs typing has the same "linking pauses for the rider" shape, so reuse that phase rather than inventing a parallel one.

The bms row is the precedent for the not-applicable case: row('bms', 'absent', 'No smart BMS'). A board without pinlock firmware should read the same way.

STEP_REACH / STEP_KEYS drive live progress states during probing; adding a key affects the completed: STEP_KEYS.length count.

Do not add a wizard step. StepTimeline is purely presentational and the caller owns the step list, so this is a change to buildSteps, not to AddBoardWizard.

New reusable UI needs a preview in the component showcase under src/app/settings/components/.

Acceptance criteria

  • PIN is a row in BoardLinkTimeline, not a new Add Board wizard step
  • Row reads absent on boards without pinlock firmware, like the bms row does
  • Row is interactive only when the probe reported a PIN is set
  • Declining completes linking and leaves the link outdated
  • Both linking surfaces (ScanStep, editBoard/link) get it from the shared timeline
  • A typed PIN is validated against the board before being saved
  • A rejected PIN shows copy that hedges on cooldown and does not auto-retry
  • Refloat tune writes are wrapped in COMM_WRITE_UNLOCK_CMD when a PIN is saved
  • Reads and telemetry polling are never wrapped
  • A timed-out wrapped write is classified via COMM_LOCK_STATUS, not blindly retried
  • A stale saved PIN surfaces as a PIN problem pointing at re-link, not a generic write failure
  • Boards without pinlock firmware are completely unaffected
  • iOS and Android stay in @parity; new UI has a showcase entry

Blocked by

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:pin-lockVESC PIN write-lock: capability probe, PIN on Board Link, unlock-wrapped writescomplexity:highCritical paths, subtle correctness, native pipelines. Use opus.ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions