fmu-manipulation-toolbox 1.9.2b1__py3-none-any.whl → 1.9.2b3__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.
Files changed (31) hide show
  1. fmu_manipulation_toolbox/__main__.py +2 -2
  2. fmu_manipulation_toolbox/__version__.py +1 -1
  3. fmu_manipulation_toolbox/assembly.py +12 -8
  4. fmu_manipulation_toolbox/checker.py +4 -2
  5. fmu_manipulation_toolbox/cli/datalog2pcap.py +143 -0
  6. fmu_manipulation_toolbox/cli/fmucontainer.py +27 -19
  7. fmu_manipulation_toolbox/cli/fmusplit.py +5 -2
  8. fmu_manipulation_toolbox/cli/fmutool.py +8 -3
  9. fmu_manipulation_toolbox/cli/utils.py +11 -1
  10. fmu_manipulation_toolbox/container.py +33 -6
  11. fmu_manipulation_toolbox/operations.py +4 -3
  12. fmu_manipulation_toolbox/resources/darwin64/container.dylib +0 -0
  13. fmu_manipulation_toolbox/resources/linux32/client_sm.so +0 -0
  14. fmu_manipulation_toolbox/resources/linux64/client_sm.so +0 -0
  15. fmu_manipulation_toolbox/resources/linux64/container.so +0 -0
  16. fmu_manipulation_toolbox/resources/win32/client_sm.dll +0 -0
  17. fmu_manipulation_toolbox/resources/win32/server_sm.exe +0 -0
  18. fmu_manipulation_toolbox/resources/win64/client_sm.dll +0 -0
  19. fmu_manipulation_toolbox/resources/win64/container.dll +0 -0
  20. fmu_manipulation_toolbox/resources/win64/server_sm.exe +0 -0
  21. fmu_manipulation_toolbox/split.py +10 -1
  22. fmu_manipulation_toolbox/version.py +1 -1
  23. fmu_manipulation_toolbox-1.9.2b3.dist-info/METADATA +42 -0
  24. {fmu_manipulation_toolbox-1.9.2b1.dist-info → fmu_manipulation_toolbox-1.9.2b3.dist-info}/RECORD +28 -29
  25. {fmu_manipulation_toolbox-1.9.2b1.dist-info → fmu_manipulation_toolbox-1.9.2b3.dist-info}/entry_points.txt +2 -1
  26. {fmu_manipulation_toolbox-1.9.2b1.dist-info → fmu_manipulation_toolbox-1.9.2b3.dist-info}/licenses/LICENSE.txt +1 -1
  27. fmu_manipulation_toolbox/gui.py +0 -749
  28. fmu_manipulation_toolbox/gui_style.py +0 -252
  29. fmu_manipulation_toolbox-1.9.2b1.dist-info/METADATA +0 -30
  30. {fmu_manipulation_toolbox-1.9.2b1.dist-info → fmu_manipulation_toolbox-1.9.2b3.dist-info}/WHEEL +0 -0
  31. {fmu_manipulation_toolbox-1.9.2b1.dist-info → fmu_manipulation_toolbox-1.9.2b3.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@ import sys
3
3
 
4
4
  def gui():
5
5
  try:
6
- from .gui import main
6
+ from .gui.gui import main
7
7
  main()
8
8
  except ModuleNotFoundError as e:
9
9
  print(f"FATAL ERROR: {e}. No GUI Available.")
@@ -11,7 +11,7 @@ def gui():
11
11
 
12
12
  def cli():
13
13
  from .cli.fmutool import fmutool
14
- fmutool(sys.argv[1:])
14
+ fmutool()
15
15
 
16
16
 
17
17
  def main():
@@ -1 +1 @@
1
- 'V1.9.2b1'
1
+ 'V1.9.2b3'
@@ -7,7 +7,7 @@ import uuid
7
7
  import xml.parsers.expat
8
8
  import zipfile
9
9
 
10
- from .container import FMUContainer, AutoWired
10
+ from .container import FMUContainer
11
11
 
12
12
  logger = logging.getLogger("fmu_manipulation_toolbox")
13
13
 
@@ -103,7 +103,8 @@ class AssemblyNode:
103
103
  def add_start_value(self, fmu_filename: str, port_name: str, value: str):
104
104
  self.start_values[Port(fmu_filename, port_name)] = value
105
105
 
106
- def make_fmu(self, fmu_directory: Path, debug=False, description_pathname=None, fmi_version=2):
106
+ def make_fmu(self, fmu_directory: Path, debug=False, description_pathname=None, fmi_version=2, datalog=False,
107
+ filename=None):
107
108
  for node in self.children.values():
108
109
  node.make_fmu(fmu_directory, debug=debug, fmi_version=fmi_version)
109
110
 
@@ -142,8 +143,11 @@ class AssemblyNode:
142
143
  for link_rule in wired.rule_link:
143
144
  self.add_link(link_rule[0], link_rule[1], link_rule[2], link_rule[3])
144
145
 
145
- container.make_fmu(self.name, self.step_size, mt=self.mt, profiling=self.profiling, sequential=self.sequential,
146
- debug=debug, ts_multiplier=self.ts_multiplier)
146
+ if filename is None:
147
+ filename = self.name
148
+
149
+ container.make_fmu(filename, self.step_size, mt=self.mt, profiling=self.profiling, sequential=self.sequential,
150
+ debug=debug, ts_multiplier=self.ts_multiplier, datalog=datalog)
147
151
 
148
152
  for node in self.children.values():
149
153
  logger.info(f"Deleting transient FMU Container '{node.name}'")
@@ -241,7 +245,7 @@ class Assembly:
241
245
  self.transient_dirnames: Set[Path] = set()
242
246
 
243
247
  if not fmu_directory.is_dir():
244
- raise AssemblyError(f"FMU directory is not valid: '{fmu_directory}'")
248
+ raise AssemblyError(f"FMU directory is not valid: '{fmu_directory.resolve()}'")
245
249
 
246
250
  self.input_pathname = fmu_directory / self.filename
247
251
  self.description_pathname = self.input_pathname # For inclusion in FMU
@@ -284,7 +288,7 @@ class Assembly:
284
288
  elif filename.endswith(".json"):
285
289
  return self.write_json(filename)
286
290
  else:
287
- logger.critical(f"Unable to write to '{filename}': format unsupported.")
291
+ raise AssemblyError(f"Unable to write to '{filename}': format unsupported.")
288
292
 
289
293
  def read_csv(self):
290
294
  name = str(self.filename.with_suffix(".fmu"))
@@ -525,9 +529,9 @@ class Assembly:
525
529
  self.root = sdd.parse(self.description_pathname)
526
530
  self.root.name = str(self.filename.with_suffix(".fmu"))
527
531
 
528
- def make_fmu(self, dump_json=False, fmi_version=2):
532
+ def make_fmu(self, dump_json=False, fmi_version=2, datalog=False, filename=None):
529
533
  self.root.make_fmu(self.fmu_directory, debug=self.debug, description_pathname=self.description_pathname,
530
- fmi_version=fmi_version)
534
+ fmi_version=fmi_version, datalog=datalog, filename=filename)
531
535
  if dump_json:
532
536
  dump_file = Path(self.input_pathname.stem + "-dump").with_suffix(".json")
533
537
  logger.info(f"Dump Json '{dump_file}'")
@@ -27,11 +27,11 @@ class OperationGenericCheck(OperationAbstract):
27
27
  return
28
28
 
29
29
  fmi_name = f"fmi{attrs['fmiVersion'][0]}"
30
-
31
30
  xsd_filename = os.path.join(os.path.dirname(__file__), "resources", "fmi-" + attrs['fmiVersion'],
32
31
  f"{fmi_name}ModelDescription.xsd")
32
+ xsd = xmlschema.XMLSchema(xsd_filename)
33
33
  try:
34
- xmlschema.validate(self.fmu.descriptor_filename, schema=xsd_filename)
34
+ xsd.validate(self.fmu.descriptor_filename)
35
35
  except XMLSchemaValidationError as error:
36
36
  logger.error(error.reason, error.msg)
37
37
  else:
@@ -43,8 +43,10 @@ class OperationGenericCheck(OperationAbstract):
43
43
  else:
44
44
  logger.error(f"This FMU does not validate with FMI standard.")
45
45
 
46
+
46
47
  _checkers_list: List[type[OperationAbstract]] = [OperationGenericCheck]
47
48
 
49
+
48
50
  def get_checkers() -> List[type[OperationAbstract]]:
49
51
  if sys.version_info < (3, 10):
50
52
  from importlib_metadata import entry_points
@@ -0,0 +1,143 @@
1
+ import argparse
2
+ import csv
3
+ import logging
4
+ import sys
5
+
6
+ from pathlib import Path
7
+ from typing import *
8
+
9
+ from .utils import setup_logger, close_logger, make_wide
10
+ from ..version import __version__ as version
11
+
12
+ logger = setup_logger()
13
+
14
+ class DatalogConverter:
15
+ def __init__(self, cvs_filename: Union[Path, str]):
16
+ self.csv_filename = Path(cvs_filename)
17
+ self.pcap_filename = self.csv_filename.with_suffix(".pcap")
18
+
19
+ def open_pcap(self):
20
+ logger.info(f"Creating PCAP file '{self.pcap_filename}'...")
21
+ file = open(self.pcap_filename, "wb")
22
+ file.write(int(0xA1B2C3D4).to_bytes(4, byteorder="big")) # Magic number
23
+ # meaning the timestamp are in min seconds and microseconds
24
+
25
+ file.write(int(2).to_bytes(2, byteorder="big")) # Major Version of fileformat
26
+ file.write(int(4).to_bytes(2, byteorder="big")) # Minor Version
27
+ file.write(int(0).to_bytes(4, byteorder="big")) # Reserved. SHOULD BE 0.
28
+ file.write(int(0).to_bytes(4, byteorder="big")) # Reserved. SHOULD BE 0.
29
+
30
+ file.write(int(0xFFFF).to_bytes(4, byteorder="big")) # snaplen indicating the maximum number of octets
31
+ # captured from each packet.
32
+
33
+ file.write(int(227).to_bytes(4, byteorder="big")) # link type. his field is defined in the Section
34
+ # # 8.1 IANA registry.
35
+ return file
36
+
37
+ def open_csv(self):
38
+ logger.debug(f"Loading '{self.csv_filename}'")
39
+ file = open(self.csv_filename, "rt")
40
+ return file
41
+
42
+ def decode_hexstring(self, hex_string: bytes, time_s, time_us):
43
+ opcode = int.from_bytes(hex_string[0:4], byteorder="little")
44
+ length = int.from_bytes(hex_string[4:8], byteorder="little")
45
+ can_id = int.from_bytes(hex_string[8:12], byteorder="little")
46
+ if opcode == 0x10: # TRANSMIT
47
+ rtr = int.from_bytes(hex_string[13:14], byteorder="little")
48
+ ide = int.from_bytes(hex_string[12:13], byteorder="little")
49
+ data_length = int.from_bytes(hex_string[14:16], byteorder="little")
50
+ raw_data = hex_string[16:]
51
+
52
+ logger.debug(f"time={time_s}.{time_us:06d} OP=0x{opcode:04X} len={length} {data_length} id={can_id}"
53
+ f" ide={ide} rtr={rtr} len={data_length} {raw_data}")
54
+
55
+ # TimeStamp
56
+ self.pcapfile.write(time_s.to_bytes(4, byteorder="big"))
57
+ self.pcapfile.write(time_us.to_bytes(4, byteorder="big"))
58
+
59
+ # Packet length
60
+ packet_length = data_length + 8
61
+ self.pcapfile.write(packet_length.to_bytes(4, byteorder="big"))
62
+ self.pcapfile.write(packet_length.to_bytes(4, byteorder="big"))
63
+
64
+ # Control and flags
65
+ control = (can_id & 0x1FFFFFFF) | ((rtr & 0b1) << 30) | ((ide & 0b1) << 31)
66
+ self.pcapfile.write(control.to_bytes(4, byteorder="big"))
67
+
68
+ # Frame Length
69
+ self.pcapfile.write(data_length.to_bytes(1, byteorder="big"))
70
+
71
+ # Reserved
72
+ self.pcapfile.write(int(0).to_bytes(2, byteorder="big"))
73
+
74
+ # DLC
75
+ dlc = int(data_length) # Classic CAN
76
+ self.pcapfile.write(dlc.to_bytes(1, byteorder="big"))
77
+
78
+ # PAYLOAD
79
+ self.pcapfile.write(raw_data)
80
+
81
+ def convert(self):
82
+ with self.open_csv() as self.csvfile, self.open_pcap() as self.pcapfile:
83
+ csv_reader = csv.DictReader(self.csvfile)
84
+
85
+ data_column_names = [ name for name in csv_reader.fieldnames if "_Data" in name ]
86
+ clock_column_names = {}
87
+
88
+ for column_name in data_column_names:
89
+ splitted_name = column_name.split(".")
90
+ splitted_name[-1] = splitted_name[-1].replace("_Data", "_Clock")
91
+ clock_column_name = ".".join(splitted_name)
92
+
93
+ if clock_column_name in csv_reader.fieldnames:
94
+ logger.debug(f"{column_name} is clocked by {clock_column_name}")
95
+ clock_column_names[column_name] = clock_column_name
96
+
97
+ for row in csv_reader:
98
+ time_s, time_us = divmod(float(row["time"]), 1)
99
+ time_s = int(time_s)
100
+ time_us = int(time_us * 1000000)
101
+
102
+ for names in data_column_names:
103
+ hex_data = row[names]
104
+ if hex_data:
105
+ try:
106
+ clock = row[clock_column_names[names]] == "1"
107
+ except KeyError:
108
+ clock = True
109
+ if clock:
110
+ self.decode_hexstring(bytes.fromhex(hex_data), time_s, time_us)
111
+
112
+
113
+ def datalog2pcap():
114
+ logger.info(f"FMUContainer version {version}")
115
+ logger.warning(f"Datalog2PCAP is still experimental.")
116
+
117
+ parser = argparse.ArgumentParser(prog="datalog2pcap", description="Convert datalog from container to PCAP file.",
118
+ formatter_class=make_wide(argparse.ArgumentDefaultsHelpFormatter),
119
+ add_help=False,
120
+ epilog="see: https://github.com/grouperenault/fmu_manipulation_toolbox/blob/main/"
121
+ "doc/datalog.md")
122
+
123
+ parser.add_argument('-h', '-help', action="help")
124
+
125
+ parser.add_argument("-can", action="store", dest="can_filename", default=None,
126
+ metavar="can-datalog.csv", required=True,
127
+ help="Datalog with CAN data and clocks.")
128
+
129
+ parser.add_argument("-debug", action="store_true", dest="debug",
130
+ help="Add lot of useful log during the process.")
131
+
132
+ config = parser.parse_args(sys.argv[1:])
133
+
134
+ if config.debug:
135
+ logger.setLevel(logging.DEBUG)
136
+
137
+ DatalogConverter(config.can_filename).convert()
138
+
139
+ close_logger(logger)
140
+
141
+
142
+ if __name__ == "__main__":
143
+ datalog2pcap()
@@ -4,7 +4,7 @@ import sys
4
4
 
5
5
  from pathlib import Path
6
6
 
7
- from .utils import setup_logger, make_wide
7
+ from .utils import setup_logger, close_logger, make_wide
8
8
  from ..assembly import Assembly, AssemblyError
9
9
  from ..container import FMUContainerError
10
10
  from ..version import __version__ as version
@@ -22,36 +22,42 @@ def fmucontainer():
22
22
 
23
23
  parser.add_argument('-h', '-help', action="help")
24
24
 
25
- parser.add_argument("-fmu-directory", action="store", dest="fmu_directory", required=False, default=".",
26
- help="Directory containing initial FMU’s and used to generate containers. "
27
- "If not defined, current directory is used.")
28
-
29
- parser.add_argument("-fmi", action="store", dest="fmi_version", required=False, default="2",
30
- help="Define version of FMI to be used for container interface."
31
- "Only '2' or '3' is supported.")
32
-
33
25
  parser.add_argument("-container", action="append", dest="container_descriptions_list", default=[],
34
26
  metavar="filename.{csv|json|ssp},[:step_size]", required=True,
35
27
  help="Description of the container to create.")
36
28
 
29
+ parser.add_argument("-datalog", action="store_true", dest="datalog", default=False,
30
+ help="Log into log file input, output and local variables of the container")
31
+
37
32
  parser.add_argument("-debug", action="store_true", dest="debug",
38
33
  help="Add lot of useful log during the process.")
39
34
 
35
+ parser.add_argument("-dump-json", action="store_true", dest="dump", default=False,
36
+ help="Dump a JSON file for each container.")
37
+
38
+ parser.add_argument("-fmi", action="store", dest="fmi_version", required=False, default="2",
39
+ help="Define version of FMI to be used for container interface."
40
+ "Only '2' or '3' is supported.")
41
+
42
+ parser.add_argument("-fmu-directory", action="store", dest="fmu_directory", required=False, default=".",
43
+ help="Directory containing initial FMU’s and used to generate containers. "
44
+ "If not defined, current directory is used.")
45
+
40
46
  parser.add_argument("-no-auto-input", action="store_false", dest="auto_input", default=True,
41
47
  help="Create ONLY explicit input.")
42
48
 
49
+ parser.add_argument("-no-auto-link", action="store_false", dest="auto_link", default=True,
50
+ help="Create ONLY explicit links.")
51
+
52
+ parser.add_argument("-auto-local", action="store_true", dest="auto_local", default=False,
53
+ help="Expose local variables of the embedded fmu's.")
54
+
43
55
  parser.add_argument("-no-auto-output", action="store_false", dest="auto_output", default=True,
44
56
  help="Create ONLY explicit output.")
45
57
 
46
58
  parser.add_argument("-auto-parameter", action="store_true", dest="auto_parameter", default=False,
47
59
  help="Expose parameters of the embedded fmu's.")
48
60
 
49
- parser.add_argument("-auto-local", action="store_true", dest="auto_local", default=False,
50
- help="Expose local variables of the embedded fmu's.")
51
-
52
- parser.add_argument("-no-auto-link", action="store_false", dest="auto_link", default=True,
53
- help="Create ONLY explicit links.")
54
-
55
61
  parser.add_argument("-mt", action="store_true", dest="mt", default=False,
56
62
  help="Enable Multi-Threaded mode for the generated container.")
57
63
 
@@ -61,9 +67,6 @@ def fmucontainer():
61
67
  parser.add_argument("-sequential", action="store_true", dest="sequential", default=False,
62
68
  help="Use sequential mode to schedule embedded fmu's.")
63
69
 
64
- parser.add_argument("-dump-json", action="store_true", dest="dump", default=False,
65
- help="Dump a JSON file for each container.")
66
-
67
70
  parser.add_argument("-vr", action="store_true", dest="ts_multiplier", default=False,
68
71
  help="Add TS_MULTIPLIER input port to control step_size")
69
72
 
@@ -92,16 +95,21 @@ def fmucontainer():
92
95
  auto_parameter=config.auto_parameter, ts_multiplier=config.ts_multiplier)
93
96
  except FileNotFoundError as e:
94
97
  logger.fatal(f"Cannot read file: {e}")
98
+ close_logger(logger)
95
99
  sys.exit(-1)
96
100
  except (FMUContainerError, AssemblyError) as e:
97
101
  logger.fatal(f"{filename}: {e}")
102
+ close_logger(logger)
98
103
  sys.exit(-2)
99
104
 
100
105
  try:
101
- assembly.make_fmu(dump_json=config.dump, fmi_version=int(config.fmi_version))
106
+ assembly.make_fmu(dump_json=config.dump, fmi_version=int(config.fmi_version), datalog=config.datalog)
102
107
  except FMUContainerError as e:
103
108
  logger.fatal(f"{filename}: {e}")
109
+ close_logger(logger)
104
110
  sys.exit(-3)
105
111
 
112
+ close_logger(logger)
113
+
106
114
  if __name__ == "__main__":
107
115
  fmucontainer()
@@ -2,8 +2,7 @@ import argparse
2
2
  import logging
3
3
  import sys
4
4
 
5
-
6
- from .utils import setup_logger, make_wide
5
+ from .utils import setup_logger, close_logger, make_wide
7
6
  from ..split import FMUSplitter, FMUSplitterError
8
7
  from ..version import __version__ as version
9
8
 
@@ -37,11 +36,15 @@ def fmusplit():
37
36
  splitter.split_fmu()
38
37
  except FMUSplitterError as e:
39
38
  logger.fatal(f"{fmu_filename}: {e}")
39
+ close_logger(logger)
40
40
  sys.exit(-1)
41
41
  except FileNotFoundError as e:
42
42
  logger.fatal(f"Cannot read file: {e}")
43
+ close_logger(logger)
43
44
  sys.exit(-2)
44
45
 
46
+ close_logger(logger)
47
+
45
48
 
46
49
  if __name__ == "__main__":
47
50
  fmusplit()
@@ -1,9 +1,7 @@
1
1
  import argparse
2
2
  import sys
3
3
 
4
- from typing import *
5
-
6
- from .utils import setup_logger, make_wide
4
+ from .utils import setup_logger, close_logger, make_wide
7
5
  from ..operations import (OperationSummary, OperationError, OperationRemoveRegexp,
8
6
  OperationRemoveSources, OperationTrimUntil, OperationKeepOnlyRegexp, OperationMergeTopLevel,
9
7
  OperationStripTopLevel, OperationRenameFromCSV, OperationSaveNamesToCSV, FMU, FMUError)
@@ -78,6 +76,7 @@ def fmutool():
78
76
 
79
77
  if cli_options.fmu_input == cli_options.fmu_output:
80
78
  logger.fatal(f"'-input' and '-output' should point to different files.")
79
+ close_logger(logger)
81
80
  sys.exit(-3)
82
81
 
83
82
  logger.info(f"READING Input='{cli_options.fmu_input}'")
@@ -85,6 +84,7 @@ def fmutool():
85
84
  fmu = FMU(cli_options.fmu_input)
86
85
  except FMUError as reason:
87
86
  logger.fatal(f"{reason}")
87
+ close_logger(logger)
88
88
  sys.exit(-4)
89
89
 
90
90
  if cli_options.apply_on:
@@ -107,6 +107,7 @@ def fmutool():
107
107
  fmu.apply_operation(operation, cli_options.apply_on)
108
108
  except OperationError as reason:
109
109
  logger.fatal(f"{reason}")
110
+ close_logger(logger)
110
111
  sys.exit(-6)
111
112
 
112
113
  if cli_options.extract_description:
@@ -119,9 +120,13 @@ def fmutool():
119
120
  fmu.repack(cli_options.fmu_output)
120
121
  except FMUError as reason:
121
122
  logger.fatal(f"FATAL ERROR: {reason}")
123
+ close_logger(logger)
122
124
  sys.exit(-5)
123
125
  else:
124
126
  logger.info(f"INFO Modified FMU is not saved. If necessary use '-output' option.")
125
127
 
128
+ close_logger(logger)
129
+
130
+
126
131
  if __name__ == "__main__":
127
132
  fmutool()
@@ -2,7 +2,8 @@ import logging
2
2
  import sys
3
3
  from colorama import Fore, Style, init
4
4
 
5
- def setup_logger():
5
+
6
+ def setup_logger() -> logging.Logger:
6
7
  class CustomFormatter(logging.Formatter):
7
8
  def format(self, record):
8
9
  log_format = "%(levelname)-8s | %(message)s"
@@ -24,6 +25,15 @@ def setup_logger():
24
25
 
25
26
  return logger
26
27
 
28
+
29
+ def close_logger(logger: logging.Logger):
30
+ for handler in logger.handlers:
31
+ logger.removeHandler(handler)
32
+ handler.close()
33
+ logger.setLevel(logging.NOTSET)
34
+ logger.propagate = True
35
+
36
+
27
37
  def make_wide(formatter, w=120, h=36):
28
38
  """Return a wider HelpFormatter, if possible."""
29
39
  try:
@@ -484,7 +484,7 @@ class FMUIOList:
484
484
 
485
485
  def add_start_value(self, cport: ContainerPort, value: str):
486
486
  reset = 1 if cport.port.causality == "input" else 0
487
- self.start_values[cport.port.type_name][cport.fmu.name].append((cport, reset, value))
487
+ self.start_values[cport.port.type_name][cport.fmu.name].append((cport.port.vr, reset, value))
488
488
 
489
489
  def write_txt(self, fmu_name, txt_file):
490
490
  for type_name in EmbeddedFMUPort.ALL_TYPES:
@@ -535,7 +535,7 @@ class ClockList:
535
535
 
536
536
  def write_txt(self, txt_file):
537
537
  print(f"# importer CLOCKS: <FMU_INDEX> <NB> <FMU_VR> <VR> [<FMU_VR> <VR>]", file=txt_file)
538
- nb_total_clocks = 0;
538
+ nb_total_clocks = 0
539
539
  for clocks in self.clocks_per_fmu.values():
540
540
  nb_total_clocks += len(clocks)
541
541
 
@@ -756,11 +756,11 @@ class FMUContainer:
756
756
  cport = ContainerPort(self.get_fmu(fmu_filename), port_name)
757
757
 
758
758
  try:
759
- if cport.port.type_name in ('Real', 'Float64', 'Float32'):
759
+ if cport.port.type_name.startswith('real'):
760
760
  value = float(value)
761
- elif cport.port.type_name in ('Integer', 'Int8', 'UInt8', 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64'):
761
+ elif cport.port.type_name.startswith('integer') or cport.port.type_name.startswith('uinteger'):
762
762
  value = int(value)
763
- elif cport.port.type_name == 'Boolean':
763
+ elif cport.port.type_name.startswith('boolean'):
764
764
  value = int(bool(value))
765
765
  elif cport.port.type_name == 'String':
766
766
  value = value
@@ -872,7 +872,7 @@ class FMUContainer:
872
872
  logger.warning(f"{cport} is not connected")
873
873
 
874
874
  def make_fmu(self, fmu_filename: Union[str, Path], step_size: Optional[float] = None, debug=False, mt=False,
875
- profiling=False, sequential=False, ts_multiplier=False):
875
+ profiling=False, sequential=False, ts_multiplier=False, datalog=False):
876
876
  if isinstance(fmu_filename, str):
877
877
  fmu_filename = Path(fmu_filename)
878
878
 
@@ -885,11 +885,16 @@ class FMUContainer:
885
885
 
886
886
  base_directory = self.fmu_directory / fmu_filename.with_suffix('')
887
887
  resources_directory = self.make_fmu_skeleton(base_directory)
888
+
888
889
  with open(base_directory / "modelDescription.xml", "wt") as xml_file:
889
890
  self.make_fmu_xml(xml_file, step_size, profiling, ts_multiplier)
890
891
  with open(resources_directory / "container.txt", "wt") as txt_file:
891
892
  self.make_fmu_txt(txt_file, step_size, mt, profiling, sequential)
892
893
 
894
+ if datalog:
895
+ with open(resources_directory / "datalog.txt", "wt") as datalog_file:
896
+ self.make_datalog(datalog_file)
897
+
893
898
  self.make_fmu_package(base_directory, fmu_filename)
894
899
  if not debug:
895
900
  self.make_fmu_cleanup(base_directory)
@@ -1173,6 +1178,28 @@ class FMUContainer:
1173
1178
  # CLOCKS
1174
1179
  clock_list.write_txt(txt_file)
1175
1180
 
1181
+ def make_datalog(self, datalog_file):
1182
+ print(f"# Datalog filename", file=datalog_file)
1183
+ print(f"{self.identifier}-datalog.csv", file=datalog_file)
1184
+
1185
+ ports = defaultdict(list)
1186
+ for input_port_name, input_port in self.inputs.items():
1187
+ ports[input_port.type_name].append((input_port.vr, input_port_name))
1188
+ for output_port_name, output_port in self.outputs.items():
1189
+ ports[output_port.port.type_name].append((output_port.vr, output_port_name))
1190
+ for link in self.links.values():
1191
+ if link.cport_from is None:
1192
+ # LS-BUS allows to connected to input clocks.
1193
+ ports[link.cport_to_list[0].port.type_name].append((link.vr, link.name))
1194
+ else:
1195
+ ports[link.cport_from.port.type_name].append((link.vr, link.name))
1196
+
1197
+ for type_name in EmbeddedFMUPort.ALL_TYPES:
1198
+ print(f"# {type_name}: <VR> <NAME>" , file=datalog_file)
1199
+ print(f"{len(ports[type_name])}", file=datalog_file)
1200
+ for port in ports[type_name]:
1201
+ print(f"{port[0]} {port[1]}", file=datalog_file)
1202
+
1176
1203
  @staticmethod
1177
1204
  def long_path(path: Union[str, Path]) -> str:
1178
1205
  # https://stackoverflow.com/questions/14075465/copy-a-file-with-a-too-long-path-to-another-directory-in-python
@@ -61,7 +61,7 @@ class FMUPort:
61
61
  self.dimension = None
62
62
 
63
63
  def dict_level(self, nb):
64
- return " ".join([f'{key}="{value}"' for key, value in self.attrs_list[nb].items()])
64
+ return " ".join([f'{key}="{Manipulation.escape(value)}"' for key, value in self.attrs_list[nb].items()])
65
65
 
66
66
  def write_xml(self, fmi_version: int, file):
67
67
  if fmi_version == 2:
@@ -121,7 +121,8 @@ class Manipulation:
121
121
  TAGS_MODEL_STRUCTURE = ("InitialUnknowns", "Derivatives", "Outputs")
122
122
 
123
123
  def __init__(self, operation, fmu):
124
- self.output_filename = tempfile.mktemp()
124
+ (fd, self.output_filename) = tempfile.mkstemp()
125
+ os.close(fd) # File will be re-opened later
125
126
  self.out = None
126
127
  self.operation = operation
127
128
  self.parser = xml.parsers.expat.ParserCreate()
@@ -176,7 +177,7 @@ class Manipulation:
176
177
  self.current_port = FMUPort()
177
178
  self.current_port.push_attrs(attrs)
178
179
  elif self.fmu.fmi_version == 2 and name in self.fmu.FMI2_TYPES:
179
- if self.current_port: # <Enumeration> can be found before port defition. Ignored.
180
+ if self.current_port: # <Enumeration> can be found before port definition. Ignored.
180
181
  self.current_port.fmi_type = name
181
182
  self.current_port.push_attrs(attrs)
182
183
  elif self.fmu.fmi_version == 3 and name in self.fmu.FMI3_TYPES:
@@ -6,6 +6,15 @@ import xml.parsers.expat
6
6
  from typing import *
7
7
  from pathlib import Path
8
8
 
9
+ from .container import EmbeddedFMUPort
10
+ import logging
11
+ import zipfile
12
+ import json
13
+ import xml.parsers.expat
14
+
15
+ from typing import *
16
+ from pathlib import Path
17
+
9
18
  from .container import EmbeddedFMUPort
10
19
 
11
20
  logger = logging.getLogger("fmu_manipulation_toolbox")
@@ -191,7 +200,7 @@ class FMUSplitterDescription:
191
200
  self.config["mt"] = flags[0] == "1"
192
201
  self.config["profiling"] = self.get_line(file) == "1"
193
202
  self.config["sequential"] = False
194
- elif len(flags) == 3:
203
+ elif len(flags) >= 3:
195
204
  self.supported_fmi_types = EmbeddedFMUPort.ALL_TYPES
196
205
  self.config["mt"] = flags[0] == "1"
197
206
  self.config["profiling"] = flags[1] == "1"
@@ -4,6 +4,6 @@ except ModuleNotFoundError:
4
4
  __version__ = "0.0.dev0"
5
5
 
6
6
  __author__ = "Nicolas.LAURENT@Renault.com"
7
- __copyright__ = "Copyright 2023-2024, Renault SAS"
7
+ __copyright__ = "Copyright 2023-2026, Renault SAS"
8
8
  __license__ = """This code is released under the 2-Clause BSD license.
9
9
  See https://github.com/grouperenault/fmu_manipulation_toolbox/blob/main/LICENSE.txt"""