Merge pull request 'Add frequency plotting and representation values in binary format' (#1) from dev into main
Reviewed-on: #1
This commit is contained in:
commit
4607dcc8ca
|
@ -29,7 +29,7 @@ Setup & debug
|
||||||
+ ``pip list`` (pip show abibas, pip uninstall abibas, etc)
|
+ ``pip list`` (pip show abibas, pip uninstall abibas, etc)
|
||||||
|
|
||||||
At this stage the package is installed in editable mode, to run it in debug mode:
|
At this stage the package is installed in editable mode, to run it in debug mode:
|
||||||
|
,
|
||||||
+ ``abibas-debug``
|
+ ``abibas-debug``
|
||||||
|
|
||||||
or in normal mode:
|
or in normal mode:
|
||||||
|
@ -40,3 +40,9 @@ If in debug mode changes made to the Flask app will be applied automatically at
|
||||||
for the changes in simulator though the tool has to be restarted.
|
for the changes in simulator though the tool has to be restarted.
|
||||||
|
|
||||||
**main** module can be manipulated for debugging purposes as well
|
**main** module can be manipulated for debugging purposes as well
|
||||||
|
|
||||||
|
Before releasing
|
||||||
|
================
|
||||||
|
+ ``pip install twine``
|
||||||
|
+ ``twine check dist/*``
|
||||||
|
+ ``twine upload --repository-url https://test.pypi.org/legacy/ dist/*``
|
||||||
|
|
|
@ -25,6 +25,7 @@ dependencies = [
|
||||||
"Flask",
|
"Flask",
|
||||||
"pyModbusTCP",
|
"pyModbusTCP",
|
||||||
"prettytable",
|
"prettytable",
|
||||||
|
"plotext",
|
||||||
]
|
]
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
urls = {homepage = "https://gittar.crabdance.com/e-corp/abibas"}
|
urls = {homepage = "https://gittar.crabdance.com/e-corp/abibas"}
|
||||||
|
|
|
@ -34,8 +34,11 @@ def _get_controller_models_fname() -> str:
|
||||||
def _get_reg_value(controller: dict, reg: dict) -> int:
|
def _get_reg_value(controller: dict, reg: dict) -> int:
|
||||||
# get first value from type definition (as default)
|
# get first value from type definition (as default)
|
||||||
value = None
|
value = None
|
||||||
k, v = next(iter(reg['PossibleValues'][0].items()), None)
|
if 'PossibleValues' not in reg or not reg['PossibleValues']:
|
||||||
value = v
|
value = 0
|
||||||
|
else:
|
||||||
|
k, v = next(iter(reg['PossibleValues'][0].items()), None)
|
||||||
|
value = v
|
||||||
# if there is a file with overriden value, take that
|
# if there is a file with overriden value, take that
|
||||||
overrides = _get_overrides_fname(controller['Type'], controller['Port'])
|
overrides = _get_overrides_fname(controller['Type'], controller['Port'])
|
||||||
if os.path.isfile(overrides):
|
if os.path.isfile(overrides):
|
||||||
|
@ -51,8 +54,8 @@ def _get_reg_value(controller: dict, reg: dict) -> int:
|
||||||
entry = [x for x in overriden if x['Name'] == reg['Name']]
|
entry = [x for x in overriden if x['Name'] == reg['Name']]
|
||||||
if entry:
|
if entry:
|
||||||
value = entry[0]['Value']
|
value = entry[0]['Value']
|
||||||
# logging.debug(
|
logging.debug(
|
||||||
# f'{reg["Name"]} value gotten: {value} from {overrides}')
|
f'{reg["Name"]} value gotten: {value} from {overrides}')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,11 +64,6 @@ def _save_overrides(fname: str, overrides: list):
|
||||||
json.dump(overrides, file, indent=2)
|
json.dump(overrides, file, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def get_version() -> str:
|
|
||||||
version = pkg_resources.get_distribution('abibas').version
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
|
||||||
def get_controller(port: int) -> dict:
|
def get_controller(port: int) -> dict:
|
||||||
controllers = get_controllers()
|
controllers = get_controllers()
|
||||||
models = get_controller_types()
|
models = get_controller_types()
|
||||||
|
@ -164,3 +162,10 @@ def remove_controller(port: int) -> bool:
|
||||||
logging.debug(f'Overrides file {overrides} not found, skip removing')
|
logging.debug(f'Overrides file {overrides} not found, skip removing')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_info() -> dict:
|
||||||
|
distribution = pkg_resources.get_distribution('abibas')
|
||||||
|
return dict(version=distribution.version,
|
||||||
|
author='Taryel Hlontsi',
|
||||||
|
license='copyleft (GPLv3)')
|
||||||
|
|
|
@ -1,4 +1,141 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"Name": "Ads",
|
||||||
|
"RunAs": "Client",
|
||||||
|
"Registers": [
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "Status2",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 0,
|
||||||
|
"DisplayAs": "DropdownVariants",
|
||||||
|
"PossibleValues": [
|
||||||
|
{
|
||||||
|
"NotOperting": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Operating": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "Sim160",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 928,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "ConfigUnixTime4",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 259,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "ConfigUnixTime3",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 258,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "ConfigUnixTime2",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 257,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "ConfigUnixTime1",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 256,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "TurbinesConfigured",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 261,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "InterfaceVersion",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 260,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "Sim2",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 770,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "Sim1",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 769,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "SystemStatusCode",
|
||||||
|
"Type": "Input",
|
||||||
|
"Address": 1,
|
||||||
|
"DisplayAs": "RawBinary",
|
||||||
|
"PossibleValues": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": true,
|
||||||
|
"Name": "Interface",
|
||||||
|
"Type": "Holding",
|
||||||
|
"Address": 512,
|
||||||
|
"PossibleValues": [
|
||||||
|
{
|
||||||
|
"Deactivated": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AircraftDetected": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AircraftNotDetected": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CanOverride": false,
|
||||||
|
"Name": "Status",
|
||||||
|
"Type": "Holding",
|
||||||
|
"Address": 0,
|
||||||
|
"PossibleValues": [
|
||||||
|
{
|
||||||
|
"NotOperting": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Operating": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DisplayVariants": [
|
||||||
|
"DropdownVariants",
|
||||||
|
"RawBinary",
|
||||||
|
"RawInteger"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "CIP300",
|
"Name": "CIP300",
|
||||||
"RunAs": "Server",
|
"RunAs": "Server",
|
||||||
|
@ -108,43 +245,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Name": "Ads",
|
|
||||||
"RunAs": "Client",
|
|
||||||
"Registers": [
|
|
||||||
{
|
|
||||||
"CanOverride": true,
|
|
||||||
"Name": "Interface",
|
|
||||||
"Type": "Holding",
|
|
||||||
"Address": 512,
|
|
||||||
"PossibleValues": [
|
|
||||||
{
|
|
||||||
"Deactivated": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AircraftDetected": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AircraftNotDetected": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"CanOverride": false,
|
|
||||||
"Name": "Status",
|
|
||||||
"Type": "Holding",
|
|
||||||
"Address": 0,
|
|
||||||
"PossibleValues": [
|
|
||||||
{
|
|
||||||
"NotOperting": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Operating": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Name": "CIP400",
|
"Name": "CIP400",
|
||||||
"RunAs": "Server",
|
"RunAs": "Server",
|
||||||
|
|
|
@ -11,12 +11,6 @@
|
||||||
"Ip": "127.0.0.1",
|
"Ip": "127.0.0.1",
|
||||||
"Port": 11507
|
"Port": 11507
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Type": "Quantec",
|
|
||||||
"Enabled": true,
|
|
||||||
"Ip": "127.0.0.1",
|
|
||||||
"Port": 11701
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Type": "Quantec",
|
"Type": "Quantec",
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
|
@ -28,5 +22,11 @@
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Ip": "127.0.0.1",
|
"Ip": "127.0.0.1",
|
||||||
"Port": 11508
|
"Port": 11508
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": "CIP400",
|
||||||
|
"Enabled": true,
|
||||||
|
"Ip": "127.0.0.1",
|
||||||
|
"Port": 11506
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -11,8 +11,7 @@ def run():
|
||||||
def run_debug():
|
def run_debug():
|
||||||
ui = _run_flask_debug()
|
ui = _run_flask_debug()
|
||||||
print(f'UI started: PID {ui.pid}')
|
print(f'UI started: PID {ui.pid}')
|
||||||
#ui.wait()
|
_run_simulator('debug')
|
||||||
_run_simulator('info')
|
|
||||||
|
|
||||||
|
|
||||||
def _run_flask_debug() -> subprocess.Popen:
|
def _run_flask_debug() -> subprocess.Popen:
|
||||||
|
|
|
@ -11,6 +11,7 @@ import signal
|
||||||
import data
|
import data
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
import plotext as plt
|
||||||
|
|
||||||
_RUN_FOR_SEC = 10
|
_RUN_FOR_SEC = 10
|
||||||
_LOG_LEVEL = 20 # INFO
|
_LOG_LEVEL = 20 # INFO
|
||||||
|
@ -18,6 +19,8 @@ _LOG_LEVEL = 20 # INFO
|
||||||
_statistic = {}
|
_statistic = {}
|
||||||
_start_time = None
|
_start_time = None
|
||||||
_active = {}
|
_active = {}
|
||||||
|
_inbound_read_freq = {}
|
||||||
|
_inbound_write_freq = {}
|
||||||
|
|
||||||
|
|
||||||
def get_run_options():
|
def get_run_options():
|
||||||
|
@ -79,22 +82,38 @@ def handle_client(instance, ct: dict):
|
||||||
filter(lambda x: x['Type'] == 'Holding' and x['CanOverride'] is True,
|
filter(lambda x: x['Type'] == 'Holding' and x['CanOverride'] is True,
|
||||||
registers), None)
|
registers), None)
|
||||||
|
|
||||||
|
inputRegs = filter(lambda x: x['Type'] == 'Input', registers)
|
||||||
|
|
||||||
regs = {}
|
regs = {}
|
||||||
for r in registers:
|
for r in registers:
|
||||||
regs[r['Address']] = r['Name']
|
regs[r['Address']] = r['Name']
|
||||||
info = {'Model': ct['Name'], 'Port': instance.port, 'Registers': regs}
|
info = {'Model': ct['Name'], 'Port': instance.port, 'Registers': regs}
|
||||||
|
|
||||||
if holdingRead:
|
if holdingRead:
|
||||||
result = instance.read_holding_registers(holdingRead['Address'], 1)
|
result = instance.read_holding_registers(holdingRead['Address'], 1)
|
||||||
if not result:
|
if result is None:
|
||||||
logging.debug(f'{ct["Name"]} client@{instance.port} no connection')
|
logging.debug(f'{ct["Name"]} client@{instance.port} no connection')
|
||||||
else:
|
else:
|
||||||
gather('read', 'holding', holdingRead['Address'], instance.port,
|
gather('read', 'holding', holdingRead['Address'], instance.port,
|
||||||
result[0], info, 'outbound')
|
result[0], info, 'outbound')
|
||||||
|
data.set_controller_register(info['Model'], info['Port'],
|
||||||
|
holdingRead['Name'], result[0])
|
||||||
if holdingWrite:
|
if holdingWrite:
|
||||||
v = holdingWrite['CurrentValue']
|
v = holdingWrite['CurrentValue']
|
||||||
instance.write_single_register(holdingWrite['Address'], v)
|
instance.write_single_register(holdingWrite['Address'], v)
|
||||||
gather('write', 'holding', holdingWrite['Address'],
|
gather('write', 'holding', holdingWrite['Address'],
|
||||||
instance.port, v, info, 'outbound')
|
instance.port, v, info, 'outbound')
|
||||||
|
for r in list(inputRegs):
|
||||||
|
result = instance.read_input_registers(r['Address'])
|
||||||
|
if result is not None:
|
||||||
|
gather('read', 'input', r['Address'], instance.port,
|
||||||
|
result[0], info, 'outbound')
|
||||||
|
data.set_controller_register(info['Model'], info['Port'],
|
||||||
|
r['Name'], result[0])
|
||||||
|
else:
|
||||||
|
logging.warning(
|
||||||
|
f'Input register {r["Name"]} on server {instance.port} is misconfigured. Read result is None'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f'{ct["Name"]} client@{instance.port} is not fully configured')
|
f'{ct["Name"]} client@{instance.port} is not fully configured')
|
||||||
|
@ -186,11 +205,13 @@ def gather(op, registry, address, port, value, info, tag='inbound'):
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
void
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global _active
|
global _active
|
||||||
global _statistic
|
global _statistic
|
||||||
|
global _inbound_read_freq
|
||||||
|
global _inbound_write_freq
|
||||||
controller = f'{info["Model"]}@{info["Port"]}'
|
controller = f'{info["Model"]}@{info["Port"]}'
|
||||||
try:
|
try:
|
||||||
# client port here is a port of incoming connection
|
# client port here is a port of incoming connection
|
||||||
|
@ -216,6 +237,104 @@ def gather(op, registry, address, port, value, info, tag='inbound'):
|
||||||
else:
|
else:
|
||||||
logging.debug(msg)
|
logging.debug(msg)
|
||||||
|
|
||||||
|
freq_key = f'{op};{registry} {address};{controller}'
|
||||||
|
if tag == 'inbound' and (op == 'read' or op == 'write'):
|
||||||
|
collection = _inbound_read_freq if op == 'read' else _inbound_write_freq
|
||||||
|
entry = collection.get(freq_key)
|
||||||
|
if entry:
|
||||||
|
entry[0] += 1
|
||||||
|
else:
|
||||||
|
collection[freq_key] = {0: 0}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_freq(interval_sec: int, dt: datetime.datetime):
|
||||||
|
global _inbound_read_freq
|
||||||
|
global _inbound_write_freq
|
||||||
|
complete_freq_period(_inbound_read_freq, interval_sec, dt)
|
||||||
|
complete_freq_period(_inbound_write_freq, interval_sec, dt)
|
||||||
|
|
||||||
|
|
||||||
|
def complete_freq_period(collection: dict, interval_sec: int,
|
||||||
|
dt: datetime.datetime):
|
||||||
|
|
||||||
|
format = '%Y-%d-%m %H:%M:%S'
|
||||||
|
dt_str = dt.strftime(format)
|
||||||
|
hour_ago_str = (dt - datetime.timedelta(hours=1)).strftime(format)
|
||||||
|
for k, v in collection.items():
|
||||||
|
v[dt_str] = v[0] / interval_sec
|
||||||
|
v[0] = 0
|
||||||
|
keys_to_del = []
|
||||||
|
# starting from 3.7 insertion order in dict is preserved
|
||||||
|
for entry, _ in v.items():
|
||||||
|
if entry == 0:
|
||||||
|
continue
|
||||||
|
if entry < hour_ago_str:
|
||||||
|
keys_to_del.append(entry)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
for entry in keys_to_del:
|
||||||
|
del v[entry]
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dict(base: dict, incoming: dict) -> dict:
|
||||||
|
result = {**base, **incoming}
|
||||||
|
for key in result.keys():
|
||||||
|
if key in base and key in incoming:
|
||||||
|
if isinstance(base[key], list) and isinstance(incoming[key], list):
|
||||||
|
result[key] = base[key] + incoming[key]
|
||||||
|
elif isinstance(base[key], list):
|
||||||
|
result[key] = [*base[key], incoming[key]]
|
||||||
|
elif isinstance(incoming[key], list):
|
||||||
|
result[key] = [base[key], *incoming[key]]
|
||||||
|
else:
|
||||||
|
result[key] = [base[key], incoming[key]]
|
||||||
|
else:
|
||||||
|
if not isinstance(result[key], list):
|
||||||
|
result[key] = [result[key]]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_freq(collection: dict):
|
||||||
|
for k, v in collection.items():
|
||||||
|
del v[0]
|
||||||
|
# print(f'{k}:')
|
||||||
|
# for timestamp, freq in v.items():
|
||||||
|
# print(f'{timestamp} {freq} ')
|
||||||
|
merged = {}
|
||||||
|
for k, v in collection.items():
|
||||||
|
merged = merge_dict(merged, v)
|
||||||
|
aggregated = {}
|
||||||
|
for k, v in merged.items():
|
||||||
|
v = round(sum(v) / len(v), 2)
|
||||||
|
aggregated[k] = v
|
||||||
|
return aggregated
|
||||||
|
|
||||||
|
|
||||||
|
def report_freq():
|
||||||
|
global _inbound_read_freq
|
||||||
|
global _inbound_write_freq
|
||||||
|
|
||||||
|
reads = aggregate_freq(_inbound_read_freq)
|
||||||
|
writes = aggregate_freq(_inbound_write_freq)
|
||||||
|
if not reads and not writes:
|
||||||
|
return
|
||||||
|
|
||||||
|
plt.date_form('Y-d-m H:M:S', 'M:S')
|
||||||
|
if reads:
|
||||||
|
plt.plot(reads.keys(),
|
||||||
|
reads.values(),
|
||||||
|
label='inbound read frequency',
|
||||||
|
color='blue')
|
||||||
|
if writes:
|
||||||
|
plt.plot(writes.keys(),
|
||||||
|
writes.values(),
|
||||||
|
label='inbound write frequency',
|
||||||
|
color='magenta')
|
||||||
|
plt.title('Inbound Read/Write Frequency')
|
||||||
|
plt.xlabel('Time (max 1 hour long)')
|
||||||
|
plt.ylabel('Operations per second')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
def report():
|
def report():
|
||||||
global _statistic
|
global _statistic
|
||||||
|
@ -300,12 +419,12 @@ class MaBank(DataBank):
|
||||||
|
|
||||||
def on_coils_change(self, address, from_value, to_value, srv_info):
|
def on_coils_change(self, address, from_value, to_value, srv_info):
|
||||||
gather('change event', 'coils', address, srv_info.client.port,
|
gather('change event', 'coils', address, srv_info.client.port,
|
||||||
f'{from_value}⟶{to_value}', self.info)
|
f'{from_value}→{to_value}', self.info)
|
||||||
|
|
||||||
def on_holding_registers_change(self, address, from_value, to_value,
|
def on_holding_registers_change(self, address, from_value, to_value,
|
||||||
srv_info):
|
srv_info):
|
||||||
gather('change event', 'holding', address, srv_info.client.port,
|
gather('change event', 'holding', address, srv_info.client.port,
|
||||||
f'{from_value}⟶{to_value}', self.info)
|
f'{from_value}→{to_value}', self.info)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -316,7 +435,7 @@ if __name__ == '__main__':
|
||||||
level=_LOG_LEVEL)
|
level=_LOG_LEVEL)
|
||||||
_start_time = datetime.datetime.now()
|
_start_time = datetime.datetime.now()
|
||||||
|
|
||||||
version = data.get_version()
|
version = data.get_info()['version']
|
||||||
logging.info(f'abibas v{version}. Light controllers MODBUS simulator')
|
logging.info(f'abibas v{version}. Light controllers MODBUS simulator')
|
||||||
|
|
||||||
while run():
|
while run():
|
||||||
|
@ -339,12 +458,14 @@ if __name__ == '__main__':
|
||||||
else:
|
else:
|
||||||
handle_client(instance, c['Model'])
|
handle_client(instance, c['Model'])
|
||||||
sleep(5)
|
sleep(5)
|
||||||
|
calculate_freq(5, datetime.datetime.now())
|
||||||
|
|
||||||
report()
|
report()
|
||||||
|
report_freq()
|
||||||
dispose()
|
dispose()
|
||||||
|
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
exc = sys.exception()
|
exc = sys.exception()
|
||||||
traceback.print_tb(exc.__traceback__, limit=3, file=sys.stdout)
|
traceback.print_tb(exc.__traceback__, limit=3, file=sys.stdout)
|
||||||
logging.critical('An epic failure just has happened!')
|
logging.critical(f'An epic failure just has happened! ({ex})')
|
||||||
dispose()
|
dispose()
|
||||||
|
|
|
@ -15,7 +15,9 @@ bp = Blueprint('controller_types', __name__)
|
||||||
@bp.route('/types')
|
@bp.route('/types')
|
||||||
def list():
|
def list():
|
||||||
models = data.get_controller_types()
|
models = data.get_controller_types()
|
||||||
return render_template('controller_types/list.html', config=models)
|
return render_template('controller_types/list.html',
|
||||||
|
config=models,
|
||||||
|
footer=data.get_info())
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/types/add', methods=("GET", "POST"))
|
@bp.route('/types/add', methods=("GET", "POST"))
|
||||||
|
@ -34,7 +36,8 @@ def add():
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for('controller_types.edit', name=request.form['name']))
|
url_for('controller_types.edit', name=request.form['name']))
|
||||||
|
|
||||||
return render_template('controller_types/create.html')
|
return render_template('controller_types/create.html',
|
||||||
|
footer=data.get_info())
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/types/<name>/edit', methods=("GET", "POST"))
|
@bp.route('/types/<name>/edit', methods=("GET", "POST"))
|
||||||
|
@ -65,7 +68,9 @@ def edit(name: str):
|
||||||
existing = _prepare_for_editing(existing['Name'])
|
existing = _prepare_for_editing(existing['Name'])
|
||||||
break
|
break
|
||||||
|
|
||||||
return render_template('controller_types/update.html', data=existing)
|
return render_template('controller_types/update.html',
|
||||||
|
data=existing,
|
||||||
|
footer=data.get_info())
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/types/<name>/copy', methods=("GET", ))
|
@bp.route('/types/<name>/copy', methods=("GET", ))
|
||||||
|
@ -94,6 +99,7 @@ def _find_model_by_name(name: str, models: dict = None) -> dict:
|
||||||
|
|
||||||
def _prepare_for_editing(name: str):
|
def _prepare_for_editing(name: str):
|
||||||
types = ['Input', 'Holding', 'Coil', 'Discrete']
|
types = ['Input', 'Holding', 'Coil', 'Discrete']
|
||||||
|
variants = ['DropdownVariants', 'RawBinary', 'RawInteger']
|
||||||
exists = _find_model_by_name(name)
|
exists = _find_model_by_name(name)
|
||||||
if not exists:
|
if not exists:
|
||||||
return None
|
return None
|
||||||
|
@ -104,6 +110,7 @@ def _prepare_for_editing(name: str):
|
||||||
processed.append(f'{k}={v}')
|
processed.append(f'{k}={v}')
|
||||||
reg['PossibleValues'] = '\n'.join(processed)
|
reg['PossibleValues'] = '\n'.join(processed)
|
||||||
exists['RegTypes'] = types
|
exists['RegTypes'] = types
|
||||||
|
exists['DisplayVariants'] = variants
|
||||||
return exists
|
return exists
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,7 +120,10 @@ def _save_model(model: dict, current_name: str):
|
||||||
del model['RegTypes']
|
del model['RegTypes']
|
||||||
_remove_reg(model, '')
|
_remove_reg(model, '')
|
||||||
for r in model['Registers']:
|
for r in model['Registers']:
|
||||||
r['PossibleValues'] = _str_to_possible(r['PossibleValues'])
|
if 'PossibleValues' in r:
|
||||||
|
r['PossibleValues'] = _str_to_possible(r['PossibleValues'])
|
||||||
|
else:
|
||||||
|
r['PossibleValues'] = []
|
||||||
models.insert(0, model)
|
models.insert(0, model)
|
||||||
data.set_controller_types(models)
|
data.set_controller_types(models)
|
||||||
|
|
||||||
|
@ -146,6 +156,8 @@ def _update_reg(model: dict, reg_name: str, form: dict):
|
||||||
updated['CanOverride'] = v == 'True'
|
updated['CanOverride'] = v == 'True'
|
||||||
elif k == f'{reg_name}-type':
|
elif k == f'{reg_name}-type':
|
||||||
updated['Type'] = v
|
updated['Type'] = v
|
||||||
|
elif k == f'{reg_name}-display-as':
|
||||||
|
updated['DisplayAs'] = v
|
||||||
elif k == f'{reg_name}-possible':
|
elif k == f'{reg_name}-possible':
|
||||||
updated['PossibleValues'] = _str_to_possible(v)
|
updated['PossibleValues'] = _str_to_possible(v)
|
||||||
_remove_reg(model, reg_name)
|
_remove_reg(model, reg_name)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Setting up light controllers and monitoring their state"""
|
"""Setting up light controllers and monitoring their state"""
|
||||||
|
|
||||||
|
import re
|
||||||
import data
|
import data
|
||||||
from flask import (Blueprint, flash, g, redirect, render_template, request,
|
from flask import (Blueprint, flash, g, redirect, render_template, request,
|
||||||
url_for)
|
url_for)
|
||||||
|
@ -10,8 +11,10 @@ bp = Blueprint('controllers', __name__)
|
||||||
|
|
||||||
@bp.route('/', methods=('GET', ))
|
@bp.route('/', methods=('GET', ))
|
||||||
def list():
|
def list():
|
||||||
data = get_list()
|
lst = get_list()
|
||||||
return render_template('controllers/index.html', config=data)
|
return render_template('controllers/index.html',
|
||||||
|
config=lst,
|
||||||
|
footer=data.get_info())
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/override', methods=('POST', ))
|
@bp.route('/override', methods=('POST', ))
|
||||||
|
@ -27,13 +30,22 @@ def update():
|
||||||
if k == 'enabled':
|
if k == 'enabled':
|
||||||
enabled = v
|
enabled = v
|
||||||
continue
|
continue
|
||||||
registers.append({'Name': k, 'Value': int(v)})
|
registers.append({'Name': k, 'Value': v})
|
||||||
|
|
||||||
controllers = data.get_controllers()
|
controllers = data.get_controllers()
|
||||||
existing = _find(port, controllers)
|
existing = _find(port, controllers)
|
||||||
if existing is None:
|
if existing is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
current_regs = data.get_controller_registers(existing['Port'])
|
||||||
|
for r in registers:
|
||||||
|
reg = next(filter(lambda x: x['Name'] == r['Name'], current_regs), r)
|
||||||
|
display_as = reg.get('DisplayAs', 'DropdownVariants')
|
||||||
|
if display_as == 'RawBinary' and _is_binary_str(r['Value']):
|
||||||
|
r['Value'] = int(r['Value'], 2)
|
||||||
|
else:
|
||||||
|
r['Value'] = int(r['Value'])
|
||||||
|
|
||||||
data.set_controller_registers(existing['Type'], existing['Port'],
|
data.set_controller_registers(existing['Type'], existing['Port'],
|
||||||
registers)
|
registers)
|
||||||
enabled = enabled == 'True'
|
enabled = enabled == 'True'
|
||||||
|
@ -89,15 +101,31 @@ def add():
|
||||||
data.set_controllers(controllers)
|
data.set_controllers(controllers)
|
||||||
return redirect(url_for('controllers.list'))
|
return redirect(url_for('controllers.list'))
|
||||||
|
|
||||||
return render_template('controllers/create.html', types=types)
|
return render_template('controllers/create.html',
|
||||||
|
types=types,
|
||||||
|
footer=data.get_info())
|
||||||
|
|
||||||
|
|
||||||
def _get_value_name(value: int, possibleValues: list) -> str:
|
def _get_current_value(reg: dict) -> str:
|
||||||
for possible in possibleValues:
|
if 'DisplayAs' in reg and reg['DisplayAs'] == 'RawBinary':
|
||||||
for k, v in possible.items():
|
return f'{reg["CurrentValue"]:b}'.zfill(16)
|
||||||
if v == value:
|
else:
|
||||||
return k
|
return reg['CurrentValue']
|
||||||
return 'Unknown'
|
|
||||||
|
|
||||||
|
def _get_value_name(reg: dict) -> str:
|
||||||
|
value = reg['CurrentValue']
|
||||||
|
display_as = reg.get('DisplayAs', 'DropdownVariants')
|
||||||
|
if display_as == 'RawBinary':
|
||||||
|
return value
|
||||||
|
elif display_as == 'RawInteger':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
for possible in reg['PossibleValues']:
|
||||||
|
for k, v in possible.items():
|
||||||
|
if v == value:
|
||||||
|
return k
|
||||||
|
return 'Undefined'
|
||||||
|
|
||||||
|
|
||||||
def get_list() -> dict:
|
def get_list() -> dict:
|
||||||
|
@ -113,9 +141,9 @@ def get_list() -> dict:
|
||||||
'Name':
|
'Name':
|
||||||
reg['Name'],
|
reg['Name'],
|
||||||
'Value':
|
'Value':
|
||||||
reg['CurrentValue'],
|
_get_current_value(reg),
|
||||||
'ValueStr':
|
'ValueStr':
|
||||||
_get_value_name(reg['CurrentValue'], reg['PossibleValues']),
|
_get_value_name(reg),
|
||||||
'Address':
|
'Address':
|
||||||
reg['Address'],
|
reg['Address'],
|
||||||
'Type':
|
'Type':
|
||||||
|
@ -123,7 +151,9 @@ def get_list() -> dict:
|
||||||
'PossibleValues':
|
'PossibleValues':
|
||||||
reg['PossibleValues'],
|
reg['PossibleValues'],
|
||||||
'CanOverride':
|
'CanOverride':
|
||||||
reg['CanOverride']
|
reg['CanOverride'],
|
||||||
|
'DisplayAs':
|
||||||
|
reg.get('DisplayAs', 'DropdownVariants')
|
||||||
})
|
})
|
||||||
|
|
||||||
return controllers
|
return controllers
|
||||||
|
@ -134,3 +164,8 @@ def _to_status(enabled: bool) -> str:
|
||||||
return 'Yes'
|
return 'Yes'
|
||||||
else:
|
else:
|
||||||
return 'No'
|
return 'No'
|
||||||
|
|
||||||
|
|
||||||
|
def _is_binary_str(s: str) -> bool:
|
||||||
|
pattern = r'^[01]+$'
|
||||||
|
return re.match(pattern, s) is not None
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
html {
|
html {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
padding: 1rem;
|
}
|
||||||
margin: 0.5rem;
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
|
@ -186,3 +190,25 @@ label {
|
||||||
flex-basis: 100%; /* Take full width of the container */
|
flex-basis: 100%; /* Take full width of the container */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: auto;
|
||||||
|
height: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #e5bdf1;
|
||||||
|
padding: 0.5rem 0.5rem;
|
||||||
|
opacity: 0.50;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey-box {
|
||||||
|
background-color: #d0d0d7;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
border-radius: 0.3rem
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<h1>Abibas (Automated Beacon Interface for Budget-friendly Aviation Safety)</h1>
|
<h1>Abibas (Automated Beacon Interface for Budget-friendly Aviation Safety)</h1>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -22,4 +23,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</section>
|
</section>
|
||||||
|
<footer>
|
||||||
|
<p>Version: {{ footer['version'] }} ⊚ Author: {{ footer['author'] }} ⊚ License: {{ footer['license'] }} </p>
|
||||||
|
</footer>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<h1>{% block title %}New Controller Type{% endblock %}</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<form method="post">
|
|
||||||
<div>
|
|
||||||
<div><label for="name">Name:</label></div>
|
|
||||||
<div><input name="name" id="name" value="{{ request.form['name'] }}" required></div>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<div>
|
|
||||||
<div><label for="runAs">Run As:</label></div>
|
|
||||||
<div><select id="runAs" name="runAs">
|
|
||||||
<option value="Server">Server</option>
|
|
||||||
<option value="Client">Client</option>
|
|
||||||
</select></div>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Add">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p>Note: once new type is created, you can Edit the type to add necessary registers. Changing "Run As" option will not be possible</p>
|
|
||||||
{% endblock %}
|
|
|
@ -19,7 +19,9 @@
|
||||||
<div class="setting-wide">
|
<div class="setting-wide">
|
||||||
<div><label for="reg-{{ reg['Name'] }}">{{ reg['Type'] }} {{ reg['Address'] }} ({{ reg['Name'] }}):</label></div>
|
<div><label for="reg-{{ reg['Name'] }}">{{ reg['Type'] }} {{ reg['Address'] }} ({{ reg['Name'] }}):</label></div>
|
||||||
|
|
||||||
<div><select id="reg-{{ reg['Name'] }}" name="{{ reg['Name'] }}">
|
<div>
|
||||||
|
{% if reg['PossibleValues'] %}
|
||||||
|
<select id="reg-{{ reg['Name'] }}" name="{{ reg['Name'] }}">
|
||||||
{% for option in reg['PossibleValues'] %}
|
{% for option in reg['PossibleValues'] %}
|
||||||
{% for key, value in option.items() %}
|
{% for key, value in option.items() %}
|
||||||
|
|
||||||
|
@ -27,7 +29,14 @@
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select></div>
|
</select>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="grey-box">{{ reg['DisplayAs'] }}</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -61,10 +61,17 @@
|
||||||
<div><label for="{{ reg['Name'] }}-address">Address:</label></div>
|
<div><label for="{{ reg['Name'] }}-address">Address:</label></div>
|
||||||
<div><input name="{{ reg['Name'] }}-address" id="{{ reg['Name'] }}-address" value="{{ request.form[reg['Name'] ~ '-address'] or reg['Address'] }}" placeholder="12345" required></div>
|
<div><input name="{{ reg['Name'] }}-address" id="{{ reg['Name'] }}-address" value="{{ request.form[reg['Name'] ~ '-address'] or reg['Address'] }}" placeholder="12345" required></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><label for="{{ reg['Name'] }}-display-as">Display value as:</label></div>
|
||||||
|
<div><select id="{{ reg['Name'] }}-display-as" name="{{ reg['Name'] }}-display-as">
|
||||||
|
{% for variant in data['DisplayVariants'] %}
|
||||||
|
<option value={{ variant }} {% if variant == reg['DisplayAs'] %}selected="selected"{% endif %}>{{ variant }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div><label for="{{ reg['Name'] }}-possible">Possible values:</label></div>
|
<div><label for="{{ reg['Name'] }}-possible">Possible values:</label></div>
|
||||||
<div><textarea name="{{ reg['Name'] }}-possible" id="{{ reg['Name'] }}-possible" rows="6" cols="28" placeholder="ValueNameOne=14 ValueNameTwo=3" required>{{ request.form[reg['Name'] ~ '-possible'] or reg['PossibleValues'] }}</textarea></div>
|
<div><textarea name="{{ reg['Name'] }}-possible" id="{{ reg['Name'] }}-possible" rows="6" cols="28" placeholder="ValueNameOne=14 ValueNameTwo=3" {% if reg['DisplayAs'] == 'DropdownVariants' %} required {% else %} disabled {% endif %} >{{ request.form[reg['Name'] ~ '-possible'] or reg['PossibleValues'] }}</textarea></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
<div class="setting">
|
<div class="setting">
|
||||||
<label {% if not reg['CanOverride'] %} class="red" {% endif %}>{{ reg['Name'] }} ({{ reg['Type'] }}):</label>
|
<label {% if not reg['CanOverride'] %} class="red" {% endif %}>{{ reg['Name'] }} ({{ reg['Type'] }}):</label>
|
||||||
<div>[{{ '%05d' % reg['Address'] }}] ➡ {{ reg['Value'] }} ({{ reg['ValueStr'] }})
|
<div>[{{ '%05d' % reg['Address'] }}] ➡ {{ reg['Value'] }} {% if reg['ValueStr'] != '' %}({{ reg['ValueStr'] }}){% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,23 +49,25 @@
|
||||||
</select></div>
|
</select></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% for reg in controller['Registers'] %}
|
{% for reg in controller['Registers'] %}
|
||||||
{% if reg['CanOverride'] %}
|
{% if reg['CanOverride'] %}
|
||||||
|
|
||||||
<div class="setting-wide">
|
<div class="setting-wide">
|
||||||
<div><label for="reg-{{ reg['Name'] }}">{{ reg['Name'] }} value:</label></div>
|
<div><label for="reg-{{ reg['Name'] }}">{{ reg['Name'] }} value:</label></div>
|
||||||
|
|
||||||
|
{% if not reg['DisplayAs'] or reg['DisplayAs'] == 'DropdownVariants' %}
|
||||||
|
|
||||||
<div><select id="reg-{{ reg['Name'] }}" name="{{ reg['Name'] }}">
|
<div><select id="reg-{{ reg['Name'] }}" name="{{ reg['Name'] }}">
|
||||||
{% for option in reg['PossibleValues'] %}
|
{% for option in reg['PossibleValues'] %}
|
||||||
{% for key, value in option.items() %}
|
{% for key, value in option.items() %}
|
||||||
{% if value == reg['Value'] %}
|
<option value="{{ value }}" {% if value == reg['Value'] %}selected{% endif %}>{{ key }} ({{ value }})</option>
|
||||||
<option value="{{ value }}" selected>{{ key }} ({{ value }})</option>
|
|
||||||
{% else %}
|
|
||||||
<option value="{{ value }}">{{ key }} ({{ value }})</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select></div>
|
</select></div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div><input id="reg-{{ reg['Name'] }}" name="{{ reg['Name'] }}" value="{{ reg['Value'] }}" required></div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in New Issue