openc3 7.1.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/config/command_modifiers.yaml +2 -2
- data/data/config/item_modifiers.yaml +10 -3
- data/lib/openc3/api/tlm_api.rb +6 -0
- data/lib/openc3/microservices/microservice.rb +20 -5
- data/lib/openc3/operators/operator.rb +34 -9
- data/lib/openc3/packets/packet_config.rb +17 -4
- data/lib/openc3/script/suite.rb +1 -1
- data/lib/openc3/script/web_socket_api.rb +5 -1
- data/lib/openc3/utilities/cli_generator.rb +427 -403
- data/lib/openc3/utilities/questdb_client.rb +51 -4
- data/lib/openc3/utilities/running_script.rb +41 -3
- data/lib/openc3/utilities/simulated_target.rb +4 -2
- data/lib/openc3/version.rb +6 -6
- 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 +1 -1
|
@@ -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
|
|
@@ -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 = ''
|
|
@@ -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
|
|
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.0'
|
|
4
4
|
module OpenC3
|
|
5
5
|
module Version
|
|
6
6
|
MAJOR = '7'
|
|
7
|
-
MINOR = '
|
|
8
|
-
PATCH = '
|
|
7
|
+
MINOR = '2'
|
|
8
|
+
PATCH = '0'
|
|
9
9
|
OTHER = ''
|
|
10
|
-
BUILD = '
|
|
10
|
+
BUILD = '57bc6819061782287e6e4c866874e13853dc6b40'
|
|
11
11
|
end
|
|
12
|
-
VERSION = '7.
|
|
13
|
-
GEM_VERSION = '7.
|
|
12
|
+
VERSION = '7.2.0'
|
|
13
|
+
GEM_VERSION = '7.2.0'
|
|
14
14
|
end
|
|
@@ -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.0",
|
|
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.0",
|
|
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.0",
|
|
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.0",
|
|
15
|
+
"@openc3/vue-common": "7.2.0",
|
|
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.0",
|
|
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.0",
|
|
12
12
|
"vuetify": "^3.7.1"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|