Files
Backend/docs/device_catalog_dynamic_architecture.md
T
2026-05-05 01:32:27 +03:30

17 KiB

راهنمای طراحی 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) لیست دیوایس‌ها

GET /api/device-hub/catalog/

کاربرد:

  • لیست همه device catalogها
  • metadata هر catalog
  • نوع ارتباط device
  • فیلدهای قابل نمایش

2) جزئیات یک دیوایس ثبت‌شده روی مزرعه

GET /api/device-hub/devices/{physical_device_uuid}/

پاسخ نمونه:

{
  "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

GET /api/device-hub/devices/{physical_device_uuid}/latest/

کاربرد:

  • آخرین payload خام
  • آخرین payload نرمال‌شده
  • آخرین readingهای قابل نمایش

4) summary داینامیک برای یک device

GET /api/device-hub/devices/{physical_device_uuid}/summary/

کاربرد:

  • به‌جای sensor_7_in_1/summary
  • خروجی بر اساس config همان device

5) نمودار مقایسه‌ای داینامیک

GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?range=7d

6) نمودار رادار داینامیک

GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?range=7d

7) values list داینامیک

GET /api/device-hub/devices/{physical_device_uuid}/values-list/?range=7d

8) دریافت history خام

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

فیلدهای پیشنهادی جدید:

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 شود.

مثال:

{
  "soil_moisture": ["soil_moisture", "soilMoisture", "moisture"],
  "soil_temperature": ["soil_temperature", "soilTemperature", "temperature"],
  "soil_ph": ["soil_ph", "soilPh", "ph"]
}

display_schema

مشخص می‌کند کدام فیلدها در UI نمایش داده شوند و label و unit آن‌ها چیست.

مثال:

{
  "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هایی فعال باشند.

مثال:

[
  "values_list",
  "comparison_chart",
  "radar_chart",
  "latest_payload",
  "anomaly_card"
]

commands_schema

برای deviceهایی که input_only هستند.

مثال:

[
  {
    "command": "turn_on",
    "label": "روشن کردن",
    "payload_schema": {
      "duration_seconds": "integer"
    }
  },
  {
    "command": "turn_off",
    "label": "خاموش کردن",
    "payload_schema": {}
  }
]

capabilities

فهرست capabilityهای device:

["measure", "history", "alert", "command"]

برای deviceهای ورودی‌محور

شما گفتی بعضی deviceها فقط باید دستور بگیرند و خروجی نمی‌دهند. این دقیقا باید در مدل و API مشخص باشد.

برای این نوع device:

  • device_communication_type = "input_only"
  • returned_data_fields = []
  • supported_widgets = []
  • commands_schema باید پر باشد

API پیشنهادی:

POST /api/device-hub/devices/{physical_device_uuid}/commands/

payload نمونه:

{
  "command": "turn_on",
  "payload": {
    "duration_seconds": 120
  }
}

پاسخ نمونه:

{
  "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

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

normalize_device_payload(device_catalog, payload)
extract_device_readings(device_catalog, payload)

این بخش باید از payload_mapping استفاده کند، نه از SENSOR_FIELDS ثابت.

ج) presenter / builder

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 بهتر است چیزی شبیه این داشته باشید:

urlpatterns = [
    path("catalog/", DeviceCatalogListView.as_view(), name="device-catalog-list"),
    path("devices/<uuid:physical_device_uuid>/", DeviceDetailView.as_view(), name="device-detail"),
    path("devices/<uuid:physical_device_uuid>/latest/", DeviceLatestPayloadView.as_view(), name="device-latest-payload"),
    path("devices/<uuid:physical_device_uuid>/summary/", DeviceSummaryView.as_view(), name="device-summary"),
    path("devices/<uuid:physical_device_uuid>/values-list/", DeviceValuesListView.as_view(), name="device-values-list"),
    path("devices/<uuid:physical_device_uuid>/comparison-chart/", DeviceComparisonChartView.as_view(), name="device-comparison-chart"),
    path("devices/<uuid:physical_device_uuid>/radar-chart/", DeviceRadarChartView.as_view(), name="device-radar-chart"),
    path("devices/<uuid:physical_device_uuid>/logs/", DeviceLogListView.as_view(), name="device-log-list"),
    path("devices/<uuid:physical_device_uuid>/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 برای یک سنسور خروجی‌محور

{
  "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 فقط ورودی

مثلا شیر برقی یا پمپ:

{
  "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 را در چه ترتیب انجام بدهی