openc3 7.2.0 → 7.3.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +108 -29
  3. data/bin/pipinstall +5 -4
  4. data/bin/pipuninstall +5 -1
  5. data/bin/uvinstall +149 -0
  6. data/data/config/interface_modifiers.yaml +15 -0
  7. data/data/config/item_modifiers.yaml +2 -1
  8. data/data/config/microservice.yaml +5 -1
  9. data/data/config/screen.yaml +2 -1
  10. data/data/config/target.yaml +38 -0
  11. data/data/config/tool.yaml +3 -1
  12. data/data/config/widgets.yaml +43 -3
  13. data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
  14. data/data/xtce_schemas/xml.xsd +116 -0
  15. data/ext/openc3/ext/telemetry/telemetry.c +1 -1
  16. data/lib/openc3/accessors/binary_accessor.rb +4 -0
  17. data/lib/openc3/accessors/template_accessor.rb +2 -2
  18. data/lib/openc3/api/cmd_api.rb +5 -1
  19. data/lib/openc3/api/interface_api.rb +2 -2
  20. data/lib/openc3/api/limits_api.rb +89 -3
  21. data/lib/openc3/api/router_api.rb +3 -3
  22. data/lib/openc3/api/tlm_api.rb +1 -1
  23. data/lib/openc3/interfaces/http_client_interface.rb +1 -1
  24. data/lib/openc3/io/json_drb.rb +24 -8
  25. data/lib/openc3/io/json_rpc.rb +0 -2
  26. data/lib/openc3/logs/log_writer.rb +43 -7
  27. data/lib/openc3/logs/packet_log_writer.rb +1 -2
  28. data/lib/openc3/microservices/decom_common.rb +15 -4
  29. data/lib/openc3/microservices/decom_microservice.rb +6 -1
  30. data/lib/openc3/microservices/interface_microservice.rb +26 -12
  31. data/lib/openc3/microservices/queue_microservice.rb +4 -2
  32. data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
  33. data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
  34. data/lib/openc3/models/cvt_model.rb +7 -7
  35. data/lib/openc3/models/db_sharded_model.rb +2 -2
  36. data/lib/openc3/models/gem_model.rb +1 -1
  37. data/lib/openc3/models/interface_model.rb +17 -11
  38. data/lib/openc3/models/interface_status_model.rb +2 -2
  39. data/lib/openc3/models/metric_model.rb +2 -2
  40. data/lib/openc3/models/microservice_model.rb +21 -0
  41. data/lib/openc3/models/microservice_status_model.rb +2 -2
  42. data/lib/openc3/models/model.rb +2 -6
  43. data/lib/openc3/models/plugin_model.rb +204 -49
  44. data/lib/openc3/models/python_package_model.rb +210 -12
  45. data/lib/openc3/models/reingest_job_model.rb +1 -1
  46. data/lib/openc3/models/scope_model.rb +34 -30
  47. data/lib/openc3/models/script_status_model.rb +73 -38
  48. data/lib/openc3/models/target_model.rb +193 -6
  49. data/lib/openc3/models/tool_model.rb +33 -20
  50. data/lib/openc3/operators/microservice_operator.rb +33 -4
  51. data/lib/openc3/operators/operator.rb +15 -5
  52. data/lib/openc3/packets/commands.rb +7 -7
  53. data/lib/openc3/packets/limits.rb +33 -2
  54. data/lib/openc3/packets/packet.rb +6 -4
  55. data/lib/openc3/packets/packet_config.rb +5 -1
  56. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  57. data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
  58. data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
  59. data/lib/openc3/packets/structure.rb +16 -12
  60. data/lib/openc3/packets/telemetry.rb +3 -3
  61. data/lib/openc3/script/commands.rb +1 -1
  62. data/lib/openc3/script/extract.rb +20 -25
  63. data/lib/openc3/script/limits.rb +1 -1
  64. data/lib/openc3/script/suite_runner.rb +43 -9
  65. data/lib/openc3/script/web_socket_api.rb +45 -5
  66. data/lib/openc3/system/target.rb +4 -1
  67. data/lib/openc3/tools/table_manager/table_config.rb +4 -1
  68. data/lib/openc3/top_level.rb +1 -1
  69. data/lib/openc3/topics/command_decom_topic.rb +16 -1
  70. data/lib/openc3/topics/limits_event_topic.rb +63 -4
  71. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
  72. data/lib/openc3/utilities/aws_bucket.rb +10 -2
  73. data/lib/openc3/utilities/cli_generator.rb +5 -1
  74. data/lib/openc3/utilities/csv.rb +5 -5
  75. data/lib/openc3/utilities/env_helper.rb +13 -1
  76. data/lib/openc3/utilities/local_mode.rb +4 -1
  77. data/lib/openc3/utilities/pypi_url.rb +39 -0
  78. data/lib/openc3/utilities/reingest_job.rb +1 -1
  79. data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
  80. data/lib/openc3/utilities/running_script.rb +160 -100
  81. data/lib/openc3/utilities/script.rb +60 -13
  82. data/lib/openc3/utilities/store_autoload.rb +39 -1
  83. data/lib/openc3/utilities/store_queued.rb +5 -1
  84. data/lib/openc3/version.rb +5 -5
  85. data/lib/openc3.rb +6 -1
  86. data/templates/plugin/plugin.gemspec +1 -1
  87. data/templates/tool_angular/package.json +2 -2
  88. data/templates/tool_react/package.json +1 -1
  89. data/templates/tool_svelte/package.json +1 -1
  90. data/templates/tool_vue/package.json +3 -3
  91. data/templates/widget/package.json +2 -2
  92. metadata +7 -3
  93. data/templates/tool_vue/.nycrc +0 -3
  94. data/templates/widget/.nycrc +0 -3
@@ -0,0 +1,116 @@
1
+ <?xml version='1.0'?>
2
+ <xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" xml:lang="en">
3
+
4
+ <xs:annotation>
5
+ <xs:documentation>
6
+ See http://www.w3.org/XML/1998/namespace.html and
7
+ http://www.w3.org/TR/REC-xml for information about this namespace.
8
+
9
+ This schema document describes the XML namespace, in a form
10
+ suitable for import by other schema documents.
11
+
12
+ Note that local names in this namespace are intended to be defined
13
+ only by the World Wide Web Consortium or its subgroups. The
14
+ following names are currently defined in this namespace and should
15
+ not be used with conflicting semantics by any Working Group,
16
+ specification, or document instance:
17
+
18
+ base (as an attribute name): denotes an attribute whose value
19
+ provides a URI to be used as the base for interpreting any
20
+ relative URIs in the scope of the element on which it
21
+ appears; its value is inherited. This name is reserved
22
+ by virtue of its definition in the XML Base specification.
23
+
24
+ lang (as an attribute name): denotes an attribute whose value
25
+ is a language code for the natural language of the content of
26
+ any element; its value is inherited. This name is reserved
27
+ by virtue of its definition in the XML specification.
28
+
29
+ space (as an attribute name): denotes an attribute whose
30
+ value is a keyword indicating what whitespace processing
31
+ discipline is intended for the content of the element; its
32
+ value is inherited. This name is reserved by virtue of its
33
+ definition in the XML specification.
34
+
35
+ Father (in any context at all): denotes Jon Bosak, the chair of
36
+ the original XML Working Group. This name is reserved by
37
+ the following decision of the W3C XML Plenary and
38
+ XML Coordination groups:
39
+
40
+ In appreciation for his vision, leadership and dedication
41
+ the W3C XML Plenary on this 10th day of February, 2000
42
+ reserves for Jon Bosak in perpetuity the XML name
43
+ xml:Father
44
+ </xs:documentation>
45
+ </xs:annotation>
46
+
47
+ <xs:annotation>
48
+ <xs:documentation>This schema defines attributes and an attribute group
49
+ suitable for use by
50
+ schemas wishing to allow xml:base, xml:lang or xml:space attributes
51
+ on elements they define.
52
+
53
+ To enable this, such a schema must import this schema
54
+ for the XML namespace, e.g. as follows:
55
+ &lt;schema . . .>
56
+ . . .
57
+ &lt;import namespace="http://www.w3.org/XML/1998/namespace"
58
+ schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
59
+
60
+ Subsequently, qualified reference to any of the attributes
61
+ or the group defined below will have the desired effect, e.g.
62
+
63
+ &lt;type . . .>
64
+ . . .
65
+ &lt;attributeGroup ref="xml:specialAttrs"/>
66
+
67
+ will define a type which will schema-validate an instance
68
+ element with any of those attributes</xs:documentation>
69
+ </xs:annotation>
70
+
71
+ <xs:annotation>
72
+ <xs:documentation>In keeping with the XML Schema WG's standard versioning
73
+ policy, this schema document will persist at
74
+ http://www.w3.org/2001/03/xml.xsd.
75
+ At the date of issue it can also be found at
76
+ http://www.w3.org/2001/xml.xsd.
77
+ The schema document at that URI may however change in the future,
78
+ in order to remain compatible with the latest version of XML Schema
79
+ itself. In other words, if the XML Schema namespace changes, the version
80
+ of this document at
81
+ http://www.w3.org/2001/xml.xsd will change
82
+ accordingly; the version at
83
+ http://www.w3.org/2001/03/xml.xsd will not change.
84
+ </xs:documentation>
85
+ </xs:annotation>
86
+
87
+ <xs:attribute name="lang" type="xs:language">
88
+ <xs:annotation>
89
+ <xs:documentation>In due course, we should install the relevant ISO 2- and 3-letter
90
+ codes as the enumerated possible values . . .</xs:documentation>
91
+ </xs:annotation>
92
+ </xs:attribute>
93
+
94
+ <xs:attribute name="space" default="preserve">
95
+ <xs:simpleType>
96
+ <xs:restriction base="xs:NCName">
97
+ <xs:enumeration value="default"/>
98
+ <xs:enumeration value="preserve"/>
99
+ </xs:restriction>
100
+ </xs:simpleType>
101
+ </xs:attribute>
102
+
103
+ <xs:attribute name="base" type="xs:anyURI">
104
+ <xs:annotation>
105
+ <xs:documentation>See http://www.w3.org/TR/xmlbase/ for
106
+ information about this attribute.</xs:documentation>
107
+ </xs:annotation>
108
+ </xs:attribute>
109
+
110
+ <xs:attributeGroup name="specialAttrs">
111
+ <xs:attribute ref="xml:base"/>
112
+ <xs:attribute ref="xml:lang"/>
113
+ <xs:attribute ref="xml:space"/>
114
+ </xs:attributeGroup>
115
+
116
+ </xs:schema>
@@ -56,7 +56,7 @@ static VALUE packets(VALUE self, VALUE target_name)
56
56
 
57
57
  if (!(RTEST(target_packets)))
58
58
  {
59
- rb_raise(rb_eRuntimeError, "Telemetry target '%s' does not exist", RSTRING_PTR(upcase_target_name));
59
+ rb_raise(rb_eRuntimeError, "Telemetry target '%s' does not exist (packets lookup)", RSTRING_PTR(upcase_target_name));
60
60
  }
61
61
 
62
62
  return target_packets;
@@ -108,6 +108,10 @@ module OpenC3
108
108
 
109
109
  def handle_read_variable_bit_size(item, _buffer)
110
110
  length_value = @packet.read(item.variable_bit_size['length_item_name'], :CONVERTED)
111
+ # length_value can be nil when reading an undersized packet where the length
112
+ # item falls outside the buffer bounds
113
+ raise "Length value #{item.variable_bit_size['length_item_name']} for item #{item.name} is nil" if length_value.nil?
114
+
111
115
  if item.array_size
112
116
  item.array_size = (length_value * item.variable_bit_size['length_bits_per_count']) + item.variable_bit_size['length_value_bit_offset']
113
117
  else
@@ -109,7 +109,7 @@ module OpenC3
109
109
  configure()
110
110
 
111
111
  success = buffer.gsub!("#{@left_char}#{item.key}#{@right_char}", value.to_s)
112
- raise "Key #{item.key} not found in template" unless success
112
+ raise "Key '#{item.key}' not found in template (write_item)" unless success
113
113
  return value
114
114
  end
115
115
 
@@ -118,7 +118,7 @@ module OpenC3
118
118
  items.each_with_index do |item, index|
119
119
  next if item.data_type == :DERIVED
120
120
  success = buffer.gsub!("#{@left_char}#{item.key}#{@right_char}", values[index].to_s)
121
- raise "Key #{item.key} not found in template" unless success
121
+ raise "Key '#{item.key}' not found in template (write_items)" unless success
122
122
  end
123
123
  return values
124
124
  end
@@ -450,7 +450,7 @@ module OpenC3
450
450
 
451
451
  # NOTE: When adding new keywords to this method, make sure to update script/commands.rb
452
452
  def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, manual: false, validate: true, queue: nil,
453
- scope: $openc3_scope, token: $openc3_token, **kwargs)
453
+ queue_username: nil, scope: $openc3_scope, token: $openc3_token, **kwargs)
454
454
  extract_string_kwargs_to_args(args, kwargs)
455
455
  unless [nil, true, false].include?(log_message)
456
456
  raise "Invalid log_message parameter: #{log_message}. Must be true or false."
@@ -535,6 +535,10 @@ module OpenC3
535
535
  'log_message' => log_message.to_s,
536
536
  'obfuscated_items' => packet['obfuscated_items'].to_s
537
537
  }
538
+ # Record the original queuing user (author) separately from 'username' (the
539
+ # user or process that actually executed the command). Command History shows
540
+ # 'username' as "Executed By" and queue_username as "Queued By".
541
+ command['queue_username'] = queue_username if queue_username
538
542
  # Users have to explicitly opt into a default queue by setting the OPENC3_DEFAULT_QUEUE
539
543
  # At which point ALL commands will go to that queue unless they specifically opt out with queue: false
540
544
  if ENV['OPENC3_DEFAULT_QUEUE'] && queue.nil?
@@ -47,7 +47,7 @@ module OpenC3
47
47
  def get_interface(interface_name, manual: false, scope: $openc3_scope, token: $openc3_token)
48
48
  authorize(permission: 'system', interface_name: interface_name, manual: manual, scope: scope, token: token)
49
49
  interface = InterfaceModel.get(name: interface_name, scope: scope)
50
- raise "Interface '#{interface_name}' does not exist" unless interface
50
+ raise "Interface '#{interface_name}' does not exist (get_interface)" unless interface
51
51
 
52
52
  interface.merge(InterfaceStatusModel.get(name: interface_name, scope: scope))
53
53
  end
@@ -160,7 +160,7 @@ module OpenC3
160
160
  # TODO: Check if they have command authority for the targets mapped to this interface
161
161
  authorize(permission: 'system_set', interface_name: interface_name, manual: manual, scope: scope, token: token)
162
162
  interface = InterfaceModel.get_model(name: interface_name, scope: scope)
163
- raise "Interface '#{interface_name}' does not exist" unless interface
163
+ raise "Interface '#{interface_name}' does not exist (unmap_target_from_interface)" unless interface
164
164
 
165
165
  if Array === target_name
166
166
  target_names = target_name
@@ -17,6 +17,7 @@
17
17
 
18
18
  require 'openc3/api/target_api'
19
19
  require 'openc3/models/cvt_model'
20
+ require 'openc3/packets/packet_item'
20
21
 
21
22
  module OpenC3
22
23
  module Api
@@ -29,12 +30,14 @@ module OpenC3
29
30
  'disable_limits',
30
31
  'get_limits',
31
32
  'set_limits',
33
+ 'set_state_color',
32
34
  'get_limits_groups',
33
35
  'enable_limits_group',
34
36
  'disable_limits_group',
35
37
  'get_limits_sets',
36
38
  'set_limits_set',
37
39
  'get_limits_set',
40
+ 'delete_limits_set',
38
41
  'get_limits_events',
39
42
  ])
40
43
 
@@ -131,7 +134,7 @@ module OpenC3
131
134
  break
132
135
  end
133
136
  end
134
- raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
137
+ raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while enabling limits" unless found_item
135
138
 
136
139
  TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
137
140
 
@@ -164,7 +167,7 @@ module OpenC3
164
167
  break
165
168
  end
166
169
  end
167
- raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
170
+ raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while disabling limits" unless found_item
168
171
 
169
172
  TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
170
173
 
@@ -236,7 +239,7 @@ module OpenC3
236
239
  end
237
240
  end
238
241
  end
239
- raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
242
+ raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while setting limits" unless found_item
240
243
  message = "Setting '#{target_name} #{packet_name} #{item_name}' limits to #{red_low} #{yellow_low} #{yellow_high} #{red_high}"
241
244
  message << " #{green_low} #{green_high}" if green_low && green_high
242
245
  message << " in set #{limits_set} with persistence #{persistence} as enabled #{enabled}"
@@ -251,6 +254,64 @@ module OpenC3
251
254
  LimitsEventTopic.write(event, scope: scope)
252
255
  end
253
256
 
257
+ # Change the color associated with a telemetry item state in realtime.
258
+ # Items with states use the state color (GREEN, YELLOW, RED) to determine
259
+ # their limits state rather than numeric red/yellow/green limits.
260
+ #
261
+ # @param target_name [String] Target name
262
+ # @param packet_name [String] Packet name
263
+ # @param item_name [String] Item name
264
+ # @param state_name [String] Name of the state to change (e.g. 'CONNECTED')
265
+ # @param color [String, nil] New color for the state. Must be GREEN, YELLOW,
266
+ # or RED. Pass nil to clear (remove) the state color. Setting a color also
267
+ # enables limits for the item, but clearing the color (nil) does not change
268
+ # whether limits are enabled.
269
+ def set_state_color(target_name, packet_name, item_name, state_name, color,
270
+ manual: false, scope: $openc3_scope, token: $openc3_token)
271
+ authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
272
+ state_name = state_name.to_s.upcase
273
+ unless color.nil?
274
+ color = color.to_s.upcase
275
+ unless PacketItem::STATE_COLORS.include?(color.intern)
276
+ raise "Invalid state color '#{color}'. Must be one of #{PacketItem::STATE_COLORS.join(', ')}."
277
+ end
278
+ end
279
+ packet = TargetModel.packet(target_name, packet_name, scope: scope)
280
+ found_item = nil
281
+ packet['items'].each do |item|
282
+ if item['name'] == item_name
283
+ unless item['states'] && item['states'][state_name]
284
+ raise "State '#{state_name}' does not exist for item '#{target_name} #{packet_name} #{item_name}'"
285
+ end
286
+ if color.nil?
287
+ # Clear the state color
288
+ item['states'][state_name].delete('color')
289
+ else
290
+ item['states'][state_name]['color'] = color
291
+ # Defining a state color implies limits are enabled for the item
292
+ item['limits'] ||= {}
293
+ item['limits']['enabled'] = true
294
+ end
295
+ found_item = item
296
+ break
297
+ end
298
+ end
299
+ raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist while setting state color" unless found_item
300
+ if color.nil?
301
+ message = "Clearing '#{target_name} #{packet_name} #{item_name}' state #{state_name} color"
302
+ else
303
+ message = "Setting '#{target_name} #{packet_name} #{item_name}' state #{state_name} color to #{color}"
304
+ end
305
+ Logger.info(message, scope: scope)
306
+
307
+ TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
308
+
309
+ event = { type: :LIMITS_STATE_COLOR, target_name: target_name, packet_name: packet_name,
310
+ item_name: item_name, state_name: state_name, color: color,
311
+ time_nsec: Time.now.to_nsec_from_epoch, message: message }
312
+ LimitsEventTopic.write(event, scope: scope)
313
+ end
314
+
254
315
  # Returns all limits_groups and their members
255
316
  # @since 5.0.0 Returns hash with values
256
317
  # @return [Hash{String => Array<Array<String, String, String>>]
@@ -292,6 +353,31 @@ module OpenC3
292
353
  time_nsec: Time.now.to_nsec_from_epoch, message: message }, scope: scope)
293
354
  end
294
355
 
356
+ # Deletes a limits set. The DEFAULT limits set and the currently active
357
+ # limits set cannot be deleted. Use set_limits_set to change the active set
358
+ # before deleting it. Note that the limits set is not removed from the
359
+ # TargetModel packet definitions; that is cleaned up on the next plugin
360
+ # install.
361
+ #
362
+ # @param limits_set [String] The name of the limits set to delete
363
+ def delete_limits_set(limits_set, manual: false, scope: $openc3_scope, token: $openc3_token)
364
+ authorize(permission: 'tlm_set', manual: manual, scope: scope, token: token)
365
+ limits_set = limits_set.to_s
366
+ raise "Cannot delete the DEFAULT limits set" if limits_set == 'DEFAULT'
367
+ if limits_set == LimitsEventTopic.current_set(scope: scope)
368
+ raise "Cannot delete the current limits set '#{limits_set}'. Use set_limits_set to change the current set first."
369
+ end
370
+ unless LimitsEventTopic.sets(scope: scope).key?(limits_set)
371
+ raise "Limits set '#{limits_set}' does not exist"
372
+ end
373
+
374
+ # Remove the limits set from Redis (limits_sets and current_limits_settings)
375
+ LimitsEventTopic.delete_set(limits_set, scope: scope)
376
+
377
+ message = "Deleting Limits Set: #{limits_set}"
378
+ Logger.info(message, scope: scope)
379
+ end
380
+
295
381
  # Returns the active limits set that applies to all telemetry
296
382
  #
297
383
  # @return [String] The current limits set
@@ -46,7 +46,7 @@ module OpenC3
46
46
  def get_router(router_name, manual: false, scope: $openc3_scope, token: $openc3_token)
47
47
  authorize(permission: 'system', router_name: router_name, manual: manual, scope: scope, token: token)
48
48
  router = RouterModel.get(name: router_name, scope: scope)
49
- raise "Router '#{router_name}' does not exist" unless router
49
+ raise "Router '#{router_name}' does not exist (get_router)" unless router
50
50
 
51
51
  router.merge(RouterStatusModel.get(name: router_name, scope: scope))
52
52
  end
@@ -144,7 +144,7 @@ module OpenC3
144
144
  # TODO: Check if they have command authority for the targets mapped to this router
145
145
  authorize(permission: 'system_set', router_name: router_name, manual: manual, scope: scope, token: token)
146
146
  router = RouterModel.get_model(name: router_name, scope: scope)
147
- raise "Router '#{router_name}' does not exist" unless router
147
+ raise "Router '#{router_name}' does not exist (map_target_to_router)" unless router
148
148
 
149
149
  if Array === target_name
150
150
  target_names = target_name
@@ -167,7 +167,7 @@ module OpenC3
167
167
  # TODO: Check if they have command authority for the targets mapped to this router
168
168
  authorize(permission: 'system_set', router_name: router_name, manual: manual, scope: scope, token: token)
169
169
  router = RouterModel.get_model(name: router_name, scope: scope)
170
- raise "Router '#{router_name}' does not exist" unless router
170
+ raise "Router '#{router_name}' does not exist (unmap_target_from_router)" unless router
171
171
 
172
172
  if Array === target_name
173
173
  target_names = target_name
@@ -123,7 +123,7 @@ module OpenC3
123
123
  target_name = target_name.upcase
124
124
  packet_name = packet_name.upcase
125
125
  unless CvtModel::VALUE_TYPES.include?(type)
126
- raise "Unknown type '#{type}' for #{target_name} #{packet_name}"
126
+ raise "Unknown type '#{type}' for #{target_name} #{packet_name} (inject_tlm)"
127
127
  end
128
128
 
129
129
  if item_hash
@@ -237,7 +237,7 @@ module OpenC3
237
237
  result['read_timeout'] = @read_timeout
238
238
  result['connect_timeout'] = @connect_timeout
239
239
  result['include_request_in_response'] = @include_request_in_response
240
- result['request_queue_length'] = @request_queue.length
240
+ result['response_queue_length'] = @response_queue.length
241
241
  return result
242
242
  end
243
243
  end
@@ -17,6 +17,7 @@
17
17
 
18
18
  require 'socket'
19
19
  require 'json'
20
+ require 'set'
20
21
  # require 'drb/acl'
21
22
  require 'drb/drb'
22
23
  require 'openc3/io/json_rpc'
@@ -47,8 +48,9 @@ module OpenC3
47
48
  # JsonDRb implements the JSON-RPC 2.0 Specification to provide an interface
48
49
  # for both internal and external tools to access the OpenC3 server. It
49
50
  # provides methods to install an access control list to control access to the
50
- # API. It also limits the available methods to a known list of allowable API
51
- # methods.
51
+ # API. Access is controlled by a required method_whitelist which names every
52
+ # method that may be invoked. There is no permissive fallback: any method not
53
+ # explicitly listed is rejected.
52
54
  class JsonDRb
53
55
  # Minimum amount of time in seconds to receive the JSON request,
54
56
  # process it, and send the response. Requests for less than this amount
@@ -62,18 +64,21 @@ module OpenC3
62
64
 
63
65
  # @return [Integer] The number of JSON-RPC requests processed
64
66
  attr_accessor :request_count
65
- # @return [Array<String>] List of methods that should be allowed
66
- attr_accessor :method_whitelist
67
+ # @return [Set<String>] Set of downcased method names which may be called
68
+ attr_reader :method_whitelist
67
69
  # @return [ACL] The access control list
68
70
  # attr_accessor :acl
69
71
 
70
72
  attr_accessor :object
71
73
 
72
- def initialize
74
+ # @param method_whitelist [Enumerable<String>] Every method name which may
75
+ # be called over JSON-RPC. Required and must not be empty since there is
76
+ # no permissive fallback.
77
+ def initialize(method_whitelist:)
78
+ self.method_whitelist = method_whitelist
73
79
  @thread = nil
74
80
  # @acl = nil
75
81
  @object = nil
76
- @method_whitelist = nil
77
82
  @request_count = 0
78
83
  @request_times = []
79
84
  @request_times_index = 0
@@ -82,6 +87,18 @@ module OpenC3
82
87
  @server_mutex = Mutex.new
83
88
  end
84
89
 
90
+ # Replaces the set of methods which may be called over JSON-RPC. Names are
91
+ # downcased to match the dispatch in {#process_request}.
92
+ #
93
+ # @param whitelist [Enumerable<String>] Every method name which may be called
94
+ def method_whitelist=(whitelist)
95
+ if whitelist.nil? or whitelist.to_a.empty?
96
+ raise ArgumentError, "method_whitelist is required and must not be empty"
97
+ end
98
+
99
+ @method_whitelist = whitelist.map { |name| name.to_s.downcase }.to_set
100
+ end
101
+
85
102
  # Returns the number of connected clients
86
103
  # @return [Integer] The number of connected clients
87
104
  def num_clients
@@ -254,8 +271,7 @@ module OpenC3
254
271
  error_code = nil
255
272
  response_data = nil
256
273
 
257
- if (@method_whitelist and @method_whitelist.include?(request.method.downcase())) or
258
- (!@method_whitelist and !JsonRpcRequest::DANGEROUS_METHODS.include?(request.method.downcase()))
274
+ if @method_whitelist.include?(request.method.downcase())
259
275
  begin
260
276
  if request.keyword_params
261
277
  result = @object.public_send(request.method.downcase().intern, *request.params, **request.keyword_params)
@@ -227,8 +227,6 @@ module OpenC3
227
227
 
228
228
  # Represents a JSON Remote Procedure Call Request
229
229
  class JsonRpcRequest < JsonRpc
230
- DANGEROUS_METHODS = Set['__send__', 'send', 'instance_eval', 'instance_exec']
231
-
232
230
  # @param method_name [String] The name of the method to call
233
231
  # @param method_params [Array<String>] Array of strings which represent the
234
232
  # parameters to send to the method
@@ -113,6 +113,7 @@ module OpenC3
113
113
  @cycle_minute = Integer(@cycle_minute) if @cycle_minute
114
114
  @enforce_time_order = ConfigParser.handle_true_false(enforce_time_order)
115
115
  @out_of_order = false
116
+ @bad_time = false
116
117
  @mutex = Mutex.new
117
118
  @file = nil
118
119
  @file_size = 0
@@ -125,6 +126,13 @@ module OpenC3
125
126
  @cleanup_offsets = []
126
127
  @cleanup_times = []
127
128
  @previous_time_nsec_since_epoch = nil
129
+ # Stored packets older than 20 years are treated as invalid (e.g. 1970 before
130
+ # clock/GPS sync). They never drive file segmentation nor become the ordering
131
+ # baseline, so a late-syncing clock doesn't thrash log files.
132
+ @min_valid_time_nsec = (@start_time - (20 * 365 * 24 * 60 * 60)).to_nsec_from_epoch
133
+ # Validity (valid/invalid time) of the packets in the currently open file. Used to
134
+ # segregate invalid-time (1970) packets into their own file. nil = no packets yet.
135
+ @current_file_valid = nil
128
136
  @tmp_dir = Dir.mktmpdir
129
137
  @wait_threads = []
130
138
 
@@ -280,6 +288,7 @@ module OpenC3
280
288
  @first_time = nil
281
289
  @last_time = nil
282
290
  @previous_time_nsec_since_epoch = nil
291
+ @current_file_valid = nil
283
292
  Logger.debug "Log File Opened : #{@filename}"
284
293
  rescue => e
285
294
  Logger.error "Error starting new log file: #{e.formatted}"
@@ -289,7 +298,14 @@ module OpenC3
289
298
 
290
299
  # @enforce_time_order requires the timestamps on each write to be greater than the previous
291
300
  # process_out_of_order ignores the timestamps for the current entry (used to ignore timestamps on metadata entries, vs actual packets)
292
- def prepare_write(time_nsec_since_epoch, data_length, redis_topic = nil, redis_offset = nil, allow_new_file: true, process_out_of_order: true)
301
+ # stored: stored packets roll a new file when GOOD times move backward (a new /
302
+ # overlapping replay run). Packets with an invalid time (e.g. 1970 before clock/GPS
303
+ # sync) - realtime or stored - are segregated into their own file: the first valid
304
+ # packet after a run of 1970s rolls a fresh, properly-named file (and vice-versa).
305
+ def prepare_write(time_nsec_since_epoch, data_length, redis_topic = nil, redis_offset = nil, allow_new_file: true, process_out_of_order: true, stored: false)
306
+ # An invalid time is a packet from before the clock/GPS synced (e.g. 1970). This
307
+ # applies to both realtime and stored packets so neither taints a file's filename.
308
+ packet_valid = !(process_out_of_order and time_nsec_since_epoch < @min_valid_time_nsec)
293
309
  # This check includes logging_enabled again because it might have changed since we acquired the mutex
294
310
  # Ensures new files based on size, and ensures always increasing time order in files
295
311
  if @logging_enabled
@@ -299,17 +315,37 @@ module OpenC3
299
315
  elsif @cycle_size and ((@file_size + data_length) > @cycle_size)
300
316
  Logger.debug("Log writer start new file due to cycle size #{@cycle_size}")
301
317
  start_new_file() if allow_new_file
318
+ elsif process_out_of_order and !@current_file_valid.nil? and packet_valid != @current_file_valid and allow_new_file
319
+ # Time validity changed (1970 <-> valid): segregate invalid-time packets into
320
+ # their own file so valid files keep a clean, correctly-timed filename.
321
+ Logger.debug("Log writer start new file due to time validity change")
322
+ start_new_file()
302
323
  elsif process_out_of_order and @enforce_time_order and @previous_time_nsec_since_epoch and (@previous_time_nsec_since_epoch > time_nsec_since_epoch)
303
- # Warning: Creating new files here can cause lots of files to be created if packets make it through out of order
304
- # Changed to just a error to prevent file thrashing
305
- unless @out_of_order
306
- Logger.error("Log writer out of order time detected (increase buffer depth?): #{Time.from_nsec_from_epoch(@previous_time_nsec_since_epoch)} #{Time.from_nsec_from_epoch(time_nsec_since_epoch)}")
307
- @out_of_order = true
324
+ if stored and packet_valid and allow_new_file
325
+ # Good stored time went backward => new/overlapping replay run. Roll a new
326
+ # file so each stored file stays monotonic (start_new_file resets @previous_time).
327
+ Logger.debug("Log writer start new file due to out of order stored time")
328
+ start_new_file()
329
+ elsif !stored
330
+ # Warning: Creating new files here can cause lots of files to be created if packets make it through out of order
331
+ # Changed to just a warning to prevent file thrashing
332
+ unless @out_of_order
333
+ Logger.warn("Log writer out of order time detected (increase buffer depth?): #{Time.from_nsec_from_epoch(@previous_time_nsec_since_epoch)} #{Time.from_nsec_from_epoch(time_nsec_since_epoch)}")
334
+ @out_of_order = true
335
+ end
308
336
  end
309
337
  end
310
338
  end
311
339
  @last_offsets[redis_topic] = redis_offset if redis_topic and redis_offset # This is needed for the redis offset marker entry at the end of the log file
312
- @previous_time_nsec_since_epoch = time_nsec_since_epoch if process_out_of_order
340
+ if process_out_of_order
341
+ @current_file_valid = packet_valid
342
+ if packet_valid
343
+ @previous_time_nsec_since_epoch = time_nsec_since_epoch
344
+ elsif !@bad_time
345
+ Logger.warn("Log writer segregating invalid packet time (before clock sync?): #{Time.from_nsec_from_epoch(time_nsec_since_epoch)}")
346
+ @bad_time = true
347
+ end
348
+ end
313
349
  end
314
350
 
315
351
  # Closing a log file isn't critical so we just log an error. NOTE: This also trims the Redis stream
@@ -96,13 +96,12 @@ module OpenC3
96
96
  @mutex.lock if take_mutex
97
97
  begin
98
98
  if entry_type == :RAW_PACKET or entry_type == :JSON_PACKET
99
- # Only care about the timestamps on the real packets being in order
100
99
  process_out_of_order = true
101
100
  else
102
101
  # Metadata timestamps don't matter
103
102
  process_out_of_order = false
104
103
  end
105
- prepare_write(time_nsec_since_epoch, data.length, redis_topic, redis_offset, allow_new_file: allow_new_file, process_out_of_order: process_out_of_order)
104
+ prepare_write(time_nsec_since_epoch, data.length, redis_topic, redis_offset, allow_new_file: allow_new_file, process_out_of_order: process_out_of_order, stored: stored)
106
105
  write_entry(entry_type, cmd_or_tlm, target_name, packet_name, time_nsec_since_epoch, stored, data, id, received_time_nsec_since_epoch: received_time_nsec_since_epoch, extra: extra) if @file
107
106
  ensure
108
107
  @mutex.unlock if take_mutex
@@ -1,4 +1,4 @@
1
- # encoding: ascii-8bit
1
+ # encoding: utf-8
2
2
 
3
3
  # Copyright 2026 OpenC3, Inc.
4
4
  # All Rights Reserved.
@@ -35,13 +35,19 @@ module OpenC3
35
35
  # (microservice name, or "REINGEST:<job_id>").
36
36
  # @param check_limits [Boolean] When false, skips the Packet#check_limits call.
37
37
  # Reingest passes false so historical data does not re-fire limits events.
38
+ # @param stored_limits_mode [String] Controls limits handling for stored packets.
39
+ # 'PROCESS' (default) processes limits normally.
40
+ # 'LOG' still evaluates limits (callback handles suppressing reactions).
41
+ # 'DISABLE' skips limits evaluation entirely for stored packets and omits
42
+ # limits states from the decommutated output.
38
43
  # @param metric [Metric, nil] Optional; when set, records decom_duration_seconds.
39
44
  # @param error_callback [Proc, nil] Called as error_callback.call(exception) when
40
45
  # Packet#process or Packet#check_limits raises. The microservice uses this to
41
46
  # bump its decom_error_total metric.
42
47
  # @return [Integer] Number of (sub)packets published.
43
48
  def decom_and_publish(packet, scope:, target_names:, logger:, name:,
44
- check_limits: true, metric: nil, error_callback: nil)
49
+ check_limits: true, stored_limits_mode: 'PROCESS',
50
+ metric: nil, error_callback: nil)
45
51
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if metric
46
52
  published = 0
47
53
 
@@ -62,9 +68,14 @@ module OpenC3
62
68
  logger.error e.message
63
69
  end
64
70
 
65
- packet_or_subpacket.check_limits(System.limits_set) if check_limits
71
+ disable_stored_limits = packet_or_subpacket.stored && stored_limits_mode == 'DISABLE'
72
+ if check_limits && !disable_stored_limits
73
+ packet_or_subpacket.check_limits(System.limits_set)
74
+ end
66
75
 
67
- TelemetryDecomTopic.write_packet(packet_or_subpacket, scope: scope)
76
+ TelemetryDecomTopic.write_packet(packet_or_subpacket,
77
+ include_limits_states: !disable_stored_limits,
78
+ scope: scope)
68
79
  published += 1
69
80
  end
70
81
 
@@ -100,6 +100,8 @@ module OpenC3
100
100
  @limits_response_thread = nil
101
101
  System.telemetry.limits_change_callback = method(:limits_change_callback)
102
102
  LimitsEventTopic.sync_system(scope: @scope)
103
+ target_model = TargetModel.get_model(name: @target_names[0], scope: @scope)
104
+ @stored_limits_mode = target_model ? (target_model.stored_limits_mode || 'PROCESS') : 'PROCESS'
103
105
  @error_count = 0
104
106
  @metric.set(name: 'decom_total', value: @count, type: 'counter')
105
107
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
@@ -184,6 +186,7 @@ module OpenC3
184
186
  logger: @logger,
185
187
  name: @name,
186
188
  check_limits: true,
189
+ stored_limits_mode: @stored_limits_mode,
187
190
  metric: @metric,
188
191
  error_callback: ->(e) {
189
192
  @error_count += 1
@@ -253,9 +256,11 @@ module OpenC3
253
256
  event = { type: :LIMITS_CHANGE, target_name: packet.target_name, packet_name: packet.packet_name,
254
257
  item_name: item.name, old_limits_state: old_limits_state.to_s, new_limits_state: item.limits.state.to_s,
255
258
  time_nsec: packet_time.to_nsec_from_epoch, message: message.to_s }
259
+ suppress_stored = packet.stored && @stored_limits_mode != 'PROCESS'
260
+ event[:suppress_stored] = true if suppress_stored
256
261
  LimitsEventTopic.write(event, scope: @scope)
257
262
 
258
- if item.limits.response
263
+ if item.limits.response && !suppress_stored
259
264
  copied_packet = packet.deep_copy
260
265
  copied_item = packet.items[item.name]
261
266
  @limits_response_queue << [copied_packet, copied_item, old_limits_state]