TC Lab Blog

The Camera Sees — First Photos and Agent Tuning


Not every day is a breakthrough. Some days you fix DNS records and wait for Royal Mail.

The blog

Launched this blog at blog.egc.land — an Astro site deployed to Cloudflare Pages. Browser-default styling, no JavaScript, no CSS framework. Markdown posts rendered at build time. The whole thing is static HTML.

Then immediately did a full content revision pass across all existing posts. Budget figures corrected (was quoting £445, actual spend was £434.42). The literature review section rewritten to honestly describe the methodology — running a structured prompt through Claude, not manually reading 60 papers. The March 5 radiator discovery expanded with the full data: 5-hour sustained event, -0.914 Pearson correlation, 22.9% combined suitability. Navigation links updated for the new March 6 biology decisions post. Small things, but accuracy matters when you’re writing about a science project.

Cloudflare DNS fix

When we moved the egc.land domain from Namecheap to Cloudflare DNS a few days ago, the staging sites for EGC’s WordPress instances lost their DNS records. Three staging sites — staging-qista-ro.egc.land, staging-ro.egc.land, staging-uk.egc.land — all returning NXDOMAIN.

The fix was more complicated than it should have been. Wrangler (Cloudflare’s CLI) has no DNS commands at all. And the OAuth token wrangler uses for authentication has zone (read) scope but can’t access the DNS records API endpoint. Had to create a separate Cloudflare API token with Zone:DNS:Edit permission, then add three A records via curl against the REST API.

The staging sites use Let’s Encrypt SSL with HTTP-01 challenges, so the A records had to be DNS-only (not proxied through Cloudflare). Proxying would intercept the ACME challenge and break cert renewal. All three sites confirmed back up within 30 seconds of adding the records.

Delivery status

The Shelly Plug S — the smart plug that will give Claude control of the room heater — is stuck with Royal Mail after two failed delivery attempts. I ordered a second Shelly from a different seller as backup.

The Camera Module 3 replacement from JAF Enterprises (Amazon) was expected today. Didn’t arrive. The Pi Hut backup order (£24, £11 cheaper than Amazon) is also in transit. At this point I’m betting on The Pi Hut arriving first and working — they hold their own stock, no commingled inventory.

The agent doesn’t need a camera to do its job. It’s been monitoring temperature and humidity autonomously for over 24 hours now. The camera adds visual assessment of cultures, but there are no cultures yet. The camera can wait.

The fridge

Checked the fridge temperature: 12°C. Way too warm. The PGR solutions (plant growth regulators — the hormones that tell cultured cells what to do) need 2-8°C. They’ve been sitting at 12°C since they were rescued from the radiator two days ago.

Turned the fridge dial up to setting 4. My kefir culture also lives in this fridge — at 4°C the kefir goes dormant and stops fermenting. PGRs win. The kefir moves to room temperature fermentation.

This is the kind of triage that defines home lab work. In a university, the PGR fridge and the lunch fridge are different fridges. In a flat, the BAP and the kefir share shelf space, and someone has to decide whose temperature requirements take priority.

Agent status

The autonomous agent on the Pi continues running. 24+ hours of continuous operation, no missed cycles, no crashes. Temperature stable in the 21-24°C range. Humidity still dipping below 40% overnight — the humidifier is on the list but not yet purchased.

The agent logs every 15 minutes. It flags humidity warnings, tracks consecutive excursions, notes recoveries. It’s doing its job. It just doesn’t have anything to control yet.

Where things stand

ItemStatus
BlogLive at blog.egc.land
AgentRunning 24/7, 24+ hours continuous
Shelly Plug SStuck with Royal Mail, backup ordered
Camera Module 3Replacement + Pi Hut backup in transit
PGR storageFridge adjusted from 12°C to target 4°C
DNSAll staging sites restored
EnvironmentTemperature good, humidity needs humidifier

The first performance review

With 27 hours and 96 entries in the agent’s log, it was time for the outer loop to do its job — review the inner loop’s work.

I pulled the full lab-log.md and read every entry. The environment data was solid: 21.5–24.2°C temperature range, 40–44.5% humidity (excluding the known humidity incident). Clear diurnal pattern — overnight low around 21.5°C at 06:00, daytime high around 23.5°C by mid-afternoon. Temperature and humidity inversely correlated, as you’d expect.

But the agent’s behaviour had problems.

The boundary bug. At 06:04, humidity hit exactly 40.0% — the agent flagged WARNING. At 06:08, still 40.0% — it marked OK. Same reading, different status. The agent had no explicit rule for boundary values.

The parrot problem. 80+ entries said nearly the same thing: “All readings within target ranges. Baseline monitoring continues.” Over and over. If every entry says the same thing, the log is worthless — you have to scan all 96 entries to find the one that’s different.

No daily summary. After a full day of monitoring, the agent produced no aggregate report. No min/max/mean, no diurnal characterisation, no assessment. The outer loop had to compute all of it manually.

Three changes to the inner agent’s brain:

  1. Boundary rule — exactly-at-boundary is OK, strictly outside is WARNING. No ambiguity.
  2. Note rotation — during stable periods, cycle between diurnal observations, streak duration, boundary proximity, and correlation notes. No more parroting.
  3. Daily summary — first cycle after midnight generates min/max/mean, warning count, and a one-sentence diurnal characterisation.

The effect was immediate. The first cycle after the update produced: “Evening warming peak passed — temp reversed 0.7°C from 23.3°C high; temp and humidity inversely tracking as expected.” — compared to the old “All readings within target ranges.”

Lesson: An autonomous agent will default to the laziest behaviour that technically satisfies its instructions. If you want meaningful observations, you have to specify what “meaningful” means. Vague instructions like “log everything” produce verbose nothing.

The camera arrives

The Pi Hut delivery came through — a genuine Raspberry Pi Camera Module 3, carrier PCB, ribbon cable, autofocus, the works. Connected to the Pi’s CSI port.

First surprise: on Debian trixie, the camera tools are rpicam-still and rpicam-hello, not libcamera-still and libcamera-hello. The old names don’t exist. Also, vcgencmd get_camera — the traditional way to check if a Pi camera is detected — shows detected=0 even when the camera works perfectly. It’s a legacy command that doesn’t know about libcamera. Use rpicam-hello --list-cameras instead.

Available cameras
-----------------
0 : imx708 [4608x2592 10-bit RGGB] (/base/soc/i2c0mux/i2c@1/imx708@1a)
    Modes: 'SRGGB10_CSI2P' : 1536x864 [120.13 fps]
                             2304x1296 [56.03 fps]
                             4608x2592 [14.35 fps]

12 megapixels. Full resolution at 14 fps. No reason to downscale — storage is cheap, detail matters when you’re inspecting plant tissue.

First test shot: 1.1MB JPEG at 4608×2592. The image was upside down — the camera module is mounted inverted on its current mount. Fixed with --vflip --hflip.

Rather than relying on the Claude agent to take photos (which it might skip or fail), I added the capture command directly to agent-loop.sh. Every 15 minutes, before the agent even starts thinking, the loop script takes a photo and saves it to /tmp/lab-latest.jpg. Reliable, no AI judgement needed.

Added a /photo endpoint to the Flask server so we can view the latest photo remotely via the API.

Two sensors, one question

The camera is currently pointed at the sensor setup. In the frame: the DHT22 module (the white perforated component wired to the ESP32) and a standalone FUMMDUS LCD hygrometer sitting right next to it.

This gives us something useful — two independent temperature and humidity readings to cross-reference.

SourceTempHumidity
DHT22 (API)22.3°C45.8%
FUMMDUS LCD~22.9°C~47%
Delta+0.6°C+1.2%

The FUMMDUS reads slightly higher on both axes. Could be sensor placement (it’s closer to the Pi, which generates heat), calibration differences between the sensors, or the LCD’s viewing angle making it hard to read precisely — the non-active LCD segments show with some opacity from the camera’s position, making digits ambiguous.

The inner agent now checks the photo each cycle and will flag any obvious discrepancy between the two sensors. Over time, this builds a drift profile — if the offset grows or changes direction, something’s wrong with one of the sensors.

The viewing angle problem

After four cycles of cross-referencing, I manually reviewed the photos against the agent’s logs. Temperature readings were perfect — 100% match. Humidity was wrong half the time. The agent was reading “47%” when the LCD actually showed “41%”. The ghost segments on the digit “1” looked like “7” from the camera’s shallow angle.

But even my initial reading was wrong. I thought the FUMMDUS humidity was 40-41%. After physically adjusting the camera to a more head-on angle, the LCD became crystal clear: 22.3°C / 42%. No ghost segments, no ambiguity. The “COM” comfort indicator and smiley face icon were visible for the first time.

The corrected offsets:

SourceTempHumidity
DHT22~22.0°C~44%
FUMMDUS22.3°C42%
Actual offset+0.3°C-2%

Much tighter than the +0.6°C / +1.2% we initially measured. The original “offset” was mostly just bad viewing angle.

Lesson: When cross-referencing instruments, the reference instrument needs to be readable first. An AI reading a blurry LCD and getting “approximately right” answers creates false confidence — the numbers look plausible but are wrong. Fix the physical setup before trusting the data.

Teaching the agent to see

Getting the agent to actually look at the photo was a three-step debugging process.

Step 1: Add it to the log format. The agent was told to cross-reference the sensors, but the log template had no **Photo:** field. The agent followed the template strictly and dropped anything that didn’t fit. Fix: add a mandatory **Photo:** line to the template. The agent immediately started logging file sizes.

Step 2: Let it use the Read tool. The agent’s constraints said “Use only the Bash tool.” Claude Code’s Read tool can natively view images — it’s a multimodal LLM — but the agent was forbidden from using it. It could only run ls -l on the file and report the size. Fix: add “Use the Read tool to view /tmp/lab-latest.jpg” to the constraints. Immediately, the agent started actually looking at the image.

Step 3: Remove contradictions. The decision rules still said “just note if the photo file exists” while the constraints said “use Read to visually inspect.” The agent followed the weaker instruction. Fix: make all three references (decision rules, log format, constraints) say the same thing.

The result — the agent now reads the FUMMDUS LCD from the photo every cycle:

- **Photo:** OK 1054 KB — LCD ~22.8 C / ~40%, delta +0.5 C / -5.3% vs DHT22

Cost of vision: ~$0.04/cycle more ($0.13 vs $0.09), ~$4/day extra. Each photo adds ~1MB of image tokens to the context. Worth it — the agent can now detect things a thermometer can’t: condensation, mould, wilting, colour changes. When cultures start, this is how it monitors plant health.

Lesson: An autonomous agent will only use the capabilities you explicitly permit. Having a tool available doesn’t mean the agent will use it — you have to tell it to, and you have to tell it consistently in every section of its instructions. One “just check if the file exists” overrides three “visually inspect the image” if the agent reads that section first.

Photo archiving

The first version overwrote /tmp/lab-latest.jpg every cycle — 96 photos per day, only the last one survived. That’s 95 lost photos per day, and the whole point of 15-minute imaging is the time-lapse.

Now each cycle saves a timestamped copy to ~/tc-lab/photos/photo_YYYYMMDD_HHMMSS.jpg. The Pi keeps 30 days (~3.2GB). The laptop backup pulls everything via rsync and never deletes — permanent archive. At ~1.1MB per photo, that’s ~106MB/day, ~39GB/year. A rounding error on a laptop drive.

Manual verification: 13 for 13

After the angle fix, I let the agent run for several hours, then pulled all 21 timestamped photos and compared each one against the agent’s logged LCD reading.

Photos 1–4 (old angle): temperature correct, humidity misread as “40%” due to ghost segments. Photos 5–7 (transition): the 1-vs-7 confusion in full effect. Photo 8: the software workaround (“if you see 47, it’s 41”) partially worked. Photo 9 onward (new angle): 13 consecutive photos, all correctly read. Temperature and humidity matched my manual reading every time.

The post-fix agent readings settled into a tight, consistent pattern:

- **Photo:** OK 1035 KB — LCD ~21.7 C / ~41%, delta +0.3 C / -2.6%

Temperature delta: +0.3 to +0.5°C. Humidity delta: -2.5 to -3.0%. Rock solid.

The system now has two independent, cross-referenced temperature and humidity measurements running autonomously every 15 minutes, with timestamped photo evidence for every reading. When cultures start, any claim about environmental conditions can be verified against the photographic record.

Where things stand

ItemStatus
BlogLive at blog.egc.land
AgentRunning 24/7, improved logging after review
CameraConnected, 4608×2592, auto-capture every cycle
Photo API/photo?key=... endpoint live
Shelly Plug SStuck with Royal Mail, backup ordered
Sensor cross-refDHT22 vs FUMMDUS LCD, +0.3°C / -2% offset (corrected)
PGR storageFridge adjusted from 12°C to target 4°C
DNSAll staging sites restored
EnvironmentTemperature good, humidity needs humidifier

The camera changes the game. The agent can now see, not just measure. Once cultures start, every 15-minute photo becomes a time-lapse of growth, contamination detection, and visual health assessment. For now, it watches two thermometers disagree by half a degree.