Pārlūkot izejas kodu

支持S7-Smart200

Lu Xianghui 1 mēnesi atpakaļ
vecāks
revīzija
986c88bd3c

+ 31 - 5
app/schemas/s7.py

@@ -18,7 +18,6 @@ SUPPORTED_POINT_TYPES = {
     "float64",
 }
 
-
 class S7IpRequest(BaseModel):
     model_config = ConfigDict(extra="forbid")
 
@@ -35,12 +34,22 @@ class S7IpRequest(BaseModel):
 
 
 class S7BaseRequest(S7IpRequest):
-    device_type: Literal["S7TCP"] = "S7TCP"
+    device_type: Literal["S7-1200", "S7-Smart200"] = "S7-1200"
     port: int = Field(default=102, ge=1, le=65535)
     rock: int = Field(ge=0, le=31)
     slot: int = Field(ge=0, le=31)
     tsap_conn_type: Literal["PG", "OP", "BASIC"] = "PG"
 
+    @field_validator("device_type", mode="before")
+    @classmethod
+    def normalize_device_type(cls, value: str) -> str:
+        normalized = str(value).upper()
+        if normalized == "S7-1200":
+            return "S7-1200"
+        if normalized == "S7-SMART200":
+            return "S7-Smart200"
+        return str(value)
+
     @field_validator("tsap_conn_type", mode="before")
     @classmethod
     def normalize_tsap_conn_type(cls, value: str) -> str:
@@ -50,7 +59,7 @@ class S7BaseRequest(S7IpRequest):
 class S7ReadSpec(BaseModel):
     model_config = ConfigDict(extra="forbid")
 
-    area: Literal["DB", "M", "I", "Q"]
+    area: Literal["DB", "M", "I", "Q", "V"]
     db: int = Field(default=0, ge=0)
     start: int = Field(ge=0)
     size: int = Field(ge=1, le=65535)
@@ -70,11 +79,17 @@ class S7ReadSpec(BaseModel):
 class S7RawReadRequest(S7BaseRequest):
     read: S7ReadSpec
 
+    @model_validator(mode="after")
+    def validate_v_area(self) -> "S7RawReadRequest":
+        if self.device_type != "S7-Smart200" and self.read.area == "V":
+            raise ValueError("V area is only supported when device_type is S7-Smart200")
+        return self
+
 
 class S7PointSpec(BaseModel):
     model_config = ConfigDict(extra="forbid")
 
-    area: Literal["DB", "M", "I", "Q"]
+    area: Literal["DB", "M", "I", "Q", "V"]
     db: int = Field(default=0, ge=0)
     start: int = Field(ge=0)
     type: str
@@ -107,6 +122,17 @@ class S7PointSpec(BaseModel):
 class S7PointReadRequest(S7BaseRequest):
     points: list[S7PointSpec] = Field(min_length=1)
 
+    @model_validator(mode="after")
+    def validate_v_area(self) -> "S7PointReadRequest":
+        if self.device_type != "S7-Smart200" and any(point.area == "V" for point in self.points):
+            raise ValueError("V area is only supported when device_type is S7-Smart200")
+        return self
+
 
 class S7ConnectScanRequest(S7IpRequest):
-    pass
+    device_type: Literal["S7-1200", "S7-Smart200"] = "S7-1200"
+
+    @field_validator("device_type", mode="before")
+    @classmethod
+    def normalize_device_type(cls, value: str) -> str:
+        return S7BaseRequest.normalize_device_type(value)

+ 3 - 1
app/services/s7_service.py

@@ -60,7 +60,7 @@ def device_payload(request: S7RawReadRequest | S7PointReadRequest) -> dict[str,
 
 
 def connect_scan_device_payload(request: S7ConnectScanRequest) -> dict[str, Any]:
-    return {"device_type": "S7TCP", "ip": request.ip, "port": 102}
+    return {"device_type": request.device_type, "ip": request.ip, "port": 102}
 
 
 def connect_client(ip: str, port: int, rock: int, slot: int, tsap_conn_type: str) -> Client:
@@ -104,6 +104,8 @@ def pdu_length_message(client: Client) -> str:
 
 
 def read_s7_bytes(client: Client, read: S7ReadSpec | S7PointSpec, size: int) -> bytearray:
+    if read.area == "V":
+        return client.db_read(1, read.start, size)
     if read.area == "DB":
         return client.db_read(read.db, read.start, size)
     return client.read_area(AREA_CODES[read.area], 0, read.start, size)

+ 6 - 5
docs/localhost-data-colllector-gateway.md

@@ -82,7 +82,7 @@ AI 调用接口时应按以下规则判断结果:
 | 点位读取并转换别名 | `POST` | `/api/dc-gateway/modbus/read-points` | 与 `/api/dc-gateway/modbus/read_points` 等价,建议优先使用下划线版本。 |
 | 原始 S7 读取 | `POST` | `/api/dc-gateway/s7/read` | 读取 S7 原始字节,只返回语义化 `communication`。 |
 | S7 点位读取并转换 | `POST` | `/api/dc-gateway/s7/read_points` | 按点位定义读取 S7 数据,并按数据类型转换为业务值。 |
-| S7 连接扫描 | `POST` | `/api/dc-gateway/s7/connect_scan` | 传 IP,扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。 |
+| S7 连接扫描 | `POST` | `/api/dc-gateway/s7/connect_scan` | 传 IP,可选 `device_type`,扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。 |
 
 ## S7 接口快速说明
 
@@ -92,7 +92,7 @@ S7 通用设备字段:
 
 | 字段 | 类型 | 必填 | 默认值 | 允许值或范围 | 含义 |
 |---|---|---:|---|---|---|
-| `device_type` | string | 否 | `S7TCP` | 只能是 `S7TCP` | 设备协议类型。 |
+| `device_type` | string | 否 | `S7-1200` | `S7-1200`、`S7-Smart200` | S7 设备类型。两者连接方式均为 S7 TCP,`S7-Smart200` 额外支持 `V` 区。 |
 | `ip` | string | 是 | 无 | 合法 IPv4 或 IPv6 地址 | S7 设备 IP 地址。 |
 | `port` | integer | 否 | `102` | `1..65535` | S7 TCP 端口,通常固定为 `102`。 |
 | `rock` | integer | 是 | 无 | `0..31` | PLC rock。 |
@@ -103,8 +103,8 @@ S7 地址字段:
 
 | 字段 | 类型 | 必填 | 默认值 | 允许值或范围 | 含义 |
 |---|---|---:|---|---|---|
-| `area` | string | 是 | 无 | `DB`、`M`、`I`、`Q` | 读取区域。 |
-| `db` | integer | `area=DB` 时必填 | `0` | `>=0` | DB 块号,`area=DB` 时必须大于 `0`。 |
+| `area` | string | 是 | 无 | `DB`、`M`、`I`、`Q`、`V` | 读取区域。`V` 仅支持 `device_type=S7-Smart200`。 |
+| `db` | integer | `area=DB` 时必填 | `0` | `>=0` | DB 块号,`area=DB` 时必须大于 `0`;`area=V` 时内部按 `DB1` 读取。 |
 | `start` | integer | 是 | 无 | `>=0` | 起始字节偏移。 |
 | `size` | integer | `/s7/read` 必填 | 无 | `1..65535` | 读取字节数。 |
 
@@ -118,6 +118,7 @@ POST /api/dc-gateway/s7/connect_scan
 
 ```json
 {
+  "device_type": "S7-Smart200",
   "ip": "192.168.1.10"
 }
 ```
@@ -130,7 +131,7 @@ POST /api/dc-gateway/s7/connect_scan
   "msg": "success",
   "data": {
     "device": {
-      "device_type": "S7TCP",
+      "device_type": "S7-Smart200",
       "ip": "192.168.1.10",
       "port": 102
     },

+ 13 - 11
docs/s7-http-api.md

@@ -10,7 +10,7 @@
 
 - `POST /api/dc-gateway/s7/read`:读取一段 S7 地址数据,只返回语义化 `communication`。
 - `POST /api/dc-gateway/s7/read_points`:按点位定义读取,并返回转换后的业务值。
-- `POST /api/dc-gateway/s7/connect_scan`:传 `ip`,扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。
+- `POST /api/dc-gateway/s7/connect_scan`:传 `ip`,可选 `device_type`,扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。
 
 ## 通用返回约定
 
@@ -42,7 +42,7 @@
 
 | 字段 | 是否必传 | 默认值 | 校验 | 含义 |
 |---|---:|---|---|---|
-| `device_type` | 否 | `S7TCP` | 只能是 `S7TCP` | 设备协议类型。 |
+| `device_type` | 否 | `S7-1200` | `S7-1200`、`S7-Smart200` | S7 设备类型。两者连接方式均为 S7 TCP,`S7-Smart200` 额外支持 `V` 区。 |
 | `ip` | 是 | 无 | 合法 IP 地址 | S7 设备 IP 地址。 |
 | `port` | 否 | `102` | `1..65535` | S7 TCP 端口,通常固定为 `102`。 |
 | `rock` | 是 | 无 | `0..31` | PLC rock。 |
@@ -63,8 +63,8 @@
 
 | 字段 | 是否必传 | 默认值 | 校验 | 含义 |
 |---|---:|---|---|---|
-| `area` | 是 | 无 | `DB`、`M`、`I`、`Q` | 读取区域。 |
-| `db` | `area=DB` 时必填 | `0` | `>=0` | DB 块号。`area=DB` 时必须大于 `0`。 |
+| `area` | 是 | 无 | `DB`、`M`、`I`、`Q`、`V` | 读取区域。`V` 仅支持 `device_type=S7-Smart200`。 |
+| `db` | `area=DB` 时必填 | `0` | `>=0` | DB 块号。`area=DB` 时必须大于 `0`;`area=V` 时内部按 `DB1` 读取。 |
 | `start` | 是 | 无 | `>=0` | 起始字节偏移。 |
 | `size` | `/s7/read` 必填 | 无 | `1..65535` | 读取字节数。 |
 
@@ -116,7 +116,7 @@ POST /api/dc-gateway/s7/read
 
 ```json
 {
-  "device_type": "S7TCP",
+  "device_type": "S7-1200",
   "ip": "192.168.1.10",
   "port": 102,
   "rock": 0,
@@ -139,7 +139,7 @@ POST /api/dc-gateway/s7/read
   "msg": "success",
   "data": {
     "device": {
-      "device_type": "S7TCP",
+      "device_type": "S7-1200",
       "ip": "192.168.1.10",
       "port": 102,
       "rock": 0,
@@ -166,7 +166,7 @@ POST /api/dc-gateway/s7/read
   "msg": "TCP : Unreachable peer",
   "data": {
     "device": {
-      "device_type": "S7TCP",
+      "device_type": "S7-1200",
       "ip": "192.168.1.10",
       "port": 102,
       "rock": 0,
@@ -193,7 +193,7 @@ POST /api/dc-gateway/s7/read_points
 
 ```json
 {
-  "device_type": "S7TCP",
+  "device_type": "S7-1200",
   "ip": "192.168.1.10",
   "port": 102,
   "rock": 0,
@@ -243,7 +243,7 @@ S7 多字节数值按大端字节序解析。
   "msg": "success",
   "data": {
     "device": {
-      "device_type": "S7TCP",
+      "device_type": "S7-1200",
       "ip": "192.168.1.10",
       "port": 102,
       "rock": 0,
@@ -285,6 +285,7 @@ POST /api/dc-gateway/s7/connect_scan
 
 ```json
 {
+  "device_type": "S7-Smart200",
   "ip": "192.168.1.10"
 }
 ```
@@ -307,7 +308,7 @@ POST /api/dc-gateway/s7/connect_scan
   "msg": "success",
   "data": {
     "device": {
-      "device_type": "S7TCP",
+      "device_type": "S7-Smart200",
       "ip": "192.168.1.10",
       "port": 102
     },
@@ -335,7 +336,7 @@ POST /api/dc-gateway/s7/connect_scan
   "msg": "success",
   "data": {
     "device": {
-      "device_type": "S7TCP",
+      "device_type": "S7-Smart200",
       "ip": "192.168.1.10",
       "port": 102
     },
@@ -350,6 +351,7 @@ POST /api/dc-gateway/s7/connect_scan
 curl -X POST http://127.0.0.1:8000/api/dc-gateway/s7/connect_scan \
   -H "Content-Type: application/json" \
   -d '{
+    "device_type": "S7-Smart200",
     "ip": "192.168.1.10"
   }'
 ```

+ 2 - 2
intergration/s7/common.py

@@ -124,9 +124,9 @@ def patch_connect_scan(successful: set[tuple[int, int, str]]):
     return patch("app.services.s7_service.connect_client", side_effect=fake_connect_client)
 
 
-def s7_device_payload(port: int) -> dict:
+def s7_device_payload(port: int, device_type: str = "S7-1200") -> dict:
     return {
-        "device_type": "S7TCP",
+        "device_type": device_type,
         "ip": "127.0.0.1",
         "port": port,
         "rock": 0,

+ 12 - 0
intergration/s7/test_s7_all_type_points.py

@@ -82,6 +82,18 @@ class S7AllTypePointsIntegrationTest(unittest.TestCase):
             self.assert_area_values(data["data"]["points"], area)
         self.assertNotIn("communication", data["data"])
 
+    def test_read_points_maps_smart200_v_area_to_db1(self):
+        memory = build_all_type_memory()
+        with Snap7TestServer(db_data=memory) as s7_server, ApiTestServer() as api_server:
+            payload = {
+                **s7_device_payload(s7_server.port, device_type="S7-Smart200"),
+                "points": point_specs_for_area("V"),
+            }
+
+            status, data = api_server.post_json("/api/dc-gateway/s7/read_points", payload)
+
+        self.assert_all_type_response(status, data, payload, "V")
+
     def assert_all_type_response(self, status: int, data: dict, payload: dict, area: str) -> None:
         assert_response_contract(self, status, data)
         self.assertEqual(0, data["code"], data)

+ 1 - 1
intergration/s7/test_s7_api.py

@@ -65,7 +65,7 @@ class S7ApiIntegrationTest(unittest.TestCase):
 
         assert_response_contract(self, status, data)
         self.assertEqual(0, data["code"], data)
-        self.assertEqual({"device_type": "S7TCP", "ip": "127.0.0.1", "port": 102}, data["data"]["device"])
+        self.assertEqual({"device_type": "S7-1200", "ip": "127.0.0.1", "port": 102}, data["data"]["device"])
         self.assertEqual(
             [
                 {"rock": 0, "slot": 1, "tsap_conn_type": "PG"},

+ 25 - 1
intergration/s7/test_s7_schema.py

@@ -14,7 +14,7 @@ class S7SchemaTest(unittest.TestCase):
             read={"area": "db", "db": 1, "start": 0, "size": 4},
         )
 
-        self.assertEqual("S7TCP", request.device_type)
+        self.assertEqual("S7-1200", request.device_type)
         self.assertEqual(102, request.port)
         self.assertEqual("PG", request.tsap_conn_type)
         self.assertEqual("DB", request.read.area)
@@ -31,6 +31,30 @@ class S7SchemaTest(unittest.TestCase):
         self.assertEqual("BASIC", request.tsap_conn_type)
         self.assertEqual("M", request.read.area)
 
+    def test_device_type_is_case_insensitive(self):
+        request = S7RawReadRequest(
+            device_type="s7-smart200",
+            ip="127.0.0.1",
+            rock=0,
+            slot=1,
+            read={"area": "v", "start": 0, "size": 1},
+        )
+
+        self.assertEqual("S7-Smart200", request.device_type)
+        self.assertEqual("V", request.read.area)
+
+    def test_v_area_requires_smart200_device_type(self):
+        with self.assertRaises(ValidationError) as context:
+            S7RawReadRequest(
+                device_type="S7-1200",
+                ip="127.0.0.1",
+                rock=0,
+                slot=1,
+                read={"area": "V", "start": 0, "size": 1},
+            )
+
+        self.assertIn("V area is only supported", str(context.exception))
+
     def test_db_area_requires_positive_db_number(self):
         with self.assertRaises(ValidationError) as context:
             S7RawReadRequest(