openc3 7.2.0 → 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/interface_modifiers.yaml +15 -0
- 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/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/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 +15 -5
- 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 +5 -1
- data/lib/openc3/packets/structure.rb +14 -10
- data/lib/openc3/script/limits.rb +1 -1
- data/lib/openc3/script/suite_runner.rb +43 -9
- data/lib/openc3/script/web_socket_api.rb +1 -1
- 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 +5 -1
- data/lib/openc3/utilities/env_helper.rb +13 -1
- data/lib/openc3/utilities/pypi_url.rb +39 -0
- data/lib/openc3/utilities/reingest_job.rb +1 -1
- data/lib/openc3/utilities/running_script.rb +17 -12
- data/lib/openc3/utilities/script.rb +14 -3
- 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/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)
|
|
@@ -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
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -79,7 +79,7 @@ module OpenC3
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# DB_Shard-aware create: writes to the store instance for the given db_shard.
|
|
82
|
-
def _db_sharded_create(db_shard, update: false, force: false, queued: false,
|
|
82
|
+
def _db_sharded_create(db_shard, update: false, force: false, queued: false, expire_seconds: nil)
|
|
83
83
|
db_shard_store = self.class.store.instance(db_shard: db_shard)
|
|
84
84
|
unless force
|
|
85
85
|
existing = db_shard_store.hget(@primary_key, @name)
|
|
@@ -89,7 +89,7 @@ module OpenC3
|
|
|
89
89
|
raise RuntimeError.new("#{@primary_key}:#{@name} doesn't exist at update") if update
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
|
-
@updated_at =
|
|
92
|
+
@updated_at = Time.now.utc.to_nsec_from_epoch
|
|
93
93
|
|
|
94
94
|
if queued
|
|
95
95
|
store = self.class.store_queued.instance(db_shard: db_shard)
|
|
@@ -444,9 +444,11 @@ module OpenC3
|
|
|
444
444
|
microservice
|
|
445
445
|
end
|
|
446
446
|
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
#
|
|
447
|
+
# Destroys the deployed MicroserviceModel which triggers the operator to kill
|
|
448
|
+
# the microservice. Also directly destroys the Interface/RouterStatusModel in
|
|
449
|
+
# an ensure block. The microservice stop() no longer recreates the status
|
|
450
|
+
# model once cancel_thread is set, so undeploy must clean it up itself to
|
|
451
|
+
# avoid leaving an orphaned status model behind.
|
|
450
452
|
def undeploy
|
|
451
453
|
type = self.class._get_type
|
|
452
454
|
name = "#{@scope}__#{type}__#{@name}"
|
|
@@ -455,15 +457,19 @@ module OpenC3
|
|
|
455
457
|
model.destroy
|
|
456
458
|
ConfigTopic.write({ kind: 'deleted', type: type.downcase, name: @name, plugin: @plugin }, scope: @scope)
|
|
457
459
|
end
|
|
458
|
-
|
|
459
|
-
if type == 'INTERFACE'
|
|
460
|
-
status_model = InterfaceStatusModel.get_model(name: @name, scope: @scope)
|
|
461
|
-
else
|
|
462
|
-
status_model = RouterStatusModel.get_model(name: @name, scope: @scope)
|
|
463
|
-
end
|
|
464
|
-
status_model.destroy if status_model
|
|
465
460
|
rescue Exception => error
|
|
466
461
|
Logger.error("Error undeploying interface/router model #{@name} in scope #{@scope} due to #{error}")
|
|
462
|
+
ensure
|
|
463
|
+
begin
|
|
464
|
+
if type == 'INTERFACE'
|
|
465
|
+
status_model = InterfaceStatusModel.get_model(name: @name, scope: @scope)
|
|
466
|
+
else
|
|
467
|
+
status_model = RouterStatusModel.get_model(name: @name, scope: @scope)
|
|
468
|
+
end
|
|
469
|
+
status_model.destroy if status_model
|
|
470
|
+
rescue Exception => error
|
|
471
|
+
Logger.error("Error destroying #{type&.downcase || 'unknown'} status model #{@name} in scope #{@scope} due to #{error}")
|
|
472
|
+
end
|
|
467
473
|
end
|
|
468
474
|
|
|
469
475
|
def unmap_target(target_name, cmd_only: false, tlm_only: false)
|
|
@@ -118,8 +118,8 @@ module OpenC3
|
|
|
118
118
|
@rxcnt = rxcnt
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
def create(update: false, force: false, queued: false
|
|
122
|
-
_db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued
|
|
121
|
+
def create(update: false, force: false, queued: false)
|
|
122
|
+
_db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def destroy
|
|
@@ -77,8 +77,8 @@ module OpenC3
|
|
|
77
77
|
@db_shard = db_shard.to_i
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def create(update: false, force: false, queued: false,
|
|
81
|
-
_db_sharded_create(@db_shard, update: update, force: force, queued: queued,
|
|
80
|
+
def create(update: false, force: false, queued: false, expire_seconds: nil)
|
|
81
|
+
_db_sharded_create(@db_shard, update: update, force: force, queued: queued, expire_seconds: expire_seconds)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def destroy
|
|
@@ -76,8 +76,8 @@ module OpenC3
|
|
|
76
76
|
@custom = custom
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def create(update: false, force: false, queued: false
|
|
80
|
-
_db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued
|
|
79
|
+
def create(update: false, force: false, queued: false)
|
|
80
|
+
_db_sharded_create(self.class._db_shard_for_name(@name, scope: @scope, use_cache: true), update: update, force: force, queued: queued)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def destroy
|