automotive-ecu 0.4.0 → 0.5.0
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.
- checksums.yaml +4 -4
- data/lib/ecu/interfaces/a2l/a2l_conversion.rb +201 -0
- data/lib/ecu/interfaces/a2l/a2l_document.rb +103 -0
- data/lib/ecu/interfaces/a2l/a2l_parser.rb +333 -0
- data/lib/ecu/interfaces/a2l/a2l_parser_error.rb +6 -0
- data/lib/ecu/interfaces/a2l/label_list.rb +4 -12
- data/lib/ecu/interfaces/a2l/s19_image.rb +153 -0
- data/lib/ecu/labels/label_list.rb +4 -2
- data/lib/ecu/labels/label_list_comparison.rb +3 -0
- data/lib/ecu/signals/signal_list_comparison.rb +3 -0
- data/lib/ecu/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0aa35f657df5646f7a32c3bbec852854dd65839dbf370cab649fe78e95c4a162
|
|
4
|
+
data.tar.gz: a4cb4d0720619beaa92bddadb93c9b15a83c9df5567c323e60a86b4a77c87c97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89d17f6da4b8e91554515e6c7956ce6cd70deb411897cc993eed2c875247012b858d54cf0212a258cdd59ceb19404d388516b4df30378039656cc97a13729706
|
|
7
|
+
data.tar.gz: 0fc0179be08391be84bd9bbecca1b7a9512941ebbe91e7bafdacfb26f1e79c7354c3832887f578b8faa4caa86c33acc1bc6e2cecb2582909c9174875a6e1dba6
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "a2l_parser_error"
|
|
4
|
+
|
|
5
|
+
class Ecu
|
|
6
|
+
class LabelList
|
|
7
|
+
class A2lConversion
|
|
8
|
+
def initialize(blocks)
|
|
9
|
+
@methods = {}
|
|
10
|
+
@tables = {}
|
|
11
|
+
@units = {}
|
|
12
|
+
index(blocks)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def convert(name, value)
|
|
16
|
+
return value if name == "NO_COMPU_METHOD"
|
|
17
|
+
|
|
18
|
+
method = lookup(@methods, name)
|
|
19
|
+
case method.values[2]
|
|
20
|
+
when "IDENTICAL"
|
|
21
|
+
value
|
|
22
|
+
when "LINEAR"
|
|
23
|
+
slope, offset = coefficients(method, "COEFFS_LINEAR", 2)
|
|
24
|
+
number(number(value) * slope + offset)
|
|
25
|
+
when "RAT_FUNC"
|
|
26
|
+
rational(method, number(value))
|
|
27
|
+
when "TAB_VERB", "TAB_INTP", "TAB_NOINTP"
|
|
28
|
+
table_value(method, number(value))
|
|
29
|
+
else
|
|
30
|
+
fail_conversion("Unsupported conversion #{method.values[2]}: #{name}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def unit(name)
|
|
35
|
+
return nil if name == "NO_COMPU_METHOD"
|
|
36
|
+
|
|
37
|
+
method = lookup(@methods, name)
|
|
38
|
+
reference = option(method, "REF_UNIT")
|
|
39
|
+
return method.values[4] unless reference
|
|
40
|
+
|
|
41
|
+
lookup(@units, reference.first).values[2]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def fail_conversion(message)
|
|
47
|
+
raise Ecu::A2lParserError, message
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def index(blocks)
|
|
51
|
+
blocks.each do |block|
|
|
52
|
+
registry = case block.kind
|
|
53
|
+
when "COMPU_METHOD" then @methods
|
|
54
|
+
when "COMPU_TAB", "COMPU_VTAB", "COMPU_VTAB_RANGE" then @tables
|
|
55
|
+
when "UNIT" then @units
|
|
56
|
+
end
|
|
57
|
+
if registry
|
|
58
|
+
name = block.values.first
|
|
59
|
+
fail_conversion("Duplicate #{block.kind} #{name}") if registry.key?(name)
|
|
60
|
+
registry[name] = block
|
|
61
|
+
else
|
|
62
|
+
index(block.children)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def lookup(registry, name)
|
|
68
|
+
registry.fetch(name) { fail_conversion("Missing reference #{name}") }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def option(block, name)
|
|
72
|
+
child = block.children.find { |entry| entry.kind == name }
|
|
73
|
+
return child.values if child
|
|
74
|
+
|
|
75
|
+
# Header fields can contain arbitrary quoted text, including keywords.
|
|
76
|
+
start = case block.kind
|
|
77
|
+
when "COMPU_METHOD" then 5
|
|
78
|
+
when "COMPU_VTAB_RANGE" then 3 + table_count(block, 2) * 3
|
|
79
|
+
when "COMPU_TAB", "COMPU_VTAB" then 4 + table_count(block, 3) * 2
|
|
80
|
+
else 0
|
|
81
|
+
end
|
|
82
|
+
position = (start...block.values.length).find { |i| block.values[i] == name }
|
|
83
|
+
block.values[(position + 1)..-1] if position
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def number(value)
|
|
87
|
+
return value if value.is_a?(Integer)
|
|
88
|
+
return Integer(value, 10) if value.is_a?(String) && /\A[+-]?[0-9]+\z/.match?(value)
|
|
89
|
+
return Integer(value, 16) if value.is_a?(String) && /\A[+-]?0[xX][0-9A-Fa-f]+\z/.match?(value)
|
|
90
|
+
|
|
91
|
+
result = Float(value)
|
|
92
|
+
fail_conversion("Non-finite numeric value #{value}") unless result.finite?
|
|
93
|
+
result
|
|
94
|
+
rescue ArgumentError, TypeError
|
|
95
|
+
fail_conversion("Invalid numeric value #{value.inspect}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def coefficients(block, keyword, count)
|
|
99
|
+
values = option(block, keyword)
|
|
100
|
+
fail_conversion("Missing #{keyword} for #{block.values.first}") unless values && values.size >= count
|
|
101
|
+
values.first(count).map { |value| number(number(value).to_f) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def rational(method, internal)
|
|
105
|
+
a, b, c, d, e, f = coefficients(method, "COEFFS", 6)
|
|
106
|
+
fail_conversion("Zero rational denominator") if [d, e, f].all?(&:zero?)
|
|
107
|
+
scale = normalization_scale([a, b, c, d, e, f])
|
|
108
|
+
a, b, c, d, e, f = [a, b, c, d, e, f].map { |coefficient| coefficient / scale }
|
|
109
|
+
# internal = (a*x*x + b*x + c) / (d*x*x + e*x + f)
|
|
110
|
+
internal = number(internal.to_f)
|
|
111
|
+
scale = normalization_scale([internal, 1.0])
|
|
112
|
+
aa, bb, cc = a / scale - internal / scale * d,
|
|
113
|
+
b / scale - internal / scale * e,
|
|
114
|
+
c / scale - internal / scale * f
|
|
115
|
+
scale = normalization_scale([aa, bb, cc])
|
|
116
|
+
aa, bb, cc = [aa, bb, cc].map { |coefficient| coefficient / scale }
|
|
117
|
+
if aa.zero?
|
|
118
|
+
fail_conversion(cc.zero? ? "ambiguous rational inverse" : "Rational domain error") if bb.zero?
|
|
119
|
+
roots = [-cc / bb]
|
|
120
|
+
else
|
|
121
|
+
discriminant = bb * bb - 4 * aa * cc
|
|
122
|
+
fail_conversion("Rational domain error") if discriminant < 0
|
|
123
|
+
if discriminant.zero?
|
|
124
|
+
roots = [-bb / (2 * aa)]
|
|
125
|
+
else
|
|
126
|
+
# Stable quadratic evaluation avoids subtracting nearly equal numbers.
|
|
127
|
+
q = -0.5 * (bb + (bb < 0 ? -Math.sqrt(discriminant) : Math.sqrt(discriminant)))
|
|
128
|
+
roots = [q / aa, cc / q]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
roots = roots.uniq.select { |root| root.finite? && !(d * root * root + e * root + f).zero? }
|
|
132
|
+
fail_conversion("Zero rational denominator or domain error") if roots.empty?
|
|
133
|
+
fail_conversion("ambiguous rational inverse") if roots.length > 1
|
|
134
|
+
roots.first
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalization_scale(values)
|
|
138
|
+
# Power-of-two scaling avoids rounding ordinary coefficients while keeping
|
|
139
|
+
# the discriminant away from overflow and underflow.
|
|
140
|
+
Math.ldexp(1.0, Math.frexp(values.map(&:abs).max).last - 1)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def table_count(table, position)
|
|
144
|
+
count = Integer(table.values[position], 10)
|
|
145
|
+
fail_conversion("Invalid table count") if count < 0
|
|
146
|
+
count
|
|
147
|
+
rescue ArgumentError, TypeError
|
|
148
|
+
fail_conversion("Invalid table count")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def table_value(method, value)
|
|
152
|
+
reference = option(method, "COMPU_TAB_REF")
|
|
153
|
+
fail_conversion("Missing COMPU_TAB_REF for #{method.values.first}") unless reference
|
|
154
|
+
table = lookup(@tables, reference.first)
|
|
155
|
+
verbal = method.values[2] == "TAB_VERB"
|
|
156
|
+
if table.kind == "COMPU_VTAB_RANGE" && verbal
|
|
157
|
+
count = table_count(table, 2)
|
|
158
|
+
rows = table.values.slice(3, count * 3)
|
|
159
|
+
fail_conversion("Malformed verbal ranges") unless rows && rows.length == count * 3
|
|
160
|
+
entries = rows.each_slice(3).map do |low, high, label|
|
|
161
|
+
low, high = number(low), number(high)
|
|
162
|
+
fail_conversion("Invalid verbal range") if low > high || label.nil?
|
|
163
|
+
[low, high, label]
|
|
164
|
+
end
|
|
165
|
+
matches = entries.select { |low, high, _| value >= low && value <= high }
|
|
166
|
+
fail_conversion("Ambiguous verbal range") if matches.size > 1
|
|
167
|
+
return matches.first[2] unless matches.empty?
|
|
168
|
+
else
|
|
169
|
+
expected = verbal ? "COMPU_VTAB" : "COMPU_TAB"
|
|
170
|
+
fail_conversion("Invalid table type #{table.kind}") unless table.kind == expected
|
|
171
|
+
count = table_count(table, 3)
|
|
172
|
+
rows = table.values.slice(4, count * 2)
|
|
173
|
+
fail_conversion("Malformed conversion table") unless rows && rows.length == count * 2
|
|
174
|
+
entries = rows.each_slice(2).map { |input, output| [number(input), verbal ? output : number(output)] }
|
|
175
|
+
fail_conversion("Duplicate table input") unless entries.map(&:first).uniq.size == entries.size
|
|
176
|
+
exact = entries.find { |input, _| input == value }
|
|
177
|
+
return exact[1] if exact
|
|
178
|
+
if method.values[2] == "TAB_INTP"
|
|
179
|
+
pair = entries.sort_by(&:first).each_cons(2).find { |left, right| value > left[0] && value < right[0] }
|
|
180
|
+
if pair
|
|
181
|
+
left, right = pair
|
|
182
|
+
# Subtract integer keys before converting, preserving adjacent 64-bit inputs.
|
|
183
|
+
distance, width = value - left[0], right[0] - left[0]
|
|
184
|
+
if distance.finite? && width.finite?
|
|
185
|
+
fraction = distance.fdiv(width)
|
|
186
|
+
else
|
|
187
|
+
scale = [value.abs, left[0].abs, right[0].abs].max
|
|
188
|
+
fraction = (value / scale - left[0] / scale) / (right[0] / scale - left[0] / scale)
|
|
189
|
+
end
|
|
190
|
+
return number(left[1] * (1.0 - fraction) + right[1] * fraction)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
default = option(table, verbal ? "DEFAULT_VALUE" : "DEFAULT_VALUE_NUMERIC")
|
|
195
|
+
return verbal ? default.first : number(default.first) if default && !default.empty?
|
|
196
|
+
|
|
197
|
+
fail_conversion("No table entry or default for #{value}")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "strscan"
|
|
2
|
+
require_relative "a2l_parser_error"
|
|
3
|
+
|
|
4
|
+
class Ecu
|
|
5
|
+
class LabelList
|
|
6
|
+
class A2lDocument
|
|
7
|
+
Block = Struct.new(:kind, :values, :children, :line, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
def initialize(text = nil, **keywords)
|
|
10
|
+
@text = keywords.fetch(:text, text)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
unless @text.is_a?(String) && @text.valid_encoding? && @text.encoding.ascii_compatible?
|
|
15
|
+
raise A2lParserError, "Invalid A2L text encoding at line 1"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@scanner = StringScanner.new(@text)
|
|
19
|
+
@line = 1
|
|
20
|
+
stack = []
|
|
21
|
+
modules = []
|
|
22
|
+
while (token = next_token)
|
|
23
|
+
value, line, quoted = token
|
|
24
|
+
if !quoted && value == "/begin"
|
|
25
|
+
kind = block_name(line)
|
|
26
|
+
ignored = (!stack.empty? && stack.last[2].nil?) ||
|
|
27
|
+
%w[MEASUREMENT A2ML IF_DATA].include?(kind)
|
|
28
|
+
block = ignored ? nil : Block.new(kind: kind, values: [], children: [], line: line)
|
|
29
|
+
stack.last[2].children << block if block && !stack.empty? && stack.last[2]
|
|
30
|
+
modules << block if block && kind == "MODULE"
|
|
31
|
+
stack << [kind, line, block]
|
|
32
|
+
elsif !quoted && value == "/end"
|
|
33
|
+
kind = block_name(line)
|
|
34
|
+
if stack.empty? || stack.last[0] != kind
|
|
35
|
+
expected = stack.empty? ? "no open block" : stack.last[0]
|
|
36
|
+
raise A2lParserError, "At line #{line}: /end #{kind}, expected #{expected}"
|
|
37
|
+
end
|
|
38
|
+
stack.pop
|
|
39
|
+
elsif !stack.empty? && stack.last[2]
|
|
40
|
+
stack.last[2].values << value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
unless stack.empty?
|
|
44
|
+
kind, line = stack.last
|
|
45
|
+
raise A2lParserError, "Unclosed #{kind} block opened at line #{line}"
|
|
46
|
+
end
|
|
47
|
+
modules
|
|
48
|
+
ensure
|
|
49
|
+
@scanner = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def block_name(line)
|
|
55
|
+
token = next_token
|
|
56
|
+
unless token && !token[2] && token[0].match?(/\A[A-Za-z_][A-Za-z_0-9]*\z/)
|
|
57
|
+
raise A2lParserError, "Expected block name at line #{line}"
|
|
58
|
+
end
|
|
59
|
+
token[0]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def next_token
|
|
63
|
+
loop do
|
|
64
|
+
return if @scanner.eos?
|
|
65
|
+
if (space = @scanner.scan(/\s+/))
|
|
66
|
+
@line += space.count("\n")
|
|
67
|
+
elsif @scanner.scan(%r{//[^\n]*})
|
|
68
|
+
next
|
|
69
|
+
elsif @scanner.scan(%r{/\*})
|
|
70
|
+
line = @line
|
|
71
|
+
comment = @scanner.scan_until(%r{\*/})
|
|
72
|
+
raise A2lParserError, "Unclosed comment at line #{line}" unless comment
|
|
73
|
+
@line += comment.count("\n")
|
|
74
|
+
else
|
|
75
|
+
break
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
line = @line
|
|
80
|
+
if @scanner.peek(1) == '"'
|
|
81
|
+
text = @scanner.scan(/"(?:\\.|[^"\\])*"/m)
|
|
82
|
+
raise A2lParserError, "Unclosed string at line #{line}" unless text
|
|
83
|
+
@line += text.count("\n")
|
|
84
|
+
value = text[1...-1].gsub(/\\(.)/m) do
|
|
85
|
+
escaped = Regexp.last_match(1)
|
|
86
|
+
case escaped
|
|
87
|
+
when '"', "'", "\\" then escaped
|
|
88
|
+
when "n" then "\n"
|
|
89
|
+
when "r" then "\r"
|
|
90
|
+
when "t" then "\t"
|
|
91
|
+
else "\\#{escaped}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
[value, line, true]
|
|
95
|
+
else
|
|
96
|
+
text = @scanner.scan(/(?:(?!\/[\/\*])[^\s"])+/)
|
|
97
|
+
raise A2lParserError, "Invalid token at line #{line}" unless text
|
|
98
|
+
[text, line, false]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "a2l_parser_error"
|
|
4
|
+
require_relative "../../labels/festwert"
|
|
5
|
+
require_relative "../../labels/festwerteblock"
|
|
6
|
+
require_relative "../../labels/kennlinie"
|
|
7
|
+
require_relative "../../labels/kennfeld"
|
|
8
|
+
require_relative "../../labels/stuetzstellenverteilung"
|
|
9
|
+
|
|
10
|
+
class Ecu
|
|
11
|
+
class LabelList
|
|
12
|
+
class A2lParser
|
|
13
|
+
def initialize(a2l, s19, address_map: nil)
|
|
14
|
+
@a2l, @s19, @address_map = a2l, s19, address_map
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
require_relative "a2l_document" unless defined?(A2lDocument)
|
|
19
|
+
require_relative "s19_image" unless defined?(S19Image)
|
|
20
|
+
require_relative "a2l_conversion" unless defined?(A2lConversion)
|
|
21
|
+
@image = S19Image.new(@s19, address_map: @address_map)
|
|
22
|
+
A2lDocument.new(@a2l).call.flat_map { |mod| decode_module(mod) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def decode_module(mod)
|
|
28
|
+
@layouts = index(mod.children, "RECORD_LAYOUT")
|
|
29
|
+
@shared = index(mod.children, "AXIS_PTS")
|
|
30
|
+
index(mod.children, "CHARACTERISTIC")
|
|
31
|
+
@common = mod.children.find { |b| b.kind == "MOD_COMMON" }
|
|
32
|
+
@conversion = A2lConversion.new(mod.children)
|
|
33
|
+
@axis_cache = {}
|
|
34
|
+
@functions = {}
|
|
35
|
+
mod.children.select { |b| b.kind == "FUNCTION" }.each do |function|
|
|
36
|
+
function.children.select { |b| %w[DEF_CHARACTERISTIC REF_CHARACTERISTIC].include?(b.kind) }.each do |refs|
|
|
37
|
+
refs.values.each { |name| @functions[name] ||= function.values.first }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
mod.children.select { |b| %w[CHARACTERISTIC AXIS_PTS].include?(b.kind) }.map do |object|
|
|
41
|
+
begin
|
|
42
|
+
object.kind == "AXIS_PTS" ? shared_axis(object.values.first, []) : characteristic(object)
|
|
43
|
+
rescue Ecu::A2lParserError, ArgumentError, KeyError => e
|
|
44
|
+
raise Ecu::A2lParserError, "#{object.values.first} at #{object.values[object.kind == 'AXIS_PTS' ? 2 : 3]} (line #{object.line}): #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def index(blocks, kind)
|
|
50
|
+
blocks.select { |b| b.kind == kind }.each_with_object({}) do |b, result|
|
|
51
|
+
name = b.values.first
|
|
52
|
+
fail_format("Duplicate #{kind} #{name} (line #{b.line})") if result.key?(name)
|
|
53
|
+
result[name] = b
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fail_format(message)
|
|
58
|
+
raise Ecu::A2lParserError, message
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def number(value)
|
|
62
|
+
fail_format("Missing number") if value.nil?
|
|
63
|
+
value.to_s.match?(/\A[+-]?0x/i) ? Integer(value, 16) : Integer(value, 10)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def numeric(value)
|
|
67
|
+
return number(value) if value.to_s.match?(/\A[+-]?(?:0x[0-9a-f]+|\d+)\z/i)
|
|
68
|
+
result = Float(value)
|
|
69
|
+
fail_format("Nonfinite fixed axis parameter") unless result.finite?
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def options(block)
|
|
74
|
+
return [] unless block
|
|
75
|
+
offset = {"CHARACTERISTIC" => 9, "AXIS_PTS" => 10, "AXIS_DESCR" => 6, "MOD_COMMON" => 1}.fetch(block.kind, 0)
|
|
76
|
+
block.values.drop(offset)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def option(block, key, length = 1)
|
|
80
|
+
return nil unless block
|
|
81
|
+
child = block.children.find { |b| b.kind == key }
|
|
82
|
+
return child.values.first(length) if child
|
|
83
|
+
values = options(block)
|
|
84
|
+
i = values.index(key)
|
|
85
|
+
i ? values.slice(i + 1, length) : nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def unit(block, conversion)
|
|
89
|
+
option(block, "PHYS_UNIT")&.first || @conversion.unit(conversion)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def metadata(block)
|
|
93
|
+
{name: block.values[0], description: block.values[1], function: @functions[block.values[0]]}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def byte_order(block)
|
|
97
|
+
order = option(block, "BYTE_ORDER")&.first || option(@common, "BYTE_ORDER")&.first || "MSB_LAST"
|
|
98
|
+
fail_format("Unsupported BYTE_ORDER #{order}") unless %w[MSB_LAST MSB_FIRST].include?(order)
|
|
99
|
+
order
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def layout(block, name)
|
|
103
|
+
fail_format("Unsupported DEPOSIT RELATIVE") if option(block, "DEPOSIT")&.first == "RELATIVE"
|
|
104
|
+
definition = @layouts[name] || fail_format("Unknown RECORD_LAYOUT #{name}")
|
|
105
|
+
values = definition.values.drop(1)
|
|
106
|
+
fields, alignments = [], {}
|
|
107
|
+
until values.empty?
|
|
108
|
+
kind = values.shift
|
|
109
|
+
if kind.start_with?("ALIGNMENT_")
|
|
110
|
+
fail_format("Unsupported critical RECORD_LAYOUT field #{kind}") unless %w[
|
|
111
|
+
ALIGNMENT_BYTE ALIGNMENT_WORD ALIGNMENT_LONG ALIGNMENT_INT64
|
|
112
|
+
ALIGNMENT_FLOAT32_IEEE ALIGNMENT_FLOAT64_IEEE
|
|
113
|
+
].include?(kind)
|
|
114
|
+
alignments[kind] = number(values.shift)
|
|
115
|
+
elsif kind == "FNC_VALUES" || %w[AXIS_PTS_X AXIS_PTS_Y].include?(kind)
|
|
116
|
+
position, type, direction, addressing = values.shift(4)
|
|
117
|
+
fail_format("Unsupported pointer addressing #{addressing}") unless addressing == "DIRECT"
|
|
118
|
+
allowed = kind == "FNC_VALUES" ? %w[ROW_DIR COLUMN_DIR] : %w[INDEX_INCR INDEX_DECR]
|
|
119
|
+
fail_format("Unsupported #{kind} direction #{direction}") unless allowed.include?(direction)
|
|
120
|
+
fields << [kind, number(position), type, direction]
|
|
121
|
+
elsif %w[NO_AXIS_PTS_X NO_AXIS_PTS_Y].include?(kind)
|
|
122
|
+
position, type = values.shift(2)
|
|
123
|
+
fields << [kind, number(position), type, nil]
|
|
124
|
+
else
|
|
125
|
+
fail_format("Unsupported critical RECORD_LAYOUT field #{kind}")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
fail_format("Duplicate RECORD_LAYOUT component") unless fields.map(&:first).uniq.size == fields.size &&
|
|
129
|
+
fields.map { |f| f[1] }.uniq.size == fields.size
|
|
130
|
+
fail_format("Invalid RECORD_LAYOUT component position") unless fields.all? { |f| f[1].positive? }
|
|
131
|
+
fields.each do |kind, position, _type, _direction|
|
|
132
|
+
next unless kind.start_with?("NO_AXIS_PTS_")
|
|
133
|
+
axis_kind = kind.sub("NO_", "")
|
|
134
|
+
consumers = fields.select { |f| [axis_kind, "FNC_VALUES"].include?(f[0]) }
|
|
135
|
+
fail_format("Runtime axis count must precede dependent components") unless consumers.all? { |f| position < f[1] }
|
|
136
|
+
end
|
|
137
|
+
[fields.sort_by { |f| f[1] }, alignments]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def alignment(type, local)
|
|
141
|
+
key = case type
|
|
142
|
+
when "UBYTE", "SBYTE" then "ALIGNMENT_BYTE"
|
|
143
|
+
when "UWORD", "SWORD" then "ALIGNMENT_WORD"
|
|
144
|
+
when "ULONG", "SLONG" then "ALIGNMENT_LONG"
|
|
145
|
+
when "A_UINT64", "A_INT64" then "ALIGNMENT_INT64"
|
|
146
|
+
when "FLOAT32_IEEE" then "ALIGNMENT_FLOAT32_IEEE"
|
|
147
|
+
when "FLOAT64_IEEE" then "ALIGNMENT_FLOAT64_IEEE"
|
|
148
|
+
else fail_format("Unknown data type #{type}")
|
|
149
|
+
end
|
|
150
|
+
value = local[key] || number(option(@common, key)&.first || "1")
|
|
151
|
+
fail_format("Invalid #{key} #{value}") unless value.positive?
|
|
152
|
+
value
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def deposit(block, name, address, dimensions, ascii: false)
|
|
156
|
+
fail_format("Invalid dimensions #{dimensions.inspect}") unless dimensions.all? { |d| d.is_a?(Integer) && d.positive? }
|
|
157
|
+
fields, local = layout(block, name)
|
|
158
|
+
data = {}
|
|
159
|
+
cursor = number(address)
|
|
160
|
+
fields.each do |kind, _position, type, direction|
|
|
161
|
+
size = S19Image::TYPES.fetch(type) { fail_format("Unknown data type #{type}") }.first
|
|
162
|
+
align = alignment(type, local)
|
|
163
|
+
cursor = (cursor + align - 1) / align * align
|
|
164
|
+
count = case kind
|
|
165
|
+
when "NO_AXIS_PTS_X", "NO_AXIS_PTS_Y" then 1
|
|
166
|
+
when "AXIS_PTS_X" then dimensions[0]
|
|
167
|
+
when "AXIS_PTS_Y" then dimensions[1]
|
|
168
|
+
else dimensions.reduce(:*)
|
|
169
|
+
end
|
|
170
|
+
fail_format("Invalid element count #{count}") unless count && count.positive?
|
|
171
|
+
data[kind] = if ascii && kind == "FNC_VALUES"
|
|
172
|
+
fail_format("ASCII requires byte storage") unless size == 1
|
|
173
|
+
@image.read(cursor, count)
|
|
174
|
+
else
|
|
175
|
+
@image.values(cursor, type, count, byte_order(block))
|
|
176
|
+
end
|
|
177
|
+
cursor += size * count
|
|
178
|
+
if kind.start_with?("NO_AXIS_PTS_")
|
|
179
|
+
i = kind.end_with?("X") ? 0 : 1
|
|
180
|
+
actual = data[kind].first
|
|
181
|
+
fail_format("Invalid runtime axis count #{actual}") unless actual.is_a?(Integer) && actual.positive? && actual <= dimensions[i]
|
|
182
|
+
dimensions[i] = actual
|
|
183
|
+
elsif kind.start_with?("AXIS_PTS_") && direction == "INDEX_DECR"
|
|
184
|
+
data[kind].reverse!
|
|
185
|
+
end
|
|
186
|
+
data["direction"] = direction if kind == "FNC_VALUES"
|
|
187
|
+
end
|
|
188
|
+
data
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def converted(block, values, conversion)
|
|
192
|
+
mask = option(block, "BIT_MASK")&.first
|
|
193
|
+
if mask
|
|
194
|
+
mask = number(mask)
|
|
195
|
+
fail_format("Invalid BIT_MASK") unless mask.positive?
|
|
196
|
+
shift = 0
|
|
197
|
+
shift += 1 while (mask & (1 << shift)).zero?
|
|
198
|
+
values = values.map do |v|
|
|
199
|
+
fail_format("BIT_MASK requires integer storage") unless v.is_a?(Integer)
|
|
200
|
+
(v & mask) >> shift
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
values.map { |v| @conversion.convert(conversion, v) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def shared_axis(name, stack)
|
|
207
|
+
fail_format("Shared axis cycle #{(stack + [name]).join(' -> ')}") if stack.include?(name)
|
|
208
|
+
return @axis_cache[name] if @axis_cache.key?(name)
|
|
209
|
+
block = @shared[name] || fail_format("Unknown AXIS_PTS_REF #{name}")
|
|
210
|
+
reference = option(block, "AXIS_PTS_REF")&.first
|
|
211
|
+
if reference
|
|
212
|
+
other = shared_axis(reference, stack + [name])
|
|
213
|
+
values = other.xvalue
|
|
214
|
+
else
|
|
215
|
+
dims = [number(block.values[7]), 1]
|
|
216
|
+
data = deposit(block, block.values[4], block.values[2], dims)
|
|
217
|
+
raw = data["AXIS_PTS_X"] || fail_format("Missing AXIS_PTS_X for #{name}")
|
|
218
|
+
values = converted(block, raw, block.values[6])
|
|
219
|
+
end
|
|
220
|
+
maximum = number(block.values[7])
|
|
221
|
+
fail_format("Shared axis count exceeds maximum") unless maximum.positive? && values.size <= maximum
|
|
222
|
+
@axis_cache[name] = Ecu::Stuetzstellenverteilung.new(**metadata(block), xdim: values.size,
|
|
223
|
+
xvalue: values, xunit: unit(block, block.values[6]))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def fixed_axis(axis, count)
|
|
227
|
+
list = axis.children.find { |b| b.kind == "FIX_AXIS_PAR_LIST" }&.values
|
|
228
|
+
if list
|
|
229
|
+
fail_format("Fixed axis count mismatch") unless list.size == count
|
|
230
|
+
return list.map { |v| numeric(v) }
|
|
231
|
+
end
|
|
232
|
+
if (params = option(axis, "FIX_AXIS_PAR", 3))
|
|
233
|
+
offset, shift, size = params.map { |v| number(v) }
|
|
234
|
+
fail_format("Nonfinite fixed axis distance") if shift > 1023
|
|
235
|
+
distance = shift.negative? ? 2.0**shift : 2**shift
|
|
236
|
+
elsif (params = option(axis, "FIX_AXIS_PAR_DIST", 3))
|
|
237
|
+
offset, distance = params.first(2).map { |v| numeric(v) }
|
|
238
|
+
size = number(params[2])
|
|
239
|
+
else
|
|
240
|
+
fail_format("Missing fixed axis parameters")
|
|
241
|
+
end
|
|
242
|
+
fail_format("Fixed axis count mismatch") unless size == count
|
|
243
|
+
count.times.map do |i|
|
|
244
|
+
value = offset + i * distance
|
|
245
|
+
fail_format("Nonfinite fixed axis value") if value.is_a?(Float) && !value.finite?
|
|
246
|
+
value
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def axis_values(axis, count, raw)
|
|
251
|
+
fail_format("Unsupported DEPOSIT RELATIVE") if option(axis, "DEPOSIT")&.first == "RELATIVE"
|
|
252
|
+
conversion = axis.values[2]
|
|
253
|
+
case axis.values.first
|
|
254
|
+
when "STD_AXIS"
|
|
255
|
+
fail_format("Missing inline axis points") unless raw
|
|
256
|
+
fail_format("Inline axis count mismatch") unless raw.size == count
|
|
257
|
+
[converted(axis, raw, conversion), unit(axis, conversion)]
|
|
258
|
+
when "COM_AXIS"
|
|
259
|
+
reference = option(axis, "AXIS_PTS_REF")&.first || fail_format("Missing AXIS_PTS_REF")
|
|
260
|
+
shared = shared_axis(reference, [])
|
|
261
|
+
fail_format("Shared axis count mismatch") unless shared.xdim == count
|
|
262
|
+
[shared.xvalue, shared.xunit]
|
|
263
|
+
when "FIX_AXIS"
|
|
264
|
+
[converted(axis, fixed_axis(axis, count), conversion), unit(axis, conversion)]
|
|
265
|
+
else
|
|
266
|
+
fail_format("Unsupported axis type #{axis.values.first}")
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def rows(values, dimensions, direction)
|
|
271
|
+
x, y = dimensions
|
|
272
|
+
direction == "COLUMN_DIR" ? y.times.map { |j| x.times.map { |i| values[i * y + j] } } : values.each_slice(x).to_a
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def characteristic(block)
|
|
276
|
+
type, address, record, conversion = block.values.values_at(2, 3, 4, 6)
|
|
277
|
+
fail_format("Unsupported calibration type #{type}") unless %w[VALUE ASCII VAL_BLK CURVE MAP].include?(type)
|
|
278
|
+
axes = block.children.select { |b| b.kind == "AXIS_DESCR" }
|
|
279
|
+
matrix = option(block, "MATRIX_DIM", 3)
|
|
280
|
+
if matrix
|
|
281
|
+
matrix = matrix.take_while { |v| v.match?(/\A[+-]?(?:0x[0-9a-f]+|\d+)\z/i) }.map { |v| number(v) }
|
|
282
|
+
fail_format("Invalid array dimensions") if matrix.empty? || matrix.any? { |d| d <= 0 }
|
|
283
|
+
fail_format("Unsupported array dimensions") if matrix.drop(2).any? { |d| d != 1 }
|
|
284
|
+
fail_format("Unsupported scalar dimensions") if type == "VALUE" && matrix.any? { |d| d != 1 }
|
|
285
|
+
fail_format("Unsupported ASCII dimensions") if type == "ASCII" && matrix.drop(1).any? { |d| d != 1 }
|
|
286
|
+
end
|
|
287
|
+
dimensions = if %w[CURVE MAP].include?(type)
|
|
288
|
+
expected = type == "CURVE" ? 1 : 2
|
|
289
|
+
fail_format("Unsupported axis dimensions") unless axes.size == expected
|
|
290
|
+
axes.map { |axis| number(axis.values[3]) }.tap { |d| d << 1 if d.size == 1 }
|
|
291
|
+
elsif type == "VALUE"
|
|
292
|
+
[1, 1]
|
|
293
|
+
else
|
|
294
|
+
dims = matrix
|
|
295
|
+
dims ||= [number(option(block, "NUMBER")&.first || fail_format("Missing NUMBER/MATRIX_DIM"))]
|
|
296
|
+
fail_format("Unsupported array dimensions") if dims.drop(2).any? { |d| d != 1 }
|
|
297
|
+
[dims[0], dims[1] || 1]
|
|
298
|
+
end
|
|
299
|
+
if matrix && %w[CURVE MAP].include?(type)
|
|
300
|
+
fail_format("MATRIX_DIM axis dimensions mismatch") unless [matrix[0], matrix[1] || 1] == dimensions
|
|
301
|
+
end
|
|
302
|
+
axes.each_with_index do |axis, i|
|
|
303
|
+
next unless axis.values.first == "COM_AXIS"
|
|
304
|
+
reference = option(axis, "AXIS_PTS_REF")&.first || fail_format("Missing AXIS_PTS_REF")
|
|
305
|
+
count = shared_axis(reference, []).xdim
|
|
306
|
+
fail_format("Shared axis count exceeds maximum") if count > dimensions[i]
|
|
307
|
+
dimensions[i] = count
|
|
308
|
+
end
|
|
309
|
+
data = deposit(block, record, address, dimensions, ascii: type == "ASCII")
|
|
310
|
+
raw = data["FNC_VALUES"] || fail_format("Missing FNC_VALUES")
|
|
311
|
+
meta = metadata(block).merge(unit: unit(block, conversion))
|
|
312
|
+
return Ecu::Festwert.new(**meta, value: raw.split("\0", 2).first || "") if type == "ASCII"
|
|
313
|
+
values = converted(block, raw, conversion)
|
|
314
|
+
case type
|
|
315
|
+
when "VALUE" then Ecu::Festwert.new(**meta, value: values.first)
|
|
316
|
+
when "VAL_BLK"
|
|
317
|
+
Ecu::Festwerteblock.new(**meta, xdim: dimensions[0], ydim: dimensions[1],
|
|
318
|
+
value: rows(values, dimensions, data["direction"]))
|
|
319
|
+
else
|
|
320
|
+
xvalue, xunit = axis_values(axes[0], dimensions[0], data["AXIS_PTS_X"])
|
|
321
|
+
meta.merge!(xdim: dimensions[0], xvalue: xvalue, xunit: xunit)
|
|
322
|
+
if type == "CURVE"
|
|
323
|
+
Ecu::Kennlinie.new(**meta, value: values)
|
|
324
|
+
else
|
|
325
|
+
yvalue, yunit = axis_values(axes[1], dimensions[1], data["AXIS_PTS_Y"])
|
|
326
|
+
Ecu::Kennfeld.new(**meta, ydim: dimensions[1], yvalue: yvalue, yunit: yunit,
|
|
327
|
+
value: rows(values, dimensions, data["direction"]))
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "a2l_parser"
|
|
2
2
|
|
|
3
3
|
class Ecu
|
|
4
4
|
class LabelList
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
str.gsub!(%r{/\*.*?\*/}, "")
|
|
9
|
-
labels = str.scan(A2LREGEXP).map do |name, description|
|
|
10
|
-
# Ugly hack: Create dummy Festwert until better parsing
|
|
11
|
-
# is implemented
|
|
12
|
-
Ecu::Festwert.new(name: name, description: description, value: 0)
|
|
13
|
-
end
|
|
14
|
-
new(labels)
|
|
5
|
+
def self.from_a2l(a2l_text, s19_text = nil, address_map: nil)
|
|
6
|
+
raise ArgumentError, "S19 text is required for A2L calibration values" if s19_text.nil?
|
|
7
|
+
new(A2lParser.new(a2l_text, s19_text, address_map: address_map).call)
|
|
15
8
|
end
|
|
16
9
|
end
|
|
17
10
|
end
|
|
18
|
-
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "a2l_parser_error"
|
|
4
|
+
|
|
5
|
+
class Ecu
|
|
6
|
+
class LabelList
|
|
7
|
+
class S19Image
|
|
8
|
+
TYPES = {
|
|
9
|
+
"UBYTE" => [1, "C", "C"],
|
|
10
|
+
"SBYTE" => [1, "c", "c"],
|
|
11
|
+
"UWORD" => [2, "S<", "S>"],
|
|
12
|
+
"SWORD" => [2, "s<", "s>"],
|
|
13
|
+
"ULONG" => [4, "L<", "L>"],
|
|
14
|
+
"SLONG" => [4, "l<", "l>"],
|
|
15
|
+
"A_UINT64" => [8, "Q<", "Q>"],
|
|
16
|
+
"A_INT64" => [8, "q<", "q>"],
|
|
17
|
+
"FLOAT32_IEEE" => [4, "e", "g"],
|
|
18
|
+
"FLOAT64_IEEE" => [8, "E", "G"]
|
|
19
|
+
}.transform_values(&:freeze).freeze
|
|
20
|
+
|
|
21
|
+
ADDRESS_SIZES = { 0 => 2, 1 => 2, 2 => 3, 3 => 4, 5 => 2, 6 => 3, 7 => 4, 8 => 3, 9 => 2 }.freeze
|
|
22
|
+
private_constant :ADDRESS_SIZES
|
|
23
|
+
|
|
24
|
+
def initialize(text, address_map: nil)
|
|
25
|
+
fail A2lParserError, "S19 text must be a String" unless text.is_a?(String)
|
|
26
|
+
unless text.encoding.ascii_compatible? && text.valid_encoding?
|
|
27
|
+
fail A2lParserError, "Invalid S19 text encoding"
|
|
28
|
+
end
|
|
29
|
+
unless address_map.nil? || address_map.respond_to?(:call)
|
|
30
|
+
fail A2lParserError, "S19 address mapper must be callable"
|
|
31
|
+
end
|
|
32
|
+
@address_map = address_map
|
|
33
|
+
segments = []
|
|
34
|
+
counts = []
|
|
35
|
+
data_records = 0
|
|
36
|
+
text.each_line.with_index(1) do |line, number|
|
|
37
|
+
line = line.strip
|
|
38
|
+
next if line.empty?
|
|
39
|
+
type, address, data = parse_record(line, number)
|
|
40
|
+
case type
|
|
41
|
+
when 1, 2, 3
|
|
42
|
+
data_records += 1
|
|
43
|
+
segments << [address, data, number] unless data.empty?
|
|
44
|
+
when 5, 6
|
|
45
|
+
counts << [address, number]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
counts.each do |count, line|
|
|
49
|
+
fail A2lParserError, "S19 data record count mismatch at line #{line}" unless count == data_records
|
|
50
|
+
end
|
|
51
|
+
fail A2lParserError, "S19 image contains no data" if segments.empty?
|
|
52
|
+
@segments = normalize_segments(segments).freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read(address, length)
|
|
56
|
+
validate_integer(address, "address")
|
|
57
|
+
validate_integer(length, "length")
|
|
58
|
+
address = mapped_address(address)
|
|
59
|
+
return "".b if length.zero?
|
|
60
|
+
|
|
61
|
+
# Find the first segment whose end is beyond the requested address.
|
|
62
|
+
index = @segments.bsearch_index { |start, data| start + data.bytesize > address }
|
|
63
|
+
result = "".b
|
|
64
|
+
remaining = length
|
|
65
|
+
while remaining.positive?
|
|
66
|
+
segment = index && @segments[index]
|
|
67
|
+
unless segment && segment[0] <= address
|
|
68
|
+
fail A2lParserError, "S19 missing bytes at address 0x#{address.to_s(16)}"
|
|
69
|
+
end
|
|
70
|
+
start, data = segment
|
|
71
|
+
offset = address - start
|
|
72
|
+
size = [remaining, data.bytesize - offset].min
|
|
73
|
+
result << data.byteslice(offset, size)
|
|
74
|
+
remaining -= size
|
|
75
|
+
address += size
|
|
76
|
+
index += 1
|
|
77
|
+
end
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def values(address, type, count, byte_order)
|
|
82
|
+
definition = TYPES[type]
|
|
83
|
+
fail A2lParserError, "Unsupported S19 data type #{type.inspect}" unless definition
|
|
84
|
+
unless ["MSB_LAST", "MSB_FIRST"].include?(byte_order)
|
|
85
|
+
fail A2lParserError, "Unsupported S19 byte order #{byte_order.inspect}"
|
|
86
|
+
end
|
|
87
|
+
validate_integer(count, "count")
|
|
88
|
+
size, little, big = definition
|
|
89
|
+
read(address, size * count).unpack("#{byte_order == 'MSB_LAST' ? little : big}*")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def validate_integer(value, name)
|
|
95
|
+
unless value.is_a?(Integer) && value >= 0
|
|
96
|
+
fail A2lParserError, "S19 #{name} must be a nonnegative Integer"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mapped_address(address)
|
|
101
|
+
return address unless @address_map
|
|
102
|
+
mapped = @address_map.call(address)
|
|
103
|
+
validate_integer(mapped, "mapped address")
|
|
104
|
+
mapped
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_record(line, number)
|
|
108
|
+
unless /\AS[0-9][0-9A-Fa-f]+\z/.match?(line) && (line.bytesize - 2).even?
|
|
109
|
+
fail A2lParserError, "Invalid S19 hex record at line #{number}"
|
|
110
|
+
end
|
|
111
|
+
type = line.getbyte(1) - 48
|
|
112
|
+
width = ADDRESS_SIZES[type]
|
|
113
|
+
fail A2lParserError, "Unknown S19 record type S#{type} at line #{number}" unless width
|
|
114
|
+
bytes = [line.byteslice(2..)].pack("H*").bytes
|
|
115
|
+
count = bytes.first
|
|
116
|
+
unless count == bytes.length - 1 && count >= width + 1
|
|
117
|
+
fail A2lParserError, "S19 record count/length mismatch at line #{number}"
|
|
118
|
+
end
|
|
119
|
+
unless (bytes.sum & 255) == 255
|
|
120
|
+
fail A2lParserError, "S19 checksum mismatch at line #{number}"
|
|
121
|
+
end
|
|
122
|
+
address = bytes.slice(1, width).reduce(0) { |value, byte| (value << 8) | byte }
|
|
123
|
+
data = bytes.slice(width + 1, count - width - 1).pack("C*")
|
|
124
|
+
if type >= 5 && !data.empty?
|
|
125
|
+
fail A2lParserError, "Invalid S19 metadata payload length at line #{number}"
|
|
126
|
+
end
|
|
127
|
+
[type, address, data]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def normalize_segments(segments)
|
|
131
|
+
normalized = []
|
|
132
|
+
segments.sort_by! { |start, _, _| start }
|
|
133
|
+
segments.each do |start, data, line|
|
|
134
|
+
index = normalized.bsearch_index { |position, bytes| position + bytes.bytesize > start }
|
|
135
|
+
consumed = 0
|
|
136
|
+
while index && index < normalized.length && consumed < data.bytesize
|
|
137
|
+
previous_start, previous_data = normalized[index]
|
|
138
|
+
overlap = [previous_start + previous_data.bytesize - start, data.bytesize - consumed].min
|
|
139
|
+
unless previous_data.byteslice(start - previous_start, overlap) == data.byteslice(consumed, overlap)
|
|
140
|
+
fail A2lParserError, "Conflicting S19 overlap at address 0x#{start.to_s(16)}, line #{line}"
|
|
141
|
+
end
|
|
142
|
+
start += overlap
|
|
143
|
+
consumed += overlap
|
|
144
|
+
index += 1
|
|
145
|
+
end
|
|
146
|
+
data = data.byteslice(consumed..) if consumed.positive?
|
|
147
|
+
normalized << [start, data.freeze].freeze unless data.empty?
|
|
148
|
+
end
|
|
149
|
+
normalized
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -19,10 +19,12 @@ class Ecu
|
|
|
19
19
|
class LabelList
|
|
20
20
|
include Enumerable
|
|
21
21
|
|
|
22
|
-
def self.from_file(file_path)
|
|
22
|
+
def self.from_file(file_path, s19_path: nil, address_map: nil)
|
|
23
23
|
case File.extname(file_path)
|
|
24
24
|
when ".lab" then self.from_lab(File.read_encoded(file_path))
|
|
25
|
-
when ".a2l"
|
|
25
|
+
when ".a2l"
|
|
26
|
+
raise ArgumentError, "S19 path is required for A2L calibration values" if s19_path.nil?
|
|
27
|
+
self.from_a2l(File.read_encoded(file_path), File.read_encoded(s19_path), address_map: address_map)
|
|
26
28
|
when ".dcm" then self.from_dcm(File.read_encoded(file_path))
|
|
27
29
|
else fail "Unknown file extension: #{file_path}!"
|
|
28
30
|
end
|
data/lib/ecu/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: automotive-ecu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Mueller
|
|
@@ -96,7 +96,12 @@ files:
|
|
|
96
96
|
- lib/ecu.rb
|
|
97
97
|
- lib/ecu/combined_list.rb
|
|
98
98
|
- lib/ecu/combined_list_comparison.rb
|
|
99
|
+
- lib/ecu/interfaces/a2l/a2l_conversion.rb
|
|
100
|
+
- lib/ecu/interfaces/a2l/a2l_document.rb
|
|
101
|
+
- lib/ecu/interfaces/a2l/a2l_parser.rb
|
|
102
|
+
- lib/ecu/interfaces/a2l/a2l_parser_error.rb
|
|
99
103
|
- lib/ecu/interfaces/a2l/label_list.rb
|
|
104
|
+
- lib/ecu/interfaces/a2l/s19_image.rb
|
|
100
105
|
- lib/ecu/interfaces/a2l/signal_list.rb
|
|
101
106
|
- lib/ecu/interfaces/dbc/signal_list.rb
|
|
102
107
|
- lib/ecu/interfaces/dcm/dcm_parser.rb
|