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,288 @@
|
|
|
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 time
|
|
10
|
+
import string
|
|
11
|
+
import random
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
# Constants for configuration keys
|
|
15
|
+
KEY_KNOWLEDGE_TYPE = "knowledge_type"
|
|
16
|
+
KEY_HOST = "host"
|
|
17
|
+
KEY_PORT = "port"
|
|
18
|
+
KEY_DB = "db"
|
|
19
|
+
KEY_TOPIC = "topic"
|
|
20
|
+
KEY_BOOTSTRAP_SERVERS = "bootstrap_servers"
|
|
21
|
+
|
|
22
|
+
# Constants for backends
|
|
23
|
+
BACKEND_REDIS = "redis"
|
|
24
|
+
BACKEND_MEMCACHED = "memcached"
|
|
25
|
+
BACKEND_IGNITE = "ignite"
|
|
26
|
+
BACKEND_HAZELCAST = "hazelcast"
|
|
27
|
+
BACKEND_TARANTOOL = "tarantool"
|
|
28
|
+
BACKEND_AEROSPIKE = "aerospike"
|
|
29
|
+
BACKEND_KAFKA = "kafka"
|
|
30
|
+
BACKEND_SQLITE = "sqlite"
|
|
31
|
+
BACKEND_OPCUA = "opc_ua"
|
|
32
|
+
|
|
33
|
+
# Constants for SQLite queries and in-memory DB
|
|
34
|
+
SQLITE_MEMORY = ":memory:"
|
|
35
|
+
SQLITE_CREATE_TABLE = "CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)"
|
|
36
|
+
SQLITE_INSERT_OR_REPLACE = "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)"
|
|
37
|
+
SQLITE_SELECT_VALUE = "SELECT value FROM kv WHERE key=?"
|
|
38
|
+
SQLITE_DELETE_KEY = "DELETE FROM kv WHERE key=?"
|
|
39
|
+
|
|
40
|
+
# Other constants
|
|
41
|
+
ERROR_UNSUPPORTED_BACKEND = "Unsupported backend: {}"
|
|
42
|
+
DEFAULT_CACHE_NAME = "default"
|
|
43
|
+
AEROSPIKE_VALUE_FIELD = "value"
|
|
44
|
+
|
|
45
|
+
# Helper function to generate random strings (if needed)
|
|
46
|
+
def random_string(length=10):
|
|
47
|
+
return ''.join(random.choice(string.ascii_letters) for _ in range(length))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class KnowledgeManager:
|
|
51
|
+
def __init__(self, config):
|
|
52
|
+
"""
|
|
53
|
+
Initialize the KnowledgeManager with a chosen backend.
|
|
54
|
+
Supported backends: redis, memcached, ignite, hazelcast, tarantool, aerospike, kafka, sqlite
|
|
55
|
+
|
|
56
|
+
:param config: A dictionary containing all configuration parameters needed by the backend.
|
|
57
|
+
"""
|
|
58
|
+
self.backend = config[KEY_KNOWLEDGE_TYPE]
|
|
59
|
+
if self.backend == BACKEND_REDIS:
|
|
60
|
+
import redis
|
|
61
|
+
self.client = redis.Redis(host=config[KEY_HOST], port=config[KEY_PORT], db=config[KEY_DB])
|
|
62
|
+
elif self.backend == BACKEND_MEMCACHED:
|
|
63
|
+
import memcache
|
|
64
|
+
self.client = memcache.Client([(config[KEY_HOST], config[KEY_PORT])])
|
|
65
|
+
elif self.backend == BACKEND_IGNITE:
|
|
66
|
+
from pyignite import Client
|
|
67
|
+
self.client = Client()
|
|
68
|
+
self.client.connect(config[KEY_HOST], config[KEY_PORT])
|
|
69
|
+
elif self.backend == BACKEND_HAZELCAST:
|
|
70
|
+
import hazelcast
|
|
71
|
+
# Configuration for hazelcast client is passed via config.
|
|
72
|
+
self.client = hazelcast.HazelcastClient(**config)
|
|
73
|
+
elif self.backend == BACKEND_TARANTOOL:
|
|
74
|
+
import tarantool
|
|
75
|
+
self.client = tarantool.Connection(config[KEY_HOST], config[KEY_PORT])
|
|
76
|
+
elif self.backend == BACKEND_AEROSPIKE:
|
|
77
|
+
import aerospike
|
|
78
|
+
cfg = {'hosts': [(config[KEY_HOST], config[KEY_PORT])]}
|
|
79
|
+
self.client = aerospike.client(cfg).connect()
|
|
80
|
+
elif self.backend == BACKEND_KAFKA:
|
|
81
|
+
from kafka import KafkaProducer, KafkaConsumer
|
|
82
|
+
self.topic = config[KEY_TOPIC]
|
|
83
|
+
self.producer = KafkaProducer(bootstrap_servers=config[KEY_BOOTSTRAP_SERVERS])
|
|
84
|
+
self.consumer = KafkaConsumer(
|
|
85
|
+
self.topic,
|
|
86
|
+
bootstrap_servers=config[KEY_BOOTSTRAP_SERVERS],
|
|
87
|
+
auto_offset_reset='earliest',
|
|
88
|
+
consumer_timeout_ms=1000
|
|
89
|
+
)
|
|
90
|
+
elif self.backend == BACKEND_SQLITE:
|
|
91
|
+
import sqlite3
|
|
92
|
+
self.conn = sqlite3.connect(SQLITE_MEMORY)
|
|
93
|
+
self.cursor = self.conn.cursor()
|
|
94
|
+
self.cursor.execute(SQLITE_CREATE_TABLE)
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(ERROR_UNSUPPORTED_BACKEND.format(self.backend))
|
|
97
|
+
|
|
98
|
+
def write(self, key, value):
|
|
99
|
+
"""Write a key-value pair to the backend."""
|
|
100
|
+
if self.backend == BACKEND_REDIS:
|
|
101
|
+
return self.client.set(key, value)
|
|
102
|
+
elif self.backend == BACKEND_MEMCACHED:
|
|
103
|
+
return self.client.set(key, value)
|
|
104
|
+
elif self.backend == BACKEND_IGNITE:
|
|
105
|
+
cache = self.client.get_or_create_cache(DEFAULT_CACHE_NAME)
|
|
106
|
+
return cache.put(key, value)
|
|
107
|
+
elif self.backend == BACKEND_HAZELCAST:
|
|
108
|
+
hz_map = self.client.get_map(DEFAULT_CACHE_NAME).blocking()
|
|
109
|
+
return hz_map.put(key, value)
|
|
110
|
+
elif self.backend == BACKEND_TARANTOOL:
|
|
111
|
+
# Assumes a space 'kv' exists with schema (key, value)
|
|
112
|
+
return self.client.insert('kv', (key, value))
|
|
113
|
+
elif self.backend == BACKEND_AEROSPIKE:
|
|
114
|
+
# Note: In Aerospike, the key should be a tuple; this is a simplified example.
|
|
115
|
+
return self.client.put(key, {AEROSPIKE_VALUE_FIELD: value})
|
|
116
|
+
elif self.backend == BACKEND_KAFKA:
|
|
117
|
+
# Kafka is a streaming system; here, writing means sending a message.
|
|
118
|
+
future = self.producer.send(self.topic, key=key.encode(), value=value.encode())
|
|
119
|
+
return future.get(timeout=10)
|
|
120
|
+
elif self.backend == BACKEND_SQLITE:
|
|
121
|
+
try:
|
|
122
|
+
self.cursor.execute(SQLITE_INSERT_OR_REPLACE, (key, value))
|
|
123
|
+
self.conn.commit()
|
|
124
|
+
return True
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.conn.rollback()
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def write_history(self, key, value):
|
|
130
|
+
"""Write a key-value pair to the backend, push to list."""
|
|
131
|
+
try:
|
|
132
|
+
if self.backend == BACKEND_REDIS:
|
|
133
|
+
return self.client.rpush(key, value)
|
|
134
|
+
warnings.warn(
|
|
135
|
+
"write_history currently only supports the Redis backend.",
|
|
136
|
+
RuntimeWarning,
|
|
137
|
+
stacklevel=2,
|
|
138
|
+
)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
warnings.warn(
|
|
141
|
+
f"Failed to write history for key '{key}': {e}",
|
|
142
|
+
RuntimeWarning,
|
|
143
|
+
stacklevel=2,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def read(self, key, queueSize=1):
|
|
147
|
+
"""Read the value for a given key from the backend."""
|
|
148
|
+
if self.backend == BACKEND_REDIS:
|
|
149
|
+
return self.client.get(key)
|
|
150
|
+
elif self.backend == BACKEND_MEMCACHED:
|
|
151
|
+
return self.client.get(key)
|
|
152
|
+
elif self.backend == BACKEND_IGNITE:
|
|
153
|
+
cache = self.client.get_or_create_cache(DEFAULT_CACHE_NAME)
|
|
154
|
+
return cache.get(key)
|
|
155
|
+
elif self.backend == BACKEND_HAZELCAST:
|
|
156
|
+
hz_map = self.client.get_map(DEFAULT_CACHE_NAME).blocking()
|
|
157
|
+
return hz_map.get(key)
|
|
158
|
+
elif self.backend == BACKEND_TARANTOOL:
|
|
159
|
+
result = self.client.select('kv', 0, 1, 0, key)
|
|
160
|
+
return result[0][1] if result else None
|
|
161
|
+
elif self.backend == BACKEND_AEROSPIKE:
|
|
162
|
+
try:
|
|
163
|
+
(key_rec, metadata, record) = self.client.get(key)
|
|
164
|
+
return record.get(AEROSPIKE_VALUE_FIELD)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return None
|
|
167
|
+
elif self.backend == BACKEND_KAFKA:
|
|
168
|
+
# For Kafka, simulate reading by consuming one message from the topic.
|
|
169
|
+
for message in self.consumer:
|
|
170
|
+
return message.value.decode()
|
|
171
|
+
return None
|
|
172
|
+
elif self.backend == BACKEND_SQLITE:
|
|
173
|
+
self.cursor.execute(SQLITE_SELECT_VALUE, (key,))
|
|
174
|
+
row = self.cursor.fetchone()
|
|
175
|
+
return row[0] if row else None
|
|
176
|
+
|
|
177
|
+
def delete(self, key):
|
|
178
|
+
"""Delete the key-value pair identified by key from the backend."""
|
|
179
|
+
if self.backend == BACKEND_REDIS:
|
|
180
|
+
return self.client.delete(key)
|
|
181
|
+
elif self.backend == BACKEND_MEMCACHED:
|
|
182
|
+
return self.client.delete(key)
|
|
183
|
+
elif self.backend == BACKEND_IGNITE:
|
|
184
|
+
cache = self.client.get_or_create_cache(DEFAULT_CACHE_NAME)
|
|
185
|
+
return cache.remove(key)
|
|
186
|
+
elif self.backend == BACKEND_HAZELCAST:
|
|
187
|
+
hz_map = self.client.get_map(DEFAULT_CACHE_NAME).blocking()
|
|
188
|
+
return hz_map.remove(key)
|
|
189
|
+
elif self.backend == BACKEND_TARANTOOL:
|
|
190
|
+
return self.client.delete('kv', key)
|
|
191
|
+
elif self.backend == BACKEND_AEROSPIKE:
|
|
192
|
+
try:
|
|
193
|
+
return self.client.remove(key)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return None
|
|
196
|
+
elif self.backend == BACKEND_KAFKA:
|
|
197
|
+
# Kafka does not support deleting individual messages.
|
|
198
|
+
return None
|
|
199
|
+
elif self.backend == BACKEND_SQLITE:
|
|
200
|
+
self.cursor.execute(SQLITE_DELETE_KEY, (key,))
|
|
201
|
+
self.conn.commit()
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
def exists(self, key):
|
|
205
|
+
"""Check whether a key exists in the backend."""
|
|
206
|
+
if self.backend == BACKEND_REDIS:
|
|
207
|
+
return self.client.exists(key)
|
|
208
|
+
elif self.backend == BACKEND_MEMCACHED:
|
|
209
|
+
return self.client.get(key) is not None
|
|
210
|
+
elif self.backend == BACKEND_IGNITE:
|
|
211
|
+
cache = self.client.get_or_create_cache(DEFAULT_CACHE_NAME)
|
|
212
|
+
return cache.get(key) is not None
|
|
213
|
+
elif self.backend == BACKEND_HAZELCAST:
|
|
214
|
+
hz_map = self.client.get_map(DEFAULT_CACHE_NAME).blocking()
|
|
215
|
+
return hz_map.contains_key(key)
|
|
216
|
+
elif self.backend == BACKEND_TARANTOOL:
|
|
217
|
+
result = self.client.select('kv', 0, 1, 0, key)
|
|
218
|
+
return bool(result)
|
|
219
|
+
elif self.backend == BACKEND_AEROSPIKE:
|
|
220
|
+
try:
|
|
221
|
+
self.client.get(key)
|
|
222
|
+
return True
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return False
|
|
225
|
+
elif self.backend == BACKEND_KAFKA:
|
|
226
|
+
# Kafka does not provide a means to check for existence of a message by key.
|
|
227
|
+
return None
|
|
228
|
+
elif self.backend == BACKEND_SQLITE:
|
|
229
|
+
self.cursor.execute('SELECT 1 FROM kv WHERE key=?', (key,))
|
|
230
|
+
return self.cursor.fetchone() is not None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def benchmark_operations(backend, config, num_operations=1000):
|
|
234
|
+
print(f"\nBenchmarking backend: {backend.upper()} with {num_operations} operations")
|
|
235
|
+
try:
|
|
236
|
+
km = KnowledgeManager(config)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f"Could not initialize backend '{backend}': {e}")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
keys = [f'key_{i}' for i in range(num_operations)]
|
|
242
|
+
value = "sample_value"
|
|
243
|
+
|
|
244
|
+
# Write benchmark
|
|
245
|
+
start_time = time.time()
|
|
246
|
+
for key in keys:
|
|
247
|
+
km.write(key, value)
|
|
248
|
+
write_time = time.time() - start_time
|
|
249
|
+
print(f"Write: {write_time:.6f} sec total, {(write_time/num_operations)*1e6:.2f} µs/op")
|
|
250
|
+
|
|
251
|
+
# Read benchmark
|
|
252
|
+
start_time = time.time()
|
|
253
|
+
for key in keys:
|
|
254
|
+
km.read(key)
|
|
255
|
+
read_time = time.time() - start_time
|
|
256
|
+
print(f"Read: {read_time:.6f} sec total, {(read_time/num_operations)*1e6:.2f} µs/op")
|
|
257
|
+
|
|
258
|
+
# Exists benchmark
|
|
259
|
+
start_time = time.time()
|
|
260
|
+
for key in keys:
|
|
261
|
+
km.exists(key)
|
|
262
|
+
exists_time = time.time() - start_time
|
|
263
|
+
print(f"Exists: {exists_time:.6f} sec total, {(exists_time/num_operations)*1e6:.2f} µs/op")
|
|
264
|
+
|
|
265
|
+
# Delete benchmark
|
|
266
|
+
start_time = time.time()
|
|
267
|
+
for key in keys:
|
|
268
|
+
km.delete(key)
|
|
269
|
+
delete_time = time.time() - start_time
|
|
270
|
+
print(f"Delete: {delete_time:.6f} sec total, {(delete_time/num_operations)*1e6:.2f} µs/op")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == '__main__':
|
|
274
|
+
# Define backend configurations here. Adjust values as needed for your environment.
|
|
275
|
+
backend_configs = {
|
|
276
|
+
'redis': {"host": "localhost", "port": 6379, "db": 0},
|
|
277
|
+
# 'memcached': {"host": "127.0.0.1", "port": 11211},
|
|
278
|
+
# 'ignite': {"host": "127.0.0.1", "port": 10800}, #TODO:Test this
|
|
279
|
+
# 'hazelcast': {}, # Provide Hazelcast-specific configuration if needed. #TODO:Test this
|
|
280
|
+
# 'tarantool': {"host": "127.0.0.1", "port": 3301}, #TODO:Test this
|
|
281
|
+
# 'aerospike': {"host": "127.0.0.1", "port": 3000}, #TODO:Test this
|
|
282
|
+
# 'kafka': {"bootstrap_servers": "localhost:9092", "topic": "knowledge"}, #TODO:Test this
|
|
283
|
+
# 'sqlite': {} # No configuration required for in-memory SQLite.
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Benchmark each backend.
|
|
287
|
+
for backend, config in backend_configs.items():
|
|
288
|
+
benchmark_operations(backend, config, num_operations=1000)
|
|
File without changes
|
|
@@ -0,0 +1,159 @@
|
|
|
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 logging
|
|
10
|
+
|
|
11
|
+
from flexCommunicator.clientLibraries.flcpy.utils.constants import FLEXCLOUD_VARIABLES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Custom logging handler for MQTT.
|
|
15
|
+
class MQTTLoggingHandler(logging.Handler):
|
|
16
|
+
def __init__(self, broker='localhost', port=1883, topic='logs'):
|
|
17
|
+
super().__init__()
|
|
18
|
+
import paho.mqtt.client as mqtt
|
|
19
|
+
self.topic = topic
|
|
20
|
+
self.client = mqtt.Client()
|
|
21
|
+
# Connect to the MQTT broker.
|
|
22
|
+
self.client.connect(broker, port)
|
|
23
|
+
# Start a background network loop.
|
|
24
|
+
self.client.loop_start()
|
|
25
|
+
|
|
26
|
+
def emit(self, record):
|
|
27
|
+
try:
|
|
28
|
+
log_entry = self.format(record)
|
|
29
|
+
self.client.publish(self.topic, log_entry)
|
|
30
|
+
except Exception:
|
|
31
|
+
self.handleError(record)
|
|
32
|
+
|
|
33
|
+
# Custom logging handler for Redis.
|
|
34
|
+
class RedisLoggingHandler(logging.Handler):
|
|
35
|
+
def __init__(self, host='localhost', port=6379, db=6, channel='logs',flexCloud=False):
|
|
36
|
+
super().__init__()
|
|
37
|
+
import redis
|
|
38
|
+
self.redis_client = redis.Redis(host=host, port=port,db=db, decode_responses=True)
|
|
39
|
+
self.channel = channel
|
|
40
|
+
self.flexCloud=flexCloud
|
|
41
|
+
|
|
42
|
+
def emit(self, record):
|
|
43
|
+
try:
|
|
44
|
+
log_entry = self.format(record)
|
|
45
|
+
if self.flexCloud:
|
|
46
|
+
self.redis_client.rpush(self.channel, log_entry)
|
|
47
|
+
else:
|
|
48
|
+
self.redis_client.publish(self.channel, log_entry)
|
|
49
|
+
|
|
50
|
+
except Exception:
|
|
51
|
+
self.handleError(record)
|
|
52
|
+
|
|
53
|
+
# Logging and Tracking Handler that wraps one active handler based on configuration.
|
|
54
|
+
class LoggingAndTrackingHandler(logging.Handler):
|
|
55
|
+
def __init__(self, config,applicationDescriptor=None):
|
|
56
|
+
"""
|
|
57
|
+
Initialize the handler based on the provided configuration.
|
|
58
|
+
|
|
59
|
+
:param config: Dictionary with logging configuration.
|
|
60
|
+
Example:
|
|
61
|
+
{
|
|
62
|
+
'log_level': 'DEBUG',
|
|
63
|
+
'logger_type': 'file', # Options: 'terminal', 'file', 'mqtt', 'redis'
|
|
64
|
+
'terminal': {},
|
|
65
|
+
'file': {'filename': 'app.log'},
|
|
66
|
+
'mqtt': {'broker': 'localhost', 'port': 1883, 'topic': 'logs'},
|
|
67
|
+
'redis': {'host': 'localhost', 'port': 6379, 'channel': 'logs'}
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.config = config
|
|
72
|
+
self.level = getattr(logging, config.get('log_level', 'INFO').upper(), logging.INFO)
|
|
73
|
+
self.applicationDescriptor = applicationDescriptor
|
|
74
|
+
self.active_handler = self._setup_handler()
|
|
75
|
+
|
|
76
|
+
def _setup_handler(self):
|
|
77
|
+
active_type = self.config.get('logger_type', 'terminal').lower()
|
|
78
|
+
if active_type == 'terminal':
|
|
79
|
+
handler = logging.StreamHandler()
|
|
80
|
+
elif active_type == 'file':
|
|
81
|
+
file_config = self.config.get('file', {})
|
|
82
|
+
if 'filename' not in file_config:
|
|
83
|
+
raise ValueError("File handler selected but no filename provided in config.")
|
|
84
|
+
handler = logging.FileHandler(file_config['filename'])
|
|
85
|
+
elif active_type == 'mqtt':
|
|
86
|
+
mqtt_config = self.config.get('mqtt', {})
|
|
87
|
+
handler = MQTTLoggingHandler(
|
|
88
|
+
broker=mqtt_config.get('broker', 'localhost'),
|
|
89
|
+
port=mqtt_config.get('port', 1883),
|
|
90
|
+
topic=mqtt_config.get('topic', 'logs')
|
|
91
|
+
)
|
|
92
|
+
elif active_type == 'redis':
|
|
93
|
+
redis_config = self.config.get('redis', {})
|
|
94
|
+
handler = RedisLoggingHandler(
|
|
95
|
+
host=redis_config.get('host', 'localhost'),
|
|
96
|
+
port=redis_config.get('port', 6379),
|
|
97
|
+
db=redis_config.get('db', 0),
|
|
98
|
+
channel=redis_config.get('channel', 'logs')
|
|
99
|
+
)
|
|
100
|
+
elif active_type == 'flexcloud':
|
|
101
|
+
channel = self._get_log_channel()
|
|
102
|
+
redis_config = self.config.get('redis', {})
|
|
103
|
+
handler = RedisLoggingHandler(
|
|
104
|
+
host=redis_config.get('host', 'localhost'),
|
|
105
|
+
port=redis_config.get('port', 6379),
|
|
106
|
+
db=redis_config.get('db', 0),
|
|
107
|
+
channel=channel,
|
|
108
|
+
flexCloud=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"Unknown logger_type specified: {active_type}")
|
|
113
|
+
return handler
|
|
114
|
+
|
|
115
|
+
def setFormatter(self, fmt):
|
|
116
|
+
"""Override setFormatter to also set the formatter on the active handler."""
|
|
117
|
+
super().setFormatter(fmt)
|
|
118
|
+
self.active_handler.setFormatter(fmt)
|
|
119
|
+
|
|
120
|
+
def emit(self, record):
|
|
121
|
+
"""Delegate emit to the wrapped active handler."""
|
|
122
|
+
self.active_handler.emit(record)
|
|
123
|
+
|
|
124
|
+
def _get_log_channel(self):
|
|
125
|
+
channel = self.applicationDescriptor["connectorID"] + FLEXCLOUD_VARIABLES.APPLICATIONS + self.applicationDescriptor["id"] + ":" + FLEXCLOUD_VARIABLES.LOGS
|
|
126
|
+
return channel
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Example usage:
|
|
130
|
+
if __name__ == '__main__':
|
|
131
|
+
config = {
|
|
132
|
+
'log_level': 'DEBUG',
|
|
133
|
+
'logger_type': 'redis', # Change to 'terminal', 'file', 'mqtt', or 'redis' as needed.
|
|
134
|
+
'terminal': {},
|
|
135
|
+
'file': {'filename': 'app.log'},
|
|
136
|
+
'mqtt': {'broker': 'localhost', 'port': 1883, 'topic': 'logs'},
|
|
137
|
+
'redis': {'host': 'localhost', 'port': 6379, 'channel': 'logs'}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Create the main logger.
|
|
141
|
+
logger = logging.getLogger("main")
|
|
142
|
+
logger.setLevel(logging.DEBUG)
|
|
143
|
+
|
|
144
|
+
# Instantiate the custom logging handler.
|
|
145
|
+
custom_handler = LoggingAndTrackingHandler(config)
|
|
146
|
+
|
|
147
|
+
# Set the formatter here in main (instead of inside the class).
|
|
148
|
+
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
149
|
+
formatter = logging.Formatter(log_format)
|
|
150
|
+
custom_handler.setFormatter(formatter)
|
|
151
|
+
|
|
152
|
+
logger.addHandler(custom_handler)
|
|
153
|
+
|
|
154
|
+
# Log some test messages.
|
|
155
|
+
logger.debug("This is a debug message.")
|
|
156
|
+
logger.info("This is an info message.")
|
|
157
|
+
logger.warning("This is a warning message.")
|
|
158
|
+
logger.error("This is an error message.")
|
|
159
|
+
logger.critical("This is a critical message.")
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
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 jsonpickle
|
|
10
|
+
|
|
11
|
+
class DataMessage(object):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._name = "CustomMessage"
|
|
14
|
+
self._dataType = "Float"
|
|
15
|
+
self._dataUnit = "mm"
|
|
16
|
+
self._value = None
|
|
17
|
+
|
|
18
|
+
# Getter and setter for name
|
|
19
|
+
@property
|
|
20
|
+
def name(self):
|
|
21
|
+
return self._name
|
|
22
|
+
|
|
23
|
+
@name.setter
|
|
24
|
+
def name(self, value):
|
|
25
|
+
self._name = value
|
|
26
|
+
|
|
27
|
+
# Getter and setter for dataType
|
|
28
|
+
@property
|
|
29
|
+
def dataType(self):
|
|
30
|
+
return self._dataType
|
|
31
|
+
|
|
32
|
+
@dataType.setter
|
|
33
|
+
def dataType(self, value):
|
|
34
|
+
self._dataType = value
|
|
35
|
+
|
|
36
|
+
# Getter and setter for dataUnit
|
|
37
|
+
@property
|
|
38
|
+
def dataUnit(self):
|
|
39
|
+
return self._dataUnit
|
|
40
|
+
|
|
41
|
+
@dataUnit.setter
|
|
42
|
+
def dataUnit(self, value):
|
|
43
|
+
self._dataUnit = value
|
|
44
|
+
|
|
45
|
+
# Getter and setter for value
|
|
46
|
+
@property
|
|
47
|
+
def value(self):
|
|
48
|
+
return self._value
|
|
49
|
+
|
|
50
|
+
@value.setter
|
|
51
|
+
def value(self, cmp):
|
|
52
|
+
self._value = cmp
|
|
53
|
+
|
|
54
|
+
def instantiate(self, json_str):
|
|
55
|
+
"""
|
|
56
|
+
Reconstruct the object from a JSON string using jsonpickle.
|
|
57
|
+
"""
|
|
58
|
+
decoded = jsonpickle.decode(json_str)
|
|
59
|
+
|
|
60
|
+
# Update only if the attributes exist
|
|
61
|
+
self._name = getattr(decoded, "_name", self._name)
|
|
62
|
+
self._dataType = getattr(decoded, "_dataType", self._dataType)
|
|
63
|
+
self._dataUnit = getattr(decoded, "_dataUnit", self._dataUnit)
|
|
64
|
+
self._value = getattr(decoded, "_value", self._value)
|
|
File without changes
|