ioiocore 0.0.1__tar.gz
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.
- ioiocore-0.0.1/CHANGELOG.md +11 -0
- ioiocore-0.0.1/HOW_TO_BUILD.md +50 -0
- ioiocore-0.0.1/HOW_TO_BUILD.pdf +0 -0
- ioiocore-0.0.1/LICENSE +20 -0
- ioiocore-0.0.1/MANIFEST.in +1 -0
- ioiocore-0.0.1/PKG-INFO +37 -0
- ioiocore-0.0.1/README.md +22 -0
- ioiocore-0.0.1/ioiocore/__init__.py +22 -0
- ioiocore-0.0.1/ioiocore/__version__.py +12 -0
- ioiocore-0.0.1/ioiocore/pattern/__init__.py +0 -0
- ioiocore-0.0.1/ioiocore/pattern/frequency_generator.py +44 -0
- ioiocore-0.0.1/ioiocore/pattern/node.py +165 -0
- ioiocore-0.0.1/ioiocore/pattern/pipeline.py +96 -0
- ioiocore-0.0.1/ioiocore/pattern/sources.py +64 -0
- ioiocore-0.0.1/ioiocore/pattern/src/__init__.py +0 -0
- ioiocore-0.0.1/ioiocore/pattern/src/i_node.py +82 -0
- ioiocore-0.0.1/ioiocore/pattern/src/i_port.py +32 -0
- ioiocore-0.0.1/ioiocore/pattern/src/o_node.py +32 -0
- ioiocore-0.0.1/ioiocore/pattern/src/o_port.py +35 -0
- ioiocore-0.0.1/ioiocore/requirements.txt +1 -0
- ioiocore-0.0.1/ioiocore.egg-info/PKG-INFO +37 -0
- ioiocore-0.0.1/ioiocore.egg-info/SOURCES.txt +36 -0
- ioiocore-0.0.1/ioiocore.egg-info/dependency_links.txt +1 -0
- ioiocore-0.0.1/ioiocore.egg-info/top_level.txt +2 -0
- ioiocore-0.0.1/setup.cfg +7 -0
- ioiocore-0.0.1/setup.py +43 -0
- ioiocore-0.0.1/test/__init__.py +0 -0
- ioiocore-0.0.1/test/node_serialization.py +76 -0
- ioiocore-0.0.1/test/node_tests.py +89 -0
- ioiocore-0.0.1/test/node_tests_connect_by_id.py +116 -0
- ioiocore-0.0.1/test/node_tests_connect_by_name.py +140 -0
- ioiocore-0.0.1/test/node_tests_propagation.py +251 -0
- ioiocore-0.0.1/test/pipeline_tests.py +142 -0
- ioiocore-0.0.1/test/run_all.py +46 -0
- ioiocore-0.0.1/test/sources_test.py +157 -0
- ioiocore-0.0.1/test/template_nodes.py +103 -0
- ioiocore-0.0.1/test/utilities.py +12 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Buld instructions
|
|
2
|
+
## Prerequisites
|
|
3
|
+
- Visual Studio Code<br>
|
|
4
|
+
- Python (Microsoft) v2024.16.1 Extension<br>
|
|
5
|
+
- Python Debugger (Microsoft) v2024.10.00<br>
|
|
6
|
+
- Pylance (Microsoft) v2024.10.01 Extension<br>
|
|
7
|
+
- Markdown Preview Github Styling (Matt Bierner) v2.1.0 Extension<br>
|
|
8
|
+
- Python >= 3.11<br>
|
|
9
|
+
- pip (must be accessible from commandline)<br>
|
|
10
|
+
**Note**: ensure that the pip package is installed ```python -m ensurepip --upgrade```<br>
|
|
11
|
+
- pipreqs (must be accessible from commandline)<br>
|
|
12
|
+
**Note**: ensure that the pipreqs package is installed ```pip install pipreqs```<br>
|
|
13
|
+
- build (must be accessible from commandline)<br>
|
|
14
|
+
**Note**: ensure that the build package is installed ```pip install build```<br>
|
|
15
|
+
- setuptools<br>
|
|
16
|
+
- numpy (for tests only)<br>
|
|
17
|
+
|
|
18
|
+
## Create requirements file
|
|
19
|
+
|
|
20
|
+
Create or update requirements.txt<br>
|
|
21
|
+
```pipreqs ./ioiocore --force```
|
|
22
|
+
|
|
23
|
+
## Build
|
|
24
|
+
Update version number:<br>
|
|
25
|
+
```./ioiocore/___version__.py```
|
|
26
|
+
|
|
27
|
+
build wheel:<br>
|
|
28
|
+
```python -m build```<br>
|
|
29
|
+
distributables are built to the ```./dist``` folder
|
|
30
|
+
|
|
31
|
+
### Upload to pypi
|
|
32
|
+
Upload to pypi using the following command: <br>
|
|
33
|
+
```python -m twine upload --repository pypi dist/*```
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
### Manual installation
|
|
37
|
+
Copy ```.whl``` file from ```./dist``` folder and install from command line: <br>
|
|
38
|
+
```pip install ./dist/ioiocore-0.0.1-py3-none-any.whl```
|
|
39
|
+
|
|
40
|
+
### Installation using PyPi
|
|
41
|
+
Package can be downloaded, installed and updated via pypi if uploaded ([Upload to pypi](#upload-to-pypi)).<br>
|
|
42
|
+
```pip install ioiocore```<br>
|
|
43
|
+
```pip install ioiocore --upgrade```
|
|
44
|
+
|
|
45
|
+
### Uninstall
|
|
46
|
+
```pip uninstall ioiocore```
|
|
47
|
+
|
|
48
|
+
## Run tests
|
|
49
|
+
Go to project base folder and execute:<br>
|
|
50
|
+
```python ./test/run_all.py```
|
|
Binary file
|
ioiocore-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2024 g.tec medical engineering GmbH
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include *.md *.pdf LICENSE .\\ioiocore\\requirements.txt CHANGELOG
|
ioiocore-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ioiocore
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A realtime processing framework for python
|
|
5
|
+
Home-page: https://www.gtec.at
|
|
6
|
+
Author: g.tec medical engineering GmbH
|
|
7
|
+
Author-email: support@gtec.at
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
|
|
16
|
+
# IOIOCore
|
|
17
|
+
|
|
18
|
+
IOIOCore is general purpose data propagation framework for realtime applications written in python.
|
|
19
|
+
|
|
20
|
+
# Node
|
|
21
|
+
A node is a individual unit or component that performs a specific task in a larger system. Each node typically has inputs, outputs, and processing logic that transforms data as it moves through the system. Nodes are interconnected to form a pipeline where the output of one node becomes the input of another, enabling complex workflows or data transformations.
|
|
22
|
+
|
|
23
|
+
## Input and Output Ports
|
|
24
|
+
Nodes receive data through input ports and send processed data out through output ports. The number and type of ports may vary depending on the node's purpose.
|
|
25
|
+
|
|
26
|
+
## Processing Logic
|
|
27
|
+
Each node contains specific logic or algorithms to process the data it receives. This could range from basic transformations (like filtering or mapping) to more complex operations (like aggregation or machine learning inference).
|
|
28
|
+
|
|
29
|
+
Create specific nodes by deriving from the Node parent class. Define input and output ports and implement the processing logic for child objects.
|
|
30
|
+
|
|
31
|
+
# Sources
|
|
32
|
+
|
|
33
|
+
TBD
|
|
34
|
+
|
|
35
|
+
# Pipeline
|
|
36
|
+
|
|
37
|
+
TBD
|
ioiocore-0.0.1/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# IOIOCore
|
|
2
|
+
|
|
3
|
+
IOIOCore is general purpose data propagation framework for realtime applications written in python.
|
|
4
|
+
|
|
5
|
+
# Node
|
|
6
|
+
A node is a individual unit or component that performs a specific task in a larger system. Each node typically has inputs, outputs, and processing logic that transforms data as it moves through the system. Nodes are interconnected to form a pipeline where the output of one node becomes the input of another, enabling complex workflows or data transformations.
|
|
7
|
+
|
|
8
|
+
## Input and Output Ports
|
|
9
|
+
Nodes receive data through input ports and send processed data out through output ports. The number and type of ports may vary depending on the node's purpose.
|
|
10
|
+
|
|
11
|
+
## Processing Logic
|
|
12
|
+
Each node contains specific logic or algorithms to process the data it receives. This could range from basic transformations (like filtering or mapping) to more complex operations (like aggregation or machine learning inference).
|
|
13
|
+
|
|
14
|
+
Create specific nodes by deriving from the Node parent class. Define input and output ports and implement the processing logic for child objects.
|
|
15
|
+
|
|
16
|
+
# Sources
|
|
17
|
+
|
|
18
|
+
TBD
|
|
19
|
+
|
|
20
|
+
# Pipeline
|
|
21
|
+
|
|
22
|
+
TBD
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
ioiocore
|
|
4
|
+
-------
|
|
5
|
+
|
|
6
|
+
A realtime processing framework for python
|
|
7
|
+
|
|
8
|
+
:copyright: (c) 2024 g.tec medical engineering GmbH
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# compatibility
|
|
13
|
+
from __future__ import absolute_import, division, print_function
|
|
14
|
+
|
|
15
|
+
# get version
|
|
16
|
+
from .__version__ import __version__
|
|
17
|
+
|
|
18
|
+
# allow lazy loading
|
|
19
|
+
#pattern
|
|
20
|
+
from .pattern.node import Node
|
|
21
|
+
from .pattern.sources import ManualSource, FixedFrequencySource, DeviceSource, FrequencyGenerator
|
|
22
|
+
from .pattern.pipeline import Pipeline
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
class FrequencyGenerator(ABC):
|
|
6
|
+
def __init__(self, frequencyHz : float):
|
|
7
|
+
self.__realtimeClockRunning = False
|
|
8
|
+
self.__realtimeClockThread = None
|
|
9
|
+
self.__targetDt = 1000/frequencyHz
|
|
10
|
+
self.__timestampStart = None
|
|
11
|
+
self.__timestamp = 0
|
|
12
|
+
self.__samplesSent = 0
|
|
13
|
+
|
|
14
|
+
def __del__(self):
|
|
15
|
+
self.stop()
|
|
16
|
+
|
|
17
|
+
def start(self):
|
|
18
|
+
if not self.__realtimeClockRunning:
|
|
19
|
+
self.__realtimeClockRunning = True
|
|
20
|
+
self.__realtimeClockThread = threading.Thread(target=self.__updateThread_DoWork, daemon=True)
|
|
21
|
+
self.__realtimeClockThread.start()
|
|
22
|
+
|
|
23
|
+
def stop(self):
|
|
24
|
+
if self.__realtimeClockRunning:
|
|
25
|
+
self.__realtimeClockRunning = False
|
|
26
|
+
self.__realtimeClockThread.join(500)
|
|
27
|
+
|
|
28
|
+
def __updateThread_DoWork(self):
|
|
29
|
+
if self.__timestampStart is None:
|
|
30
|
+
self.__timestampStart = time.time()*1000
|
|
31
|
+
while self.__realtimeClockRunning:
|
|
32
|
+
self.__timestamp = time.time()*1000 - self.__timestampStart
|
|
33
|
+
|
|
34
|
+
if self.__samplesSent >= round(self.__timestamp / self.__targetDt):
|
|
35
|
+
time.sleep(self.__targetDt /1000)
|
|
36
|
+
else:
|
|
37
|
+
samplesToSend = round(self.__timestamp / self.__targetDt) - self.__samplesSent
|
|
38
|
+
for i in range(0, samplesToSend):
|
|
39
|
+
self.update()
|
|
40
|
+
self.__samplesSent += 1
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def update(self):
|
|
44
|
+
pass
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from .src.i_node import INode
|
|
2
|
+
from .src.o_node import ONode
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
class Node():
|
|
7
|
+
class __INode(INode):
|
|
8
|
+
def __init__(self, updateFcn, inputPorts = None ):
|
|
9
|
+
super().__init__(inputPorts)
|
|
10
|
+
self.__updateFcn = updateFcn
|
|
11
|
+
|
|
12
|
+
def __del__(self):
|
|
13
|
+
self.__updateFcn = None
|
|
14
|
+
super().__del__()
|
|
15
|
+
|
|
16
|
+
def update(self):
|
|
17
|
+
data = []
|
|
18
|
+
for i in range(0,len(self.InputPorts)):
|
|
19
|
+
if(self.InputPorts[i].DataCount > 0):
|
|
20
|
+
data.append(self.InputPorts[i].read())
|
|
21
|
+
else:
|
|
22
|
+
data.append(None)
|
|
23
|
+
self.__updateFcn(data)
|
|
24
|
+
|
|
25
|
+
class __ONode(ONode):
|
|
26
|
+
def __init__(self, outputPorts = None ):
|
|
27
|
+
super().__init__(outputPorts)
|
|
28
|
+
|
|
29
|
+
def __del__(self):
|
|
30
|
+
super().__del__()
|
|
31
|
+
|
|
32
|
+
def write_internal(self, data):
|
|
33
|
+
if len(data) is not len(self.OutputPorts):
|
|
34
|
+
raise ValueError('Number of OutputPorts do not match received data.')
|
|
35
|
+
|
|
36
|
+
for i in range(0,len(data)):
|
|
37
|
+
self.write(i, data[i])
|
|
38
|
+
|
|
39
|
+
def __init__(self, inputPorts : list[str] = None , outputPorts : list[str] = None):
|
|
40
|
+
if inputPorts is None and outputPorts is None:
|
|
41
|
+
raise ValueError('Node without inputs and outputs is not allowed. \'inputPorts\' and/or \'outputPorts\' must be defined.')
|
|
42
|
+
if inputPorts is not None:
|
|
43
|
+
duplicates = self.__checkForDuplicates(inputPorts)
|
|
44
|
+
if duplicates:
|
|
45
|
+
raise ValueError(f'Duplicate input port names found {duplicates}. Every port must feature a unique name')
|
|
46
|
+
if outputPorts is not None:
|
|
47
|
+
duplicates = self.__checkForDuplicates(outputPorts)
|
|
48
|
+
if duplicates:
|
|
49
|
+
raise ValueError(f'Duplicate input port names found {duplicates}. Every port must feature a unique name')
|
|
50
|
+
if inputPorts is not None and outputPorts is not None:
|
|
51
|
+
duplicates = self.__checkForDuplicates(outputPorts + inputPorts)
|
|
52
|
+
if len(duplicates) > 0:
|
|
53
|
+
raise ValueError(f'Duplicate port names found {duplicates}. Every port must feature a unique name')
|
|
54
|
+
self.__inode = None
|
|
55
|
+
self.__onode = None
|
|
56
|
+
if inputPorts is not None:
|
|
57
|
+
self.__inode = self.__INode(self.__update, inputPorts)
|
|
58
|
+
if outputPorts is not None:
|
|
59
|
+
self.__onode = self.__ONode(outputPorts)
|
|
60
|
+
|
|
61
|
+
def __checkForDuplicates(self, l):
|
|
62
|
+
seen = set()
|
|
63
|
+
duplicates = set()
|
|
64
|
+
for item in l:
|
|
65
|
+
if item in seen:
|
|
66
|
+
duplicates.add(item)
|
|
67
|
+
else:
|
|
68
|
+
seen.add(item)
|
|
69
|
+
return duplicates
|
|
70
|
+
|
|
71
|
+
def __del__(self):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def __dict__(self):
|
|
75
|
+
oPorts = {}
|
|
76
|
+
iPorts = {}
|
|
77
|
+
if self.__onode is not None:
|
|
78
|
+
cnt = 0
|
|
79
|
+
for op in self.__onode.OutputPorts:
|
|
80
|
+
oPorts[cnt] = op.PortName
|
|
81
|
+
cnt+=1
|
|
82
|
+
if self.__inode is not None:
|
|
83
|
+
cnt = 0
|
|
84
|
+
for ip in self.__inode.InputPorts:
|
|
85
|
+
iPorts[cnt] = ip.PortName
|
|
86
|
+
cnt+=1
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"name" : self.__class__.__name__,
|
|
90
|
+
"outputs" : oPorts,
|
|
91
|
+
"inputs" : iPorts
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
return json.dumps(self.__dict__(), indent=4)
|
|
96
|
+
|
|
97
|
+
def __get_output_port_index_by_name(self, node, outputPortName):
|
|
98
|
+
port = None
|
|
99
|
+
for p in node.__onode.OutputPorts:
|
|
100
|
+
if p.PortName == outputPortName:
|
|
101
|
+
port = p
|
|
102
|
+
break
|
|
103
|
+
if port is None:
|
|
104
|
+
raise ValueError(f'Could not find input port named \'{outputPortName}\'.')
|
|
105
|
+
|
|
106
|
+
return node.__onode.OutputPorts.index(port)
|
|
107
|
+
|
|
108
|
+
def __get_input_port_index_by_name(self, node, inputPortName):
|
|
109
|
+
port = None
|
|
110
|
+
for p in node.__inode.InputPorts:
|
|
111
|
+
if p.PortName == inputPortName:
|
|
112
|
+
port = p
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if port is None:
|
|
116
|
+
raise ValueError(f'Could not find output port named\'{inputPortName}\'.')
|
|
117
|
+
|
|
118
|
+
return node.__inode.InputPorts.index(port)
|
|
119
|
+
|
|
120
|
+
def __update(self, data):
|
|
121
|
+
if self.__inode is None:
|
|
122
|
+
raise TypeError('No input ports defined. Update is only available if input ports are defined.')
|
|
123
|
+
|
|
124
|
+
dataOut = self.update(data)
|
|
125
|
+
|
|
126
|
+
if self.__onode is not None:
|
|
127
|
+
self.write(dataOut)
|
|
128
|
+
|
|
129
|
+
def write(self, data):
|
|
130
|
+
if self.__onode is None:
|
|
131
|
+
raise TypeError('No output ports defined. Write is only available if output ports are defined.')
|
|
132
|
+
|
|
133
|
+
self.__onode.write_internal(data)
|
|
134
|
+
|
|
135
|
+
def connect_by_name(self, outputPortName, node, inputPortName):
|
|
136
|
+
if self.__onode is None:
|
|
137
|
+
raise ValueError('Connect is only available if outputs are defined.')
|
|
138
|
+
io = self.__get_output_port_index_by_name(self, outputPortName)
|
|
139
|
+
ii = self.__get_input_port_index_by_name(node, inputPortName)
|
|
140
|
+
self.connect_by_id(io, node, ii)
|
|
141
|
+
|
|
142
|
+
def disconnect_by_name(self, outputPortName, node, inputPortName):
|
|
143
|
+
if self.__onode is None:
|
|
144
|
+
raise ValueError('Connect is only available if outputs are defined.')
|
|
145
|
+
io = self.__get_output_port_index_by_name(self, outputPortName)
|
|
146
|
+
ii = self.__get_input_port_index_by_name(node, inputPortName)
|
|
147
|
+
self.disconnect_by_id(io, node, ii)
|
|
148
|
+
|
|
149
|
+
def connect_by_id(self, outputPortId, node, inputPortId):
|
|
150
|
+
if self.__onode is None:
|
|
151
|
+
raise ValueError('Connect is only available if outputs are defined.')
|
|
152
|
+
if len(self.__onode.OutputPorts) <= outputPortId:
|
|
153
|
+
raise ValueError('Port {outputPortId} is out of bounds')
|
|
154
|
+
self.__onode.connect(outputPortId, node.__inode.InputPorts[inputPortId])
|
|
155
|
+
|
|
156
|
+
def disconnect_by_id(self, outputPortId, node, inputPortId):
|
|
157
|
+
if self.__onode is None:
|
|
158
|
+
raise ValueError('Disconnect is only available if outputs are defined.')
|
|
159
|
+
if len(self.__onode.OutputPorts) <= outputPortId:
|
|
160
|
+
raise ValueError('Port {outputPortId} is out of bounds')
|
|
161
|
+
self.__onode.disconnect(outputPortId, node.__inode.InputPorts[inputPortId])
|
|
162
|
+
|
|
163
|
+
@abstractmethod
|
|
164
|
+
def update(self, data):
|
|
165
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from .node import Node
|
|
2
|
+
from .sources import *
|
|
3
|
+
|
|
4
|
+
class Pipeline:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.__nodes = {}
|
|
7
|
+
|
|
8
|
+
def add_node(self, name: str, node: Node):
|
|
9
|
+
if name in self.__nodes.keys():
|
|
10
|
+
raise ValueError(f"A node with the name '{name}' already exists.")
|
|
11
|
+
self.__nodes[name] = node
|
|
12
|
+
|
|
13
|
+
def remove_node(self, name: str):
|
|
14
|
+
if name not in self.__nodes.keys():
|
|
15
|
+
raise KeyError(f"No node found with the name '{name}'.")
|
|
16
|
+
del self.__nodes[name]
|
|
17
|
+
|
|
18
|
+
def __get_ports(self, n1 : Node):
|
|
19
|
+
return [n1.__dict__()['inputs'], n1.__dict__()['outputs']]
|
|
20
|
+
|
|
21
|
+
def __n1_is_output(self, n1 : Node, port_name1 : str, n2 : Node, port_name2 : str ):
|
|
22
|
+
n1p = self.__get_ports(n1)
|
|
23
|
+
n2p = self.__get_ports(n2)
|
|
24
|
+
if len(n1p[1]) == 0 and len(n2p[1]) == 0 :
|
|
25
|
+
raise ValueError(f"No outputs found for {n1.__class__.__name__} or {n2.__class__.__name__}.")
|
|
26
|
+
if len(n1p[0]) == 0 and len(n2p[0])== 0 :
|
|
27
|
+
raise ValueError(f"No inputs found for {n1.__class__.__name__} or {n2.__class__.__name__}.")
|
|
28
|
+
|
|
29
|
+
n1_isOutput = False
|
|
30
|
+
if port_name1 in n1p[0].values():
|
|
31
|
+
n1_isOutput = False
|
|
32
|
+
if port_name1 in n1p[1].values():
|
|
33
|
+
n1_isOutput = True
|
|
34
|
+
if port_name1 not in n1p[0].values() and port_name1 not in n1p[1].values():
|
|
35
|
+
raise ValueError(f'Port {port_name1} not found in {n1.__class__.__name__}.')
|
|
36
|
+
if port_name2 not in n2p[0].values() and port_name2 not in n2p[1].values():
|
|
37
|
+
raise ValueError(f'Port {port_name2} not found in {n2.__class__.__name__}.')
|
|
38
|
+
return n1_isOutput
|
|
39
|
+
|
|
40
|
+
def __check_nodes(self, node_name1 : str, node_name2 : str):
|
|
41
|
+
n1 : Node = None
|
|
42
|
+
try:
|
|
43
|
+
n1 : Node = self.__nodes[node_name1]
|
|
44
|
+
except:
|
|
45
|
+
raise ValueError(f'Pipeline does not contain \'{node_name1}\'.')
|
|
46
|
+
try:
|
|
47
|
+
n2 : Node = self.__nodes[node_name2]
|
|
48
|
+
except:
|
|
49
|
+
raise ValueError(f'Pipeline does not contain \'{node_name2}\'.')
|
|
50
|
+
return [n1, n2]
|
|
51
|
+
|
|
52
|
+
def connect(self, node_name1 : str, port_name1 : str, node_name2 : str, port_name2 : str):
|
|
53
|
+
nodes = self.__check_nodes(node_name1, node_name2)
|
|
54
|
+
ports = [port_name1, port_name2]
|
|
55
|
+
if self.__n1_is_output(nodes[0], port_name1, nodes[1], port_name2):
|
|
56
|
+
nodes[0].connect_by_name(ports[0], nodes[1], ports[1])
|
|
57
|
+
else:
|
|
58
|
+
nodes[1].connect_by_name(ports[1], nodes[0], ports[0])
|
|
59
|
+
|
|
60
|
+
def disconnect(self, node_name1 : str, port_name1 : str, node_name2 : str, port_name2 : str):
|
|
61
|
+
nodes = self.__check_nodes(node_name1, node_name2)
|
|
62
|
+
ports = [port_name1, port_name2]
|
|
63
|
+
if self.__n1_is_output(nodes[0], port_name1, nodes[1], port_name2):
|
|
64
|
+
nodes[0].disconnect_by_name(ports[0], nodes[1], ports[1])
|
|
65
|
+
else:
|
|
66
|
+
nodes[1].disconnect_by_name(ports[1], nodes[0], ports[0])
|
|
67
|
+
|
|
68
|
+
def get_source_nodes(self):
|
|
69
|
+
sources : list[Node] = []
|
|
70
|
+
for node in self.__nodes.values():
|
|
71
|
+
np = self.__get_ports(node)
|
|
72
|
+
if len(np[0]) == 0 and len(np[1]) > 0:
|
|
73
|
+
sources.append(node)
|
|
74
|
+
if len(sources) is None:
|
|
75
|
+
raise ValueError(f'No source node was found.')
|
|
76
|
+
return sources
|
|
77
|
+
|
|
78
|
+
def start(self):
|
|
79
|
+
found = False
|
|
80
|
+
for node in self.__nodes.values():
|
|
81
|
+
if hasattr(node, 'start'):
|
|
82
|
+
found = True
|
|
83
|
+
node.start()
|
|
84
|
+
|
|
85
|
+
if found == False:
|
|
86
|
+
raise ValueError(f'Could not find a source providing a \'start\' method.')
|
|
87
|
+
|
|
88
|
+
def stop(self):
|
|
89
|
+
found = False
|
|
90
|
+
for node in self.__nodes.values():
|
|
91
|
+
if hasattr(node, 'stop'):
|
|
92
|
+
found = True
|
|
93
|
+
node.stop()
|
|
94
|
+
|
|
95
|
+
if found == False:
|
|
96
|
+
raise ValueError(f'Could not find a source providing a \'stop\' method.')
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from .node import Node
|
|
2
|
+
from .frequency_generator import FrequencyGenerator
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
class ManualSource(Node):
|
|
6
|
+
def __init__(self, outputPorts):
|
|
7
|
+
super().__init__(inputPorts=None, outputPorts=outputPorts)
|
|
8
|
+
|
|
9
|
+
def __del__(self):
|
|
10
|
+
super().__del__()
|
|
11
|
+
|
|
12
|
+
class FixedFrequencySource(Node, FrequencyGenerator):
|
|
13
|
+
def __init__(self, outputPorts, samplingRateHz):
|
|
14
|
+
super().__init__(inputPorts=None, outputPorts=outputPorts)
|
|
15
|
+
super(Node, self).__init__(samplingRateHz)
|
|
16
|
+
|
|
17
|
+
def __del__(self):
|
|
18
|
+
super().__del__()
|
|
19
|
+
super(Node, self).__del__()
|
|
20
|
+
|
|
21
|
+
def update(self):
|
|
22
|
+
self.write(self.generate_sample())
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def generate_sample(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
class DeviceSource(Node):
|
|
29
|
+
def __init__(self, outputPorts, serial):
|
|
30
|
+
super().__init__(inputPorts=None, outputPorts=outputPorts)
|
|
31
|
+
|
|
32
|
+
def __del__(self):
|
|
33
|
+
super().__del__()
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def start_scanning():
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def stop_scanning():
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def get_available_devices():
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def open(self, serial):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def close(self):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def start(self):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def stop(self):
|
|
64
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from .i_port import IPort
|
|
2
|
+
import threading
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
class INode(ABC):
|
|
8
|
+
class UpdateMode(Enum):
|
|
9
|
+
Synchronized = 1 #update when all input ports delivered data
|
|
10
|
+
Asynchron = 2 #update when any input ports delivered data
|
|
11
|
+
|
|
12
|
+
def __init__(self, inputPort : list[str]):
|
|
13
|
+
self.InputPorts : list[IPort] = []
|
|
14
|
+
for p in inputPort:
|
|
15
|
+
port = IPort(p)
|
|
16
|
+
port.set_data_available_eventhandler(self.__on_data_available)
|
|
17
|
+
self.InputPorts.append(port)
|
|
18
|
+
self.NodeUpdateMode = self.UpdateMode.Synchronized
|
|
19
|
+
|
|
20
|
+
self.__event : threading.Event = threading.Event()
|
|
21
|
+
self.__updateThreadRunning = False
|
|
22
|
+
self.__updateThread = None
|
|
23
|
+
self.__updateCnt = 0
|
|
24
|
+
self.__totalTimeMs = 0
|
|
25
|
+
self.__updateTimeMs = 0
|
|
26
|
+
self.__start()
|
|
27
|
+
|
|
28
|
+
def __del__(self):
|
|
29
|
+
self.__stop()
|
|
30
|
+
|
|
31
|
+
def __start(self):
|
|
32
|
+
if not self.__updateThreadRunning:
|
|
33
|
+
self.__updateThreadRunning = True
|
|
34
|
+
self.__updateThread = threading.Thread(target=self.__updateThread_DoWork, daemon=True)
|
|
35
|
+
self.__updateThread.start()
|
|
36
|
+
|
|
37
|
+
def __stop(self):
|
|
38
|
+
if self.__updateThreadRunning:
|
|
39
|
+
self.__updateThreadRunning = False
|
|
40
|
+
self.__event.set()
|
|
41
|
+
self.__updateThread.join()
|
|
42
|
+
self.__updateThread = None
|
|
43
|
+
|
|
44
|
+
def __on_data_available(self):
|
|
45
|
+
if self.NodeUpdateMode is self.UpdateMode.Asynchron:
|
|
46
|
+
self.__event.set()
|
|
47
|
+
else:
|
|
48
|
+
allPortsAcquired = True
|
|
49
|
+
for port in self.InputPorts:
|
|
50
|
+
if port.DataCount <= 0:
|
|
51
|
+
allPortsAcquired = False
|
|
52
|
+
break
|
|
53
|
+
if allPortsAcquired:
|
|
54
|
+
self.__event.set()
|
|
55
|
+
|
|
56
|
+
def __updateThread_DoWork(self):
|
|
57
|
+
try:
|
|
58
|
+
self.__event.wait()
|
|
59
|
+
self.__event.clear()
|
|
60
|
+
while self.__updateThreadRunning:
|
|
61
|
+
if self.__updateThreadRunning:
|
|
62
|
+
updateCnt = -1
|
|
63
|
+
for port in self.InputPorts:
|
|
64
|
+
if updateCnt == -1 or port.DataCount <= updateCnt:
|
|
65
|
+
updateCnt = port.DataCount
|
|
66
|
+
for i in range(updateCnt):
|
|
67
|
+
start = time.time()
|
|
68
|
+
self.update()
|
|
69
|
+
end = time.time()
|
|
70
|
+
self.__updateCnt += 1
|
|
71
|
+
self.__totalTimeMs += (end - start)*1000
|
|
72
|
+
self.__updateTimeMs = self.__totalTimeMs / self.__updateCnt
|
|
73
|
+
|
|
74
|
+
self.__event.wait()
|
|
75
|
+
self.__event.clear()
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.__stop()
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def update(self):
|
|
82
|
+
pass
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
|
|
3
|
+
class IPort:
|
|
4
|
+
def __init__(self, portName : str):
|
|
5
|
+
self.__queue : queue.Queue = queue.Queue()
|
|
6
|
+
self.DataCount : int = 0
|
|
7
|
+
self.PortName = portName
|
|
8
|
+
self.IsConnected = False
|
|
9
|
+
self.__eventHandler : function
|
|
10
|
+
self.__readCnt = 0
|
|
11
|
+
|
|
12
|
+
def write(self, data):
|
|
13
|
+
self.__queue.put(data, block=True, timeout=None)
|
|
14
|
+
self.DataCount = self.__queue.qsize()
|
|
15
|
+
if self.__eventHandler is not None:
|
|
16
|
+
self.__eventHandler()
|
|
17
|
+
|
|
18
|
+
def read(self):
|
|
19
|
+
if self.DataCount > 0:
|
|
20
|
+
try:
|
|
21
|
+
self.__readCnt += 1
|
|
22
|
+
return self.__queue.get(block=True, timeout=None)
|
|
23
|
+
except:
|
|
24
|
+
return None
|
|
25
|
+
else:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def set_data_available_eventhandler(self, handler):
|
|
29
|
+
self.__eventHandler = handler
|
|
30
|
+
|
|
31
|
+
def remove_data_available_eventhandler(self, handler):
|
|
32
|
+
self.__eventHandler = None
|