flexCommunicator 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.
- flexCommunicator/__init__.py +0 -0
- flexCommunicator/clientLibraries/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/action/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/action/action.py +85 -0
- flexCommunicator/clientLibraries/flcpy/action/action_client.py +70 -0
- flexCommunicator/clientLibraries/flcpy/action/goal_handle.py +50 -0
- flexCommunicator/clientLibraries/flcpy/codec/ROS2Codec.py +227 -0
- flexCommunicator/clientLibraries/flcpy/codec/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/commands/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/commands/commandProcessor.py +113 -0
- flexCommunicator/clientLibraries/flcpy/commands/composer.py +25 -0
- flexCommunicator/clientLibraries/flcpy/communication/CommunicationManager.py +579 -0
- flexCommunicator/clientLibraries/flcpy/communication/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/contracts/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/contracts/contractVariable.py +93 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudConfiguration.py +16 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudManager.py +162 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudOrchestrator.py +248 -0
- flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudVariable.py +130 -0
- flexCommunicator/clientLibraries/flcpy/flexNode.py +892 -0
- flexCommunicator/clientLibraries/flcpy/fmi/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/fmi/fmuNode.py +166 -0
- flexCommunicator/clientLibraries/flcpy/fmi/node.py +360 -0
- flexCommunicator/clientLibraries/flcpy/knowledge/KnowledgeManager.py +288 -0
- flexCommunicator/clientLibraries/flcpy/knowledge/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/logging/LoggingAndTracking.py +159 -0
- flexCommunicator/clientLibraries/flcpy/logging/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/messages/StandardizedMessages.py +64 -0
- flexCommunicator/clientLibraries/flcpy/messages/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/node.py +486 -0
- flexCommunicator/clientLibraries/flcpy/security/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/security/security.py +189 -0
- flexCommunicator/clientLibraries/flcpy/service/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/service/client.py +58 -0
- flexCommunicator/clientLibraries/flcpy/service/service.py +67 -0
- flexCommunicator/clientLibraries/flcpy/transport/PropertyBinding.py +108 -0
- flexCommunicator/clientLibraries/flcpy/transport/PropertyRegistry.py +142 -0
- flexCommunicator/clientLibraries/flcpy/transport/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/utils/Timer.py +22 -0
- flexCommunicator/clientLibraries/flcpy/utils/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/utils/auxiliary.py +261 -0
- flexCommunicator/clientLibraries/flcpy/utils/constants.py +276 -0
- flexCommunicator/clientLibraries/flcpy/utils/decorators.py +70 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/__init__.py +0 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/runtimeMonitor.py +101 -0
- flexCommunicator/clientLibraries/flcpy/validityFrame/validityFrameManager.py +958 -0
- flexcommunicator-0.1.0.dist-info/METADATA +568 -0
- flexcommunicator-0.1.0.dist-info/RECORD +53 -0
- flexcommunicator-0.1.0.dist-info/WHEEL +5 -0
- flexcommunicator-0.1.0.dist-info/licenses/LICENSE +201 -0
- flexcommunicator-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
|
|
10
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import *
|
|
11
|
+
from flexCommunicator.clientLibraries.flcpy.flexCloud.flexCloudVariable import flexCloudVariable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class flexCloudConfigParameter(flexCloudVariable):
|
|
15
|
+
def __init__(self, name, initial_value=None, syncType="onchange", sync_interval=None):
|
|
16
|
+
super().__init__(name, initial_value=initial_value, syncType=syncType, sync_interval=sync_interval,history=False,statusVariable=False,configVariable = True)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import string
|
|
12
|
+
import random
|
|
13
|
+
import threading
|
|
14
|
+
|
|
15
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import *
|
|
16
|
+
|
|
17
|
+
# Constants for configuration keys
|
|
18
|
+
KEY_KNOWLEDGE_TYPE = "knowledge_type"
|
|
19
|
+
KEY_HOST = "host"
|
|
20
|
+
KEY_PORT = "port"
|
|
21
|
+
KEY_DB = "db"
|
|
22
|
+
KEY_TOPIC = "topic"
|
|
23
|
+
KEY_BOOTSTRAP_SERVERS = "bootstrap_servers"
|
|
24
|
+
|
|
25
|
+
# Constants for backends
|
|
26
|
+
BACKEND_REDIS = "redis"
|
|
27
|
+
BACKEND_MEMCACHED = "memcached"
|
|
28
|
+
BACKEND_IGNITE = "ignite"
|
|
29
|
+
BACKEND_HAZELCAST = "hazelcast"
|
|
30
|
+
BACKEND_TARANTOOL = "tarantool"
|
|
31
|
+
BACKEND_AEROSPIKE = "aerospike"
|
|
32
|
+
BACKEND_KAFKA = "kafka"
|
|
33
|
+
BACKEND_SQLITE = "sqlite"
|
|
34
|
+
|
|
35
|
+
# Constants for SQLite queries and in-memory DB
|
|
36
|
+
SQLITE_MEMORY = ":memory:"
|
|
37
|
+
SQLITE_CREATE_TABLE = "CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)"
|
|
38
|
+
SQLITE_INSERT_OR_REPLACE = "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)"
|
|
39
|
+
SQLITE_SELECT_VALUE = "SELECT value FROM kv WHERE key=?"
|
|
40
|
+
SQLITE_DELETE_KEY = "DELETE FROM kv WHERE key=?"
|
|
41
|
+
|
|
42
|
+
# Other constants
|
|
43
|
+
ERROR_UNSUPPORTED_BACKEND = "Unsupported backend: {}"
|
|
44
|
+
DEFAULT_CACHE_NAME = "default"
|
|
45
|
+
AEROSPIKE_VALUE_FIELD = "value"
|
|
46
|
+
|
|
47
|
+
# Helper function to generate random strings (if needed)
|
|
48
|
+
def random_string(length=10):
|
|
49
|
+
return ''.join(random.choice(string.ascii_letters) for _ in range(length))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class flexCloudManager:
|
|
53
|
+
def __init__(self, config,autostart=False):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the flexCloudManager with a chosen backend.
|
|
56
|
+
Supported backends: redis
|
|
57
|
+
|
|
58
|
+
:param config: A dictionary containing all configuration parameters needed by the backend.
|
|
59
|
+
"""
|
|
60
|
+
self.backend = config[KEY_KNOWLEDGE_TYPE]
|
|
61
|
+
#SUBSCRIPTION VARIABLES
|
|
62
|
+
self.subscribe_topics = []
|
|
63
|
+
self.event_callbacks = {} # Mapping: topic -> callback function
|
|
64
|
+
self._consumer_thread = None
|
|
65
|
+
#BACKEND CONFIGURATION
|
|
66
|
+
if self.backend == BACKEND_REDIS:
|
|
67
|
+
import redis
|
|
68
|
+
self.client = redis.Redis(host=config[KEY_HOST], port=config[KEY_PORT], db=config[KEY_DB])
|
|
69
|
+
self.pubsub = self.client.pubsub()
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(ERROR_UNSUPPORTED_BACKEND.format(self.backend))
|
|
72
|
+
|
|
73
|
+
#AUTO-START SUBSCRIPTON THREAD
|
|
74
|
+
if autostart:
|
|
75
|
+
self._consumer_thread = threading.Thread(target=self._listen, daemon=True)
|
|
76
|
+
self._consumer_thread.start()
|
|
77
|
+
|
|
78
|
+
def write(self, key, value):
|
|
79
|
+
"""Write a key-value pair to the backend."""
|
|
80
|
+
if self.backend == BACKEND_REDIS:
|
|
81
|
+
return self.client.set(key, value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read(self, key, queueSize=1):
|
|
85
|
+
"""Read the value for a given key from the backend."""
|
|
86
|
+
if self.backend == BACKEND_REDIS:
|
|
87
|
+
return self.client.get(key)
|
|
88
|
+
|
|
89
|
+
def write_history(self, key, value):
|
|
90
|
+
"""
|
|
91
|
+
Append a value to the history list for `key` in Redis.
|
|
92
|
+
Returns the new length of the list.
|
|
93
|
+
"""
|
|
94
|
+
if self.backend == BACKEND_REDIS:
|
|
95
|
+
# RPUSH adds to the tail of the list, creating it if needed
|
|
96
|
+
return self.client.rpush(key, value)
|
|
97
|
+
|
|
98
|
+
def read_history(self, key, history_size=1):
|
|
99
|
+
"""
|
|
100
|
+
Read up to `history_size` most recent values for `key` from Redis.
|
|
101
|
+
Returns a list of values (oldest→newest within that window).
|
|
102
|
+
"""
|
|
103
|
+
if self.backend == BACKEND_REDIS:
|
|
104
|
+
# Use a negative start index to grab the last `history_size` elements
|
|
105
|
+
start = -history_size
|
|
106
|
+
end = -1
|
|
107
|
+
return self.client.lrange(key, start, end)
|
|
108
|
+
|
|
109
|
+
def publish(self,key,value):
|
|
110
|
+
try:
|
|
111
|
+
data = json.dumps(value).encode('utf-8')
|
|
112
|
+
except:
|
|
113
|
+
data = value
|
|
114
|
+
self.client.publish(channel=key, message=data)
|
|
115
|
+
|
|
116
|
+
def delete(self, key):
|
|
117
|
+
"""Delete the key-value pair identified by key from the backend."""
|
|
118
|
+
if self.backend == BACKEND_REDIS:
|
|
119
|
+
return self.client.delete(key)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def exists(self, key):
|
|
123
|
+
"""Check whether a key exists in the backend."""
|
|
124
|
+
if self.backend == BACKEND_REDIS:
|
|
125
|
+
return self.client.exists(key)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def subscribe(self, topic, callback):
|
|
129
|
+
"""Register a callback for the given topic."""
|
|
130
|
+
if topic not in self.subscribe_topics:
|
|
131
|
+
self.subscribe_topics.append(topic)
|
|
132
|
+
self.event_callbacks[topic] = callback
|
|
133
|
+
|
|
134
|
+
if self.backend == TRANSPORT_PROTOCOLS.PROTOCOL_REDIS and self.pubsub:
|
|
135
|
+
self.pubsub.subscribe(topic)
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(ERROR_UNSUPPORTED_BACKEND.format(self.backend))
|
|
138
|
+
def start(self):
|
|
139
|
+
"""Start the listener thread."""
|
|
140
|
+
self._consumer_thread = threading.Thread(target=self._listen, daemon=True)
|
|
141
|
+
self._consumer_thread.start()
|
|
142
|
+
|
|
143
|
+
def stop(self):
|
|
144
|
+
"""Stop the listener thread."""
|
|
145
|
+
self._consumer_thread.join(timeout=2)
|
|
146
|
+
self.pubsub.close()
|
|
147
|
+
|
|
148
|
+
#-----------------------helper functions------------------------------
|
|
149
|
+
def _listen(self):
|
|
150
|
+
try:
|
|
151
|
+
for message in self.pubsub.listen():
|
|
152
|
+
if message['type'] == 'message':
|
|
153
|
+
topic = message['channel'].decode('utf-8') #TODO: check if needed
|
|
154
|
+
if topic in self.event_callbacks:
|
|
155
|
+
message_decoded = message['data'].decode('utf-8')
|
|
156
|
+
self.event_callbacks[topic](message_decoded) #TODO: check if needed
|
|
157
|
+
except (ValueError, OSError) as e:
|
|
158
|
+
# Socket was likely closed during shutdown; exit gracefully.
|
|
159
|
+
print(FLEXCLOUD_ERRORS.ERR_REDIS_SUBSCRIPTION_CLOSED.format(e))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
import threading
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
|
16
|
+
|
|
17
|
+
import redis
|
|
18
|
+
|
|
19
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import *
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ComponentSpec:
|
|
23
|
+
"""
|
|
24
|
+
name: unique component identifier (used for Redis keying)
|
|
25
|
+
spin: callable that starts the component
|
|
26
|
+
depends_on: optional list of component names that must be RUNNING first
|
|
27
|
+
"""
|
|
28
|
+
name: str
|
|
29
|
+
spin: Callable[[], None]
|
|
30
|
+
depends_on: Tuple[str, ...] = ()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StartupError(RuntimeError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StartupOrchestrator:
|
|
38
|
+
"""
|
|
39
|
+
Orchestrates startup of Python components that expose state in Redis.
|
|
40
|
+
|
|
41
|
+
Default Redis key pattern: "{name}:state" with value "RUNNING", etc.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
components: Sequence[ComponentSpec],
|
|
47
|
+
*,
|
|
48
|
+
# Redis connection settings (integrated)
|
|
49
|
+
redis_url: Optional[str] = None,
|
|
50
|
+
host: str = "localhost",
|
|
51
|
+
port: int = 6379,
|
|
52
|
+
db: int = 6,
|
|
53
|
+
username: Optional[str] = None,
|
|
54
|
+
password: Optional[str] = None,
|
|
55
|
+
ssl: bool = False,
|
|
56
|
+
socket_timeout_s: float = 2.0,
|
|
57
|
+
decode_responses: bool = True,
|
|
58
|
+
health_check_interval_s: int = 10,
|
|
59
|
+
# Orchestration settings
|
|
60
|
+
connectorID: str = "appName@projectName-key",
|
|
61
|
+
running_value: str = APPLICATION_STATUS.RUNNING,
|
|
62
|
+
poll_interval_s: float = 0.1,
|
|
63
|
+
spin_in_thread: bool = True,
|
|
64
|
+
ping_on_init: bool = True,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.components: Dict[str, ComponentSpec] = {c.name: c for c in components}
|
|
67
|
+
|
|
68
|
+
# Orchestration config
|
|
69
|
+
self.key_pattern = connectorID + ":applications:{name}:status"
|
|
70
|
+
self.running_value = running_value
|
|
71
|
+
self.poll_interval_s = poll_interval_s
|
|
72
|
+
self.spin_in_thread = spin_in_thread
|
|
73
|
+
|
|
74
|
+
# Build Redis client (integrated)
|
|
75
|
+
if redis_url:
|
|
76
|
+
self.r = redis.Redis.from_url(
|
|
77
|
+
redis_url,
|
|
78
|
+
socket_timeout=socket_timeout_s,
|
|
79
|
+
decode_responses=decode_responses,
|
|
80
|
+
health_check_interval=health_check_interval_s,
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
self.r = redis.Redis(
|
|
84
|
+
host=host,
|
|
85
|
+
port=port,
|
|
86
|
+
db=db,
|
|
87
|
+
username=username,
|
|
88
|
+
password=password,
|
|
89
|
+
ssl=ssl,
|
|
90
|
+
socket_timeout=socket_timeout_s,
|
|
91
|
+
decode_responses=decode_responses,
|
|
92
|
+
health_check_interval=health_check_interval_s,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if ping_on_init:
|
|
96
|
+
try:
|
|
97
|
+
self.r.ping()
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise StartupError(f"Redis ping failed (check connection settings): {e}") from e
|
|
100
|
+
|
|
101
|
+
# Validate dependencies reference known components
|
|
102
|
+
for c in components:
|
|
103
|
+
for dep in c.depends_on:
|
|
104
|
+
if dep not in self.components:
|
|
105
|
+
raise ValueError(f"Component '{c.name}' depends on unknown component '{dep}'")
|
|
106
|
+
|
|
107
|
+
def close(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Close the Redis connection pool cleanly.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
self.r.close()
|
|
113
|
+
except Exception:
|
|
114
|
+
# redis-py versions differ; best-effort close
|
|
115
|
+
try:
|
|
116
|
+
self.r.connection_pool.disconnect()
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def _key_for(self, name: str) -> str:
|
|
121
|
+
return self.key_pattern.format(name=name)
|
|
122
|
+
|
|
123
|
+
def _read_state(self, name: str) -> str:
|
|
124
|
+
try:
|
|
125
|
+
val = self.r.get(self._key_for(name))
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise StartupError(f"Redis GET failed for '{self._key_for(name)}': {e}") from e
|
|
128
|
+
|
|
129
|
+
if val is None:
|
|
130
|
+
return APPLICATION_STATUS.UNKNOWN
|
|
131
|
+
# if decode_responses=False, val is bytes; else it's str
|
|
132
|
+
if isinstance(val, bytes):
|
|
133
|
+
return val.decode("utf-8", errors="replace")
|
|
134
|
+
return str(val)
|
|
135
|
+
|
|
136
|
+
def _wait_for_running(self, name: str, timeout_s: float) -> None:
|
|
137
|
+
print(f"Waiting for '{name}' to become {self.running_value}...")
|
|
138
|
+
deadline = time.time() + timeout_s
|
|
139
|
+
while True:
|
|
140
|
+
state = self._read_state(name)
|
|
141
|
+
if state == self.running_value:
|
|
142
|
+
print(f"'{name}' is now {self.running_value}.")
|
|
143
|
+
return
|
|
144
|
+
if state == APPLICATION_STATUS.ERROR:
|
|
145
|
+
raise StartupError(f"Component '{name}' entered ERROR state while starting")
|
|
146
|
+
|
|
147
|
+
if time.time() >= deadline:
|
|
148
|
+
raise StartupError(
|
|
149
|
+
f"Timeout waiting for '{name}' to become {self.running_value}. "
|
|
150
|
+
f"Last state: {state!r}, key: {self._key_for(name)!r}"
|
|
151
|
+
)
|
|
152
|
+
time.sleep(self.poll_interval_s)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _topological_order(self, subset: Optional[Iterable[str]] = None) -> List[str]:
|
|
156
|
+
if subset is None:
|
|
157
|
+
nodes: Set[str] = set(self.components.keys())
|
|
158
|
+
else:
|
|
159
|
+
nodes = set(subset)
|
|
160
|
+
|
|
161
|
+
# Ensure requested subset includes its dependencies
|
|
162
|
+
missing_edges = []
|
|
163
|
+
for n in nodes:
|
|
164
|
+
for d in self.components[n].depends_on:
|
|
165
|
+
if d not in nodes:
|
|
166
|
+
missing_edges.append((n, d))
|
|
167
|
+
if missing_edges:
|
|
168
|
+
pairs = ", ".join([f"{n}->{d}" for n, d in sorted(missing_edges)])
|
|
169
|
+
raise ValueError("Subset does not include all dependencies. Missing edges: " + pairs)
|
|
170
|
+
|
|
171
|
+
indeg: Dict[str, int] = {n: 0 for n in nodes}
|
|
172
|
+
adj: Dict[str, List[str]] = {n: [] for n in nodes}
|
|
173
|
+
|
|
174
|
+
for n in nodes:
|
|
175
|
+
for d in self.components[n].depends_on:
|
|
176
|
+
adj[d].append(n) # d -> n
|
|
177
|
+
indeg[n] += 1
|
|
178
|
+
|
|
179
|
+
q = sorted([n for n, deg in indeg.items() if deg == 0])
|
|
180
|
+
out: List[str] = []
|
|
181
|
+
|
|
182
|
+
while q:
|
|
183
|
+
n = q.pop(0)
|
|
184
|
+
out.append(n)
|
|
185
|
+
for m in adj[n]:
|
|
186
|
+
indeg[m] -= 1
|
|
187
|
+
if indeg[m] == 0:
|
|
188
|
+
q.append(m)
|
|
189
|
+
q.sort()
|
|
190
|
+
|
|
191
|
+
if len(out) != len(nodes):
|
|
192
|
+
remaining = [n for n in nodes if n not in out]
|
|
193
|
+
raise ValueError(f"Dependency cycle detected among: {sorted(remaining)}")
|
|
194
|
+
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
def _start_one(self, name: str) -> None:
|
|
198
|
+
spec = self.components[name]
|
|
199
|
+
|
|
200
|
+
def runner():
|
|
201
|
+
spec.spin()
|
|
202
|
+
|
|
203
|
+
if self.spin_in_thread:
|
|
204
|
+
threading.Thread(target=runner, name=f"spin:{name}", daemon=True).start()
|
|
205
|
+
else:
|
|
206
|
+
runner()
|
|
207
|
+
|
|
208
|
+
def startup(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
order: Optional[Sequence[str]] = None,
|
|
212
|
+
subset: Optional[Iterable[str]] = None,
|
|
213
|
+
timeout_per_component_s: float = 30.0,
|
|
214
|
+
fail_fast: bool = True,
|
|
215
|
+
ensure_not_running: bool = False,
|
|
216
|
+
) -> List[str]:
|
|
217
|
+
if order is None:
|
|
218
|
+
start_order = self._topological_order(subset=subset)
|
|
219
|
+
else:
|
|
220
|
+
start_order = list(order)
|
|
221
|
+
|
|
222
|
+
failures: List[str] = []
|
|
223
|
+
|
|
224
|
+
for name in start_order:
|
|
225
|
+
if name not in self.components:
|
|
226
|
+
raise ValueError(f"Unknown component in order: '{name}'")
|
|
227
|
+
|
|
228
|
+
current = self._read_state(name)
|
|
229
|
+
if ensure_not_running and current == self.running_value:
|
|
230
|
+
msg = f"Component '{name}' is already {self.running_value}"
|
|
231
|
+
if fail_fast:
|
|
232
|
+
raise StartupError(msg)
|
|
233
|
+
failures.append(msg)
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
self._start_one(name)
|
|
237
|
+
try:
|
|
238
|
+
self._wait_for_running(name, timeout_s=timeout_per_component_s)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
msg = f"Failed to start '{name}': {e}"
|
|
241
|
+
if fail_fast:
|
|
242
|
+
raise StartupError(msg) from e
|
|
243
|
+
failures.append(msg)
|
|
244
|
+
|
|
245
|
+
if failures:
|
|
246
|
+
raise StartupError("One or more components failed:\n- " + "\n- ".join(failures))
|
|
247
|
+
|
|
248
|
+
return start_order
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#**************************************************************************
|
|
2
|
+
# * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
|
|
3
|
+
# *
|
|
4
|
+
# * This file is part of the flexIA project.
|
|
5
|
+
# *
|
|
6
|
+
# * flexIA can not be copied and/or distributed without the express
|
|
7
|
+
# * permission of Bert Van Acker
|
|
8
|
+
# *************************************************************************
|
|
9
|
+
import threading
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import *
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class flexCloudVariable(object):
|
|
17
|
+
def __init__(self, name, initial_value=0, syncType="onchange", sync_interval=None,history=True,statusVariable=False,configVariable = False):
|
|
18
|
+
self._name = name
|
|
19
|
+
self._value = initial_value
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
#flexCloud handler
|
|
24
|
+
self.history = history
|
|
25
|
+
self.flexCloud_manager = None
|
|
26
|
+
self.flexCloud_connectorID = None
|
|
27
|
+
self.flexCloud_applicationID = None
|
|
28
|
+
|
|
29
|
+
# flexCloud application status
|
|
30
|
+
self.statusVariable = statusVariable
|
|
31
|
+
if statusVariable:
|
|
32
|
+
self.history = False
|
|
33
|
+
# flexCloud config variable
|
|
34
|
+
self.configVariable = configVariable
|
|
35
|
+
if configVariable:
|
|
36
|
+
self.history = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _sync_to_redis(self):
|
|
40
|
+
if self.statusVariable:
|
|
41
|
+
key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + ":"+ self._name
|
|
42
|
+
elif self.configVariable:
|
|
43
|
+
key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + FLEXCLOUD_VARIABLES.CONFIGURATION + self._name
|
|
44
|
+
else:
|
|
45
|
+
key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + FLEXCLOUD_VARIABLES.VARIABLES + self._name
|
|
46
|
+
self._write_knowledge(key=key, value=self._value)
|
|
47
|
+
|
|
48
|
+
def _retrieve_from_redis(self,queueSize=1):
|
|
49
|
+
if self.statusVariable:
|
|
50
|
+
key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + ":"+ self._name
|
|
51
|
+
elif self.configVariable:
|
|
52
|
+
key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + FLEXCLOUD_VARIABLES.CONFIGURATION + self._name
|
|
53
|
+
else:
|
|
54
|
+
key = self.flexCloud_connectorID + FLEXCLOUD_VARIABLES.APPLICATIONS + self.flexCloud_applicationID + FLEXCLOUD_VARIABLES.VARIABLES + self._name
|
|
55
|
+
return self._read_knowledge(key=key,queueSize=queueSize,decode=True) #TODO: assuming that variables are always stored raw
|
|
56
|
+
|
|
57
|
+
def initialize(self):
|
|
58
|
+
""" Initialize the flexCloud variable"""
|
|
59
|
+
self._sync_to_redis()
|
|
60
|
+
|
|
61
|
+
def set(self, value):
|
|
62
|
+
if value != self._value:
|
|
63
|
+
self._value = value
|
|
64
|
+
self._sync_to_redis()
|
|
65
|
+
|
|
66
|
+
def get(self,queueSize=1):
|
|
67
|
+
with self._lock:
|
|
68
|
+
# -- variable with history --
|
|
69
|
+
if self.history:
|
|
70
|
+
queue = self._retrieve_from_redis(queueSize=queueSize)
|
|
71
|
+
if len(queue) == 1:
|
|
72
|
+
self._value = queue[0] #AUTO-SET VALUE
|
|
73
|
+
return queue[0]
|
|
74
|
+
else:
|
|
75
|
+
return queue
|
|
76
|
+
# -- variable without history --
|
|
77
|
+
else:
|
|
78
|
+
value = self._retrieve_from_redis(queueSize=1)
|
|
79
|
+
self._value = value #AUTO-SET VALUE
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
def fetch_from_redis(self): #TODO: this needs to be checked periodically for changes in the redis if variable is read/write!
|
|
83
|
+
value = self._read_knowledge(key=self._name)
|
|
84
|
+
with self._lock:
|
|
85
|
+
self._value = value
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --------------------------- HELPER FUNCTIONS ------------------------
|
|
91
|
+
def _write_knowledge(self, key, value):
|
|
92
|
+
"""Write a value to the knowledge Manager."""
|
|
93
|
+
if isinstance(key, str):
|
|
94
|
+
if isinstance(value, str):
|
|
95
|
+
value = str(value)
|
|
96
|
+
if self.history:
|
|
97
|
+
return self.flexCloud_manager.write_history(key, value)
|
|
98
|
+
else:
|
|
99
|
+
return self.flexCloud_manager.write(key, value)
|
|
100
|
+
else:
|
|
101
|
+
# Convert the class instance to a dictionary
|
|
102
|
+
class_dict = {}
|
|
103
|
+
for key_attr, attr_value in key.__dict__.items():
|
|
104
|
+
public_key = key_attr.lstrip('_')
|
|
105
|
+
class_dict[public_key] = attr_value
|
|
106
|
+
value = json.dumps(class_dict) # Serialize the dictionary to a JSON string
|
|
107
|
+
if self.history:
|
|
108
|
+
return self.flexCloud_manager.write_history(key.name, value)
|
|
109
|
+
else:
|
|
110
|
+
return self.flexCloud_manager.write(key.name, value)
|
|
111
|
+
|
|
112
|
+
def _read_knowledge(self, key, queueSize=1, decode=True):
|
|
113
|
+
"""Read a value from the knowledge Manager."""
|
|
114
|
+
# -- variable with history --
|
|
115
|
+
if self.history:
|
|
116
|
+
value = self.flexCloud_manager.read_history(key, queueSize)
|
|
117
|
+
print(value)
|
|
118
|
+
if decode:
|
|
119
|
+
if value is not None:
|
|
120
|
+
# decode all values of array
|
|
121
|
+
for i in range (0,len(value),1):
|
|
122
|
+
value[i] = value[i].decode(ENCODING.ENCODING_UTF8)
|
|
123
|
+
return value
|
|
124
|
+
|
|
125
|
+
# -- variable without history --
|
|
126
|
+
else:
|
|
127
|
+
value = self.flexCloud_manager.read(key)
|
|
128
|
+
if decode:
|
|
129
|
+
value = value.decode(ENCODING.ENCODING_UTF8)
|
|
130
|
+
return value
|