openc3 7.1.1 → 7.2.1
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/bin/openc3cli +11 -23
- data/data/config/command_modifiers.yaml +2 -2
- data/data/config/interface_modifiers.yaml +15 -0
- data/data/config/item_modifiers.yaml +10 -3
- data/data/config/target.yaml +17 -0
- data/data/config/widgets.yaml +28 -1
- data/lib/openc3/accessors/binary_accessor.rb +4 -0
- data/lib/openc3/api/cmd_api.rb +5 -1
- data/lib/openc3/api/limits_api.rb +72 -0
- data/lib/openc3/api/tlm_api.rb +6 -0
- data/lib/openc3/microservices/decom_common.rb +15 -4
- data/lib/openc3/microservices/decom_microservice.rb +6 -1
- data/lib/openc3/microservices/interface_microservice.rb +25 -11
- data/lib/openc3/microservices/microservice.rb +20 -5
- data/lib/openc3/microservices/queue_microservice.rb +4 -2
- data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
- data/lib/openc3/models/cvt_model.rb +2 -2
- data/lib/openc3/models/db_sharded_model.rb +2 -2
- data/lib/openc3/models/interface_model.rb +16 -10
- data/lib/openc3/models/interface_status_model.rb +2 -2
- data/lib/openc3/models/metric_model.rb +2 -2
- data/lib/openc3/models/microservice_status_model.rb +2 -2
- data/lib/openc3/models/model.rb +2 -6
- data/lib/openc3/models/plugin_model.rb +15 -14
- data/lib/openc3/models/python_package_model.rb +3 -1
- data/lib/openc3/models/reingest_job_model.rb +1 -1
- data/lib/openc3/models/script_status_model.rb +73 -38
- data/lib/openc3/models/target_model.rb +13 -0
- data/lib/openc3/operators/operator.rb +49 -14
- data/lib/openc3/packets/commands.rb +3 -3
- data/lib/openc3/packets/limits.rb +23 -0
- data/lib/openc3/packets/packet.rb +5 -3
- data/lib/openc3/packets/packet_config.rb +22 -5
- data/lib/openc3/packets/structure.rb +14 -10
- data/lib/openc3/script/limits.rb +1 -1
- data/lib/openc3/script/suite.rb +1 -1
- data/lib/openc3/script/suite_runner.rb +43 -9
- data/lib/openc3/script/web_socket_api.rb +6 -2
- data/lib/openc3/system/target.rb +4 -1
- data/lib/openc3/tools/table_manager/table_config.rb +4 -1
- data/lib/openc3/top_level.rb +1 -1
- data/lib/openc3/topics/limits_event_topic.rb +62 -3
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
- data/lib/openc3/utilities/cli_generator.rb +432 -404
- data/lib/openc3/utilities/env_helper.rb +13 -1
- data/lib/openc3/utilities/pypi_url.rb +39 -0
- data/lib/openc3/utilities/questdb_client.rb +51 -4
- data/lib/openc3/utilities/reingest_job.rb +1 -1
- data/lib/openc3/utilities/running_script.rb +58 -15
- data/lib/openc3/utilities/script.rb +14 -3
- data/lib/openc3/utilities/simulated_target.rb +4 -2
- data/lib/openc3/utilities/store_autoload.rb +39 -1
- data/lib/openc3/utilities/store_queued.rb +5 -1
- data/lib/openc3/version.rb +5 -5
- data/lib/openc3.rb +6 -1
- data/templates/command_validator/command_validator.py +8 -10
- data/templates/command_validator/command_validator.rb +6 -9
- data/templates/plugin/LICENSE.md +16 -4
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13ecd5ed69a40943e0ceb4732af0922106d73ba3e28c7e10521e3cbcbb2728c9
|
|
4
|
+
data.tar.gz: e8a55a75e964de92cdc8fa9a27091d472437004a0775a05c894d4a16d5232677
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77c7ed5bcae0862bf8d7ccdeefbc79cdc814b04501831e5de5a2b7b1d685420f731da2bb230435a1d091697156ab1d58a40cf793869da48e191b7c87ccbffea9
|
|
7
|
+
data.tar.gz: 90571e3012ae117610314df3d32d67d77a843f2de8edb2c9b51bab3335cf0618e7301a9ba0c55616acad6a2d0ca1b4634924531f657bd438c0fd490aeca6a327
|
data/bin/openc3cli
CHANGED
|
@@ -671,30 +671,18 @@ def cli_script_list(args, options)
|
|
|
671
671
|
end
|
|
672
672
|
|
|
673
673
|
def parse_suite_runner_options(options)
|
|
674
|
-
suite_runner = nil
|
|
675
674
|
# suite must be given to enable Suite Runner execution
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
suite_runner['method'] = options[:method]
|
|
688
|
-
else
|
|
689
|
-
suite_runner['method'] = 'start'
|
|
690
|
-
end
|
|
691
|
-
if options[:options]
|
|
692
|
-
suite_runner['options'] = options[:options].split(',')
|
|
693
|
-
else
|
|
694
|
-
suite_runner['options'] = ["continueAfterError"]
|
|
695
|
-
end
|
|
696
|
-
end
|
|
697
|
-
return suite_runner
|
|
675
|
+
return nil unless options[:suite]
|
|
676
|
+
require 'openc3/script/suite_runner'
|
|
677
|
+
# Build the canonical hash via the shared helper so the CLI applies the same
|
|
678
|
+
# shape, defaults, and validation (e.g. script requires group) as execution.
|
|
679
|
+
return OpenC3::SuiteRunner.build_options(
|
|
680
|
+
suite: options[:suite],
|
|
681
|
+
group: options[:group],
|
|
682
|
+
script: options[:script],
|
|
683
|
+
method: options[:method],
|
|
684
|
+
options: options[:options]&.split(','),
|
|
685
|
+
)
|
|
698
686
|
end
|
|
699
687
|
|
|
700
688
|
def cli_script_run(args, options)
|
|
@@ -417,12 +417,12 @@ VALIDATOR:
|
|
|
417
417
|
return [False, "TGT PKT ITEM is 0"]
|
|
418
418
|
self.cmd_acpt_cnt = tlm("INST HEALTH_STATUS CMD_ACPT_CNT")
|
|
419
419
|
# Return true to indicate Success, false to indicate Failure,
|
|
420
|
-
# and
|
|
420
|
+
# and None to indicate Unknown. The second value is the optional message.
|
|
421
421
|
return [True, None]
|
|
422
422
|
|
|
423
423
|
def post_check(self, command):
|
|
424
424
|
wait_check(f"INST HEALTH_STATUS CMD_ACPT_CNT > {self.cmd_acpt_cnt}", 10)
|
|
425
425
|
# Return true to indicate Success, false to indicate Failure,
|
|
426
|
-
# and
|
|
426
|
+
# and None to indicate Unknown. The second value is the optional message.
|
|
427
427
|
return [True, None]
|
|
428
428
|
since: 5.19.0
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
MAP_TARGET:
|
|
3
3
|
summary: Maps a target name to an interface
|
|
4
|
+
description: See [Mapping Targets to Interfaces](/docs/configuration/interfaces#mapping-targets-to-interfaces) for more information.
|
|
4
5
|
parameters:
|
|
5
6
|
- name: Target Name
|
|
6
7
|
required: true
|
|
7
8
|
description: Target name to map to this interface
|
|
8
9
|
values: .+
|
|
10
|
+
- name: Enabled State
|
|
11
|
+
required: false
|
|
12
|
+
description: Initial enabled state of the target on this interface. Defaults to ENABLED.
|
|
13
|
+
values: ["ENABLED", "DISABLED"]
|
|
9
14
|
ruby_example: |
|
|
10
15
|
INTERFACE DATA_INT tcpip_client_interface.rb host.docker.internal 8080 8081 10.0 nil BURST
|
|
11
16
|
MAP_TARGET DATA
|
|
@@ -14,12 +19,17 @@ MAP_TARGET:
|
|
|
14
19
|
MAP_TARGET DATA
|
|
15
20
|
MAP_CMD_TARGET:
|
|
16
21
|
summary: Maps a target name to an interface for commands only
|
|
22
|
+
description: See [Mapping Targets to Interfaces](/docs/configuration/interfaces#mapping-targets-to-interfaces) for more information.
|
|
17
23
|
since: 5.2.0
|
|
18
24
|
parameters:
|
|
19
25
|
- name: Target Name
|
|
20
26
|
required: true
|
|
21
27
|
description: Command target name to map to this interface
|
|
22
28
|
values: .+
|
|
29
|
+
- name: Enabled State
|
|
30
|
+
required: false
|
|
31
|
+
description: Initial enabled state of the command target on this interface. Defaults to ENABLED.
|
|
32
|
+
values: ["ENABLED", "DISABLED"]
|
|
23
33
|
ruby_example: |
|
|
24
34
|
INTERFACE CMD_INT tcpip_client_interface.rb host.docker.internal 8080 8081 10.0 nil BURST
|
|
25
35
|
MAP_CMD_TARGET DATA # Only DATA commands go on the CMD_INT interface
|
|
@@ -28,12 +38,17 @@ MAP_CMD_TARGET:
|
|
|
28
38
|
MAP_CMD_TARGET DATA # Only DATA commands go on the CMD_INT interface
|
|
29
39
|
MAP_TLM_TARGET:
|
|
30
40
|
summary: Maps a target name to an interface for telemetry only
|
|
41
|
+
description: See [Mapping Targets to Interfaces](/docs/configuration/interfaces#mapping-targets-to-interfaces) for more information.
|
|
31
42
|
since: 5.2.0
|
|
32
43
|
parameters:
|
|
33
44
|
- name: Target Name
|
|
34
45
|
required: true
|
|
35
46
|
description: Telemetry target name to map to this interface
|
|
36
47
|
values: .+
|
|
48
|
+
- name: Enabled State
|
|
49
|
+
required: false
|
|
50
|
+
description: Initial enabled state of the telemetry target on this interface. Defaults to ENABLED.
|
|
51
|
+
values: ["ENABLED", "DISABLED"]
|
|
37
52
|
ruby_example: |
|
|
38
53
|
INTERFACE TLM_INT tcpip_client_interface.rb host.docker.internal 8080 8081 10.0 nil BURST
|
|
39
54
|
MAP_TLM_TARGET DATA # Only DATA telemetry received on TLM_INT interface
|
|
@@ -111,20 +111,27 @@ CONVERTED_DATA:
|
|
|
111
111
|
values: \d+
|
|
112
112
|
LIMITS:
|
|
113
113
|
summary: Defines a set of limits for a telemetry item
|
|
114
|
-
description:
|
|
114
|
+
description: |
|
|
115
|
+
If limits are violated a message is printed in the Command and Telemetry Server
|
|
115
116
|
to indicate an item went out of limits. Other tools also use this information
|
|
116
117
|
to update displays with different colored telemetry items or other useful information.
|
|
117
118
|
The concept of "limits sets" is defined to allow for different limits values
|
|
118
119
|
in different environments. For example, you might want tighter or looser limits
|
|
119
120
|
on telemetry if your environment changes such as during thermal vacuum testing.
|
|
121
|
+
|
|
122
|
+
A DEFAULT limits set is required for every telemetry item with limits. If you
|
|
123
|
+
define additional named sets (e.g. TVAC), the DEFAULT set must be defined first.
|
|
124
|
+
Attempting to define a named set before DEFAULT will raise an error of the form
|
|
125
|
+
"DEFAULT limits set must be defined for TARGET PACKET ITEM before setting limits set NAME".
|
|
120
126
|
example: |
|
|
121
127
|
LIMITS DEFAULT 3 ENABLED -80.0 -70.0 60.0 80.0 -20.0 20.0
|
|
122
128
|
LIMITS TVAC 3 ENABLED -80.0 -30.0 30.0 80.0
|
|
123
129
|
parameters:
|
|
124
130
|
- name: Limits Set
|
|
125
131
|
required: true
|
|
126
|
-
description: Name of the limits set.
|
|
127
|
-
|
|
132
|
+
description: Name of the limits set. A DEFAULT set is required and must be
|
|
133
|
+
defined before any other named sets for this item. If you have no unique
|
|
134
|
+
limits sets use the keyword DEFAULT.
|
|
128
135
|
values: .+
|
|
129
136
|
- name: Persistence
|
|
130
137
|
required: true
|
data/data/config/target.yaml
CHANGED
|
@@ -170,3 +170,20 @@ TARGET:
|
|
|
170
170
|
values: \d+
|
|
171
171
|
example: |
|
|
172
172
|
DB_SHARD 0
|
|
173
|
+
STORED_LIMITS_MODE:
|
|
174
|
+
summary: Controls how limits are evaluated for stored (non-real-time) telemetry packets
|
|
175
|
+
description:
|
|
176
|
+
Sets the limits handling policy for packets where the stored flag is true
|
|
177
|
+
(e.g., packets from file interfaces or historical data replay).
|
|
178
|
+
PROCESS processes limits normally including logging and reactions.
|
|
179
|
+
LOG evaluates limits and logs state changes but does not trigger limits reactions
|
|
180
|
+
or update the current limits state used by the API.
|
|
181
|
+
DISABLE skips limits processing entirely for stored packets.
|
|
182
|
+
since: 7.2.1
|
|
183
|
+
parameters:
|
|
184
|
+
- name: Mode
|
|
185
|
+
required: true
|
|
186
|
+
description: "PROCESS (default), LOG, or DISABLE"
|
|
187
|
+
values: ^(PROCESS|LOG|DISABLE)$
|
|
188
|
+
example: |
|
|
189
|
+
STORED_LIMITS_MODE DISABLE
|
data/data/config/widgets.yaml
CHANGED
|
@@ -1579,7 +1579,10 @@ Canvas Widgets:
|
|
|
1579
1579
|
collection:
|
|
1580
1580
|
CANVAS:
|
|
1581
1581
|
summary: Layout widget for the other canvas widgets
|
|
1582
|
-
description:
|
|
1582
|
+
description:
|
|
1583
|
+
All canvas widgets must be enclosed within a CANVAS widget.
|
|
1584
|
+
The canvas background defaults to white. Use SETTING BACKCOLOR to set a
|
|
1585
|
+
different background color.
|
|
1583
1586
|
warning: The canvas coordinate frame places (0,0) in the upper-left corner of the canvas.
|
|
1584
1587
|
parameters:
|
|
1585
1588
|
- name: Width
|
|
@@ -1590,6 +1593,30 @@ Canvas Widgets:
|
|
|
1590
1593
|
required: true
|
|
1591
1594
|
description: Height of the canvas
|
|
1592
1595
|
values: .+
|
|
1596
|
+
settings:
|
|
1597
|
+
BACKCOLOR:
|
|
1598
|
+
summary: Sets the background color of the canvas
|
|
1599
|
+
since: 7.2.1
|
|
1600
|
+
parameters:
|
|
1601
|
+
- name: Color name or Red value
|
|
1602
|
+
required: true
|
|
1603
|
+
description: Common name for the color, e.g. 'black', 'red', etc.
|
|
1604
|
+
Alternatively if two more parameters are passed this is the
|
|
1605
|
+
Red value of the RGB value
|
|
1606
|
+
values: .+
|
|
1607
|
+
- name: Green value
|
|
1608
|
+
required: false
|
|
1609
|
+
description: Green value of the RGB value
|
|
1610
|
+
values: .+
|
|
1611
|
+
- name: Blue value
|
|
1612
|
+
required: false
|
|
1613
|
+
description: Blue value of the RGB value
|
|
1614
|
+
values: .+
|
|
1615
|
+
example: |
|
|
1616
|
+
CANVAS 100 50
|
|
1617
|
+
SETTING BACKCOLOR 17 21 28
|
|
1618
|
+
CANVASLABEL 5 30 "Dark canvas" 18 white
|
|
1619
|
+
END
|
|
1593
1620
|
CANVASLABEL:
|
|
1594
1621
|
summary: Draws text onto the canvas
|
|
1595
1622
|
parameters:
|
|
@@ -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
|
data/lib/openc3/api/cmd_api.rb
CHANGED
|
@@ -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?
|
|
@@ -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
|
|
|
@@ -251,6 +254,50 @@ 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] New color for the state. Must be GREEN, YELLOW, or RED.
|
|
266
|
+
def set_state_color(target_name, packet_name, item_name, state_name, color,
|
|
267
|
+
manual: false, scope: $openc3_scope, token: $openc3_token)
|
|
268
|
+
authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
|
|
269
|
+
state_name = state_name.to_s.upcase
|
|
270
|
+
color = color.to_s.upcase
|
|
271
|
+
unless PacketItem::STATE_COLORS.include?(color.intern)
|
|
272
|
+
raise "Invalid state color #{color}. Must be one of #{PacketItem::STATE_COLORS.join(' ')}."
|
|
273
|
+
end
|
|
274
|
+
packet = TargetModel.packet(target_name, packet_name, scope: scope)
|
|
275
|
+
found_item = nil
|
|
276
|
+
packet['items'].each do |item|
|
|
277
|
+
if item['name'] == item_name
|
|
278
|
+
unless item['states'] && item['states'][state_name]
|
|
279
|
+
raise "State '#{state_name}' does not exist for item '#{target_name} #{packet_name} #{item_name}'"
|
|
280
|
+
end
|
|
281
|
+
item['states'][state_name]['color'] = color
|
|
282
|
+
# Defining a state color implies limits are enabled for the item
|
|
283
|
+
item['limits'] ||= {}
|
|
284
|
+
item['limits']['enabled'] = true
|
|
285
|
+
found_item = item
|
|
286
|
+
break
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
raise "Item '#{target_name} #{packet_name} #{item_name}' does not exist" unless found_item
|
|
290
|
+
message = "Setting '#{target_name} #{packet_name} #{item_name}' state #{state_name} color to #{color}"
|
|
291
|
+
Logger.info(message, scope: scope)
|
|
292
|
+
|
|
293
|
+
TargetModel.set_packet(target_name, packet_name, packet, scope: scope)
|
|
294
|
+
|
|
295
|
+
event = { type: :LIMITS_STATE_COLOR, target_name: target_name, packet_name: packet_name,
|
|
296
|
+
item_name: item_name, state_name: state_name, color: color,
|
|
297
|
+
time_nsec: Time.now.to_nsec_from_epoch, message: message }
|
|
298
|
+
LimitsEventTopic.write(event, scope: scope)
|
|
299
|
+
end
|
|
300
|
+
|
|
254
301
|
# Returns all limits_groups and their members
|
|
255
302
|
# @since 5.0.0 Returns hash with values
|
|
256
303
|
# @return [Hash{String => Array<Array<String, String, String>>]
|
|
@@ -292,6 +339,31 @@ module OpenC3
|
|
|
292
339
|
time_nsec: Time.now.to_nsec_from_epoch, message: message }, scope: scope)
|
|
293
340
|
end
|
|
294
341
|
|
|
342
|
+
# Deletes a limits set. The DEFAULT limits set and the currently active
|
|
343
|
+
# limits set cannot be deleted. Use set_limits_set to change the active set
|
|
344
|
+
# before deleting it. Note that the limits set is not removed from the
|
|
345
|
+
# TargetModel packet definitions; that is cleaned up on the next plugin
|
|
346
|
+
# install.
|
|
347
|
+
#
|
|
348
|
+
# @param limits_set [String] The name of the limits set to delete
|
|
349
|
+
def delete_limits_set(limits_set, manual: false, scope: $openc3_scope, token: $openc3_token)
|
|
350
|
+
authorize(permission: 'tlm_set', manual: manual, scope: scope, token: token)
|
|
351
|
+
limits_set = limits_set.to_s
|
|
352
|
+
raise "Cannot delete the DEFAULT limits set" if limits_set == 'DEFAULT'
|
|
353
|
+
if limits_set == LimitsEventTopic.current_set(scope: scope)
|
|
354
|
+
raise "Cannot delete the current limits set '#{limits_set}'. Use set_limits_set to change the current set first."
|
|
355
|
+
end
|
|
356
|
+
unless LimitsEventTopic.sets(scope: scope).key?(limits_set)
|
|
357
|
+
raise "Limits set '#{limits_set}' does not exist"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Remove the limits set from Redis (limits_sets and current_limits_settings)
|
|
361
|
+
LimitsEventTopic.delete_set(limits_set, scope: scope)
|
|
362
|
+
|
|
363
|
+
message = "Deleting Limits Set: #{limits_set}"
|
|
364
|
+
Logger.info(message, scope: scope)
|
|
365
|
+
end
|
|
366
|
+
|
|
295
367
|
# Returns the active limits set that applies to all telemetry
|
|
296
368
|
#
|
|
297
369
|
# @return [String] The current limits set
|
data/lib/openc3/api/tlm_api.rb
CHANGED
|
@@ -457,6 +457,12 @@ module OpenC3
|
|
|
457
457
|
alias subscribe_packet subscribe_packets
|
|
458
458
|
|
|
459
459
|
# Get packets based on ID returned from subscribe_packet.
|
|
460
|
+
# Packets are ordered within each subscribed packet stream (target/packet pair)
|
|
461
|
+
# but are NOT interleaved by time across streams. If chronological order across
|
|
462
|
+
# streams is required, sort the returned array by the 'time' field. Sorting only
|
|
463
|
+
# orders the current batch - packets across separate get_packets calls may still
|
|
464
|
+
# arrive out of order, so subscribers needing global ordering must buffer and merge
|
|
465
|
+
# across calls.
|
|
460
466
|
# @param id [String] ID returned from subscribe_packets or last call to get_packets
|
|
461
467
|
# @param block [Integer] Unused - Blocking must be implemented at the client
|
|
462
468
|
# @param count [Integer] Maximum number of packets to return from EACH packet stream
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# encoding:
|
|
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,
|
|
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.
|
|
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,
|
|
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]
|
|
@@ -272,6 +272,11 @@ module OpenC3
|
|
|
272
272
|
else
|
|
273
273
|
next nil # Don't ack disabled targets
|
|
274
274
|
end
|
|
275
|
+
rescue RangeError => e
|
|
276
|
+
# Command parameter out of range is a user error, not a bug,
|
|
277
|
+
# so only log the message and not the full stack trace
|
|
278
|
+
@logger.error "#{@interface.name}: #{e.message}"
|
|
279
|
+
next e.message
|
|
275
280
|
rescue => e
|
|
276
281
|
@logger.error "#{@interface.name}: #{msg_hash}"
|
|
277
282
|
@logger.error "#{@interface.name}: #{e.formatted}"
|
|
@@ -281,6 +286,9 @@ module OpenC3
|
|
|
281
286
|
command.extra ||= {}
|
|
282
287
|
command.extra['cmd_string'] = msg_hash['cmd_string']
|
|
283
288
|
command.extra['username'] = msg_hash['username']
|
|
289
|
+
command.extra['interface_name'] = @interface.name
|
|
290
|
+
# Original author of a queued command (set when released from a queue)
|
|
291
|
+
command.extra['queue_username'] = msg_hash['queue_username'] if msg_hash['queue_username']
|
|
284
292
|
# Add approver info if this was a critical command that was approved
|
|
285
293
|
if critical_model
|
|
286
294
|
command.extra['approver'] = critical_model.approver
|
|
@@ -706,16 +714,19 @@ module OpenC3
|
|
|
706
714
|
# Try to do clean disconnect because we're going down
|
|
707
715
|
disconnect(false)
|
|
708
716
|
end
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
717
|
+
unless @cancel_thread
|
|
718
|
+
if @interface_or_router == 'INTERFACE'
|
|
719
|
+
InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
|
|
720
|
+
else
|
|
721
|
+
RouterStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
|
|
722
|
+
end
|
|
713
723
|
end
|
|
714
724
|
@logger.info "#{@interface.name}: Stopped packet reading"
|
|
715
725
|
end
|
|
716
726
|
|
|
717
727
|
def handle_packet(packet)
|
|
718
|
-
|
|
728
|
+
# Skip status update if stop() has been called to avoid re-creating the status model
|
|
729
|
+
InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope) unless @cancel_thread
|
|
719
730
|
packet.received_time = Time.now.sys unless packet.received_time
|
|
720
731
|
|
|
721
732
|
if packet.stored
|
|
@@ -853,7 +864,8 @@ module OpenC3
|
|
|
853
864
|
|
|
854
865
|
# If the interface is set to auto_reconnect then delay so the thread
|
|
855
866
|
# can come back around and allow the interface a chance to reconnect.
|
|
856
|
-
if
|
|
867
|
+
# Skip reconnect if stop() has been called to avoid re-creating the status model
|
|
868
|
+
if allow_reconnect and @interface.auto_reconnect and @interface.state != 'DISCONNECTED' and !@cancel_thread
|
|
857
869
|
attempting()
|
|
858
870
|
if !@cancel_thread
|
|
859
871
|
# @logger.debug "reconnect delay: #{@interface.reconnect_delay}"
|
|
@@ -861,10 +873,12 @@ module OpenC3
|
|
|
861
873
|
end
|
|
862
874
|
else
|
|
863
875
|
@interface.state = 'DISCONNECTED'
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
876
|
+
unless @cancel_thread
|
|
877
|
+
if @interface_or_router == 'INTERFACE'
|
|
878
|
+
InterfaceStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
|
|
879
|
+
else
|
|
880
|
+
RouterStatusModel.set(@interface.as_json(), queued: true, scope: @scope)
|
|
881
|
+
end
|
|
868
882
|
end
|
|
869
883
|
end
|
|
870
884
|
end
|
|
@@ -914,4 +928,4 @@ if __FILE__ == $0
|
|
|
914
928
|
OpenC3::InterfaceMicroservice.run
|
|
915
929
|
OpenC3::ThreadManager.instance.shutdown
|
|
916
930
|
OpenC3::ThreadManager.instance.join
|
|
917
|
-
end
|
|
931
|
+
end
|
|
@@ -155,11 +155,26 @@ module OpenC3
|
|
|
155
155
|
|
|
156
156
|
prefix = "#{@scope}/microservices/#{@name}/"
|
|
157
157
|
file_count = 0
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
# Tolerate transient object store failures during startup. On a busy or
|
|
159
|
+
# underpowered cluster the bucket store (e.g. MinIO) can be briefly
|
|
160
|
+
# unreachable while many microservices start at once. Rather than crash
|
|
161
|
+
# and CrashLoopBackOff (which can outlast deploy timeouts), retry for a
|
|
162
|
+
# bounded time before giving up.
|
|
163
|
+
startup_timeout = (ENV['OPENC3_MICROSERVICE_STARTUP_BUCKET_TIMEOUT'] || 60).to_f
|
|
164
|
+
startup_deadline = Time.now + startup_timeout
|
|
165
|
+
begin
|
|
166
|
+
file_count = 0
|
|
167
|
+
client.list_objects(bucket: bucket, prefix: prefix).each do |object|
|
|
168
|
+
response_target = OpenC3.sanitize_path(File.join(@temp_dir, object.key.split(prefix)[-1]))
|
|
169
|
+
FileUtils.mkdir_p(File.dirname(response_target))
|
|
170
|
+
client.get_object(bucket: bucket, key: object.key, path: response_target)
|
|
171
|
+
file_count += 1
|
|
172
|
+
end
|
|
173
|
+
rescue => error
|
|
174
|
+
raise if Time.now >= startup_deadline
|
|
175
|
+
@logger.warn("Microservice #{@name} startup: bucket access failed (#{error.class}: #{error.message}); retrying for up to #{startup_timeout.to_i}s")
|
|
176
|
+
sleep(5)
|
|
177
|
+
retry
|
|
163
178
|
end
|
|
164
179
|
|
|
165
180
|
# Adjust @work_dir to microservice files downloaded if files and a relative path
|
|
@@ -76,12 +76,14 @@ module OpenC3
|
|
|
76
76
|
end
|
|
77
77
|
validate = command.key?('validate') ? command['validate'] : true
|
|
78
78
|
timeout = command['timeout']
|
|
79
|
-
|
|
79
|
+
# Pass queue_username so Command History is attributed to the original
|
|
80
|
+
# author rather than the queue microservice name
|
|
81
|
+
cmd(command['target_name'], command['cmd_name'], cmd_params, queue: false, validate: validate, timeout: timeout, queue_username: command['username'], scope: @scope)
|
|
80
82
|
elsif command['value']
|
|
81
83
|
# Legacy format: use single string parameter for backwards compatibility
|
|
82
84
|
validate = command.key?('validate') ? command['validate'] : true
|
|
83
85
|
timeout = command['timeout']
|
|
84
|
-
cmd(command['value'], queue: false, validate: validate, timeout: timeout, scope: @scope)
|
|
86
|
+
cmd(command['value'], queue: false, validate: validate, timeout: timeout, queue_username: command['username'], scope: @scope)
|
|
85
87
|
else
|
|
86
88
|
@logger.error "QueueProcessor: Invalid command format, missing required fields"
|
|
87
89
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# encoding: ascii-8bit
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 OpenC3, Inc.
|
|
4
|
+
# All Rights Reserved.
|
|
5
|
+
#
|
|
6
|
+
# This program is distributed in the hope that it will be useful,
|
|
7
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
8
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
9
|
+
# See LICENSE.md for more details.
|
|
10
|
+
#
|
|
11
|
+
# This file may also be used under the terms of a commercial license
|
|
12
|
+
# if purchased from OpenC3, Inc.
|
|
13
|
+
|
|
14
|
+
require 'time'
|
|
15
|
+
require 'openc3/utilities/migration'
|
|
16
|
+
require 'openc3/core_ext/time'
|
|
17
|
+
require 'openc3/models/scope_model'
|
|
18
|
+
require 'openc3/models/script_status_model'
|
|
19
|
+
|
|
20
|
+
module OpenC3
|
|
21
|
+
# Earlier versions of the Python script runner wrote the ScriptStatusModel
|
|
22
|
+
# updated_at field as an ISO8601 string (e.g. "2026-06-30T12:34:56.789012Z")
|
|
23
|
+
# instead of integer nanoseconds since the epoch. This migration rewrites any
|
|
24
|
+
# such string values as integers so updated_at is consistently an int.
|
|
25
|
+
#
|
|
26
|
+
# NOTE: The stored JSON is edited in place rather than reloaded through the
|
|
27
|
+
# model and re-created, because ScriptStatusModel#create always sets
|
|
28
|
+
# updated_at to the current time, which would discard the original timestamp.
|
|
29
|
+
class UpdatedAtToInt < Migration
|
|
30
|
+
def self.run
|
|
31
|
+
ScopeModel.names.each do |scope|
|
|
32
|
+
[ScriptStatusModel::RUNNING_PRIMARY_KEY, ScriptStatusModel::COMPLETED_PRIMARY_KEY].each do |key|
|
|
33
|
+
primary_key = "#{key}__#{scope}"
|
|
34
|
+
Store.hgetall(primary_key).each do |name, json|
|
|
35
|
+
data = JSON.parse(json, allow_nan: true)
|
|
36
|
+
updated_at = data['updated_at']
|
|
37
|
+
next unless updated_at.is_a?(String)
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
data['updated_at'] = Time.parse(updated_at).to_nsec_from_epoch
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
# Leave unparsable values untouched
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
Store.hset(primary_key, name, JSON.generate(data, allow_nan: true))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless ENV['OPENC3_NO_MIGRATE']
|
|
54
|
+
OpenC3::UpdatedAtToInt.run
|
|
55
|
+
end
|
|
@@ -26,8 +26,8 @@ module OpenC3
|
|
|
26
26
|
@@override_cache = {}
|
|
27
27
|
|
|
28
28
|
VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED]
|
|
29
|
-
def self.build_json_from_packet(packet)
|
|
30
|
-
packet.decom
|
|
29
|
+
def self.build_json_from_packet(packet, include_limits_states: true)
|
|
30
|
+
packet.decom(include_limits_states: include_limits_states)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Get a Store instance routed to the correct db_shard for a target
|