Open source work
Smart Insoles for Realtime Gait Analysis
A pair of sensorized insoles for real-time gait and plantar-pressure analysis, giving users immediate feedback through a companion app while generating objective movement records for longer-term review. I led the embedded system behind the product, from firmware and BLE architecture to the electronics redesign and reference tooling.
The product was built to close a clinical visibility gap. Patients with movement-related conditions struggle to describe what happens between appointments, while clinicians see only a brief snapshot in the office. The insoles capture everyday movement instead of relying on memory.
The hard part was making two battery-powered BLE devices behave like one reliable product. The firmware had to coordinate sensor acquisition, left-right synchronization, offline recording, power management, provisioning, OTA updates with rollback, diagnostics, and the mobile-facing protocol and documentation.
Project Brief
- Work
- Embedded firmware, electronics redesign, BLE architecture, mobile integration
- Product
- BLE-connected smart insoles for gait and foot-pressure analysis
- Platform
- Nordic nRF52840 in each insole, running Zephyr through Nordic's nRF Connect SDK
- Sensors
- 8 FSRs per insole, 6-axis IMU, sensor fusion
- Sampling
- 200 Hz acquisition, decimated to 100 Hz for analysis, activity based sampling
- Power Target
- At least 14 hours from a 300 mAh battery (met, with headroom remaining)
- Tooling
- nRF Power Profiler Kit 2, MCUboot, mcumgr over BLE, GitHub Actions, Cloudflare R2, Android test app
Product Context
01 The Need
Patients with movement-related conditions often cannot accurately describe their symptoms during brief clinical visits. A patient might feel fine in the office but struggle to explain what a bad day actually looks like.
Clinicians are left with no objective data for the 350-plus days a year between appointments. They see how a patient presents in a controlled setting, not how the patient moves through a normal day.
The product was designed to close that gap with continuous gait and pressure data, recorded during everyday activity, that shows the clinician what happens outside the office.
02 What the Data Enables
The system was designed around individual baselines rather than population averages, because every person walks differently even when healthy. Tracking a patient against their own history makes regression visible earlier than a generic norm would.
For care teams, the value was real-world visibility. Pressure distribution, gait changes, recovery, and adherence could be observed between visits instead of reconstructed from memory during them.
That framing shaped what the firmware had to deliver: a continuous, trustworthy record of movement during ordinary daily life, collected robustly enough that the patient did not have to think about the device.
Challenges Faced
01 Make Two Devices Feel Like One Product
This was the first problem the project posed, before any custom hardware existed. The product is physically two BLE devices, but nobody wants to set up, charge, update, and troubleshoot two devices. Pairing, connection state, battery reporting, data availability, firmware updates, and recovery all had to be presented to the user as one wearable.
It was also the challenge that produced the most important design decisions. Two flaky devices cannot be papered over with coordination logic: each insole had to be robust on its own first, and only then could the synchronization between them be made robust. That ordering shaped the whole firmware architecture.
02 Ship a Demo-able Product Without Mortgaging the Architecture
The pressure that dominated the project was the tension between two timescales: the product needed a reliable, demonstrable version soon, and it needed foundations that would carry requirements that did not exist yet.
The features that are expensive to retrofit, like over-the-air updates, rollback, diagnostics, provisioning, and version compatibility between firmware and app, had to be in the design from the beginning, even though none of them would ever show up in a demo.
Integration complexity ran underneath everything else. Most features only worked when the firmware, the mobile app, and the electronics agreed, and I was responsible for making sure they did.
03 Store Offline Sessions Within a Tight Power Budget
The firmware needed to support real-time BLE streaming when the app was connected, but recorded sessions also had to survive phone unavailability, BLE interruptions, and app disconnects. Offline recording was part of the product design from day one.
The difficult part was the power cost of that safety net. The flash part on the original electronics drew around 20 mA during writes, typical for ordinary flash, and nearly three times the roughly 7 mA of an average BLE transmission in this system.
Writing locally therefore cost more than transmitting, the opposite of the usual assumption, so offline recording had to be designed as a power-critical feature as much as a reliability feature.
04 Preserve Battery Life in a Wearable Form Factor
The insoles were battery-powered and worn inside footwear throughout the day, so power consumption was a primary design constraint. The target was a full work-day: at least 14 hours from a 300 mAh battery, with every hour beyond that a direct improvement to the user experience.
Every firmware feature had to be checked against the power profile. Sampling rate, BLE transmissions, on-device processing, flash writes, sensor polls, synchronization, and sleep modes all drew from the same budget.
05 Work With Electronics That Limited the Product
The original electronics were holding product quality back. The most basic gap was that there was no way to measure the battery level at all: the original design simply had no measurement path for it, and no firmware can read a voltage the hardware never exposes.
The debug connector was nearly as costly. Reaching it required so much disassembly that the boards could not be live-debugged inside a worn insole, which is where the data worth debugging gets produced. I found workarounds to keep development moving, but they cost time on every session and did nothing for the product itself.
USB charging existed without convenient serial access, fault states were invisible to the user, and the PCB occupied space that a larger battery could have used. These were board-level problems, and the fix had to be a new board revision.
06 Keep Firmware Testing Unblocked
Firmware development depended on BLE behavior that the production mobile app could not yet exercise on its side. Several key flows, including configuration, streaming, DFU, health reporting, and synchronization, could not be tested end to end with the production app.
Waiting would have parked that risk until the end of the project, when integration problems are most expensive to fix. To keep embedded development moving, I needed a BLE client that supported the full feature set immediately.
Technical Approach
01 Design First, Then Devkits, Then Hardware
Work started where the risk was highest. The biggest unknown was the two-device architecture itself, so the project began on Nordic devkits, proving that a primary-secondary insole pair coordinating with a phone could work the way I had designed it on paper before committing any of it to product hardware.
From there, firmware development moved onto the existing product electronics. The hardware redesign came later: once the new boards arrived, the firmware was ported across and extended with the features the new hardware made possible, battery monitoring among them.
The supporting work followed the same design-first discipline. The BLE protocol and its interface documentation were fixed before the BLE implementation began, and the Android test app was then built against that contract rather than alongside a moving target. Over-the-air updates, rollback, diagnostics, and provisioning were all present in the first architecture pass.
02 Firmware Architecture
The firmware was designed around a continuous sensor and communication pipeline: pressure sensors and IMU, driver layer, acquisition and filtering, sensor fusion, step event detection, then either the BLE data model or the flash session store, and finally the mobile app.
The architecture separated hardware-specific drivers from application logic so sensor handling, BLE communication, power management, session storage, DFU, and device-health features could evolve independently. It also made the primary-secondary roles explicit: the primary insole acted as its own data source and as a gateway for the secondary insole.
The firmware was built on Zephyr through Nordic's nRF Connect SDK. Both the chip family and the RTOS were familiar ground by this point: I had already rebuilt the MTronic motion and light sensor firmware on the nRF52832 and Zephyr, and first proved that low-power Nordic platform on a wearable epilepsy monitor. The nRF52840 here was a step up in memory and connectivity, but the platform underneath was one I already knew well.
On that foundation I built a hardware-watchdog-enforced, event-driven runtime. Interrupt handlers stayed intentionally lean: they captured time-sensitive events and handed deferred processing to Zephyr work queues. Sensor handling, BLE follow-up work, and state transitions ran outside the ISR path, which kept latency-sensitive paths short, reduced timing jitter, and made the firmware easier to reason about as subsystems were added.
The runtime was organized around explicit operating modes rather than a single always-on behavior. Sleep, walking, high-activity streaming, deferred sync, and firmware-update flows each had different power, communication, and storage expectations, and giving each mode its own contract made those expectations enforceable rather than implicit.
03 Sensor Acquisition and Fusion
Each insole used an array of 8 force sensing resistors to measure plantar pressure. These analog signals were scaled and filtered through op-amp circuitry before being sampled by a high-resolution external ADC. Each insole also carried an ICM-42670 6-axis IMU for motion sensing.
Both pressure and motion data were sampled at 200 Hz and decimated to 100 Hz for analysis, with the rate reduced further in low-activity mode. These numbers mattered more than usual here, because power profiling showed that sensor sampling and onboard analysis, not the radio and not flash, were the dominant consumers in the runtime budget. Battery life depended more on the acquisition path than on anything else.
The firmware did not try to perform every possible calculation. Speed, cadence, deeper gait analysis, and pressure-distribution analysis ran in the mobile app, because nothing about them required embedded hardware. The firmware focused on the work that had to happen close to the sensors: acquisition, fusion, step-event detection, data integrity, and communication. Pressure-matrix data combined with the IMU detected and classified steps, heel strike and toe-off were computed on the device, the IMU's built-in pedometer cross-checked step counts, and the remaining IMU axes fed pose estimation.
Keeping signal conditioning and feature extraction tight to the acquisition path was a habit from earlier biosignal work on the wearable epilepsy monitor, where noisy surface EMG had to be filtered and reduced to features on the device itself before anything left the wearable.
04 BLE Topology and Protocol Design
I designed the BLE interface between the insoles and the mobile application: GATT services and characteristics for sensor streaming, device configuration, device health, firmware updates, connection management, left/right coordination, and stored session synchronization. The primary insole represented the pair to the app and acted as a gateway for the secondary, tunneling raw data through when the app needed both feet, and otherwise sending processed results from on-device analysis.
The topology borrowed directly from my work on the Simplifier Gateway, where multiple safety cores and a communication core exchange validated state over IPC in a safety-critical industrial product. The insoles map onto the same shape: peers with explicit roles, carefully synchronized state, and a third party that must always see a coherent picture. The gateway version of the problem was far more extreme in its determinism and durability demands, which made the insole version feel familiar and considerably more forgiving, and the habits of explicit roles and clean recovery behavior carried over directly.
BLE pairing alone could not determine whether a new peer was the smartphone or the secondary insole, so I implemented an application-level authentication and identity layer on top of BLE using a secure key-based approach. Once identity was established, the connection manager could assign phone, primary, and secondary roles correctly even if peers connected in an unexpected order, without tearing down the session and starting over. A stalled peer that failed to complete identification within its window was disconnected rather than left half-connected.
The keys behind that authentication were provisioned per device during production, alongside serial numbers and other factory data. Treating manufacturing as part of the system design, where every unit needs identity, keys, and a test path before it ships, was a lesson carried over from building the production test cabinet that screens every Simplifier Gateway unit before final assembly.
Sensor and derived-data packets carried timestamps and integrity checks so streamed and synchronized records could be interpreted consistently across firmware and app. The transport layer was MTU-aware: rather than assuming a fixed payload size, it adapted to the negotiated MTU of each BLE link so data could move across the primary-secondary-app topology without avoidable fragmentation.
05 Flash-Backed Session Sync
The product could not depend on the mobile app always being connected, so recorded sessions had to survive disconnection. The challenge, established early by power measurement, was that the original flash part drew around 20 mA during writes against roughly 7 mA for BLE transmission.
I attacked that from both sides: the part and the schedule. On the hardware side, I researched low-power flash options, communicated with suppliers, and selected a part with roughly 4 mA active write current, a fifth of the original, after checking it was available long term, practical to source, and straightforward to develop against.
On the firmware side, writes were restructured around RAM buffering: data was batched in memory and committed in efficient, scheduled writes rather than persisted continuously. Live streaming and deferred recording used consistent data formats, and stored sessions carried enough metadata for the app to understand what was pending and resume an interrupted sync cleanly.
06 Electronics Redesign
The redesign existed because product quality demanded it, and the gaps were concrete. The original design had no battery monitoring at all, and the debug access required so much disassembly that boards could not be live-debugged while being worn. The new revision fixed both, along with measurement quality, user interaction, fault visibility, and mechanical space.
Moving FSR measurement to an external high-resolution ADC improved pressure precision and, just as usefully, freed the MCU's internal analog resources for the battery measurement the product had been missing. A user button added power, reset, pairing, unpairing, and recovery controls. USB serial access and Tag-Connect ECV3 castellated board-edge connectors made development and service workflows practical, including debugging a board inside a worn insole.
Shrinking the PCB won back room for a larger battery, a trade-off between board area and assembly practicality I had worked through before when squeezing the wearable epilepsy monitor down to a roughly 0.5 x 2 inch main PCB.
A fault LED was added with a dead-man-switch behavior: the firmware had to regularly hold the LED off, so if the firmware hung or stopped servicing that mechanism, the LED turned on and the user saw a fault instead of a silently dead device.
07 Power Profiling and Runtime Control
Power profiling began with the project itself rather than as a response to problems. The method came from the MTronic motion and light sensor, where pushing battery life from months to years meant measuring the cost of every operation instead of estimating it: profile individual peripherals and features with the nRF Power Profiler Kit 2, then compare those against the complete system cycle.
Because each peripheral and feature had a measured profile, the battery-life impact of algorithm and scheduling choices could be estimated with confidence before committing to them. This is what surfaced that sensor sampling and onboard analysis dominated the budget, and what made the flash-part comparison earlier a calculation rather than a guess.
The mode-driven architecture existed from the first design pass, and profiling is what it was built for: sleep behavior, walking-mode flash recording, high-activity streaming, sync, and firmware-update flows could each be evaluated against the runtime budget they imposed, instead of leaving power as a tuning pass at the end.
Profiling also became part of regression testing, because new features often reduced battery life in ways nobody predicted. Repeated measurement caught those regressions while they were still cheap to fix, and it is how the 14-hour target was met with room to spare.
Details That Made the Product Robust
Firmware Updates and Rollback
OTA firmware update was built around MCUboot and mcumgr over BLE, with images transferred over a dedicated management channel that worked even if the main application failed to start. It followed the classic active-backup partition structure: a new image installed while a known working image stayed available for rollback.
The interesting part was confirmation. After an update, the newly booted firmware had to pass self-tests before accepting the image: peripheral checks, sensor input checks, voltage checks, and a successful exchange with the mobile app. Requiring app communication was deliberate, because the update itself was pushed from the app. A device that boots cleanly but can no longer talk to the thing that updated it is a failed update by definition, even if every other check passes.
If confirmation did not complete, the device reverted to the last working version. That single mechanism protected against bad boots, bad peripherals, broken BLE behavior, and broken app compatibility alike, and it removed the risk of accepting a build that could boot but not actually run the product.
- GitHub Actions built the firmware binaries and signed the release images.
- The same pipeline generated version manifests and release metadata, then pushed the update artifacts to Cloudflare R2.
- The mobile app performed event-based checks against R2, compared the installed version with available releases, and offered updates when a newer build existed.
Device Health and Fault Visibility
Device-health reporting exposed firmware state, connection status, synchronization state, battery level, DFU state, and operational health indicators to the app, so the app never had to guess what an insole was doing.
The reports were structured diagnostic records rather than generic fault flags, each carrying an error code, source-file identifier, source line, contextual error value, and timestamp, with recent records available to the app for inspection. Firmware bugs could be diagnosed over the air, field failures were easy to classify, and the same records later became the basis for cloud-backed firmware diagnostics.
Cloudflare R2 stored a per-release diagnostic map for decoding these reports and crash dumps. Because every firmware version could shift diagnostic codes and stack-trace locations, the map ensured each health report traced back to the correct source lines for the exact build that generated it, which was essential while the firmware was changing daily.
For the moments when the device could not rely on the app to explain its state, the dead-man-switch fault LED provided a local indication path.
Android Test App as Reference Client
With the protocol and documentation fixed early, I built a dedicated Android test app against that contract, because the production app was not keeping up with the BLE features the firmware needed to exercise.
It started as a development unblocker and grew into everything else: a protocol debugger, a reference implementation for the mobile team, and a field and service tool. In practice it became an admin panel for the insoles, letting me inspect both devices directly, trigger commands, watch live state, and compare firmware behavior across versions without waiting for app-side support.
Of everything built to support the project, this tool had the largest effect on velocity. Firmware features were validated against a working client the day they were written, and integration risk that would normally accumulate until the end of the project was paid down continuously instead.
- Scan, connect, bond, and authenticate devices, and negotiate and inspect MTU state.
- Send every supported command and parse the device responses.
- Parse device-health notifications, raise alerts on reported errors, and push health events to the cloud for analytics.
- Show live state for both insoles: connection, authentication, firmware versions, MTU.
- Visualize live FSR and IMU data.
- Run OTA updates through mcumgr and MCUboot, fetching current and older builds from Cloudflare R2 for firmware A/B testing.
Embedded-Mobile Interface Documentation
The interface documentation was written before the BLE implementation started, which made it a contract rather than a description. It covered GATT services and characteristics, binary data structures, sensor packet formats, configuration commands, expected connection behavior, left/right data handling, stored session synchronization, DFU and rollback behavior, and error-recovery scenarios.
That mattered because the app team and external integrators were not always deeply familiar with BLE packet layouts, binary fields, and low-level firmware conventions. The documentation gave them something to build against instead of forcing them to reverse-engineer the interface from firmware behavior, and it is what allowed the test app and the production app to be developed against the same fixed target.
Part of that contract was a small, well-defined device-status view: current operating mode, battery level, firmware version, bonding and authentication state, enabled BLE services, and whether the primary-secondary link was fully established. With it, the app could tell whether the insoles were ready for streaming, still in setup, or sitting in a partial state that needed recovery, without guesswork or hardcoded assumptions.
Conclusion
The easy version of this product is a sensor pipe that reads the FSRs and the IMU and streams the values to an app. The actual product needed provisioning and factory data, user setup, peer authentication, connection and reconnection behavior, explicit failure modes, recovery paths, over-the-air updates with rollback, structured diagnostics, test tooling, and a cloud release pipeline. All of it had to hold together as one system across two insoles, a phone app, and the electronics underneath, and owning that whole surface was the real work of this project.
The decisions I am most satisfied with are the early ones. Crash analytics, updates that self-test and roll back, firmware-to-app version compatibility, per-device identity, and graceful degradation under fault were settled before the first line of application code rather than patched in after the first field failures. That is the pattern this project confirmed for me: technical rigor and engineering judgment, applied early, are what a robust product is made of.
Outcomes
01 Reliable Two-Insole Experience
The firmware made two physical BLE devices behave like one coordinated product. The primary/secondary topology kept the mobile app experience simple while still supporting raw data tunneling, processed reporting, battery reporting, DFU coordination, and recovery across both insoles.
02 Battery Target Met, With Headroom
The insoles met the 14-hour full-work-day target from the 300 mAh battery. Measurement showed the dominant costs were sensor sampling and onboard analysis rather than radio or storage, so further gains are available through algorithm optimization alone, without touching the hardware.
03 Recording That Survived Disconnection
The flash-backed storage path kept session data safe whenever the app was away, and the combination of a low-power flash part and RAM-buffered, scheduled writes kept the power cost of that safety net low. A dropped connection no longer meant a lost session.
04 Hardware That No Longer Limited the Product
The electronics redesign gave the product battery monitoring it previously lacked entirely, more precise pressure measurement, room for a larger battery, user-facing controls and fault indication, and debug access that finally allowed live work on a worn insole.
05 Faster Firmware Validation
The contract-first protocol documentation and the Android test app removed the main integration bottleneck. Firmware features were validated against a working client before the production app caught up, and both later served the mobile team as a reference for BLE behavior and sensor-data handling.
