Reviving the Terrashroom TS-01 after the cloud died
In June 2025 Terrashroom shut down the cloud its TS-01 mushroom chambers depended on. An estimated 1,040 units lost their app, their presets and their camera overnight. This is how one of them got local, better-than-stock firmware in a week, with a multimeter, a $10 USB-serial adapter, 13 years of tinkering with microcontrollers and IoT devices, and a lot of Claude Code sessions running in parallel.
The short version
- 3 to 9 October 2026: from the first boot log to firmware release 1.1.0.
- About 240 commits and more than 100 merged pull requests, nearly all of them written by Claude Code sessions working side by side, each on its own branch.
- What exists now: AutoMyco, an ESPHome firmware that runs the whole grow on the chamber, an iPhone app, a website with installers, a no-wire Bluetooth install, optional cloud history and push alerts, and staged fleet updates.
- What it cost: one ESP32 module and one USB-serial adapter, killed by pulling a ground wire first.
The device, and why it died
The TS-01 is a countertop grow chamber. It has a water reservoir and an ultrasonic mister, two fans, a 5 V blower, a 24 V heater, a 24-LED ring light, a 64-LED base strip, a UV-C lamp for sterilising the empty chamber, and a camera for timelapses. Inside are two ESP32s: the main board running the vendor's Arduino and Blynk firmware, and a sealed camera board.
The stock firmware ran no climate control without the vendor's app. When the cloud went, the chamber became a box of fans with a light. There was no open teardown we could find. A closed-source community firmware, Hackshroom, existed, and later we found lbaker2/terrashroom-resurrection, whose GPIO map matched ours exactly. Both got credit where we used them.
I decided not to buy another cloud gadget. I'd reverse engineer this one, write everything down, and build firmware that answers to nobody's server, for my unit and for everyone else's.
Backup first, and rules the agent can't skip
Before any code, I wrote the hard rules into the project's CLAUDE.md, which every Claude session reads when it starts:
- Never write flash until a full backup has been read twice with matching hashes, and copied.
- Never burn eFuses.
- Gate every write on flash encryption and secure boot being off.
- The UV-C lamp is an eye hazard. It's off by default everywhere, and nothing turns it on without a person asking.
- Flash dumps can hold Wi-Fi passwords. They never get committed.
A sixth rule came later, the hard way: never connect or disconnect a serial adapter while the chamber is powered. Ground goes on first and comes off last.
On day one I buzzed out the header with a meter. The mounting screw was ground, pin 1 was 5 V, and listening on pin 5 gave the full stock boot log at 115200 baud. It named the firmware, the I²C pins and the cloud host, and relayed one line from a second ESP32: {"status":"ready","version":"camera/0.1.7.09"}.
Then came the backup the rules demand: two full 4 MB reads, 205 seconds each, with identical SHA-256, kept in two copies. Claude wrote the tool that does it in one esptool session, because every new esptool process glitched the reset line and dropped the chip out of download mode.
Claude as a team, not a chatbot
Most of the software was written by Claude Code, running as many sessions at once. Each had its own git worktree and branch, did one job, and opened one pull request that was squash-merged. The roles sorted themselves out:
- An orchestrator kept the task order in the bench log, sequenced merges and cut the releases (1.0.0 on 4 October, 1.1.0 on 9 October).
- Bench and setup sessions drove the chamber over its local API during tests, read its current monitor and logged every result with a date.
- A camera session and a UV-C session took the two hardest parts. They shared the one chamber by asking before each update and putting production firmware back afterwards.
- iOS, website, cloud and hardware sessions built the app, the installers, the AWS back end and a drop-in v2 main board in KiCad.
- Review and security sessions read the others' pull requests before merge. Some of the best fixes in the project came from them.
One required CI check holds it together. It validates every firmware config, runs a safety checker (UV-C must boot off, no load may boot on, every update must switch the loads off first), compiles the public firmware against a size gate, and runs the host, site and iOS tests, a license lint and a secrets scan.
Where 13 years at the bench mattered
Claude was fast and thorough at decoding, writing and testing. These are the places where experience changed the outcome:
- Wiring safety. After the dead module (below), the bench got a USB isolator, series resistors on the signal lines and the power-off rule. The new module wasn't powered until its 3.3 V rail read the same as the bare board, and every load cable was checked for shorts before it went back on.
- Knowing what to measure. Regulator in and out, and a hot module, pointed at the ESP32. A few drops of isopropyl showed which part of the can dried first: the die itself. A resistor sweep across the water probe (220 Ω reads wet, 1 MΩ reads dry) found its threshold. A "1 kΩ" strap that wouldn't pull a pin low turned out, by its colour bands, to be 1 MΩ.
- Trusting ears and eyes over numbers. When the self-test said the blower had failed, I'd heard it run. That turned a "broken blower" into a measurement bug. A fan I watched blowing out overruled another firmware's "intake" label.
- Saying no to shortcuts. An open setup hotspot came out of the public build, because anyone in radio range could have claimed a new chamber. The camera gets no more updates until its stock image is backed up. UV-C stays unplugged during serial flashes until its pin is measured.
- Product calls. The no-wire install became the main route, knowingly without a backup, because a backup can't be read over Bluetooth (the wired installer keeps the full backup). "Make sure others can do it with just my app and servers." "Why do I have to run a CLI?" "Build it so we can fix it later," which put jumpers on the v2 board wherever a pinout was uncertain.
The honest lows
- A button press wiped settings before the backup. Holding the rear Bluetooth button to try for download mode triggered the vendor's reset-config instead. Little was lost, since the cloud was dead, but the backup isn't factory-pristine. If you own one: don't long-press that button before you've backed up.
- A 15-minute reboot loop (ESPHome's default when no Home Assistant client is connected), and a grow-light schedule running three hours late because of the wrong time zone.
- We killed the ESP32. About ten minutes after the first install from stock passed on 4 October, the chamber went silent and I heard a buzz. The 3.3 V rail sat at 2.5 V and the module was hot. The probable cause: the USB-serial adapter came off while the chamber was powered, ground first. With ground gone, the signal wires became the only path between the laptop's ground and the chamber's isolated supply, and the current went through both chips' protection diodes. Both died at that moment. The module came off with hot air two days later, and a new one was flashed and back on Wi-Fi on 8 October.
- A false "blower not detected" on the last day, from the new load check (more below).
- Some hardware is still open. The water probe can't see distilled water (it measured 100–150 kΩ, far above the board's threshold). The base light strip has been dark since the first flash. The UV-C output drove nothing measurable on the bench.
- The camera's stock firmware still needs backing up. The over-the-air camera flash left the only stock copy in the other slot, and the next camera update would erase it. So the camera is frozen until that copy is read out over serial.
What we found
- The GPIO map: fans on 32 and 33, blower 25, mister 13, heater 12, UV-C 18, camera power 26, water probe 15 and 23, lights on 5 and 14, and a current monitor whose shunt was 0.01 Ω, not the 0.1 we first assumed.
- The cloud datastreams: a handler table in flash for the vendor app's virtual pins: species, fan and heater duty, lights, and a manual UV switch.
- 13 species presets (temperature, humidity, idle airflow), plus stock bugs: Blue Oyster set no targets at all, and the stock self-test switched the UV-C lamp on and passed a lamp that drew nothing.
- The camera protocol: newline-delimited JSON over a UART. On an update command, the camera asks
api.terrashroom.orgfor a firmware URL. That domain had lapsed. - The camera only trusts Amazon. A self-signed certificate and a real Let's Encrypt certificate were both refused. Certificate logs showed the vendor had always used Amazon-issued certificates, so the re-registered domain got an AWS endpoint with an Amazon certificate. On 9 October the sealed camera flashed ESPHome over the air, with no case opening. Its first frame was a sharp 1600×1200 JPEG, and its Wi-Fi went from 65–90 % packet loss on stock to none.
- The camera server is single-threaded: a second viewer waited 9–10 seconds.
- The flash LED stopped the camera. ESPHome gave the flash LED PWM channel 0, and the camera driver hard-codes channel 0 for the image sensor's clock. Flash on, clock off, picture gone. The fix is one line.
- Quick power cycles brown the camera out, so camera power now stays off for at least 10 seconds, whatever asks for the cycle.
- The stock Bluetooth update has no authentication, only a digest check. That's what lets owners install without a cable.
What we built
Firmware with interlocks
The controller is tuned from the stock values and runs on the chamber with no cloud. It handles pulsed misting, top-ups, an over-humidity purge, staged heating, species airflow, grow sessions and grow phases. Around it sit the interlocks. The heater cuts out on a stale sensor or above 88 °F, and never heats still air. The mister locks out on low water and fails closed at boot. UV-C is off at every boot, refused on low water and cut by a hard timer. Every update switches the heater, mister and UV-C off first.
A load check says when a fan, the blower, the mister or the heater isn't plugged in. Its first version called a running blower missing. The blower runs on 5 V and adds only about 6 mA to the 24 V rail being measured, and the camera booting and a light effect moved the rail by more than that. Now every load is measured between two baselines. On a noisy rail the check says "unsure" instead of "failed", and the blower can be confirmed but never called missing.
An iPhone app
A native app talks to the chamber over its local network. It has a dashboard built around "is my grow OK?", grow sessions, lights, history, the activity log and a camera view. It also updates the firmware: the first real update moved about 1 MB in 10.8 seconds. It's backed by close to 500 tests against a mock chamber, and it's in TestFlight beta.
Install without opening anything
Hold the rear button for about three seconds, and the stock firmware opens its own Bluetooth update window. The app or the website then sends AutoMyco block by block. The chamber checks the whole image before it switches over, and on first boot AutoMyco picks up the Wi-Fi the stock firmware had saved. The first run from stock passed, but 180 blocks failed their checksum and the link dropped once. iOS had been sending "apply this block" ahead of its queued writes. The fix, plus automatic reconnect and resume, went into all three installers.
Website, cloud and fleet
- A website with email-code sign-in, gated downloads, a live check of which install your browser supports, and a wired installer that won't write until it has two matching backups saved to your disk.
- Fleet updates with a staged rollout. 1.1.0 went to 10 % of chambers first, and an update never installs mid-grow without the owner confirming twice.
- Grow recipes as data, so a preset fix doesn't need a reflash.
- Push alerts for an empty tank, heater faults, missing loads, stuck sensors and grow milestones.
- Remote access and cloud history, read-only by design. There is no command path, so the cloud can never switch anything on.
Security and safety lessons
- Interlocks live on the device. The cloud, the app and Home Assistant can all vanish, and the heater still cuts out.
- Treat every web page as hostile. A read-only probe showed the stock ESPHome web server would run actions on a plain GET from any site, so an
<img>tag on any page could have switched UV-C on. A guard now refuses foreign hosts, foreign origins and GET actions. - Trust on first use is a bug. A screenshot of a chamber's ID was once enough to subscribe a phone to its alerts. Chambers now bind themselves at first boot, show only a hashed handle, and pairing needs a code read from the chamber.
- A kill switch mustn't be a denial-of-service button. A cost cut-off tied to a traffic alarm would have let anyone switch remote access off for everyone with about 2,000 junk requests. It's budget-only now.
- Keys belong to a pairing, not a device. A camera key copied before a chamber was sold would have kept working for the new owner. Each pairing now makes a new one.
- A revived domain is a responsibility. It serves firmware only for the owner's unit, keeps nothing from other people's chambers, and never pushes changes nobody asked for.
- Respect the 4 MB slot. The public firmware has to fit the stock firmware's spare update slot, and CI fails anything over it.
By the numbers
| Orphaned units (estimate) | ~1,040 |
|---|---|
| First boot log to release 1.1.0 | 7 days |
| Commits / merged pull requests | ~240 / 100+ |
| Stock backup | 2 × 4 MB reads, identical SHA-256 |
| First firmware update from the app | 1,073,024 bytes in 10.8 s |
| Public firmware size | 1,622,915 of 1,769,472 bytes (91.7 %) |
| iOS tests | ~490 |
| Chips destroyed | 2 |
What's next
- Back up the camera's stock firmware over serial, then unfreeze camera updates.
- Find out what the UV-C output really drives, and fix the water probe and the base strip.
- Bring up the v2 main board.
- One full run from stock on a phone, then tell the other owners.
- Publish the firmware, app and docs as open-source mirrors.
How to use Claude for a hardware revival
- Write the hard rules down first. Backup before write, no eFuses, hazards off by default, no secrets in git. The agent follows them every session, and that's what makes it safe to let it near real hardware.
- Keep the physical and the irreversible human. Needles, power, lamps, domains and the first flash belong to the person at the bench.
- Log everything, dated, in the repo. The bench log became the shared memory between dozens of sessions, and the source for this post.
- One session, one branch, one pull request. With a single required CI check, many sessions can work at once without trampling each other.
- Make safety a test, not a promise. Safety checker, size gate and secrets scan on every pull request.
- Run separate review and security sessions. They found the GET-drives-UV-C hole, the trust-on-first-use identity, the kill-switch DoS and the reusable camera key.
- Believe the bench over the model. When the numbers and the room disagree, measure again.
- Plan for being wrong, both of you. Inferred pins stayed labelled "inferred" until the bench confirmed them. The worst mistake of the week was mine, with a ground wire, and now it's a rule.
Own a TS-01? The firmware, installers and help are at automyco.com. Not affiliated with Terrashroom.