Tinkerings

Building a Vegetable Patch Tracking Service with Johnny-Five

Date Published

Building a Vegetable Patch Tracking Service with Johnny-Five

Situation

A vegetable patch is not a dashboard. It has no API, no logs, and no error messages. The only way to know whether a plant needs water is to walk outside, push a finger into the soil, and make a judgement — which means the plants that get watered are the ones near the door, and the ones that die are the ones you forgot.

I wanted a system that would sense soil moisture continuously, indicate wet or dry state immediately, and treat each plant as its own unit of concern rather than as an anonymous pin on a board.

Task

Build a Node.js service using Johnny-Five — the JavaScript robotics framework for Arduino — that:

Represents multiple plants as first-class entities

Reads soil moisture from analogue sensors per plant

Drives LEDs to indicate wet or dry state

Runs on a physical board, with a clear startup and shutdown lifecycle

Action

1. Defined the plants declaratively in a config array.
PLANTS_CONFIG lists each plant as a data object: a name, a soil sensor pin, and the pins for the wet and dry indicator LEDs. The vegetables are described as data, not baked into logic. Adding a plant means editing the array, not writing new code.

2. Gave each plant its own state object.
Each entry in plantsState groups the plant's name, its Sensor instance, its Led instances, and a combined state holder. A plant is a single thing with several parts, not several loose variables that happen to travel together.

3. Iterated over the config rather than repeating per-plant code.
PLANTS_CONFIG.forEach(plant => addPlantState(plant)). The startup logic is proportional to zero plants, not to four. If the patch doubles in size, the code doesn't change.

4. Abstracted the hardware through Johnny-Five's Board.
The physical Arduino is instantiated as a Board instance, and pins are addressed by name ("a7", "b0", "b1") rather than by byte-level register writes. The hardware is described in the same language as the rest of the program.

5. Used the board.on("ready") lifecycle hook for initialisation.
Hardware cannot be talked to until the board reports ready. Deferring all sensor and LED construction into this callback is the correct pattern — anything before it is undefined behaviour.

6. Used the board.on("exit") lifecycle hook for cleanup.
An embedded process should release its hardware on shutdown. Wrapping the teardown in exit ensures that LEDs, sensors, and the board itself are handled cleanly when the process terminates.

7. Lit a dedicated "system running" LED on pin 13.
A single indicator that the service has booted. This is the embedded equivalent of a health check — a physical signal that the process is alive and past its ready state. The activated() function encapsulates that and logs to console.

8. Handled sensor input as events, not as polling.
plantState.soilSensor.on("change", ...) registers a listener that fires when the analogue reading changes. No setInterval, no tight loop, no busy-waiting. The board reports, the code reacts.

9. Made the wet/dry decision on a calibrated threshold.
soilSensor.value < 350 and > 350 are the two branches. The number is a calibration value for the specific soil sensor in use — capacitive sensors, for example, read lower in wet soil and higher in dry — and the threshold is the boundary between the two states. The magic number should ideally be named as a constant, but the concept is right.

10. Modelled the indicator LEDs as domain states, not as truth values.
wetState and dryState are Led instances that mean "the soil is wet" and "the soil is dry". Not led1 and led2, not greenLed and redLed — the names describe what the hardware means in the domain, not what colour it happens to be.

11. Kept a combined state holder so both LEDs move together.
bothMoistureStates groups the two indicator pins so that a single toggle() call flips the pair. The wet/dry state is treated as one logical unit of display even though it is physically two LEDs.

12. Wired the feedback loop: sensor → logic → actuator.
The soil sensor produces a value; the change handler evaluates it; the LED state is updated in response. This is the canonical embedded control loop, and it is visible in the code.

13. Shared a single Board instance across all plants.
There is one physical Arduino, so there is one Board object. Every plant's Sensor and Led instances are attached to the same board. The code reflects the hardware rather than pretending each plant has its own microcontroller.

14. Encoded the two plant states as wet and dry, not as boolean or numeric.
The domain is binary at the decision level — a plant is either wet enough or it is not — but the code names those states in the language of the vegetable patch. The threshold is a number, but the state is a word.

15. Left room for the config to grow without touching the logic.
Adding "Potatoes 3" or "Tomatoes 1" to PLANTS_CONFIG is the only change required to extend coverage. The startup, the state construction, and the event binding are all driven by the array.

Result

A Node.js service that treats each plant in the vegetable patch as an entity with its own sensor, its own indicator LEDs, and its own state. The physical hardware is described through Johnny-Five's abstractions, and the code is organised around the domain — plants, wetness, dryness — rather than around pin numbers and register writes.

The work is a small piece of embedded automation, but the design decisions it rests on are the same ones that scale: declarative configuration, event-driven input, lifecycle hooks, domain-named state, and a clean separation between the hardware layer and the logic that acts on it. A vegetable patch is not a distributed system. But the habit of writing code that models the problem in the language of the problem is the same habit that does scale, whether the domain is soil moisture or markets.