# راهنمای طراحی Device Catalog داینامیک ## هدف هدف این تغییر این است که اضافه کردن یک دیوایس جدید فقط با ثبت اطلاعات در دیتابیس یا پنل ادمین انجام شود و برای هر دیوایس جدید نیازی به اضافه کردن فایل، ویو، serializer یا service جدید در کد نباشد. الان ساختار پروژه برای بعضی دیوایس‌ها device-specific است؛ مثلا: - `device_hub/sensor_7_in_1_urls.py` - `Sensor7In1SummaryView` - `get_sensor_7_in_1_summary_data` - `get_sensor_7_in_1_radar_chart_data` - `get_sensor_7_in_1_comparison_chart_data` این ساختار برای یک MVP خوب است، ولی برای scale شدن مناسب نیست. چون برای هر دیوایس جدید باید: - route جدید بسازید - view جدید بسازید - serializer جدید بسازید - service جدید بسازید - منطق mapping payload جدید اضافه کنید این دقیقا چیزی است که باید حذف شود. --- ## مشکل ساختار فعلی الان backend تا حدی بر اساس `device type` یا `sensor-7-in-1` branch می‌زند، نه بر اساس یک configuration عمومی. نمونه‌ها: - `device_hub/views.py` - `Sensor7In1SummaryView` - `Sensor7In1RadarChartView` - `Sensor7In1ComparisonChartView` - `device_hub/services.py` - `get_primary_soil_sensor` - `get_sensor_7_in_1_summary_data` - `get_sensor_7_in_1_values_list_data` - `get_sensor_7_in_1_radar_chart_data` - `get_sensor_7_in_1_comparison_chart_data` - `device_hub/sensor_serializers.py` - `Sensor7In1SummarySerializer` - `Sensor7In1MetaSerializer` مشکل این approach: 1. اضافه شدن هر device جدید نیاز به deploy کد دارد. 2. naming پروژه به device خاص وابسته می‌شود. 3. APIها generic نیستند. 4. frontend مجبور می‌شود endpointهای مخصوص هر device را صدا بزند. 5. منطق business به‌جای data-driven بودن، hard-coded شده است. --- ## معماری پیشنهادی ### اصل طراحی به‌جای این‌که برای هر device endpoint جدا داشته باشیم، باید فقط یک سری endpoint عمومی داشته باشیم که بر اساس: - `physical_device_uuid` یا - `device_catalog_uuid` یا - `device_catalog.code` اطلاعات همان device را برگردانند. یعنی backend باید: 1. device را پیدا کند 2. configuration آن device را از catalog بخواند 3. payload mapping آن device را بخواند 4. widgetهای قابل نمایش آن را تشخیص دهد 5. خروجی استاندارد بسازد --- ## APIهای پیشنهادی ### 1) لیست دیوایس‌ها ```http GET /api/device-hub/catalog/ ``` کاربرد: - لیست همه device catalogها - metadata هر catalog - نوع ارتباط device - فیلدهای قابل نمایش --- ### 2) جزئیات یک دیوایس ثبت‌شده روی مزرعه ```http GET /api/device-hub/devices/{physical_device_uuid}/ ``` پاسخ نمونه: ```json { "code": 200, "msg": "success", "data": { "uuid": "farm-device-uuid", "physical_device_uuid": "device-uuid", "name": "Soil Sensor #1", "device_catalog": { "uuid": "catalog-uuid", "code": "soil_sensor_v1", "name": "Soil Sensor V1", "device_communication_type": "output_only" }, "specifications": {}, "power_source": {}, "last_payload_at": "2025-01-01T10:00:00Z" } } ``` --- ### 3) آخرین داده‌ی یک device ```http GET /api/device-hub/devices/{physical_device_uuid}/latest/ ``` کاربرد: - آخرین payload خام - آخرین payload نرمال‌شده - آخرین readingهای قابل نمایش --- ### 4) summary داینامیک برای یک device ```http GET /api/device-hub/devices/{physical_device_uuid}/summary/ ``` کاربرد: - به‌جای `sensor_7_in_1/summary` - خروجی بر اساس config همان device --- ### 5) نمودار مقایسه‌ای داینامیک ```http GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?range=7d ``` --- ### 6) نمودار رادار داینامیک ```http GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?range=7d ``` --- ### 7) values list داینامیک ```http GET /api/device-hub/devices/{physical_device_uuid}/values-list/?range=7d ``` --- ### 8) دریافت history خام ```http GET /api/device-hub/devices/{physical_device_uuid}/logs/?page=1&page_size=20 ``` این endpoint برای debug و audit خیلی مهم است. --- ## تغییر مهم در مدل‌ها ### 1) `DeviceCatalog` الان این مدل شروع خوبی دارد، ولی برای dynamic شدن کافی نیست. مدل فعلی در: - `device_hub/models.py:6` فیلدهای پیشنهادی جدید: ```python display_schema = models.JSONField(default=dict, blank=True) payload_mapping = models.JSONField(default=dict, blank=True) supported_widgets = models.JSONField(default=list, blank=True) commands_schema = models.JSONField(default=list, blank=True) capabilities = models.JSONField(default=list, blank=True) ``` ### توضیح هر فیلد #### `payload_mapping` مشخص می‌کند payload خام این device چطور به فیلدهای استاندارد سیستم map شود. مثال: ```json { "soil_moisture": ["soil_moisture", "soilMoisture", "moisture"], "soil_temperature": ["soil_temperature", "soilTemperature", "temperature"], "soil_ph": ["soil_ph", "soilPh", "ph"] } ``` #### `display_schema` مشخص می‌کند کدام فیلدها در UI نمایش داده شوند و label و unit آن‌ها چیست. مثال: ```json { "fields": [ { "id": "soil_moisture", "label": "رطوبت خاک", "unit": "%", "ideal_min": 45, "ideal_max": 65 }, { "id": "soil_temperature", "label": "دمای خاک", "unit": "°C", "ideal_min": 18, "ideal_max": 28 } ] } ``` #### `supported_widgets` مشخص می‌کند برای این device چه widgetهایی فعال باشند. مثال: ```json [ "values_list", "comparison_chart", "radar_chart", "latest_payload", "anomaly_card" ] ``` #### `commands_schema` برای deviceهایی که `input_only` هستند. مثال: ```json [ { "command": "turn_on", "label": "روشن کردن", "payload_schema": { "duration_seconds": "integer" } }, { "command": "turn_off", "label": "خاموش کردن", "payload_schema": {} } ] ``` #### `capabilities` فهرست capabilityهای device: ```json ["measure", "history", "alert", "command"] ``` --- ## برای deviceهای ورودی‌محور شما گفتی بعضی deviceها فقط باید دستور بگیرند و خروجی نمی‌دهند. این دقیقا باید در مدل و API مشخص باشد. برای این نوع device: - `device_communication_type = "input_only"` - `returned_data_fields = []` - `supported_widgets = []` - `commands_schema` باید پر باشد API پیشنهادی: ```http POST /api/device-hub/devices/{physical_device_uuid}/commands/ ``` payload نمونه: ```json { "command": "turn_on", "payload": { "duration_seconds": 120 } } ``` پاسخ نمونه: ```json { "code": 200, "msg": "command accepted", "data": { "physical_device_uuid": "device-uuid", "command": "turn_on", "status": "queued" } } ``` --- ## چه جاهایی باید در پروژه تغییر کند ### 1) حذف وابستگی به `sensor_7_in_1` #### فایل‌هایی که باید refactor شوند - `device_hub/views.py` - `device_hub/services.py` - `device_hub/sensor_serializers.py` - `device_hub/sensor_7_in_1_urls.py` - `device_hub/comparison_urls.py` - `device_hub/urls.py` #### چه چیزی باید تغییر کند - viewهای device-specific حذف شوند - routeهای generic جایگزین شوند - serviceهای `get_sensor_7_in_1_*` به serviceهای generic تبدیل شوند --- ### 2) ساخت service عمومی برای پیدا کردن device در `device_hub/services.py` باید این لایه‌ها ایجاد شود: #### الف) resolver ```python get_farm_device_by_physical_uuid(physical_device_uuid) get_device_catalog_for_farm_device(farm_device) get_latest_device_log(farm_device) get_device_logs(farm_device, range_value=None) ``` #### ب) normalizer ```python normalize_device_payload(device_catalog, payload) extract_device_readings(device_catalog, payload) ``` این بخش باید از `payload_mapping` استفاده کند، نه از `SENSOR_FIELDS` ثابت. #### ج) presenter / builder ```python build_device_summary(farm_device) build_device_values_list(farm_device, range_value) build_device_comparison_chart(farm_device, range_value) build_device_radar_chart(farm_device, range_value) ``` --- ### 3) ثابت‌های hard-coded باید از کد خارج شوند الان این موارد hard-coded هستند: - `SENSOR_FIELDS` - `COMPARISON_CHART_FIELD_ALIASES` - `VALUES_LIST_FIELDS` - `RADAR_CHART_FIELDS` این‌ها الان در: - `device_hub/services.py:16` هستند و باید به config وابسته به `DeviceCatalog` منتقل شوند. یعنی: - به‌جای constant سراسری - از `device_catalog.display_schema` - و `device_catalog.payload_mapping` استفاده شود. --- ### 4) serializerهای اختصاصی باید generic شوند الان در: - `device_hub/sensor_serializers.py:6` serializerها مخصوص 7-in-1 هستند. باید این‌ها جایگزین شوند: - `DeviceMetaSerializer` - `DeviceFieldValueSerializer` - `DeviceValuesListSerializer` - `DeviceSummarySerializer` - `DeviceComparisonChartSerializer` - `DeviceRadarChartSerializer` یعنی نام serializer نباید به یک device خاص گره خورده باشد. --- ### 5) endpointهای generic بسازید در `device_hub/urls.py` بهتر است چیزی شبیه این داشته باشید: ```python urlpatterns = [ path("catalog/", DeviceCatalogListView.as_view(), name="device-catalog-list"), path("devices//", DeviceDetailView.as_view(), name="device-detail"), path("devices//latest/", DeviceLatestPayloadView.as_view(), name="device-latest-payload"), path("devices//summary/", DeviceSummaryView.as_view(), name="device-summary"), path("devices//values-list/", DeviceValuesListView.as_view(), name="device-values-list"), path("devices//comparison-chart/", DeviceComparisonChartView.as_view(), name="device-comparison-chart"), path("devices//radar-chart/", DeviceRadarChartView.as_view(), name="device-radar-chart"), path("devices//logs/", DeviceLogListView.as_view(), name="device-log-list"), path("devices//commands/", DeviceCommandView.as_view(), name="device-command"), path("external/", SensorExternalAPIView.as_view(), name="sensor-external-api"), ] ``` --- ## روند اضافه کردن device جدید بدون تغییر کد بعد از این refactor، اضافه کردن device جدید باید این‌طوری باشد: ### مرحله 1 یک رکورد جدید در `DeviceCatalog` ایجاد شود. ### مرحله 2 این اطلاعات برایش ثبت شود: - `code` - `name` - `device_communication_type` - `payload_mapping` - `display_schema` - `supported_widgets` - `commands_schema` ### مرحله 3 هنگام ثبت `FarmDevice`، آن device به همین catalog وصل شود. ### مرحله 4 از این به بعد frontend فقط با `physical_device_uuid` به endpointهای generic می‌زند. بدون تغییر کد. --- ## نمونه config برای یک سنسور خروجی‌محور ```json { "code": "soil_sensor_v2", "name": "Soil Sensor V2", "device_communication_type": "output_only", "returned_data_fields": [ "soil_moisture", "soil_temperature", "soil_ph" ], "payload_mapping": { "soil_moisture": ["moisture", "soil_moisture"], "soil_temperature": ["temperature", "soil_temperature"], "soil_ph": ["ph", "soil_ph"] }, "display_schema": { "fields": [ { "id": "soil_moisture", "label": "رطوبت خاک", "unit": "%", "ideal_min": 45, "ideal_max": 65 }, { "id": "soil_temperature", "label": "دمای خاک", "unit": "°C", "ideal_min": 18, "ideal_max": 28 }, { "id": "soil_ph", "label": "PH خاک", "unit": "pH", "ideal_min": 6, "ideal_max": 7.5 } ] }, "supported_widgets": [ "values_list", "comparison_chart", "radar_chart", "latest_payload" ], "commands_schema": [] } ``` --- ## نمونه config برای یک device فقط ورودی مثلا شیر برقی یا پمپ: ```json { "code": "irrigation_valve_v1", "name": "Irrigation Valve V1", "device_communication_type": "input_only", "returned_data_fields": [], "payload_mapping": {}, "display_schema": { "fields": [] }, "supported_widgets": [], "commands_schema": [ { "command": "open", "label": "باز کردن شیر", "payload_schema": { "duration_seconds": "integer" } }, { "command": "close", "label": "بستن شیر", "payload_schema": {} } ] } ``` --- ## پیشنهاد مرحله‌بندی پیاده‌سازی ### فاز 1: Generic read API اول این‌ها را بسازید: - `DeviceDetailView` - `DeviceLatestPayloadView` - `DeviceSummaryView` - `DeviceValuesListView` - `DeviceComparisonChartView` - `DeviceRadarChartView` و فعلا داده را با fallback از منطق فعلی بسازید. ### فاز 2: Config-driven normalization بعد: - `payload_mapping` - `display_schema` - `supported_widgets` را به `DeviceCatalog` اضافه کنید و منطق hard-coded را حذف کنید. ### فاز 3: Command API برای `input_only` deviceها: - `DeviceCommandView` - command validation - queue / external broker integration ### فاز 4: Admin / CMS support برای اینکه بدون کد device جدید اضافه شود، باید از طریق: - Django Admin یا - پنل داخلی بتوانید `DeviceCatalog` را مدیریت کنید. --- ## حداقل تغییر‌هایی که همین الان باید انجام بدهید اگر بخواهی با کمترین تغییر از ساختار فعلی به ساختار بهتر برسی، این‌ها مهم‌ترین کارها هستند: ### ضروری 1. حذف endpointهای `sensor_7_in_1`-محور 2. ساخت endpointهای generic با `physical_device_uuid` 3. جدا کردن منطق extraction از device-specific code 4. انتقال field mapping از constant به دیتابیس 5. اضافه کردن schema برای commandها ### مهم ولی فاز بعدی 1. admin برای `DeviceCatalog` 2. validation قوی برای `payload_mapping` 3. caching برای summary/chartها 4. swagger dynamic docs برای command schema --- ## جمع‌بندی اگر هدفت این است که: - device جدید بدون تغییر کد اضافه شود - frontend فقط با `device_uuid` کار کند - بعضی deviceها فقط command بگیرند - بعضی deviceها telemetry بدهند پس باید طراحی از: - `device-specific code` به این مدل تغییر کند: - `catalog-driven architecture` یعنی: - `DeviceCatalog` منبع حقیقت باشد - APIها generic باشند - parsing و rendering بر اساس config انجام شود - commandها هم از schema خود device خوانده شوند --- ## فایل‌های کلیدی برای refactor - `device_hub/models.py:6` - `device_hub/views.py:19` - `device_hub/services.py:16` - `device_hub/sensor_serializers.py:1` - `device_hub/urls.py:1` - `device_hub/sensor_7_in_1_urls.py:1` - `device_hub/comparison_urls.py:1` - `device_hub/seeds.py:12` --- ## پیشنهاد نهایی بهترین مسیر این است که: 1. endpointهای generic را اضافه کنی 2. endpointهای قدیمی `sensor_7_in_1` را deprecated کنی 3. config مورد نیاز را به `DeviceCatalog` اضافه کنی 4. frontend را به `physical_device_uuid`-based API منتقل کنی اگر خواستی، در مرحله بعد من می‌توانم همین طراحی را به تسک اجرایی تبدیل کنم و دقیقا بگویم: - چه model fieldهایی اضافه شوند - چه serializerهایی ساخته شوند - چه endpointهایی پیاده شوند - و refactor را در چه ترتیب انجام بدهی