automotive-ecu 0.3.5 → 0.4.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/dcm/dcm_parser.rb +113 -192
- data/lib/ecu/interfaces/dcm/dcm_parser_error.rb +9 -6
- data/lib/ecu/interfaces/dcm/dcm_scanner.rb +152 -0
- data/lib/ecu/interfaces/dcm/festwert.rb +2 -0
- data/lib/ecu/interfaces/dcm/festwerteblock.rb +2 -0
- data/lib/ecu/interfaces/dcm/kennfeld.rb +2 -0
- data/lib/ecu/interfaces/dcm/kennlinie.rb +2 -0
- data/lib/ecu/interfaces/dcm/label.rb +2 -0
- data/lib/ecu/interfaces/dcm/label_list.rb +3 -1
- data/lib/ecu/interfaces/dcm/stuetzstellenverteilung.rb +2 -0
- data/lib/ecu/version.rb +1 -1
- metadata +3 -3
- data/lib/ecu/interfaces/dcm/dcm_lexer.rb +0 -90
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06356ef10ff5e9387768bcef5b8f980a233263adf609778247d869bdc30f5eec
|
|
4
|
+
data.tar.gz: 05f73e206a0661aee679be4b5e0c3007129f8ba428cb40ad3912fd1bf013164f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b334f982850cacd8374d235bf21e7fb6547cf2534b2aa11c1cea70c794c6f815f136b3500d7e5c73034ebf92e18b3ad16238d858879db1df96397775c649f86b
|
|
7
|
+
data.tar.gz: d68b943480f1ff35c9705c0667c277501d67472b9aec4cfd57235edf8eb485ad4c28327598dfb280b17a2d0a6c63b79a62526d7a42b95b0dd51e9cd93385cdd6
|
|
@@ -1,238 +1,159 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "dcm_parser_error"
|
|
5
|
-
require "set"
|
|
3
|
+
require_relative "dcm_scanner"
|
|
6
4
|
|
|
7
5
|
class Ecu
|
|
8
6
|
class LabelList
|
|
7
|
+
# Recursive-descent parser for DCM 2.0:
|
|
8
|
+
#
|
|
9
|
+
# file := comment* HEADER (comment | functions | label)*
|
|
10
|
+
# functions := FUNKTIONEN (FKT ...)* END
|
|
11
|
+
# label := KIND name dimensions property* END
|
|
12
|
+
# property := KEYWORD value* EOL
|
|
13
|
+
#
|
|
14
|
+
# Comments before the header are #headers, those after it #subheaders.
|
|
9
15
|
class DcmParser
|
|
16
|
+
LABEL_KINDS = {
|
|
17
|
+
"FESTWERT" => Festwert,
|
|
18
|
+
"FESTWERTEBLOCK" => Festwerteblock,
|
|
19
|
+
"KENNLINIE" => Kennlinie,
|
|
20
|
+
"GRUPPENKENNLINIE" => Gruppenkennlinie,
|
|
21
|
+
"FESTKENNLINIE" => Festkennlinie,
|
|
22
|
+
"KENNFELD" => Kennfeld,
|
|
23
|
+
"GRUPPENKENNFELD" => Gruppenkennfeld,
|
|
24
|
+
"FESTKENNFELD" => Festkennfeld,
|
|
25
|
+
"STUETZSTELLENVERTEILUNG" => Stuetzstellenverteilung,
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
FUNCTIONS = "FUNKTIONEN"
|
|
29
|
+
FUNCTION = "FKT"
|
|
30
|
+
END_LABEL = "END"
|
|
31
|
+
|
|
32
|
+
# keyword => [label attribute, value reader of DcmScanner]
|
|
33
|
+
PROPERTIES = {
|
|
34
|
+
"LANGNAME" => [:description, :text],
|
|
35
|
+
"FUNKTION" => [:function, :identifier],
|
|
36
|
+
"DISPLAYNAME" => [nil, :identifier], # repeats the label name
|
|
37
|
+
"EINHEIT_X" => [:xunit, :text],
|
|
38
|
+
"EINHEIT_Y" => [:yunit, :text],
|
|
39
|
+
"EINHEIT_W" => [:unit, :text],
|
|
40
|
+
"ST/X" => [:xvalue, :numbers],
|
|
41
|
+
"ST/Y" => [:yvalue, :numbers],
|
|
42
|
+
"WERT" => [:value, :numbers],
|
|
43
|
+
"ST_TX/X" => [:xvalue, :texts],
|
|
44
|
+
"ST_TX/Y" => [:yvalue, :texts],
|
|
45
|
+
"TEXT" => [:value, :texts],
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Readers of values that may continue on following lines.
|
|
49
|
+
LIST_READERS = %i[numbers texts].freeze
|
|
50
|
+
|
|
51
|
+
attr_reader :headers, :subheaders, :labels
|
|
10
52
|
|
|
11
|
-
ARY_PROPERTIES = Set.new([:xvalue, :yvalue, :value]).freeze
|
|
12
|
-
KEY_MAPPING = {
|
|
13
|
-
"ST/X": :xvalue,
|
|
14
|
-
"ST/Y": :yvalue,
|
|
15
|
-
"WERT": :value,
|
|
16
|
-
"ST_TX/X": :xvalue,
|
|
17
|
-
"ST_TX/Y": :yvalue,
|
|
18
|
-
"TEXT": :value,
|
|
19
|
-
"FUNKTION": :function,
|
|
20
|
-
"DISPLAYNAME": :displayname,
|
|
21
|
-
"EINHEIT_X": :xunit,
|
|
22
|
-
"EINHEIT_Y": :yunit,
|
|
23
|
-
"EINHEIT_W": :unit,
|
|
24
|
-
"LANGNAME": :description,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
attr_reader :lexer, :state, :labels, :headers, :subheaders
|
|
28
53
|
def initialize(doc)
|
|
29
|
-
@
|
|
30
|
-
@state = :PRE_HEADER
|
|
31
|
-
@labels = []
|
|
54
|
+
@scanner = DcmScanner.new(doc)
|
|
32
55
|
@headers = []
|
|
33
56
|
@subheaders = []
|
|
34
|
-
@
|
|
35
|
-
reset_label!
|
|
57
|
+
@labels = []
|
|
36
58
|
end
|
|
37
59
|
|
|
38
60
|
def call
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
case tok
|
|
44
|
-
when :HEADER then next_state(:POST_HEADER)
|
|
45
|
-
when :COMMENT then add_header(lexer.token_value)
|
|
46
|
-
when :NEWLINE then # noop
|
|
47
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
48
|
-
end
|
|
49
|
-
when :POST_HEADER
|
|
50
|
-
case tok
|
|
51
|
-
when :FUNKTIONEN then next_state(:FUNCTIONS_CONTENT)
|
|
52
|
-
when :FESTWERT then start_label(Festwert)
|
|
53
|
-
when :FESTWERTEBLOCK then start_label(Festwerteblock)
|
|
54
|
-
when :KENNLINIE then start_label(Kennlinie)
|
|
55
|
-
when :GRUPPENKENNLINIE then start_label(Gruppenkennlinie)
|
|
56
|
-
when :FESTKENNLINIE then start_label(Festkennlinie)
|
|
57
|
-
when :KENNFELD then start_label(Kennfeld)
|
|
58
|
-
when :GRUPPENKENNFELD then start_label(Gruppenkennfeld)
|
|
59
|
-
when :FESTKENNFELD then start_label(Festkennfeld)
|
|
60
|
-
when :STUETZSTELLENVERTEILUNG then start_label(Stuetzstellenverteilung)
|
|
61
|
-
when :COMMENT then add_subheader(lexer.token_value)
|
|
62
|
-
when :NEWLINE then # noop
|
|
63
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
64
|
-
end
|
|
65
|
-
when :FUNCTIONS_CONTENT
|
|
66
|
-
case tok
|
|
67
|
-
when :FKT then next_state(:FUNCTION_CONTENT)
|
|
68
|
-
when :NEWLINE then # noop
|
|
69
|
-
when :END then next_state(:POST_HEADER)
|
|
70
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
71
|
-
end
|
|
72
|
-
when :FUNCTION_CONTENT
|
|
73
|
-
case tok
|
|
74
|
-
when :IDENTIFIER then # append that shit?
|
|
75
|
-
when :QUOTED_TEXT then # append that shit as well?
|
|
76
|
-
when :NEWLINE then next_state(:FUNCTIONS_CONTENT)
|
|
77
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
78
|
-
end
|
|
79
|
-
when :LABEL_HEADLINE
|
|
80
|
-
case tok
|
|
81
|
-
when :IDENTIFIER then add_name(lexer.token_value)
|
|
82
|
-
when :DIMENSIONS_SEP then # noop, order handled by #add_dimension
|
|
83
|
-
when :INT then add_dimension(lexer.token_value.to_i)
|
|
84
|
-
when :NEWLINE then next_state(:LABEL_CONTENT)
|
|
85
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
86
|
-
end
|
|
87
|
-
when :LABEL_CONTENT
|
|
88
|
-
case tok
|
|
89
|
-
when :WERT then start_property(:WERT, :NUMERIC_PROPERTY)
|
|
90
|
-
when :TEXT then start_property(:TEXT, :TEXT_PROPERTY)
|
|
91
|
-
when :EINHEIT_X then start_property(:EINHEIT_X, :TEXT_PROPERTY)
|
|
92
|
-
when :EINHEIT_Y then start_property(:EINHEIT_Y, :TEXT_PROPERTY)
|
|
93
|
-
when :EINHEIT_W then start_property(:EINHEIT_W, :TEXT_PROPERTY)
|
|
94
|
-
when :LANGNAME then start_property(:LANGNAME, :TEXT_PROPERTY)
|
|
95
|
-
when :FUNKTION then start_property(:FUNKTION, :ID_PROPERTY)
|
|
96
|
-
when :DISPLAYNAME then start_property(:DISPLAYNAME, :ID_PROPERTY)
|
|
97
|
-
when :END then labels << finish_label
|
|
98
|
-
when :"ST/X" then start_property(:"ST/X", :NUMERIC_PROPERTY)
|
|
99
|
-
when :"ST/Y" then start_property(:"ST/Y", :NUMERIC_PROPERTY)
|
|
100
|
-
when :"ST_TX/X" then start_property(:"ST_TX/X", :TEXT_PROPERTY)
|
|
101
|
-
when :"ST_TX/Y" then start_property(:"ST_TX/Y", :TEXT_PROPERTY)
|
|
102
|
-
when :NEWLINE then # noop, DCM badly formatted
|
|
103
|
-
when :COMMENT then # noop, some programs add SST in comments
|
|
104
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
105
|
-
end
|
|
106
|
-
when :NUMERIC_PROPERTY
|
|
107
|
-
case tok
|
|
108
|
-
when :FLOAT then append_property(lexer.token_value.to_f)
|
|
109
|
-
when :INT then append_property(lexer.token_value.to_i)
|
|
110
|
-
when :NEWLINE then finish_property
|
|
111
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
112
|
-
end
|
|
113
|
-
when :TEXT_PROPERTY
|
|
114
|
-
case tok
|
|
115
|
-
when :QUOTED_TEXT then append_property(lexer.quoted_value)
|
|
116
|
-
when :NEWLINE then finish_property
|
|
117
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
118
|
-
end
|
|
119
|
-
when :ID_PROPERTY
|
|
120
|
-
case tok
|
|
121
|
-
when :IDENTIFIER then append_property(lexer.token_value)
|
|
122
|
-
when :NEWLINE then finish_property
|
|
123
|
-
else raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{@state})", lexer)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
rescue DcmParserError
|
|
127
|
-
raise
|
|
128
|
-
rescue StandardError => e
|
|
129
|
-
raise DcmParserError.new("#{e.message} on #{e.backtrace.first} (state: #{@state})", lexer)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
fail_on_truncation!
|
|
61
|
+
read_comments(headers)
|
|
62
|
+
@scanner.header or raise @scanner.error("Expected #{DcmScanner::HEADER}")
|
|
63
|
+
|
|
64
|
+
read_blocks
|
|
133
65
|
[labels, headers, subheaders]
|
|
134
66
|
end
|
|
135
67
|
|
|
136
|
-
|
|
137
|
-
def fail_on_truncation!
|
|
138
|
-
return if %i[PRE_HEADER POST_HEADER].include?(@state)
|
|
68
|
+
private
|
|
139
69
|
|
|
140
|
-
|
|
141
|
-
|
|
70
|
+
def read_comments(into)
|
|
71
|
+
loop do
|
|
72
|
+
@scanner.skip_blank_lines
|
|
73
|
+
comment = @scanner.comment or return
|
|
142
74
|
|
|
143
|
-
|
|
144
|
-
if tok == :UNKNOWN_CHAR
|
|
145
|
-
"UNKNOWN_CHAR: #{lexer.token_value}"
|
|
146
|
-
else
|
|
147
|
-
tok
|
|
75
|
+
into << comment
|
|
148
76
|
end
|
|
149
77
|
end
|
|
150
78
|
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
79
|
+
def read_blocks
|
|
80
|
+
loop do
|
|
81
|
+
read_comments(subheaders)
|
|
82
|
+
return if @scanner.at_end?
|
|
156
83
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def add_subheader(str)
|
|
162
|
-
@subheaders << str[1..].strip
|
|
163
|
-
end
|
|
84
|
+
start = @scanner.pos
|
|
85
|
+
keyword = @scanner.keyword
|
|
86
|
+
next skip_functions if keyword == FUNCTIONS
|
|
164
87
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@constructor = constructor
|
|
88
|
+
kind = LABEL_KINDS[keyword] or raise @scanner.error("Unexpected #{keyword || "text"}", start)
|
|
89
|
+
labels << read_label(kind, start)
|
|
168
90
|
end
|
|
169
91
|
end
|
|
170
92
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@constructor.new(**@properties).tap { reset_label! }
|
|
175
|
-
end
|
|
176
|
-
end
|
|
93
|
+
# The function list is not used, but must be well-formed.
|
|
94
|
+
def skip_functions
|
|
95
|
+
@scanner.end_of_line
|
|
177
96
|
|
|
178
|
-
|
|
179
|
-
|
|
97
|
+
loop do
|
|
98
|
+
@scanner.skip_noise
|
|
99
|
+
at = @scanner.pos
|
|
100
|
+
keyword = @scanner.keyword
|
|
180
101
|
|
|
181
|
-
|
|
102
|
+
case keyword
|
|
103
|
+
when END_LABEL then return @scanner.end_of_line
|
|
104
|
+
when FUNCTION then @scanner.skip_line
|
|
105
|
+
else raise @scanner.error("Unexpected #{keyword || "end of file"} in #{FUNCTIONS}", at)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
182
108
|
end
|
|
183
109
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
110
|
+
def read_label(kind, start)
|
|
111
|
+
props = { name: @scanner.name }
|
|
112
|
+
props.merge!(@scanner.dimensions)
|
|
187
113
|
|
|
188
|
-
|
|
189
|
-
@
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
end
|
|
114
|
+
loop do
|
|
115
|
+
@scanner.skip_noise
|
|
116
|
+
at = @scanner.pos
|
|
117
|
+
keyword = @scanner.keyword or raise @scanner.error(missing_property_message, at)
|
|
118
|
+
break @scanner.end_of_line if keyword == END_LABEL
|
|
194
119
|
|
|
195
|
-
|
|
196
|
-
next_state(newstate) do
|
|
197
|
-
@key = KEY_MAPPING[key]
|
|
198
|
-
@is_ary = ARY_PROPERTIES.include?(@key)
|
|
199
|
-
if @is_ary
|
|
200
|
-
@properties[@key] ||= []
|
|
201
|
-
@value = @properties[@key] # alias: append_property writes directly to target
|
|
202
|
-
else
|
|
203
|
-
@value = nil
|
|
204
|
-
end
|
|
120
|
+
read_property(keyword, props, at)
|
|
205
121
|
end
|
|
122
|
+
|
|
123
|
+
build(kind, props, start)
|
|
206
124
|
end
|
|
207
125
|
|
|
208
|
-
def
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
else
|
|
212
|
-
fail "Multiple definitions for #{@key}" if @value
|
|
126
|
+
def read_property(keyword, props, at)
|
|
127
|
+
attribute, reader = PROPERTIES[keyword]
|
|
128
|
+
raise @scanner.error("Unknown property #{keyword}", at) unless reader
|
|
213
129
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
end
|
|
130
|
+
value = @scanner.public_send(reader)
|
|
131
|
+
return unless attribute
|
|
217
132
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
next if @is_ary # values already in @properties[@key] via @value alias
|
|
133
|
+
store(props, attribute, reader, value, at)
|
|
134
|
+
end
|
|
221
135
|
|
|
222
|
-
|
|
223
|
-
|
|
136
|
+
# Lists such as ST/X may span several lines, single values may not.
|
|
137
|
+
def store(props, attribute, reader, value, at)
|
|
138
|
+
if LIST_READERS.include?(reader)
|
|
139
|
+
(props[attribute] ||= []).concat(value)
|
|
140
|
+
return
|
|
224
141
|
end
|
|
142
|
+
|
|
143
|
+
raise @scanner.error("Duplicate #{attribute}", at) if props.key?(attribute)
|
|
144
|
+
|
|
145
|
+
props[attribute] = value
|
|
225
146
|
end
|
|
226
147
|
|
|
227
|
-
def
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@
|
|
231
|
-
@is_ary = false
|
|
232
|
-
@value = nil
|
|
148
|
+
def build(kind, props, start)
|
|
149
|
+
kind.new(**props)
|
|
150
|
+
rescue ArgumentError => e
|
|
151
|
+
raise @scanner.error("#{props[:name]}: #{e.message.lines.first.strip}", start)
|
|
233
152
|
end
|
|
234
153
|
|
|
154
|
+
def missing_property_message
|
|
155
|
+
@scanner.at_end? ? "Unexpected end of file" : "Expected a property or #{END_LABEL}"
|
|
156
|
+
end
|
|
235
157
|
end
|
|
236
|
-
|
|
237
158
|
end
|
|
238
159
|
end
|
|
@@ -4,21 +4,24 @@ class Ecu
|
|
|
4
4
|
CTXLENGHT = 5
|
|
5
5
|
|
|
6
6
|
attr_reader :doc, :lineno
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
# offset: byte position in doc where parsing failed
|
|
9
|
+
def initialize(msg, doc, offset)
|
|
8
10
|
@msg = msg
|
|
9
|
-
@doc =
|
|
10
|
-
@lineno =
|
|
11
|
+
@doc = doc
|
|
12
|
+
@lineno = doc.byteslice(0, offset).count("\n") + 1
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def message = @msg
|
|
14
16
|
def context
|
|
15
|
-
|
|
16
|
-
.lines[ctx_startline..ctx_endline]
|
|
17
|
+
lines[ctx_startline..ctx_endline]
|
|
17
18
|
.each
|
|
18
19
|
.with_index(ctx_startline + 1)
|
|
19
20
|
.map { |line, n| present(line, n, n == lineno) }
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
def lines = @lines ||= doc.lines
|
|
24
|
+
|
|
22
25
|
def present(line, n, highlight)
|
|
23
26
|
case highlight
|
|
24
27
|
when true then fmt(n) + " => | " + line
|
|
@@ -27,7 +30,7 @@ class Ecu
|
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def ctx_startline = [0, lineno - CTXLENGHT].max
|
|
30
|
-
def ctx_endline = @endline ||= [
|
|
33
|
+
def ctx_endline = @endline ||= [lines.size - 1, lineno + CTXLENGHT].min
|
|
31
34
|
def fmt_str = "%#{(ctx_endline + 1).to_s.length}d"
|
|
32
35
|
def fmt(n) = fmt_str % n
|
|
33
36
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
require_relative "dcm_parser_error"
|
|
5
|
+
|
|
6
|
+
class Ecu
|
|
7
|
+
class LabelList
|
|
8
|
+
# Reads DCM text one construct at a time. Each reader consumes what it
|
|
9
|
+
# returns, including the line break, and raises DcmParserError when the
|
|
10
|
+
# input is malformed.
|
|
11
|
+
#
|
|
12
|
+
# Values must be separated by blanks, also from their keyword:
|
|
13
|
+
#
|
|
14
|
+
# WERT 1 2.5 ok
|
|
15
|
+
# WERT-1 rejected
|
|
16
|
+
# ST/X 1.5.2 rejected
|
|
17
|
+
class DcmScanner
|
|
18
|
+
DIMENSION_NAMES = %i[xdim ydim].freeze
|
|
19
|
+
DIMENSION_SEP = "@"
|
|
20
|
+
MAX_DIMENSIONS = DIMENSION_NAMES.size
|
|
21
|
+
|
|
22
|
+
HEADER = "KONSERVIERUNG_FORMAT 2.0"
|
|
23
|
+
LINE_END = /(?:\r\n|\n|\r)/
|
|
24
|
+
EOL = /[ \t]*(?:\r\n|\n|\r|\z)/
|
|
25
|
+
BLANK_LINES = /(?:[ \t]*#{LINE_END})*(?:[ \t]+\z)?/
|
|
26
|
+
NOISE = /(?:[ \t]*(?:\*[^\r\n]*)?#{LINE_END})*/
|
|
27
|
+
COMMENT = /[ \t]*\*([^\r\n]*)(?:\r\n|\n|\r|\z)/
|
|
28
|
+
KEYWORD = %r{[ \t]*([A-Z][A-Z_/]*)}
|
|
29
|
+
IDENTIFIER = /[ \t]+([A-Za-z][A-Za-z0-9_.\[\]]*)/
|
|
30
|
+
QUOTED = /[ \t]+"([^"]*)"/
|
|
31
|
+
REST_OF_LINE = /[^\r\n]*/
|
|
32
|
+
NUMBER_CHARS = /\A[-+.eE0-9 \t]*\z/
|
|
33
|
+
TRAILING_DOT = /\A[-+]?[0-9]+\.\z/ # "9." is a valid DCM number, Ruby disagrees
|
|
34
|
+
|
|
35
|
+
attr_reader :doc
|
|
36
|
+
|
|
37
|
+
def initialize(doc)
|
|
38
|
+
@doc = doc
|
|
39
|
+
@s = StringScanner.new(doc)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pos = @s.pos
|
|
43
|
+
def at_end? = @s.eos?
|
|
44
|
+
|
|
45
|
+
def error(message, at = pos)
|
|
46
|
+
DcmParserError.new(message, doc, at)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def header
|
|
50
|
+
@s.skip(HEADER) or return false
|
|
51
|
+
|
|
52
|
+
end_of_line
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def skip_blank_lines
|
|
57
|
+
@s.skip(BLANK_LINES)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Blank and comment lines, which carry no data inside a label.
|
|
61
|
+
def skip_noise
|
|
62
|
+
@s.skip(NOISE)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Text of the next comment line without the leading "*", or nil.
|
|
66
|
+
def comment
|
|
67
|
+
@s.skip(COMMENT) ? @s[1].strip : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Upper-case word such as WERT, ST_TX/X or END, or nil.
|
|
71
|
+
def keyword
|
|
72
|
+
@s.skip(KEYWORD) ? @s[1] : nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def end_of_line
|
|
76
|
+
return if @s.skip(EOL)
|
|
77
|
+
|
|
78
|
+
raise error("Unexpected #{@s.rest.lines.first.strip.inspect}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def skip_line
|
|
82
|
+
@s.skip(REST_OF_LINE)
|
|
83
|
+
end_of_line
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Label name, which is followed by the dimensions on the same line.
|
|
87
|
+
def name
|
|
88
|
+
@s.skip(IDENTIFIER) or raise error("Expected an identifier")
|
|
89
|
+
@s[1]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def identifier
|
|
93
|
+
value = name
|
|
94
|
+
end_of_line
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Exactly one quoted text, e.g. ` "kg"`.
|
|
99
|
+
def text
|
|
100
|
+
at = pos
|
|
101
|
+
values = texts
|
|
102
|
+
raise error("Expected one quoted text", at) unless values.size == 1
|
|
103
|
+
|
|
104
|
+
values.first
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Any number of quoted texts, e.g. ` "a" "b"`.
|
|
108
|
+
def texts
|
|
109
|
+
values = []
|
|
110
|
+
values << @s[1] while @s.skip(QUOTED)
|
|
111
|
+
end_of_line
|
|
112
|
+
values
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Any number of integers and floats, e.g. ` 1 2.5 -3e2`.
|
|
116
|
+
def numbers
|
|
117
|
+
at = pos
|
|
118
|
+
line = @s.scan(REST_OF_LINE)
|
|
119
|
+
|
|
120
|
+
unless line.empty? || line.start_with?(" ", "\t")
|
|
121
|
+
raise error("Whitespace required before value", at)
|
|
122
|
+
end
|
|
123
|
+
raise error("Invalid number in #{line.strip.inspect}", at) unless line.match?(NUMBER_CHARS)
|
|
124
|
+
|
|
125
|
+
values = line.split
|
|
126
|
+
values.map! { number(_1) or raise error("Invalid number #{_1.inspect}", at) }
|
|
127
|
+
end_of_line
|
|
128
|
+
values
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Label dimensions as { xdim:, ydim: }, e.g. ` 3 @ 4` or ` 3`.
|
|
132
|
+
def dimensions
|
|
133
|
+
at = pos
|
|
134
|
+
tokens = @s.scan(REST_OF_LINE).split
|
|
135
|
+
tokens.delete(DIMENSION_SEP)
|
|
136
|
+
raise error("Too many dimensions", at) if tokens.size > MAX_DIMENSIONS
|
|
137
|
+
|
|
138
|
+
ints = tokens.map { Integer(_1, 10, exception: false) or raise error("Invalid dimension #{_1.inspect}", at) }
|
|
139
|
+
end_of_line
|
|
140
|
+
DIMENSION_NAMES.first(ints.size).zip(ints).to_h
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def number(token)
|
|
146
|
+
Integer(token, 10, exception: false) ||
|
|
147
|
+
Float(token, exception: false) ||
|
|
148
|
+
(token.to_f if TRAILING_DOT.match?(token))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative "festwert"
|
|
2
4
|
require_relative "festwerteblock"
|
|
3
5
|
require_relative "kennlinie"
|
|
@@ -29,7 +31,7 @@ class Ecu
|
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def to_dcm(indented=false)
|
|
32
|
-
buf =
|
|
34
|
+
buf = +""
|
|
33
35
|
|
|
34
36
|
unless headers.empty?
|
|
35
37
|
headers.each { buf << "* " << _1 << "\n" }
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Mueller
|
|
@@ -99,9 +99,9 @@ files:
|
|
|
99
99
|
- lib/ecu/interfaces/a2l/label_list.rb
|
|
100
100
|
- lib/ecu/interfaces/a2l/signal_list.rb
|
|
101
101
|
- lib/ecu/interfaces/dbc/signal_list.rb
|
|
102
|
-
- lib/ecu/interfaces/dcm/dcm_lexer.rb
|
|
103
102
|
- lib/ecu/interfaces/dcm/dcm_parser.rb
|
|
104
103
|
- lib/ecu/interfaces/dcm/dcm_parser_error.rb
|
|
104
|
+
- lib/ecu/interfaces/dcm/dcm_scanner.rb
|
|
105
105
|
- lib/ecu/interfaces/dcm/festkennfeld.rb
|
|
106
106
|
- lib/ecu/interfaces/dcm/festkennlinie.rb
|
|
107
107
|
- lib/ecu/interfaces/dcm/festwert.rb
|
|
@@ -164,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
164
164
|
- !ruby/object:Gem::Version
|
|
165
165
|
version: '0'
|
|
166
166
|
requirements: []
|
|
167
|
-
rubygems_version: 4.0.
|
|
167
|
+
rubygems_version: 4.0.23
|
|
168
168
|
specification_version: 4
|
|
169
169
|
summary: A simple API for managing automotive ecu signals and labels
|
|
170
170
|
test_files: []
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "strscan"
|
|
4
|
-
|
|
5
|
-
class Ecu
|
|
6
|
-
class LabelList
|
|
7
|
-
class DcmLexer
|
|
8
|
-
attr_reader :doc
|
|
9
|
-
def initialize(doc)
|
|
10
|
-
@doc = doc
|
|
11
|
-
@scan = StringScanner.new(doc)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
KEYWORDS = [
|
|
15
|
-
"FUNKTIONEN", "FESTWERT", "FESTWERTEBLOCK", "KENNLINIE", "GRUPPENKENNLINIE", "FESTKENNLINIE",
|
|
16
|
-
"KENNFELD", "GRUPPENKENNFELD", "FESTKENNFELD", "STUETZSTELLENVERTEILUNG",
|
|
17
|
-
"FKT",
|
|
18
|
-
"ST/X", "ST/Y", "WERT", "ST_TX/X", "ST_TX/Y", "TEXT",
|
|
19
|
-
"FUNKTION",
|
|
20
|
-
"EINHEIT_X", "EINHEIT_Y", "EINHEIT_W",
|
|
21
|
-
"LANGNAME", "DISPLAYNAME",
|
|
22
|
-
"END",
|
|
23
|
-
].freeze
|
|
24
|
-
|
|
25
|
-
HEADER = "KONSERVIERUNG_FORMAT 2.0"
|
|
26
|
-
WHITESPACE = %r{ [ \t]+ }x
|
|
27
|
-
NEWLINE = %r{ \r\n|\n }x
|
|
28
|
-
COMMENT = %r{ ^\*.*$ }x
|
|
29
|
-
DIMENSIONS_SEP = %r{ @ }x
|
|
30
|
-
QUOTED_TEXT = %r{ "[^"]*" }x
|
|
31
|
-
IDENTIFIER = %r{ [A-Za-z][A-Za-z0-9_\.\[\]]* }x
|
|
32
|
-
UNSIGNED_INT = %r{ [0]|[1-9][0-9]* }x
|
|
33
|
-
INT = %r{ [-]?#{UNSIGNED_INT} }x
|
|
34
|
-
FLOAT_EXP = %r{ [eE][+-]?[0-9]+ }x
|
|
35
|
-
FLOAT = %r{
|
|
36
|
-
[-+]?
|
|
37
|
-
(?:
|
|
38
|
-
#{UNSIGNED_INT}?[.][0-9]+#{FLOAT_EXP} | # 1.23e10 or .45e3
|
|
39
|
-
#{UNSIGNED_INT} #{FLOAT_EXP} | # 3e4
|
|
40
|
-
#{UNSIGNED_INT}?[.][0-9]+ | # 7.3 or .50
|
|
41
|
-
#{UNSIGNED_INT} [.][0-9]* # 3.02 or 9.
|
|
42
|
-
)
|
|
43
|
-
}x
|
|
44
|
-
KW_RE = /#{Regexp.union(KEYWORDS.sort)}\b/
|
|
45
|
-
KW_TABLE = Hash[KEYWORDS.map { [_1, _1.upcase.to_sym] }]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def next_token
|
|
49
|
-
# This is a hot path during DCM parsing. Make sure to optimize
|
|
50
|
-
# here are far as possible
|
|
51
|
-
|
|
52
|
-
@scan.skip(WHITESPACE)
|
|
53
|
-
|
|
54
|
-
return if @scan.eos?
|
|
55
|
-
|
|
56
|
-
case @doc.getbyte(@scan.pos)
|
|
57
|
-
when 10, 13 then @scan.skip(NEWLINE) && :NEWLINE # \n \r
|
|
58
|
-
when 34 then @scan.skip(QUOTED_TEXT) && :QUOTED_TEXT # "
|
|
59
|
-
when 42 then @scan.skip(COMMENT) && :COMMENT # *
|
|
60
|
-
when 64 then @scan.skip(DIMENSIONS_SEP) && :DIMENSIONS_SEP # @
|
|
61
|
-
when 43, 45, 46, 48..57 # + - . 0-9
|
|
62
|
-
if @scan.skip(FLOAT) then :FLOAT
|
|
63
|
-
elsif @scan.skip(INT) then :INT
|
|
64
|
-
else @scan.getch; :UNKNOWN_CHAR
|
|
65
|
-
end
|
|
66
|
-
else
|
|
67
|
-
if s = @scan.scan(KW_RE) then KW_TABLE[s]
|
|
68
|
-
elsif @scan.skip(HEADER) then :HEADER
|
|
69
|
-
elsif @scan.skip(IDENTIFIER) then :IDENTIFIER
|
|
70
|
-
else @scan.getch; :UNKNOWN_CHAR
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def token_value
|
|
76
|
-
@scan.matched
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Extracts quoted-text content without surrounding quotes in a single allocation.
|
|
80
|
-
def quoted_value
|
|
81
|
-
@scan.string.byteslice(@scan.pos - @scan.matched_size + 1, @scan.matched_size - 2)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def lineno
|
|
85
|
-
@doc.byteslice(0, @scan.pos).count("\n") +
|
|
86
|
-
(@scan.beginning_of_line? ? 0 : 1)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|