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
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
#
|
|
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
|
+
|
|
2
14
|
class EnvHelper
|
|
3
15
|
def self.enabled?(key)
|
|
4
16
|
['true', '1', 'yes', 'on'].include?(ENV[key].to_s.downcase)
|
|
@@ -0,0 +1,39 @@
|
|
|
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 'uri'
|
|
15
|
+
require 'openc3/utilities/logger'
|
|
16
|
+
|
|
17
|
+
module OpenC3
|
|
18
|
+
class PypiUrl
|
|
19
|
+
DEFAULT = 'https://pypi.org/simple'
|
|
20
|
+
|
|
21
|
+
# Validate that a resolved pypi_url is an http(s) URL before it is handed to
|
|
22
|
+
# pip. The value can come from a user-writable setting or ENV, so a malformed
|
|
23
|
+
# or non-http value is rejected and replaced with the default rather than
|
|
24
|
+
# passed through to pip.
|
|
25
|
+
#
|
|
26
|
+
# @param pypi_url [String] the resolved pypi index url to validate
|
|
27
|
+
# @return [String] the original url if valid, otherwise DEFAULT
|
|
28
|
+
def self.validate(pypi_url)
|
|
29
|
+
uri = URI.parse(pypi_url)
|
|
30
|
+
unless uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
|
|
31
|
+
raise URI::InvalidURIError, "not an http(s) URL"
|
|
32
|
+
end
|
|
33
|
+
pypi_url
|
|
34
|
+
rescue URI::InvalidURIError => e
|
|
35
|
+
Logger.error("Invalid pypi_url '#{pypi_url}' (#{e.message}); falling back to #{DEFAULT}")
|
|
36
|
+
DEFAULT
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -511,9 +511,14 @@ module OpenC3
|
|
|
511
511
|
# @param value_type [Symbol] :RAW or :CONVERTED
|
|
512
512
|
# @param item_name [String, nil] Original (unsanitized) item name for mapping values.
|
|
513
513
|
# Defaults to safe_item_name if not provided.
|
|
514
|
+
# @param existing_columns [Hash{String=>String}, nil] Map of column name to QuestDB
|
|
515
|
+
# column type for the table. When provided and a converted (__C) column is absent or
|
|
516
|
+
# non-numeric (e.g. a states column stored as VARCHAR), CONVERTED aggregation falls
|
|
517
|
+
# back to the raw column (mirrors the non-reduced read path). When nil, no fallback
|
|
518
|
+
# check is performed.
|
|
514
519
|
# @return [Array<String>, Hash] Two-element array: [select_fragments, column_mapping]
|
|
515
520
|
# column_mapping maps result column alias to [item_name, reduced_type, value_type]
|
|
516
|
-
def self.build_aggregation_selects(safe_item_name, value_type, item_name: nil)
|
|
521
|
+
def self.build_aggregation_selects(safe_item_name, value_type, item_name: nil, existing_columns: nil)
|
|
517
522
|
item_name ||= safe_item_name
|
|
518
523
|
selects = []
|
|
519
524
|
mapping = {}
|
|
@@ -526,7 +531,17 @@ module OpenC3
|
|
|
526
531
|
mapping[alias_name] = [item_name, reduced_type, :RAW]
|
|
527
532
|
end
|
|
528
533
|
when :CONVERTED
|
|
529
|
-
|
|
534
|
+
# The converted (__C) column only exists, and is only numerically aggregatable, when
|
|
535
|
+
# the item has a numeric conversion. When it is absent (e.g. POSPROGRESS, which only
|
|
536
|
+
# has a raw and __F column) or non-numeric (e.g. a states column stored as VARCHAR),
|
|
537
|
+
# aggregate the raw column instead, matching the CONVERTED fallback in
|
|
538
|
+
# decode_packet_row/JsonPacket#read.
|
|
539
|
+
converted_col = "#{safe_item_name}__C"
|
|
540
|
+
col = if existing_columns.nil? || numeric_column_type?(existing_columns[converted_col])
|
|
541
|
+
converted_col
|
|
542
|
+
else
|
|
543
|
+
safe_item_name
|
|
544
|
+
end
|
|
530
545
|
{ 'CN' => :MIN, 'CX' => :MAX, 'CA' => :AVG, 'CS' => :STDDEV }.each do |suffix, reduced_type|
|
|
531
546
|
alias_name = "#{safe_item_name}__#{suffix}"
|
|
532
547
|
selects << "#{reduced_type.to_s.downcase}(\"#{col}\") as \"#{alias_name}\""
|
|
@@ -544,9 +559,11 @@ module OpenC3
|
|
|
544
559
|
#
|
|
545
560
|
# @param packet_def [Hash, nil] Packet definition from TargetModel.packet
|
|
546
561
|
# @param value_type [Symbol] :RAW or :CONVERTED
|
|
562
|
+
# @param existing_columns [Set<String>, nil] Column names that actually exist in the
|
|
563
|
+
# table, used for CONVERTED-to-raw fallback (see build_aggregation_selects).
|
|
547
564
|
# @return [Array<String>, Boolean] Two-element array: [select_fragments, has_numeric_items]
|
|
548
565
|
# select_fragments includes TIMESTAMP_SELECT as the first element.
|
|
549
|
-
def self.build_packet_reduced_selects(packet_def, value_type)
|
|
566
|
+
def self.build_packet_reduced_selects(packet_def, value_type, existing_columns: nil)
|
|
550
567
|
selects = [TIMESTAMP_SELECT]
|
|
551
568
|
has_items = false
|
|
552
569
|
return [selects, false] unless packet_def && packet_def['items']
|
|
@@ -558,7 +575,7 @@ module OpenC3
|
|
|
558
575
|
next unless value_type == :RAW || value_type == :CONVERTED
|
|
559
576
|
|
|
560
577
|
safe_name = sanitize_column_name(item['name'])
|
|
561
|
-
agg_selects, _mapping = build_aggregation_selects(safe_name, value_type)
|
|
578
|
+
agg_selects, _mapping = build_aggregation_selects(safe_name, value_type, existing_columns: existing_columns)
|
|
562
579
|
selects.concat(agg_selects)
|
|
563
580
|
has_items = true
|
|
564
581
|
end
|
|
@@ -615,6 +632,36 @@ module OpenC3
|
|
|
615
632
|
false
|
|
616
633
|
end
|
|
617
634
|
|
|
635
|
+
# QuestDB column types that can be aggregated with min/max/avg/stddev.
|
|
636
|
+
# Used by reduced queries to decide whether a converted (__C) column is
|
|
637
|
+
# numeric or must fall back to the raw column (e.g. states stored as VARCHAR).
|
|
638
|
+
NUMERIC_COLUMN_TYPES = Set.new(['BYTE', 'SHORT', 'INT', 'LONG', 'FLOAT', 'DOUBLE']).freeze
|
|
639
|
+
|
|
640
|
+
# Return true if the given QuestDB column type can be numerically aggregated.
|
|
641
|
+
#
|
|
642
|
+
# @param column_type [String, nil] QuestDB column type (e.g. 'DOUBLE', 'VARCHAR')
|
|
643
|
+
# @return [Boolean]
|
|
644
|
+
def self.numeric_column_type?(column_type)
|
|
645
|
+
!column_type.nil? && NUMERIC_COLUMN_TYPES.include?(column_type.to_s.upcase)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Return a hash mapping column name to QuestDB column type for a table.
|
|
649
|
+
# Used by reduced queries to fall back to raw columns when a converted
|
|
650
|
+
# (__C) column was never created (e.g. items with no read_conversion) or
|
|
651
|
+
# is non-numeric (e.g. states stored as VARCHAR).
|
|
652
|
+
#
|
|
653
|
+
# @param table_name [String] Sanitized table name
|
|
654
|
+
# @return [Hash{String=>String}, nil] { column_name => column_type }, or nil if
|
|
655
|
+
# the table does not exist or the schema cannot be queried
|
|
656
|
+
def self.table_columns(table_name, db_shard: 0)
|
|
657
|
+
result = query_with_retry("SHOW COLUMNS FROM \"#{table_name}\"", max_retries: 1, label: "show columns", db_shard: db_shard)
|
|
658
|
+
return nil unless result
|
|
659
|
+
# SHOW COLUMNS returns rows of [column, type, ...]
|
|
660
|
+
result.values.each_with_object({}) { |row, hash| hash[row[0]] = row[1] }
|
|
661
|
+
rescue StandardError
|
|
662
|
+
nil
|
|
663
|
+
end
|
|
664
|
+
|
|
618
665
|
# Execute a paginated TSDB query, yielding each non-empty PG::Result page.
|
|
619
666
|
# Handles LIMIT pagination and retry on error.
|
|
620
667
|
#
|
|
@@ -45,6 +45,21 @@ if not defined? RAILS_ROOT
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
SCRIPT_API = 'script-api'
|
|
48
|
+
# Per-script frontend channel events are mirrored into a short-lived Redis
|
|
49
|
+
# stream so RunningScriptChannel can serve both the backlog (events published
|
|
50
|
+
# before a client subscribed -- the anycable broadcast is pub/sub with no
|
|
51
|
+
# history) and the live feed from a single ordered source. MAXLEN bounds
|
|
52
|
+
# memory; the TTL is refreshed on every write so the stream lives while the
|
|
53
|
+
# script runs and is auto-removed by Redis shortly after the last event -- no
|
|
54
|
+
# explicit cleanup is needed for completion, crash, or an orphaned/killed
|
|
55
|
+
# process.
|
|
56
|
+
RUNNING_SCRIPT_CHANNEL_PREFIX = 'running-script-channel:'
|
|
57
|
+
RUNNING_SCRIPT_REPLAY_MAXLEN = 1000
|
|
58
|
+
RUNNING_SCRIPT_REPLAY_TTL = 86400 # seconds
|
|
59
|
+
# Flush the replay queue quickly so live output/state stays near real-time while
|
|
60
|
+
# remaining non-blocking. Applied via set_update_interval in RunningScript's
|
|
61
|
+
# initialize so it only affects this (running script) process.
|
|
62
|
+
RUNNING_SCRIPT_REPLAY_FLUSH_INTERVAL = 0.1 # seconds
|
|
48
63
|
|
|
49
64
|
def running_script_publish(channel_name, data)
|
|
50
65
|
stream_name = [SCRIPT_API, channel_name].compact.join(":")
|
|
@@ -52,9 +67,27 @@ def running_script_publish(channel_name, data)
|
|
|
52
67
|
end
|
|
53
68
|
|
|
54
69
|
def running_script_anycable_publish(channel_name, data)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
json = JSON.generate(data, allow_nan: true)
|
|
71
|
+
if channel_name.start_with?(RUNNING_SCRIPT_CHANNEL_PREFIX)
|
|
72
|
+
# Per-script events are delivered to the frontend by RunningScriptChannel
|
|
73
|
+
# tailing this replay stream (backlog + live from a single ordered source),
|
|
74
|
+
# so they are NOT broadcast over anycable pub/sub -- doing both would deliver
|
|
75
|
+
# every message twice. Queued (EphemeralStoreQueued) so the script never
|
|
76
|
+
# blocks on Redis; best-effort so a replay failure can never affect script
|
|
77
|
+
# execution. Topic (read by the channel) uses the EphemeralStore.
|
|
78
|
+
begin
|
|
79
|
+
replay_key = "#{channel_name}:replay"
|
|
80
|
+
OpenC3::EphemeralStoreQueued.instance.write_topic(replay_key, { 'data' => json }, '*', RUNNING_SCRIPT_REPLAY_MAXLEN, '~')
|
|
81
|
+
OpenC3::EphemeralStoreQueued.instance.expire(replay_key, RUNNING_SCRIPT_REPLAY_TTL)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
OpenC3::Logger.warn("running_script replay write failed: #{e.message}") rescue nil
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
# Other channels (all-scripts, etc.) are still delivered live over pub/sub.
|
|
87
|
+
stream_name = [SCRIPT_API, channel_name].compact.join(":")
|
|
88
|
+
stream_data = {"stream" => stream_name, "data" => json}
|
|
89
|
+
OpenC3::Store.publish("__anycable__", JSON.generate(stream_data, allow_nan: true))
|
|
90
|
+
end
|
|
58
91
|
end
|
|
59
92
|
|
|
60
93
|
module OpenC3
|
|
@@ -480,10 +513,10 @@ class RunningScript
|
|
|
480
513
|
errors: nil, # array of errors that occurred during the script run
|
|
481
514
|
pid: nil, # pid of the script process - set by the script itself when it starts
|
|
482
515
|
script_engine: script_engine, # script engine filename
|
|
483
|
-
updated_at: nil, # Set by create/update
|
|
516
|
+
updated_at: nil, # Set by create/update
|
|
484
517
|
scope: scope # Scope of the script
|
|
485
518
|
)
|
|
486
|
-
script_status.create(
|
|
519
|
+
script_status.create()
|
|
487
520
|
|
|
488
521
|
# Set proper secrets for running script
|
|
489
522
|
process.environment['SECRET_KEY_BASE'] = nil
|
|
@@ -536,6 +569,11 @@ class RunningScript
|
|
|
536
569
|
|
|
537
570
|
def initialize(script_status)
|
|
538
571
|
@@instance = self
|
|
572
|
+
# Flush the replay queue (used by running_script_anycable_publish) quickly so
|
|
573
|
+
# live output/state stays near real-time. Set explicitly here so the interval
|
|
574
|
+
# is applied even if the queue instance was already created. Only affects
|
|
575
|
+
# this running-script process.
|
|
576
|
+
OpenC3::EphemeralStoreQueued.instance.set_update_interval(RUNNING_SCRIPT_REPLAY_FLUSH_INTERVAL)
|
|
539
577
|
@script_status = script_status
|
|
540
578
|
@script_status.pid = Process.pid
|
|
541
579
|
@user_input = ''
|
|
@@ -789,17 +827,22 @@ class RunningScript
|
|
|
789
827
|
|
|
790
828
|
def run
|
|
791
829
|
if @script_status.suite_runner
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
830
|
+
# Normalize the received hash through the shared helper so options/method
|
|
831
|
+
# defaults and validation match what openc3cli and the GUI construct.
|
|
832
|
+
sr = OpenC3::SuiteRunner.build_options(
|
|
833
|
+
suite: @script_status.suite_runner['suite'],
|
|
834
|
+
group: @script_status.suite_runner['group'],
|
|
835
|
+
script: @script_status.suite_runner['script'],
|
|
836
|
+
method: @script_status.suite_runner['method'],
|
|
837
|
+
options: @script_status.suite_runner['options'],
|
|
838
|
+
)
|
|
839
|
+
parse_options(sr['options'])
|
|
840
|
+
if sr['script']
|
|
841
|
+
run_text("OpenC3::SuiteRunner.start(#{sr['suite']}, #{sr['group']}, '#{sr['script']}')", initial_filename: "SCRIPTRUNNER")
|
|
842
|
+
elsif sr['group']
|
|
843
|
+
run_text("OpenC3::SuiteRunner.#{sr['method']}(#{sr['suite']}, #{sr['group']})", initial_filename: "SCRIPTRUNNER")
|
|
801
844
|
else
|
|
802
|
-
run_text("OpenC3::SuiteRunner.#{
|
|
845
|
+
run_text("OpenC3::SuiteRunner.#{sr['method']}(#{sr['suite']})", initial_filename: "SCRIPTRUNNER")
|
|
803
846
|
end
|
|
804
847
|
else
|
|
805
848
|
if not @script_engine and (@script_status.start_line_no != 1 or !@script_status.end_line_no.nil?)
|
|
@@ -139,16 +139,23 @@ class Script < OpenC3::TargetFile
|
|
|
139
139
|
process.io.stdout = stdout
|
|
140
140
|
process.io.stderr = stderr
|
|
141
141
|
process.start
|
|
142
|
-
|
|
142
|
+
begin
|
|
143
|
+
process.poll_for_exit(10) # wait for max 10s
|
|
144
|
+
rescue ChildProcess::TimeoutError
|
|
145
|
+
process.stop
|
|
146
|
+
stderr_results = "Suite analysis timed out - possible infinite loop in script\n"
|
|
147
|
+
success = false
|
|
148
|
+
end
|
|
143
149
|
stdout.rewind
|
|
144
150
|
stdout_results = stdout.read
|
|
145
151
|
stdout.close
|
|
146
152
|
stdout.unlink
|
|
147
153
|
stderr.rewind
|
|
148
|
-
stderr_results
|
|
154
|
+
stderr_results ||= ""
|
|
155
|
+
stderr_results += stderr.read
|
|
149
156
|
stderr.close
|
|
150
157
|
stderr.unlink
|
|
151
|
-
success = process.exit_code == 0
|
|
158
|
+
success = process.exit_code == 0 if success
|
|
152
159
|
else
|
|
153
160
|
require temp.path
|
|
154
161
|
stdout_results = OpenC3::SuiteRunner.build_suites.as_json().to_json(allow_nan: true)
|
|
@@ -203,6 +210,10 @@ class Script < OpenC3::TargetFile
|
|
|
203
210
|
line_no = nil,
|
|
204
211
|
end_line_no = nil
|
|
205
212
|
)
|
|
213
|
+
# Verify the script exists before spawning a run. Without this check a run
|
|
214
|
+
# is started and the missing file only surfaces as a runtime error inside
|
|
215
|
+
# the spawned process. Returning nil lets the caller return a 404.
|
|
216
|
+
return nil unless Script.body(scope, name)
|
|
206
217
|
RunningScript.spawn(scope, name, suite_runner, disconnect, environment, user_full_name, username, line_no, end_line_no)
|
|
207
218
|
end
|
|
208
219
|
|
|
@@ -44,8 +44,10 @@ module OpenC3
|
|
|
44
44
|
@tlm_packets.each do |name, packet|
|
|
45
45
|
packet.restore_defaults
|
|
46
46
|
ids = packet.id_items
|
|
47
|
-
ids
|
|
48
|
-
|
|
47
|
+
if ids
|
|
48
|
+
ids.each do |id|
|
|
49
|
+
packet.public_send((id.name + '=').to_sym, id.id_value)
|
|
50
|
+
end
|
|
49
51
|
end
|
|
50
52
|
end
|
|
51
53
|
|
|
@@ -123,8 +123,46 @@ module OpenC3
|
|
|
123
123
|
@redis_pool = StoreConnectionPool.new(size: pool_size) { build_redis() }
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# cap/base for the equal-jitter reconnect backoff (seconds)
|
|
127
|
+
REDIS_BACKOFF_CAP = 5.0
|
|
128
|
+
REDIS_BACKOFF_BASE = 0.625
|
|
129
|
+
|
|
126
130
|
def build_redis
|
|
127
|
-
|
|
131
|
+
# reconnect_attempts retries the connection a few times with equal-jitter
|
|
132
|
+
# backoff so a transient network blip is handled inside the client instead
|
|
133
|
+
# of immediately surfacing a connection error to callers. The jitter
|
|
134
|
+
# de-syncs many clients retrying the same blip to avoid a thundering herd
|
|
135
|
+
# on recovery.
|
|
136
|
+
#
|
|
137
|
+
# This mirrors the Python store's Retry(EqualJitterBackoff(cap: 5, base:
|
|
138
|
+
# 0.625), 3): per-retry backoff tops out at 5s on the final (3rd) retry
|
|
139
|
+
# (~0.6-1.25s, ~1.25-2.5s, ~2.5-5s). redis-rb takes a fixed Array of delays
|
|
140
|
+
# (no per-failure backoff callable), so we sample the jittered delays once
|
|
141
|
+
# per connection here.
|
|
142
|
+
# Connection, read, and write timeouts are left as the default: 1s
|
|
143
|
+
return Redis.new(
|
|
144
|
+
url: @redis_url,
|
|
145
|
+
username: @redis_username,
|
|
146
|
+
password: @redis_key,
|
|
147
|
+
reconnect_attempts: reconnect_backoff_delays()
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Equal-jitter backoff delays for 3 retries, matching Python's
|
|
152
|
+
# EqualJitterBackoff. Each retry's delay is randomized within the upper half
|
|
153
|
+
# of an exponentially-growing ceiling.
|
|
154
|
+
def reconnect_backoff_delays
|
|
155
|
+
(1..3).map do |failures|
|
|
156
|
+
# ceiling = exponential growth (base doubles each retry), clamped to cap.
|
|
157
|
+
# For base=0.625, cap=5: failures 1,2,3 -> 1.25, 2.5, 5.0 seconds.
|
|
158
|
+
# temp = half the ceiling: the guaranteed minimum wait for this retry.
|
|
159
|
+
temp = [REDIS_BACKOFF_CAP, REDIS_BACKOFF_BASE * (2 ** failures)].min / 2.0
|
|
160
|
+
# Final delay = fixed half (temp) + random half (rand*temp, in [0, temp)),
|
|
161
|
+
# i.e. a value uniformly in [temp, 2*temp) = [ceiling/2, ceiling).
|
|
162
|
+
# The fixed half keeps a sane floor; the random half de-syncs clients so
|
|
163
|
+
# they don't all reconnect in lockstep (thundering herd) after a blip.
|
|
164
|
+
temp + rand * temp
|
|
165
|
+
end
|
|
128
166
|
end
|
|
129
167
|
|
|
130
168
|
###########################################################################
|
|
@@ -103,7 +103,11 @@ module OpenC3
|
|
|
103
103
|
OpenC3.kill_thread(self, @update_thread) if @update_thread
|
|
104
104
|
@update_thread = nil
|
|
105
105
|
# Drain the queue before shutdown
|
|
106
|
-
|
|
106
|
+
begin
|
|
107
|
+
process_queue()
|
|
108
|
+
rescue
|
|
109
|
+
# Best effort - Redis may already be unavailable during shutdown
|
|
110
|
+
end
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
MessageStruct = Struct.new(:message, :args, :kwargs, :block)
|
data/lib/openc3/version.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# encoding: ascii-8bit
|
|
2
2
|
|
|
3
|
-
OPENC3_VERSION = '7.
|
|
3
|
+
OPENC3_VERSION = '7.2.1'
|
|
4
4
|
module OpenC3
|
|
5
5
|
module Version
|
|
6
6
|
MAJOR = '7'
|
|
7
|
-
MINOR = '
|
|
7
|
+
MINOR = '2'
|
|
8
8
|
PATCH = '1'
|
|
9
9
|
OTHER = ''
|
|
10
|
-
BUILD = '
|
|
10
|
+
BUILD = 'c0b096796e3070340b34bc4368c21f3972ede7a4'
|
|
11
11
|
end
|
|
12
|
-
VERSION = '7.
|
|
13
|
-
GEM_VERSION = '7.
|
|
12
|
+
VERSION = '7.2.1'
|
|
13
|
+
GEM_VERSION = '7.2.1'
|
|
14
14
|
end
|
data/lib/openc3.rb
CHANGED
|
@@ -19,7 +19,12 @@ require 'resolv-replace'
|
|
|
19
19
|
# Set default encodings
|
|
20
20
|
saved_verbose = $VERBOSE; $VERBOSE = nil
|
|
21
21
|
Encoding.default_external = Encoding::ASCII_8BIT
|
|
22
|
-
Encoding.default_internal =
|
|
22
|
+
# NOTE: Do NOT set Encoding.default_internal = ASCII_8BIT. Doing so makes every
|
|
23
|
+
# read that specifies an encoding (e.g. File.read(path, encoding: 'UTF-8'))
|
|
24
|
+
# transcode into ASCII-8BIT, which raises Encoding::UndefinedConversionError on
|
|
25
|
+
# any non-ASCII byte. On Ruby 3.4 this breaks bootsnap's compile cache (it reads
|
|
26
|
+
# source as UTF-8 to work around Ruby bug #22023) for any file containing
|
|
27
|
+
# non-ASCII characters, including stdlib files like 'matrix'. Leave it nil.
|
|
23
28
|
$VERBOSE = saved_verbose
|
|
24
29
|
|
|
25
30
|
# Add OpenC3 bin folder to PATH
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
from openc3.packets.command_validator import CommandValidator
|
|
2
2
|
# Using the OpenC3 API requires the following imports:
|
|
3
|
-
|
|
3
|
+
from openc3.api import tlm, wait_check
|
|
4
4
|
|
|
5
5
|
# Custom command validator class
|
|
6
6
|
# See https://docs.openc3.com/docs/configuration/command
|
|
7
7
|
class <%= validator_class %>(CommandValidator):
|
|
8
|
-
def __init__(self, *args):
|
|
9
|
-
super().__init__()
|
|
10
|
-
self.args = args
|
|
11
|
-
|
|
12
8
|
# Called before a command is sent
|
|
13
|
-
# @param command [
|
|
9
|
+
# @param command [Packet] The command object containing all the command details
|
|
14
10
|
# @return [list] First element is True/False/None for success/failure/unknown,
|
|
15
11
|
# second element is an optional message string
|
|
16
12
|
def pre_check(self, command):
|
|
17
13
|
# Add your pre-command validation logic here
|
|
18
14
|
# Example:
|
|
19
|
-
# target_name = command
|
|
20
|
-
# command_name = command
|
|
21
|
-
#
|
|
15
|
+
# target_name = command.target_name
|
|
16
|
+
# command_name = command.packet_name
|
|
17
|
+
# for item_name, item_def in command.items.items():
|
|
18
|
+
# item_value = command.read(item_name)
|
|
19
|
+
|
|
22
20
|
# self.count = tlm("TARGET PACKET COUNT")
|
|
23
21
|
#
|
|
24
22
|
# if some_condition:
|
|
@@ -29,7 +27,7 @@ class <%= validator_class %>(CommandValidator):
|
|
|
29
27
|
return [True, None]
|
|
30
28
|
|
|
31
29
|
# Called after a command is sent
|
|
32
|
-
# @param command [
|
|
30
|
+
# @param command [Packet] The command object containing all the command details
|
|
33
31
|
# @return [list] First element is True/False/None for success/failure/unknown,
|
|
34
32
|
# second element is an optional message string
|
|
35
33
|
def post_check(self, command):
|
|
@@ -5,21 +5,18 @@ module OpenC3
|
|
|
5
5
|
# Custom command validator class
|
|
6
6
|
# See https://docs.openc3.com/docs/configuration/command
|
|
7
7
|
class <%= validator_class %> < CommandValidator
|
|
8
|
-
def initialize(*args)
|
|
9
|
-
super()
|
|
10
|
-
@args = args
|
|
11
|
-
end
|
|
12
|
-
|
|
13
8
|
# Called before a command is sent
|
|
14
|
-
# @param command [
|
|
9
|
+
# @param command [Packet] The command object containing all the command details
|
|
15
10
|
# @return [Array<Boolean, String>] First element is true/false/nil for success/failure/unknown,
|
|
16
11
|
# second element is an optional message string
|
|
17
12
|
def pre_check(command)
|
|
18
13
|
# Add your pre-command validation logic here
|
|
19
14
|
# Example:
|
|
20
|
-
# target_name = command
|
|
21
|
-
# command_name = command
|
|
22
|
-
#
|
|
15
|
+
# target_name = command.target_name
|
|
16
|
+
# command_name = command.packet_name
|
|
17
|
+
# command.items.each do |item_name, item_def|
|
|
18
|
+
# item_value = command.read(item_name)
|
|
19
|
+
# end
|
|
23
20
|
# @count = tlm("TARGET PACKET COUNT")
|
|
24
21
|
#
|
|
25
22
|
# if some_condition
|
data/templates/plugin/LICENSE.md
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright 2026 <COPYRIGHT HOLDER>
|
|
2
2
|
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
|
5
|
+
deal in the Software without restriction, including without limitation the
|
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
4
9
|
|
|
5
|
-
The above copyright notice and this permission notice shall be included in
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
6
12
|
|
|
7
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "<%= tool_name %>",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.1",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"ng": "ng",
|
|
6
6
|
"start": "ng serve",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@angular/platform-browser-dynamic": "^18.2.6",
|
|
24
24
|
"@angular/router": "^18.2.6",
|
|
25
25
|
"@astrouxds/astro-web-components": "^7.24.0",
|
|
26
|
-
"@openc3/js-common": "7.
|
|
26
|
+
"@openc3/js-common": "7.2.1",
|
|
27
27
|
"rxjs": "~7.8.0",
|
|
28
28
|
"single-spa": "^5.9.5",
|
|
29
29
|
"single-spa-angular": "^9.2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "<%= tool_name %>",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@astrouxds/astro-web-components": "^7.24.0",
|
|
14
|
-
"@openc3/js-common": "7.
|
|
15
|
-
"@openc3/vue-common": "7.
|
|
14
|
+
"@openc3/js-common": "7.2.1",
|
|
15
|
+
"@openc3/vue-common": "7.2.1",
|
|
16
16
|
"axios": "^1.7.7",
|
|
17
17
|
"date-fns": "^4.1.0",
|
|
18
18
|
"lodash": "^4.17.21",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "<%= widget_name %>",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@astrouxds/astro-web-components": "^7.24.0",
|
|
11
|
-
"@openc3/vue-common": "7.
|
|
11
|
+
"@openc3/vue-common": "7.2.1",
|
|
12
12
|
"vuetify": "^3.7.1"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openc3
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.
|
|
4
|
+
version: 7.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Melton
|
|
@@ -1106,6 +1106,7 @@ files:
|
|
|
1106
1106
|
- lib/openc3/migrations/20250402000000_periodic_only_default.rb
|
|
1107
1107
|
- lib/openc3/migrations/20260203000000_remove_store_id.rb
|
|
1108
1108
|
- lib/openc3/migrations/20260204000000_remove_decom_reducer.rb
|
|
1109
|
+
- lib/openc3/migrations/20260701000000_updated_at_to_int.rb
|
|
1109
1110
|
- lib/openc3/models/activity_model.rb
|
|
1110
1111
|
- lib/openc3/models/auth_model.rb
|
|
1111
1112
|
- lib/openc3/models/cvt_model.rb
|
|
@@ -1259,6 +1260,7 @@ files:
|
|
|
1259
1260
|
- lib/openc3/utilities/migration.rb
|
|
1260
1261
|
- lib/openc3/utilities/open_telemetry.rb
|
|
1261
1262
|
- lib/openc3/utilities/process_manager.rb
|
|
1263
|
+
- lib/openc3/utilities/pypi_url.rb
|
|
1262
1264
|
- lib/openc3/utilities/python_proxy.rb
|
|
1263
1265
|
- lib/openc3/utilities/quaternion.rb
|
|
1264
1266
|
- lib/openc3/utilities/questdb_client.rb
|