automotive-ecu 0.1.10 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sandbox +16 -0
  3. data/lib/ecu/combined_list.rb +9 -0
  4. data/lib/ecu/interfaces/dcm/dcm_lexer.rb +78 -0
  5. data/lib/ecu/interfaces/dcm/dcm_parser.rb +215 -0
  6. data/lib/ecu/interfaces/dcm/dcm_parser_error.rb +35 -0
  7. data/lib/ecu/interfaces/dcm/festkennfeld.rb +0 -3
  8. data/lib/ecu/interfaces/dcm/festkennlinie.rb +0 -3
  9. data/lib/ecu/interfaces/dcm/festwert.rb +0 -4
  10. data/lib/ecu/interfaces/dcm/festwerteblock.rb +0 -4
  11. data/lib/ecu/interfaces/dcm/gruppenkennfeld.rb +0 -3
  12. data/lib/ecu/interfaces/dcm/gruppenkennlinie.rb +0 -3
  13. data/lib/ecu/interfaces/dcm/kennfeld.rb +0 -4
  14. data/lib/ecu/interfaces/dcm/kennlinie.rb +0 -4
  15. data/lib/ecu/interfaces/dcm/label.rb +2 -20
  16. data/lib/ecu/interfaces/dcm/label_list.rb +11 -56
  17. data/lib/ecu/interfaces/dcm/stuetzstellenverteilung.rb +0 -4
  18. data/lib/ecu/interfaces/json/label_list.rb +9 -0
  19. data/lib/ecu/interfaces/lab/combined_list.rb +1 -1
  20. data/lib/ecu/interfaces/lab/lab_lexer.rb +57 -0
  21. data/lib/ecu/interfaces/lab/lab_parser.rb +119 -48
  22. data/lib/ecu/interfaces/lab/lab_parser_error.rb +38 -0
  23. data/lib/ecu/interfaces/lab/label.rb +0 -14
  24. data/lib/ecu/interfaces/lab/label_list.rb +1 -1
  25. data/lib/ecu/interfaces/lab/signal.rb +0 -8
  26. data/lib/ecu/interfaces/lab/signal_list.rb +1 -1
  27. data/lib/ecu/labels/label.rb +9 -1
  28. data/lib/ecu/labels/label_list.rb +1 -0
  29. data/lib/ecu/version.rb +1 -1
  30. metadata +9 -6
  31. data/lib/ecu/interfaces/dcm/buffer.rb +0 -39
  32. data/lib/ecu/interfaces/dcm/functions.rb +0 -13
  33. data/lib/ecu/interfaces/dcm/malformed_dcm_error.rb +0 -34
  34. data/lib/ecu/interfaces/dcm/property_parser.rb +0 -50
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6ed1425651404fd10b15eca32ce4cf04ed030ba99d7272932c53b4ec10b9fbd
4
- data.tar.gz: '08788f3221f54b377daa533e5911fb4e88aad96b1a5afa772f0e73f570931a89'
3
+ metadata.gz: 75ae19b7bbf77cd45e42cc9e6b5138d19e85101df900fe844e3102bf18ac2c79
4
+ data.tar.gz: ecbc4929534d9245169ca0eaa7518df4fb18a08727b66244f4b6382a053af827
5
5
  SHA512:
6
- metadata.gz: 75d03a13154e6d75c963d2a32b7aacd553112df0cf2105c5bd82d2ceb4cf6ea9ba8cdc916e0e9c1abc610a41e985aba418d9e6ecc506d5cb19edf01c78370b31
7
- data.tar.gz: cdf26bf590bf8f780da6724ca31563f5786580e241619e9c844d423e1496f078d5359e7ecf5868564f8235b600556b96b6b2ec4446a8a90746e521d18496f615
6
+ metadata.gz: ff02d2b04386b8129d249be984934bcda8d53bd5f24057541f36c555152cbda2b008ac9ee1d4bdfec90d67fdcbc2c0455c0eaac2d5df524f44b857d21cad0cf2
7
+ data.tar.gz: fef42ccdfe5947851955aad5fc2655cabd047576268310da2cc1b6606e3c4390f6db91907a21b24710140149dfafd8b74670ac2aa701009a9ba8c9f4625d872b
data/bin/sandbox ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/ecu/combined_list"
4
+
5
+ begin
6
+ list = Ecu::CombinedList.from_file('spec/fixtures/lab/advanced.lab')
7
+ list.labels.each do |item|
8
+ puts item
9
+ end
10
+ list.signals.each do |item|
11
+ puts item.description
12
+ end
13
+ rescue Ecu::LabParserError => e
14
+ puts e.message
15
+ puts e.context
16
+ end
@@ -6,6 +6,15 @@ require_relative "interfaces/lab/combined_list"
6
6
  class Ecu
7
7
  class CombinedList
8
8
 
9
+ def self.from_file(file_path)
10
+ case File.extname(file_path)
11
+ when ".lab" then self.from_lab(File.read_encoded(file_path))
12
+ when ".a2l" then self.from_a2l(File.read_encoded(file_path))
13
+ when ".dbc" then self.from_dbc(File.read_encoded(file_path))
14
+ else fail "Unknown file extension: #{file_path}!"
15
+ end
16
+ end
17
+
9
18
  def initialize(signals=SignalList.new, labels=LabelList.new, headers=[], subheaders=[])
10
19
  @signals = signals
11
20
  @labels = labels
@@ -0,0 +1,78 @@
1
+ require "strscan"
2
+
3
+ class Ecu
4
+ class LabelList
5
+ class DcmLexer
6
+ attr_reader :doc
7
+ def initialize(doc)
8
+ @doc = doc
9
+ @scan = StringScanner.new(doc)
10
+ end
11
+
12
+ KEYWORDS = [
13
+ "FUNKTIONEN", "FESTWERT", "FESTWERTEBLOCK", "KENNLINIE", "GRUPPENKENNLINIE", "FESTKENNLINIE",
14
+ "KENNFELD", "GRUPPENKENNFELD", "FESTKENNFELD", "STUETZSTELLENVERTEILUNG",
15
+ "FKT",
16
+ "ST/X", "ST/Y", "WERT", "ST_TX/X", "ST_TX/Y", "TEXT",
17
+ "FUNKTION",
18
+ "EINHEIT_X", "EINHEIT_Y", "EINHEIT_W",
19
+ "LANGNAME", "DISPLAYNAME",
20
+ "END",
21
+ ].freeze
22
+
23
+ HEADER = "KONSERVIERUNG_FORMAT 2.0"
24
+ WHITESPACE = %r{ [ \t]+ }x
25
+ NEWLINE = %r{ \r\n|\n }x
26
+ COMMENT = %r{ ^\*.*$ }x
27
+ DIMENSIONS_SEP = %r{ @ }x
28
+ QUOTED_TEXT = %r{ "[^"]*" }x
29
+ IDENTIFIER = %r{ [A-Za-z][A-Za-z0-9_\.]* }x
30
+ UNSIGNED_INT = %r{ [0]|[1-9][0-9]* }x
31
+ INT = %r{ [-]?#{UNSIGNED_INT} }x
32
+ FLOAT_EXP = %r{ [eE][+-]?[0-9]+ }x
33
+ FLOAT = %r{
34
+ [-+]?
35
+ (?:
36
+ #{UNSIGNED_INT}?[.][0-9]+#{FLOAT_EXP} | # 1.23e10 or .45e3
37
+ #{UNSIGNED_INT} #{FLOAT_EXP} | # 3e4
38
+ #{UNSIGNED_INT}?[.][0-9]+ | # 7.3 or .50
39
+ #{UNSIGNED_INT} [.][0-9]* # 3.02 or 9.
40
+ )
41
+ }x
42
+ KW_RE = /#{Regexp.union(KEYWORDS.sort)}\b/
43
+ KW_TABLE = Hash[KEYWORDS.map { [_1, _1.upcase.to_sym] }]
44
+
45
+
46
+ def next_token
47
+ @scan.skip(WHITESPACE)
48
+
49
+ return if @scan.eos?
50
+
51
+ case
52
+ when @scan.skip(NEWLINE) then :NEWLINE
53
+ when s = @scan.scan(KW_RE) then KW_TABLE[s]
54
+ when @scan.skip(QUOTED_TEXT) then :QUOTED_TEXT
55
+ when @scan.skip(COMMENT) then :COMMENT
56
+ when @scan.skip(DIMENSIONS_SEP) then :DIMENSIONS_SEP
57
+ when @scan.skip(HEADER) then :HEADER
58
+ when @scan.skip(IDENTIFIER) then :IDENTIFIER
59
+ when @scan.skip(FLOAT) then :FLOAT
60
+ when @scan.skip(INT) then :INT
61
+
62
+ else
63
+ @scan.getch
64
+ :UNKNOWN_CHAR
65
+ end
66
+ end
67
+
68
+ def token_value
69
+ @doc.byteslice(@scan.pos - @scan.matched_size, @scan.matched_size)
70
+ end
71
+
72
+ def lineno
73
+ @doc.byteslice(0, @scan.pos).count("\n") +
74
+ (@scan.beginning_of_line? ? 0 : 1)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,215 @@
1
+ require_relative "dcm_lexer"
2
+ require_relative "dcm_parser_error"
3
+
4
+ class Ecu
5
+ class LabelList
6
+ class DcmParser
7
+
8
+ ARY_PROPERTIES = %i( xvalue yvalue value )
9
+ KEY_MAPPING = {
10
+ "ST/X": :xvalue,
11
+ "ST/Y": :yvalue,
12
+ "WERT": :value,
13
+ "ST_TX/X": :xvalue,
14
+ "ST_TX/Y": :yvalue,
15
+ "TEXT": :value,
16
+ "FUNKTION": :function,
17
+ "DISPLAYNAME": :displayname,
18
+ "EINHEIT_X": :xunit,
19
+ "EINHEIT_Y": :yunit,
20
+ "EINHEIT_W": :unit,
21
+ "LANGNAME": :description,
22
+ }
23
+
24
+ attr_reader :lexer, :state, :labels, :headers, :subheaders
25
+ def initialize(doc)
26
+ @lexer = DcmLexer.new(doc)
27
+ @state = :PRE_HEADER
28
+ @labels = []
29
+ @headers = []
30
+ @subheaders = []
31
+ reset_label!
32
+ end
33
+
34
+ def call
35
+ while tok = lexer.next_token
36
+ begin
37
+ case @state
38
+ in :PRE_HEADER
39
+ case tok
40
+ in :HEADER then next_state(:POST_HEADER)
41
+ in :COMMENT then add_header(lexer.token_value)
42
+ in :NEWLINE then # noop
43
+ end
44
+ in :POST_HEADER
45
+ case tok
46
+ in :FUNKTIONEN then next_state(:FUNCTIONS_CONTENT)
47
+ in :FESTWERT then start_label(Festwert)
48
+ in :FESTWERTEBLOCK then start_label(Festwerteblock)
49
+ in :KENNLINIE then start_label(Kennlinie)
50
+ in :GRUPPENKENNLINIE then start_label(Gruppenkennlinie)
51
+ in :FESTKENNLINIE then start_label(Festkennlinie)
52
+ in :KENNFELD then start_label(Kennfeld)
53
+ in :GRUPPENKENNFELD then start_label(Gruppenkennfeld)
54
+ in :FESTKENNFELD then start_label(Festkennfeld)
55
+ in :STUETZSTELLENVERTEILUNG then start_label(Stuetzstellenverteilung)
56
+ in :COMMENT then add_subheader(lexer.token_value)
57
+ in :NEWLINE then # noop
58
+ end
59
+ in :FUNCTIONS_CONTENT
60
+ case tok
61
+ in :FKT then next_state(:FUNCTION_CONTENT)
62
+ in :NEWLINE then # noop
63
+ in :END then next_state(:POST_HEADER)
64
+ end
65
+ in :FUNCTION_CONTENT
66
+ case tok
67
+ in :IDENTIFIER then # append that shit?
68
+ in :QUOTED_TEXT then # append that shit as well?
69
+ in :NEWLINE then next_state(:FUNCTIONS_CONTENT)
70
+ end
71
+ in :LABEL_HEADLINE
72
+ case tok
73
+ in :IDENTIFIER then add_name(lexer.token_value)
74
+ in :DIMENSIONS_SEP then # noop, order handled by #add_dimension
75
+ in :INT then add_dimension(lexer.token_value.to_i)
76
+ in :NEWLINE then next_state(:LABEL_CONTENT)
77
+ end
78
+ in :LABEL_CONTENT
79
+ case tok
80
+ in :WERT then start_property(:WERT, :NUMERIC_PROPERTY)
81
+ in :TEXT then start_property(:TEXT, :TEXT_PROPERTY)
82
+ in :EINHEIT_X then start_property(:EINHEIT_X, :TEXT_PROPERTY)
83
+ in :EINHEIT_Y then start_property(:EINHEIT_Y, :TEXT_PROPERTY)
84
+ in :EINHEIT_W then start_property(:EINHEIT_W, :TEXT_PROPERTY)
85
+ in :LANGNAME then start_property(:LANGNAME, :TEXT_PROPERTY)
86
+ in :FUNKTION then start_property(:FUNKTION, :ID_PROPERTY)
87
+ in :DISPLAYNAME then start_property(:DISPLAYNAME, :ID_PROPERTY)
88
+ in :END then labels << finish_label
89
+ in :"ST/X" then start_property(:"ST/X", :NUMERIC_PROPERTY)
90
+ in :"ST/Y" then start_property(:"ST/Y", :NUMERIC_PROPERTY)
91
+ in :"ST_TX/X" then start_property(:"ST_TX/X", :TEXT_PROPERTY)
92
+ in :"ST_TX/Y" then start_property(:"ST_TX/Y", :TEXT_PROPERTY)
93
+ in :NEWLINE then # noop, DCM badly formatted
94
+ in :COMMENT then # noop, some programs add SST in comments
95
+ end
96
+ in :NUMERIC_PROPERTY
97
+ case tok
98
+ in :FLOAT then append_property(lexer.token_value.to_f)
99
+ in :INT then append_property(lexer.token_value.to_i)
100
+ in :NEWLINE then finish_property
101
+ end
102
+ in :TEXT_PROPERTY
103
+ case tok
104
+ in :QUOTED_TEXT then append_property(lexer.token_value[1..-2])
105
+ in :NEWLINE then finish_property
106
+ end
107
+ in :ID_PROPERTY
108
+ case tok
109
+ in :IDENTIFIER then append_property(lexer.token_value)
110
+ in :NEWLINE then finish_property
111
+ end
112
+ end
113
+ rescue NoMatchingPatternError => e
114
+ raise DcmParserError.new("Unexpected token #{debug_token(tok)} (state: #{state})", lexer)
115
+ rescue StandardError => e
116
+ raise DcmParserError.new("#{e.message} (state: #{@state})", lexer)
117
+ end
118
+ end
119
+ [labels, headers, subheaders]
120
+ end
121
+
122
+ def debug_token(tok)
123
+ if tok == :UNKNOWN_CHAR
124
+ "UNKNOWN_CHAR: #{lexer.token_value}"
125
+ else
126
+ tok
127
+ end
128
+ end
129
+
130
+ def next_state(newstate)
131
+ @state = newstate
132
+ yield if block_given?
133
+ end
134
+
135
+ def add_header(str)
136
+ @headers << str[1..].strip
137
+ end
138
+
139
+ def add_subheader(str)
140
+ @subheaders << str[1..].strip
141
+ end
142
+
143
+ def start_label(constructor)
144
+ next_state(:LABEL_HEADLINE) do
145
+ @constructor = constructor
146
+ end
147
+ end
148
+
149
+ def finish_label
150
+ next_state(:POST_HEADER) do
151
+ @properties
152
+ .except(:displayname)
153
+ .then { @constructor.new(**_1) }
154
+ .tap { reset_label! }
155
+ end
156
+ end
157
+
158
+ def add_name(str)
159
+ fail "Duplicate name" if @properties.key?(:name)
160
+
161
+ @properties[:name] = str
162
+ end
163
+
164
+ def add_dimension(n)
165
+ fail "Wrong order name/dimensions" unless @properties.key?(:name)
166
+ fail "Too many dimensions" if @properties.key?(:xdim) && @properties.key?(:ydim)
167
+
168
+ if @properties.key?(:xdim)
169
+ @properties[:ydim] = n
170
+ else
171
+ @properties[:xdim] = n
172
+ end
173
+ end
174
+
175
+ def start_property(key, newstate)
176
+ next_state(newstate) do
177
+ @key = KEY_MAPPING[key]
178
+ @value = ARY_PROPERTIES.include?(@key) ? [] : nil
179
+ end
180
+ end
181
+
182
+ def append_property(value)
183
+ # TODO: Check if constructor allows array values
184
+ if ARY_PROPERTIES.include?(@key)
185
+ @value << value
186
+ else
187
+ fail "Multiple definitions for #{@key}" if @value
188
+
189
+ @value = value
190
+ end
191
+ end
192
+
193
+ def finish_property
194
+ next_state(:LABEL_CONTENT) do
195
+ @properties.merge!({ @key => @value }, &method(:merge_property))
196
+ end
197
+ end
198
+
199
+ def merge_property(key, old, new)
200
+ fail "Multiple property lines not allowed for #{key}" unless ARY_PROPERTIES.include?(key)
201
+
202
+ old + new
203
+ end
204
+
205
+ def reset_label!
206
+ @constructor = nil
207
+ @properties = {}
208
+ @key = nil
209
+ @value = nil
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+ end
@@ -0,0 +1,35 @@
1
+ class Ecu
2
+ class DcmParserError < StandardError
3
+
4
+ CTXLENGHT = 5
5
+
6
+ attr_reader :doc, :lineno
7
+ def initialize(msg, lexer)
8
+ @msg = msg
9
+ @doc = lexer.doc
10
+ @lineno = lexer.lineno
11
+ end
12
+
13
+ def message = @msg
14
+ def context
15
+ @doc
16
+ .lines[ctx_startline..ctx_endline]
17
+ .each
18
+ .with_index(ctx_startline + 1)
19
+ .map { |line, n| present(line, n, n == lineno) }
20
+ end
21
+
22
+ def present(line, n, highlight)
23
+ case highlight
24
+ when true then fmt(n) + " => | " + line
25
+ when false then fmt(n) + " | " + line
26
+ end
27
+ end
28
+
29
+ def ctx_startline = [0, lineno - CTXLENGHT].max
30
+ def ctx_endline = @endline ||= [doc.lines.count - 1, lineno + CTXLENGHT].min
31
+ def fmt_str = "%#{(ctx_endline + 1).to_s.length}d"
32
+ def fmt(n) = fmt_str % n
33
+
34
+ end
35
+ end
@@ -2,8 +2,5 @@ require_relative "kennfeld"
2
2
 
3
3
  class Ecu
4
4
  class Festkennfeld < Kennfeld
5
- def self.dcm_header
6
- %r{^FESTKENNFELD\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)\s+(?<ydim>\d+)}
7
- end
8
5
  end
9
6
  end
@@ -2,8 +2,5 @@ require_relative "kennlinie"
2
2
 
3
3
  class Ecu
4
4
  class Festkennlinie < Kennlinie
5
- def self.dcm_header
6
- %r{^FESTKENNLINIE\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)}
7
- end
8
5
  end
9
6
  end
@@ -1,9 +1,5 @@
1
1
  class Ecu
2
2
  class Festwert < Label
3
- def self.dcm_header
4
- %r{FESTWERT\s+(?<name>[A-Za-z0-9\._]+)}
5
- end
6
-
7
3
  def to_dcm(indented=false)
8
4
  fmtstr = indented ? "%-25s%s\n" : "%s %s\n"
9
5
 
@@ -2,10 +2,6 @@ require_relative "../../../core_ext"
2
2
 
3
3
  class Ecu
4
4
  class Festwerteblock < Label
5
- def self.dcm_header
6
- %r{^FESTWERTEBLOCK\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)(?:\s+@\s+(?<ydim>\d+))?}
7
- end
8
-
9
5
  def to_dcm(indented=false)
10
6
  fmtstr = indented ? "%-25s%s %s" : "%s %s %d"
11
7
 
@@ -2,8 +2,5 @@ require_relative "kennfeld"
2
2
 
3
3
  class Ecu
4
4
  class Gruppenkennfeld < Kennfeld
5
- def self.dcm_header
6
- %r{^GRUPPENKENNFELD\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)\s+(?<ydim>\d+)}
7
- end
8
5
  end
9
6
  end
@@ -2,8 +2,5 @@ require_relative "kennlinie"
2
2
 
3
3
  class Ecu
4
4
  class Gruppenkennlinie < Kennlinie
5
- def self.dcm_header
6
- %r{^GRUPPENKENNLINIE\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)}
7
- end
8
5
  end
9
6
  end
@@ -1,9 +1,5 @@
1
1
  class Ecu
2
2
  class Kennfeld < Label
3
- def self.dcm_header
4
- %r{^KENNFELD\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)\s+(?<ydim>\d+)}
5
- end
6
-
7
3
  def to_dcm(indented=false)
8
4
  fmtstr = indented ? "%-25s%s %s %s\n" : "%s %s %d %d\n"
9
5
 
@@ -1,9 +1,5 @@
1
1
  class Ecu
2
2
  class Kennlinie < Label
3
- def self.dcm_header
4
- %r{^KENNLINIE\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)}
5
- end
6
-
7
3
  def to_dcm(indented=false)
8
4
  fmtstr = indented ? "%-25s%s %s\n" : "%s %s %d\n"
9
5
 
@@ -1,25 +1,7 @@
1
- require_relative "property_parser"
2
-
3
1
  class Ecu
4
2
  class Label
5
- def self.from_dcm(input)
6
- lines = case input
7
- in Array then input
8
- in String then input.lines
9
- end
10
- hsh = dcm_header.extract_captures(lines.first)
11
-
12
- hsh[:xdim] = hsh[:xdim].to_i if hsh.key?(:xdim)
13
- hsh[:ydim] = hsh[:ydim].to_i if hsh.key?(:ydim)
14
-
15
- fail "Malformed DCM" if hsh.empty?
16
- fail "Malformed DCM" unless lines.last.match?(/^END$/)
17
-
18
- lines[1..-2]
19
- .map { DcmPropertyParser.call(_1) }
20
- .each { hsh.merge!(_1) { |_, old, new| old + new } }
21
-
22
- new(**hsh)
3
+ def to_dcm
4
+ fail "To be defined by child classes"
23
5
  end
24
6
  end
25
7
  end
@@ -1,4 +1,3 @@
1
- require_relative "functions"
2
1
  require_relative "festwert"
3
2
  require_relative "festwerteblock"
4
3
  require_relative "kennlinie"
@@ -8,69 +7,25 @@ require_relative "kennfeld"
8
7
  require_relative "gruppenkennfeld"
9
8
  require_relative "festkennfeld"
10
9
  require_relative "stuetzstellenverteilung"
11
- require_relative "malformed_dcm_error"
12
- require_relative "buffer"
10
+
11
+ require_relative "dcm_parser"
13
12
 
14
13
  class Ecu
15
14
  class LabelList
16
15
 
17
- DCM_HEADER = "KONSERVIERUNG_FORMAT 2.0"
18
- BLANKLINE_REGEX = /^\s*$/
19
- COMMENT_REGEX = /^\*.*/
16
+ DCM_HEADER = "KONSERVIERUNG_FORMAT 2.0"
20
17
 
21
18
  def self.from_dcm(str)
22
- buffer = DcmBuffer.new
23
- headers = []
24
- subheaders = []
25
- functions = []
26
- labels = {}
27
-
28
- str.each_line.lazy.with_index(1).each do |line, n|
29
- line = normalize_whitespace(line)
30
- case line
31
- when BLANKLINE_REGEX then next
32
- when COMMENT_REGEX
33
- case buffer.header
34
- when :pre then headers << line[1..].strip
35
- when :after then subheaders << line[1..].strip
36
- when :done then # Header time over, do nothing
37
- end
38
- when DCM_HEADER then buffer.header_seen!
39
- when Functions.dcm_header then buffer.start!(Functions, [line])
40
- when Festwert.dcm_header then buffer.start!(Festwert, [line])
41
- when Festwerteblock.dcm_header then buffer.start!(Festwerteblock, [line])
42
- when Kennlinie.dcm_header then buffer.start!(Kennlinie, [line])
43
- when Gruppenkennlinie.dcm_header then buffer.start!(Gruppenkennlinie, [line])
44
- when Festkennlinie.dcm_header then buffer.start!(Festkennlinie, [line])
45
- when Kennfeld.dcm_header then buffer.start!(Kennfeld, [line])
46
- when Gruppenkennfeld.dcm_header then buffer.start!(Gruppenkennfeld, [line])
47
- when Festkennfeld.dcm_header then buffer.start!(Festkennfeld, [line])
48
- when Stuetzstellenverteilung.dcm_header then buffer.start!(Stuetzstellenverteilung, [line])
49
- when "END" then
50
- case obj = buffer.finish!(line)
51
- when Label
52
- fail "Duplicate label #{obj.name}" unless labels[obj.name].nil?
53
-
54
- labels[obj.name] = obj
55
- when Functions
56
- fail "Duplicate functions definition" unless functions.empty?
57
-
58
- functions = obj
59
- else
60
- fail "Unknown object #{obj}"
61
- end
62
- else
63
- buffer.append!(line)
64
- end
65
- rescue StandardError => e
66
- raise MalformedDcmError.new(e.message, n, str), e.message
67
- end
68
-
69
- new(labels.values, headers, subheaders, true)
19
+ DcmParser.new(str)
20
+ .then { _1.call }
21
+ .then { new(*_1, true) }
70
22
  end
71
23
 
72
- def self.normalize_whitespace(line)
73
- line.chomp.rstrip
24
+ def self.from_dcm_partial(str)
25
+ str
26
+ .then { DCM_HEADER + "\n\n" + _1 }
27
+ .then { from_dcm(_1) }
28
+ .first
74
29
  end
75
30
 
76
31
  def to_dcm(indented=false)
@@ -1,9 +1,5 @@
1
1
  class Ecu
2
2
  class Stuetzstellenverteilung < Label
3
- def self.dcm_header
4
- %r{STUETZSTELLENVERTEILUNG\s+(?<name>[A-Za-z0-9\._]+)\s+(?<xdim>\d+)}
5
- end
6
-
7
3
  def to_dcm(indented=false)
8
4
  fmtstr = indented ? "%-25s%s %s\n" : "%s %s %d\n"
9
5
 
@@ -0,0 +1,9 @@
1
+ require "json"
2
+
3
+ class Ecu
4
+ class LabelList
5
+ def to_json
6
+ self.map(&:to_h).to_json
7
+ end
8
+ end
9
+ end
@@ -4,7 +4,7 @@ require_relative "label_list"
4
4
  class Ecu
5
5
  class CombinedList
6
6
  def self.from_lab(str)
7
- signals, labels, headers, subheaders = LabParser.call(str)
7
+ signals, labels, headers, subheaders = LabParser.new(str).call
8
8
  new(signals, labels, headers, subheaders)
9
9
  end
10
10
 
@@ -0,0 +1,57 @@
1
+ require "strscan"
2
+
3
+ class Ecu
4
+ class LabLexer
5
+ attr_reader :doc
6
+ def initialize(doc)
7
+ @doc = doc
8
+ @scan = StringScanner.new(doc)
9
+ end
10
+
11
+ KEYWORDS = [
12
+ "[SETTINGS]",
13
+ "[RAMCELL]",
14
+ "[Label]",
15
+ ].freeze
16
+
17
+ NEWLINE = %r{ \r\n|\n }x
18
+ SETTINGS_HEADER = %r{ \[SETTINGS\]\s*#{NEWLINE} }x
19
+ LABELS_HEADER = %r{ \[Label\]\s*#{NEWLINE} }x
20
+ SIGNALS_HEADER = %r{ \[RAMCELL\]\s*#{NEWLINE} }x
21
+ STRING = %r{ [^\r\n;]+ }x
22
+ WHITESPACE = %r{ [ \t]+ }x
23
+ SEPARATOR = %r{ [;] }x
24
+ COMMENT = %r{ ^[;].*$ }x
25
+
26
+ def next_token(state)
27
+ @scan.skip(WHITESPACE)
28
+
29
+ return if @scan.eos?
30
+
31
+ case
32
+ when @scan.skip(SETTINGS_HEADER) then :SETTINGS_HEADER
33
+ when @scan.skip(LABELS_HEADER) then :LABELS_HEADER
34
+ when @scan.skip("[LABEL]\n") then :LABELS_HEADER
35
+ when @scan.skip(SIGNALS_HEADER) then :SIGNALS_HEADER
36
+ when @scan.skip(STRING) then :STRING
37
+ when @scan.beginning_of_line? && @scan.skip(COMMENT) then :COMMENT
38
+ when @scan.skip(SEPARATOR) then :SEPARATOR
39
+ when @scan.skip(NEWLINE) then :NEWLINE
40
+ when @scan.skip(ANYTHING) then :ANYTHING
41
+
42
+ else
43
+ @scan.getch
44
+ :UNKNOWN_CHAR
45
+ end
46
+ end
47
+
48
+ def token_value
49
+ @doc.byteslice(@scan.pos - @scan.matched_size, @scan.matched_size)
50
+ end
51
+
52
+ def lineno
53
+ @doc.byteslice(0, @scan.pos).count("\n") +
54
+ (@scan.beginning_of_line? ? 0 : 1)
55
+ end
56
+ end
57
+ end
@@ -1,61 +1,132 @@
1
1
  require_relative "../../labels"
2
2
  require_relative "../../signals"
3
3
 
4
- class LabParser
5
- def self.call(str)
6
- lines = str.lines
7
-
8
- [
9
- extract_signals(block_bounded_by(lines, "[RAMCELL]", /\A\[/)),
10
- extract_labels(block_bounded_by(lines, "[Label]", /\A\[/)),
11
- extract_headers(block_bounded_by(lines, 0, "[SETTINGS]")),
12
- extract_headers(block_bounded_by(lines, "[SETTINGS]", /\A\[/)),
13
- ]
14
- end
4
+ require_relative "lab_lexer"
5
+ require_relative "lab_parser_error"
15
6
 
16
- def self.extract_labels(lines)
17
- Ecu::LabelList.new lines.
18
- reject { |l| l.blank? }.
19
- reject { |l| l.start_with?(";") }.
20
- map { |l| Ecu::Label.from_lab(l) }
21
- end
7
+ class Ecu
8
+ class LabParser
22
9
 
23
- def self.extract_signals(lines)
24
- Ecu::SignalList.new lines.
25
- reject { |l| l.blank? }.
26
- reject { |l| l.start_with?(";") }.
27
- map { |l| Ecu::Signal.from_lab(l) }
28
- end
10
+ attr_reader :lexer, :state, :labels, :signals, :headers, :subheaders
11
+ def initialize(doc)
12
+ @lexer = LabLexer.new(doc)
13
+ @state = :NO_SECTION
14
+ @signals = []
15
+ @labels = []
16
+ @headers = []
17
+ @subheaders = []
18
+ reset_entry!
19
+ end
29
20
 
30
- def self.extract_headers(lines)
31
- lines.
32
- select { |l| l.match(/\A; /) }.
33
- map { |l| l[2..-1].chomp }
34
- end
21
+ def reset_entry!
22
+ @properties = {}
23
+ end
35
24
 
36
- def self.block_bounded_by(lines, upper, lower)
37
- upper = case upper
38
- when Integer then upper
39
- when String
40
- return [] unless lines.any? { |l| l.strip.downcase == upper.downcase }
41
- lines.index { |l| l.strip.downcase == upper.downcase } + 1
42
- when Regexp
43
- return [] unless lines.any? { |l| l.match?(upper) }
44
- lines.index { |l| l.match?(upper) } + 1
25
+ def call
26
+ while tok = lexer.next_token(state)
27
+ begin
28
+ case state
29
+ in :NO_SECTION
30
+ case tok
31
+ in :SETTINGS_HEADER then next_state(:SETTINGS)
32
+ in :SIGNALS_HEADER then next_state(:SIGNALS)
33
+ in :LABELS_HEADER then next_state(:LABELS)
34
+ in :COMMENT then add_header(lexer.token_value)
35
+ in :NEWLINE then # noop
36
+ end
37
+ in :SETTINGS
38
+ case tok
39
+ in :SIGNALS_HEADER then next_state(:SIGNALS)
40
+ in :LABELS_HEADER then next_state(:LABELS)
41
+ in :COMMENT then add_subheader(lexer.token_value)
42
+ in :STRING then # save that shit?
43
+ in :SEPARATOR then # now comes the value
44
+ in :NEWLINE then # next setting
45
+ end
46
+ in :LABELS
47
+ case tok
48
+ in :SIGNALS_HEADER then next_state(:SIGNALS)
49
+ in :STRING then start_label(lexer.token_value)
50
+ in :COMMENT then # noop
51
+ in :NEWLINE then # noop
52
+ end
53
+ in :SIGNALS
54
+ case tok
55
+ in :LABELS_HEADER then next_state(:LABELS)
56
+ in :STRING then start_signal(lexer.token_value)
57
+ in :COMMENT then # noop
58
+ in :NEWLINE then # noop
59
+ end
60
+ in :LABEL
61
+ case tok
62
+ in :NEWLINE then labels << finish_label
63
+ in :SEPARATOR then # noop, sometimes used as end separator
45
64
  end
65
+ in :SIGNAL
66
+ case tok
67
+ in :STRING then add_property(lexer.token_value)
68
+ in :SEPARATOR then # noop, next property
69
+ in :NEWLINE then signals << finish_signal
70
+ end
71
+ end
72
+ rescue NoMatchingPatternError => e
73
+ raise LabParserError.new("Unexpected token #{debug_token(tok)} (state: #{state})", lexer)
74
+ rescue StandardError => e
75
+ raise LabParserError.new("#{e.message} (state: #{@state})", lexer)
76
+ end
77
+ end
78
+ [signals, labels, headers, subheaders]
79
+ end
46
80
 
47
- segment = lines[upper..-1]
81
+ def debug_token(tok)
82
+ "#{tok}: \"#{lexer.token_value}\" - #{@properties.inspect}"
83
+ end
48
84
 
49
- lower = case lower
50
- when Integer then lower
51
- when String
52
- return lines[upper..-1] unless segment.any? { |l| l.strip.downcase == lower.downcase }
53
- upper + segment.index { |l| l.strip.downcase == lower.downcase }
54
- when Regexp
55
- return lines[upper..-1] unless segment.any? { |l| l.match?(lower) }
56
- upper + segment.index { |l| l.match?(lower) }
57
- end
85
+ def next_state(newstate)
86
+ @state = newstate
87
+ yield if block_given?
88
+ end
89
+
90
+ def add_header(str)
91
+ @headers << str[1..].strip
92
+ end
93
+
94
+ def add_subheader(str)
95
+ @subheaders << str[1..].strip
96
+ end
97
+
98
+ def start_label(name)
99
+ next_state(:LABEL) do
100
+ @properties[:name] = name
101
+ end
102
+ end
103
+
104
+ def finish_label
105
+ Festwert.new(**@properties, value: nil).tap do
106
+ reset_entry!
107
+ next_state(:LABELS)
108
+ end
109
+ end
110
+
111
+ def start_signal(name)
112
+ next_state(:SIGNAL) do
113
+ @properties[:name] = name
114
+ end
115
+ end
116
+
117
+ def finish_signal
118
+ Signal.new(**@properties).tap do
119
+ reset_entry!
120
+ next_state(:SIGNALS)
121
+ end
122
+ end
58
123
 
59
- lines[upper...lower]
124
+ def add_property(str)
125
+ case @properties
126
+ in { task:, description: } then @properties[:description] << " " << str
127
+ in { task: } then @properties[:description] = str.strip
128
+ in { ** } then @properties[:task] = str.split("&").first
129
+ end
130
+ end
60
131
  end
61
132
  end
@@ -0,0 +1,38 @@
1
+ # TODO: This is exactly the same as the DcmParserError, maybe pull these
2
+ # together?
3
+
4
+ class Ecu
5
+ class LabParserError < StandardError
6
+
7
+ CTXLENGHT = 5
8
+
9
+ attr_reader :doc, :lineno
10
+ def initialize(msg, lexer)
11
+ @msg = msg
12
+ @doc = lexer.doc
13
+ @lineno = lexer.lineno
14
+ end
15
+
16
+ def message = @msg
17
+ def context
18
+ @doc
19
+ .lines[ctx_startline..ctx_endline]
20
+ .each
21
+ .with_index(ctx_startline + 1)
22
+ .map { |line, n| present(line, n, n == lineno) }
23
+ end
24
+
25
+ def present(line, n, highlight)
26
+ case highlight
27
+ when true then fmt(n) + " => | " + line
28
+ when false then fmt(n) + " | " + line
29
+ end
30
+ end
31
+
32
+ def ctx_startline = [0, lineno - CTXLENGHT].max
33
+ def ctx_endline = @endline ||= [doc.lines.count - 1, lineno + CTXLENGHT].min
34
+ def fmt_str = "%#{(ctx_endline + 1).to_s.length}d"
35
+ def fmt(n) = fmt_str % n
36
+
37
+ end
38
+ end
@@ -1,19 +1,5 @@
1
1
  class Ecu
2
2
  class Label
3
-
4
- # This is a silly hack, since the *.lab file doesn't contain any information
5
- # about the label in question (just the name). Subsequently, just initialize
6
- # a Festwert with no value and hope if does the job correctly
7
-
8
- def self.from_lab(line)
9
- name, description = line.strip.split(";")
10
- if description.nil?
11
- Festwert.new(name: name, value: nil)
12
- else
13
- Festwert.new(name: name, value: nil, description: description)
14
- end
15
- end
16
-
17
3
  def to_lab
18
4
  "#{name};#{description}"
19
5
  end
@@ -4,7 +4,7 @@ require_relative "lab_parser"
4
4
  class Ecu
5
5
  class LabelList
6
6
  def self.from_lab(str)
7
- _, labels, headers, subheaders = LabParser.call(str)
7
+ _, labels, headers, subheaders = LabParser.new(str).call
8
8
  new(labels, headers, subheaders)
9
9
  end
10
10
 
@@ -1,13 +1,5 @@
1
1
  class Ecu
2
2
  class Signal
3
- def self.from_lab(line)
4
- name, tasks, comment = line.strip.split(";")
5
- maintask = tasks.split("&").first rescue nil
6
- new(name: name,
7
- task: maintask,
8
- description: comment&.strip)
9
- end
10
-
11
3
  def to_lab
12
4
  "#{name};#{task};#{description}"
13
5
  end
@@ -4,7 +4,7 @@ require_relative "lab_parser"
4
4
  class Ecu
5
5
  class SignalList
6
6
  def self.from_lab(str)
7
- signals, _, headers, subheaders = LabParser.call(str)
7
+ signals, _, headers, subheaders = LabParser.new(str).call
8
8
  new(signals, headers, subheaders)
9
9
  end
10
10
 
@@ -13,7 +13,15 @@ class Ecu
13
13
  attr_reader :name, :function, :description
14
14
 
15
15
  def init_error(message)
16
- fail ArgumentError, "Error creating #{name}: " + message
16
+ fail ArgumentError, "Error creating #{name}: " +
17
+ message + "\n" +
18
+ inspect_instance_vars + "\n\n"
19
+ end
20
+
21
+ def inspect_instance_vars
22
+ instance_variables
23
+ .map { "#{_1} = #{instance_variable_get(_1)}" }
24
+ .join("\n")
17
25
  end
18
26
 
19
27
  def type
@@ -10,6 +10,7 @@ require_relative "../interfaces/dcm/label_list"
10
10
  require_relative "../interfaces/lab/label_list"
11
11
  require_relative "../interfaces/a2l/label_list"
12
12
  require_relative "../interfaces/mfile/label_list"
13
+ require_relative "../interfaces/json/label_list"
13
14
 
14
15
  require "set"
15
16
  require_relative "../../core_ext"
data/lib/ecu/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Ecu
2
- VERSION = "0.1.10"
2
+ VERSION = "0.2.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: automotive-ecu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Mueller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-29 00:00:00.000000000 Z
11
+ date: 2025-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: terminal-table
@@ -97,6 +97,7 @@ files:
97
97
  - bin/pry
98
98
  - bin/rake
99
99
  - bin/rspec
100
+ - bin/sandbox
100
101
  - bin/setup
101
102
  - lib/core_ext.rb
102
103
  - lib/ecu.rb
@@ -105,23 +106,25 @@ files:
105
106
  - lib/ecu/interfaces/a2l/label_list.rb
106
107
  - lib/ecu/interfaces/a2l/signal_list.rb
107
108
  - lib/ecu/interfaces/dbc/signal_list.rb
108
- - lib/ecu/interfaces/dcm/buffer.rb
109
+ - lib/ecu/interfaces/dcm/dcm_lexer.rb
110
+ - lib/ecu/interfaces/dcm/dcm_parser.rb
111
+ - lib/ecu/interfaces/dcm/dcm_parser_error.rb
109
112
  - lib/ecu/interfaces/dcm/festkennfeld.rb
110
113
  - lib/ecu/interfaces/dcm/festkennlinie.rb
111
114
  - lib/ecu/interfaces/dcm/festwert.rb
112
115
  - lib/ecu/interfaces/dcm/festwerteblock.rb
113
- - lib/ecu/interfaces/dcm/functions.rb
114
116
  - lib/ecu/interfaces/dcm/gruppenkennfeld.rb
115
117
  - lib/ecu/interfaces/dcm/gruppenkennlinie.rb
116
118
  - lib/ecu/interfaces/dcm/kennfeld.rb
117
119
  - lib/ecu/interfaces/dcm/kennlinie.rb
118
120
  - lib/ecu/interfaces/dcm/label.rb
119
121
  - lib/ecu/interfaces/dcm/label_list.rb
120
- - lib/ecu/interfaces/dcm/malformed_dcm_error.rb
121
- - lib/ecu/interfaces/dcm/property_parser.rb
122
122
  - lib/ecu/interfaces/dcm/stuetzstellenverteilung.rb
123
+ - lib/ecu/interfaces/json/label_list.rb
123
124
  - lib/ecu/interfaces/lab/combined_list.rb
125
+ - lib/ecu/interfaces/lab/lab_lexer.rb
124
126
  - lib/ecu/interfaces/lab/lab_parser.rb
127
+ - lib/ecu/interfaces/lab/lab_parser_error.rb
125
128
  - lib/ecu/interfaces/lab/label.rb
126
129
  - lib/ecu/interfaces/lab/label_list.rb
127
130
  - lib/ecu/interfaces/lab/signal.rb
@@ -1,39 +0,0 @@
1
- class DcmBuffer
2
- attr_reader :buffer, :header, :constructor
3
- def initialize
4
- @constructor = nil
5
- @header = :pre
6
- @buffer = []
7
- end
8
-
9
- def start!(constructor, buffer)
10
- fail "Nested parameter" unless self.buffer.empty?
11
- fail "Missing DCM header" if self.header == :pre
12
-
13
- @header = :done
14
- @constructor = constructor
15
- @buffer = buffer
16
- end
17
-
18
- def finish!(line)
19
- fail "Unexpected END" if constructor.nil?
20
- append!(line)
21
-
22
- constructor.from_dcm(buffer).tap { reset! }
23
- end
24
-
25
- def reset!
26
- @constructor = nil
27
- @buffer = []
28
- end
29
-
30
- def header_seen!
31
- @header = :after
32
- end
33
-
34
- def append!(line)
35
- fail "No label started" if constructor.nil?
36
-
37
- @buffer << line
38
- end
39
- end
@@ -1,13 +0,0 @@
1
- require_relative "property_parser"
2
-
3
- class Ecu
4
- class Functions
5
- def self.dcm_header
6
- /FUNKTIONEN.*/
7
- end
8
-
9
- def self.from_dcm(input)
10
- new
11
- end
12
- end
13
- end
@@ -1,34 +0,0 @@
1
- class Ecu
2
- class MalformedDcmError < StandardError
3
- attr_reader :lineno, :filecontent
4
- def initialize(msg, lineno, filecontent)
5
- super(msg)
6
- @lineno = lineno
7
- @filecontent = filecontent
8
- end
9
-
10
- def to_s = "Malformed DCM: #{super} near line ##{lineno}"
11
- def to_str = to_s
12
-
13
- def context
14
- filecontent
15
- .lines
16
- .each
17
- .with_index(1)
18
- .map { |line, n| present(line, n, n == lineno) }
19
- .then { _1[ctx_startline..ctx_endline] }
20
- end
21
-
22
- def present(line, n, highlight)
23
- case highlight
24
- when true then fmt(n) + " => | " + line
25
- when false then fmt(n) + " | " + line
26
- end
27
- end
28
-
29
- def ctx_startline = [0, lineno - 7].max
30
- def ctx_endline = @endline ||= [filecontent.lines.count - 1, lineno + 3].min
31
- def fmt_str = "%#{(ctx_endline + 1).to_s.length}d"
32
- def fmt(n) = fmt_str % n
33
- end
34
- end
@@ -1,50 +0,0 @@
1
- class Ecu
2
- class DcmPropertyParser
3
-
4
- def self.call(line)
5
- key, _, value = line.strip.partition(/\s+/)
6
- eval_property(key, value)
7
- end
8
-
9
- private
10
-
11
- def self.eval_property(key, value)
12
- case key
13
- when "ST/X" then { xvalue: numeric_array(value) }
14
- when "ST/Y" then { yvalue: numeric_array(value) }
15
- when "WERT" then { value: numeric_array(value) }
16
- when "ST_TX/X" then { xvalue: string_array(value) }
17
- when "ST_TX/Y" then { yvalue: string_array(value) }
18
- when "TEXT" then { value: string_array(value) }
19
- when "FUNKTION" then { function: value }
20
- when "EINHEIT_X" then { xunit: string_value(value) }
21
- when "EINHEIT_Y" then { yunit: string_value(value) }
22
- when "EINHEIT_W" then { unit: string_value(value) }
23
- when "LANGNAME" then { description: string_value(value) }
24
- when "DISPLAYNAME" then { }
25
- else fail ArgumentError, "Unknown key #{key}"
26
- end
27
- end
28
-
29
- def self.string_value(str)
30
- str.delete_surrounding('"')
31
- end
32
-
33
- def self.numeric_array(str)
34
- str.split.map { numeric_value(_1) }
35
- end
36
-
37
- def self.string_array(str)
38
- str.scan(/"([^"]*)"/).flatten
39
- end
40
-
41
- def self.numeric_value(str)
42
- case str
43
- in /^\d+$/ then str.to_i
44
- in /^\d+\.$/ then str.to_f
45
- else Float(str)
46
- end
47
- end
48
-
49
- end
50
- end