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,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bell interface models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Literal, Optional
|
|
7
|
+
|
|
8
|
+
from opensignalbox.common.models import CamelModel
|
|
9
|
+
from opensignalbox.common.variables import SharedVariable
|
|
10
|
+
from opensignalbox.interface.models import (
|
|
11
|
+
Interface,
|
|
12
|
+
InterfaceBase,
|
|
13
|
+
InterfaceTypeEnum,
|
|
14
|
+
InterfaceUpdate,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BellConnectionType(str, Enum):
|
|
19
|
+
"""Connection types for Bell interfaces."""
|
|
20
|
+
|
|
21
|
+
MODBUS_ADAPTER = "ModbusAdapter"
|
|
22
|
+
DIRECT_TCP = "DirectTCP"
|
|
23
|
+
TEST = "Test" # For testing purposes only
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ForceStateEnum(str, Enum):
|
|
27
|
+
"""Enum for force states."""
|
|
28
|
+
|
|
29
|
+
ON = "on"
|
|
30
|
+
OFF = "off"
|
|
31
|
+
NONE = "none" # To remove forcing
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BellChannelConfig(CamelModel):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BellChannel(CamelModel):
|
|
39
|
+
"""Model for Bell channels."""
|
|
40
|
+
|
|
41
|
+
system_name: str
|
|
42
|
+
user_name: str
|
|
43
|
+
description: str = ""
|
|
44
|
+
tags: List[str] = []
|
|
45
|
+
in_count: int = 0
|
|
46
|
+
in_code: str = ""
|
|
47
|
+
in_shared_variable: Optional[SharedVariable] = None
|
|
48
|
+
out_count: int = 0
|
|
49
|
+
last_out_count: int = 0
|
|
50
|
+
out_code: str = ""
|
|
51
|
+
out_shared_variable_name: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BellInterfaceFields(CamelModel):
|
|
55
|
+
"""Mixin with Bell-specific fields."""
|
|
56
|
+
|
|
57
|
+
connection_type: BellConnectionType = BellConnectionType.MODBUS_ADAPTER
|
|
58
|
+
|
|
59
|
+
channels: dict[int, BellChannel] = {}
|
|
60
|
+
|
|
61
|
+
# Fields for modbus connection
|
|
62
|
+
modbus_address: Optional[int] = None
|
|
63
|
+
|
|
64
|
+
# Fields for TCP connection
|
|
65
|
+
tcp_host: Optional[str] = None
|
|
66
|
+
tcp_port: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BellInterfaceBase(Interface, BellInterfaceFields):
|
|
70
|
+
"""Base model with Bell-specific fields."""
|
|
71
|
+
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BellInterface(BellInterfaceBase):
|
|
76
|
+
"""Runtime model for Bell interfaces."""
|
|
77
|
+
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class BellInterfaceCreate(InterfaceBase, BellInterfaceFields):
|
|
82
|
+
"""API model for creating Bell interfaces."""
|
|
83
|
+
|
|
84
|
+
interface_type: Literal[InterfaceTypeEnum.BELL] = InterfaceTypeEnum.BELL
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BellInterfacePublic(BellInterfaceBase):
|
|
88
|
+
"""API model for Bell interface responses."""
|
|
89
|
+
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BellInterfaceUpdate(InterfaceUpdate):
|
|
94
|
+
"""API model for updating Bell interfaces."""
|
|
95
|
+
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class BellForceMessage(CamelModel):
|
|
100
|
+
"""Message for setting force state on Bell channels."""
|
|
101
|
+
|
|
102
|
+
channel: int
|
|
103
|
+
state: ForceStateEnum
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface controller for managing all interfaces in the opensignalbox-interface module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Type
|
|
9
|
+
|
|
10
|
+
from anyio import sleep
|
|
11
|
+
from opensignalbox.common.messaging import get_messager
|
|
12
|
+
from opensignalbox.common.variables import SharedVariableCollection
|
|
13
|
+
from opensignalbox.interface.adapters.controller import AdapterController
|
|
14
|
+
from opensignalbox.interface.interfaces.base import InterfaceHandler
|
|
15
|
+
from opensignalbox.interface.interfaces.basicio.handler import BasicIOInterfaceHandler
|
|
16
|
+
from opensignalbox.interface.interfaces.bell.handler import BellInterfaceHandler
|
|
17
|
+
from opensignalbox.interface.models import (
|
|
18
|
+
Interface,
|
|
19
|
+
InterfaceTypeEnum,
|
|
20
|
+
InterfaceTypeInfo,
|
|
21
|
+
ModuleConfig,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
messager = get_messager()
|
|
26
|
+
|
|
27
|
+
MAX_RETRIES = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InterfaceController:
|
|
31
|
+
"""Controller for managing all interfaces."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
config_path: Path,
|
|
36
|
+
output_variables: SharedVariableCollection,
|
|
37
|
+
adapters: AdapterController,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.interfaces: Dict[str, Interface] = {}
|
|
40
|
+
self.config_path = config_path
|
|
41
|
+
self.output_variables = output_variables
|
|
42
|
+
self.input_variable_subs: Dict[str, List[Any]] = {}
|
|
43
|
+
self.adapters = adapters
|
|
44
|
+
self.module_config = ModuleConfig()
|
|
45
|
+
self.output_sync_countdown: int = 0
|
|
46
|
+
|
|
47
|
+
# Initialize interface handlers dictionary (will be populated by register_handler)
|
|
48
|
+
self.interface_handlers: Dict[InterfaceTypeEnum, Type[InterfaceHandler]] = {}
|
|
49
|
+
|
|
50
|
+
# Dictionary to store interface-specific routers
|
|
51
|
+
self.interface_routers: Dict[InterfaceTypeEnum, Any] = {}
|
|
52
|
+
|
|
53
|
+
def register_handler(
|
|
54
|
+
self, interface_type: InterfaceTypeEnum, handler: Type[InterfaceHandler]
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Register a handler for an interface type.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
interface_type: The type of interface this handler handles
|
|
60
|
+
handler: The handler class for this interface type
|
|
61
|
+
"""
|
|
62
|
+
# Initialize the handler with controller references
|
|
63
|
+
handler.init(
|
|
64
|
+
output_variables=self.output_variables,
|
|
65
|
+
input_variable_subs=self.input_variable_subs,
|
|
66
|
+
variable_update_callback=self.variable_update_callback,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Set the controllers for the handler
|
|
70
|
+
handler.set_controllers(self, self.adapters)
|
|
71
|
+
|
|
72
|
+
# Register the handler
|
|
73
|
+
self.interface_handlers[interface_type] = handler
|
|
74
|
+
|
|
75
|
+
# Check if the handler provides a router
|
|
76
|
+
router = handler.get_router()
|
|
77
|
+
if router:
|
|
78
|
+
self.interface_routers[interface_type] = router
|
|
79
|
+
|
|
80
|
+
def register_interface_handlers(self) -> None:
|
|
81
|
+
"""Register all interface handlers."""
|
|
82
|
+
self.register_handler(InterfaceTypeEnum.BASICIO, BasicIOInterfaceHandler)
|
|
83
|
+
self.register_handler(InterfaceTypeEnum.BELL, BellInterfaceHandler)
|
|
84
|
+
|
|
85
|
+
def add(self, interface: Interface) -> Interface:
|
|
86
|
+
"""Add an interface to the controller.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
interface: The interface to add
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The initialized interface
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
FileExistsError: If an interface with the same system name already exists
|
|
96
|
+
TypeError: If the interface type is unknown
|
|
97
|
+
"""
|
|
98
|
+
if interface.system_name in self.interfaces:
|
|
99
|
+
raise FileExistsError(
|
|
100
|
+
f"Interface with system name '{interface.system_name}' already exists"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Use the handler based on interface type
|
|
105
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
106
|
+
if not handler:
|
|
107
|
+
raise TypeError(f"Unknown interface type: {interface.interface_type}")
|
|
108
|
+
|
|
109
|
+
# Handler.add already does type conversion if needed
|
|
110
|
+
specialized_interface = handler.add(interface)
|
|
111
|
+
self.interfaces[interface.system_name] = specialized_interface
|
|
112
|
+
return specialized_interface
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to add interface: {e}")
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
def get_all(self) -> Dict[str, Interface]:
|
|
118
|
+
"""Get all interfaces.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dictionary of interfaces keyed by system name
|
|
122
|
+
"""
|
|
123
|
+
return self.interfaces
|
|
124
|
+
|
|
125
|
+
def get_interface_type_infos(self) -> List["InterfaceTypeInfo"]:
|
|
126
|
+
"""Get all available interface types with their metadata.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of interface type info objects
|
|
130
|
+
"""
|
|
131
|
+
from opensignalbox.interface.models import InterfaceTypeInfo
|
|
132
|
+
|
|
133
|
+
type_infos = []
|
|
134
|
+
for interface_type, handler_class in self.interface_handlers.items():
|
|
135
|
+
# Get metadata from the handler class
|
|
136
|
+
metadata = getattr(handler_class, "metadata", {})
|
|
137
|
+
|
|
138
|
+
type_info = InterfaceTypeInfo(
|
|
139
|
+
interface_type=interface_type,
|
|
140
|
+
name=metadata.get("name", interface_type.value),
|
|
141
|
+
description=metadata.get(
|
|
142
|
+
"description", f"{interface_type.value} interface"
|
|
143
|
+
),
|
|
144
|
+
supported_connection_types=metadata.get(
|
|
145
|
+
"supported_connection_types", []
|
|
146
|
+
),
|
|
147
|
+
required_fields=metadata.get("required_fields", []),
|
|
148
|
+
optional_fields=metadata.get("optional_fields", []),
|
|
149
|
+
)
|
|
150
|
+
type_infos.append(type_info)
|
|
151
|
+
|
|
152
|
+
return type_infos
|
|
153
|
+
|
|
154
|
+
def get(self, system_name: str) -> Interface:
|
|
155
|
+
"""Get an interface by system name.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
system_name: The system name of the interface
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The interface
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
KeyError: If no interface with the given system name exists
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
return self.interfaces[system_name]
|
|
168
|
+
except KeyError as exc:
|
|
169
|
+
raise KeyError(f"Cannot find interface '{system_name}'") from exc
|
|
170
|
+
|
|
171
|
+
def update(self, system_name: str, fields: Dict[str, Any]) -> Interface:
|
|
172
|
+
"""Update an interface.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
system_name: The system name of the interface to update
|
|
176
|
+
fields: The fields to update
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The updated interface
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
KeyError: If no interface with the given system name exists
|
|
183
|
+
TypeError: If the interface type is unknown
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
interface = self.interfaces[system_name]
|
|
187
|
+
# Use the handler based on interface type
|
|
188
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
189
|
+
if not handler:
|
|
190
|
+
raise TypeError(f"Unknown interface type: {interface.interface_type}")
|
|
191
|
+
# Update the interface using the handler
|
|
192
|
+
handler.update(interface, fields)
|
|
193
|
+
return self.interfaces[system_name]
|
|
194
|
+
except KeyError as exc:
|
|
195
|
+
raise KeyError(f"Cannot find interface '{system_name}'") from exc
|
|
196
|
+
|
|
197
|
+
def remove(self, system_name: str) -> None:
|
|
198
|
+
"""Remove an interface.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
system_name: The system name of the interface to remove
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
KeyError: If no interface with the given system name exists
|
|
205
|
+
TypeError: If the interface type is unknown
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
interface = self.interfaces[system_name]
|
|
209
|
+
# Use the handler based on interface type
|
|
210
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
211
|
+
if not handler:
|
|
212
|
+
raise TypeError(f"Unknown interface type: {interface.interface_type}")
|
|
213
|
+
# Remove the interface using the handler
|
|
214
|
+
handler.remove(interface)
|
|
215
|
+
del self.interfaces[system_name]
|
|
216
|
+
except KeyError as exc:
|
|
217
|
+
raise KeyError(f"Cannot find interface '{system_name}'") from exc
|
|
218
|
+
|
|
219
|
+
def variable_update_callback(self, variable: str, json_data: str) -> None:
|
|
220
|
+
"""Callback for variable updates.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
variable: The name of the variable that was updated
|
|
224
|
+
json_data: The serialized JSON data of the variable update
|
|
225
|
+
"""
|
|
226
|
+
# Check if we have any interfaces subscribed to this variable
|
|
227
|
+
if variable in self.input_variable_subs:
|
|
228
|
+
for iface_tuple in self.input_variable_subs[variable]:
|
|
229
|
+
interface_name = iface_tuple[0] # Assuming the tuple structure
|
|
230
|
+
|
|
231
|
+
# Get the interface and its handler
|
|
232
|
+
try:
|
|
233
|
+
interface = self.interfaces[interface_name]
|
|
234
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
235
|
+
if handler:
|
|
236
|
+
# Let the handler process the variable update
|
|
237
|
+
handler.handle_variable_update(interface, variable, json_data)
|
|
238
|
+
except (KeyError, TypeError) as e:
|
|
239
|
+
logger.error(f"Error handling variable update: {e}")
|
|
240
|
+
|
|
241
|
+
def sync_interfaces(self) -> None:
|
|
242
|
+
"""Synchronize all interfaces with their adapters."""
|
|
243
|
+
for interface_name, interface in self.interfaces.items():
|
|
244
|
+
# logger.debug(f"Syncing interface '{interface_name}', with enabled={interface.enabled}, connected={interface.is_connected}")
|
|
245
|
+
if not interface.enabled or not interface.is_connected:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
249
|
+
if not handler:
|
|
250
|
+
logger.error(
|
|
251
|
+
f"Unknown interface type for '{interface_name}': {interface.interface_type}"
|
|
252
|
+
)
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
retries = 0
|
|
256
|
+
while retries < MAX_RETRIES:
|
|
257
|
+
try:
|
|
258
|
+
# Attempt to sync the interface
|
|
259
|
+
# Read from hardware
|
|
260
|
+
handler.read_data(interface)
|
|
261
|
+
# Write to hardware
|
|
262
|
+
handler.write_data(interface)
|
|
263
|
+
interface.error = None # Clear any previous error
|
|
264
|
+
break
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Error syncing interface '{interface_name}': {e}")
|
|
267
|
+
logger.info(
|
|
268
|
+
f"Retrying sync for interface '{interface_name}' ({retries + 1}/{MAX_RETRIES})"
|
|
269
|
+
)
|
|
270
|
+
retries += 1
|
|
271
|
+
if retries >= MAX_RETRIES:
|
|
272
|
+
interface.error = str(e)
|
|
273
|
+
logger.error(
|
|
274
|
+
f"Max retries reached for interface '{interface_name}', disabling it."
|
|
275
|
+
)
|
|
276
|
+
interface.enabled = False
|
|
277
|
+
handler.disconnect(interface) # Ensure we disconnect on failure
|
|
278
|
+
handler.connect(interface) # Attempt to reconnect
|
|
279
|
+
|
|
280
|
+
async def tick(self) -> None:
|
|
281
|
+
"""Perform a tick update for interfaces not synchronized on systick."""
|
|
282
|
+
self.sync_interfaces()
|
|
283
|
+
await sleep(self.module_config.input_update_period)
|
|
284
|
+
|
|
285
|
+
def load_from_json(self) -> None:
|
|
286
|
+
"""Load configuration from a JSON file."""
|
|
287
|
+
|
|
288
|
+
# First the module config
|
|
289
|
+
self.config_filepath = self.config_path / "module_config.json"
|
|
290
|
+
if not self.config_filepath.exists():
|
|
291
|
+
logger.warning(f"Module config file not found: {self.config_filepath}")
|
|
292
|
+
logger.info("Creating default module config")
|
|
293
|
+
# Create default config if it doesn't exist
|
|
294
|
+
self.module_config = ModuleConfig()
|
|
295
|
+
self.save_module_config()
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
with open(self.config_filepath, "r") as f:
|
|
299
|
+
config_data = json.load(f)
|
|
300
|
+
self.module_config.input_update_period = config_data.get(
|
|
301
|
+
"input_update_period", 0.1
|
|
302
|
+
)
|
|
303
|
+
self.module_config.output_update_period = config_data.get(
|
|
304
|
+
"output_update_period", 1
|
|
305
|
+
)
|
|
306
|
+
self.module_config.sync_on_systick = config_data.get(
|
|
307
|
+
"sync_on_systick", False
|
|
308
|
+
)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(
|
|
311
|
+
f"Error loading module config from {self.config_filepath}, using defaults: {e}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Now load each interface in the interfaces directory
|
|
315
|
+
interfaces_config_dir = self.config_path / "interfaces"
|
|
316
|
+
if not interfaces_config_dir.exists():
|
|
317
|
+
logger.warning(
|
|
318
|
+
f"Interfaces config directory not found: {interfaces_config_dir}"
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
# Clear existing interfaces
|
|
323
|
+
for interface in self.interfaces.values():
|
|
324
|
+
try:
|
|
325
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
326
|
+
if handler:
|
|
327
|
+
handler.remove(interface)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.error(f"Error removing interface '{interface.system_name}': {e}")
|
|
330
|
+
|
|
331
|
+
for interface_file in interfaces_config_dir.glob("*.json"):
|
|
332
|
+
try:
|
|
333
|
+
with open(interface_file, "r") as f:
|
|
334
|
+
interface_data = json.load(f)
|
|
335
|
+
# Use the handler based on interface type
|
|
336
|
+
interface_name = interface_data.get("system_name")
|
|
337
|
+
interface_type = interface_data.get("interface_type")
|
|
338
|
+
adapter_name = interface_data.get("adapter_name")
|
|
339
|
+
|
|
340
|
+
if adapter_name:
|
|
341
|
+
adapter = self.adapters.get(adapter_name)
|
|
342
|
+
if not adapter:
|
|
343
|
+
logger.error(
|
|
344
|
+
f"Adapter '{adapter_name}' not found for interface '{interface_name}'"
|
|
345
|
+
)
|
|
346
|
+
raise KeyError(f"Adapter '{adapter_name}' not found")
|
|
347
|
+
|
|
348
|
+
handler = self.interface_handlers.get(interface_type)
|
|
349
|
+
if not handler:
|
|
350
|
+
logger.error(
|
|
351
|
+
f"Unknown interface type for '{interface_name}': {interface_type}"
|
|
352
|
+
)
|
|
353
|
+
raise TypeError(f"Unknown interface type: {interface_type}")
|
|
354
|
+
handler.load_from_json(interface_data)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.error(f"Error loading interface from {interface_file}: {e}")
|
|
357
|
+
|
|
358
|
+
def save_module_config(self) -> None:
|
|
359
|
+
"""Save the module configuration to a JSON file."""
|
|
360
|
+
try:
|
|
361
|
+
with open(self.config_filepath, "w") as f:
|
|
362
|
+
json.dump(self.module_config.model_dump(), f, indent=2)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Error saving module config to {self.config_filepath}: {e}")
|
|
365
|
+
|
|
366
|
+
def save_to_json(self, suffix: str | None = None) -> None:
|
|
367
|
+
"""Save configuration to a JSON file.
|
|
368
|
+
Args:
|
|
369
|
+
suffix: Optional suffix to append to the interface filenames
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
# Save the module config first
|
|
373
|
+
self.save_module_config()
|
|
374
|
+
|
|
375
|
+
# Save each interface
|
|
376
|
+
interfaces_config_dir = self.config_path / "interfaces"
|
|
377
|
+
interfaces_config_dir.mkdir(parents=True, exist_ok=True)
|
|
378
|
+
|
|
379
|
+
for interface in self.interfaces.values():
|
|
380
|
+
interface_file = (
|
|
381
|
+
interfaces_config_dir / f"{interface.system_name}{suffix or ''}.json"
|
|
382
|
+
)
|
|
383
|
+
try:
|
|
384
|
+
with open(interface_file, "w") as f:
|
|
385
|
+
# Use the handler based on interface type
|
|
386
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
387
|
+
if handler:
|
|
388
|
+
json.dump(handler.save_to_json(interface), f, indent=2)
|
|
389
|
+
else:
|
|
390
|
+
logger.error(
|
|
391
|
+
f"Unknown interface type for '{interface.system_name}': {interface.interface_type}"
|
|
392
|
+
)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
logger.error(
|
|
395
|
+
f"Error saving interface '{interface.system_name}' to {interface_file}: {e}"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def get_interface_routers(self) -> Dict[InterfaceTypeEnum, Any]:
|
|
399
|
+
"""Get all registered interface routers.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Dictionary of FastAPI routers keyed by interface type
|
|
403
|
+
"""
|
|
404
|
+
return self.interface_routers
|
|
405
|
+
|
|
406
|
+
def connect_all(self) -> None:
|
|
407
|
+
"""Connect all interfaces."""
|
|
408
|
+
for interface in self.interfaces.values():
|
|
409
|
+
if not interface.enabled:
|
|
410
|
+
continue
|
|
411
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
412
|
+
if handler:
|
|
413
|
+
retries = 0
|
|
414
|
+
while retries < MAX_RETRIES and not interface.is_connected:
|
|
415
|
+
retries += 1
|
|
416
|
+
logger.info(
|
|
417
|
+
f"Connecting interface '{interface.system_name}' (attempt {retries}/{MAX_RETRIES})"
|
|
418
|
+
)
|
|
419
|
+
handler.connect(interface)
|
|
420
|
+
if interface.is_connected:
|
|
421
|
+
logger.info(
|
|
422
|
+
f"Interface '{interface.system_name}' connected successfully"
|
|
423
|
+
)
|
|
424
|
+
interface.error = None
|
|
425
|
+
break
|
|
426
|
+
else:
|
|
427
|
+
logger.error(
|
|
428
|
+
f"Failed to connect interface '{interface.system_name}'"
|
|
429
|
+
)
|
|
430
|
+
interface.error = "Connection failed"
|
|
431
|
+
interface.enabled = False
|
|
432
|
+
else:
|
|
433
|
+
logger.error(
|
|
434
|
+
f"Unknown interface type for '{interface.system_name}': {interface.interface_type}"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def disconnect_all(self) -> None:
|
|
438
|
+
"""Disconnect all interfaces."""
|
|
439
|
+
for interface in self.interfaces.values():
|
|
440
|
+
handler = self.interface_handlers.get(interface.interface_type)
|
|
441
|
+
if handler:
|
|
442
|
+
handler.disconnect(interface)
|
|
443
|
+
else:
|
|
444
|
+
logger.error(
|
|
445
|
+
f"Unknown interface type for '{interface.system_name}': {interface.interface_type}"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def get_adapter_for_interface(self, interface: Interface) -> Optional[Any]:
|
|
449
|
+
"""Get the adapter instance for a given interface.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
interface: The interface to get the adapter for
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
The adapter instance, or None if no adapter is assigned
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
KeyError: If the adapter name is not found
|
|
459
|
+
"""
|
|
460
|
+
if not hasattr(interface, "adapter_name") or not interface.adapter_name:
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
adapter = self.adapters.get(interface.adapter_name)
|
|
464
|
+
if not adapter:
|
|
465
|
+
raise KeyError(f"Adapter '{interface.adapter_name}' not found")
|
|
466
|
+
|
|
467
|
+
return adapter
|