Writing/Bluetooth
The appliance that was never in the room
I built a coffee machine app without a coffee machine. The constraint turned out to be the most useful decision in the project.
CoffeeBrew pairs with a coffee maker over Bluetooth, reads how much water and beans it has left, hands it your Wi-Fi so it keeps working when your phone is elsewhere, and brews on a schedule. I built all of it without ever having the machine.
That sounds like a limitation and it was the most useful constraint in the project, because it forced a decision on day one that most connected-hardware apps postpone until it is expensive.
The shortcut that ruins the codebase
The tempting move is to scatter a demo flag through the connect flow. It works immediately, it demos well, and it is unpickable afterwards, because the real behaviour and the fake behaviour end up interleaved inside the same functions. When the hardware finally arrives you are not integrating it, you are excavating.
“Scatter demo flags through the connect flow and the real behaviour and the fake behaviour end up interleaved in the same functions.”
A protocol with six methods
So the device is an interface: scan, read sensors, list networks, provision Wi-Fi, send a brew command. The simulated machine is one implementation of it, with delays, a signal strength that varies, four fake networks and sensor values that drift the way real ones do. The connect flow never knows which implementation it is talking to, because it cannot see past the interface.
export interface CoffeeMakerProtocol { getScanFilter(): ScanFilter readSensors(id): Promise<SensorData> readAvailableWifiNetworks(id) provisionWifi(id, credentials) sendBrewCommand(id, command)} // one implementation today, two tomorrowSwapping in a real machine is a file. That is the whole claim, and it is checkable: there is no branch anywhere in the app that asks whether the device is real.
The part that is real regardless
Everything above that interface is built properly, because none of it depends on the hardware existing: the pairing flow with all its failure states, the backend, the checkout for beans, and the scheduled brews.
That last one is worth its own note. A scheduled brew is worthless if the reminder needs the app to be open, and a client-side timer is exactly that, because it dies with the process and the operating system will kill the process. So the schedule lives in the database and a cron job inside the database sends the notification. The phone is a client of the schedule and never its owner, which is true whether or not there is a machine at the other end.
