AgriTwin-GH

Greenhouse Unity Scene

Complete documentation for all greenhouse controllers, central state applier, and backend integration patterns.


Table of Contents

  1. Overview
  2. Unity Source Project — unity_module/
  3. Complete Project Reference
  4. GreenhouseStateApplier — Central Controller
  5. Individual Actuator Controllers
  6. Environment Controllers
  7. JSON Schema & Format
  8. Setup Instructions
  9. Live Testing in Play Mode
  10. Future FastAPI / Python Backend Integration
  11. Architecture & Design Patterns
  12. Scenario Runner — sample_greenhouse_scenarios.py

Overview

This greenhouse Unity scene is composed of:

All scripts are designed for Python backend integration. The central applier reads a JSON file at runtime, detects changes, and applies the full greenhouse state automatically. This allows testing backend-style control patterns locally inside Unity before connecting to FastAPI.



Unity Source Project — unity_module/

The unity_module/ folder at the repository root contains the complete Unity Editor source project for the AgriTwin-GH 3D greenhouse scene. Opening this folder in Unity 2022 LTS or later gives you the full editable scene with all assets, scripts, audio, and skyboxes — no additional downloads required.

Folder Structure

unity_module/
├── Assets/                              # All scene content
│   ├── Scenes/                          # Unity scene files
│   │   ├── Environment.unity            # Main greenhouse scene (open this in Unity)
│   │   └── SampleScene.unity            # Blank reference scene
│   ├── Scripts/                         # All 12 C# controller scripts
│   │   ├── GreenhouseStateApplier.cs    # Central JSON-driven orchestrator
│   │   ├── CropStageController.cs       # Per-plant 6-stage visual swap
│   │   ├── CropHealthIndicator.cs       # RGB status lights + blinking
│   │   ├── TimeOfDayController.cs       # Skybox, fog, directional light
│   │   ├── FluorescentLightController.cs
│   │   ├── HeaterController.cs
│   │   ├── EnergyCanisterController.cs
│   │   ├── HumidifierController.cs
│   │   ├── WindowFanController.cs
│   │   ├── VentController.cs
│   │   ├── WaterTankFloorController.cs
│   │   └── FreeCameraController.cs
│   ├── Environment/                     # Custom greenhouse prefabs and FBX models
│   │   ├── Bld_GreenMouse.prefab        # Main greenhouse building structure
│   │   ├── Crop Slot 1/2/3.prefab       # Crop container slot variants
│   │   ├── Seeding.prefab               # Stage 0 crop model
│   │   ├── Vegetative.fbx               # Stage 1 crop model
│   │   ├── Flowering Initiation.prefab  # Stage 2 crop model
│   │   ├── Flowering.prefab             # Stage 3 crop model
│   │   ├── Unripe.prefab                # Stage 4 crop model
│   │   ├── Ripe.prefab                  # Stage 5 crop model
│   │   ├── Fluorescent Light.fbx        # Actuator model — grow lights
│   │   ├── Heater.fbx                   # Actuator model — heater
│   │   ├── Humidifier (1).fbx           # Actuator model — humidifier
│   │   ├── Window fan.fbx               # Actuator model — window fan
│   │   ├── Vent.fbx                     # Actuator model — vent
│   │   ├── Water Tank Floor.fbx         # Actuator model — water tank
│   │   ├── Energy Canister.fbx          # Actuator model — energy canister
│   │   └── StatusIndicator.prefab       # Reusable RGB indicator light
│   ├── AllSkyFree/                      # Skybox cubemap pack (11 sky environments)
│   │   ├── Cartoon Base BlueSky/        # Day sky — PNG cubemap + material
│   │   ├── Cartoon Base NightSky/       # Night sky
│   │   ├── Cold Sunset/ Cold Night/     # Sunset and cold-night variants
│   │   ├── Deep Dusk/ Epic_BlueSunset/  # Dusk and dramatic sunset
│   │   └── ...                          # 11 total sky environments
│   ├── Pandazole_Ultimate_Pack/         # Greenhouse building 3D asset pack
│   │   └── Pandazole Farm Ranch Pack/   # Models, textures, prefabs, materials
│   ├── PolyOne/Stylized Tomato/         # Stylized tomato 3D asset
│   │   └── Model/ Texture/ Prefabs/     # FBX model + textures + diffuse material
│   ├── SimpleSky/                       # Alternative skybox pack
│   │   └── Materials/ Models/ Textures/ # Sky sphere material and textures
│   ├── Nature Sound FX/                 # WAV audio library for actuator sounds
│   │   ├── Steam/                       # Fan / humidifier steam audio (8 WAVs)
│   │   ├── Water/ Wind/ Rain/           # Ambient and environmental audio
│   │   └── ...                          # 11 sound categories total
│   ├── Imports/                         # Externally imported 3D assets
│   │   └── Tomato (2).glb               # GLTF tomato model (via GLTFUtility)
│   ├── StreamingAssets/
│   │   └── greenhouse_state.json        # Runtime JSON state file read by GreenhouseStateApplier
│   └── Settings/                        # URP and render pipeline asset settings
├── Packages/
│   ├── manifest.json                    # Unity package dependencies
│   └── packages-lock.json              # Locked package versions
├── ProjectSettings/                     # Full Unity project configuration
│   ├── ProjectSettings.asset            # Project name, version, target platform
│   ├── GraphicsSettings.asset           # URP pipeline assignment
│   ├── QualitySettings.asset            # Quality tiers
│   ├── AudioManager.asset               # Global audio settings
│   └── ...                              # Physics, input, navmesh, VFX, XR settings
└── build/                               # WebGL export (Git LFS — Brotli binaries)
    ├── Build/
    │   ├── build.data.br                # Scene & asset data (~18 MB, LFS-tracked)
    │   ├── build.wasm.br                # Unity WASM runtime (LFS-tracked)
    │   ├── build.framework.js.br        # JS framework loader (LFS-tracked)
    │   └── build.loader.js              # Bootstrap entry point
    ├── StreamingAssets/                 # Default state JSON for the deployed scene
    └── index.html                       # WebGL entry point

Unity Package Dependencies

The project uses Universal Render Pipeline (URP) 17.5.0 (Unity 6) with the following key packages defined in Packages/manifest.json:

Package Version Purpose
com.unity.render-pipelines.universal 17.5.0 URP — lighting, shaders, post-processing
com.unity.inputsystem 1.19.0 New Input System for camera controls
com.siccity.gltfutility git Runtime GLTF/GLB model import
com.unity.ai.navigation 2.0.11 NavMesh (reserved for future agents)
com.unity.timeline 1.8.11 Animation timeline support
com.unity.visualscripting 1.9.11 Visual scripting support

Git LFS — What Is Tracked

Large binary files in unity_module/ are stored via Git LFS so the repository stays lean:

Pattern Examples
**/*.fbx All actuator and crop 3D models
**/*.glb Imported GLTF tomato model
**/*.png Skybox cubemaps, all textures
**/*.wav All audio clips in Nature Sound FX
**/*.pdf AllSkyFree documentation PDF
build/Build/*.br Brotli-compressed WebGL bundle

Opening in Unity

  1. Install Unity 2022 LTS or Unity 6 (matching URP 17.x) via Unity Hub
  2. In Unity Hub click Add project from disk → select the unity_module/ folder
  3. Unity will auto-regenerate Library/ on first open (takes 2–5 min)
  4. Open Assets/Scenes/Environment.unity — press Play to run the live scene
  5. The scene reads Assets/StreamingAssets/greenhouse_state.json every second — replace or update this file from Python/FastAPI to drive the 3D scene in real time


Complete Project Reference

All C# Scripts — Quick Reference

Script Purpose Type Key Method(s)
GreenhouseStateApplier.cs Central orchestrator; reads JSON, detects changes, applies state Manager SetState() dispatches to all controllers
FluorescentLightController.cs Controls fluorescent light (status + spots) Actuator SetState(bool)
HeaterController.cs Controls heater (indicator + glow) Actuator SetState(bool)
EnergyCanisterController.cs Controls energy canister (status light) Actuator SetState(bool)
HumidifierController.cs Controls humidifier (light + fog + audio) Actuator SetState(bool)
WindowFanController.cs Controls window fan (light + particles + audio) Actuator SetState(bool)
VentController.cs Controls vent system (light + particles + audio) Actuator SetState(bool)
WaterTankFloorController.cs Controls water tank (light + audio) Actuator SetState(bool)
CropStageController.cs Controls one crop’s visual stage Environment SetStageByName(string)
TimeOfDayController.cs Controls lighting, skybox, fog, night lights Environment SetTimeOfDayByName(string)
CropHealthIndicator.cs Controls RGB health lights + blinking Environment SetGreen(), SetYellow(), SetRed(), SetBlinkGreen(bool)
FreeCameraController.cs Provides free camera controls Utility (no public state methods)

Scene GameObject Hierarchy

Your Unity scene should have this structure:

Scene/
├── GreenhouseManager (Empty GameObject)
│   └── [GreenhouseStateApplier component attached]
│
├── Actuators/ (folder, organizing lights/effects)
│   ├── FluorescentLight
│   │   └── [FluorescentLightController component]
│   ├── Heater
│   │   └── [HeaterController component]
│   ├── EnergyCanister
│   │   └── [EnergyCanisterController component]
│   ├── Humidifier
│   │   └── [HumidifierController component]
│   │       └── StatusIndicator (Light child)
│   │       └── FogEffect (ParticleSystem child)
│   │       └── AudioSource (parent object)
│   ├── WindowFan
│   │   └── [WindowFanController component]
│   │       └── StatusIndicator (Light child)
│   │       └── AirFlow (ParticleSystem child)
│   │       └── AudioSource (parent object)
│   ├── Vent
│   │   └── [VentController component]
│   │       └── StatusIndicator (Light child)
│   │       └── AirFlow (ParticleSystem child)
│   │       └── AudioSource (parent object)
│   └── WaterTank
│       └── [WaterTankFloorController component]
│           └── StatusIndicator (Light child)
│           └── AudioSource (same object)
│
├── Crops/ (folder, organizing 15 crop plants)
│   ├── Crop_01
│   │   └── [CropStageController component]
│   │       ├── Seedling (Model)
│   │       ├── Vegetative (Model)
│   │       ├── FloweringInitiation (Model)
│   │       ├── Flowering (Model)
│   │       ├── Unripe (Model)
│   │       └── Ripe (Model)
│   │   └── CropHealth (ChildObject)
│   │       └── [CropHealthIndicator component]
│   │           ├── GreenLight (Light)
│   │           ├── YellowLight (Light)
│   │           └── RedLight (Light)
│   ├── Crop_02 ... Crop_15
│   │   └── [same structure as Crop_01]
│
├── Environment/ (folder)
│   └── [TimeOfDayController component on main light]
│       └── DirectionalLight (sun/moon)
│       └── NightLights (array of GameObjects for night)
│
├── Camera (MainCamera)
│   └── [FreeCameraController component]
│
└── [Skybox, Fog, Lighting settings]

Growth Stages — Complete Reference

When a crop grows, it progresses through 6 distinct stages. Each stage has a corresponding GameObject model that becomes visible/invisible.

Stage Progression (0 → 5)

Index Name Visual Duration (Typical) Description
0 Seedling Tiny sprout Days 0–7 Initial germination; very small plant
1 Vegetative Young plant Days 7–21 Leaf growth; expanding stem
2 FloweringInitiation Budding Days 21–28 Buds beginning to form
3 Flowering Blooming Days 28–42 Full flowers open; peak beauty
4 Unripe Young fruit Days 42–56 Fruit formed but not mature
5 Ripe Ready to harvest Days 56+ Fully mature; ready for pickup

JSON Stage Values (Case-Insensitive)

"cropStage": { "stage": "Seedling" }
"cropStage": { "stage": "Vegetative" }
"cropStage": { "stage": "FloweringInitiation" }
"cropStage": { "stage": "Flowering" }
"cropStage": { "stage": "Unripe" }
"cropStage": { "stage": "Ripe" }

Variants accepted (all auto-converted):

Key Behaviors by Stage


Actuators — Complete List

All 8 actuators follow the unified SetState(bool) pattern: true = on, false = off.

Group 1: Lighting Actuators

1. FluorescentLight

2. Heater

3. EnergyCanister

Group 2: Effects with Audio

4. Humidifier

5. WindowFan

6. Vent

7. WaterTankFloor

Group 3: Utility Lights

No additional actuators; lights are controlled by environment systems


Environment Systems

Time of Day

Affects global lighting, skybox, fog, and night lights.

Time Lighting Skybox Fog Sun Angle Night Lights
Morning Warm (0.75, 0.72, 0.65) Clear sky Warm fog (on) 25°, 30° Off
Afternoon Bright (1.0, 1.0, 1.0) Bright blue Off 60°, 0° Off
Evening Orange (1.0, 0.55, 0.3) Orange sky Orange fog (on) 15°, 220° Off
Night Dark blue (0.4, 0.45, 0.6) Night sky Dim fog (on) -10°, 0° On

JSON Values (Case-Insensitive):

"timeOfDay": { "time": "Morning" | "Afternoon" | "Evening" | "Night" }

Crop Health Indicator

Controls RGB status lights showing crop health and maturity.

State Color Light Blink? Meaning
Green RGB Green Bright Auto* Healthy
Yellow RGB Yellow Medium Never Stressed
Red RGB Red Bright Never Critical

*Blinking occurs only when all crops are Ripe, regardless of JSON blinkGreen value.

JSON Values (Case-Insensitive):

"cropHealth": {
    "state": "Green" | "Yellow" | "Red",
    "blinkGreen": false  // IGNORED  computed from crop stages
}

Blink Speed: Configurable in CropHealthIndicator Inspector (blinkSpeed = 2.0 = 2 blinks/sec)


Complete Data Flow

JSON File (greenhouse_state.json)
    ↓
GreenhouseStateApplier.Update() [every 1s]
    ↓
Change Detected? (compare file content hash)
    ↓ YES
ParseJson() → GreenhouseState object
    ↓
ApplyState() → dispatches to 8 controllers + 3 environment systems
    ↓
Each Controller.SetState() or SetStageByName() or SetTimeOfDayByName()
    ↓
Scene updates: Lights on/off, Particles play/stop, Models swap, Fog changes
    ↓
GreenhouseStateApplier.LateUpdate() [every frame]
    ↓
Re-assert health color (so CropStageController.SetGreen() doesn't override JSON)
    ↓
Scene persists health color every frame

File Structure in Project

Assets/
├── Scripts/
│   ├── GreenhouseStateApplier.cs          [Central orchestrator]
│   ├── CropStageController.cs              [Crop stage visuals]
│   ├── CropHealthIndicator.cs              [RGB health lights]
│   ├── TimeOfDayController.cs              [Global lighting/sky]
│   ├── FluorescentLightController.cs       [Light actuator]
│   ├── HeaterController.cs                 [Light actuator]
│   ├── EnergyCanisterController.cs         [Light actuator]
│   ├── HumidifierController.cs             [Effects + audio]
│   ├── WindowFanController.cs              [Effects + audio]
│   ├── VentController.cs                   [Effects + audio]
│   ├── WaterTankFloorController.cs         [Effects + audio]
│   ├── FreeCameraController.cs             [Utility]
│   └── GREENHOUSE_INTEGRATION_GUIDE.md     [This document]
│
└── StreamingAssets/
    └── greenhouse_state.json               [Runtime state file]

What It Does

GreenhouseStateApplier.cs is the single central point that controls:

It reads a JSON file from disk at a configurable polling interval, detects changes, and applies updates automatically during Play Mode.

Key Features

Reads JSON at runtime — no code changes needed to update scene state
Detects changes automatically — only reapplies when file content changes
Supports any crop count — JSON array can have 15, 50, or 100 crops
Safe missing references — logs clear warnings but never crashes
Health color auto-logic — forces Green+blink when all crops are Ripe
LateUpdate assertion — health color persists even if other scripts modify it
Production-ready — clean error handling, verbose logging, easy to extend


Inspector Setup

  1. Create empty GameObject in your scene (e.g., "GreenhouseManager")
  2. Attach GreenhouseStateApplier component
  3. Assign every field in the Inspector by dragging scene GameObjects:

Actuators to Assign

Environment & Crop to Assign

Settings


JSON File Location

Path: Assets/StreamingAssets/greenhouse_state.json

If the StreamingAssets folder doesn’t exist, create it:

Assets/
├── StreamingAssets/
│   └── greenhouse_state.json


Individual Actuator Controllers

Architecture

All actuator scripts follow the unified control pattern:


1. FluorescentLightController.cs

Purpose: Controls a fluorescent light fixture with status indicator and point lights

Controls:

Public Methods:

Inspector Fields:


2. HeaterController.cs

Purpose: Controls a heater with indicator light and glow effect

Controls:

Public Methods:

Inspector Fields:


3. EnergyCanisterController.cs

Purpose: Controls an energy canister with status light

Controls:

Public Methods:

Inspector Fields:


4. HumidifierController.cs

Purpose: Controls a humidifier with fog particle effect and audio

Controls:

Public Methods:

Inspector Fields:

Auto-Found:


5. WindowFanController.cs

Purpose: Controls a window fan with air flow effect and audio

Controls:

Public Methods:

Inspector Fields:

Auto-Found:


6. VentController.cs

Purpose: Controls a ventilation system with air flow and audio trigger

Controls:

Public Methods:

Inspector Fields:

Auto-Found:

Note: Audio plays only on state changes, not continuously (unlike fan)


7. WaterTankFloorController.cs

Purpose: Controls a water tank with status light and audio

Controls:

Public Methods:

Inspector Fields:

Auto-Found:



Environment Controllers

8. CropStageController.cs

Purpose: Controls a single crop’s visual stage and updates its health indicator

Growth Stages (0-5):

Public Methods:

Inspector Fields:

Auto-Behavior:


9. TimeOfDayController.cs

Purpose: Controls scene lighting, skybox, fog, and night lights

Time Periods:

Public Methods:

Inspector Fields:


10. CropHealthIndicator.cs

Purpose: Controls green/yellow/red status lights with optional blinking

Health States:

Public Methods:

Inspector Fields:

Auto-Behavior (via GreenhouseStateApplier):


11. FreeCameraController.cs

Purpose: Provides free camera movement and look controls

Controls:

Inspector Fields:



JSON Schema & Format

Complete JSON Structure

{
    "fluorescentLight":  { "isOn": true | false },
    "heater":            { "isOn": true | false },
    "energyCanister":    { "isOn": true | false },
    "humidifier":        { "isOn": true | false },
    "windowFan":         { "isOn": true | false },
    "vent":              { "isOn": true | false },
    "waterTankFloor":    { "isOn": true | false },
    
    "cropStage":  { "stage": "Seedling|Vegetative|FloweringInitiation|Flowering|Unripe|Ripe" },
    
    "timeOfDay":  { "time": "Morning|Afternoon|Evening|Night" },
    
    "cropHealth": {
        "state": "Green|Yellow|Red",
        "blinkGreen": false  // IGNORED  computed from crop stages
    }
}

Important Notes


Sample JSON File

{
    "fluorescentLight":  { "isOn": true  },
    "heater":            { "isOn": false },
    "energyCanister":    { "isOn": true  },
    "humidifier":        { "isOn": true  },
    "windowFan":         { "isOn": false },
    "vent":              { "isOn": true  },
    "waterTankFloor":    { "isOn": false },
    "cropStage":         { "stage": "Ripe" },
    "timeOfDay":         { "time":  "Morning" },
    "cropHealth":        { "state": "Green", "blinkGreen": false }
}


Setup Instructions

Step 1: Verify File Structure

Ensure your project has:

Assets/
├── Scripts/
│   ├── GreenhouseStateApplier.cs
│   ├── CropStageController.cs
│   ├── CropHealthIndicator.cs
│   ├── TimeOfDayController.cs
│   ├── FluorescentLightController.cs
│   ├── HeaterController.cs
│   ├── EnergyCanisterController.cs
│   ├── HumidifierController.cs
│   ├── WindowFanController.cs
│   ├── VentController.cs
│   ├── WaterTankFloorController.cs
│   ├── FreeCameraController.cs
│   └── GREENHOUSE_INTEGRATION_GUIDE.md (this file)
└── StreamingAssets/
    └── greenhouse_state.json

Step 2: Create Manager GameObject

  1. In the Hierarchy, right-click → Create Empty
  2. Name it "GreenhouseManager"
  3. Attach GreenhouseStateApplier component to it

Step 3: Assign Inspector References

In the Inspector for GreenhouseStateApplier, drag:

Actuators (8 items)

  1. Fluorescent Light ← GameObject with FluorescentLightController
  2. Heater ← GameObject with HeaterController
  3. Energy Canister ← GameObject with EnergyCanisterController
  4. Humidifier ← GameObject with HumidifierController
  5. Window Fan ← GameObject with WindowFanController
  6. Vent ← GameObject with VentController
  7. Water Tank Floor ← GameObject with WaterTankFloorController

Crops & Environment (3 items)

  1. Crop Stages array — Set Size = 15 (or however many crops you have), then drag each crop GameObject into Element 0–14. Note: All crops receive the same cropStage value from the JSON.
  2. Time Of Day ← GameObject with TimeOfDayController
  3. Crop Health ← GameObject with CropHealthIndicator

Step 4: Create JSON File

  1. Create folder: Assets/StreamingAssets/
  2. Create file: greenhouse_state.json
  3. Paste the sample JSON content (see Sample JSON File above)

Step 5: Play & Test

Press Play — the GreenhouseStateApplier reads the JSON file and applies the state automatically within 1 second (default poll interval).



Live Testing in Play Mode

How to Test Dynamic Updates

While Play Mode is running:

  1. Open the JSON file in VS Code, Notepad++, or any text editor:
    Assets/StreamingAssets/greenhouse_state.json
    
  2. Make a change (Examples):
    • Turn on heater: "heater": { "isOn": false }"heater": { "isOn": true }
    • Change crop stage (applies to all 15 crops): "cropStage": { "stage": "Seedling" }"cropStage": { "stage": "Ripe" }
    • Set health to Yellow: "state": "Green""state": "Yellow"
    • Change time: "time": "Morning""time": "Night"
  3. Save the file (Ctrl+S)

  4. Within 1 second, the scene updates automatically:
    • Lights turn on/off
    • Particles start/stop
    • Audio plays/stops
    • Crop models swap stages
    • Sky and fog change
    • Health indicator colors shift
  5. Watch the Console for debug messages:
    [GreenhouseStateApplier] Crop[0] stage -> Ripe
    [GreenhouseStateApplier] CropHealth -> Green (blinking) [auto-overridden: all crops Ripe]
    [GreenhouseStateApplier] All states applied successfully.
    

Force Refresh

In the Inspector during Play Mode, tick the Force Refresh checkbox to immediately re-apply without waiting for the next poll interval. The checkbox auto-resets after use.



Future FastAPI / Python Backend Integration

Swapping File to API

The current script reads from a local JSON file. To connect to a real Python FastAPI backend:

Step 1: Locate the Read Method

Find this in GreenhouseStateApplier.cs:

string ReadJsonFromDisk()
{
    try
    {
        if (!File.Exists(_resolvedFilePath))
        {
            Debug.LogWarning("[GreenhouseStateApplier] JSON file not found: " + _resolvedFilePath);
            return null;
        }

        return File.ReadAllText(_resolvedFilePath);
    }
    catch (Exception ex)
    {
        Debug.LogError("[GreenhouseStateApplier] Failed to read JSON file: " + ex.Message);
        return null;
    }
}

Step 2: Replace with FastAPI Call

private IEnumerator ReadJsonFromApi()
{
    string apiUrl = "http://localhost:8000/state";  // Your FastAPI endpoint

    UnityWebRequest request = UnityWebRequest.Get(apiUrl);
    yield return request.SendWebRequest();

    if (request.result == UnityWebRequest.Result.Success)
    {
        return request.downloadHandler.text;
    }
    else
    {
        Debug.LogError("[GreenhouseStateApplier] API request failed: " + request.error);
        return null;
    }
}

Step 3: Update TryReadAndApply

Change from synchronous to coroutine:

// Before (synchronous)
string json = ReadJsonFromDisk();

// After (coroutine)
StartCoroutine(ReadJsonFromApiCoroutine());

IEnumerator ReadJsonFromApiCoroutine()
{
    yield return StartCoroutine(ReadJsonFromApi());
    // Continue with parsing and applying...
}

That’s it. Everything else in the pipeline (parsing, change detection, applying to controllers) stays exactly the same. The individual controller scripts need zero changes.



Architecture & Design Patterns

Why This Structure?

Separation of Concerns

Each layer knows nothing about lower layers and can be tested independently.

Unified Control Pattern

Every actuator exposes SetState(bool) — a single, predictable method signature that the central applier can call uniformly. This makes it trivial to add new actuators later.

LateUpdate for Health Persistence

CropStageController.Update() might call SetGreen() (because a crop is Ripe). If GreenhouseStateApplier.Update() happens to run after that, it would override the health you just set in the JSON.

Solution: GreenhouseStateApplier uses LateUpdate() to re-assert health color after all Update() calls finish. This guarantees the health color you set in JSON persists every frame, regardless of execution order.

All-Ripe Override Logic

When all 15 crops are Ripe, the logic is:

  1. Always set health to Green (even if JSON says Yellow/Red)
  2. Always enable blinking (even if JSON says "blinkGreen": false)
  3. Automatically detect when this condition starts/stops

This is business logic: “A fully mature crop garden should always show green + blinking.” The JSON cannot override this — it’s part of the scene’s DNA.

Fault Tolerance


Future Extensibility

Adding More Crops (or Fewer)

Since all crops share a single cropStage value, adding or removing crops is trivial:

  1. Create new crop GameObject(s) in the scene with CropStageController component
  2. In GreenhouseStateApplier Inspector, increase Crop Stages array Size
  3. Drag the new crop(s) into the new array slots
  4. Done. No JSON or code changes required. The single cropStage value applies to all crops automatically.

Adding a New Actuator

  1. Create the new controller script (e.g., SoilSensorController.cs)
  2. Add to GreenhouseStateApplier:
    public SoilSensorController soilSensor;
       
    void ApplySoilSensor(ActuatorState s)
    {
        if (s == null) return;
        if (!AssertRef(soilSensor, "SoilSensorController")) return;
        soilSensor.SetState(s.isOn);
        Log("SoilSensor -> " + s.isOn);
    }
    
  3. Call in ApplyState():
    ApplySoilSensor(state.soilSensor);
    
  4. Update GreenhouseState data class:
    public ActuatorState soilSensor;
    
  5. Update JSON schema and sample file
  6. Done. No other scripts touched.

Testing Individual Controllers

You can test any controller in isolation by manually calling its methods in the editor:

// Test FluorescentLight
fluorescent.SetState(true);
fluorescent.TurnOff();

// Test CropStage
crop.SetStageByName("Ripe");
crop.NextStage();

// Test Health
health.SetRed();
health.SetBlinkGreen(true);

Or enable debugManualControl in the Inspector to toggle values in Play Mode without touching code.


Shader Updates

Lit Shader - Leaf Green Default Color


Common Integration Patterns

Pattern 1: Direct Local Testing (Current)

JSON File (disk)
    ↓
GreenhouseStateApplier (reads file every 1s)
    ↓
Individual Controllers (apply state)
    ↓
Scene (lights, particles, audio)

Pattern 2: FastAPI Backend (Future)

FastAPI Backend (Python)
    ↓ GET /state
    ↓
GreenhouseStateApplier (polls API every 1s)
    ↓
Individual Controllers (apply state)
    ↓
Scene (lights, particles, audio)

Only the first read step changes. Everything after is identical.


Debugging Checklist

Issue Solution
Script not compiling Check for missing references in Inspector
Scene not updating on JSON change Verify file path is correct; check Console for errors
Health color won’t change to Red Check debugManualControl is false; ensure not all crops are Ripe
Crop stage not changing Verify cropStages array is assigned and not empty
No blink even though all Ripe Verify cropHealth is assigned; check health state is “Green” or all-Ripe override should apply
Reference warnings in Console Drag missing controller from scene into the Inspector slot

Version History

Version Changes
1.0 Initial release: 8 actuators, 3 environment controllers, central JSON applier
- Support for 15 crops (expandable)
- LateUpdate health persistence
- All-Ripe auto-blink override logic


Scenario Runner — sample_greenhouse_scenarios.py

Overview

sample_greenhouse_scenarios.py is a Python script that drives the live WebGL greenhouse through a curated set of 15 real-world growing conditions. It replaces manual JSON editing by POSTing each scenario directly to the FastAPI backend, which the Unity WebGL build polls every second.

Why HTTP and not a JSON file? WebGL builds run inside a browser sandbox and cannot read from the local filesystem. Writing greenhouse_state.json on disk has no effect on the deployed scene. The script POSTs to POST /api/greenhouse-3d/state instead — the backend stores the state in memory and the Unity WebGL build retrieves it via GET /api/greenhouse-3d/state.


Location

scripts/sample_greenhouse_scenarios.py

Usage

python scripts/sample_greenhouse_scenarios.py

Environment Overrides

Variable Default Description
AGRITWIN_NO_SERVER 0 Set to 1 to skip launching main.py (use an already-running backend)
AGRITWIN_API_URL http://localhost:8000 Base URL of the FastAPI backend

Run Order

Phase 1 — Backend + Browser

  1. Starts main.py as a background process
  2. Waits for GET /api/system/health to return 200 (up to 120 s)
  3. Opens the WebGL greenhouse at http://localhost:8000/greenhouse-3d/ in the default browser

Phase 2 — Scenario Walkthrough


How It Works

sample_greenhouse_scenarios.py
    ↓  POST /api/greenhouse-3d/state  (JSON body)
FastAPI backend  (greenhouse_3d.py — stores state in memory)
    ↓  GET /api/greenhouse-3d/state  (polled every 1 s)
GreenhouseStateApplier.cs  (UnityWebRequest coroutine)
    ↓
Individual Controllers  (apply state)
    ↓
WebGL Scene  (lights, particles, audio, crop stages, sky)

Scenario Catalogue

15 scenarios covering the full crop lifecycle and edge cases:

# Name Stage Time Health
1 Germination — Pre-Dawn Start Seedling Night Green
2 Seedling — Morning Warm-Up Seedling Morning Green
3 Vegetative — Peak Growth Afternoon Vegetative Afternoon Green
4 Vegetative — Mild Heat Stress Warning Vegetative Afternoon Yellow
5 Flowering Initiation — Evening Transition FloweringInitiation Evening Green
6 Full Bloom — Optimal Conditions Flowering Afternoon Green
7 Full Bloom — Disease Alert (Critical) Flowering Night Red
8 Unripe Fruit — Night Ripening Mode Unripe Night Green
9 Unripe Fruit — Water Deficit Stress Unripe Morning Yellow
10 Ripe — Harvest-Ready (Full Blink) Ripe Afternoon Green
11 Post-Harvest — Greenhouse Reset (All Off) Seedling Night Green
12 Emergency — Total System Failure Flowering Night Red
13 Cold Snap — Winter Night Protocol Vegetative Night Green
14 Heatwave — Maximum Ventilation Flowering Afternoon Yellow
15 Ideal Cycle — End-to-End Showcase Flowering Afternoon Green

API Endpoint

Method URL Purpose
POST /api/greenhouse-3d/state Script writes each scenario here
GET /api/greenhouse-3d/state Unity WebGL polls this every second

The POST body is the standard greenhouse JSON schema (see JSON Schema & Format).


Exit Codes

Code Meaning
0 All scenarios applied successfully
1 Backend failed to start or an API call failed

Quick Start Summary

What You Have

12 C# Scripts — all production-ready, well-commented
8 Actuator Controllers — lights, effects, audio (all use SetState(bool))
3 Environment Controllers — crop stages, time of day, health indicator
1 Central Applier — reads JSON, auto-applies state
1 Sample JSON File — ready to edit and test
1 Integration Guide — this document (everything you need)

What You Do

  1. Create GreenhouseManager GameObject in your scene
  2. Attach GreenhouseStateApplier component
  3. Drag 10 controller GameObjects into Inspector slots
  4. Set JSON file path (default: Assets/StreamingAssets/greenhouse_state.json)
  5. Press Play — scene auto-updates as you edit the JSON

Key Concepts

Unified Control

Every actuator is commanded identically: SetState(bool). You pass true or false. That’s it.

Single Crop Stage Value

The JSON has one cropStage value (not 15). It applies to all 15 crops simultaneously. This matches your real project where the backend sends a single stage value.

Green blinking automatically activates when all crops are Ripe. You cannot manually disable it — it’s built in. Remove it from the JSON or set another state (Yellow/Red) stops blinking.

Change Detection

The applier only re-applies when file content actually changes. Touching the file without changing content = no re-apply (efficient).

LateUpdate Guarantee

Health color persists every frame, even if CropStageController.Update() tries to modify it. The applier always has the final say.


Example JSON Commands

Fully Ripe Greenhouse

{
    "fluorescentLight":  { "isOn": true  },
    "heater":            { "isOn": true  },
    "energyCanister":    { "isOn": true  },
    "humidifier":        { "isOn": true  },
    "windowFan":         { "isOn": true  },
    "vent":              { "isOn": true  },
    "waterTankFloor":    { "isOn": true  },
    "cropStage":         { "stage": "Ripe" },
    "timeOfDay":         { "time": "Afternoon" },
    "cropHealth":        { "state": "Green", "blinkGreen": false }
}

Result: All lights on, all particles running, all audio playing, every crop shows Ripe model, green light automatically blinking.

Seedling Startup (Morning)

{
    "fluorescentLight":  { "isOn": true  },
    "heater":            { "isOn": false },
    "energyCanister":    { "isOn": false },
    "humidifier":        { "isOn": true  },
    "windowFan":         { "isOn": false },
    "vent":              { "isOn": false },
    "waterTankFloor":    { "isOn": true  },
    "cropStage":         { "stage": "Seedling" },
    "timeOfDay":         { "time": "Morning" },
    "cropHealth":        { "state": "Green", "blinkGreen": false }
}

Result: Lights on for growth, heater off (seedlings sensitive), humidifier on, all crops show Seedling model, morning lighting, green light (no blink).

Stressed Crop (Emergency)

{
    "fluorescentLight":  { "isOn": true  },
    "heater":            { "isOn": true  },
    "energyCanister":    { "isOn": true  },
    "humidifier":        { "isOn": false },
    "windowFan":         { "isOn": true  },
    "vent":              { "isOn": true  },
    "waterTankFloor":    { "isOn": false },
    "cropStage":         { "stage": "Flowering" },
    "timeOfDay":         { "time": "Night" },
    "cropHealth":        { "state": "Red", "blinkGreen": false }
}

Result: All systems active, humidifier off, crops in Flowering, night lighting, red health light (critical stress).


Common Workflows

Workflow 1: Develop Locally vs Integrate with Backend

Phase 1: Local Testing (NOW)
  └─ Edit JSON manually
  └─ Watch scene update in real-time
  └─ Verify all actuators work
  └─ Test all combinations

Phase 2: Backend Integration (FUTURE)
  └─ Replace ReadJsonFromDisk() with UnityWebRequest
  └─ Point to your FastAPI /state endpoint
  └─ Everything else unchanged
  └─ Drop into production

Workflow 2: Adding a New Actuator

1. Create MyNewController.cs with SetState(bool)
2. Add to GreenhouseStateApplier class:
   - Public field: public MyNewController myNew;
   - Apply method: void ApplyMyNew(ActuatorState s) { ... }
   - Call in ApplyState(): ApplyMyNew(state.myNew);
3. Update GreenhouseState data class:
   - public ActuatorState myNew;
4. Update JSON schema and sample file
5. Done. No other scripts touched.

Workflow 3: Debugging Why Health Won’t Change

Check in this order:

  1. Verify cropHealth component is assigned in Inspector
  2. Verify debugManualControl is false on CropHealthIndicator
  3. Check Console for [GreenhouseStateApplier] messages
  4. If all crops are Ripe, health is forced to Green (this is intentional)
  5. Try forceRefresh checkbox in Inspector to re-apply immediately

File Format Quick Reference

Field Type Valid Values
fluorescentLight.isOn bool true, false
heater.isOn bool true, false
energyCanister.isOn bool true, false
humidifier.isOn bool true, false
windowFan.isOn bool true, false
vent.isOn bool true, false
waterTankFloor.isOn bool true, false
cropStage.stage string Seedling, Vegetative, FloweringInitiation, Flowering, Unripe, Ripe (case-insensitive)
timeOfDay.time string Morning, Afternoon, Evening, Night (case-insensitive)
cropHealth.state string Green, Yellow, Red (case-insensitive)
cropHealth.blinkGreen bool true, false (IGNORED — auto-computed)

Performance Notes


End of Integration Guide