opensignalbox-interface 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opensignalbox/interface/__init__.py +42 -0
- opensignalbox/interface/adapters/__init__.py +3 -0
- opensignalbox/interface/adapters/base.py +148 -0
- opensignalbox/interface/adapters/controller.py +286 -0
- opensignalbox/interface/adapters/modbus/__init__.py +3 -0
- opensignalbox/interface/adapters/modbus/adapter.py +485 -0
- opensignalbox/interface/assets/favicon.ico +0 -0
- opensignalbox/interface/interfaces/__init__.py +3 -0
- opensignalbox/interface/interfaces/base.py +170 -0
- opensignalbox/interface/interfaces/basicio/__init__.py +3 -0
- opensignalbox/interface/interfaces/basicio/connections.py +239 -0
- opensignalbox/interface/interfaces/basicio/handler.py +1096 -0
- opensignalbox/interface/interfaces/basicio/models.py +165 -0
- opensignalbox/interface/interfaces/bell/__init__.py +3 -0
- opensignalbox/interface/interfaces/bell/connections.py +279 -0
- opensignalbox/interface/interfaces/bell/handler.py +802 -0
- opensignalbox/interface/interfaces/bell/models.py +103 -0
- opensignalbox/interface/interfaces/controller.py +467 -0
- opensignalbox/interface/main.py +187 -0
- opensignalbox/interface/models.py +155 -0
- opensignalbox/interface/routes/__init__.py +4 -0
- opensignalbox/interface/routes/adapters.py +119 -0
- opensignalbox/interface/routes/interfaces.py +104 -0
- opensignalbox/interface/routes.py +204 -0
- opensignalbox/interface/utils.py +29 -0
- opensignalbox/interface/version.py +5 -0
- opensignalbox/interface/web_ui/.gitignore +24 -0
- opensignalbox/interface/web_ui/README.md +5 -0
- opensignalbox/interface/web_ui/WEB_UI_REFACTORING.md +136 -0
- opensignalbox/interface/web_ui/components.json +17 -0
- opensignalbox/interface/web_ui/dist/assets/index-DhsEc6uo.css +2020 -0
- opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js +39056 -0
- opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js.map +1 -0
- opensignalbox/interface/web_ui/dist/index.html +14 -0
- opensignalbox/interface/web_ui/index.html +13 -0
- opensignalbox/interface/web_ui/package-lock.json +3710 -0
- opensignalbox/interface/web_ui/package.json +39 -0
- opensignalbox/interface/web_ui/src/App.vue +32 -0
- opensignalbox/interface/web_ui/src/assets/favicon.ico +0 -0
- opensignalbox/interface/web_ui/src/assets/index.css +83 -0
- opensignalbox/interface/web_ui/src/components/SharedVariablePicker.vue +112 -0
- opensignalbox/interface/web_ui/src/components/adapters/AdapterList.vue +183 -0
- opensignalbox/interface/web_ui/src/components/adapters/types.ts +35 -0
- opensignalbox/interface/web_ui/src/components/interfaces/InterfaceList.vue +200 -0
- opensignalbox/interface/web_ui/src/components/ui/badge/Badge.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/badge/index.ts +25 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbItem.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbLink.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbList.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbPage.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/index.ts +7 -0
- opensignalbox/interface/web_ui/src/components/ui/button/Button.vue +26 -0
- opensignalbox/interface/web_ui/src/components/ui/button/index.ts +35 -0
- opensignalbox/interface/web_ui/src/components/ui/card/Card.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardContent.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardDescription.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardFooter.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardHeader.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardTitle.vue +18 -0
- opensignalbox/interface/web_ui/src/components/ui/card/index.ts +6 -0
- opensignalbox/interface/web_ui/src/components/ui/checkbox/Checkbox.vue +33 -0
- opensignalbox/interface/web_ui/src/components/ui/checkbox/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/Collapsible.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/index.ts +3 -0
- opensignalbox/interface/web_ui/src/components/ui/command/Command.vue +30 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandDialog.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandEmpty.vue +20 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandGroup.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandInput.vue +33 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandItem.vue +26 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandList.vue +27 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandSeparator.vue +23 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandShortcut.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/command/index.ts +9 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/Dialog.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogClose.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogContent.vue +50 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogDescription.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogFooter.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogHeader.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogScrollContent.vue +59 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTitle.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/index.ts +9 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue +38 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +41 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/index.ts +16 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormControl.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormDescription.vue +20 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormItem.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormLabel.vue +23 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormMessage.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/form/index.ts +7 -0
- opensignalbox/interface/web_ui/src/components/ui/form/injectionKeys.ts +4 -0
- opensignalbox/interface/web_ui/src/components/ui/form/useFormField.ts +30 -0
- opensignalbox/interface/web_ui/src/components/ui/input/Input.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/input/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/label/Label.vue +27 -0
- opensignalbox/interface/web_ui/src/components/ui/label/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberField.vue +23 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldContent.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldInput.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/index.ts +5 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/Popover.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/PopoverContent.vue +48 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/PopoverTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/index.ts +3 -0
- opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroup.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
- opensignalbox/interface/web_ui/src/components/ui/radio-group/index.ts +2 -0
- opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollArea.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollBar.vue +30 -0
- opensignalbox/interface/web_ui/src/components/ui/scroll-area/index.ts +2 -0
- opensignalbox/interface/web_ui/src/components/ui/select/Select.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectContent.vue +53 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectGroup.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectItem.vue +44 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectItemText.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectLabel.vue +13 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollDownButton.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollUpButton.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectSeparator.vue +17 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectTrigger.vue +31 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectValue.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/select/index.ts +11 -0
- opensignalbox/interface/web_ui/src/components/ui/separator/Separator.vue +35 -0
- opensignalbox/interface/web_ui/src/components/ui/separator/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/Sheet.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetClose.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetContent.vue +56 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetDescription.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetFooter.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetHeader.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTitle.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/index.ts +31 -0
- opensignalbox/interface/web_ui/src/components/ui/table/Table.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableBody.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableCaption.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableCell.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableEmpty.vue +37 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableFooter.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableHead.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableHeader.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableRow.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/index.ts +9 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/Tabs.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/TabsContent.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/TabsList.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/TabsTrigger.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/index.ts +4 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInput.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputInput.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItem.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/index.ts +5 -0
- opensignalbox/interface/web_ui/src/components/ui/textarea/Textarea.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/textarea/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/Tooltip.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipContent.vue +31 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipProvider.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/index.ts +4 -0
- opensignalbox/interface/web_ui/src/libs/utils.ts +6 -0
- opensignalbox/interface/web_ui/src/main.ts +141 -0
- opensignalbox/interface/web_ui/src/views/Overview.vue +11 -0
- opensignalbox/interface/web_ui/src/views/Settings.vue +6 -0
- opensignalbox/interface/web_ui/src/views/SleafordEast.vue +205 -0
- opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterEdit.vue +343 -0
- opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterView.vue +270 -0
- opensignalbox/interface/web_ui/src/views/interfaces/InterfaceCreate.vue +0 -0
- opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceEdit.vue +795 -0
- opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceView.vue +648 -0
- opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceEdit.vue +790 -0
- opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceView.vue +437 -0
- opensignalbox/interface/web_ui/src/vite-env.d.ts +1 -0
- opensignalbox/interface/web_ui/tailwind.config.js +94 -0
- opensignalbox/interface/web_ui/tsconfig.app.json +27 -0
- opensignalbox/interface/web_ui/tsconfig.json +30 -0
- opensignalbox/interface/web_ui/tsconfig.node.json +12 -0
- opensignalbox/interface/web_ui/tsconfig.tsbuildinfo +1 -0
- opensignalbox/interface/web_ui/vite.config.d.ts +2 -0
- opensignalbox/interface/web_ui/vite.config.js +60 -0
- opensignalbox/interface/web_ui/vite.config.ts +62 -0
- opensignalbox_interface-0.1.0.dist-info/METADATA +49 -0
- opensignalbox_interface-0.1.0.dist-info/RECORD +209 -0
- opensignalbox_interface-0.1.0.dist-info/WHEEL +4 -0
- opensignalbox_interface-0.1.0.dist-info/entry_points.txt +2 -0
- opensignalbox_interface-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modbus adapter implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import socket
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
import serial
|
|
10
|
+
from opensignalbox.interface.adapters.base import BaseAdapter
|
|
11
|
+
from opensignalbox.interface.models import AdapterTypeEnum
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
SERIAL_TIMEOUT = 1 # seconds
|
|
16
|
+
NETWORK_TIMEOUT = 1 # seconds
|
|
17
|
+
STATUS_UPDATE_PERIOD = 1 # seconds
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def modbus_crc(msg: bytearray) -> int:
|
|
21
|
+
"""
|
|
22
|
+
Calculate Modbus CRC for a message.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
msg: The message to calculate CRC for
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The calculated CRC
|
|
29
|
+
"""
|
|
30
|
+
crc = 0xFFFF
|
|
31
|
+
for n in range(len(msg)):
|
|
32
|
+
crc ^= msg[n]
|
|
33
|
+
for _ in range(8):
|
|
34
|
+
if crc & 1:
|
|
35
|
+
crc >>= 1
|
|
36
|
+
crc ^= 0xA001
|
|
37
|
+
else:
|
|
38
|
+
crc >>= 1
|
|
39
|
+
return crc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ModbusAdapter(BaseAdapter):
|
|
43
|
+
"""Adapter for Modbus communication."""
|
|
44
|
+
|
|
45
|
+
# Metadata for the API discovery
|
|
46
|
+
metadata = {
|
|
47
|
+
"supported_connection_types": [
|
|
48
|
+
"BasicIOModbusConnection",
|
|
49
|
+
"BellModbusConnection",
|
|
50
|
+
],
|
|
51
|
+
"required_fields": ["systemName", "userName", "isNetwork"],
|
|
52
|
+
"optional_fields": [
|
|
53
|
+
"description",
|
|
54
|
+
"baud",
|
|
55
|
+
"address",
|
|
56
|
+
"serialPort",
|
|
57
|
+
"networkPort",
|
|
58
|
+
"enabled",
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get_router(cls) -> Any:
|
|
64
|
+
"""Get API router for Modbus adapter endpoints.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
FastAPI router with Modbus adapter specific endpoints
|
|
68
|
+
"""
|
|
69
|
+
from fastapi import APIRouter, HTTPException
|
|
70
|
+
from opensignalbox.interface.models import (
|
|
71
|
+
AdapterBase,
|
|
72
|
+
AdapterCreate,
|
|
73
|
+
AdapterPublic,
|
|
74
|
+
AdapterUpdate,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
router = APIRouter(tags=["adapters/modbus"])
|
|
78
|
+
|
|
79
|
+
@router.get("/{system_name}")
|
|
80
|
+
async def get_adapter(system_name: str):
|
|
81
|
+
"""Get a Modbus adapter by system name."""
|
|
82
|
+
if not cls.adapter_controller:
|
|
83
|
+
raise HTTPException(
|
|
84
|
+
status_code=500, detail="Adapter controller not initialized"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
adapter = cls.adapter_controller.get(system_name)
|
|
89
|
+
if adapter.adapter_type != "Modbus":
|
|
90
|
+
raise HTTPException(
|
|
91
|
+
status_code=404,
|
|
92
|
+
detail=f"Modbus adapter '{system_name}' not found",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Convert BaseAdapter to AdapterPublic
|
|
96
|
+
adapter_dict = {
|
|
97
|
+
"system_name": adapter.system_name,
|
|
98
|
+
"user_name": adapter.user_name,
|
|
99
|
+
"description": adapter.description,
|
|
100
|
+
"adapter_type": adapter.adapter_type,
|
|
101
|
+
"enabled": adapter.enabled,
|
|
102
|
+
"error": adapter.error,
|
|
103
|
+
"is_connected": adapter.is_connected,
|
|
104
|
+
"is_network": adapter.is_network,
|
|
105
|
+
"serial_port": adapter.serial_port,
|
|
106
|
+
"baud": adapter.baud,
|
|
107
|
+
"address": adapter.address,
|
|
108
|
+
"network_port": adapter.network_port,
|
|
109
|
+
}
|
|
110
|
+
return AdapterPublic(**adapter_dict)
|
|
111
|
+
except KeyError as exc:
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=404, detail=f"Adapter '{system_name}' not found"
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
@router.post("/")
|
|
117
|
+
async def create_adapter(adapter: AdapterCreate):
|
|
118
|
+
"""Create a new Modbus adapter."""
|
|
119
|
+
if not cls.adapter_controller:
|
|
120
|
+
raise HTTPException(
|
|
121
|
+
status_code=500, detail="Adapter controller not initialized"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Extract common fields
|
|
126
|
+
adapter_base = AdapterBase(
|
|
127
|
+
**adapter.model_dump(), adapter_type=AdapterTypeEnum.MODBUS
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Create the adapter
|
|
131
|
+
new_adapter = cls.adapter_controller.add(
|
|
132
|
+
adapter_base, **adapter.model_dump()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Convert BaseAdapter to AdapterPublic
|
|
136
|
+
adapter_dict = {
|
|
137
|
+
"system_name": new_adapter.system_name,
|
|
138
|
+
"user_name": new_adapter.user_name,
|
|
139
|
+
"description": new_adapter.description,
|
|
140
|
+
"adapter_type": new_adapter.adapter_type,
|
|
141
|
+
"enabled": new_adapter.enabled,
|
|
142
|
+
"is_connected": new_adapter.is_connected,
|
|
143
|
+
}
|
|
144
|
+
return AdapterPublic(**adapter_dict)
|
|
145
|
+
except FileExistsError as exc:
|
|
146
|
+
raise HTTPException(
|
|
147
|
+
status_code=400,
|
|
148
|
+
detail=f"Adapter '{adapter.system_name}' already exists",
|
|
149
|
+
) from exc
|
|
150
|
+
except TypeError as exc:
|
|
151
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
152
|
+
|
|
153
|
+
@router.patch("/{system_name}")
|
|
154
|
+
async def update_adapter(system_name: str, adapter_update: AdapterUpdate):
|
|
155
|
+
"""Update a Modbus adapter."""
|
|
156
|
+
if not cls.adapter_controller:
|
|
157
|
+
raise HTTPException(
|
|
158
|
+
status_code=500, detail="Adapter controller not initialized"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Check that the adapter exists and is Modbus type
|
|
163
|
+
adapter = cls.adapter_controller.get(system_name)
|
|
164
|
+
if adapter.adapter_type != "Modbus":
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
status_code=404,
|
|
167
|
+
detail=f"Modbus adapter '{system_name}' not found",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Extract update fields
|
|
171
|
+
kwargs = {
|
|
172
|
+
k: v
|
|
173
|
+
for k, v in adapter_update.model_dump().items()
|
|
174
|
+
if v is not None and k != "system_name"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Update the adapter
|
|
178
|
+
updated_adapter = cls.adapter_controller.update(system_name, **kwargs)
|
|
179
|
+
|
|
180
|
+
# Convert BaseAdapter to AdapterPublic
|
|
181
|
+
adapter_dict = {
|
|
182
|
+
"system_name": updated_adapter.system_name,
|
|
183
|
+
"user_name": updated_adapter.user_name,
|
|
184
|
+
"description": updated_adapter.description,
|
|
185
|
+
"adapter_type": updated_adapter.adapter_type,
|
|
186
|
+
"enabled": updated_adapter.enabled,
|
|
187
|
+
"is_connected": updated_adapter.is_connected,
|
|
188
|
+
}
|
|
189
|
+
return AdapterPublic(**adapter_dict)
|
|
190
|
+
except KeyError as exc:
|
|
191
|
+
raise HTTPException(
|
|
192
|
+
status_code=404, detail=f"Adapter '{system_name}' not found"
|
|
193
|
+
) from exc
|
|
194
|
+
|
|
195
|
+
@router.delete("/{system_name}")
|
|
196
|
+
async def delete_adapter(system_name: str):
|
|
197
|
+
"""Delete a Modbus adapter."""
|
|
198
|
+
if not cls.adapter_controller:
|
|
199
|
+
raise HTTPException(
|
|
200
|
+
status_code=500, detail="Adapter controller not initialized"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Check that the adapter exists and is Modbus type
|
|
205
|
+
adapter = cls.adapter_controller.get(system_name)
|
|
206
|
+
if adapter.adapter_type != "Modbus":
|
|
207
|
+
raise HTTPException(
|
|
208
|
+
status_code=404,
|
|
209
|
+
detail=f"Modbus adapter '{system_name}' not found",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
cls.adapter_controller.remove(system_name)
|
|
213
|
+
return {"status": "success"}
|
|
214
|
+
except KeyError as exc:
|
|
215
|
+
raise HTTPException(
|
|
216
|
+
status_code=404, detail=f"Adapter '{system_name}' not found"
|
|
217
|
+
) from exc
|
|
218
|
+
|
|
219
|
+
@router.post("/{system_name}/connect")
|
|
220
|
+
async def connect_adapter(system_name: str):
|
|
221
|
+
"""Connect a Modbus adapter."""
|
|
222
|
+
if not cls.adapter_controller:
|
|
223
|
+
raise HTTPException(
|
|
224
|
+
status_code=500, detail="Adapter controller not initialized"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
adapter = cls.adapter_controller.get(system_name)
|
|
229
|
+
if adapter.adapter_type != "Modbus":
|
|
230
|
+
raise HTTPException(
|
|
231
|
+
status_code=404,
|
|
232
|
+
detail=f"Modbus adapter '{system_name}' not found",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
adapter.connect()
|
|
236
|
+
return {"status": "success", "connected": adapter.is_connected}
|
|
237
|
+
except KeyError as exc:
|
|
238
|
+
raise HTTPException(
|
|
239
|
+
status_code=404, detail=f"Adapter '{system_name}' not found"
|
|
240
|
+
) from exc
|
|
241
|
+
|
|
242
|
+
@router.post("/{system_name}/disconnect")
|
|
243
|
+
async def disconnect_adapter(system_name: str):
|
|
244
|
+
"""Disconnect a Modbus adapter."""
|
|
245
|
+
if not cls.adapter_controller:
|
|
246
|
+
raise HTTPException(
|
|
247
|
+
status_code=500, detail="Adapter controller not initialized"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
adapter = cls.adapter_controller.get(system_name)
|
|
252
|
+
if adapter.adapter_type != "Modbus":
|
|
253
|
+
raise HTTPException(
|
|
254
|
+
status_code=404,
|
|
255
|
+
detail=f"Modbus adapter '{system_name}' not found",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
adapter.disconnect()
|
|
259
|
+
return {"status": "success", "connected": adapter.is_connected}
|
|
260
|
+
except KeyError as exc:
|
|
261
|
+
raise HTTPException(
|
|
262
|
+
status_code=404, detail=f"Adapter '{system_name}' not found"
|
|
263
|
+
) from exc
|
|
264
|
+
|
|
265
|
+
return router
|
|
266
|
+
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
system_name: str,
|
|
270
|
+
user_name: str,
|
|
271
|
+
description: str = "",
|
|
272
|
+
enabled: bool = True,
|
|
273
|
+
is_network: bool = False,
|
|
274
|
+
serial_port: Optional[str] = None,
|
|
275
|
+
baud: int = 9600,
|
|
276
|
+
address: Optional[str] = None,
|
|
277
|
+
network_port: Optional[int] = 0,
|
|
278
|
+
**kwargs: Any,
|
|
279
|
+
) -> None:
|
|
280
|
+
super().__init__(system_name, user_name, description)
|
|
281
|
+
self.adapter_type = AdapterTypeEnum.MODBUS
|
|
282
|
+
self.enabled = enabled
|
|
283
|
+
self.is_network = is_network
|
|
284
|
+
self.serial_port = serial_port
|
|
285
|
+
self.baud = baud
|
|
286
|
+
self.address = address
|
|
287
|
+
self.network_port = network_port
|
|
288
|
+
self.connection: Union[serial.Serial, socket.socket, None] = None
|
|
289
|
+
self.tid = 0 # Transaction ID for Modbus TCP
|
|
290
|
+
|
|
291
|
+
def attempt_connect(self) -> None:
|
|
292
|
+
"""Attempt to connect to the Modbus device."""
|
|
293
|
+
logger.info(
|
|
294
|
+
f"Connecting to Modbus device: {self.system_name} ({self.adapter_type})"
|
|
295
|
+
)
|
|
296
|
+
if self.is_network:
|
|
297
|
+
self._connect_network()
|
|
298
|
+
else:
|
|
299
|
+
self._connect_serial()
|
|
300
|
+
|
|
301
|
+
def _connect_network(self) -> None:
|
|
302
|
+
"""Connect to a Modbus TCP device."""
|
|
303
|
+
if not self.address:
|
|
304
|
+
raise ValueError("Network address is required for network connection")
|
|
305
|
+
|
|
306
|
+
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
307
|
+
self.connection.settimeout(NETWORK_TIMEOUT)
|
|
308
|
+
logger.info(
|
|
309
|
+
f"Connecting to Modbus TCP device at {self.address}:{self.network_port}"
|
|
310
|
+
)
|
|
311
|
+
self.connection.connect((self.address, self.network_port))
|
|
312
|
+
|
|
313
|
+
def _connect_serial(self) -> None:
|
|
314
|
+
"""Connect to a Modbus RTU device."""
|
|
315
|
+
if not isinstance(self.serial_port, str):
|
|
316
|
+
self.serial_port = f"COM{self.serial_port}"
|
|
317
|
+
|
|
318
|
+
self.connection = serial.Serial(
|
|
319
|
+
port=self.serial_port,
|
|
320
|
+
baudrate=self.baud,
|
|
321
|
+
bytesize=serial.EIGHTBITS,
|
|
322
|
+
parity=serial.PARITY_NONE,
|
|
323
|
+
stopbits=serial.STOPBITS_ONE,
|
|
324
|
+
timeout=SERIAL_TIMEOUT,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def attempt_disconnect(self) -> None:
|
|
328
|
+
"""Attempt to disconnect from the Modbus device."""
|
|
329
|
+
if self.connection:
|
|
330
|
+
self.connection.close()
|
|
331
|
+
self.connection = None
|
|
332
|
+
self.is_connected = False
|
|
333
|
+
logger.debug(
|
|
334
|
+
f"Disconnected from Modbus device: {self.system_name} ({self.adapter_type})"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def attempt_write(self, data: bytearray) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Write data to the Modbus device.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
data: The data to write
|
|
343
|
+
|
|
344
|
+
Raises:
|
|
345
|
+
ConnectionError: If not connected to a device
|
|
346
|
+
IOError: If the write fails
|
|
347
|
+
"""
|
|
348
|
+
if not self.connection:
|
|
349
|
+
raise ConnectionError("Not connected to a device")
|
|
350
|
+
|
|
351
|
+
if self.is_network:
|
|
352
|
+
self._write_network(data)
|
|
353
|
+
else:
|
|
354
|
+
self._write_serial(data)
|
|
355
|
+
|
|
356
|
+
def _write_network(self, data: bytearray) -> None:
|
|
357
|
+
"""Write data to a Modbus TCP device."""
|
|
358
|
+
if not isinstance(self.connection, socket.socket):
|
|
359
|
+
raise ConnectionError("Invalid network connection")
|
|
360
|
+
|
|
361
|
+
# Increment transaction ID
|
|
362
|
+
self.tid = (self.tid + 1) % 65536
|
|
363
|
+
|
|
364
|
+
# Create MBAP header
|
|
365
|
+
mbap = bytearray(
|
|
366
|
+
[
|
|
367
|
+
(self.tid >> 8) & 0xFF, # Transaction ID high byte
|
|
368
|
+
self.tid & 0xFF, # Transaction ID low byte
|
|
369
|
+
0x00, # Protocol ID high byte
|
|
370
|
+
0x00, # Protocol ID low byte
|
|
371
|
+
(len(data) >> 8) & 0xFF, # Length high byte
|
|
372
|
+
len(data) & 0xFF, # Length low byte
|
|
373
|
+
]
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Prepend MBAP header to data
|
|
377
|
+
message = mbap + data
|
|
378
|
+
|
|
379
|
+
# Send data
|
|
380
|
+
self.connection.sendall(message)
|
|
381
|
+
|
|
382
|
+
def _write_serial(self, data: bytearray) -> None:
|
|
383
|
+
"""Write data to a Modbus RTU device."""
|
|
384
|
+
if not isinstance(self.connection, serial.Serial):
|
|
385
|
+
raise ConnectionError("Invalid serial connection")
|
|
386
|
+
|
|
387
|
+
# Calculate CRC
|
|
388
|
+
crc = modbus_crc(data)
|
|
389
|
+
|
|
390
|
+
# Append CRC to data
|
|
391
|
+
message = data + bytearray([(crc & 0xFF), (crc >> 8) & 0xFF])
|
|
392
|
+
|
|
393
|
+
# Send data
|
|
394
|
+
self.connection.write(message)
|
|
395
|
+
|
|
396
|
+
def attempt_read(self) -> bytearray:
|
|
397
|
+
"""
|
|
398
|
+
Read data from the Modbus device.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
The read data
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
ConnectionError: If not connected to a device
|
|
405
|
+
IOError: If the read fails
|
|
406
|
+
"""
|
|
407
|
+
if not self.connection:
|
|
408
|
+
raise ConnectionError("Not connected to a device")
|
|
409
|
+
|
|
410
|
+
# Read response
|
|
411
|
+
if self.is_network:
|
|
412
|
+
return self._read_network()
|
|
413
|
+
else:
|
|
414
|
+
return self._read_serial()
|
|
415
|
+
|
|
416
|
+
def _read_network(self) -> bytearray:
|
|
417
|
+
"""Read data from a Modbus TCP device."""
|
|
418
|
+
if not isinstance(self.connection, socket.socket):
|
|
419
|
+
raise ConnectionError("Invalid network connection")
|
|
420
|
+
|
|
421
|
+
# Read MBAP header (7 bytes)
|
|
422
|
+
mbap = self.connection.recv(7)
|
|
423
|
+
if len(mbap) != 7:
|
|
424
|
+
raise IOError("Failed to read MBAP header")
|
|
425
|
+
|
|
426
|
+
# Extract length from MBAP header
|
|
427
|
+
length = (mbap[4] << 8) | mbap[5] - 1 # Subtract 1 for unit ID byte
|
|
428
|
+
|
|
429
|
+
# Read data
|
|
430
|
+
data = self.connection.recv(length)
|
|
431
|
+
if len(data) != length:
|
|
432
|
+
raise IOError("Failed to read complete response")
|
|
433
|
+
|
|
434
|
+
# Check transaction ID
|
|
435
|
+
tid = (mbap[0] << 8) | mbap[1]
|
|
436
|
+
if tid != self.tid:
|
|
437
|
+
self.connection.recv(1024) # Flush buffer
|
|
438
|
+
raise IOError(
|
|
439
|
+
f"Transaction ID mismatch: expected {self.tid}, got {tid}. Flushing buffer."
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return bytearray(data[2:]) # Skip function code and unit ID byte
|
|
443
|
+
|
|
444
|
+
def _read_serial(self) -> bytearray:
|
|
445
|
+
"""Read data from a Modbus RTU device."""
|
|
446
|
+
if not isinstance(self.connection, serial.Serial):
|
|
447
|
+
raise ConnectionError("Invalid serial connection")
|
|
448
|
+
|
|
449
|
+
# Read function code and length (2 bytes)
|
|
450
|
+
header = self.connection.read(2)
|
|
451
|
+
if len(header) != 2:
|
|
452
|
+
raise IOError("Failed to read response header")
|
|
453
|
+
|
|
454
|
+
# Extract function code
|
|
455
|
+
function_code = header[1]
|
|
456
|
+
|
|
457
|
+
# Check for exception response
|
|
458
|
+
if function_code & 0x80:
|
|
459
|
+
exception_code = self.connection.read(1)
|
|
460
|
+
raise IOError(f"Modbus exception: {exception_code[0]}")
|
|
461
|
+
|
|
462
|
+
# Select length based on function code
|
|
463
|
+
if function_code == 15: # Write multiple coils
|
|
464
|
+
length = 4
|
|
465
|
+
elif function_code == 3: # Read holding registers
|
|
466
|
+
header += self.connection.read(1)
|
|
467
|
+
length = header[-1]
|
|
468
|
+
else:
|
|
469
|
+
length = 2
|
|
470
|
+
|
|
471
|
+
# Read data and CRC
|
|
472
|
+
data = self.connection.read(length + 2) # +2 for CRC
|
|
473
|
+
if len(data) != length + 2:
|
|
474
|
+
raise IOError("Failed to read complete response")
|
|
475
|
+
|
|
476
|
+
# Verify CRC
|
|
477
|
+
message = header + data[:-2]
|
|
478
|
+
crc = modbus_crc(bytearray(message))
|
|
479
|
+
received_crc = (data[-1] << 8) | data[-2]
|
|
480
|
+
|
|
481
|
+
if crc != received_crc:
|
|
482
|
+
raise IOError("CRC check failed")
|
|
483
|
+
|
|
484
|
+
# Return data without CRC
|
|
485
|
+
return bytearray(data[:-2])
|
|
Binary file
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interface classes and types for opensignalbox-interface module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from opensignalbox.interface.models import Interface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InterfaceHandler(ABC):
|
|
12
|
+
"""Base abstract class for all interface handlers."""
|
|
13
|
+
|
|
14
|
+
# Interface controller reference for API routes
|
|
15
|
+
interface_controller = None
|
|
16
|
+
adapter_controller = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def init(cls, output_variables, input_variable_subs, variable_update_callback):
|
|
20
|
+
"""Initialize handler with controller references."""
|
|
21
|
+
cls.output_variables = output_variables
|
|
22
|
+
cls.input_variable_subs = input_variable_subs
|
|
23
|
+
cls.variable_update_callback = variable_update_callback
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def set_controllers(cls, interface_controller, adapter_controller):
|
|
27
|
+
"""Set the interface and adapter controller references for API routes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
interface_controller: The interface controller instance
|
|
31
|
+
adapter_controller: The adapter controller instance
|
|
32
|
+
"""
|
|
33
|
+
cls.interface_controller = interface_controller
|
|
34
|
+
cls.adapter_controller = adapter_controller
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def add(interface: Interface) -> Interface:
|
|
39
|
+
"""Add a new interface.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
interface: The interface to add
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The initialized interface with additional runtime fields
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def update(interface: Interface, fields: Dict[str, Any]) -> None:
|
|
52
|
+
"""Update an existing interface.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
interface: The interface to update
|
|
56
|
+
fields: The fields to update
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def remove(interface: Interface) -> None:
|
|
63
|
+
"""Remove an interface.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
interface: The interface to remove
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def handle_variable_update(
|
|
73
|
+
interface: Interface, variable: str, json_data: str
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Handle an update to a subscribed variable.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
interface: The interface associated with the variable
|
|
79
|
+
variable: The name of the variable that was updated
|
|
80
|
+
json_data: The serialized JSON data of the variable update
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def connect(interface: Interface) -> bool:
|
|
87
|
+
"""Connect to the interface.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
interface: The interface to connect
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if connection was successful, False otherwise
|
|
94
|
+
"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def disconnect(interface: Interface) -> None:
|
|
100
|
+
"""Disconnect from the interface.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
interface: The interface to disconnect
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def read_data(interface: Interface) -> None:
|
|
110
|
+
"""Read data.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
interface: The interface to read data for
|
|
114
|
+
"""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def write_data(interface: Interface) -> None:
|
|
120
|
+
"""Write data to hardware using the adapter.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
interface: The interface to write data for
|
|
124
|
+
"""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_router(cls) -> Optional[Any]:
|
|
129
|
+
"""Get API router for this interface type.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Optional FastAPI router to be included in the API
|
|
133
|
+
Returns None if no custom routes are needed
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
```python
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_router(cls):
|
|
139
|
+
from fastapi import APIRouter
|
|
140
|
+
|
|
141
|
+
router = APIRouter()
|
|
142
|
+
|
|
143
|
+
@router.get("/my-interface/status")
|
|
144
|
+
async def get_status():
|
|
145
|
+
# Access interfaces via the controller reference
|
|
146
|
+
interfaces = cls.interface_controller.get_all()
|
|
147
|
+
return {"status": "ok", "interface_count": len(interfaces)}
|
|
148
|
+
|
|
149
|
+
return router
|
|
150
|
+
```
|
|
151
|
+
"""
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def load_from_json(cls, interface_data: Any) -> None:
|
|
157
|
+
"""Load interface data from JSON."""
|
|
158
|
+
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def save_to_json(interface: Interface) -> Any:
|
|
164
|
+
"""Save interface data to JSON.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A dictionary representation of the interface data
|
|
168
|
+
"""
|
|
169
|
+
# Default implementation returns simple model dump
|
|
170
|
+
pass
|