commit 19605dea2fe7f0085fc7545fced2ed2f838ba7a8
parent dc5ad1ab3c736894d64d1b44d08638d432d8b992
Author: AbhinavRai01 <abhinavrai004@gmail.com>
Date: Fri, 27 Mar 2026 01:37:23 +0530
simulator done
Diffstat:
10 files changed, 432 insertions(+), 179 deletions(-)
diff --git a/backend/node_server/config/db.js b/backend/node_server/config/db.js
@@ -1,9 +1,11 @@
require('dotenv').config();
const { QdrantClient } = require('@qdrant/js-client-rest');
+const {mongoose} = require('mongoose');
const COLLECTION_NAME = 'Farm_Memory';
// 1. Initialize Client with the Fix
+console.log("🔧 Initializing Qdrant Client...", process.env.QDRANT_URL);
const client = new QdrantClient({
url: process.env.QDRANT_URL,
apiKey: process.env.QDRANT_API_KEY,
@@ -49,4 +51,14 @@ const initDB = async () => {
}
};
-module.exports = { client, initDB, COLLECTION_NAME };
-\ No newline at end of file
+const connectMongoDB = async () => {
+ try {
+ await mongoose.connect(process.env.MONGODB_URI, {
+ });
+ console.log("✅ Connected to MongoDB");
+ } catch (err) {
+ console.error("❌ MongoDB Connection Failed:", err.message);
+ }
+};
+
+module.exports = { client, initDB, connectMongoDB, COLLECTION_NAME };
+\ No newline at end of file
diff --git a/backend/node_server/controllers/cropController.js b/backend/node_server/controllers/cropController.js
@@ -0,0 +1,38 @@
+const CropStateSchema = require('../schema/cropSchema');
+
+const createCrop = async (req, res) => {
+ try {
+ const { crop_id, crop, stage, ...rest } = req.body;
+
+ if (!crop_id) {
+ return res.status(400).json({ error: 'crop_id is required' });
+ }
+
+ const existingCrop = await CropStateSchema.findOne({ crop_id });
+ if (existingCrop) {
+ return res.status(409).json({ error: 'Crop with this ID already exists' });
+ }
+
+ const newCrop = new CropStateSchema({
+ crop_id,
+ crop,
+ stage: stage || 'seedling',
+ sequence_number: 0,
+ total_crop_lifetime_days: 0,
+ planted_at: new Date(),
+ last_updated: new Date(),
+ ...rest
+ });
+
+ const savedCrop = await newCrop.save();
+
+ res.status(201).json({
+ message: 'Crop created successfully',
+ data: savedCrop
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+module.exports = { createCrop };
+\ No newline at end of file
diff --git a/backend/node_server/controllers/farmController.js b/backend/node_server/controllers/farmController.js
@@ -106,4 +106,6 @@ const getCropHistory = async (req, res) => {
}
};
+
+
module.exports = { addMemory, getDashboard, getCropHistory };
\ No newline at end of file
diff --git a/backend/node_server/index.js b/backend/node_server/index.js
@@ -1,7 +1,8 @@
const express = require('express');
const cors = require('cors');
-const { initDB } = require('./config/db');
+const { initDB, connectMongoDB } = require('./config/db');
const farmRoutes = require('./routes/farmRoutes');
+const cropRoutes = require('./routes/cropRoutes');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -12,10 +13,12 @@ app.use(cors());
// Initialize Database & Indexes
initDB();
+connectMongoDB();
// Mount Routes
// All routes inside farmRoutes will be prefixed with /api
app.use('/api', farmRoutes);
+app.use('/api/crop', cropRoutes);
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
diff --git a/backend/node_server/package-lock.json b/backend/node_server/package-lock.json
@@ -13,10 +13,20 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
+ "mongoose": "^9.3.3",
"nodemon": "^3.1.11",
"uuid": "^13.0.0"
}
},
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
+ "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
"node_modules/@qdrant/js-client-rest": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.17.0.tgz",
@@ -44,6 +54,21 @@
"pnpm": ">=8"
}
},
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -116,9 +141,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
- "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -139,6 +164,15 @@
"node": ">=8"
}
},
+ "node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -686,6 +720,15 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
+ "node_modules/kareem": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz",
+ "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -704,6 +747,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
+ },
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -756,6 +805,104 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/mongodb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz",
+ "integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.1.1",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "9.3.3",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.3.3.tgz",
+ "integrity": "sha512-sfv5LOIPWeN5o/281kp4Rx9ZnuXb0g8CtvBTi7trYQs2PYYx8LWXegXxG3ar7VEns1o+d4h9LI/Dtc7dTTyYmA==",
+ "license": "MIT",
+ "dependencies": {
+ "kareem": "3.2.0",
+ "mongodb": "~7.1",
+ "mpath": "0.9.0",
+ "mquery": "6.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
+ "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -870,9 +1017,9 @@
}
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -900,6 +1047,15 @@
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"license": "MIT"
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
@@ -1108,6 +1264,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+ "license": "MIT"
+ },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -1120,6 +1282,15 @@
"node": ">=10"
}
},
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -1171,6 +1342,18 @@
"nodetouch": "bin/nodetouch.js"
}
},
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@@ -1186,9 +1369,9 @@
}
},
"node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
+ "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"license": "Apache-2.0",
"peer": true,
"bin": {
@@ -1245,6 +1428,28 @@
"node": ">= 0.8"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/backend/node_server/package.json b/backend/node_server/package.json
@@ -15,6 +15,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
+ "mongoose": "^9.3.3",
"nodemon": "^3.1.11",
"uuid": "^13.0.0"
}
diff --git a/backend/node_server/routes/cropRoutes.js b/backend/node_server/routes/cropRoutes.js
@@ -0,0 +1,7 @@
+const express = require('express');
+const router = express.Router();
+const { createCrop } = require('../controllers/cropController');
+
+router.post('/create', createCrop);
+
+module.exports = router;
+\ No newline at end of file
diff --git a/backend/node_server/schema/cropSchema.js b/backend/node_server/schema/cropSchema.js
@@ -0,0 +1,34 @@
+const mongoose = require('mongoose');
+
+const sensorSchema = new mongoose.Schema({
+ pH: [Number],
+ EC: [Number],
+ temp: [Number],
+ humidity: [Number]
+}, { _id: false });
+
+const cropStateSchema = new mongoose.Schema({
+ crop_id: { type: String, required: true, unique: true },
+ crop: String,
+ stage: String,
+ sequence_number: { type: Number, default: 0 },
+
+ cycle_duration_hours: { type: Number, default: 1 },
+ total_crop_lifetime_days: { type: Number, default: 0 },
+ planted_at: { type: Date, default: Date.now },
+ last_updated: { type: Date, default: Date.now },
+
+ sensors: sensorSchema,
+
+ action_taken: mongoose.Schema.Types.Mixed,
+ outcome: String,
+ explanation_log: String,
+ bandit_action_id: Number,
+ strategic_intent: String,
+ reward_score: Number,
+ visual_diagnosis: String,
+
+ schema_version: { type: String, default: "1.1" }
+});
+
+module.exports = mongoose.model('CropState', cropStateSchema);
+\ No newline at end of file
diff --git a/simulator/lettuce_brain_v1.zip b/simulator/lettuce_brain_v1.zip
Binary files differ.
diff --git a/simulator/main.py b/simulator/main.py
@@ -1,73 +1,27 @@
import base64
-import time
import os
-import json
-import threading
import uvicorn
import numpy as np
import torch
-import torch.nn as nn
from io import BytesIO
from collections import deque
-from dataclasses import dataclass, asdict
from fastapi import FastAPI
from pydantic import BaseModel
+from typing import List
from PIL import Image
from dotenv import load_dotenv
+from pymongo import MongoClient
+from typing import Optional
load_dotenv()
-# --- AZURE DIGITAL TWINS CONFIG ---
-try:
- from azure.identity import DefaultAzureCredential
- from azure.digitaltwins.core import DigitalTwinsClient
-
- credential = DefaultAzureCredential()
- adt_url = os.environ.get("ADT_URL", "simulator.api.krc.digitaltwins.azure.net")
- client = DigitalTwinsClient(adt_url, credential)
- twin_id = "HydrophonicTank"
- AZURE_ENABLED = True
-except Exception as e:
- print(f"Azure Digital Twins disabled: {e}")
- AZURE_ENABLED = False
-
-
-def sync_to_azure(state):
- if not AZURE_ENABLED:
- return
- ph, ec, water_temp, air_temp, humidity, vpd, biomass = state
- payload = {
- "ph": float(ph),
- "ec": float(ec),
- "water_temp": float(water_temp),
- "air_temp": float(air_temp),
- "humidity": float(humidity),
- "vpd": float(vpd),
- "biomass_g": float(biomass),
- }
- try:
- client.publish_telemetry(twin_id, payload)
- except Exception as e:
- print(f"Azure Sync Error: {e}")
-
-
-# --- CONFIG ---
-MODEL_PATH = "models/PPO/lettuce_brain_v1.zip"
-HISTORY_LEN = 20
-
-
-# --- DATA MODELS ---
-@dataclass
-class FarmStateData:
- ph: float
- ec: float
- water_temp: float
- air_temp: float
- humidity: float
- vpd: float
- biomass_g: float
- tank_volume_l: float
+MONGO_URI = os.environ.get("MONGO_URI", "mongodb+srv://abhi:lovesv7@demeter.qfvttv1.mongodb.net/?appName=Demeter")
+mongo_client = MongoClient(MONGO_URI)
+db = mongo_client["test"]
+crops_collection = db["cropstates"]
+MODEL_PATH = "/lettuce_brain_v1.zip"
+HISTORY_LEN = 20
class FarmAction(BaseModel):
acid_dosage_ml: float = 0.0
@@ -75,10 +29,12 @@ class FarmAction(BaseModel):
nutrient_dosage_ml: float = 0.0
fan_speed_pct: float = 0.0
water_refill_l: float = 0.0
- debug_force_ph: float | None = None
+ debug_force_ph: Optional[float] = None
+class BatchActionRequest(BaseModel):
+ crop_id: str
+ action: FarmAction
-# --- PHYSICS ENGINE (Research Grade) ---
class ResidualPhysicsNet(torch.nn.Module):
def __init__(self, state_dim, action_dim):
super().__init__()
@@ -94,27 +50,23 @@ class ResidualPhysicsNet(torch.nn.Module):
x = torch.cat([state, action], dim=-1)
return self.net(x)
-
class DigitalTwin:
- def __init__(self):
- # Initial State: [pH, EC, WaterT, AirT, Hum, VPD, Biomass]
- self.state = np.array([6.0, 1.5, 20.0, 24.0, 60.0, 1.0, 10.0], dtype=np.float32)
+ def __init__(self, crop_id: str, initial_state: list):
+ self.crop_id = crop_id
+ self.state = np.array(initial_state, dtype=np.float32)
self.tank_volume = 100.0
self.plant_health = 100.0
- self.crop_id = "BATCH-VERDANT-X1"
-
self.residual_model = ResidualPhysicsNet(7, 4)
- # History
self.history = {
- "ph": deque([6.0] * 5, maxlen=HISTORY_LEN),
- "ec": deque([1.5] * 5, maxlen=HISTORY_LEN),
- "water_temp": deque([20.0] * 5, maxlen=HISTORY_LEN),
- "air_temp": deque([24.0] * 5, maxlen=HISTORY_LEN),
- "humidity": deque([60.0] * 5, maxlen=HISTORY_LEN),
+ "ph": deque([float(self.state[0])] * 5, maxlen=HISTORY_LEN),
+ "ec": deque([float(self.state[1])] * 5, maxlen=HISTORY_LEN),
+ "water_temp": deque([float(self.state[2])] * 5, maxlen=HISTORY_LEN),
+ "air_temp": deque([float(self.state[3])] * 5, maxlen=HISTORY_LEN),
+ "humidity": deque([float(self.state[4])] * 5, maxlen=HISTORY_LEN),
"co2": deque([400.0] * 5, maxlen=HISTORY_LEN),
"light_intensity": deque([0.0] * 5, maxlen=HISTORY_LEN),
- "vpd": deque([1.0] * 5, maxlen=HISTORY_LEN),
+ "vpd": deque([float(self.state[5])] * 5, maxlen=HISTORY_LEN),
}
def _calculate_vpd(self, temp, hum):
@@ -126,17 +78,13 @@ class DigitalTwin:
if action is None:
action = FarmAction()
- u = np.array(
- [
- action.acid_dosage_ml / 10.0,
- action.base_dosage_ml / 10.0,
- action.nutrient_dosage_ml / 20.0,
- action.fan_speed_pct / 100.0,
- ],
- dtype=np.float32,
- )
+ u = np.array([
+ action.acid_dosage_ml / 10.0,
+ action.base_dosage_ml / 10.0,
+ action.nutrient_dosage_ml / 20.0,
+ action.fan_speed_pct / 100.0,
+ ], dtype=np.float32)
- # 1. Physics Calculations
ph, ec, wt, at, hum, vpd, bio = self.state
d_ph = (u[1] * 0.5) - (u[0] * 0.5) + (0.001 * bio)
@@ -145,137 +93,136 @@ class DigitalTwin:
uptake = 0.02 * bio * vpd
d_ec = (u[2] * 0.2) - (uptake / self.tank_volume)
-
d_at = 0.1 - (u[3] * 1.5)
d_hum = 1.0 - (u[3] * 5.0)
stress = abs(vpd - 1.0)
growth = 0.1 * bio * (1.0 - min(stress, 1.0))
- physics_delta = np.array(
- [d_ph, d_ec, 0, d_at, d_hum, 0, growth], dtype=np.float32
- )
+ physics_delta = np.array([d_ph, d_ec, 0, d_at, d_hum, 0, growth], dtype=np.float32)
- # 2. Neural Residual
with torch.no_grad():
- nn_delta = self.residual_model(
- torch.tensor(self.state), torch.tensor(u)
- ).numpy()
+ nn_delta = self.residual_model(torch.tensor(self.state), torch.tensor(u)).numpy()
- # 3. Update State
self.state += physics_delta + (nn_delta * 0.05)
-
- # Clip & Recalc
self.state[3] = np.clip(self.state[3], 0, 50)
self.state[4] = np.clip(self.state[4], 0, 100)
self.state[5] = self._calculate_vpd(self.state[3], self.state[4])
self.state[6] = max(0.1, self.state[6])
- # Calculate Health
ph_score = max(0, 1.0 - abs(self.state[0] - 6.0))
vpd_score = max(0, 1.0 - abs(self.state[5] - 1.0))
-
- # FIX: Ensure calculation results in a standard float
- health_calc = (float(ph_score) + float(vpd_score)) * 50.0
- self.plant_health = max(0.0, min(100.0, health_calc))
+ self.plant_health = max(0.0, min(100.0, (float(ph_score) + float(vpd_score)) * 50.0))
self._update_history()
return self._generate_image()
def _update_history(self):
s = self.state
- # FIX: Explicit float() casting prevents numpy errors in JSON
self.history["ph"].append(float(s[0]))
self.history["ec"].append(float(s[1]))
self.history["water_temp"].append(float(s[2]))
self.history["air_temp"].append(float(s[3]))
self.history["humidity"].append(float(s[4]))
self.history["vpd"].append(float(s[5]))
- self.history["co2"].append(400.0)
- self.history["light_intensity"].append(0.0)
def _generate_image(self):
- bucket = int(self.plant_health // 10) * 10
- bucket = max(0, min(90, bucket))
+ bucket = max(0, min(90, int(self.plant_health // 10) * 10))
filename = f"{bucket}.png"
-
if os.path.exists(filename):
return Image.open(filename)
return Image.new("RGB", (512, 512), (50, 50, 50))
-
-# --- SERVER ---
app = FastAPI()
-sim = DigitalTwin()
-
+simulators = {}
+
+def sync_simulators_from_db():
+ db_crops = crops_collection.find({})
+ for crop in db_crops:
+ cid = crop.get("crop_id")
+ if not cid:
+ continue
+
+ if cid not in simulators:
+ sensors = crop.get("sensors", {})
+
+ ph_val = sensors.get("pH", [6.0])
+ ec_val = sensors.get("EC", [1.5])
+ temp_val = sensors.get("temp", [24.0])
+ hum_val = sensors.get("humidity", [60.0])
+
+ state = [
+ ph_val[-1] if isinstance(ph_val, list) else ph_val,
+ ec_val[-1] if isinstance(ec_val, list) else ec_val,
+ 20.0,
+ temp_val[-1] if isinstance(temp_val, list) else temp_val,
+ hum_val[-1] if isinstance(hum_val, list) else hum_val,
+ 1.0,
+ 10.0
+ ]
+ simulators[cid] = DigitalTwin(cid, state)
@app.get("/simulation/state")
-async def get_state():
- pil_img = sim.step()
-
- buf = BytesIO()
- pil_img.save(buf, format="PNG")
- img_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
-
- # FIX: Casting numpy values to python types for JSON serialization
- return {
- "sensor_window": {k: list(v) for k, v in sim.history.items()},
- "metadata": {
- "crop": "lettuce",
- "stage": "vegetative" if sim.state[6] > 5.0 else "seedling",
- # FIX: Convert health to float before rounding
- "health": round(float(sim.plant_health), 1),
- "crop_id": sim.crop_id,
- "biomass_est": round(float(sim.state[6]), 2),
- },
- "image": img_b64,
- }
-
-
-@app.get("/azure/state")
-async def fetch_from_azure():
- if not AZURE_ENABLED:
- return {"status": "error", "message": "Azure Digital Twins is disabled."}
-
- try:
- twin = client.get_digital_twin(twin_id)
-
- biomass = float(twin.get("biomass_g", 0.0))
-
- return {
- "sensor_window": {
- "ph": [twin.get("ph", 0.0)],
- "ec": [twin.get("ec", 0.0)],
- "water_temp": [twin.get("water_temp", 0.0)],
- "air_temp": [twin.get("air_temp", 0.0)],
- "humidity": [twin.get("humidity", 0.0)],
- "vpd": [twin.get("vpd", 0.0)],
- },
+async def get_all_states():
+ sync_simulators_from_db()
+
+ response = []
+ for cid, sim in simulators.items():
+ pil_img = sim._generate_image()
+ buf = BytesIO()
+ pil_img.save(buf, format="PNG")
+ img_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
+
+ response.append({
+ "crop_id": cid,
+ "sensor_window": {k: list(v) for k, v in sim.history.items()},
"metadata": {
- "crop": "lettuce",
- "stage": "vegetative" if biomass > 5.0 else "seedling",
- "health": 100.0,
- "crop_id": twin.get("$dtId", "Unknown"),
- "biomass_est": round(biomass, 2),
+ "health": round(float(sim.plant_health), 1),
+ "biomass_est": round(float(sim.state[6]), 2),
},
- "image": "",
- }
- except Exception as e:
- return {"status": "error", "message": str(e)}
-
+ "image": img_b64,
+ })
+ return response
@app.post("/simulation/action")
-async def take_action(action: FarmAction):
- sim.step(action)
-
- sync_to_azure(sim.state)
-
- return {
- "status": "success",
- "new_state": {"ph": float(sim.state[0]), "ec": float(sim.state[1])},
- }
+async def take_batch_actions(payload: List[BatchActionRequest]):
+ sync_simulators_from_db()
+
+ results = []
+ for req in payload:
+ cid = req.crop_id
+ if cid not in simulators:
+ continue
+
+ sim = simulators[cid]
+ sim.step(req.action)
+
+ push_payload = {
+ "sensors.pH": {"$each": [float(sim.state[0])], "$slice": -5},
+ "sensors.EC": {"$each": [float(sim.state[1])], "$slice": -5},
+ "sensors.temp": {"$each": [float(sim.state[3])], "$slice": -5},
+ "sensors.humidity": {"$each": [float(sim.state[4])], "$slice": -5}
+ }
+
+ crops_collection.update_one(
+ {"crop_id": cid},
+ {
+ "$push": push_payload,
+ "$inc": {"sequence_number": 1}
+ }
+ )
+ results.append({
+ "crop_id": cid,
+ "status": "success",
+ "new_state": {
+ "pH": float(sim.state[0]),
+ "EC": float(sim.state[1])
+ }
+ })
+
+ return {"updated_crops": results}
if __name__ == "__main__":
port = int(os.environ.get("SIMULATOR_PORT", 8001))
- uvicorn.run(app, host="0.0.0.0", port=port)
+ uvicorn.run(app, host="0.0.0.0", port=port)
+\ No newline at end of file