The Project
I built and shipped OMATA's first commercial iOS utility app, my first commercial mobile app and my first iOS app. I taught myself the platform while the product was still asking basic questions about Bluetooth, device configuration, hand calibration, ride-data transfer, FIT-file decoding, Strava uploads, testing, and App Store delivery.
The Outcomes
I entered an unfamiliar technical system, identified what had to exist for the product to work, learned the necessary tools, and delivered the result under real product constraints. Tyson Soelberg and Pepijn Van Eeckhoudt were active collaborators who volunteered their time because they wanted OMATA and its brand to succeed.
Sometimes I wonder if I do projects to feel the satisfaction of having done them as much as whatever their purpose might be from the perspective of their initial motivation. And then sometimes I wonder if I do projects because the stories are the purpose. When it became clear that either I got this App done or the entire endeavor of OMATA was going to falter, I remember a feeling of dreadful excitement. It wasn’t even like I had a clear sense as to how I would get this done (I mean technically, which felt daunting.) What I did know is that it had to get done. There was no optionality. And that was the story; that I could do something that I didn’t know how to do. And I also knew that someday I would have to put it down on the page as some kind of record of how I work when the commercial, personal, technical demands are real and I don’t yet know what I’m doing. So here we are. I’d never shipped an iOS App and had barely done much more than “Hello, World”. But, I learned it out on the kitchen table, even while the product kept changing underneath me; even with specs and screen designs that I never really balked at even while looking at them I had a feeling that everything was an unknown. And there was this Bluetooth thing to a connected device whose firmware was barely realized.
Physical behavior, device protocols, data structures, interfaces, async state, auth, testing, release. I’d build a thing, watch it come out too slow or too fragile or too confusing, and build it again around what the device actually did. The repository holds all of that in the form I trust most — working code, revised models, bug fixes, the diagnostic tools I wrote just to see what was going on, and the release commits.
I carried it through the last stretch, too, which is the part nobody photographs. The first screen felt like a milestone and it wasn’t. The product showed up in an accumulation of small things: the right local directory. A callback that actually fires. A summary file that makes sense. Calibration state that holds. A safeguard around a transfer that takes too long. An OAuth flow that works. Screenshots at whatever dimensions Apple wants this month.
First commercial iOS app. No formal training, no previous release. What I had was a product that needed an app, a list of things I didn’t know that kept getting longer, and a conviction that the app had to exist. So I worked out what had to be learned, learned it, and kept building until the utility could hold the instrument up out in the world. That’s what I’d want anyone deciding whether to work with me to look at.
The Git history shows what changed. The diary shows what it felt like to carry the change while also carrying the company.
The line is blunt because the period was blunt. The app was one thread in a much bigger tangle — support, shipping, marketing, manufacturing, sales. It still had to be built carefully enough to carry the product into other people’s hands.
Diary excerpts on this page are verbatim, dated entries from my two OMATA process-diary volumes, Diary of a Hardware Startup. … marks an elision within an entry; nothing else is altered except two obvious keystroke typos.
OMATA One was a physical instrument with a second life inside a phone. The device could display the ride while I was riding, yet almost everything around the ride depended on the utility app. It paired with the instrument, read its settings and totals, calibrated the hands, transferred ride files, decoded them into useful summaries, connected to Strava, and provided the controls that kept the device usable over time.
I had never shipped a mobile application before. No practice to fall back on, no earlier release to crib from. The product needed the app, so I needed to learn how to build it. That was the whole calculation.
Swift, Apple’s Bluetooth APIs, the structure of FIT files, asynchronous device communication, local data modeling, OAuth, interfaces, testing, crash reporting, Fastlane, certificates, App Store delivery — all of it learned while the project was moving. The curriculum arrived as a sequence of inconveniences. A file was in the wrong directory. A callback never arrived. A device connected twice. A ride took too long to decode. A screen had to explain where a physical hand was pointing and why. I’d learn the thing in front of me, then go back to the thing that still didn’t work.
There was no version of this where I left the app half-understood. Without a way to pair, configure, calibrate, update, transfer, and share, OMATA simply could not exist in the form we were trying to make. So I worked out what the platform wanted, taught myself enough to give it that, and kept going.
In my project book(s) I found two estimates for the app work. An agency that was preferred had a May 17, 2016 estimate for an iOS Utility App V1 described a three-month effort with senior design and engineering time and a total of $173,250 after a 25% discount. The Android estimate described a two-month effort and came to $94,050 after the same discount. Together they put an outside-market comparison of $267,300 beside the work the product needed.
No bueno. There were a couple of issues. The main one was not the estimate; that was the secondary one. The main one was my instinct that said that hiving off the work to someone else meant that the knowledge and expertise was going somewhere else and there’d be this forever consequence of not knowing how thigns worked at the level I knew I needed. If the App wasn’t done fully “in house” it would always be a critical path towards updates, fixes, changes — an unacceptable externality.
Those numbers matter because the app was never a side project. It was a required part of the instrument. The quote was fair — and we didn’t have the money for that team, or for the maintenance that would follow it. So the choice was to keep waiting on a budget that wasn’t coming, or learn the work and carry it myself. I chose the work.
So then what I was setting myself up to become was “$300k of work” — a multi-person design-and-engineering project compressed into one person’s nights, weekends, and working days. The value was in the code, the released application, the accumulated debugging knowledge, and the ability to keep managing the app after its first release.
I had to skill up to do the App. That sentence is from the diary, and it’s the center of this whole project. I’d built sketches and small experiments before. This was different — a commercial release, no formal training, no App Store history, no team of specialists standing behind me. I learned Swift, Xcode, Core Bluetooth, FIT data, OAuth, local storage, testing, certificates, and release operations because OMATA needed those things to exist and nobody else was going to do it.
Tyson and Pepijn volunteered their time because they wanted to see the product and the brand succeed. Pascal Wever brought serious UI and UX work. Harri at Haltian walked me through the device and firmware side. Will took my FIT SDK questions. That belongs in the record and I want it there.
What I carried was the job of holding all those pieces together, and I was doing it inside a company where the division of labor had gone lopsided. That part got under my skin, and the diary says so plainly enough.
| Quoted work | Timeline | Estimate after discount |
|---|---|---|
| iOS Utility App V1 | Approx. 3 months | $173,250 |
| Android Utility App V1 | Approx. 2 months | $94,050 |
| Combined outside comparison | Design and engineering scope | $267,300 |
Before there was a stable app, there was a flow diagram. The December 21, 2016 B1 App UX board names the states that the hardware would force the software to handle: account creation, device discovery, pairing, a summary view, calibration, upload, activity files, and the recovery paths around a timeout. It also assigns responsibility between the front-end app, the device Bluetooth API, and the firmware.
I keep that board because it is the work before it became code, and because it turned out to be right. What the diagram already knew, and I did not yet, is that the app was never really an app — it was a coordination problem between a rider, a phone, a physical instrument, a data record, and a service that none of them controlled. Drawing it made a sequence out of that. Every line of Bluetooth code I wrote afterwards had to answer to the sequence on that board.
Before the app there was the BLE Tool, which was not an app at all. Buttons for battery information, calibration, activity files, totals, device files, settings, system logs, last activity — stacked up with no design to speak of. The diary compares it to computing from 1997, which is generous. Ugly was the point. It gave me somewhere to interrogate the device API and get straight answers out of it, months before any of that had to look like something a customer would touch.
The download workflow was manual on purpose. Bluetooth on. Put the OMATA into connect mode. Find the device. List its activities. Download a ride. Go and locate the FIT file it produced. Upload that to Strava. Seven steps, each one a seam between the device, the computer, the filesystem, and the service — and a seam is where things come apart. Doing it by hand meant I could watch exactly which one failed, and then go and build that knowledge into the app. Automating it first would have hidden the very thing I needed to see.
There is a seven-minute screen recording from this part of the work. I’m talking through Xcode, opening folders, running an example, trying to explain exactly where everything stops working. The menu bar says Tuesday, February 21, at 8:51 in the morning. The SDK files are from February 2017. This is before the March starting point of the main app repository’s history below; I was already working through the pieces the app would need.
I wanted to use Dynastream’s FIT SDK because I needed to read and write ride files, and handling the format myself was turning into a substantial job. In the recording I explain the definition records and the data records: one describes the fields to expect, the other carries their values. A definition could appear partway through a file, with the records it described following it. I was trying to understand a binary format shared by devices from different manufacturers, with all the variation that implied. Using the library supplied by the people behind the format seemed sensible. I thought I could include it in the project and get on with building the app.
The SDK even came with an iOS example. I had its C++ static library, an Objective-C++ view controller, and Xcode 8.3 beta 2. The library’s name appeared red in the project navigator, which made it look missing. I checked that it was there. I built the example. It worked. I cleaned the project and built it again. Still worked. Then I put in a breakpoint and ran it in the simulator to check that the example code actually executed. There was almost nothing on the phone’s screen, but the code ran.
Then I added a Swift file. That was the language I wanted to write the app in. Xcode didn’t offer to create a bridging header on the first attempt; on another attempt it did. I checked the build settings to make sure the project knew about the header, then tried importing fit.hpp, which the working example already used. The build failed immediately: map could not be found, followed by a failure to import the bridging header. The same SDK header was usable from the example’s Objective-C++ file and broke the build when I brought it through this route into Swift.
You can hear me checking my own understanding as I go. I recognise map as part of the C++ standard library, open the header, and follow the error back to the include near the top. I haven’t solved it. I’ve got a working example and a small change that breaks it, and I can show exactly what I did.
I like having this recording in the account of the app. It catches the work while I’m still inside the problem, explaining it out loud and asking for help. This was my first commercial iOS app. Learning enough Swift to draw the interface was one part of it; here I was getting into static libraries, C++ headers, Objective-C++, and compiler settings because the rides had to be readable. I was willing to follow the problem into all of that. The product needed a working app, and I had taken responsibility for making it work.
Ride data is where the work got much bigger than the screen made it look. FIT is a binary protocol, and the SDK shipped examples and static libraries in C, C++, Objective-C, Objective-C++, C# and Java. My app was in Swift, which was on none of those lists. I started writing my own parser, because I wanted to understand the data by hand and because I did not yet know what I was signing up for. Then I looked properly at the maintenance ahead of me and stopped. Instead I built a small bridge so Swift could call the SDK’s C++ directly. Writing the parser would have taught me more. Not writing it is why the app shipped.
The FitFileMapMaker repository preserves that struggle in the details. Header search paths had to point at the SDK. A static library had to be linked. Objective-C++ wrapper files had to sit between the SDK and the Swift code. The generated interface and bridging-header settings had to be understood well enough for Xcode to stop treating the pieces as strangers. This was self-teaching through the build system, one error and one successful build at a time.
What I got out of it was the whole path in my head at once: bytes on the instrument, through the bridge, into a model, out through the summary and detail views, off to a share or an upload. An outside firm would have delivered the same feature and taken that understanding home with them. I kept it, and every bug for the next two years was cheaper because of it.
That is the honest ledger of this entire project, written down while it was still an open question. I always think that. Building it myself was the right call for the app — it shipped, it worked, and the knowledge stayed in the company. It was also how a person ends up doing the work of several, and there is no version of the story where only the first half is true.
The earliest material sits before the main repository begins. In the OMATA app discussions, I was trying to describe an iOS application before the production instrument was available to describe itself. How would an iPhone find an OMATA? How would the two devices establish a conversation? Which files would live on the instrument? What would calibration mean inside an actual device protocol? How could I test an app when the hardware was still changing underneath it?
One answer was a Nordic development kit pressed into service as a fake OMATA. I modified the Nordic SDK’s Bluetooth UART service until the board behaved enough like the real instrument for the iOS side to hold a conversation with it. The project kept asking for workarounds like this. The hardware was unfinished, so I built a smaller piece of hardware that was finished enough to lie convincingly. And while doing it I was learning iOS Bluetooth, device protocol behaviour, and the uneasy relationship between the two — from a device I had built to stand in for a device that did not exist yet.
The early device model already contained the outline of the app that would follow: a device file, settings, totals, and timestamped activity files. Calibration appeared as a start-and-stop operation in which someone positioned the hands and the device stored the result. The app would have to translate those device-level operations into instructions a rider could follow without knowing anything about the protocol underneath.
The repository opens on March 19, 2017 with a commit called “Start,” which is as much thought as I had given it. The first weeks are a record of trying things on and discarding them — an initial structure, a model-view-viewmodel arrangement, navigation, an overview screen, a bottom nav, and a Bluetooth model that I hoped would become the single place the device got talked to. Some of that survived the year. Most of it did not.
Everything was bending around one stubborn path: information had to get from a physical instrument to a ride summary that made sense on a phone. April brought in FIT-file handling and the SDK. That first FIT work ran on test data and a set of assumptions I already knew were wrong, because the device data was still incomplete and waiting was not an option. Then the code started returning actual summaries — distance, duration, ascent, speed, timestamps, battery, activity records — and for the first time the thing on the screen had come off the road.
A FIT file is a structured record. A ride is a morning, a climb that hurt, and a line on a map. Those are not the same object, and the app had to hold both at once — enough fidelity to preserve exactly what the instrument recorded, enough interpretation that a person could recognise their own afternoon in it.
The interface started borrowing from the instrument around the same time. Circular scales, hands, red reference marks, big condensed figures, dark fields, acid-green state changes. The point was that the app should feel like it belonged to the object in your hand rather than to your phone. A generic fitness dashboard would have been faster to assemble, and it would have quietly undone the entire argument the product was making.
I also wanted a way to see the work accumulate without relying on my memory of it. The graph below counts nonblank lines in every tracked Swift file at selected commits from the first repository start through the December 29 submission preparation. It includes the app’s source, playground, and supporting code. The count is a rough record of the project’s expanding surface as device behavior, data handling, calibration, sharing, and release operations enter the picture.
| Commit date | Commit | Nonblank Swift lines |
|---|---|---|
| 2017-03-19 | cadbe25, Start | 0 |
| 2017-03-31 | 18977b4 | 2,972 |
| 2017-04-28 | e0c3036 | 5,988 |
| 2017-05-20 | 2b7274b | 7,596 |
| 2017-06-30 | d93d142 | 10,906 |
| 2017-07-30 | ddc3615 | 12,190 |
| 2017-08-12 | ac13071 | 12,232 |
| 2017-10-01 | 7efe8da | 12,232 |
| 2017-10-31 | 2a336a7 | 12,232 |
| 2017-11-19 | 3b6b767 | 12,541 |
| 2017-12-29 | 2cb790a, submitted to iTunes Connect | 16,335 |
May is where the sample data runs out and real OMATA files arrive, which is always the moment a design finds out whether it was right. The path from a FIT file into a view got refactored. FIT structures moved into their own files. Units became part of the model instead of something remembered at the point of display. And activity decoding became a performance concern, which is the polite way of saying the ride list had started making the phone wait.
A full activity file held far more information than a ride list needed. Decoding every large file each time the view loaded made the delay visible. I needed a lighter representation for the list and a fuller representation for the ride detail, with the app making that distinction at the data-model level.
The fix was summary FIT files. Activity files stayed rich; summaries carried only the top-line numbers a list actually shows. Conceptually it is a small idea, and it is the kind of small idea a product lives or dies on. It let a ride list behave like a list, rather than re-reading somebody’s entire afternoon every time they scrolled past it.
The pace of the repository changes in June. The desktop utility’s FIT-related work was folded into the iOS project. A rough utility view appeared. The app scanned for OMATA devices, chose among devices that were available to connect, and began dealing with the fact that Bluetooth discovery happens on its own schedule.
The code had to answer a basic question: what does it mean for an OMATA to exist inside an app? A serial number became the key to a local device directory. Device data could be derived from files on disk. Settings, totals, summaries, and activities could be modeled as related records instead of loose values passed between screens. The application began to mirror the device’s own organization while adding the structure a rider needed on a phone.
Then I tried to actually transfer a ride, and the floor gave way. Activity files took minutes. A download could start by accident. A transfer could stop dead because a completion callback never arrived. The bytes received could exceed the file size the device had promised. And the hardest one to see: a connected device and a finished operation are two entirely different states, and I had been treating them as one. The app had to show progress, protect the transaction, and — mostly for my sake — leave enough of a trail to explain itself when it stalled.
The repository records these problems in unusually direct language. A double-tap became the gesture required to download an activity. A cache appeared after full FIT decoding proved too slow. A summary FIT file was generated from a larger activity file. Totals reloaded when new activity data arrived. File URLs were retained so an activity could be shared as the actual file, which sounds obvious after the fact and took real work to make reliable.
The app was becoming a product system in the same way the instrument was becoming a product. Internal architecture, Bluetooth timing, local storage, screen design, and the rider’s patience were all part of the same thing.
That entry is from December, six months after the transfer work started, which tells you how long this particular thing took to corner. Doesn’t crash, just never returns. A crash would have been a kindness — it leaves a report, it points at a line. This just hung, silently, sometimes, on somebody else’s ride. Those are the bugs that cost the most and show up nowhere in a feature list.
Calibration is where the app touched the object most literally. Somebody was moving physical hands on a physical instrument and asking the software to remember where they ended up. The screen was issuing instructions to a body, the Bluetooth layer was issuing instructions to a device, and the two had to come away agreeing about what had just happened. Nothing else in the project asked for that.
The repository’s June 28 calibration commit adds a substantial calibration view controller. Around it are the less glamorous pieces that make a calibration usable: transaction state, completion handlers, delays between Bluetooth operations, and safeguards against entering calibration while another operation is still in progress. I had to keep the device, the app, and the user’s hands in sync.
The calibration artwork is what made that followable — simple diagrams of the hands, regions highlighted, one state per picture, for speed and distance and time and ascent. It reads as illustration. It was engineering. Those drawings are the protocol made visible, and without them the instruction would have been a paragraph nobody finishes.
The value of a repository is that it keeps the problems in the shape they were in before anyone tidied them into a success story. A commit message is often a small confession. The code shows the exact moment a simple feature quietly acquired state, timing, storage, error handling, and a second attempt at the thing that had not worked the first time.
Bluetooth was asynchronous, stateful, and easy to enter twice. The app could attempt a quick connection and a normal connection in overlapping ways. The Bluetooth model needed completion handlers, transaction guards, and clearer meanings for connecting, connected, syncing, and finished.
FIT decoding could be slow enough to make the app feel broken. I responded with a lighter summary model, caching, and more careful control over when full activity data was necessary. The iOS filesystem had its own ideas about where downloaded files belonged. Standard device files had to be written to the correct application directory, and activity data had to remain associated with the device that produced it.
Calibration could crash on a device’s first calibration because my count assumptions were wrong. The first pairing could freeze. Uploads could appear without the expected device name. These were failures in a product that would eventually be used by people with a real instrument in their hands.
None of that had a clever solution. Isolate the state. Make the data path visible. Add a small structure you can test. Instrument the behaviour. Rebuild. Try again. I learned iOS almost entirely through its refusals, which is slow and humiliating and turns out to be the version that sticks.
Read the last four sentences of that entry as a list and it is almost funny. That was an 18 hour day. Xcode crash. Had to reinstall. Refactoring Bluetooth. Nervous about the App Store. Any one of those is a bad week. An Xcode crash erases the fragile continuity of a long debugging session — you do not lose the code, you lose the state you had built up in your head. Refactoring Bluetooth meant opening the core of the app. And the Store was one more unfamiliar system with its own files, warnings, certificates and rituals, waiting at the end of it. That is the part I remember.
The custom tab bar was a mistake. I knew it while I was building it, which is the worst time to know. It bought a small amount of visual consistency with the instrument and cost days, and days were the one thing the company did not have. Such a use of time that no one will appreciate — that is true of most of what makes a product feel finished, and it is why this kind of work is so hard to defend in a status update.
The photographs here run from 2016 through 2018: the kitchen table, café tables, the studio, a flight, a video call. Phones and laptops appear among breakfast plates, paper notes, cables and empty bottles. In my account of building the app, I wrote: “I found myself coding every evening, most weekends, squeezing in a ride to clear my head or let a problem sit and marinate until a solution came to mind.”
The app’s responsibilities expanded again in July. Strava connection material and assets appeared on July 5. The repository added OAuth mechanics, authentication, FIT uploads, ride naming, sharing, and analytics around those uploads.
July is where the ride finally left the instrument. The OMATA recorded it as a physical object on a physical device; the app’s job was to get it into the rest of a rider’s life, which for most of them meant Strava and nowhere else. Making that path dependable meant authentication, file selection, upload progress, error handling, and a durable answer to the question of which device belongs to which account — a question nobody asks until it gets the wrong answer.
At the same time, I was making the app less fragile. Activity lists stopped processing every full activity file just to show a count. A first-device calibration crash was fixed. Bluetooth responses became more structured. A paired OMATA became a prerequisite for Strava connection. Analytics recorded calibration, overview, ride, upload, and device behavior.
The Strava button took an afternoon to draw. What sat behind it was OAuth, FIT-file integrity, device identity, connection state, error handling, and a flow a person could get through without being told how. Every product has a few of these — one tap on the surface, six weeks underneath — and they are invariably the ones described in a roadmap as “Strava integration.”
By October the app was off my machine and onto other people’s phones, which changes what a bug is. In isolation, a defect is something that fails. In the field it is something a person has to be walked through — usually by me, usually in person, usually on a weekend.
The admission is buried in the middle of that entry: that I didn’t tell her directly about. The double-tap was not a support problem. It was an interaction I had built and then had to explain in person, which is a reasonable working definition of an interaction that does not work. It never appeared on any feature list, and it was the whole difference between a ride you could see and a ride you could actually get off the device.
“Ugh” is carrying a lot here, huh? Getting the app from my machine onto somebody else’s phone was not a step at the end of the work. It was a whole category of work I guess one would assume would be pretty straightforward. One might reasonably expect it to just install.
By November the earlier experiments had cohered into something solid enough to push against, which is a milestone that never appears on a plan. I came back to the app, built a smaller proving ground for the algorithms, and spent the month closing the distance between what the device did and what a person saw.
Didn’t look at the App at all. That is the whole condition of the year in six words. The app almost never got a whole day — marketing, support and the back office took the front of it, and I came back to code at night with the same unfinished problem exactly where I had left it. The last line of that entry is the one that stings: That’s my fault, I suppose? It wasn’t, and I spent a lot of evenings deciding it was.
Building a second, uglier tool in order to make progress on the first one looks like a detour and is usually the shortcut. The desktop version gave the algorithms somewhere to be wrong cheaply, away from Bluetooth timing and a phone.
Note the word rituals. Pairing is not a feature, it is a small ceremony two devices perform, and every manufacturer performs it slightly differently. “Sensor support” is one line on a spec sheet and a drawer of other people’s hardware on your desk, tested one make and model at a time.
With those, we have a viable app. That sentence is the turn. Viable meant the obligations were met — move the data, don’t lose it, let the rider find it again — and everything else was, in my word at the time, candy. Notice what gets classified as candy under pressure: the elevation profiles, and the first screen I had spent days on and now suspected was baroque. A table of numbers would have been less of a headache. I did not build the table of numbers.
The finished interface carried the character of OMATA through the application. The overview used large numerical readouts, circular marks, hands, and red accents. The ride list used a dense, monospaced register of duration, ascent, date, and ride number. The settings screen exposed the device name, serial number, hardware, firmware, battery, storage, pairing, calibration, sensors, and Strava.
None of that was decoration. The dark field made the data feel continuous with the instrument rather than borrowed from the phone. The circular graphics tied the screen back to the analog face in your hand. And the settings screen told the truth about what OMATA actually was — a real device with firmware, storage, a battery and maintenance needs, none of which a rectangle of fitness charts would have admitted. Giving the chores some character is what kept them feeling like part of the same object.
The image record should carry the same range. The project was Xcode windows, book pages, interface studies, development-kit material, Slack excerpts, hardware, calibration diagrams, simulator screens, and final screens. The atmosphere belongs beside the instrumental details because that is what building the app actually looked like.
The development views show two parts of that implementation. In Reveal I could inspect the hierarchy and constraints behind the overview screen, down to an individual tab-bar image. In Xcode I was working through Bluetooth scanning and connection callbacks while the simulator displayed the instrument view. Drawing the interface and keeping it supplied with device data both stayed part of my day-to-day work.
Pascal Wever led the UI and UX design. These February 2017 studies show how much there was to try: coral, blue, yellow, black and white ride lists; different weights of numbers; photographic, topographic and geometric welcome screens. The complete set is here, including the variations.
The source repository preserves the app as a sequence of screens rather than a single polished endpoint. I like that distinction. It shows the utility carrying entry, connection, controls, ride records, device maintenance, and the visual relationship between a phone and an instrument.
A commit in late June reads “Commit before TestFlight today,” and anyone who has shipped software knows the exact feeling in that sentence. The app already did device discovery, local data, transfers, calibration and summaries. Now it had to become a different kind of object — one that could be installed, tested, signed, described and submitted by somebody who was not me.
Release turned out to be its own discipline, learned from zero in December. Fastlane, certificates, entitlements, App Store metadata, icons, screenshots — including the discovery that a nice-looking simulator capture is not what Apple means by a screenshot file. The repository holds the 1024-pixel icon, the TestFlight packaging, the release configuration, the screenshot prep. Apple approved it. A commit on December 29 reads, “This is what we pushed to iTunes Connect,” which is the least triumphant sentence imaginable for the end of a year like that.
I do not remember the exact public App Store launch date or the initial version, and the surviving record does not need me to invent either one. What it does preserve is the delivery sequence: the first unfamiliar experiments, the release candidate, TestFlight, Apple review, and the submission system that put the app into the world.
The diagnostic app was a pressure-release valve and a laboratory. It gave me a place to exercise device commands, isolate transfer behavior, and test the API without pretending that the polished app was the only place where the system could be questioned.
The approval arrived inside the same paragraph as a workaround and another government form. That is a more accurate picture of the launch than a clean finish line: one system had accepted the app while several others still needed attention.
Approval did not end the work. I was still bringing the diagnostic app and the regular Utility App into a shared Xcode project, learning the rules of frameworks and bridging headers by running into each one.
Approval is not an ending, which is something you only learn by shipping. The diary keeps going because the product kept going. Devices came back carrying new firmware. Customers found paths through the app I had never imagined, let alone tested. And the software had to absorb the consequences of a physical system being used a long way from my desk.
Dinner is a tin of emergency sardines and the technical note directly after it is precise. That is the texture of the whole period, and it is why I keep the diary rather than a case study — no summary of this project would ever include the sardines, and the sardines are half of what it was. The bug itself was a good one: Bluetooth had become a spatial problem. Two OMATAs in the same room, and the app had to work out which one you meant. Identity and scanning stopped being plumbing and became a feature, the way things do when a product meets more than one of itself.
Renaming a ride. It sounds like an afternoon. But a FIT file has nowhere to put a name — the format simply does not carry one — so a name has to live outside the file while somehow staying attached to it. Hence sidecar files, and hence a data model where the activity, the summary and the sidecar all have to agree about what a ride is called. Some parts that were never designed out is doing a lot of quiet work in that entry.
Largely my fault as I had forgotten that they are not rolled together — and then, immediately, of course they are not. Two firmwares, BLE and MCU, shipping in one file and updating separately. I had designed the update flow for one of them. By this point the app was part of a living hardware system where firmware versions could drift apart on somebody else’s bench, and Pepijn was the reason that got caught rather than discovered by a customer.
Read what that entry moves through without changing gear. A difficult call. A house. A suspicion about how bad things really are. Then an off-by-one in the firmware — the device reports zero items when it holds one, so the list comes back empty — found by getting forensic about it. Then whether we can pay our suppliers this month. The engineering does not degrade as the company does. If anything it sharpens, because it is the one part of the situation that will answer you honestly. That is worth saying plainly to anyone considering this kind of work: the code was never the hard part.
The utility kept accumulating the practical features that make a product usable over time: aiding data, automatic upload, device updates, and the explanations needed when the instrument was not connected in the expected way.
That is the atmosphere the Git history cannot supply: travel, fatigue, another person asleep nearby, and the app still open because the problem was still there.
I made this backoffice utilty sorta app to read the bar code off the box to help manage the ingest of products from the factory. This would’ve come in handy on the other side of the logistics chain — at the factory — where under better circumstancdes having someone QA every single built product before acceptance would have helped things. In the end, the factory did all kindsa things like ship broken device, ship devices that weren’t calibrated, ship devices without firmware, ship boxes without mounts and accessories. Etcetera. They typically denied responsibility which I think is legally fair given the rules of commerce but made for some festering resentment.
Alongside the retail app, I built smaller applications that let me concentrate on one part of the problem: talking to the device, drawing a ride, or getting a FIT file onto a map. Some became tools we used to prepare and service Ones. Others stayed experiments. I was learning the APIs while trying to keep a company moving, and these smaller projects gave me somewhere to try things without bringing the whole Utility App into every test.
The desktop BLE Tool had given me a way to exercise the device API while Harri was building it at Haltian. RX brought that approach onto iOS. I wanted the portability of a phone, and I wanted to know that Bluetooth communication held up on different hardware. I could ask the One for its activities, battery information or crash data, send it firmware, and move the hands through calibration.
I kept the interface deliberately direct. Buttons for operations. A list of activities. Very little between me and the device. The retail app needed to guide a rider through those operations; RX was for a small group of people who needed to get at them individually. I also imagined giving it to bike shops carrying the One, as a diagnostic tool for a customer’s instrument.
There is a little episode in the book that explains how closely this software was tied to shipping hardware. In mid-February 2019, Cary was updating firmware and calibrating hands before sending Ones out. The RX TestFlight build had reached its 90-day expiry. Apple wanted a video showing the app in use before releasing the next build. So, in the middle of breakfast, I made the video and sent it off. They released the build about an hour later, and Cary could carry on.
Maintaining RX meant keeping those service operations available to the people preparing the instruments. I was dealing with calibration code, firmware versions and Apple’s review requirements while somebody else was waiting to finish a shipment.
PX was short for Postcard. I wanted a small image that represented a ride: distance, ascent and elapsed time, with a rendering of the One whose hands showed their positions at the end of that ride. Something recognizably OMATA that a rider could share.
I wrote the experiment in the lounge at Copenhagen airport during an eight-hour delay on the way home from Oulu. Those trips involved a full day at Haltian, the evening flight to Helsinki, a night in the small cabin-like airport hotel, and then Copenhagen the next morning. I had passed through CPH several times before discovering that I had lounge access beyond passport control. On this occasion I had effectively a working day to spend there.
Moving the graphics around and drawing them into an image context were things I hadn’t done before. The postcard made me work through them. I was placing the instrument, its hands, the numerical ride data and the OMATA lettering into one image, then looking at it at the size somebody might actually share.
I was already doubtful about the photorealistic instrument. At that size the hands were hard to read. Perhaps seeing the device was enough; perhaps a smaller overlay on a proper photograph would work better. I put the experiment in front of the team, got no feedback, and went back to other work. Those questions about scale and legibility were still open.
MP was a separate experiment in getting a FIT file through the APIs and onto a Google Map. It opened a static FIT file and let me switch between visual styles. I wanted to see how maps might fit into the Utility App, and to try the design with actual route data on screen.
The controls included Demure Camo, Tactical, Aqua Light and Dark. The outputs show how much could change around a route: the treatment of water, the detail in the terrain, the size of place names, the contrast between streets and the line marking the ride. I wasn’t especially satisfied with the styles at the time. Having them running in an app meant I could keep comparing them with the route in place.
The annotated image brings several pieces of the work together: decoded ride data, geographic coordinates, map rendering, typography and the instrument drawing. I could work on how the ride looked because I had already spent the time getting at the information inside its FIT file. MP remained an internal exploration of possible additions to the Utility App.
Android was a stretch I couldn’t take on myself. Most of our users seemed to be on iOS, and I was already trying to keep up with that app and the rest of the company. An initial offer of help didn’t develop into a codebase. That was disappointing; I had started to think we might have a way to get it done.
Then Michael Henderson got in touch. He had a day a week free and wanted an interesting project for his portfolio. He offered to build the Android app in exchange for an OMATA One, which he would need for testing anyway. I was enormously grateful. He got it to a working state, leaving me to figure out publication.
The Android work belongs in this account with Michael’s contribution clearly attached to it. By then, the product needed an app on another platform as well as the iOS app, firmware and service tools we already maintained. The book records a working Android build and an unfinished publication step. It also records how much I depended on people who liked the One enough to offer their time.
Tyson Soelberg and Pepijn Van Eeckhoudt gave their time to OMATA because they wanted the product and the brand to succeed, which is the only reason anyone volunteers for something this hard. Reading back through the diary it is obvious how much of this I experienced as being alone with it. That is what the entries record, and it is not the whole truth. Pepijn caught the firmware-sync problem before a customer did. Tyson was there for the parts nobody logs. Their names belong in this account for the same reason the sardines do: leave them out and the record is tidier and less true.
Tyson Soelberg
Active collaborator on the OMATA effort. Volunteered time to the work, wanting the product and the brand to succeed.
Pepijn Van Eeckhoudt
Active collaborator on the OMATA effort, including firmware and device R&D alongside the app. Volunteered time on the same terms.
Three views of the utility app: device information and configuration, the live ride dashboard, and accumulated rides.
The instrument, the phone, and the equipment around them: hardware and software sharing the same work surface.
2017-09-19 — CrossStudio, firmware source, disassembly, and a device programming trace in the same frame.
2017-09-19 — A Swift playground used to turn ride data into a plotted shape.
2017-09-19 — The early app UI system, spread across the flows the instrument would require.
Page 283 of 260 Weeks of a Hardware Startup, Volume 1 — Xcode project settings above an early OMATA Utility App interface.
Printed page 263 of 260 Weeks of a Hardware Startup, Volume 1 — the server-side file-upload service and its build output.
Printed page 267 — an early Utility App screen organized around the instrument’s operational controls.
Printed pages 268–269 — the BLE Tool taking shape as a desktop instrument for device files, calibration, and firmware. BLE Tool — a desktop app that I made before I even went into the iOS App project. It was a way to figure out how to deal with establishing state between a bluetooth device and an app, something I had never done before. This was also where I could exercise the firmware's API and see what was missing and what I would need in that API. Eventually this could do calibration, doiwnload rides (activity files), and other of the basic stuff I would need to eventually have the iOS app support. It was an essential step in understanding the interaction between the hardware and software layers of the OMATA One system.
Printed pages 270–271 — Xcode and the Rx calibration work behind the hands-on device controls.
Printed pages 272–273 — the Utility App as a connected system of dashboard, ride history, and device settings.
Printed pages 274–275 — the Postcard experiment, turning a ride record into a shareable image.
Printed pages 276–277 — route data, map styling, and another attempt to give the ride a visual form.
Printed pages 278–279 — the Android App project in Android Studio, including its Kotlin structure and successful build.
Printed page 281 — the Direction 1 UI board, preserved as a complete Illustrator-derived design spread.
Printed page 246 — The agency’s Android Utility App estimate: $94,050 after discount.
Printed page 247 — The agency’s iOS Utility App estimate: $173,250 after discount.
Printed page 248 — a working desk with Xcode, shipping boxes, cables, and the app in progress.
Printed page 249 — Xcode, the OMATA codebase, and the book documenting the work.
Printed page 250 — the internal BLE Tool used to exercise the device API.
Printed page 251 — the download-and-upload workflow from the internal BLE Tool to Strava.
Printed pages 252–253 — the early B1 App UX flows, preserved as a complete board.
Printed page 254 — the gauge geometry taking shape in an Xcode playground.
Printed page 255 — the overview view model under active revision.
Printed page 256 — configuring the FIT SDK include path inside Xcode.
Printed page 256 — linking the FIT SDK library and wrapper files.
Printed page 256 — the Swift/Objective-C interface reference behind the bridge.
Printed page 256 — the bridging-header settings that made the FIT SDK callable from Swift.
Printed page 259 — R2 the Kat getting in the middle of things.
2017-09-23 — GNSS assistance research, browser documentation, and request testing in Paw.
2017-12-12 — Serial output, Stack Overflow, and engineering discussion sharing one working day.
2018-01-02 — iOS App 1.0 waiting in iTunes Connect for release.
2018-01-02 — A large firmware transfer, counted in frames and bytes.
2018-01-16 — The BLE log exposing battery state and the firmware-transfer state machine.
2018-01-24 — The desktop as project index: app assets, simulator captures, documents, and the system asking for a restart.
2018-01-29 — Trying baud rates until the serial connection finally answered.
2018-02-05 — Console noise from the surrounding system while the data pipeline was being chased.
2018-02-05 — The files produced by a ride: activities, summaries, totals, settings, and the system log.
2018-02-05 — Looking inside the activity store and the filesystem events around it.
2018-02-13 — A FIT-file transfer stopping with Finder error code -36.
2018-02-13 — Reading an activity file over BLE, byte by byte.
2018-02-14 — Strava rejecting the redirect URI during integration work.
2018-03-16 — The utility app inside the larger product story: rigorous design, exposed electronics, and a public campaign.
2019-02-11 — The companion desktop utility with calibration, device records, activity files, and diagnostic output.
R2 the Kat getting in between things, February 2017.
Early Bluetooth implementation notes beside the Swift code.
Serial output tracking BLE messages and state changes.
FIT decoding, source code, terminal output and messages sharing the desktop.
A FIT decoder stopping on an invalid value, June 2017.
TestFlight instructions for exercising the OMATA beta.
Battery-discharge plots with estimated runtimes, July 2017.
An OMATA instrument drawing on the laptop lid.
A video call from the studio, surrounded by books and pinned notes.
The PenguinCam camera example running in Xcode, December 2017.
GPS aiding-data code, with the debug message: Sent all the chunks. Now what?
Decoding firmware versions and FIT protocol fields from bytes.
At some point I made an OMATA screen saver.
Another interruption: the monitor reports Input Signal Not Found.
Two OMATA instruments, a keyboard, a spoon and handwritten notes.
Interface exploration 01 — Coral month-by-month ride history.
Interface exploration 02 — Coral ride list with large distance figures.
Interface exploration 03 — Dark ride list with multicolored numbers.
Interface exploration 04 — White ride list emphasizing distance and elevation.
Interface exploration 05 — A quieter gray ride list with named routes.
Interface exploration 06 — Compact black-and-white ride list and toolbar.
Interface exploration 07 — Blue ride-history treatment.
Interface exploration 08 — Yellow ride list with a small bar chart.
Interface exploration 09 — Black ride list with white distance figures.
Interface exploration 10 — Red welcome screen and OMATA ring.
Interface exploration 11 — Gray welcome screen built around concentric circles.
Interface exploration 12 — White welcome screen with ring graphics.
Interface exploration 13 — Welcome screen with an elevation-profile illustration.
Interface exploration 14 — Photographic mountain-road welcome screen.
Interface exploration 15 — Welcome screen combining a route and ride statistics.
Interface exploration 16 — Green photographic welcome screen.
Interface exploration 17 — Red topographic welcome screen.
Interface exploration 18 — Welcome screen with faceted terrain.
macOS completely crashes. I had to reinstall while sitting in a hotel room in Oulu Finland. Unpleasant.
Julian at the kitchen table with a phone, laptop, papers and breakfast.
Source code and a terminal window on a laptop at a café table.
Xcode, a phone, an OMATA One and coffee sharing a table.
An airplane window above snow-covered terrain, December 2016.
A phone reading the QR code on an OMATA box.
At some point I made this screen saver.
The app in the simulator amid laptops, cables and empty bottles.
A laptop running Xcode inside the refrigerator, with an OMATA One held in front.
The desk I stood at for many a year.
A two-person video call captured during the app-development period.
OMATA RX service app with direct controls for firmware updates, hand calibration, crash data, GPS aiding data, battery information and device reset, above a list of activities.
OMATA PX postcard viewed in the iPhone Photos app: a rendered OMATA One dial beside 51 miles, 923 feet of ascent and 2 hours 18 minutes.
OMATA route-map composition with a white route through Will Rogers State Historic Park, a rendered instrument, and ride annotations of 29 miles, 2563 feet and 2 hours 43 minutes.
OMATA map-style study with an orange-red route, green parkland, dark streets and blue ocean near Santa Monica.
OMATA map-style study with an orange-red route, black ocean, shaded green terrain and large labels for Santa Monica and Brentwood.
OMATA map-style study with an orange-red route, pale land beyond green park boundaries, black ocean, and labels for Topanga, Pacific Palisades and Venice.
Reveal inspecting the OMATA B1 overview screen, with the view hierarchy, tab-bar image selection, layout constraints and instrument-inspired ride totals visible.
OMATA Utility App running in the iPhone X simulator beside Xcode Bluetooth scanning and connection callbacks; the console reports completed overview animations.
See Also