-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathdumpMWB.py
More file actions
1897 lines (1527 loc) · 138 KB
/
dumpMWB.py
File metadata and controls
1897 lines (1527 loc) · 138 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import argparse
import os
import sys
import time
import traceback
import json
# Function for dumping bytearrays to JSON (as strings)
class JSON_ByteArrayEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, bytearray):
return ''.join(f'{x:02X}' for x in obj)
return super().default(obj)
from common_utils import enum_converters, object_printer
from classes.PblRecordManager import PblRecordManager
from classes.StringStorage import StringStorage
from classes.ObjectLoader import ObjectLoader
# PBL is needed for parsing .key files, which will be done by the PblRecordManager class
# The file "pbl.dll" should be in the working directory, in the "bin" folder
pbl_dll_path = os.path.join(os.path.abspath(os.getcwd()), 'bin/pbl.dll')
pbl_record_manager = PblRecordManager(pbl_dll_path)
# Get a list of "layer data" objects necessary for resolving DOP references with the UDS protocol, in order of relevance
### object_loader = instance of ObjectLoader, with StringStorage instance loaded from the target project
### project_folder_path = project path (folder with .db and .key files)
def get_protocol_layer_data_list(object_loader, project_folder_path):
# Load the layer data for the UDS protocol, which is always 'UDSOnCAN', with its associated PoolID
uds_protocol_layer_data = object_loader.load_object_by_id(project_folder_path, '0.0.0@PR_UDSOnCAN.pr', '#RtGen_DB_LAYER_DATA')
# In some rare cases, even the definition for standard OBD protocol will be needed, if the reference isn't found in UDS
obd_protocol_layer_data = object_loader.load_object_by_id(project_folder_path, '0.0.0@PR_OBDOnCAN.pr', '#RtGen_DB_LAYER_DATA')
# The UDS protocol's layer data may reference "parent layers" which will have layer data objects too
protocol_layer_data_list = []
for protocol_parent_layer_PoolID in uds_protocol_layer_data['parent_layers_vector']:
protocol_layer_data_list.append(object_loader.load_object_by_id(project_folder_path, protocol_parent_layer_PoolID, '#RtGen_DB_LAYER_DATA'))
# Add the main protocols layer data at the end of the list, since they will be parsed in this order and these are the least important layers
protocol_layer_data_list.append(uds_protocol_layer_data)
protocol_layer_data_list.append(obd_protocol_layer_data)
return protocol_layer_data_list
# Convert a list of coefficients to a polynomial
### coefficients = list of coefficients for each term
def polynomial_to_string(coefficients):
# This list will contain all terms of the polynomial, as formatted strings
terms = []
# The coefficients are given in ascending order of power (0 to n)
for power, coefficient in enumerate(coefficients):
# Don't write terms with the coefficient 0
if coefficient == 0:
continue
# The first term has no "x"
if power == 0:
term = '{}'.format(coefficient)
# The second term has no "power"
elif power == 1:
term = '{} * x'.format(coefficient)
# All other terms have a coefficient and power
else:
term = '{} * x**{}'.format(coefficient, power)
# Add the term to the list
terms.append(term)
# Construct the polynomial as all terms added together
return ' + '.join(terms) or '0'
# Parse an object, returning a dictionary with its important fields
### object_loader = instance of ObjectLoader, with StringStorage instance loaded from the target project
### layer_data_objects = list or "layer data" objects, used for solving references which don't specify the PoolID (file name)
### project_folder_path = project path (folder with .db and .key files)
### dop = object to parse
def parse_dop(object_loader, layer_data_objects, project_folder_path, dop):
# Create the dictionary which will be filled with the object's parsed fields
output_object = {}
# Parse the object, depending on its type
match dop['#OBJECT_TYPE']:
# Parameter: MCD-2D V2.2 - 7.3.5.4
case 'MCD_DB_PARAMETER' | 'MCD_DB_PARAMETER_SIMPLE':
output_object['type'] = 'PARAMETER'
# Get the LONG-NAME, LONG-NAME-ID and DESCRIPTION of the parameter
output_object['long_name'] = dop['long_name']
output_object['long_name_id'] = dop['long_name_id']
output_object['description'] = dop['description']
# BYTE-POSITION and BIT-POSITION specify the start position of the parameter in the PDU, where the counting starts with 0
output_object['byte_position'] = dop['byte_position'] if dop['is_byte_pos_available'] else None
output_object['bit_position'] = dop['bit_position']
# BIT-POSITION shall not have a value greater than 7
if output_object['bit_position'] not in range(0, 8):
raise RuntimeError('BIT-POSITION {} not between [0, 7]'.format(output_object['bit_position']))
# Parse the PARAM, depending on its type
match dop['mcd_parameter_type']:
# VALUE parameter: MCD-2D V2.2 - page 78, paragraph 1
case 'eVALUE':
# A VALUE parameter is a frequently used parameter type
# It references a DOP, which converts a value from coded into physical representation (and vice versa)
output_object['parameter_type'] = 'VALUE'
# In a response message, PHYSICAL-DEFAULT-VALUE is used for verification of the received value
# In this case, the received coded value shall be converted into the physical representation
# After that, the specified and the received values can be compared
output_object['default_value'] = None
if dop['default_mcd_value'] is not None:
# Ensure the default value's data type is valid
if not dop['default_mcd_value']['data_type'].startswith('eA_'):
raise RuntimeError('Strange PHYSICAL-DEFAULT-VALUE type: {}'.format(dop['default_mcd_value']['data_type']))
# Store the default value
output_object['default_value'] = {}
output_object['default_value']['data_type'] = dop['default_mcd_value']['data_type'][1:]
output_object['default_value']['value'] = dop['default_mcd_value']['value']
# The referenced DOP might be missing the PoolID
# The following function will try to find it using the DOP reference maps found in the database's LayerData
try:
parameter_dop = object_loader.load_DOP_by_reference_without_PoolID(project_folder_path, layer_data_objects, dop['db_object_ref'])
# If not found (error in project), mimic the MCD error that is thrown on projects with this sort of problem
except:
output_object['dop'] = {'#error': 'Access to database element failed - DOP with name: {}'.format(dop['db_object_ref']['object_id'])}
# The DOP is usually found
else:
# Add the referenced DOP to the object
output_object['dop'] = parse_dop(object_loader, layer_data_objects, project_folder_path, parameter_dop)
# RESERVED parameter: MCD-2D V2.2 - page 78, paragraph 2
case 'eRESERVED':
# A parameter of type RESERVED is used when the parameter should be ignored by the D-server
# Such parameters are not interpreted and are not shown on the user's display
output_object['parameter_type'] = 'RESERVED'
# RESERVED parameters must have a default value
if dop['default_mcd_value'] is None:
raise RuntimeError('RESERVED has no default')
# Ensure the default value's data type is valid
if not dop['default_mcd_value']['data_type'].startswith('eA_'):
raise RuntimeError('Strange DEFAULT-VALUE type: {}'.format(dop['default_mcd_value']['data_type']))
# Store the default value
output_object['default_value'] = {}
output_object['default_value']['data_type'] = dop['default_mcd_value']['data_type'][1:]
output_object['default_value']['value'] = dop['default_mcd_value']['value']
# A parameter of type RESERVED shouldn't reference any DOP
if dop['db_object_ref'] is not None:
raise RuntimeError('CODED-CONST references DOP')
# Instead, the DIAG-CODED-TYPE is included in the RESERVED PARAM's definition
# "Pretend" the current object is of type 'DOP', in order to add it as a field like with all other PARAMs
dummy_dop = dop
dummy_dop['#OBJECT_TYPE'] = 'DB_DOP_SIMPLE_BASE'
dummy_dop['is_reserved'] = True # marker so that parse_dop() allows coded type A_BITFIELD
output_object['dop'] = parse_dop(object_loader, layer_data_objects, project_folder_path, dummy_dop)
# CODED-CONST parameter: MCD-2D V2.2 - page 79, paragraph 1
case 'eCODED_CONST':
# CODED-CONST parameters can be used in a response for verification of the received value without converting it into the physical representation
output_object['parameter_type'] = 'CODED-CONST'
# CODED-CONST parameters must have a default value
if dop['default_mcd_value'] is None:
raise RuntimeError('CODED-CONST has no default')
# Ensure the default value's data type is valid
if not dop['default_mcd_value']['data_type'].startswith('eA_'):
raise RuntimeError('Strange CODED-DEFAULT-VALUE type: {}'.format(dop['default_mcd_value']['data_type']))
# Store the constant value
output_object['constant'] = {}
output_object['constant']['data_type'] = dop['default_mcd_value']['data_type'][1:]
output_object['constant']['value'] = dop['default_mcd_value']['value']
# A parameter of type CODED-CONST shouldn't reference any DOP
if dop['db_object_ref'] is not None:
raise RuntimeError('CODED-CONST references DOP')
# Instead, the DIAG-CODED-TYPE is included in the CODED-CONST PARAM's definition
# "Pretend" the current object is of type 'DOP', in order to add it as a field like with all other PARAMs
dummy_dop = dop
dummy_dop['#OBJECT_TYPE'] = 'DB_DOP_SIMPLE_BASE'
output_object['dop'] = parse_dop(object_loader, layer_data_objects, project_folder_path, dummy_dop)
# PHYS-CONST parameter: MCD-2D V2.2 - page 79, paragraph 2
case 'ePHYS_CONST':
# PHYS-CONST is an alternative for CODED-CONST with the difference that the value is given in the physical representation
output_object['parameter_type'] = 'PHYS-CONST'
# In a response, the received coded value shall be converted into the physical representation
# After that, the specified and the calculated values can be compared
# PHYS-CONST parameters must have a default value
if dop['default_mcd_value'] is None:
raise RuntimeError('PHYS-CONST has no default')
# Ensure the default value's data type is valid
if not dop['default_mcd_value']['data_type'].startswith('eA_'):
raise RuntimeError('Strange PHYSICAL-DEFAULT-VALUE type: {}'.format(dop['default_mcd_value']['data_type']))
# Store the constant value
output_object['constant'] = {}
output_object['constant']['data_type'] = dop['default_mcd_value']['data_type'][1:]
output_object['constant']['value'] = dop['default_mcd_value']['value']
# The referenced DOP might be missing the PoolID
# The following function will try to find it using the DOP reference maps found in the database's LayerData
parameter_dop = object_loader.load_DOP_by_reference_without_PoolID(project_folder_path, layer_data_objects, dop['db_object_ref'])
# Add the referenced DOP to the object
output_object['dop'] = parse_dop(object_loader, layer_data_objects, project_folder_path, parameter_dop)
# SYSTEM parameter: MCD-2D V2.2 - page 80, paragraph 1
case 'eSYSTEM':
# The parameter of type `SYSTEM` is used to cause the D-server to calculate a value that depends on the run time information
output_object['parameter_type'] = 'SYSTEM'
# The member `SYSPARAM` defines this value type
output_object['sysparam'] = dop['sys_param']
output_object['default_value'] = None
if dop['default_mcd_value'] is not None:
raise RuntimeError('SYSTEM parameter has default')
# The value received by the D-server is a physical value and shall be coded using the referenced DOP
parameter_dop = object_loader.load_DOP_by_reference_without_PoolID(project_folder_path, layer_data_objects, dop['db_object_ref'])
# Add the referenced DOP to the object
output_object['dop'] = parse_dop(object_loader, layer_data_objects, project_folder_path, parameter_dop)
# For unknown types, display the object and raise an exception
case _:
object_printer.print_indented(0, '')
object_printer.print_object(dop, 'dop', 0)
object_printer.print_indented(0, '')
raise RuntimeError('Unknown MCD type: {}'.format(dop['mcd_parameter_type']))
# Data object property: MCD-2D V2.2 - 7.3.6.2
case 'DB_DOP_SIMPLE_BASE':
output_object['type'] = 'DOP'
# The class `DATA-OBJECT-PROP` describes how to extract and decode a single data item from the byte stream of the response message,
# and how to transform the internal value into its physical representation using a computational method (COMPU-METHOD)
# A DATA-OBJECT-PROP consists of a COMPU-METHOD, a DIAG-CODED-TYPE and a PHYSICAL-TYPE
# Optionally, an INTERNAL-CONSTR and a PHYS-CONSTR can be given for a DATA-OBJECT-PROP, and a UNIT may be referenced
# DIAG-CODED-TYPE is responsible for extraction of the coded value out of the byte stream
diag_coded_type = dop['diag_coded_type']
# Some sanity checks...
if diag_coded_type['is_condensed']:
raise RuntimeError('Parameter is CONDENSED')
if not diag_coded_type['base_data_type_as_mcd_data_type'].startswith('eA_'):
raise RuntimeError('Strange BASE-DATA-TYPE: {}'.format(diag_coded_type['base_data_type_as_mcd_data_type']))
# The coded value type inside the message is described by the member BASE-DATA-TYPE of the DIAG-CODED-TYPE
# Get the coded value's BASE-DATA-TYPE (without the 'e' enum prefix)
output_object['coded_base_data_type'] = diag_coded_type['base_data_type_as_mcd_data_type'][1:]
# BITFIELDs aren't even a part of ODX from what I can see
# I have only encountered them as the coded type for RESERVED parameters
if output_object['coded_base_data_type'] == 'A_BITFIELD' and 'is_reserved' not in dop:
raise RuntimeError('Encountered A_BITFIELD as coded type')
# Handle the DIAG-CODED-TYPE based on its type
match diag_coded_type['type']:
# The length of the parameter is to be extracted from the PDU
case 'eLEADING_LENGTH_INFO_TYPE':
output_object['diag_coded_type'] = 'LEADING-LENGTH-INFO-TYPE'
# BIT-LENGTH specifies the bit count at the beginning of this parameter and shall be greater than 0
output_object['bit_length'] = diag_coded_type['bit_length']
if output_object['bit_length'] <= 0:
raise RuntimeError('Invalid BIT-LENGTH: {}'.format(output_object['bit_length']))
# These bits contain the length of the parameter, in bytes
# The bits of length information are not part of the coded value
# That means the data extraction of the coded value starts at the byte edge to this length information
# The minimum and the maximum length of a parameter in a message are specified by MIN-LENGTH and MAX-LENGTH
case 'eMIN_MAX_LENGTH_TYPE':
output_object['diag_coded_type'] = 'MIN-MAX-LENGTH-TYPE'
# Get the MIN-LENGTH and MAX-LENGTH
output_object['min_length'] = diag_coded_type['min_length']
output_object['max_length'] = diag_coded_type['max_length']
# TERMINATION specifies a possible premature end of a parameter in the message,
# that is, the end of the parameteris reached before MAX-LENGTH bytes have been read
match diag_coded_type['termination']:
# The byte stream to be extracted is terminated by the first of the following conditions:
# - finding termination character 0x00 (A_ASCIISTRING and A_UTF8STRING*) or 0x0000 (A_UNICODE2STRING) after MIN-LENGTH bytes
# - reaching MAX-LENGTH bytes
# - reaching end of PDU
case 'eZERO':
output_object['termination'] = 'ZERO'
# The byte stream to be extracted is terminated by the first of the following conditions:
# - finding termination character 0xFF (A_ASCIISTRING and A_UTF8STRING) or 0xFFFF (A_UNICODE2STRING) after MIN-LENGTH bytes
# - reaching MAX-LENGTH bytes
# - reaching end of PDU
case 'eHEX_FF':
output_object['termination'] = 'HEX-FF'
# The byte stream to be extracted is terminated by the first of the following conditions:
# - reaching MAX-LENGTH bytes
# - reaching end of PDU
case 'eENDOFPDU':
output_object['termination'] = 'END-OF-PDU'
case _:
raise RuntimeError('Unknown TERMINATION for MIN-MAX-LENGTH-TYPE: {}'.format(diag_coded_type['termination']))
# In the case of A_UNICODE2STRING, the length of the actual payload and the values of MIN-LENGTH and MAX-LENGTH shall be even
if output_object['coded_base_data_type'] == 'A_UNICODE2STRING' and (output_object['min_length'] % 2 != 0 or output_object['max_length'] % 2 != 0):
raise RuntimeError('MIN-LENGTH and MAX-LENGTH for coded BASE-DATA-TYPE A_UNICODE2STRING should be even, not {}, {}'.format(output_object['min_length'], output_object['max_length']))
# The parameter always occupies the specified length of BIT-LENGTH bits inside the message
case 'eSTANDARD_LENGTH_TYPE':
output_object['diag_coded_type'] = 'STANDARD-LENGTH-TYPE'
# Get the BIT-LENGTH
output_object['bit_length'] = diag_coded_type['bit_length']
# The value of BIT-LENGTH shall be greater than 0
if output_object['bit_length'] <= 0:
raise RuntimeError('Invalid BIT-LENGTH: {}'.format(output_object['bit_length']))
# Optionally, an attribute BIT-MASK can be specified, if some bits inside the response message should be masked out
# Get the BIT-MASK and save it as None if it's empty
output_object['bit_mask'] = diag_coded_type['bit_mask'] if diag_coded_type['bit_mask'] != bytearray() else None
# The CONDENSED attribute, if True, will raise an exception above, it's not important yet, has not been encountered
# I have not encountered other types yet
case _:
raise RuntimeError('Unknown DIAG-CODED-TYPE type: {}'.format(diag_coded_type['type']))
# MIN-MAX-LENGTH-TYPE is only allowed for the internal data types A_BYTEFIELD, A_ASCIISTRING, A_UNICODE2STRING and A_UTF8STRING
if diag_coded_type['type'] == 'eMIN_MAX_LENGTH_TYPE':
if output_object['coded_base_data_type'] not in ['A_BYTEFIELD', 'A_ASCIISTRING', 'A_UNICODE2STRING', 'A_UTF8STRING']:
raise RuntimeError('MIN-MAX-LENGTH-TYPE not allowed for BASE-DATA-TYPE {}'.format(output_object['coded_base_data_type']))
# Regardless of the type of length specification, the following additional restrictions have to be considered depending on the BASE-DATA-TYPE
else:
if output_object['coded_base_data_type'] == 'A_FLOAT32' and output_object['bit_length'] != 32:
raise RuntimeError('BIT-LENGTH for A_FLOAT32 must be 32, not {}'.format(output_object['bit_length']))
if output_object['coded_base_data_type'] == 'A_FLOAT64' and output_object['bit_length'] != 64:
raise RuntimeError('BIT-LENGTH for A_FLOAT64 must be 64, not {}'.format(output_object['bit_length']))
if output_object['coded_base_data_type'] in ['A_ASCIISTRING', 'A_UTF8STRING'] and output_object['bit_length'] % 8 != 0:
# Note: seems like MCD Kernel allows A_BYTEFIELD not to cover whole bytes (and decodes it correctly), contrary to ODX spec
raise RuntimeError('BIT-LENGTH for {} must be multiple of 8, not {}'.format(output_object['coded_base_data_type'], output_object['bit_length']))
if output_object['coded_base_data_type'] == 'A_UNICODE2STRING' and output_object['bit_length'] % 16 != 0:
raise RuntimeError('BIT-LENGTH for A_UNICODE2STRING must be multiple of 16, not {}'.format(output_object['bit_length']))
# There are some very rare parameters that are defined as A_UINT32/A_INT32 but have a BIT-LENGTH way bigger than 32
# I guess they were supposed to be arrays, but...?
# Not even the MCD Kernel can decode them, it just errors out with "Invalid byte length: ..."
# An exception won't be raised for those, they will be dumped with their original BIT-LENGTH
if output_object['coded_base_data_type'] in ['A_UINT32', 'A_INT32'] and output_object['bit_length'] not in range(1, 32 +1):
output_object['#error'] = f'BIT-LENGTH for {output_object['coded_base_data_type']} must be between 1 and 32, not {output_object['bit_length']}'
# The member ENCODING of the DIAG-CODED-TYPE gives the method used for encoding of the value in the PDU
# Remove the 'e' prefix and replace underscores by dashes, to convert the enum value to a standard ODX name
output_object['encoding'] = diag_coded_type['encoding'][1:].replace('_', '-')
# The ODX spec seems a bit vague, but my understanding is that only non-float numerical types can have BIT-MASKs
# Well, I have encountered them for A_BYTEFIELD too, but it's just 1s for the entire BIT-LENGTH, so it doesn't really mask anything out
if 'bit_mask' in output_object and output_object['bit_mask'] is not None:
if output_object['coded_base_data_type'] not in ['A_UINT32', 'A_INT32', 'A_BYTEFIELD']:
raise RuntimeError(f'Unexpected: type {output_object['coded_base_data_type']} (encoding {output_object['encoding']}) has BIT-MASK ({output_object['bit_mask']})')
# Only for the BCD-UP encoding for A_BYTEFIELD, the BIT-MASK isn't always all 1s
expected_bit_mask = bytearray(int('1' * output_object['bit_length'], 2).to_bytes((output_object['bit_length'] + 7) // 8))
if output_object['coded_base_data_type'] == 'A_BYTEFIELD' and output_object['encoding'] != 'BCD-UP' and output_object['bit_mask'] != expected_bit_mask:
raise RuntimeError(f'Unexpected BIT-MASK for A_BYTEFIELD (encoding {output_object['encoding']}): {output_object['bit_mask']} (expected {expected_bit_mask})')
# Fix float parameters that have the wrong encoding
if output_object['encoding'] == 'NONE' and output_object['coded_base_data_type'] in ['A_FLOAT32', 'A_FLOAT64']:
output_object['encoding'] = 'IEEE754'
# The loader has already checked that the ENCODING attribute is valid for the BASE-DATA-TYPE, # but it is checked here again against the documentation
if ((output_object['encoding'] == 'BCD-UP' and output_object['coded_base_data_type'] not in ['A_UINT32', 'A_BYTEFIELD' ]) or
(output_object['encoding'] == 'BCD-P' and output_object['coded_base_data_type'] not in ['A_UINT32', 'A_BYTEFIELD' ]) or
(output_object['encoding'] == '2C' and output_object['coded_base_data_type'] not in ['A_INT32' ]) or
(output_object['encoding'] == '1C' and output_object['coded_base_data_type'] not in ['A_INT32' ]) or
(output_object['encoding'] == 'SM' and output_object['coded_base_data_type'] not in ['A_INT32' ]) or
(output_object['encoding'] == 'IEEE754' and output_object['coded_base_data_type'] not in ['A_FLOAT32', 'A_FLOAT64' ]) or
(output_object['encoding'] == 'ISO-8859-1' and output_object['coded_base_data_type'] not in ['A_ASCIISTRING' ]) or
(output_object['encoding'] == 'ISO-8859-2' and output_object['coded_base_data_type'] not in ['A_ASCIISTRING' ]) or
(output_object['encoding'] == 'UCS-2' and output_object['coded_base_data_type'] not in ['A_UNICODE2STRING' ]) or
(output_object['encoding'] == 'UTF-8' and output_object['coded_base_data_type'] not in ['A_UTF8STRING' ]) or
(output_object['encoding'] == 'NONE' and output_object['coded_base_data_type'] not in ['A_UINT32', 'A_BYTEFIELD', 'A_BITFIELD'])):
raise RuntimeError('Invalid ENCODING for {}: {}'.format(output_object['coded_base_data_type'], output_object['encoding']))
if output_object['encoding'] not in ['BCD-UP', 'BCD-P', '2C', '1C', 'SM', 'IEEE754', 'ISO-8859-1', 'ISO-8859-2', 'UCS-2', 'UTF-8', 'NONE']:
raise RuntimeError('Unknown ENCODING (for {}): {}'.format(output_object['coded_base_data_type'], output_object['encoding']))
# Store the IS-HIGHLOW-BYTE-ORDER attribute as 'big'/'little' endianness
output_object['endianness'] = 'big' if diag_coded_type['is_high_low_byte_order'] else 'little'
# PHYSICAL-TYPE specifies the physical representation of the value
if 'physical_type' in dop:
physical_type = dop['physical_type']
# Some sanity checks...
if not physical_type['base_data_type_as_mcd_data_type'].startswith('eA_'):
raise RuntimeError('Strange PHYSICAL-TYPE BASE-DATA-TYPE: {}'.format(physical_type['base_data_type_as_mcd_data_type']))
# The member BASE-DATA-TYPE of PHYSICAL-TYPE gives the type and encoding of the physical value
output_object['physical_base_data_type'] = physical_type['base_data_type_as_mcd_data_type'][1:]
# Hopefully BITFIELD is never a physical type
if output_object['physical_base_data_type'] == 'A_BITFIELD':
raise RuntimeError('Encountered A_BITFIELD as physical type')
# Get the DISPLAY-RADIX (numeric representation: 2 = BIN, 8 = OCT, 10 = DEC, 16 = HEX)
output_object['display_radix'] = physical_type['display_radix']
if output_object['display_radix'] not in [2, 8, 10, 16]:
raise RuntimeError('Invalid DISPLAY-RADIX: {}'.format(output_object['display_radix']))
# The DISPLAY-RADIX is only applicable to the physical data type A_UINT32
if output_object['physical_base_data_type'] != 'A_UINT32':
output_object['display_radix'] = None
# There is also a possibility to specify the number of digits to be displayed after the decimal point by the member PRECISION
output_object['precision'] = physical_type['precision'] if physical_type['is_precision_available'] else None
# PRECISION can be given for A_FLOAT32 and A_FLOAT64 physical data types
if output_object['precision'] is not None and output_object['physical_base_data_type'] not in ['A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('PRECISION available for non-FLOAT PHYSICAL-TYPE {}: {}'.format(output_object['physical_base_data_type'], output_object['precision']))
# The calculated physical value can be directly displayed on the tester
# Usually, units are used to augment this displayed value with additional information
# The declaration of the unit to be used is given via the reference UNIT-REF
if 'units_ref' in dop:
output_object['units'] = None
if dop['units_ref'] is not None:
# Get the UNIT
units_dop = object_loader.load_object_by_reference(project_folder_path, dop['units_ref']) # MCD_DB_UNIT
# Store the UNIT's fields
output_object['units'] = {}
output_object['units']['long_name'] = units_dop['long_name']
output_object['units']['long_name_id'] = units_dop['long_name_id']
output_object['units']['description'] = units_dop['description']
output_object['units']['display_name'] = units_dop['display_name']
output_object['units']['factor_si_to_unit'] = units_dop['factor_si_to_unit']
output_object['units']['offset_si_to_unit'] = units_dop['offset_si_to_unit']
# Store the PHYSICAL-DIMENSION's fields, if specified
output_object['units']['si_unit'] = None
if units_dop['physical_dimension'] is not None:
output_object['units']['si_unit'] = {}
output_object['units']['si_unit']['length_exponent'] = units_dop['physical_dimension']['length_exponent']
output_object['units']['si_unit']['mass_exponent'] = units_dop['physical_dimension']['mass_exponent']
output_object['units']['si_unit']['time_exponent'] = units_dop['physical_dimension']['time_exponent']
output_object['units']['si_unit']['current_exponent'] = units_dop['physical_dimension']['current_exponent']
output_object['units']['si_unit']['temperature_exponent'] = units_dop['physical_dimension']['temperature_exponent']
output_object['units']['si_unit']['molar_amount_exponent'] = units_dop['physical_dimension']['molar_amount_exponent']
output_object['units']['si_unit']['luminous_intensity_exponent'] = units_dop['physical_dimension']['luminous_intensity_exponent']
# The validity of the internal value can be restricted to a given interval via the INTERNAL-CONSTR and its members LOWER-LIMIT and UPPER-LIMIT
if 'internal_constraint_ref' in dop:
output_object['internal_constraint'] = None
if dop['internal_constraint_ref'] is not None:
# Get the INTERNAL-CONSTR
internal_constraint = object_loader.load_object_by_reference(project_folder_path, dop['internal_constraint_ref']) # MCD_CONSTRAINT
# Sanity check...
if internal_constraint['is_computed']:
raise RuntimeError('IC should not be computed')
# The LIMITs' data types should match the coded data type
if internal_constraint['interval']['lower_limit']['data_type'] != 'e'+output_object['coded_base_data_type'] or internal_constraint['interval']['lower_limit']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('IC limit data types do not match coded type: {}, {} vs {}'.format(internal_constraint['interval']['lower_limit']['data_type'], internal_constraint['interval']['upper_limit']['data_type'], output_object['coded_base_data_type']))
# Get the LIMITs
output_object['internal_constraint'] = {'lower_limit': {}, 'upper_limit': {}}
# LOWER-LIMIT
output_object['internal_constraint']['lower_limit']['type'] = internal_constraint['interval']['lower_limit_type'].replace('eLIMIT_', '')
output_object['internal_constraint']['lower_limit']['value'] = None if output_object['internal_constraint']['lower_limit']['type'] == 'INFINITE' else internal_constraint['interval']['lower_limit']['value']
# UPPER-LIMIT
output_object['internal_constraint']['upper_limit']['type'] = internal_constraint['interval']['upper_limit_type'].replace('eLIMIT_', '')
output_object['internal_constraint']['upper_limit']['value'] = None if output_object['internal_constraint']['upper_limit']['type'] == 'INFINITE' else internal_constraint['interval']['upper_limit']['value']
# Additionally, SCALE-CONSTRS can be used to define valid, non-valid, non-defined or non-available sub-intervals within the interval spanned by INTERNAL-CONSTR
# The limits of the sub-intervals are defined with LOWER-LIMIT and UPPER-LIMIT in the same way as in INTERNAL-CONSTR
# Sanity check...
if internal_constraint['scale_constraints'] is None:
raise RuntimeError('IC has no SCALE-CONSTRS')
# Go through all SCALE-CONSTRs inside SCALE-CONSTRS
output_object['internal_constraint']['scale_constraints'] = []
for scale_constraint in internal_constraint['scale_constraints']:
# The LIMITs' data types should match the coded data type
if scale_constraint['interval']['lower_limit']['data_type'] != 'e'+output_object['coded_base_data_type'] or scale_constraint['interval']['lower_limit']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('IC SCALE-CONSTR limit data types do not match coded type: {}, {} vs {}'.format(scale_constraint['interval']['lower_limit']['data_type'], scale_constraint['interval']['upper_limit']['data_type'], output_object['coded_base_data_type']))
# Get the LIMITs
scale_constraint_output_object = {'lower_limit': {}, 'upper_limit': {}}
# LOWER-LIMIT
scale_constraint_output_object['lower_limit']['type'] = scale_constraint['interval']['lower_limit_type'].replace('eLIMIT_', '')
scale_constraint_output_object['lower_limit']['value'] = None if scale_constraint_output_object['lower_limit']['type'] == 'INFINITE' else scale_constraint['interval']['lower_limit']['value']
# UPPER-LIMIT
scale_constraint_output_object['upper_limit']['type'] = scale_constraint['interval']['upper_limit_type'].replace('eLIMIT_', '')
scale_constraint_output_object['upper_limit']['value'] = None if scale_constraint_output_object['upper_limit']['type'] == 'INFINITE' else scale_constraint['interval']['upper_limit']['value']
# The attribute VALIDITY of each SCALE-CONSTR can take the values VALID, NOT-VALID, NOT-DEFINED or NOT-AVAILABLE
scale_constraint_output_object['validity'] = scale_constraint['range_info'][7:].replace('_', '-')
# Optionally, a SHORT-LABEL can be used to add an identifier to a SCALE-CONSTR
scale_constraint_output_object['short_label'] = scale_constraint['short_label']
scale_constraint_output_object['description'] = scale_constraint['description']
# Add the SCALE-CONSTR to the list
output_object['internal_constraint']['scale_constraints'].append(scale_constraint_output_object)
# The implicit valid range of a data type is defined by BASE-DATA-TYPE, restricted by ENCODING and size of parameter which is defined by the BIT-LENGTH
# The explicit valid range is defined by UPPER-LIMIT and LOWER-LIMIT, minus all SCALE-CONSTRs with VALIDITY != VALID
# If UPPER-LIMIT or LOWER-LIMIT is missing, the explicit valid range is not restricted in that direction
# The valid range is defined by the intersection of the implicit and the explicit valid range
# If the DATA-OBJECT-PROP is used for calculation of physical value from the internal one, the internal value is matched against the valid range directly
# In a similar way, it is possible to specify constraints for the physical value inside PHYS-CONSTR, if the physical type of the DATA-OBJECT-PROP is a numerical type
if 'physical_constraint_ref' in dop:
output_object['physical_constraint'] = None
if dop['physical_constraint_ref'] is not None:
if output_object['physical_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64', 'A_BYTEFIELD']:
raise RuntimeError('PC only allowed for numerical physical types, not {}'.format(output_object['physical_base_data_type']))
# Get the PHYS-CONSTR
physical_constraint = object_loader.load_object_by_reference(project_folder_path, dop['physical_constraint_ref']) # MCD_CONSTRAINT
# Sanity check...
if not physical_constraint['is_computed']:
raise RuntimeError('PC should be computed')
# If UPPER-LIMIT or LOWER-LIMIT is missing, the explicit valid range is not restricted in that direction
# Get the LIMITs
output_object['physical_constraint'] = {'lower_limit': {}, 'upper_limit': {}}
# LOWER-LIMIT
if physical_constraint['interval']['lower_limit'] is None:
output_object['physical_constraint']['lower_limit']['type'] = 'INFINITE'
output_object['physical_constraint']['lower_limit']['value'] = None
else:
# The LOWER-LIMIT's data type should match the physical data type
if physical_constraint['interval']['lower_limit']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('PC LOWER-LIMIT data type does not match physical type: {} vs {}'.format(physical_constraint['interval']['lower_limit']['data_type'], output_object['physical_base_data_type']))
output_object['physical_constraint']['lower_limit']['type'] = physical_constraint['interval']['lower_limit_type'].replace('eLIMIT_', '')
output_object['physical_constraint']['lower_limit']['value'] = None if output_object['physical_constraint']['lower_limit']['type'] == 'INFINITE' else physical_constraint['interval']['lower_limit']['value']
# UPPER-LIMIT
if physical_constraint['interval']['upper_limit'] is None:
output_object['physical_constraint']['upper_limit']['type'] = 'INFINITE'
output_object['physical_constraint']['upper_limit']['value'] = None
else:
# The UPPER-LIMIT's data type should match the physical data type
if physical_constraint['interval']['upper_limit']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('PC UPPER-LIMIT data type does not match physical type: {} vs {}'.format(physical_constraint['interval']['upper_limit']['data_type'], output_object['physical_base_data_type']))
output_object['physical_constraint']['upper_limit']['type'] = physical_constraint['interval']['upper_limit_type'].replace('eLIMIT_', '')
output_object['physical_constraint']['upper_limit']['value'] = None if output_object['physical_constraint']['upper_limit']['type'] == 'INFINITE' else physical_constraint['interval']['upper_limit']['value']
# SCALE-CONSTRS can be defined for PHYS-CONSTRs too
output_object['physical_constraint']['scale_constraints'] = []
for scale_constraint in physical_constraint['scale_constraints']:
# Get the LIMITs
scale_constraint_output_object = {'lower_limit': {}, 'upper_limit': {}}
# LOWER-LIMIT
if scale_constraint['interval']['lower_limit'] is None:
scale_constraint_output_object['lower_limit']['type'] = 'INFINITE'
scale_constraint_output_object['lower_limit']['value'] = None
else:
# The LOWER-LIMIT's data type should match the physical data type
if scale_constraint['interval']['lower_limit']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('IC SCALE-CONSTR LOWER-LIMIT data type does not match coded type: {} vs {}'.format(scale_constraint['interval']['lower_limit']['data_type'], output_object['physical_base_data_type']))
scale_constraint_output_object['lower_limit']['type'] = scale_constraint['interval']['lower_limit_type'].replace('eLIMIT_', '')
scale_constraint_output_object['lower_limit']['value'] = None if scale_constraint_output_object['lower_limit']['type'] == 'INFINITE' else scale_constraint['interval']['lower_limit']['value']
# UPPER-LIMIT
scale_constraint_output_object['upper_limit'] = {}
if scale_constraint['interval']['upper_limit'] is None:
scale_constraint_output_object['upper_limit']['type'] = 'INFINITE'
scale_constraint_output_object['upper_limit']['value'] = None
else:
# The UPPER-LIMIT's data type should match the physical data type
if scale_constraint['interval']['upper_limit']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('IC SCALE-CONSTR UPPER-LIMIT data type does not match coded type: {} vs {}'.format(scale_constraint['interval']['upper_limit']['data_type'], output_object['physical_base_data_type']))
scale_constraint_output_object['upper_limit']['type'] = scale_constraint['interval']['upper_limit_type'].replace('eLIMIT_', '')
scale_constraint_output_object['upper_limit']['value'] = None if scale_constraint_output_object['upper_limit']['type'] == 'INFINITE' else scale_constraint['interval']['upper_limit']['value']
# The attribute VALIDITY of each SCALE-CONSTR can take the values VALID, NOT-VALID, NOT-DEFINED or NOT-AVAILABLE
scale_constraint_output_object['validity'] = scale_constraint['range_info'][7:].replace('_', '-')
# Optionally, a SHORT-LABEL can be used to add an identifier to a SCALE-CONSTR
scale_constraint_output_object['short_label'] = scale_constraint['short_label']
scale_constraint_output_object['description'] = scale_constraint['description']
# Add the SCALE-CONSTR to the list
output_object['physical_constraint']['scale_constraints'].append(scale_constraint_output_object)
# The implicit valid range is not restricted by the size or encoding of the data type, only by the general restriction given by the BASE-DATA-TYPE
# If the DATA-OBJECT-PROP is used for calculation of physical value from the internal one, the physical value is matched against the valid physical range after applying the conversion method
# Computational methods (COMPU-METHOD) are needed to calculate between the internal (coded) type and the physical type of the diagnostic values
if 'compu_method' in dop:
# A COMPU-METHOD can specify a COMPU-INTERNAL-TO-PHYS and/or a COMPU-PHYS-TO-INTERNAL element, which specifies the transformation
# of a coded value read out of the PDU into a physical value of a type compliant to the DOP-BASE this COMPU-METHOD belongs to
compu_method = dop['compu_method']
# Get the COMPU-METHOD's CATEGORY (type)
compu_category = compu_method['compu_category']
# For all COMPU-METHODs, except those of categories IDENTICAL, TEXTTABLE and COMPUCODE, the calculation type (float or integer) is deduced from the type of the internal and physical values
# It has a decisive influence on the precision of the calculation
output_object['calculation_type'] = None
if compu_category not in ['eIDENTICAL', 'eTEXTTABLE']:
# In any other case, the calculation is of type A_INT32
output_object['calculation_type'] = 'A_INT32'
# If one of the types (physical or internal) is A_FLOAT32 or A_FLOAT64, the calculation is of type A_FLOAT64
if output_object['coded_base_data_type'] in ['A_FLOAT32', 'A_FLOAT64'] or output_object['physical_base_data_type'] in ['A_FLOAT32', 'A_FLOAT64']:
output_object['calculation_type'] = 'A_FLOAT64'
# If both types are A_UINT32, the calculation is of type A_UINT32
if output_object['coded_base_data_type'] == 'A_UINT32' and output_object['physical_base_data_type'] == 'A_UINT32':
output_object['calculation_type'] = 'A_UINT32'
# Computational methods: MCD-2D V2.2 - 7.3.6.6
match compu_category:
# The internal value and the physical value are identical
case 'eIDENTICAL':
output_object['compu_category'] = 'IDENTICAL'
# For COMPU-METHODs of this type, the data objects COMPU-INTERNAL-TO-PHYS and COMPU-PHYS-TO-INTERNAL are not allowed
if compu_method['compu_internal_to_phys'] is not None or compu_method['compu_phys_to_internal'] is not None:
raise RuntimeError('COMPU-INTERNAL-TO-PHYS and COMPU-PHYS-TO-INTERNAL are not allowed for COMPU-METHOD of type IDENTICAL')
# The input value is multiplied by a factor and an offset is added, optionally the sum is then divided by an unsigned integer
case 'eLINEAR':
output_object['compu_category'] = 'LINEAR'
# Exactly one COMPU-SCALE must be defined
if len(compu_method['compu_internal_to_phys']['compu_scales']) != 1:
raise RuntimeError('Exactly one COMPU-SCALE must be defined, not {}'.format(len(compu_method['compu_internal_to_phys']['compu_scales'])))
# Get the only COMPU-SCALE
compu_scale = compu_method['compu_internal_to_phys']['compu_scales'][0]
# It contains COMPU-RATIONAL-COEFFS, within which COMPU-NUMERATOR and COMPU-DENOMINATOR are declared
compu_rational_coeffs = compu_scale['compu_rational_coeffs']
compu_numerator = compu_rational_coeffs['numerator']
compu_denominator = compu_rational_coeffs['denominator']
# The numerator should contain two values
if len(compu_numerator) == 2:
# The first one is the offset (VN0), the second one is the factor (VN1)
offset = compu_numerator[0]
factor = compu_numerator[1]
# If it contains one value, use the default factor
elif len(compu_numerator) == 1:
offset = compu_numerator[0]
factor = 1
# If it contains no values, use the default offset and factor
elif len(compu_numerator) == 0:
offset = 0
factor = 1
else:
raise RuntimeError('Unexpected amount of values in numerator: {}'.format(len(compu_numerator)))
# If the denominator is present, it shall specify exactly one unsigned integer value
if len(compu_denominator) not in [0, 1]:
raise RuntimeError('The denominator should contain zero or one values; contains: {}'.format(len(compu_denominator)))
# If the COMPU-DENOMINATOR is not specified, the numerical value 1 is assumed for the denominator
divisor = 1 if len(compu_denominator) == 0 else compu_denominator[0]
# The LIMITs at COMPU-SCALE can be used to restrict the value domain
compu_scale_output_object = {'coded_lower_limit': {}, 'coded_upper_limit': {}, 'physical_lower_limit': {}, 'physical_upper_limit': {}}
# Coded LOWER-LIMIT
if compu_scale['lower_limit_as_coded_value'] is None:
compu_scale_output_object['coded_lower_limit']['type'] = 'INFINITE'
compu_scale_output_object['coded_lower_limit']['value'] = None
else:
compu_scale_output_object['coded_lower_limit']['type'] = compu_scale['lower_limit_as_coded_value']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['coded_lower_limit']['type'] == 'INFINITE':
compu_scale_output_object['coded_lower_limit']['value'] = None
else:
compu_scale_output_object['coded_lower_limit']['value'] = compu_scale['lower_limit_as_coded_value']['mcd_value']['value']
if compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('COMPU-SCALE coded LOWER-LIMIT data type does not match coded type: {} vs {}'.format(compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'], output_object['coded_base_data_type']))
# Coded UPPER-LIMIT
if compu_scale['upper_limit_as_coded_value'] is None:
compu_scale_output_object['coded_upper_limit']['type'] = 'INFINITE'
compu_scale_output_object['coded_upper_limit']['value'] = None
else:
compu_scale_output_object['coded_upper_limit']['type'] = compu_scale['upper_limit_as_coded_value']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['coded_upper_limit']['type'] == 'INFINITE':
compu_scale_output_object['coded_upper_limit']['value'] = None
else:
compu_scale_output_object['coded_upper_limit']['value'] = compu_scale['upper_limit_as_coded_value']['mcd_value']['value']
if compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('COMPU-SCALE coded UPPER-LIMIT data type does not match coded type: {} vs {}'.format(compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'], output_object['coded_base_data_type']))
# Physical LOWER-LIMIT
if compu_scale['lower_limit'] is None:
compu_scale_output_object['physical_lower_limit']['type'] = 'INFINITE'
compu_scale_output_object['physical_lower_limit']['value'] = None
else:
compu_scale_output_object['physical_lower_limit']['type'] = compu_scale['lower_limit']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['physical_lower_limit']['type'] == 'INFINITE':
compu_scale_output_object['physical_lower_limit']['value'] = None
else:
compu_scale_output_object['physical_lower_limit']['value'] = compu_scale['lower_limit']['mcd_value']['value']
if compu_scale['lower_limit']['mcd_value']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('COMPU-SCALE physical LOWER-LIMIT data type does not match physical type: {} vs {}'.format(compu_scale['lower_limit']['mcd_value']['data_type'], output_object['physical_base_data_type']))
# Physical UPPER-LIMIT
if compu_scale['upper_limit'] is None:
compu_scale_output_object['physical_upper_limit']['type'] = 'INFINITE'
compu_scale_output_object['physical_upper_limit']['value'] = None
else:
compu_scale_output_object['physical_upper_limit']['type'] = compu_scale['upper_limit']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['physical_upper_limit']['type'] == 'INFINITE':
compu_scale_output_object['physical_upper_limit']['value'] = None
else:
compu_scale_output_object['physical_upper_limit']['value'] = compu_scale['upper_limit']['mcd_value']['value']
if compu_scale['upper_limit']['mcd_value']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('COMPU-SCALE physical UPPER-LIMIT data type does not match physical type: {} vs {}'.format(compu_scale['upper_limit']['mcd_value']['data_type'], output_object['physical_base_data_type']))
# Generate the mathematical expression (linear function) of the COMPU-SCALE
compu_scale_output_object['formula'] = '({} + (x * {})) / {}'.format(offset, factor, divisor)
# Store the COMPU-SCALE
output_object['compu_scale'] = compu_scale_output_object
# Check possible data types
if output_object['coded_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('Coded BASE-DATA-TYPE {} not allowed for LINEAR'.format(output_object['coded_base_data_type']))
if output_object['physical_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('Physical BASE-DATA-TYPE {} not allowed for LINEAR'.format(output_object['physical_base_data_type']))
# Same as LINEAR, but multiple COMPU-SCALES (intervals) can be defined, each with its own formula
case 'eSCALE_LINEAR':
output_object['compu_category'] = 'SCALE-LINEAR'
# At least one COMPU-SCALE must be defined
if len(compu_method['compu_internal_to_phys']['compu_scales']) < 1:
raise RuntimeError('At least one COMPU-SCALE must be defined, not {}'.format(len(compu_method['compu_internal_to_phys']['compu_scales'])))
# Go through each COMPU-SCALE
output_object['compu_scales'] = []
for compu_scale in compu_method['compu_internal_to_phys']['compu_scales']:
# A COMPU-SCALE contains COMPU-RATIONAL-COEFFS, within which COMPU-NUMERATOR and COMPU-DENOMINATOR are declared
compu_rational_coeffs = compu_scale['compu_rational_coeffs']
compu_numerator = compu_rational_coeffs['numerator']
compu_denominator = compu_rational_coeffs['denominator']
# The numerator should contain two values
if len(compu_numerator) == 2:
# The first one is the offset (VN0), the second one is the factor (VN1)
offset = compu_numerator[0]
factor = compu_numerator[1]
# If it contains one value, that is the offset, use the default factor
elif len(compu_numerator) == 1:
offset = compu_numerator[0]
factor = 1
# If it contains no values, use the default offset and factor
elif len(compu_numerator) == 0:
offset = 0
factor = 1
else:
raise RuntimeError('Unexpected amount of values in numerator: {}'.format(len(compu_numerator)))
# If the denominator is present, it shall specify exactly one unsigned integer value
if len(compu_denominator) not in [0, 1]:
raise RuntimeError('The denominator should contain zero or one values; contains: {}'.format(len(compu_denominator)))
# If the COMPU-DENOMINATOR is not specified, the numerical value 1 is assumed for the denominator
divisor = 1 if len(compu_denominator) == 0 else compu_denominator[0]
# The LIMITs at COMPU-SCALE can be used to restrict the value domain
compu_scale_output_object = {'coded_lower_limit': {}, 'coded_upper_limit': {}, 'physical_lower_limit': {}, 'physical_upper_limit': {}}
# Coded LOWER-LIMIT
if compu_scale['lower_limit_as_coded_value'] is None:
compu_scale_output_object['coded_lower_limit']['type'] = 'INFINITE'
compu_scale_output_object['coded_lower_limit']['value'] = None
else:
compu_scale_output_object['coded_lower_limit']['type'] = compu_scale['lower_limit_as_coded_value']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['coded_lower_limit']['type'] == 'INFINITE':
compu_scale_output_object['coded_lower_limit']['value'] = None
else:
compu_scale_output_object['coded_lower_limit']['value'] = compu_scale['lower_limit_as_coded_value']['mcd_value']['value']
if compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('COMPU-SCALE coded LOWER-LIMIT data type does not match coded type: {} vs {}'.format(compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'], output_object['coded_base_data_type']))
# Coded UPPER-LIMIT
if compu_scale['upper_limit_as_coded_value'] is None:
compu_scale_output_object['coded_upper_limit']['type'] = 'INFINITE'
compu_scale_output_object['coded_upper_limit']['value'] = None
else:
compu_scale_output_object['coded_upper_limit']['type'] = compu_scale['upper_limit_as_coded_value']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['coded_upper_limit']['type'] == 'INFINITE':
compu_scale_output_object['coded_upper_limit']['value'] = None
else:
compu_scale_output_object['coded_upper_limit']['value'] = compu_scale['upper_limit_as_coded_value']['mcd_value']['value']
if compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('COMPU-SCALE coded UPPER-LIMIT data type does not match coded type: {} vs {}'.format(compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'], output_object['coded_base_data_type']))
# Physical LOWER-LIMIT
if compu_scale['lower_limit'] is None:
compu_scale_output_object['physical_lower_limit']['type'] = 'INFINITE'
compu_scale_output_object['physical_lower_limit']['value'] = None
else:
compu_scale_output_object['physical_lower_limit']['type'] = compu_scale['lower_limit']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['physical_lower_limit']['type'] == 'INFINITE':
compu_scale_output_object['physical_lower_limit']['value'] = None
else:
compu_scale_output_object['physical_lower_limit']['value'] = compu_scale['lower_limit']['mcd_value']['value']
if compu_scale['lower_limit']['mcd_value']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('COMPU-SCALE physical LOWER-LIMIT data type does not match physical type: {} vs {}'.format(compu_scale['lower_limit']['mcd_value']['data_type'], output_object['physical_base_data_type']))
# Physical UPPER-LIMIT
if compu_scale['upper_limit'] is None:
compu_scale_output_object['physical_upper_limit']['type'] = 'INFINITE'
compu_scale_output_object['physical_upper_limit']['value'] = None
else:
compu_scale_output_object['physical_upper_limit']['type'] = compu_scale['upper_limit']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['physical_upper_limit']['type'] == 'INFINITE':
compu_scale_output_object['physical_upper_limit']['value'] = None
else:
compu_scale_output_object['physical_upper_limit']['value'] = compu_scale['upper_limit']['mcd_value']['value']
if compu_scale['upper_limit']['mcd_value']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('COMPU-SCALE physical UPPER-LIMIT data type does not match physical type: {} vs {}'.format(compu_scale['upper_limit']['mcd_value']['data_type'], output_object['physical_base_data_type']))
# Generate the mathematical expression (linear function) of the COMPU-SCALE
compu_scale_output_object['formula'] = '({} + (x * {})) / {}'.format(offset, factor, divisor)
# Add the COMPU-SCALE to the list
output_object['compu_scales'].append(compu_scale_output_object)
# The category SCALE-LINEAR may define an optionally default scale, COMPU-DEFAULT-VALUE
output_object['compu_default_value'] = None
if compu_method['compu_internal_to_phys']['compu_default_value'] is not None:
output_object['compu_default_value'] = compu_method['compu_internal_to_phys']['compu_default_value']['value']
# The COMPU-DEFAULT-VALUE's data type seems to be Unicode string...?
if compu_method['compu_internal_to_phys']['compu_default_value']['data_type'] != 'eA_UNICODE2STRING':
raise RuntimeError('COMPU-DEFAULT-VALUE ({}) data type is not A_UNICODE2STRING: {}'.format(compu_method['compu_internal_to_phys']['compu_default_value']['data_type']))
# Check possible data types
if output_object['coded_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('Coded BASE-DATA-TYPE {} not allowed for SCALE-LINEAR'.format(output_object['coded_base_data_type']))
if output_object['physical_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('Physical BASE-DATA-TYPE {} not allowed for SCALE-LINEAR'.format(output_object['physical_base_data_type']))
# A rational function (numerator and denominator are polynomials) can be defined for multiple disjunctive intervals
case 'eSCALE_RAT_FUNC':
output_object['compu_category'] = 'SCALE-RAT-FUNC'
# At least one COMPU-SCALE must be defined
if len(compu_method['compu_internal_to_phys']['compu_scales']) < 1:
raise RuntimeError('At least one COMPU-SCALE must be defined, not {}'.format(len(compu_method['compu_internal_to_phys']['compu_scales'])))
# Go through each COMPU-SCALE
output_object['compu_scales'] = []
for compu_scale in compu_method['compu_internal_to_phys']['compu_scales']:
# A COMPU-SCALE contains COMPU-RATIONAL-COEFFS, within which COMPU-NUMERATOR and COMPU-DENOMINATOR are declared
compu_rational_coeffs = compu_scale['compu_rational_coeffs']
compu_numerator = compu_rational_coeffs['numerator']
compu_denominator = compu_rational_coeffs['denominator']
# The LIMITs at COMPU-SCALE can be used to restrict the value domain
compu_scale_output_object = {'coded_lower_limit': {}, 'coded_upper_limit': {}, 'physical_lower_limit': {}, 'physical_upper_limit': {}}
# Coded LOWER-LIMIT
if compu_scale['lower_limit_as_coded_value'] is None:
compu_scale_output_object['coded_lower_limit']['type'] = 'INFINITE'
compu_scale_output_object['coded_lower_limit']['value'] = None
else:
compu_scale_output_object['coded_lower_limit']['type'] = compu_scale['lower_limit_as_coded_value']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['coded_lower_limit']['type'] == 'INFINITE':
compu_scale_output_object['coded_lower_limit']['value'] = None
else:
compu_scale_output_object['coded_lower_limit']['value'] = compu_scale['lower_limit_as_coded_value']['mcd_value']['value']
if compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('COMPU-SCALE coded LOWER-LIMIT data type does not match coded type: {} vs {}'.format(compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'], output_object['coded_base_data_type']))
# Coded UPPER-LIMIT
if compu_scale['upper_limit_as_coded_value'] is None:
compu_scale_output_object['coded_upper_limit']['type'] = 'INFINITE'
compu_scale_output_object['coded_upper_limit']['value'] = None
else:
compu_scale_output_object['coded_upper_limit']['type'] = compu_scale['upper_limit_as_coded_value']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['coded_upper_limit']['type'] == 'INFINITE':
compu_scale_output_object['coded_upper_limit']['value'] = None
else:
compu_scale_output_object['coded_upper_limit']['value'] = compu_scale['upper_limit_as_coded_value']['mcd_value']['value']
if compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('COMPU-SCALE coded UPPER-LIMIT data type does not match coded type: {} vs {}'.format(compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'], output_object['coded_base_data_type']))
# Physical LOWER-LIMIT
if compu_scale['lower_limit'] is None:
compu_scale_output_object['physical_lower_limit']['type'] = 'INFINITE'
compu_scale_output_object['physical_lower_limit']['value'] = None
else:
compu_scale_output_object['physical_lower_limit']['type'] = compu_scale['lower_limit']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['physical_lower_limit']['type'] == 'INFINITE':
compu_scale_output_object['physical_lower_limit']['value'] = None
else:
compu_scale_output_object['physical_lower_limit']['value'] = compu_scale['lower_limit']['mcd_value']['value']
if compu_scale['lower_limit']['mcd_value']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('COMPU-SCALE physical LOWER-LIMIT data type does not match physical type: {} vs {}'.format(compu_scale['lower_limit']['mcd_value']['data_type'], output_object['physical_base_data_type']))
# Physical UPPER-LIMIT
if compu_scale['upper_limit'] is None:
compu_scale_output_object['physical_upper_limit']['type'] = 'INFINITE'
compu_scale_output_object['physical_upper_limit']['value'] = None
else:
compu_scale_output_object['physical_upper_limit']['type'] = compu_scale['upper_limit']['limit_type'].replace('eLIMIT_', '')
if compu_scale_output_object['physical_upper_limit']['type'] == 'INFINITE':
compu_scale_output_object['physical_upper_limit']['value'] = None
else:
compu_scale_output_object['physical_upper_limit']['value'] = compu_scale['upper_limit']['mcd_value']['value']
if compu_scale['upper_limit']['mcd_value']['data_type'] != 'e'+output_object['physical_base_data_type']:
raise RuntimeError('COMPU-SCALE physical UPPER-LIMIT data type does not match physical type: {} vs {}'.format(compu_scale['upper_limit']['mcd_value']['data_type'], output_object['physical_base_data_type']))
# Generate the mathematical expression (linear function) of the COMPU-SCALE
numerator = polynomial_to_string(compu_numerator)
denominator = polynomial_to_string(compu_denominator)
compu_scale_output_object['formula'] = '({}) / ({})'.format(numerator, denominator)
# Do not allow division by zero
if denominator == '0':
raise RuntimeError('Denominator is 0')
# Add the COMPU-SCALE to the list
output_object['compu_scales'].append(compu_scale_output_object)
# The category SCALE-RAT-FUNC may define an optionally default scale, COMPU-DEFAULT-VALUE
output_object['compu_default_value'] = None
if compu_method['compu_internal_to_phys']['compu_default_value'] is not None:
output_object['compu_default_value'] = compu_method['compu_internal_to_phys']['compu_default_value']['value']
# The COMPU-DEFAULT-VALUE's data type seems to be Unicode string...?
if compu_method['compu_internal_to_phys']['compu_default_value']['data_type'] != 'eA_UNICODE2STRING':
raise RuntimeError('COMPU-DEFAULT-VALUE ({}) data type is not A_UNICODE2STRING: {}'.format(compu_method['compu_internal_to_phys']['compu_default_value']['data_type']))
# Check possible data types
if output_object['coded_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('Coded BASE-DATA-TYPE {} not allowed for SCALE-RAT-FUNC'.format(output_object['coded_base_data_type']))
if output_object['physical_base_data_type'] not in ['A_INT32', 'A_UINT32', 'A_FLOAT32', 'A_FLOAT64']:
raise RuntimeError('Physical BASE-DATA-TYPE {} not allowed for SCALE-RAT-FUNC'.format(output_object['physical_base_data_type']))
# The internal value is transformed into a textual expression
case 'eTEXTTAB':
output_object['compu_category'] = 'TEXTTABLE'
# At least one COMPU-SCALE must be defined
if len(compu_method['compu_internal_to_phys']['compu_scales']) < 1:
raise RuntimeError('At least one COMPU-SCALE must be defined for TEXTTABLE; defined: {}'.format(len(compu_method['compu_internal_to_phys']['compu_scales'])))
# In each COMPU-SCALE, LOWER-LIMIT and UPPER-LIMIT define an interval
output_object['compu_scales'] = []
for compu_scale in compu_method['compu_internal_to_phys']['compu_scales']:
# Only the coded LIMITs matter, since the physical LIMITs will both contain the same string as in the COMPU-CONST
compu_scale_output_object = {}
# Both LIMITs should be CLOSED
if compu_scale['lower_limit_as_coded_value']['limit_type'] != 'eLIMIT_CLOSED' or compu_scale['upper_limit_as_coded_value']['limit_type'] != 'eLIMIT_CLOSED':
raise RuntimeError('Unexpected LOWER/UPPER-LIMIT types: {}, {}'.format(compu_scale['lower_limit_as_coded_value']['limit_type'], compu_scale['upper_limit_as_coded_value']['limit_type']))
# Both LIMITs' data types must match the coded data type
if compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type'] or compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'] != 'e'+output_object['coded_base_data_type']:
raise RuntimeError('Data types of coded LOWER/UPPER-LIMIT {}, {} do not match coded data type {}'.format(compu_scale['lower_limit_as_coded_value']['mcd_value']['data_type'], compu_scale['upper_limit_as_coded_value']['mcd_value']['data_type'], compu_scale_output_object['coded_base_data_type']))
# Get the LIMITs
compu_scale_output_object['lower_limit'] = compu_scale['lower_limit_as_coded_value']['mcd_value']['value']
compu_scale_output_object['upper_limit'] = compu_scale['upper_limit_as_coded_value']['mcd_value']['value']
# If the internal data type is a string type, the definition of a range is not allowed
# If the element UPPER-LIMIT is explicitly specified, its content shall be equal to the content of LOWER-LIMIT
if output_object['coded_base_data_type'] in ['A_ASCIISTRING', 'A_UNICODE2STRING']:
if compu_scale_output_object['lower_limit'] != compu_scale_output_object['upper_limit']:
raise RuntimeError('UPPER-LIMIT does not match LOWER-LIMIT for TEXTTABLE with string coded data type: {} vs {}'.format(compu_scale_output_object['lower_limit'], compu_scale_output_object['upper_limit']))