From 59ade04f7ecdff1310fea0098f1216757b70516a Mon Sep 17 00:00:00 2001
From: Taryel Hlontsi
Date: Wed, 25 Oct 2023 16:32:23 +0200
Subject: [PATCH] Add ability to run arbitrary client script, specifying func
codes
---
src/data/__init__.py | 18 +-
src/data/controller-models.json | 343 +++++++++++-------
src/data/controllers.json | 24 +-
src/modbus/simulator.py | 145 +++++---
src/ui/controller_types.py | 26 +-
src/ui/controllers.py | 70 ++--
src/ui/templates/controller_types/create.html | 1 +
src/ui/templates/controller_types/list.html | 13 +-
src/ui/templates/controller_types/update.html | 38 +-
src/ui/templates/controllers/index.html | 12 +-
10 files changed, 465 insertions(+), 225 deletions(-)
diff --git a/src/data/__init__.py b/src/data/__init__.py
index 02eb0e3..d09a2ed 100644
--- a/src/data/__init__.py
+++ b/src/data/__init__.py
@@ -64,10 +64,12 @@ def _save_overrides(fname: str, overrides: list):
json.dump(overrides, file, indent=2)
-def get_controller(port: int) -> dict:
+def get_controller(port: int, model: str) -> dict:
controllers = get_controllers()
models = get_controller_types()
- controller = next(filter(lambda x: x['Port'] == port, controllers), None)
+ controller = next(
+ filter(lambda x: x['Port'] == port and x['Type'] == model,
+ controllers), None)
if not controller:
return None
model = next(filter(lambda x: x['Name'] == controller['Type'], models),
@@ -76,12 +78,13 @@ def get_controller(port: int) -> dict:
return controller
-def get_controller_registers(port: int) -> list:
+def get_controller_registers(port: int, model_name: str) -> list:
result = []
- controller = get_controller(port)
+ controller = get_controller(port, model_name)
for reg in controller['Model']['Registers']:
reg['CurrentValue'] = _get_reg_value(controller, reg)
result.append(reg)
+ result = sorted(result, key=lambda x: x['Address'])
return result
@@ -112,6 +115,7 @@ def set_controller_register(ct: str, port: int, reg: str, val):
def get_controllers() -> dict:
+ """Returns controllers along with their type represented as 'Model'."""
with open(_get_controlers_fname(), 'r') as file:
try:
controllers = json.load(file)
@@ -133,9 +137,15 @@ def set_controllers(data: list):
def get_controller_types() -> dict:
+ """Returns controller types as they are in the json file."""
with open(_get_controller_models_fname(), 'r') as file:
try:
models = json.load(file)
+ for m in models:
+ registers = m.get('Registers', [])
+ for r in registers:
+ if 'Function' not in r:
+ r['Function'] = 'F16'
except (json.JSONDecodeError):
models = []
return models
diff --git a/src/data/controller-models.json b/src/data/controller-models.json
index 08b7f22..08b3f59 100644
--- a/src/data/controller-models.json
+++ b/src/data/controller-models.json
@@ -1,91 +1,53 @@
[
+ {
+ "Name": "ThirdPartyInput",
+ "RunAs": "Client-script",
+ "Script": "import time\r\nt = int(time.time() * 1000)\r\ncounter = 0\r\nchunks = []\r\nchunks.append(2)\r\nchunks.append(counter)\r\nchunks.append((t >> 48))\r\nchunks.append((t >> 32) & 0xFFFF)\r\nchunks.append((t >> 16) & 0xFFFF)\r\nchunks.append(t & 0xFFFF)\r\nchunks.append(128)\r\nchunks.append(5)\r\nchunks.append(65)\r\ninstance.write_multiple_registers(505, chunks)",
+ "ScriptExample": "import time\r\nt = int(time.time() * 1000)\r\ncounter = 0\r\nchunks = []\r\nchunks.append(2)\r\nchunks.append(counter)\r\nchunks.append((t >> 48))\r\nchunks.append((t >> 32) & 0xFFFF)\r\nchunks.append((t >> 16) & 0xFFFF)\r\nchunks.append(t & 0xFFFF)\r\nchunks.append(129)\r\nchunks.append(1)\r\nchunks.append(65)\r\ninstance.write_multiple_registers(505, chunks)"
+ },
{
"Name": "Ads",
"RunAs": "Client",
"Registers": [
{
"CanOverride": false,
- "Name": "Status2",
- "Type": "Input",
+ "Name": "Status",
+ "Type": "Holding",
+ "Function": "F3",
"Address": 0,
"DisplayAs": "DropdownVariants",
"PossibleValues": [
{
- "NotOperting": 0
+ "Oki": 1
},
{
- "Operating": 1
+ "Error": 0
}
]
},
{
"CanOverride": false,
- "Name": "Sim160",
+ "Name": "LastExposedTurbine",
"Type": "Input",
+ "Function": "F4",
"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",
+ "Name": "SecondExposedTurbine",
"Type": "Input",
+ "Function": "F4",
"Address": 770,
"DisplayAs": "RawBinary",
"PossibleValues": []
},
{
"CanOverride": false,
- "Name": "Sim1",
+ "Name": "FirstExposedTurbine",
"Type": "Input",
+ "Function": "F4",
"Address": 769,
"DisplayAs": "RawBinary",
"PossibleValues": []
@@ -94,32 +56,72 @@
"CanOverride": false,
"Name": "SystemStatusCode",
"Type": "Input",
+ "Function": "F4",
"Address": 1,
"DisplayAs": "RawBinary",
"PossibleValues": []
},
{
- "CanOverride": true,
- "Name": "Interface",
- "Type": "Holding",
- "Address": 512,
- "PossibleValues": [
- {
- "Deactivated": 0
- },
- {
- "AircraftDetected": 1
- },
- {
- "AircraftNotDetected": 3
- }
- ]
+ "CanOverride": false,
+ "Name": "InterfaceVersion",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 260,
+ "DisplayAs": "RawBinary",
+ "PossibleValues": []
},
{
"CanOverride": false,
- "Name": "Status",
- "Type": "Holding",
+ "Name": "TurbinesConfigured",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 261,
+ "DisplayAs": "RawBinary",
+ "PossibleValues": []
+ },
+ {
+ "CanOverride": false,
+ "Name": "ConfigUnixTime1",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 256,
+ "DisplayAs": "RawBinary",
+ "PossibleValues": []
+ },
+ {
+ "CanOverride": false,
+ "Name": "ConfigUnixTime2",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 257,
+ "DisplayAs": "RawBinary",
+ "PossibleValues": []
+ },
+ {
+ "CanOverride": false,
+ "Name": "ConfigUnixTime3",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 258,
+ "DisplayAs": "RawBinary",
+ "PossibleValues": []
+ },
+ {
+ "CanOverride": false,
+ "Name": "ConfigUnixTime4",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 259,
+ "DisplayAs": "RawBinary",
+ "PossibleValues": []
+ },
+ {
+ "CanOverride": false,
+ "Name": "Status2",
+ "Type": "Input",
+ "Function": "F4",
"Address": 0,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"NotOperting": 0
@@ -134,6 +136,16 @@
"DropdownVariants",
"RawBinary",
"RawInteger"
+ ],
+ "Functions": [
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F15",
+ "F16"
]
},
{
@@ -142,40 +154,33 @@
"Registers": [
{
"CanOverride": true,
- "Name": "RemoteReset",
- "Type": "Coil",
- "Address": 20001,
+ "Name": "PhotecellSensing",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 20202,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
- "NoReset": 0
+ "Night": 1
},
{
- "Reset": 1
- }
- ]
- },
- {
- "CanOverride": false,
- "Name": "ForcedOffAdls",
- "Type": "Holding",
- "Address": 20210,
- "PossibleValues": [
- {
- "Auto": 0
+ "Twilight": 2
},
{
- "AircraftNotDetected": 128
+ "Day": 3
},
{
- "AircraftDetected": 129
+ "FailSafeIntensity": 255
}
]
},
{
"CanOverride": true,
- "Name": "Status",
+ "Name": "FullSystemStatus",
"Type": "Input",
+ "Function": "F4",
"Address": 19000,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"Normal": 1
@@ -188,6 +193,9 @@
},
{
"FailAndAlarm": 4
+ },
+ {
+ "BAD": 666
}
]
},
@@ -195,7 +203,9 @@
"CanOverride": true,
"Name": "TimeOfDay",
"Type": "Input",
+ "Function": "F4",
"Address": 20200,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"ControllerOff": 0
@@ -223,11 +233,53 @@
}
]
},
+ {
+ "CanOverride": false,
+ "Name": "ForcedOffAdls",
+ "Type": "Holding",
+ "Function": "F3",
+ "Address": 20210,
+ "DisplayAs": "DropdownVariants",
+ "PossibleValues": [
+ {
+ "Auto": 0
+ },
+ {
+ "AircraftNotDetected": 128
+ },
+ {
+ "AircraftDetected": 129
+ }
+ ]
+ }
+ ],
+ "DisplayVariants": [
+ "DropdownVariants",
+ "RawBinary",
+ "RawInteger"
+ ],
+ "Functions": [
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F15",
+ "F16"
+ ]
+ },
+ {
+ "Name": "CIP400",
+ "RunAs": "Server",
+ "Registers": [
{
"CanOverride": true,
- "Name": "OverallSystemStatus",
+ "Name": "SelfStatus",
"Type": "Input",
+ "Function": "F4",
"Address": 19005,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"Normal": 1
@@ -242,18 +294,14 @@
"FailAndAlarm": 4
}
]
- }
- ]
- },
- {
- "Name": "CIP400",
- "RunAs": "Server",
- "Registers": [
+ },
{
"CanOverride": true,
"Name": "RemoteReset",
"Type": "Coil",
+ "Function": "F5",
"Address": 20001,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"NoReset": 0
@@ -263,28 +311,13 @@
}
]
},
- {
- "CanOverride": false,
- "Name": "ForcedOffAdls",
- "Type": "Holding",
- "Address": 20210,
- "PossibleValues": [
- {
- "Auto": 0
- },
- {
- "AircraftNotDetected": 128
- },
- {
- "AircraftDetected": 129
- }
- ]
- },
{
"CanOverride": true,
"Name": "Status",
"Type": "Input",
+ "Function": "F4",
"Address": 19000,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"Normal": 1
@@ -304,7 +337,9 @@
"CanOverride": true,
"Name": "TimeOfDay",
"Type": "Input",
+ "Function": "F4",
"Address": 20200,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"ControllerOff": 0
@@ -336,7 +371,9 @@
"CanOverride": true,
"Name": "PhotocellSensing",
"Type": "Input",
+ "Function": "F4",
"Address": 20202,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"Night": 1
@@ -353,25 +390,39 @@
]
},
{
- "CanOverride": true,
- "Name": "OverallSystemStatus",
- "Type": "Input",
- "Address": 19005,
+ "CanOverride": false,
+ "Name": "ForcedOffAdls",
+ "Type": "Holding",
+ "Function": "F6",
+ "Address": 20210,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
- "Normal": 1
+ "Auto": 0
},
{
- "Fail": 2
+ "AircraftNotDetected": 128
},
{
- "Alarm": 3
- },
- {
- "FailAndAlarm": 4
+ "AircraftDetected": 129
}
]
}
+ ],
+ "DisplayVariants": [
+ "DropdownVariants",
+ "RawBinary",
+ "RawInteger"
+ ],
+ "Functions": [
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F15",
+ "F16"
]
},
{
@@ -380,9 +431,30 @@
"Registers": [
{
"CanOverride": false,
- "Name": "ForcedOffAdls",
+ "Name": "ForcedOffAdlsWrite",
"Type": "Holding",
+ "Function": "F6",
"Address": 512,
+ "DisplayAs": "DropdownVariants",
+ "PossibleValues": [
+ {
+ "Auto": 0
+ },
+ {
+ "AircraftDetected": 1
+ },
+ {
+ "AircraftNotDetected": 3
+ }
+ ]
+ },
+ {
+ "CanOverride": true,
+ "Name": "ForcedOffAdlsRead",
+ "Type": "Input",
+ "Function": "F4",
+ "Address": 512,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"Auto": 0
@@ -399,7 +471,9 @@
"CanOverride": true,
"Name": "OverallSystemStatus",
"Type": "Input",
+ "Function": "F4",
"Address": 0,
+ "DisplayAs": "DropdownVariants",
"PossibleValues": [
{
"NotOperting": 0
@@ -409,6 +483,21 @@
}
]
}
+ ],
+ "DisplayVariants": [
+ "DropdownVariants",
+ "RawBinary",
+ "RawInteger"
+ ],
+ "Functions": [
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F15",
+ "F16"
]
}
]
\ No newline at end of file
diff --git a/src/data/controllers.json b/src/data/controllers.json
index 34c8463..c2f83c5 100644
--- a/src/data/controllers.json
+++ b/src/data/controllers.json
@@ -1,10 +1,4 @@
[
- {
- "Type": "Ads",
- "Enabled": true,
- "Ip": "127.0.0.1",
- "Port": 2502
- },
{
"Type": "CIP400",
"Enabled": true,
@@ -15,7 +9,19 @@
"Type": "Quantec",
"Enabled": true,
"Ip": "127.0.0.1",
- "Port": 11700
+ "Port": 11704
+ },
+ {
+ "Type": "ThirdPartyInput",
+ "Enabled": true,
+ "Ip": "127.0.0.1",
+ "Port": 2502
+ },
+ {
+ "Type": "Ads",
+ "Enabled": true,
+ "Ip": "127.0.0.1",
+ "Port": 2502
},
{
"Type": "CIP300",
@@ -24,9 +30,9 @@
"Port": 11508
},
{
- "Type": "CIP400",
+ "Type": "Quantec",
"Enabled": true,
"Ip": "127.0.0.1",
- "Port": 11506
+ "Port": 11705
}
]
\ No newline at end of file
diff --git a/src/modbus/simulator.py b/src/modbus/simulator.py
index 09f65dd..0ce7813 100755
--- a/src/modbus/simulator.py
+++ b/src/modbus/simulator.py
@@ -62,61 +62,120 @@ def remove_instance(instance, key: str):
del _active[key]
-def handle_server(instance):
- for reg in data.get_controller_registers(instance.port):
+def handle_server(instance, ct: dict):
+ for reg in data.get_controller_registers(instance.port, ct.get('Name',
+ '')):
if not reg['CanOverride']:
continue
tp = reg['Type'].lower()
v = reg['CurrentValue']
if tp == 'input' and v is not None:
instance.data_bank.set_input_registers(int(reg['Address']), [v])
+ if tp == 'holding' and v is not None:
+ instance.data_bank.set_holding_registers(int(reg['Address']), [v])
+
+
+def handle_client_script(instance, script: str):
+ exec(script, {}, {'instance': instance})
def handle_client(instance, ct: dict):
- registers = data.get_controller_registers(instance.port)
- holdingRead = next(
- filter(lambda x: x['Type'] == 'Holding' and x['CanOverride'] is False,
- registers), None)
+ """ Acts as a Modbus client. Function codes matter.
- holdingWrite = next(
- filter(lambda x: x['Type'] == 'Holding' and x['CanOverride'] is True,
- registers), None)
+ FC1 - read coils
+ FC4 - read input
+ FC3 - read holding
+ FC5 - write coils
+ FC6 - write holding """
- inputRegs = filter(lambda x: x['Type'] == 'Input', registers)
+ registers = data.get_controller_registers(instance.port, ct['Name'])
+
+ inputRead = filter(lambda x: x['Function'] == 'F4', registers)
+ holdingRead = filter(lambda x: x['Function'] == 'F3', registers)
+ coilsRead = filter(lambda x: x['Function'] == 'F1', registers)
+ coilsWrite = filter(lambda x: x['Function'] == 'F5', registers)
+ holdingWrite = filter(lambda x: x['Function'] == 'F6', registers)
regs = {}
for r in registers:
regs[r['Address']] = r['Name']
info = {'Model': ct['Name'], 'Port': instance.port, 'Registers': regs}
- if holdingRead:
- result = instance.read_holding_registers(holdingRead['Address'], 1)
- if result is None:
- logging.debug(f'{ct["Name"]} client@{instance.port} no connection')
- else:
- gather('read', 'holding', holdingRead['Address'], instance.port,
- result[0], info, 'outbound')
+ warn = '{0} register {1} on server {2} is misconfigured. {3} call result is {4}'
+ connected = False
+
+ # Input read
+ for r in list(inputRead):
+ 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'],
- holdingRead['Name'], result[0])
- if holdingWrite:
- v = holdingWrite['CurrentValue']
- instance.write_single_register(holdingWrite['Address'], v)
- gather('write', 'holding', holdingWrite['Address'],
- 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:
- logging.warning(
- f'{ct["Name"]} client@{instance.port} is not fully configured')
+ r['Name'], result[0])
+ connected = True
+ else:
+ logging.debug(
+ warn.format('Input', r['Name'], instance.port, 'FC4', result))
+ break
+
+ # Holding read
+ for r in list(holdingRead):
+ result = instance.read_holding_registers(r['Address'])
+ if result is not None:
+ gather('read', 'holding', r['Address'], instance.port, result[0],
+ info, 'outbound')
+ data.set_controller_register(info['Model'], info['Port'],
+ r['Name'], result[0])
+ connected = True
+ else:
+ logging.debug(
+ warn.format('Holding', r['Name'], instance.port, 'FC3',
+ result))
+ break
+
+ # Coils read
+ for r in list(coilsRead):
+ result = instance.read_coils(r['Address'])
+ if result is not None:
+ gather('read', 'coils', r['Address'], instance.port, result[0],
+ info, 'outbound')
+ data.set_controller_register(info['Model'], info['Port'],
+ r['Name'], result[0])
+ connected = True
+ else:
+ logging.debug(
+ warn.format('Coils', r['Name'], instance.port, 'FC1', result))
+ break
+
+ # Coils write
+ for r in list(coilsWrite):
+ result = instance.write_single_coil(r['Address'],
+ r.get('CurrentValue', False))
+ if result is True:
+ gather('write', 'coils', r['Address'], instance.port,
+ r.get('CurrentValue', False), info, 'outbound')
+ connected = True
+ else:
+ logging.debug(
+ warn.format('Coils', r['Name'], instance.port, 'FC5', result))
+ break
+
+ # Holding write
+ for r in list(holdingWrite):
+ result = instance.write_single_register(r['Address'],
+ r.get('CurrentValue', 0))
+ if result is True:
+ gather('write', 'holding', r['Address'], instance.port,
+ r.get('CurrentValue', 0), info, 'outbound')
+ connected = True
+ else:
+ logging.debug(
+ warn.format('Holding', r['Name'], instance.port, 'FC6',
+ result))
+ break
+
+ if not connected:
+ logging.debug(f'{ct["Name"]} client@{instance.port} no connection')
def add_new(controllers: list):
@@ -297,9 +356,6 @@ def merge_dict(base: dict, incoming: dict) -> dict:
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)
@@ -446,7 +502,7 @@ if __name__ == '__main__':
for k in list(_active.keys()):
instance = _active[k]['instance']
c = next(
- filter(lambda c: instance.port == int(c['Port']),
+ filter(lambda x: f"{x['Type']}@{x['Port']}" == k,
controllers), None)
if not c or not c['Enabled']:
@@ -454,9 +510,12 @@ if __name__ == '__main__':
continue
if type(instance) is ModbusServer:
- handle_server(instance)
+ handle_server(instance, c['Model'])
else:
- handle_client(instance, c['Model'])
+ if c['Model']['RunAs'] == 'Client-script':
+ handle_client_script(instance, c['Model']['Script'])
+ else:
+ handle_client(instance, c['Model'])
sleep(5)
calculate_freq(5, datetime.datetime.now())
diff --git a/src/ui/controller_types.py b/src/ui/controller_types.py
index 876a0ce..3d424ab 100644
--- a/src/ui/controller_types.py
+++ b/src/ui/controller_types.py
@@ -61,6 +61,11 @@ def edit(name: str):
_save_model(existing, name)
existing = _prepare_for_editing(name)
break
+ elif key.startswith('script_'):
+ existing['Script'] = request.form['script']
+ _save_model(existing, name)
+ existing = _prepare_for_editing(name)
+ break
elif key.startswith('rename_'):
_abort_if_cannot_delete(name)
existing['Name'] = request.form['name']
@@ -98,11 +103,14 @@ def _find_model_by_name(name: str, models: dict = None) -> dict:
def _prepare_for_editing(name: str):
+ functions = ['F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F15', 'F16']
types = ['Input', 'Holding', 'Coil', 'Discrete']
variants = ['DropdownVariants', 'RawBinary', 'RawInteger']
exists = _find_model_by_name(name)
if not exists:
return None
+ if exists['RunAs'] == 'Client-script':
+ return exists
for reg in exists['Registers']:
processed = []
for possible in reg['PossibleValues']:
@@ -111,19 +119,21 @@ def _prepare_for_editing(name: str):
reg['PossibleValues'] = '\n'.join(processed)
exists['RegTypes'] = types
exists['DisplayVariants'] = variants
+ exists['Functions'] = functions
return exists
def _save_model(model: dict, current_name: str):
models = data.get_controller_types()
models = [x for x in models if x['Name'] != current_name]
- del model['RegTypes']
- _remove_reg(model, '')
- for r in model['Registers']:
- if 'PossibleValues' in r:
- r['PossibleValues'] = _str_to_possible(r['PossibleValues'])
- else:
- r['PossibleValues'] = []
+ if model['RunAs'] != 'Client-script':
+ del model['RegTypes']
+ _remove_reg(model, '')
+ for r in model['Registers']:
+ if 'PossibleValues' in r:
+ r['PossibleValues'] = _str_to_possible(r['PossibleValues'])
+ else:
+ r['PossibleValues'] = []
models.insert(0, model)
data.set_controller_types(models)
@@ -156,6 +166,8 @@ def _update_reg(model: dict, reg_name: str, form: dict):
updated['CanOverride'] = v == 'True'
elif k == f'{reg_name}-type':
updated['Type'] = v
+ elif k == f'{reg_name}-function':
+ updated['Function'] = v
elif k == f'{reg_name}-display-as':
updated['DisplayAs'] = v
elif k == f'{reg_name}-possible':
diff --git a/src/ui/controllers.py b/src/ui/controllers.py
index d139266..5a100a8 100644
--- a/src/ui/controllers.py
+++ b/src/ui/controllers.py
@@ -37,17 +37,21 @@ def update():
if existing is None:
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'])
+ if existing['Model']['RunAs'] != 'Client-script':
- data.set_controller_registers(existing['Type'], existing['Port'],
- registers)
+ current_regs = data.get_controller_registers(existing['Port'],
+ existing['Type'])
+ 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'],
+ registers)
enabled = enabled == 'True'
if existing['Enabled'] != enabled:
@@ -87,7 +91,7 @@ def add():
controller = _find(p, controllers)
if p < 1024 or p > 65536:
flash('Port should be in range 1025-65536')
- elif controller:
+ elif controller and controller['Model']['RunAs'] == 'Server':
flash('This port is already assigned to another controller!')
elif t not in types:
flash(f'Type {t} was not found')
@@ -134,27 +138,31 @@ def get_list() -> dict:
for controller in controllers:
controller['Enabled'] = _to_status(controller['Enabled'])
controller['Registers'] = []
- registers = data.get_controller_registers(controller['Port'])
+ if controller['Model']['RunAs'] != 'Client-script':
+ registers = data.get_controller_registers(controller['Port'],
+ controller['Type'])
- for reg in registers:
- controller['Registers'].append({
- 'Name':
- reg['Name'],
- 'Value':
- _get_current_value(reg),
- 'ValueStr':
- _get_value_name(reg),
- 'Address':
- reg['Address'],
- 'Type':
- reg['Type'],
- 'PossibleValues':
- reg['PossibleValues'],
- 'CanOverride':
- reg['CanOverride'],
- 'DisplayAs':
- reg.get('DisplayAs', 'DropdownVariants')
- })
+ for reg in registers:
+ controller['Registers'].append({
+ 'Name':
+ reg['Name'],
+ 'Value':
+ _get_current_value(reg),
+ 'ValueStr':
+ _get_value_name(reg),
+ 'Address':
+ reg['Address'],
+ 'Type':
+ reg['Type'],
+ 'PossibleValues':
+ reg['PossibleValues'],
+ 'CanOverride':
+ reg['CanOverride'],
+ 'DisplayAs':
+ reg.get('DisplayAs', 'DropdownVariants'),
+ 'Function':
+ reg['Function']
+ })
return controllers
diff --git a/src/ui/templates/controller_types/create.html b/src/ui/templates/controller_types/create.html
index ecd3acf..42d4f51 100644
--- a/src/ui/templates/controller_types/create.html
+++ b/src/ui/templates/controller_types/create.html
@@ -16,6 +16,7 @@
diff --git a/src/ui/templates/controller_types/list.html b/src/ui/templates/controller_types/list.html
index 18b8e83..231fef0 100644
--- a/src/ui/templates/controller_types/list.html
+++ b/src/ui/templates/controller_types/list.html
@@ -14,7 +14,16 @@
Copy
Delete
- {% for reg in type['Registers'] %}
+
+ {% if type['RunAs']== 'Client-script' %}
+
+
+
+
+
+ {% endif %}
+
+ {% for reg in type['Registers'] %}
@@ -30,10 +39,12 @@
{% endfor %}
{% endfor %}
+
{{ reg['Function'] }}
{% else %}
{{ reg['DisplayAs'] }}
+
{{ reg['Function'] }}
{% endif %}
diff --git a/src/ui/templates/controller_types/update.html b/src/ui/templates/controller_types/update.html
index 4d394a9..56e10af 100644
--- a/src/ui/templates/controller_types/update.html
+++ b/src/ui/templates/controller_types/update.html
@@ -18,7 +18,33 @@
-
+
+
+ {% if data['RunAs'] == 'Client-script' %}
+
+
+
+
+ Example of a script. `instance` object is available and represents MODBUS client that can be used for reading or writing registers
+
+
+
+
+
+
+
+
+
+ {% else %}
@@ -55,6 +81,15 @@
{% endfor %}
+
+
+
+
+
@@ -76,5 +111,6 @@
{% endfor %}
+ {% endif %}
{% endblock %}
diff --git a/src/ui/templates/controllers/index.html b/src/ui/templates/controllers/index.html
index 575c912..8d94d77 100644
--- a/src/ui/templates/controllers/index.html
+++ b/src/ui/templates/controllers/index.html
@@ -21,6 +21,9 @@
{{ controller['Enabled'] }}
+ {% if controller['Type'] == 'Client-script' %}
+ Code is going to be here
+ {% else %}
{% for reg in controller['Registers'] %}
@@ -30,6 +33,7 @@
{% endfor %}
+ {% endif %}
Manual override
@@ -50,6 +54,7 @@
{% for reg in controller['Registers'] %}
+
{% if reg['CanOverride'] %}
@@ -63,14 +68,17 @@
{% endfor %}
{% endfor %}
-
+
+ {{ reg['Function'] }}
+
{% else %}
-
+
{% endif %}
{% endif %}
+
{% endfor %}
--
2.25.1