rsmp 0.35.2 → 0.38.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +22 -0
  3. data/.github/copilot-instructions.md +33 -0
  4. data/.github/workflows/copilot-setup-steps.yml +35 -0
  5. data/.github/workflows/rspec.yaml +2 -1
  6. data/.github/workflows/rubocop.yaml +17 -0
  7. data/.gitignore +7 -7
  8. data/.rubocop.yml +80 -0
  9. data/Gemfile +13 -1
  10. data/Gemfile.lock +73 -35
  11. data/Rakefile +3 -3
  12. data/bin/console +2 -4
  13. data/documentation/tasks.md +4 -4
  14. data/lib/rsmp/cli.rb +147 -124
  15. data/lib/rsmp/collect/ack_collector.rb +8 -7
  16. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  17. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  18. data/lib/rsmp/collect/alarm_matcher.rb +3 -3
  19. data/lib/rsmp/collect/collector/logging.rb +17 -0
  20. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  21. data/lib/rsmp/collect/collector/status.rb +34 -0
  22. data/lib/rsmp/collect/collector.rb +69 -150
  23. data/lib/rsmp/collect/command_matcher.rb +19 -6
  24. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  25. data/lib/rsmp/collect/distributor.rb +14 -11
  26. data/lib/rsmp/collect/filter.rb +31 -15
  27. data/lib/rsmp/collect/matcher.rb +7 -11
  28. data/lib/rsmp/collect/queue.rb +4 -4
  29. data/lib/rsmp/collect/receiver.rb +10 -12
  30. data/lib/rsmp/collect/state_collector.rb +116 -77
  31. data/lib/rsmp/collect/status_collector.rb +6 -6
  32. data/lib/rsmp/collect/status_matcher.rb +17 -7
  33. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -37
  34. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  35. data/lib/rsmp/component/component_base.rb +89 -0
  36. data/lib/rsmp/component/component_proxy.rb +75 -0
  37. data/lib/rsmp/component/components.rb +63 -0
  38. data/lib/rsmp/convert/export/json_schema.rb +116 -110
  39. data/lib/rsmp/convert/import/yaml.rb +21 -18
  40. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +5 -6
  41. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +2 -1
  42. data/lib/rsmp/helpers/error.rb +71 -0
  43. data/lib/rsmp/{inspect.rb → helpers/inspect.rb} +6 -10
  44. data/lib/rsmp/log/archive.rb +98 -0
  45. data/lib/rsmp/log/colorization.rb +41 -0
  46. data/lib/rsmp/log/filtering.rb +54 -0
  47. data/lib/rsmp/log/logger.rb +206 -0
  48. data/lib/rsmp/{logging.rb → log/logging.rb} +5 -7
  49. data/lib/rsmp/message.rb +159 -148
  50. data/lib/rsmp/{node.rb → node/node.rb} +19 -17
  51. data/lib/rsmp/node/protocol.rb +37 -0
  52. data/lib/rsmp/node/site/site.rb +195 -0
  53. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  54. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  55. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  56. data/lib/rsmp/node/supervisor/supervisor.rb +72 -0
  57. data/lib/rsmp/{task.rb → node/task.rb} +29 -19
  58. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  59. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  60. data/lib/rsmp/proxy/modules/send.rb +76 -0
  61. data/lib/rsmp/proxy/modules/state.rb +25 -0
  62. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  63. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  64. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  65. data/lib/rsmp/proxy/proxy.rb +199 -0
  66. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  67. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  68. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  69. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  70. data/lib/rsmp/proxy/site/site_proxy.rb +205 -0
  71. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  72. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  73. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  74. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  75. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +178 -0
  76. data/lib/rsmp/tlc/detector_logic.rb +18 -34
  77. data/lib/rsmp/tlc/input_states.rb +126 -0
  78. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  79. data/lib/rsmp/tlc/modules/display.rb +78 -0
  80. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  81. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  82. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  83. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  84. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  85. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  86. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  87. data/lib/rsmp/tlc/modules/system.rb +140 -0
  88. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  89. data/lib/rsmp/tlc/signal_group.rb +37 -41
  90. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  91. data/lib/rsmp/tlc/signal_priority.rb +39 -35
  92. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  93. data/lib/rsmp/tlc/traffic_controller.rb +39 -1006
  94. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  95. data/lib/rsmp/version.rb +1 -1
  96. data/lib/rsmp.rb +86 -49
  97. data/rsmp.gemspec +24 -30
  98. metadata +87 -130
  99. data/lib/rsmp/archive.rb +0 -76
  100. data/lib/rsmp/collect/message_matchers.rb +0 -0
  101. data/lib/rsmp/component_base.rb +0 -87
  102. data/lib/rsmp/component_proxy.rb +0 -57
  103. data/lib/rsmp/components.rb +0 -65
  104. data/lib/rsmp/error.rb +0 -71
  105. data/lib/rsmp/logger.rb +0 -216
  106. data/lib/rsmp/proxy.rb +0 -695
  107. data/lib/rsmp/site.rb +0 -188
  108. data/lib/rsmp/site_proxy.rb +0 -389
  109. data/lib/rsmp/supervisor.rb +0 -287
  110. data/lib/rsmp/supervisor_proxy.rb +0 -516
  111. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -0,0 +1,71 @@
1
+ module RSMP
2
+ class Error < StandardError
3
+ end
4
+
5
+ class InvalidPacket < Error
6
+ end
7
+
8
+ class MalformedMessage < Error
9
+ end
10
+
11
+ class SchemaError < Error
12
+ attr_accessor :schemas
13
+ end
14
+
15
+ class InvalidMessage < Error
16
+ end
17
+
18
+ class UnknownMessage < Error
19
+ end
20
+
21
+ class MissingAcknowledgment < Error
22
+ end
23
+
24
+ class MissingWatchdog < Error
25
+ end
26
+
27
+ class MessageRejected < Error
28
+ end
29
+
30
+ class MissingAttribute < InvalidMessage
31
+ end
32
+
33
+ class FatalError < Error
34
+ end
35
+
36
+ class HandshakeError < FatalError
37
+ end
38
+
39
+ class NotReady < Error
40
+ end
41
+
42
+ class TimeoutError < Error
43
+ end
44
+
45
+ class DisconnectError < Error
46
+ end
47
+
48
+ class ConnectionError < Error
49
+ end
50
+
51
+ class UnknownComponent < Error
52
+ end
53
+
54
+ class UnknownCommand < Error
55
+ end
56
+
57
+ class UnknownStatus < Error
58
+ end
59
+
60
+ class ConfigurationError < Error
61
+ end
62
+
63
+ class RepeatedAlarmError < Error
64
+ end
65
+
66
+ class RepeatedStatusError < Error
67
+ end
68
+
69
+ class TimestampError < Error
70
+ end
71
+ end
@@ -12,23 +12,20 @@
12
12
 
13
13
  module RSMP
14
14
  module Inspect
15
-
16
15
  def inspector *short_items
17
16
  instance_variables.map do |var_name|
18
17
  var = instance_variable_get(var_name)
19
18
  class_name = var.class.name
20
19
 
21
20
  short = short_items.include?(var_name) ||
22
- class_name.start_with?('Async') ||
23
- class_name.start_with?('RSMP')
21
+ class_name.start_with?('Async') ||
22
+ class_name.start_with?('RSMP')
24
23
 
25
24
  if short
26
- if var.is_a? Array
27
- "#{var_name}: #<#{class_name}:#{class_name.object_id}, #{var.size} items>"
28
- elsif var.is_a? Hash
25
+ if var.is_a?(Array) || var.is_a?(Hash)
29
26
  "#{var_name}: #<#{class_name}:#{class_name.object_id}, #{var.size} items>"
30
27
  else
31
- "#{var_name}: #{var.to_s}"
28
+ "#{var_name}: #{var}"
32
29
  end
33
30
  else
34
31
  "#{var_name}: #{var.inspect}"
@@ -39,8 +36,7 @@ module RSMP
39
36
  # override this if you want additional variable to be shown in the short format,
40
37
  # or ottherwise change the inspect format
41
38
  def inspect
42
- "#<#{self.class.name}:#{self.object_id}, #{inspector}>"
39
+ "#<#{self.class.name}:#{object_id}, #{inspector}>"
43
40
  end
44
-
45
41
  end
46
- end
42
+ end
@@ -0,0 +1,98 @@
1
+ # Archive of log items, which can be messages or info items.
2
+ # All items are timestamped, and stored chronologically.
3
+
4
+ module RSMP
5
+ class Archive
6
+ include Inspect
7
+
8
+ attr_reader :items
9
+
10
+ @index = 0
11
+
12
+ class << self
13
+ attr_accessor :index
14
+ end
15
+
16
+ def initialize(max = 100)
17
+ @items = []
18
+ @max = max
19
+ end
20
+
21
+ def inspect
22
+ "#<#{self.class.name}:#{object_id}, #{inspector(:@items)}>"
23
+ end
24
+
25
+ def self.prepare_item(item)
26
+ raise ArgumentError unless item.is_a? Hash
27
+
28
+ cleaned = item.slice(:author, :level, :ip, :port, :site_id, :component, :text, :message, :exception)
29
+ cleaned[:timestamp] = Clock.now
30
+ if item[:message]
31
+ cleaned[:direction] = item[:message].direction
32
+ cleaned[:component] = item[:message].attributes['cId']
33
+ end
34
+
35
+ cleaned
36
+ end
37
+
38
+ def self.increase_index
39
+ self.index += 1
40
+ end
41
+
42
+ def self.current_index
43
+ index
44
+ end
45
+
46
+ def by_level(levels)
47
+ items.select { |item| levels.include? item[:level] }
48
+ end
49
+
50
+ def strings
51
+ items.map { |item| item[:str] }
52
+ end
53
+
54
+ def add(item)
55
+ item[:index] = RSMP::Archive.increase_index
56
+ @items << item
57
+ return unless @items.size > @max
58
+
59
+ @items.shift
60
+ end
61
+
62
+ private
63
+
64
+ def find(options, &)
65
+ # search backwards from newest to older, stopping once messages
66
+ # are older that options[:earliest]
67
+ out = []
68
+ @items.reverse_each do |item|
69
+ break if too_old?(item, options[:earliest])
70
+ next unless matches_filters?(item, options, &)
71
+
72
+ out.unshift item
73
+ end
74
+ out
75
+ end
76
+
77
+ def too_old?(item, earliest)
78
+ earliest && item[:timestamp] < earliest
79
+ end
80
+
81
+ def matches_filters?(item, options, &block)
82
+ return false if options[:level] && item[:level] != options[:level]
83
+ return false if options[:type] && !matches_type?(item, options[:type])
84
+ return false if options[:with_message] && !message?(item)
85
+ return false if block_given? && block.call != true
86
+
87
+ true
88
+ end
89
+
90
+ def matches_type?(item, type)
91
+ item[:message] && item[:message].type == type
92
+ end
93
+
94
+ def message?(item)
95
+ item[:direction] && item[:message]
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,41 @@
1
+ module RSMP
2
+ class Logger
3
+ # Handles colorization of log output
4
+ module Colorization
5
+ def default_colors
6
+ {
7
+ 'info' => 'white',
8
+ 'log' => 'light_blue',
9
+ 'statistics' => 'light_black',
10
+ 'warning' => 'light_yellow',
11
+ 'error' => 'red',
12
+ 'debug' => 'light_black',
13
+ 'collect' => 'light_black'
14
+ }
15
+ end
16
+
17
+ def colorize_with_map(level, str, colors)
18
+ color = colors[level.to_s]
19
+ color ? str.colorize(color.to_sym) : str
20
+ end
21
+
22
+ def apply_hash_colors(level, str)
23
+ colors = default_colors
24
+ colors.merge! @settings['color'] if @settings['color'].is_a?(Hash)
25
+ colorize_with_map(level, str, colors)
26
+ end
27
+
28
+ def colorize(level, str)
29
+ return str if @settings['color'] == false || @settings['color'].nil?
30
+
31
+ if @settings['color'] == true || @settings['color'].is_a?(Hash)
32
+ apply_hash_colors(level, str)
33
+ elsif %i[nack warning error].include?(level)
34
+ str.colorize(@settings['color']).bold
35
+ else
36
+ str.colorize @settings['color']
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ module RSMP
2
+ class Logger
3
+ # Handles filtering logic for log output
4
+ module Filtering
5
+ def level_enabled?(item)
6
+ return false if @settings['info'] == false && item[:level] == :info
7
+ return false if @settings['debug'] != true && item[:level] == :debug
8
+ return false if @settings['statistics'] != true && item[:level] == :statistics
9
+ return false if @settings['test'] != true && item[:level] == :test
10
+
11
+ true
12
+ end
13
+
14
+ def message_ignored?(item)
15
+ return false unless item[:message]
16
+
17
+ type = item[:message].type
18
+ ack = %w[MessageAck MessageNotAck].include?(type)
19
+ return true unless ignorable?(type, ack, item)
20
+ return true unless acknowledgement_enabled?(ack, item)
21
+
22
+ false
23
+ end
24
+
25
+ def ignorable?(type, ack, item)
26
+ @ignorable.each_pair do |key, types|
27
+ ignore = [types].flatten
28
+ next unless @settings[key] == false
29
+ return false if ignore.include?(type)
30
+
31
+ return false if ack && item[:message].original && ignore.include?(item[:message].original.type)
32
+ end
33
+ true
34
+ end
35
+
36
+ def acknowledgement_enabled?(ack, item)
37
+ return true unless ack
38
+ return true if @settings['acknowledgements'] != false
39
+ return true if %i[not_acknowledged warning error].include?(item[:level])
40
+
41
+ false
42
+ end
43
+
44
+ def output?(item, force: false)
45
+ return false if muted?(item)
46
+ return false if @settings['active'] == false && force != true
47
+ return false unless level_enabled?(item)
48
+ return false if message_ignored?(item)
49
+
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,206 @@
1
+ module RSMP
2
+ class Logger
3
+ include Filtering
4
+ include Colorization
5
+
6
+ attr_accessor :settings
7
+
8
+ def default_output_settings
9
+ {
10
+ 'active' => true,
11
+ 'path' => nil,
12
+ 'stream' => nil,
13
+ 'color' => true,
14
+ 'debug' => false,
15
+ 'statistics' => false,
16
+ 'hide_ip_and_port' => false,
17
+ 'acknowledgements' => false,
18
+ 'watchdogs' => false,
19
+ 'alarms' => true,
20
+ 'json' => false,
21
+ 'tabs' => '-'
22
+ }
23
+ end
24
+
25
+ def default_field_settings
26
+ {
27
+ 'prefix' => false,
28
+ 'index' => false,
29
+ 'author' => false,
30
+ 'timestamp' => true,
31
+ 'ip' => false,
32
+ 'port' => false,
33
+ 'site_id' => true,
34
+ 'component' => true,
35
+ 'direction' => false,
36
+ 'level' => false,
37
+ 'id' => true,
38
+ 'text' => true
39
+ }
40
+ end
41
+
42
+ def default_logger_settings
43
+ default_output_settings.merge(default_field_settings)
44
+ end
45
+
46
+ def default_field_lengths
47
+ {
48
+ 'index' => 7,
49
+ 'author' => 13,
50
+ 'timestamp' => 24,
51
+ 'ip' => 22,
52
+ 'port' => 5,
53
+ 'site_id' => 19,
54
+ 'component' => 19,
55
+ 'direction' => 3,
56
+ 'level' => 7,
57
+ 'id' => 4
58
+ }
59
+ end
60
+
61
+ def ignorable_messages
62
+ {
63
+ 'versions' => ['Version'],
64
+ 'statuses' => %w[StatusRequest StatusSubscribe StatusUnsubscribe StatusResponse StatusUpdate],
65
+ 'commands' => %w[CommandRequest CommandResponse],
66
+ 'watchdogs' => 'Watchdog',
67
+ 'alarms' => ['Alarm'],
68
+ 'aggregated_status' => %w[AggregatedStatus AggregatedStatusRequest]
69
+ }
70
+ end
71
+
72
+ def apply_default_lengths(settings)
73
+ lengths = default_field_lengths
74
+ settings.to_h do |key, value|
75
+ if value == true && lengths[key]
76
+ [key, lengths[key]]
77
+ else
78
+ [key, value]
79
+ end
80
+ end
81
+ end
82
+
83
+ def initialize(settings = {})
84
+ @ignorable = ignorable_messages
85
+ @settings = settings ? default_logger_settings.merge(settings) : default_logger_settings
86
+ @settings = apply_default_lengths(@settings)
87
+ @muted = {}
88
+ setup_output_destination
89
+ end
90
+
91
+ def setup_output_destination
92
+ @stream = if @settings['stream']
93
+ @settings['stream']
94
+ elsif @settings['path']
95
+ File.open(@settings['path'], 'a') # appending
96
+ else
97
+ $stdout
98
+ end
99
+ end
100
+
101
+ def mute(ip, port)
102
+ @muted["#{ip}:#{port}"] = true
103
+ end
104
+
105
+ def unmute(ip, port)
106
+ @muted.delete "#{ip}:#{port}"
107
+ end
108
+
109
+ def unmute_all
110
+ @muted = {}
111
+ end
112
+
113
+ def muted?(item)
114
+ item[:ip] && item[:port] && @muted["#{item[:ip]}:#{item[:port]}"]
115
+ end
116
+
117
+ def level_enabled?(item)
118
+ return false if @settings['info'] == false && item[:level] == :info
119
+ return false if @settings['debug'] != true && item[:level] == :debug
120
+ return false if @settings['statistics'] != true && item[:level] == :statistics
121
+ return false if @settings['test'] != true && item[:level] == :test
122
+
123
+ true
124
+ end
125
+
126
+ def output(level, str)
127
+ return if str.empty? || /^\s+$/.match(str)
128
+
129
+ str = colorize level, str
130
+ @stream.puts str
131
+ @stream.flush
132
+ end
133
+
134
+ def log(item, force: false)
135
+ return unless output?(item, force: force)
136
+
137
+ output item[:level], build_output(item)
138
+ end
139
+
140
+ def self.shorten_message_id(m_id, length = 4)
141
+ if m_id
142
+ m_id[0..(length - 1)].ljust(length)
143
+ else
144
+ ' ' * length
145
+ end
146
+ end
147
+
148
+ def dump(archive, num: nil)
149
+ num ||= archive.items.size
150
+ log = archive.items.last(num).map do |item|
151
+ str = build_output item
152
+ colorize item[:level], str
153
+ end
154
+ log.join("\n")
155
+ end
156
+
157
+ def build_part(parts, item, key, &block)
158
+ skey = key.to_s
159
+ return unless @settings[skey]
160
+
161
+ part = item[key]
162
+ part = yield part if block
163
+ part = part.to_s
164
+ part = part.ljust @settings[skey] if @settings[skey].is_a?(Integer)
165
+
166
+ # replace the first char with a dash if string is all whitespace
167
+ part = @settings['tabs'].ljust(part.length) if @settings['tabs'] && part !~ /\S/
168
+ parts << part
169
+ end
170
+
171
+ def add_metadata_parts(parts, item)
172
+ build_part(parts, item, :prefix) { @settings['prefix'] if @settings['prefix'] != false }
173
+ build_part(parts, item, :index)
174
+ build_part(parts, item, :author)
175
+ build_part(parts, item, :timestamp) { |part| Clock.to_s part }
176
+ end
177
+
178
+ def add_connection_parts(parts, item)
179
+ build_part(parts, item, :ip)
180
+ build_part(parts, item, :port)
181
+ build_part(parts, item, :site_id)
182
+ build_part(parts, item, :component)
183
+ end
184
+
185
+ def add_message_parts(parts, item)
186
+ build_part(parts, item, :direction) { |part| { in: 'In', out: 'Out' }[part] }
187
+ build_part(parts, item, :level, &:capitalize)
188
+ build_part(parts, item, :id) { Logger.shorten_message_id(item[:message].m_id, 4) if item[:message] }
189
+ build_part(parts, item, :text)
190
+ build_part(parts, item, :json) { item[:message]&.json }
191
+ build_part(parts, item, :exception) { |e| [e.class, e.backtrace].flatten.join("\n") }
192
+ end
193
+
194
+ def add_output_parts(parts, item)
195
+ add_metadata_parts(parts, item)
196
+ add_connection_parts(parts, item)
197
+ add_message_parts(parts, item)
198
+ end
199
+
200
+ def build_output(item)
201
+ parts = []
202
+ add_output_parts(parts, item)
203
+ parts.join(' ').chomp(@settings['tabs'].to_s).rstrip
204
+ end
205
+ end
206
+ end
@@ -6,21 +6,19 @@ module RSMP
6
6
  module Logging
7
7
  attr_reader :archive, :logger
8
8
 
9
- def initialize_logging options
9
+ def initialize_logging(options)
10
10
  @archive = options[:archive] || RSMP::Archive.new
11
11
  @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
12
12
  end
13
13
 
14
- def author
15
- end
14
+ def author; end
16
15
 
17
- def log str, options={}
18
- default = { text:str, level: :log, author: author, ip: @ip, port: @port }
16
+ def log(str, options = {})
17
+ default = { text: str, level: :log, author: author, ip: @ip, port: @port }
19
18
  prepared = RSMP::Archive.prepare_item default.merge(options)
20
19
  @archive.add prepared
21
20
  @logger.log prepared
22
21
  prepared
23
22
  end
24
-
25
23
  end
26
- end
24
+ end