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,1096 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handler for Basic IO interfaces.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from math import ceil
|
|
7
|
+
from typing import Any, Dict, cast
|
|
8
|
+
|
|
9
|
+
from anyio import sleep
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
from opensignalbox.common.messaging import get_messager
|
|
13
|
+
from opensignalbox.common.sv_wire import decode_sv_message
|
|
14
|
+
from opensignalbox.interface.adapters.base import BaseAdapter
|
|
15
|
+
from opensignalbox.interface.interfaces.base import InterfaceHandler
|
|
16
|
+
from opensignalbox.interface.interfaces.basicio.connections import (
|
|
17
|
+
BasicIOConnection,
|
|
18
|
+
BasicIOModbusConnection,
|
|
19
|
+
BasicIOTestConnection,
|
|
20
|
+
)
|
|
21
|
+
from opensignalbox.interface.models import Interface
|
|
22
|
+
from opensignalbox.interface.utils import swap_bytes
|
|
23
|
+
|
|
24
|
+
from .models import (
|
|
25
|
+
BasicIOConnectionType,
|
|
26
|
+
BasicIOForceMessage,
|
|
27
|
+
BasicIOInterface,
|
|
28
|
+
BasicIOInterfaceCreate,
|
|
29
|
+
BasicIOInterfacePublic,
|
|
30
|
+
BasicIOInterfaceUpdate,
|
|
31
|
+
IfaceChanTuple,
|
|
32
|
+
InputChannel,
|
|
33
|
+
InputChannelCreate,
|
|
34
|
+
OutputChannel,
|
|
35
|
+
OutputChannelCreate,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
messager = get_messager()
|
|
40
|
+
|
|
41
|
+
STATUS_UPDATE_PERIOD = 0.2 # seconds
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BasicIOInterfaceHandler(InterfaceHandler):
|
|
45
|
+
"""Handler for Basic IO interfaces."""
|
|
46
|
+
|
|
47
|
+
# Metadata for the API discovery
|
|
48
|
+
metadata = {
|
|
49
|
+
"supported_connection_types": [
|
|
50
|
+
"BasicIOModbusConnection",
|
|
51
|
+
"BasicIOTestConnection",
|
|
52
|
+
],
|
|
53
|
+
"required_fields": ["system_name", "user_name", "interface_type"],
|
|
54
|
+
"optional_fields": [
|
|
55
|
+
"description",
|
|
56
|
+
"adapter_name",
|
|
57
|
+
"enabled",
|
|
58
|
+
"modbus_address",
|
|
59
|
+
"input_start_address",
|
|
60
|
+
"output_start_address",
|
|
61
|
+
"input_bits",
|
|
62
|
+
"output_bits",
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def get_router(cls) -> Any:
|
|
68
|
+
"""Get API router for Basic IO interface endpoints.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
FastAPI router with Basic IO interface specific endpoints
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
router = APIRouter(tags=["interfaces/basicio"])
|
|
75
|
+
|
|
76
|
+
@router.get("/{system_name}")
|
|
77
|
+
async def get_interface(system_name: str):
|
|
78
|
+
"""Get a BasicIO interface by system name."""
|
|
79
|
+
if not cls.interface_controller:
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=500, detail="Interface controller not initialized"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
interface = cls.interface_controller.get(system_name)
|
|
86
|
+
if interface.interface_type != "BasicIO":
|
|
87
|
+
raise HTTPException(
|
|
88
|
+
status_code=404,
|
|
89
|
+
detail=f"BasicIO interface '{system_name}' not found",
|
|
90
|
+
)
|
|
91
|
+
return BasicIOInterfacePublic(
|
|
92
|
+
**interface.model_dump(),
|
|
93
|
+
input_map=[
|
|
94
|
+
InputChannelCreate(**input.model_dump(), channel=channel)
|
|
95
|
+
for channel, input in interface.input_channels.items()
|
|
96
|
+
],
|
|
97
|
+
output_map=[
|
|
98
|
+
OutputChannelCreate(**output.model_dump(), channel=channel)
|
|
99
|
+
for channel, output in interface.output_channels.items()
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
except KeyError as exc:
|
|
103
|
+
raise HTTPException(
|
|
104
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
105
|
+
) from exc
|
|
106
|
+
|
|
107
|
+
@router.post("/")
|
|
108
|
+
async def create_interface(interface: BasicIOInterfaceCreate):
|
|
109
|
+
"""Create a new Basic IO interface."""
|
|
110
|
+
if not cls.interface_controller:
|
|
111
|
+
raise HTTPException(
|
|
112
|
+
status_code=500, detail="Interface controller not initialized"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if interface.interface_type != "BasicIO":
|
|
116
|
+
raise HTTPException(
|
|
117
|
+
status_code=400, detail="Interface type must be BasicIO"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
interface_model = BasicIOInterface(**interface.model_dump())
|
|
122
|
+
|
|
123
|
+
# Add the interface
|
|
124
|
+
new_interface = cls.interface_controller.add(interface_model)
|
|
125
|
+
return new_interface
|
|
126
|
+
except FileExistsError as exc:
|
|
127
|
+
raise HTTPException(
|
|
128
|
+
status_code=400,
|
|
129
|
+
detail=f"Interface '{interface.system_name}' already exists",
|
|
130
|
+
) from exc
|
|
131
|
+
except TypeError as exc:
|
|
132
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
133
|
+
|
|
134
|
+
@router.patch("/{system_name}")
|
|
135
|
+
async def update_interface(
|
|
136
|
+
system_name: str, interface_update: BasicIOInterfaceUpdate
|
|
137
|
+
):
|
|
138
|
+
"""Update a Basic IO interface."""
|
|
139
|
+
if not cls.interface_controller:
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=500, detail="Interface controller not initialized"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Check that the interface exists and is Basic IO type
|
|
146
|
+
interface = cls.interface_controller.get(system_name)
|
|
147
|
+
if interface.interface_type != "BasicIO":
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=404,
|
|
150
|
+
detail=f"Basic IO interface '{system_name}' not found",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Extract update fields
|
|
154
|
+
fields = {
|
|
155
|
+
k: v
|
|
156
|
+
for k, v in interface_update.model_dump().items()
|
|
157
|
+
if v is not None
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Update the interface
|
|
161
|
+
updated_interface = cls.interface_controller.update(system_name, fields)
|
|
162
|
+
return updated_interface
|
|
163
|
+
except KeyError as exc:
|
|
164
|
+
raise HTTPException(
|
|
165
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
166
|
+
) from exc
|
|
167
|
+
|
|
168
|
+
@router.delete("/{system_name}")
|
|
169
|
+
async def delete_interface(system_name: str):
|
|
170
|
+
"""Delete a Basic IO interface."""
|
|
171
|
+
if not cls.interface_controller:
|
|
172
|
+
raise HTTPException(
|
|
173
|
+
status_code=500, detail="Interface controller not initialized"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Check that the interface exists and is Basic IO type
|
|
178
|
+
interface = cls.interface_controller.get(system_name)
|
|
179
|
+
if interface.interface_type != "BasicIO":
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
status_code=404,
|
|
182
|
+
detail=f"Basic IO interface '{system_name}' not found",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
cls.interface_controller.remove(system_name)
|
|
186
|
+
return {"status": "success"}
|
|
187
|
+
except KeyError as exc:
|
|
188
|
+
raise HTTPException(
|
|
189
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
190
|
+
) from exc
|
|
191
|
+
|
|
192
|
+
@router.post("/{system_name}/connect")
|
|
193
|
+
async def connect_interface(system_name: str):
|
|
194
|
+
"""Connect to a Basic IO interface."""
|
|
195
|
+
if not cls.interface_controller:
|
|
196
|
+
raise HTTPException(
|
|
197
|
+
status_code=500, detail="Interface controller not initialized"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
interface = cls.interface_controller.get(system_name)
|
|
202
|
+
if interface.interface_type != "BasicIO":
|
|
203
|
+
raise HTTPException(
|
|
204
|
+
status_code=404,
|
|
205
|
+
detail=f"Basic IO interface '{system_name}' not found",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Connect to the interface
|
|
209
|
+
success = cls.connect(interface)
|
|
210
|
+
if success:
|
|
211
|
+
return {"status": "success"}
|
|
212
|
+
else:
|
|
213
|
+
raise HTTPException(
|
|
214
|
+
status_code=500,
|
|
215
|
+
detail=f"Failed to connect to interface. Detail: {interface.error}",
|
|
216
|
+
)
|
|
217
|
+
except KeyError as exc:
|
|
218
|
+
raise HTTPException(
|
|
219
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
220
|
+
) from exc
|
|
221
|
+
|
|
222
|
+
@router.get("/{system_name}/channel-status")
|
|
223
|
+
async def status_stream(request: Request, system_name: str):
|
|
224
|
+
"""Stream the channel status of a Basic IO interface."""
|
|
225
|
+
if not cls.interface_controller:
|
|
226
|
+
raise HTTPException(
|
|
227
|
+
status_code=500, detail="Interface controller not initialized"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
|
|
232
|
+
async def status_generator():
|
|
233
|
+
if not cls.interface_controller:
|
|
234
|
+
raise HTTPException(
|
|
235
|
+
status_code=500,
|
|
236
|
+
detail="Interface controller not initialized",
|
|
237
|
+
)
|
|
238
|
+
interface = cls.interface_controller.get(system_name)
|
|
239
|
+
if interface.interface_type != "BasicIO":
|
|
240
|
+
raise HTTPException(
|
|
241
|
+
status_code=404,
|
|
242
|
+
detail=f"Basic IO interface '{system_name}' not found",
|
|
243
|
+
)
|
|
244
|
+
while True:
|
|
245
|
+
if await request.is_disconnected():
|
|
246
|
+
break
|
|
247
|
+
# Use controller's get_status method
|
|
248
|
+
yield f"data: {cls.get_channel_status(interface)}\n\n"
|
|
249
|
+
await sleep(STATUS_UPDATE_PERIOD)
|
|
250
|
+
|
|
251
|
+
return StreamingResponse(
|
|
252
|
+
status_generator(), media_type="text/event-stream"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except KeyError as exc:
|
|
256
|
+
raise HTTPException(
|
|
257
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
258
|
+
) from exc
|
|
259
|
+
|
|
260
|
+
@router.put("/{system_name}/forcing")
|
|
261
|
+
async def set_forcing(system_name: str, force_message: BasicIOForceMessage):
|
|
262
|
+
"""Set forcing for a channel in a Basic IO interface."""
|
|
263
|
+
if not cls.interface_controller:
|
|
264
|
+
raise HTTPException(
|
|
265
|
+
status_code=500, detail="Interface controller not initialized"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
interface = cls.interface_controller.get(system_name)
|
|
270
|
+
if interface.interface_type != "BasicIO":
|
|
271
|
+
raise HTTPException(
|
|
272
|
+
status_code=404,
|
|
273
|
+
detail=f"Basic IO interface '{system_name}' not found",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
cls.set_forcing(
|
|
277
|
+
interface,
|
|
278
|
+
force_message.channel,
|
|
279
|
+
force_message.iotype,
|
|
280
|
+
force_message.state,
|
|
281
|
+
)
|
|
282
|
+
return {"status": "success"}
|
|
283
|
+
except KeyError as exc:
|
|
284
|
+
raise HTTPException(
|
|
285
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
286
|
+
) from exc
|
|
287
|
+
|
|
288
|
+
return router
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def add(interface: Interface) -> Interface:
|
|
292
|
+
"""
|
|
293
|
+
Initialize a BasicIOInterface from an Interface.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
interface: The interface to initialize. Must be a BasicIOInterface or
|
|
297
|
+
convertible to a BasicIOInterface.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
The initialized BasicIOInterface.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
TypeError: If the interface cannot be converted to a BasicIOInterface.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
307
|
+
|
|
308
|
+
# Initialize channels
|
|
309
|
+
for input in basicio_interface.input_channels.values():
|
|
310
|
+
input.shared_variable = (
|
|
311
|
+
BasicIOInterfaceHandler.output_variables.new_from_logic(
|
|
312
|
+
system_name=input.system_name,
|
|
313
|
+
user_name=input.user_name,
|
|
314
|
+
description=input.description,
|
|
315
|
+
tags=input.tags,
|
|
316
|
+
value=input.default_value,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Create channels for all configured bits
|
|
321
|
+
for channel in range(1, basicio_interface.input_bits + 1):
|
|
322
|
+
if channel not in basicio_interface.input_channels:
|
|
323
|
+
basicio_interface.input_channels[channel] = InputChannel(
|
|
324
|
+
system_name="",
|
|
325
|
+
user_name="",
|
|
326
|
+
description="",
|
|
327
|
+
tags=[],
|
|
328
|
+
default_value=False,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Set up variable subscriptions
|
|
332
|
+
if BasicIOInterfaceHandler.input_variable_subs is not None:
|
|
333
|
+
for channel, output in basicio_interface.output_channels.items():
|
|
334
|
+
if output.shared_variable_name:
|
|
335
|
+
if (
|
|
336
|
+
output.shared_variable_name
|
|
337
|
+
not in BasicIOInterfaceHandler.input_variable_subs
|
|
338
|
+
):
|
|
339
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
340
|
+
output.shared_variable_name
|
|
341
|
+
] = []
|
|
342
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
343
|
+
output.shared_variable_name
|
|
344
|
+
].append(IfaceChanTuple(basicio_interface.system_name, channel))
|
|
345
|
+
|
|
346
|
+
# Create a closure that captures the interface reference
|
|
347
|
+
def create_variable_callback(iface):
|
|
348
|
+
def callback(var: str, json_data: str):
|
|
349
|
+
# Forward to controller's callback which has access to all interfaces
|
|
350
|
+
if BasicIOInterfaceHandler.variable_update_callback:
|
|
351
|
+
BasicIOInterfaceHandler.variable_update_callback(
|
|
352
|
+
var, json_data
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return callback
|
|
356
|
+
|
|
357
|
+
# Subscribe with the closure
|
|
358
|
+
messager.sub(
|
|
359
|
+
output.shared_variable_name,
|
|
360
|
+
create_variable_callback(basicio_interface),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Ensure all output channels exist
|
|
364
|
+
for channel in range(1, basicio_interface.output_bits + 1):
|
|
365
|
+
if channel not in basicio_interface.output_channels:
|
|
366
|
+
basicio_interface.output_channels[channel] = OutputChannel(
|
|
367
|
+
shared_variable_name=None
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return basicio_interface
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def update(interface: Interface, fields: Dict[str, Any]) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Update a BasicIOInterface.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
interface: The interface to update. Must be a BasicIOInterface or convertible to one.
|
|
379
|
+
fields: The fields to update.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
TypeError: If the interface cannot be converted to a BasicIOInterface.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
386
|
+
|
|
387
|
+
# Handle basic field updates
|
|
388
|
+
for field, value in fields.items():
|
|
389
|
+
if hasattr(basicio_interface, field) and value is not None:
|
|
390
|
+
setattr(basicio_interface, field, value)
|
|
391
|
+
|
|
392
|
+
# Clear input channels above input_bits limit
|
|
393
|
+
if "input_bits" in fields:
|
|
394
|
+
input_bits = fields["input_bits"]
|
|
395
|
+
channels_to_remove = [
|
|
396
|
+
ch for ch in basicio_interface.input_channels.keys() if ch > input_bits
|
|
397
|
+
]
|
|
398
|
+
for channel_index in channels_to_remove:
|
|
399
|
+
# Clean up shared variable
|
|
400
|
+
channel = basicio_interface.input_channels[channel_index]
|
|
401
|
+
if channel.shared_variable:
|
|
402
|
+
BasicIOInterfaceHandler.output_variables.remove(
|
|
403
|
+
channel.shared_variable.system_name
|
|
404
|
+
)
|
|
405
|
+
del basicio_interface.input_channels[channel_index]
|
|
406
|
+
|
|
407
|
+
# Clear output channels above output_bits limit
|
|
408
|
+
if "output_bits" in fields:
|
|
409
|
+
output_bits = fields["output_bits"]
|
|
410
|
+
channels_to_remove = [
|
|
411
|
+
ch
|
|
412
|
+
for ch in basicio_interface.output_channels.keys()
|
|
413
|
+
if ch > output_bits
|
|
414
|
+
]
|
|
415
|
+
for channel_index in channels_to_remove:
|
|
416
|
+
# Clean up variable subscription
|
|
417
|
+
if basicio_interface.output_channels[
|
|
418
|
+
channel_index
|
|
419
|
+
].shared_variable_name:
|
|
420
|
+
old_variable_name = basicio_interface.output_channels[
|
|
421
|
+
channel_index
|
|
422
|
+
].shared_variable_name
|
|
423
|
+
if old_variable_name in BasicIOInterfaceHandler.input_variable_subs:
|
|
424
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
425
|
+
old_variable_name
|
|
426
|
+
] = [
|
|
427
|
+
item
|
|
428
|
+
for item in BasicIOInterfaceHandler.input_variable_subs[
|
|
429
|
+
old_variable_name
|
|
430
|
+
]
|
|
431
|
+
if not (
|
|
432
|
+
item.interface == basicio_interface.system_name
|
|
433
|
+
and item.channel == channel_index
|
|
434
|
+
)
|
|
435
|
+
]
|
|
436
|
+
# Clean up empty lists
|
|
437
|
+
if not BasicIOInterfaceHandler.input_variable_subs[
|
|
438
|
+
old_variable_name
|
|
439
|
+
]:
|
|
440
|
+
del BasicIOInterfaceHandler.input_variable_subs[
|
|
441
|
+
old_variable_name
|
|
442
|
+
]
|
|
443
|
+
del basicio_interface.output_channels[channel_index]
|
|
444
|
+
|
|
445
|
+
# Handle input channel updates (if provided)
|
|
446
|
+
input_map = fields.get("input_map")
|
|
447
|
+
if input_map is not None:
|
|
448
|
+
# Get list of channels in the new map
|
|
449
|
+
new_channels = {
|
|
450
|
+
channel_data.get("channel")
|
|
451
|
+
for channel_data in input_map
|
|
452
|
+
if channel_data.get("channel") is not None
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Remove channels not in the new map
|
|
456
|
+
channels_to_remove = [
|
|
457
|
+
ch
|
|
458
|
+
for ch in basicio_interface.input_channels.keys()
|
|
459
|
+
if ch not in new_channels
|
|
460
|
+
]
|
|
461
|
+
logger.debug(
|
|
462
|
+
f"Removing channels: {channels_to_remove} from input channels of {basicio_interface.system_name}"
|
|
463
|
+
)
|
|
464
|
+
for channel_index in channels_to_remove:
|
|
465
|
+
# Clean up shared variable
|
|
466
|
+
channel = basicio_interface.input_channels[channel_index]
|
|
467
|
+
if channel.shared_variable:
|
|
468
|
+
BasicIOInterfaceHandler.output_variables.remove(
|
|
469
|
+
channel.shared_variable.system_name
|
|
470
|
+
)
|
|
471
|
+
del basicio_interface.input_channels[channel_index]
|
|
472
|
+
|
|
473
|
+
# Process each channel in the input map
|
|
474
|
+
for channel_data in input_map:
|
|
475
|
+
channel_index = channel_data.get("channel")
|
|
476
|
+
if channel_index is None:
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
# Update or create channel
|
|
480
|
+
if channel_index in basicio_interface.input_channels:
|
|
481
|
+
# Update existing channel
|
|
482
|
+
for field, value in channel_data.items():
|
|
483
|
+
if field != "channel" and hasattr(
|
|
484
|
+
basicio_interface.input_channels[channel_index], field
|
|
485
|
+
):
|
|
486
|
+
setattr(
|
|
487
|
+
basicio_interface.input_channels[channel_index],
|
|
488
|
+
field,
|
|
489
|
+
value,
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
# Create new channel
|
|
493
|
+
basicio_interface.input_channels[channel_index] = InputChannel(
|
|
494
|
+
system_name=channel_data.get("system_name", ""),
|
|
495
|
+
user_name=channel_data.get("user_name", ""),
|
|
496
|
+
description=channel_data.get("description", ""),
|
|
497
|
+
tags=channel_data.get("tags", []),
|
|
498
|
+
default_value=channel_data.get("default_value", False),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Register shared variable if needed
|
|
502
|
+
basicio_interface.input_channels[
|
|
503
|
+
channel_index
|
|
504
|
+
].shared_variable = (
|
|
505
|
+
BasicIOInterfaceHandler.output_variables.new_from_logic(
|
|
506
|
+
system_name=basicio_interface.input_channels[
|
|
507
|
+
channel_index
|
|
508
|
+
].system_name,
|
|
509
|
+
user_name=basicio_interface.input_channels[
|
|
510
|
+
channel_index
|
|
511
|
+
].user_name,
|
|
512
|
+
description=basicio_interface.input_channels[
|
|
513
|
+
channel_index
|
|
514
|
+
].description,
|
|
515
|
+
tags=basicio_interface.input_channels[channel_index].tags,
|
|
516
|
+
value=basicio_interface.input_channels[
|
|
517
|
+
channel_index
|
|
518
|
+
].default_value,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Handle output channel updates (if provided)
|
|
523
|
+
output_map = fields.get("output_map")
|
|
524
|
+
if output_map is not None:
|
|
525
|
+
# Get list of channels in the new map
|
|
526
|
+
new_channels = {
|
|
527
|
+
channel_data.get("channel")
|
|
528
|
+
for channel_data in output_map
|
|
529
|
+
if channel_data.get("channel") is not None
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
# Remove channels not in the new map
|
|
533
|
+
channels_to_remove = [
|
|
534
|
+
ch
|
|
535
|
+
for ch in basicio_interface.output_channels.keys()
|
|
536
|
+
if ch not in new_channels
|
|
537
|
+
]
|
|
538
|
+
for channel_index in channels_to_remove:
|
|
539
|
+
# Clean up variable subscription
|
|
540
|
+
if basicio_interface.output_channels[
|
|
541
|
+
channel_index
|
|
542
|
+
].shared_variable_name:
|
|
543
|
+
old_variable_name = basicio_interface.output_channels[
|
|
544
|
+
channel_index
|
|
545
|
+
].shared_variable_name
|
|
546
|
+
if old_variable_name in BasicIOInterfaceHandler.input_variable_subs:
|
|
547
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
548
|
+
old_variable_name
|
|
549
|
+
] = [
|
|
550
|
+
item
|
|
551
|
+
for item in BasicIOInterfaceHandler.input_variable_subs[
|
|
552
|
+
old_variable_name
|
|
553
|
+
]
|
|
554
|
+
if not (
|
|
555
|
+
item.interface == basicio_interface.system_name
|
|
556
|
+
and item.channel == channel_index
|
|
557
|
+
)
|
|
558
|
+
]
|
|
559
|
+
# Clean up empty lists
|
|
560
|
+
if not BasicIOInterfaceHandler.input_variable_subs[
|
|
561
|
+
old_variable_name
|
|
562
|
+
]:
|
|
563
|
+
del BasicIOInterfaceHandler.input_variable_subs[
|
|
564
|
+
old_variable_name
|
|
565
|
+
]
|
|
566
|
+
del basicio_interface.output_channels[channel_index]
|
|
567
|
+
|
|
568
|
+
# Process each channel in the output map
|
|
569
|
+
for channel_data in output_map:
|
|
570
|
+
channel_index = channel_data.get("channel")
|
|
571
|
+
if channel_index is None:
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
# Check if we're changing the variable subscription
|
|
575
|
+
old_variable_name = None
|
|
576
|
+
if channel_index in basicio_interface.output_channels:
|
|
577
|
+
old_variable_name = basicio_interface.output_channels[
|
|
578
|
+
channel_index
|
|
579
|
+
].shared_variable_name
|
|
580
|
+
|
|
581
|
+
new_variable_name = channel_data.get("shared_variable_name")
|
|
582
|
+
|
|
583
|
+
# Update or create channel
|
|
584
|
+
if channel_index in basicio_interface.output_channels:
|
|
585
|
+
# Update existing channel
|
|
586
|
+
for field, value in channel_data.items():
|
|
587
|
+
if field != "channel" and hasattr(
|
|
588
|
+
basicio_interface.output_channels[channel_index], field
|
|
589
|
+
):
|
|
590
|
+
setattr(
|
|
591
|
+
basicio_interface.output_channels[channel_index],
|
|
592
|
+
field,
|
|
593
|
+
value,
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
# Create new channel
|
|
597
|
+
basicio_interface.output_channels[channel_index] = OutputChannel(
|
|
598
|
+
shared_variable_name=channel_data.get("shared_variable_name")
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Handle variable subscription changes if needed
|
|
602
|
+
if old_variable_name != new_variable_name:
|
|
603
|
+
# Remove old subscription if it exists
|
|
604
|
+
if (
|
|
605
|
+
old_variable_name
|
|
606
|
+
and old_variable_name
|
|
607
|
+
in BasicIOInterfaceHandler.input_variable_subs
|
|
608
|
+
):
|
|
609
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
610
|
+
old_variable_name
|
|
611
|
+
] = [
|
|
612
|
+
item
|
|
613
|
+
for item in BasicIOInterfaceHandler.input_variable_subs[
|
|
614
|
+
old_variable_name
|
|
615
|
+
]
|
|
616
|
+
if not (
|
|
617
|
+
item.interface == basicio_interface.system_name
|
|
618
|
+
and item.channel == channel_index
|
|
619
|
+
)
|
|
620
|
+
]
|
|
621
|
+
# Clean up empty lists
|
|
622
|
+
if not BasicIOInterfaceHandler.input_variable_subs[
|
|
623
|
+
old_variable_name
|
|
624
|
+
]:
|
|
625
|
+
del BasicIOInterfaceHandler.input_variable_subs[
|
|
626
|
+
old_variable_name
|
|
627
|
+
]
|
|
628
|
+
# Unsubscribe
|
|
629
|
+
BasicIOInterfaceHandler.input_variable_subs.pop(
|
|
630
|
+
old_variable_name
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Add new subscription if needed
|
|
634
|
+
if new_variable_name:
|
|
635
|
+
if (
|
|
636
|
+
new_variable_name
|
|
637
|
+
not in BasicIOInterfaceHandler.input_variable_subs
|
|
638
|
+
):
|
|
639
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
640
|
+
new_variable_name
|
|
641
|
+
] = []
|
|
642
|
+
BasicIOInterfaceHandler.input_variable_subs[
|
|
643
|
+
new_variable_name
|
|
644
|
+
].append(
|
|
645
|
+
IfaceChanTuple(basicio_interface.system_name, channel_index)
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Create subscription
|
|
649
|
+
def create_variable_callback(iface):
|
|
650
|
+
def callback(var: str, json_data: str):
|
|
651
|
+
# Forward to controller's callback which has access to all interfaces
|
|
652
|
+
if BasicIOInterfaceHandler.variable_update_callback:
|
|
653
|
+
BasicIOInterfaceHandler.variable_update_callback(
|
|
654
|
+
var, json_data
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
return callback
|
|
658
|
+
|
|
659
|
+
# Subscribe with the closure
|
|
660
|
+
messager.sub(
|
|
661
|
+
new_variable_name,
|
|
662
|
+
create_variable_callback(basicio_interface),
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
@staticmethod
|
|
666
|
+
def remove(interface: Interface) -> None:
|
|
667
|
+
"""
|
|
668
|
+
Remove a BasicIOInterface.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
interface: The interface to remove.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
675
|
+
|
|
676
|
+
# Clean up variable subscriptions
|
|
677
|
+
for channel in basicio_interface.input_channels.values():
|
|
678
|
+
if channel.shared_variable:
|
|
679
|
+
BasicIOInterfaceHandler.output_variables.remove(
|
|
680
|
+
channel.shared_variable.system_name
|
|
681
|
+
)
|
|
682
|
+
for variable, sub in BasicIOInterfaceHandler.input_variable_subs.items():
|
|
683
|
+
if sub[0] == basicio_interface.system_name:
|
|
684
|
+
BasicIOInterfaceHandler.input_variable_subs.pop(variable)
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def handle_variable_update(
|
|
688
|
+
interface: Interface, variable: str, json_data: str
|
|
689
|
+
) -> None:
|
|
690
|
+
"""
|
|
691
|
+
Handle a variable update for a BasicIOInterface.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
interface: The interface to handle the update for.
|
|
695
|
+
variable: The name of the variable that was updated.
|
|
696
|
+
json_data: The JSON data of the updated variable.
|
|
697
|
+
"""
|
|
698
|
+
|
|
699
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
700
|
+
|
|
701
|
+
# Find channels subscribed to this variable
|
|
702
|
+
channels = []
|
|
703
|
+
if variable in BasicIOInterfaceHandler.input_variable_subs:
|
|
704
|
+
for item in BasicIOInterfaceHandler.input_variable_subs[variable]:
|
|
705
|
+
if item.interface == basicio_interface.system_name:
|
|
706
|
+
channels.append(item.channel)
|
|
707
|
+
|
|
708
|
+
# No channels to update
|
|
709
|
+
if not channels:
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
# Parse the variable data
|
|
713
|
+
try:
|
|
714
|
+
data = decode_sv_message(json_data).data
|
|
715
|
+
value = bool(data.get("value", False))
|
|
716
|
+
|
|
717
|
+
# Update each channel
|
|
718
|
+
for channel in channels:
|
|
719
|
+
if channel in basicio_interface.output_channels:
|
|
720
|
+
basicio_interface.output_channels[channel].variable_value = value
|
|
721
|
+
except Exception as e:
|
|
722
|
+
logger.error(f"Error parsing variable data: {e}")
|
|
723
|
+
|
|
724
|
+
@staticmethod
|
|
725
|
+
def connect(interface: Interface) -> bool:
|
|
726
|
+
"""
|
|
727
|
+
Connect to a BasicIOInterface using the specified adapter.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
interface: The interface to connect.
|
|
731
|
+
adapter: The adapter to use for communication.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
True if connection was successful, False otherwise.
|
|
735
|
+
"""
|
|
736
|
+
|
|
737
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
738
|
+
|
|
739
|
+
# Check if the interface is enabled
|
|
740
|
+
if not basicio_interface.enabled:
|
|
741
|
+
return False
|
|
742
|
+
|
|
743
|
+
# Get the appropriate connection strategy based on connection type
|
|
744
|
+
match basicio_interface.connection_type:
|
|
745
|
+
case BasicIOConnectionType.TEST:
|
|
746
|
+
return BasicIOTestConnection.connect(basicio_interface)
|
|
747
|
+
case (
|
|
748
|
+
BasicIOConnectionType.MODBUS_ADAPTER
|
|
749
|
+
| BasicIOConnectionType.DINGTIAN_MODBUS_ADAPTER
|
|
750
|
+
):
|
|
751
|
+
if BasicIOInterfaceHandler.interface_controller:
|
|
752
|
+
adapter = BasicIOInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
753
|
+
interface
|
|
754
|
+
)
|
|
755
|
+
if not adapter or not adapter.enabled:
|
|
756
|
+
return False # No adapter configured or adapter is disabled
|
|
757
|
+
return BasicIOModbusConnection.connect(basicio_interface, adapter)
|
|
758
|
+
return False # No interface controller
|
|
759
|
+
case _:
|
|
760
|
+
logger.error(
|
|
761
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
762
|
+
)
|
|
763
|
+
interface.error = (
|
|
764
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
765
|
+
)
|
|
766
|
+
interface.enabled = False
|
|
767
|
+
return False
|
|
768
|
+
|
|
769
|
+
@staticmethod
|
|
770
|
+
def disconnect(interface: Interface) -> None:
|
|
771
|
+
"""
|
|
772
|
+
Disconnect from a BasicIOInterface.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
interface: The interface to disconnect.
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
True if disconnection was successful, False otherwise.
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
782
|
+
|
|
783
|
+
# Get the appropriate connection strategy based on connection type
|
|
784
|
+
match basicio_interface.connection_type:
|
|
785
|
+
case BasicIOConnectionType.TEST:
|
|
786
|
+
BasicIOTestConnection.disconnect(basicio_interface)
|
|
787
|
+
case (
|
|
788
|
+
BasicIOConnectionType.MODBUS_ADAPTER
|
|
789
|
+
| BasicIOConnectionType.DINGTIAN_MODBUS_ADAPTER
|
|
790
|
+
):
|
|
791
|
+
if BasicIOInterfaceHandler.interface_controller:
|
|
792
|
+
adapter = BasicIOInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
793
|
+
interface
|
|
794
|
+
)
|
|
795
|
+
if not adapter:
|
|
796
|
+
return
|
|
797
|
+
BasicIOModbusConnection.disconnect(basicio_interface, adapter)
|
|
798
|
+
case _:
|
|
799
|
+
logger.error(
|
|
800
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
801
|
+
)
|
|
802
|
+
interface.error = (
|
|
803
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
804
|
+
)
|
|
805
|
+
interface.enabled = False
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
@staticmethod
|
|
809
|
+
def read_data(interface: Interface, adapter: BaseAdapter | None = None) -> None:
|
|
810
|
+
"""
|
|
811
|
+
Read data from the adapter for a BasicIOInterface.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
interface: The interface to read data for.
|
|
815
|
+
adapter: The adapter to read from.
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
819
|
+
|
|
820
|
+
# Skip if no input bits
|
|
821
|
+
if basicio_interface.input_bits <= 0:
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
# Get the appropriate connection strategy based on connection type
|
|
825
|
+
connection: type[BasicIOConnection]
|
|
826
|
+
match basicio_interface.connection_type:
|
|
827
|
+
case BasicIOConnectionType.TEST:
|
|
828
|
+
connection = BasicIOTestConnection
|
|
829
|
+
case (
|
|
830
|
+
BasicIOConnectionType.MODBUS_ADAPTER
|
|
831
|
+
| BasicIOConnectionType.DINGTIAN_MODBUS_ADAPTER
|
|
832
|
+
):
|
|
833
|
+
connection = BasicIOModbusConnection
|
|
834
|
+
if BasicIOInterfaceHandler.interface_controller:
|
|
835
|
+
adapter = BasicIOInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
836
|
+
interface
|
|
837
|
+
)
|
|
838
|
+
case _:
|
|
839
|
+
logger.error(
|
|
840
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
841
|
+
)
|
|
842
|
+
interface.error = (
|
|
843
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
844
|
+
)
|
|
845
|
+
interface.enabled = False
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
# Read from the hardware
|
|
849
|
+
try:
|
|
850
|
+
# Read the words from the adapter (starting at the configured address)
|
|
851
|
+
data = connection.receive_data(basicio_interface, adapter=adapter)
|
|
852
|
+
|
|
853
|
+
if not data:
|
|
854
|
+
logger.error(
|
|
855
|
+
f"No data read from adapter for interface '{basicio_interface.system_name}'"
|
|
856
|
+
)
|
|
857
|
+
return
|
|
858
|
+
|
|
859
|
+
# Convert data to integer array
|
|
860
|
+
words = []
|
|
861
|
+
for i in range(0, len(data), 2):
|
|
862
|
+
if i + 1 < len(data):
|
|
863
|
+
word = (data[i] << 8) | data[i + 1]
|
|
864
|
+
words.append(word)
|
|
865
|
+
|
|
866
|
+
# Apply byte and word swapping if needed
|
|
867
|
+
if basicio_interface.input_byte_swap:
|
|
868
|
+
words = [swap_bytes(word) for word in words]
|
|
869
|
+
|
|
870
|
+
if basicio_interface.input_word_swap and len(words) >= 2:
|
|
871
|
+
for i in range(0, len(words), 2):
|
|
872
|
+
if i + 1 < len(words):
|
|
873
|
+
words[i], words[i + 1] = words[i + 1], words[i]
|
|
874
|
+
|
|
875
|
+
# Update channel values
|
|
876
|
+
for channel_index in range(1, basicio_interface.input_bits + 1):
|
|
877
|
+
if channel_index in basicio_interface.input_channels:
|
|
878
|
+
# Calculate word index and bit position
|
|
879
|
+
word_idx = (channel_index - 1) // 16
|
|
880
|
+
bit_pos = (channel_index - 1) % 16
|
|
881
|
+
|
|
882
|
+
# Get the bit value if the word index is valid
|
|
883
|
+
if word_idx < len(words):
|
|
884
|
+
bit_value = bool((words[word_idx] >> bit_pos) & 1)
|
|
885
|
+
|
|
886
|
+
channel = basicio_interface.input_channels[channel_index]
|
|
887
|
+
|
|
888
|
+
# Update the hardware value
|
|
889
|
+
channel.hardware_value = bit_value
|
|
890
|
+
|
|
891
|
+
channel.variable_value = (
|
|
892
|
+
channel.force_value if channel.force_enable else bit_value
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Update the shared variable if changed
|
|
896
|
+
if channel.shared_variable is not None:
|
|
897
|
+
if (
|
|
898
|
+
channel.shared_variable.data.model_dump()["value"]
|
|
899
|
+
!= channel.variable_value
|
|
900
|
+
):
|
|
901
|
+
channel.shared_variable.update(
|
|
902
|
+
{"value": channel.variable_value}
|
|
903
|
+
)
|
|
904
|
+
except Exception as e:
|
|
905
|
+
logger.error(
|
|
906
|
+
f"Error reading data for interface '{basicio_interface.system_name}': {e}"
|
|
907
|
+
)
|
|
908
|
+
interface.error = str(e)
|
|
909
|
+
|
|
910
|
+
@staticmethod
|
|
911
|
+
def write_data(interface: Interface, adapter: BaseAdapter | None = None) -> None:
|
|
912
|
+
"""
|
|
913
|
+
Write data to the adapter for a BasicIOInterface.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
interface: The interface to write data for.
|
|
917
|
+
adapter: The adapter to write to.
|
|
918
|
+
"""
|
|
919
|
+
|
|
920
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
921
|
+
|
|
922
|
+
# Skip if no output bits
|
|
923
|
+
if basicio_interface.output_bits <= 0:
|
|
924
|
+
return
|
|
925
|
+
|
|
926
|
+
# Calculate number of words needed to represent all bits
|
|
927
|
+
num_words = ceil(basicio_interface.output_bits / 16)
|
|
928
|
+
|
|
929
|
+
# Prepare words array
|
|
930
|
+
words = [0] * num_words
|
|
931
|
+
|
|
932
|
+
# Set bits based on channel values
|
|
933
|
+
for channel in range(1, basicio_interface.output_bits + 1):
|
|
934
|
+
if channel in basicio_interface.output_channels:
|
|
935
|
+
# Calculate word index and bit position
|
|
936
|
+
word_idx = (channel - 1) // 16
|
|
937
|
+
bit_pos = (channel - 1) % 16
|
|
938
|
+
|
|
939
|
+
# Determine the value to write
|
|
940
|
+
if basicio_interface.output_channels[channel].force_enable:
|
|
941
|
+
# Use forced value if forcing is enabled
|
|
942
|
+
value = basicio_interface.output_channels[channel].force_value
|
|
943
|
+
else:
|
|
944
|
+
# Otherwise use the variable value
|
|
945
|
+
value = basicio_interface.output_channels[channel].variable_value
|
|
946
|
+
|
|
947
|
+
# Set the bit in the corresponding word
|
|
948
|
+
if value:
|
|
949
|
+
words[word_idx] |= 1 << bit_pos
|
|
950
|
+
|
|
951
|
+
# Apply byte and word swapping if needed
|
|
952
|
+
if basicio_interface.output_byte_swap:
|
|
953
|
+
words = [swap_bytes(word) for word in words]
|
|
954
|
+
|
|
955
|
+
if basicio_interface.output_word_swap and len(words) >= 2:
|
|
956
|
+
for i in range(0, len(words), 2):
|
|
957
|
+
if i + 1 < len(words):
|
|
958
|
+
words[i], words[i + 1] = words[i + 1], words[i]
|
|
959
|
+
|
|
960
|
+
# Convert words to byte array
|
|
961
|
+
data = bytearray()
|
|
962
|
+
for word in words:
|
|
963
|
+
data.append(word & 0xFF) # Low byte
|
|
964
|
+
data.append((word >> 8) & 0xFF) # High byte
|
|
965
|
+
|
|
966
|
+
connection: type[BasicIOConnection]
|
|
967
|
+
# Get the appropriate connection strategy based on connection type
|
|
968
|
+
match basicio_interface.connection_type:
|
|
969
|
+
case BasicIOConnectionType.TEST:
|
|
970
|
+
connection = BasicIOTestConnection
|
|
971
|
+
case (
|
|
972
|
+
BasicIOConnectionType.MODBUS_ADAPTER
|
|
973
|
+
| BasicIOConnectionType.DINGTIAN_MODBUS_ADAPTER
|
|
974
|
+
):
|
|
975
|
+
connection = BasicIOModbusConnection
|
|
976
|
+
if BasicIOInterfaceHandler.interface_controller:
|
|
977
|
+
adapter = BasicIOInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
978
|
+
interface
|
|
979
|
+
)
|
|
980
|
+
case _:
|
|
981
|
+
logger.error(
|
|
982
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
983
|
+
)
|
|
984
|
+
interface.error = (
|
|
985
|
+
f"Unsupported connection type: {basicio_interface.connection_type}"
|
|
986
|
+
)
|
|
987
|
+
interface.enabled = False
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
# Write to the adapter
|
|
991
|
+
try:
|
|
992
|
+
connection.send_data(basicio_interface, data=data, adapter=adapter)
|
|
993
|
+
# Update hardware value after successful write
|
|
994
|
+
for channel in range(1, basicio_interface.output_bits + 1):
|
|
995
|
+
if channel in basicio_interface.output_channels:
|
|
996
|
+
if basicio_interface.output_channels[channel].force_enable:
|
|
997
|
+
basicio_interface.output_channels[
|
|
998
|
+
channel
|
|
999
|
+
].hardware_value = basicio_interface.output_channels[
|
|
1000
|
+
channel
|
|
1001
|
+
].force_value
|
|
1002
|
+
else:
|
|
1003
|
+
basicio_interface.output_channels[
|
|
1004
|
+
channel
|
|
1005
|
+
].hardware_value = basicio_interface.output_channels[
|
|
1006
|
+
channel
|
|
1007
|
+
].variable_value
|
|
1008
|
+
except Exception as e:
|
|
1009
|
+
logger.error(
|
|
1010
|
+
f"Error writing data for interface '{basicio_interface.system_name}': {e}"
|
|
1011
|
+
)
|
|
1012
|
+
interface.error = str(e)
|
|
1013
|
+
|
|
1014
|
+
@staticmethod
|
|
1015
|
+
def get_channel_status(interface: Interface):
|
|
1016
|
+
"""
|
|
1017
|
+
Get the channel status of a BasicIOInterface.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
interface: The interface to get the status of.
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
A string describing the current status.
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
1027
|
+
|
|
1028
|
+
return basicio_interface.model_dump_json(
|
|
1029
|
+
include={"input_channels", "output_channels"}, by_alias=True
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
@staticmethod
|
|
1033
|
+
def set_forcing(
|
|
1034
|
+
interface: Interface, channel_index: int, iotype: str, state: str
|
|
1035
|
+
) -> None:
|
|
1036
|
+
"""
|
|
1037
|
+
Set forcing for a channel in a BasicIOInterface.
|
|
1038
|
+
|
|
1039
|
+
Args:
|
|
1040
|
+
interface: The interface to set forcing for.
|
|
1041
|
+
channel: The channel number.
|
|
1042
|
+
is_input: Whether the channel is an input.
|
|
1043
|
+
enabled: Whether forcing is enabled.
|
|
1044
|
+
value: The value to force.
|
|
1045
|
+
"""
|
|
1046
|
+
|
|
1047
|
+
if iotype not in ["input", "output"]:
|
|
1048
|
+
raise ValueError("Invalid iotype. Must be 'input' or 'output'.")
|
|
1049
|
+
if state not in ["on", "off", "none"]:
|
|
1050
|
+
raise ValueError("Invalid state. Must be 'on', 'off', or 'none'.")
|
|
1051
|
+
if channel_index is None:
|
|
1052
|
+
raise ValueError("Channel must be specified.")
|
|
1053
|
+
|
|
1054
|
+
basicio_interface = cast(BasicIOInterface, interface)
|
|
1055
|
+
|
|
1056
|
+
# Set forcing for the specified channel
|
|
1057
|
+
if iotype == "input":
|
|
1058
|
+
if channel_index in basicio_interface.input_channels:
|
|
1059
|
+
channel = basicio_interface.input_channels[channel_index]
|
|
1060
|
+
channel.force_enable = state in ["on", "off"]
|
|
1061
|
+
channel.force_value = state == "on"
|
|
1062
|
+
|
|
1063
|
+
# Update shared variable if forced
|
|
1064
|
+
if channel.force_enable:
|
|
1065
|
+
channel.variable_value = channel.force_value
|
|
1066
|
+
else:
|
|
1067
|
+
# If not forcing, reset the variable value to hardware value
|
|
1068
|
+
channel.variable_value = channel.hardware_value
|
|
1069
|
+
|
|
1070
|
+
if channel.shared_variable is not None:
|
|
1071
|
+
# Update the shared variable value if changed
|
|
1072
|
+
if (
|
|
1073
|
+
channel.shared_variable.data.model_dump()["value"]
|
|
1074
|
+
!= channel.variable_value
|
|
1075
|
+
):
|
|
1076
|
+
channel.shared_variable.update(
|
|
1077
|
+
{"value": channel.variable_value}
|
|
1078
|
+
)
|
|
1079
|
+
else:
|
|
1080
|
+
if channel_index in basicio_interface.output_channels:
|
|
1081
|
+
channel = basicio_interface.output_channels[channel_index]
|
|
1082
|
+
channel.force_enable = state in ["on", "off"]
|
|
1083
|
+
channel.force_value = state == "on"
|
|
1084
|
+
|
|
1085
|
+
@classmethod
|
|
1086
|
+
def load_from_json(cls, interface_data: Any) -> None:
|
|
1087
|
+
basicio_interface = BasicIOInterface.model_validate(interface_data)
|
|
1088
|
+
if cls.interface_controller is None:
|
|
1089
|
+
raise ValueError(
|
|
1090
|
+
"Interface controller not set. Call set_controller() first."
|
|
1091
|
+
)
|
|
1092
|
+
cls.interface_controller.add(basicio_interface)
|
|
1093
|
+
|
|
1094
|
+
@staticmethod
|
|
1095
|
+
def save_to_json(interface: Interface) -> str:
|
|
1096
|
+
return interface.model_dump_json()
|