openc3 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +105 -13
  3. data/bin/pipinstall +38 -6
  4. data/data/config/command_modifiers.yaml +1 -0
  5. data/data/config/item_modifiers.yaml +2 -1
  6. data/data/config/microservice.yaml +12 -1
  7. data/data/config/parameter_modifiers.yaml +49 -7
  8. data/data/config/table_parameter_modifiers.yaml +3 -1
  9. data/data/config/target.yaml +11 -0
  10. data/data/config/target_config.yaml +6 -2
  11. data/lib/openc3/accessors/template_accessor.rb +9 -0
  12. data/lib/openc3/api/cmd_api.rb +2 -1
  13. data/lib/openc3/api/metrics_api.rb +11 -1
  14. data/lib/openc3/api/tlm_api.rb +21 -6
  15. data/lib/openc3/core_ext/faraday.rb +1 -1
  16. data/lib/openc3/interfaces/interface.rb +1 -6
  17. data/lib/openc3/io/json_api.rb +1 -1
  18. data/lib/openc3/logs/log_writer.rb +3 -1
  19. data/lib/openc3/microservices/decom_common.rb +128 -0
  20. data/lib/openc3/microservices/decom_microservice.rb +27 -96
  21. data/lib/openc3/microservices/interface_decom_common.rb +28 -10
  22. data/lib/openc3/microservices/interface_microservice.rb +16 -9
  23. data/lib/openc3/microservices/log_microservice.rb +1 -1
  24. data/lib/openc3/microservices/microservice.rb +3 -2
  25. data/lib/openc3/microservices/queue_microservice.rb +1 -1
  26. data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
  27. data/lib/openc3/microservices/text_log_microservice.rb +1 -2
  28. data/lib/openc3/models/cvt_model.rb +24 -13
  29. data/lib/openc3/models/db_sharded_model.rb +110 -0
  30. data/lib/openc3/models/interface_model.rb +9 -0
  31. data/lib/openc3/models/interface_status_model.rb +33 -3
  32. data/lib/openc3/models/metric_model.rb +96 -37
  33. data/lib/openc3/models/microservice_model.rb +7 -0
  34. data/lib/openc3/models/microservice_status_model.rb +30 -3
  35. data/lib/openc3/models/plugin_model.rb +9 -1
  36. data/lib/openc3/models/python_package_model.rb +1 -1
  37. data/lib/openc3/models/reaction_model.rb +27 -9
  38. data/lib/openc3/models/reingest_job_model.rb +153 -0
  39. data/lib/openc3/models/scope_model.rb +3 -2
  40. data/lib/openc3/models/script_status_model.rb +4 -20
  41. data/lib/openc3/models/target_model.rb +113 -100
  42. data/lib/openc3/models/trigger_model.rb +24 -7
  43. data/lib/openc3/packets/packet_config.rb +4 -1
  44. data/lib/openc3/script/api_shared.rb +39 -2
  45. data/lib/openc3/script/calendar.rb +32 -10
  46. data/lib/openc3/script/extract.rb +46 -13
  47. data/lib/openc3/script/script.rb +2 -2
  48. data/lib/openc3/script/script_runner.rb +4 -4
  49. data/lib/openc3/script/telemetry.rb +3 -3
  50. data/lib/openc3/script/web_socket_api.rb +29 -22
  51. data/lib/openc3/system/system.rb +20 -3
  52. data/lib/openc3/topics/command_decom_topic.rb +4 -2
  53. data/lib/openc3/topics/command_topic.rb +8 -5
  54. data/lib/openc3/topics/decom_interface_topic.rb +31 -11
  55. data/lib/openc3/topics/interface_topic.rb +88 -27
  56. data/lib/openc3/topics/limits_event_topic.rb +62 -41
  57. data/lib/openc3/topics/router_topic.rb +61 -21
  58. data/lib/openc3/topics/system_events_topic.rb +18 -1
  59. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
  60. data/lib/openc3/topics/telemetry_topic.rb +4 -2
  61. data/lib/openc3/topics/topic.rb +77 -5
  62. data/lib/openc3/utilities/aws_bucket.rb +2 -0
  63. data/lib/openc3/utilities/cli_generator.rb +3 -2
  64. data/lib/openc3/utilities/ctrf.rb +231 -0
  65. data/lib/openc3/utilities/metric.rb +15 -1
  66. data/lib/openc3/utilities/questdb_client.rb +177 -40
  67. data/lib/openc3/utilities/reingest_job.rb +377 -0
  68. data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
  69. data/lib/openc3/utilities/store_autoload.rb +78 -52
  70. data/lib/openc3/utilities/store_queued.rb +20 -12
  71. data/lib/openc3/version.rb +5 -5
  72. data/templates/plugin/plugin.gemspec +13 -1
  73. data/templates/tool_angular/package.json +2 -2
  74. data/templates/tool_react/package.json +1 -1
  75. data/templates/tool_svelte/package.json +1 -1
  76. data/templates/tool_vue/package.json +3 -4
  77. data/templates/tool_vue/src/router.js +2 -2
  78. data/templates/widget/package.json +2 -2
  79. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a06901f01bc9d8e7b6f0b123296da67475c08ea9cf50ef788a86b6da1308fcb
4
- data.tar.gz: 34732b912cea1b4677f3e94ac0ba9f437e99217ccf54f9250816176ef5b63db1
3
+ metadata.gz: fa824943ce72cce586cdfe03c1a5a8395120801bd845174c25433a15bf986306
4
+ data.tar.gz: 620cc66576e14e7e696419e5be0b5704190452d29efc35a315a489099b20f82e
5
5
  SHA512:
6
- metadata.gz: 5cda606ccb010606ad4b432685f53428cbf10b216f1cd06f2f55baac28fb723514dc3f8dfeadafc0958734237d3a7faf3f42439695481145914434e2a4bb1a58
7
- data.tar.gz: 98532ea424c421554d45a6d155fadfc27e67ddf969a96be7653daebb30d5530dbb7601e13b5d3a3afa7dfd56718c161c060e81d67a5ee967a9248225554e1a03
6
+ metadata.gz: 1f392d51b2e5870128a995aa9e5717f9a4ed3bf0b8f55952b9141ec34898406734a2a834cf18d09b78d6e82f5675c2c0f3269e2ef62964ec58f2b96b8933ce80
7
+ data.tar.gz: a92a073ff672da55a44b85b700cb5b6a908208e9a10ddcad77828f02de0f877c3db2252d7d93857ab5b65d55b39594dfc109d72028a82cbfeaa6e468878eef94
data/bin/openc3cli CHANGED
@@ -45,7 +45,8 @@ require 'irb/completion'
45
45
  require 'digest'
46
46
  require 'argon2'
47
47
 
48
- $redis_url = "redis://#{ENV['OPENC3_REDIS_HOSTNAME']}:#{ENV['OPENC3_REDIS_PORT']}"
48
+ $redis_shardnum = ENV['OPENC3_SHARDNUM'] || "0"
49
+ $redis_url = "redis://#{ENV['OPENC3_REDIS_HOSTNAME'].to_s.gsub("SHARDNUM", $redis_shardnum)}:#{ENV['OPENC3_REDIS_PORT']}"
49
50
 
50
51
  ERROR_CODE = 1
51
52
 
@@ -75,6 +76,7 @@ def print_usage
75
76
  puts " cli pkguninstall PKGFILENAME SCOPE # Uninstall loaded package (Ruby gem or python package)"
76
77
  puts " cli xtce_converter # Convert to and from the XTCE format. Run with --help for more info."
77
78
  puts " cli cstol_converter # Converts CSTOL files (.prc) to COSMOS. Run with --help for more info."
79
+ puts " cli setpassword # Set the initial password from OPENC3_API_PASSWORD env var"
78
80
  puts ""
79
81
  end
80
82
 
@@ -588,35 +590,44 @@ def run_bridge(filename, params)
588
590
  end
589
591
  end
590
592
 
591
- def cli_script_monitor(script_id)
593
+ def cli_script_monitor(script_id, format: 'text')
592
594
  ret_code = ERROR_CODE
593
595
  require 'openc3/script'
596
+ require 'openc3/utilities/ctrf'
594
597
  begin
595
598
  OpenC3::RunningScriptWebSocketApi.new(id: script_id) do |api|
596
599
  while (resp = api.read) do
597
600
  # see ScriptRunner.vue for types and states
598
601
  case resp['type']
599
602
  when 'file'
600
- puts "Filename #{resp['filename']} scope #{resp['scope']}"
603
+ puts "Filename #{resp['filename']} scope #{resp['scope']}" unless format == 'ctrf'
601
604
  when 'line'
602
605
  fn = resp['filename'].nil? ? '<no file>' : resp['filename']
603
- puts "At [#{fn}:#{resp['line_no']}] state [#{resp['state']}]"
606
+ puts "At [#{fn}:#{resp['line_no']}] state [#{resp['state']}]" unless format == 'ctrf'
604
607
  if resp['state'] == 'error' or resp['state'] == 'crashed'
605
608
  $script_interrupt_text = ''
606
- puts 'script failed'
609
+ puts 'script failed' unless format == 'ctrf'
607
610
  break
608
611
  end
609
612
  when 'output'
610
- puts resp['line']
613
+ puts resp['line'] unless format == 'ctrf'
611
614
  when 'complete'
612
615
  $script_interrupt_text = ''
613
- puts 'script complete'
616
+ if resp['report']
617
+ if format == 'ctrf'
618
+ ctrf_data = OpenC3::Ctrf.convert_report(resp['report'])
619
+ puts JSON.pretty_generate(ctrf_data)
620
+ else
621
+ puts resp['report']
622
+ end
623
+ end
624
+ puts 'script complete' unless format == 'ctrf'
614
625
  ret_code = 0
615
626
  break
616
627
  # These conditions are all handled by the else
617
628
  # when 'running', 'breakpoint', 'waiting', 'time'
618
629
  else
619
- puts resp.pretty_inspect
630
+ puts resp.pretty_inspect unless format == 'ctrf'
620
631
  end
621
632
  end
622
633
  end
@@ -692,10 +703,10 @@ def cli_script_run(args, options)
692
703
  puts id
693
704
  $script_interrupt_text = " Script #{args[1]} still running remotely.\n" # for Ctrl-C
694
705
  if (options[:wait] < 1) then
695
- ret_code = cli_script_monitor(id)
706
+ ret_code = cli_script_monitor(id, format: options[:format])
696
707
  else
697
708
  Timeout::timeout(options[:wait], nil, "--wait #{options[:wait]} exceeded") do
698
- ret_code = cli_script_monitor(id)
709
+ ret_code = cli_script_monitor(id, format: options[:format])
699
710
  rescue Timeout::ExitException, Timeout::Error => e
700
711
  # Timeout exceptions are also raised by the Websocket API, so we check
701
712
  if e.message =~ /^--wait /
@@ -795,7 +806,7 @@ rescue => e
795
806
  end
796
807
 
797
808
  def cli_script(args=[])
798
- options = {scope: 'DEFAULT', disconnect: false, wait: 0, verbose: false}
809
+ options = {scope: 'DEFAULT', disconnect: false, wait: 0, verbose: false, format: 'text'}
799
810
  option_parser = OptionParser.new do |opts|
800
811
  opts.banner = "Usage: script --scope SCOPE [init | list | spawn | run]\n" +
801
812
  " init Initialize running scripts (Enterprise Only)\n" +
@@ -812,6 +823,9 @@ def cli_script(args=[])
812
823
  opts.on("--scope SCOPE", "Run with specified scope (default = DEFAULT)") do |arg|
813
824
  options[:scope] = arg
814
825
  end
826
+ opts.on("--format FORMAT", "Output format: text or ctrf (default = text)") do |arg|
827
+ options[:format] = arg
828
+ end
815
829
  opts.on("--suite SUITE", "Run with specified suite") do |arg|
816
830
  options[:suite] = arg
817
831
  end
@@ -892,6 +906,25 @@ def cli_script(args=[])
892
906
  exit(ret_code)
893
907
  end
894
908
 
909
+ def set_password
910
+ password = ENV['OPENC3_API_PASSWORD']
911
+ argon2_profile = ENV["OPENC3_ARGON2_PROFILE"]&.to_sym || :rfc_9106_low_memory
912
+ if password.nil? or password.empty?
913
+ abort "OPENC3_API_PASSWORD environment variable is required"
914
+ end
915
+ if password.length < 8
916
+ abort "Password must be at least 8 characters"
917
+ end
918
+ redis = Redis.new(url: $redis_url, username: ENV['OPENC3_REDIS_USERNAME'], password: ENV['OPENC3_REDIS_PASSWORD'])
919
+ if redis.exists('OPENC3__TOKEN') == 1
920
+ abort "Password has already been set. Use the web interface to change the password."
921
+ end
922
+ pw_hash = Argon2::Password.create(password, profile: argon2_profile)
923
+ redis.set('OPENC3__TOKEN', pw_hash)
924
+ puts "Password set successfully."
925
+ exit 0
926
+ end
927
+
895
928
  def migrate_password_hash
896
929
  password = ENV['OPENC3_API_PASSWORD']
897
930
  argon2_profile = ENV["OPENC3_ARGON2_PROFILE"]&.to_sym || :rfc_9106_low_memory
@@ -1111,6 +1144,48 @@ if not ARGV[0].nil? # argument(s) given
1111
1144
  end
1112
1145
  cli_pkg_uninstall(ARGV[1], scope: ARGV[2])
1113
1146
 
1147
+ when 'reingest'
1148
+ # Internal command spawned by StorageController via ProcessManager so the
1149
+ # reingest runs in its own process and System singleton resets cannot
1150
+ # collide with the cmd-tlm-api Rails server.
1151
+ if ARGV[1].nil? || ARGV[2].nil? || ARGV[1] == '--help' || ARGV[1] == '-h'
1152
+ puts "Usage: cli reingest JOB_ID SCOPE"
1153
+ exit(ARGV[1].nil? ? 1 : 0)
1154
+ end
1155
+ require 'openc3/utilities/reingest_job'
1156
+ require 'openc3/models/reingest_job_model'
1157
+ job_id = ARGV[1]
1158
+ scope = ARGV[2]
1159
+ job = OpenC3::ReingestJobModel.get_model(name: job_id, scope: scope)
1160
+ if job.nil?
1161
+ OpenC3::Logger.error("Reingest job #{job_id} not found in scope #{scope}")
1162
+ exit(1)
1163
+ end
1164
+ begin
1165
+ OpenC3::ReingestJob.new(
1166
+ job_id: job_id,
1167
+ files: job.files,
1168
+ path: job.path,
1169
+ bucket: job.bucket,
1170
+ scope: scope,
1171
+ target_version: job.target_version,
1172
+ ).run
1173
+ rescue Exception => e
1174
+ # ReingestJob#run already marks Crashed for errors raised during the run.
1175
+ # This catches failures from the constructor itself (or anything before
1176
+ # run gets its rescue installed) so the model doesn't sit in Queued forever.
1177
+ OpenC3::Logger.error("Reingest job #{job_id} crashed before run: #{e.formatted}")
1178
+ begin
1179
+ job.state = 'Crashed'
1180
+ job.error = e.message
1181
+ job.finished_at = Time.now.utc.iso8601
1182
+ job.update
1183
+ rescue => e2
1184
+ OpenC3::Logger.error("Reingest job #{job_id} failed to mark Crashed: #{e2.message}")
1185
+ end
1186
+ exit(1)
1187
+ end
1188
+
1114
1189
  when 'generate'
1115
1190
  # To test against a local copy call this file from the root cosmos directory like this:
1116
1191
  # ruby -Iopenc3/lib openc3/bin/openc3cli generate ...
@@ -1358,8 +1433,9 @@ if not ARGV[0].nil? # argument(s) given
1358
1433
  end
1359
1434
  end
1360
1435
  end
1361
- # Unless explicitly disabled, ensure the tools bucket is public
1362
- unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false)
1436
+ # Unless explicitly disabled, ensure the tools bucket is public.
1437
+ # OPENC3_TOOLS_BUCKET_PRIVATE keeps the tools bucket private; the cmd-tlm-api proxies reads via ToolsController.
1438
+ unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false) || ENV.fetch("OPENC3_TOOLS_BUCKET_PRIVATE", false)
1363
1439
  client.ensure_public(ENV['OPENC3_TOOLS_BUCKET'])
1364
1440
  end
1365
1441
  # Always ensure the scriptrunner policy is in place since it is required for script execution
@@ -1377,6 +1453,22 @@ if not ARGV[0].nil? # argument(s) given
1377
1453
  end
1378
1454
  run_migrations(ARGV[1])
1379
1455
 
1456
+ when 'setpassword'
1457
+ if ARGV[1] == '--help' || ARGV[1] == '-h'
1458
+ puts "Usage: cli setpassword"
1459
+ puts ""
1460
+ puts "Set the initial COSMOS password from the OPENC3_API_PASSWORD environment variable."
1461
+ puts "This allows you to skip the password creation screen in the web interface."
1462
+ puts ""
1463
+ puts "The password must be at least 8 characters. This command will fail if a"
1464
+ puts "password has already been set."
1465
+ puts ""
1466
+ puts "Options:"
1467
+ puts " -h, --help Show this help message"
1468
+ exit 0
1469
+ end
1470
+ set_password()
1471
+
1380
1472
  when 'migratepassword'
1381
1473
  migrate_password_hash()
1382
1474
 
data/bin/pipinstall CHANGED
@@ -1,13 +1,45 @@
1
1
  #!/bin/sh
2
- uv venv "$PYTHONUSERBASE"
2
+ uv venv "$PYTHONUSERBASE" --allow-existing
3
3
  echo "uv pip install $@"
4
4
  uv pip install --python "$PYTHONUSERBASE" "$@"
5
5
  if [ $? -eq 0 ]; then
6
6
  echo "Command succeeded"
7
- else
8
- echo "Command failed - retrying with --no-index"
9
- uv pip install --python "$PYTHONUSERBASE" --no-index "$@"
10
- if [ $? -ne 0 ]; then
11
- echo "ERROR: uv pip install failed"
7
+ exit 0
8
+ fi
9
+
10
+ # Collect the last arg and all preceding args
11
+ LAST_ARG=""
12
+ OPTS=""
13
+ PREV=""
14
+ for ARG in "$@"; do
15
+ if [ -n "$PREV" ]; then
16
+ OPTS="${OPTS} ${PREV}"
17
+ fi
18
+ PREV="$ARG"
19
+ done
20
+ LAST_ARG="$PREV"
21
+
22
+ # If last arg is a directory with pyproject.toml, the build may have failed
23
+ # (e.g. Poetry package-mode = false). Try compiling and installing declared
24
+ # dependencies only, without attempting to build the package itself.
25
+ if [ -d "$LAST_ARG" ] && [ -f "$LAST_ARG/pyproject.toml" ]; then
26
+ echo "Warning: Failed to build Python package, attempting to install declared dependencies from pyproject.toml"
27
+ TMPFILE=$(mktemp)
28
+ uv pip compile ${OPTS} "${LAST_ARG}/pyproject.toml" > "$TMPFILE"
29
+ if [ $? -eq 0 ] && [ -s "$TMPFILE" ]; then
30
+ uv pip install --python "$PYTHONUSERBASE" ${OPTS} -r "$TMPFILE"
31
+ if [ $? -eq 0 ]; then
32
+ echo "Dependencies installed successfully"
33
+ rm -f "$TMPFILE"
34
+ exit 0
35
+ fi
12
36
  fi
37
+ rm -f "$TMPFILE"
38
+ fi
39
+
40
+ echo "Warning: Install failed - retrying with --no-index"
41
+ uv pip install --python "$PYTHONUSERBASE" --no-index "$@"
42
+ if [ $? -ne 0 ]; then
43
+ echo "ERROR: uv pip install failed"
44
+ exit 1
13
45
  fi
@@ -199,6 +199,7 @@ HIDDEN:
199
199
  summary: Hides this command from all OpenC3 tools such as Command Sender and Handbook Creator
200
200
  description: Hidden commands do not appear in the Script Runner popup helper when writing scripts.
201
201
  The command still exists in the system and can be sent by scripts.
202
+ since: 6.10.1
202
203
  DISABLED:
203
204
  summary: Disables this command from being sent
204
205
  description: Hides the command and also disables it from being sent by scripts.
@@ -192,4 +192,5 @@ HIDDEN:
192
192
  summary: Hides this item from all the OpenC3 tools
193
193
  description: This item will not appear in PacketViewer or Item Choosers.
194
194
  It also hides this item from appearing in the Script Runner popup helper
195
- when writing scripts. The item will also not be included in decom data.
195
+ when writing scripts. The item will also not be included in decom data.
196
+ since: 6.10.0
@@ -168,11 +168,22 @@ MICROSERVICE:
168
168
  since: 6.0.0
169
169
  parameters:
170
170
  - name: Shard
171
- required: false
171
+ required: true
172
172
  description: Shard number starting from 0
173
173
  values: \d+
174
174
  example: |
175
175
  SHARD 0
176
+ DB_SHARD:
177
+ summary: Shard for target database database if sharding Redis/TSDB
178
+ description: DB Shard. Only used if running multiple database shards typically in Kubernetes
179
+ since: 7.1.0
180
+ parameters:
181
+ - name: DB Shard
182
+ required: true
183
+ description: DB Shard number starting from 0
184
+ values: \d+
185
+ example: |
186
+ DB_SHARD 0
176
187
  STOPPED:
177
188
  summary: Initially creates the microservice in a stopped state (not enabled)
178
189
  since: 6.2.0
@@ -82,14 +82,15 @@ WRITE_CONVERSION:
82
82
  [INST inst_cmds.txt](https://github.com/OpenC3/cosmos/blob/main/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_cmds.txt)
83
83
  or [INST2 inst_cmds.txt](https://github.com/OpenC3/cosmos/blob/main/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_cmds.txt).
84
84
 
85
- :::info Multiple write conversions on command parameters
85
+ :::info[Multiple write conversions on command parameters]
86
86
  When a command is built, each item gets written (and write conversions are run)
87
87
  to set the default value. Then items are written (again write conversions are run)
88
88
  with user provided values. Thus write conversions can be run twice. Also there are
89
89
  no guarantees which parameters have already been written. The packet itself has a
90
- given_values() method which can be used to retrieve a hash of the user provided
90
+ `given_values` attribute which can be used to retrieve a hash of the user provided
91
91
  values to the command. That can be used to check parameter values passed in.
92
92
  :::
93
+
93
94
  parameters:
94
95
  - name: Class Filename
95
96
  required: true
@@ -105,9 +106,48 @@ WRITE_CONVERSION:
105
106
  to the class constructor.
106
107
  values: .*
107
108
  ruby_example: |
108
- WRITE_CONVERSION ip_write_conversion.rb
109
+ # Example command with a WRITE_CONVERSION that sets a command parameter
110
+ # based on the given values of other parameters
111
+ COMMAND INST BLOCK BIG_ENDIAN "Send variable block of data"
112
+ APPEND_PARAMETER BYTE 8 UINT MIN MAX 0x55 "Byte to duplicate"
113
+ FORMAT_STRING "0x%0X"
114
+ APPEND_PARAMETER LENGTH 32 UINT MIN MAX 0 "Length of data"
115
+ APPEND_PARAMETER DATA 0 BLOCK "" "Variable block of data"
116
+ WRITE_CONVERSION block_conversion.rb
117
+ HIDDEN # Because we're filling it in with a conversion
118
+
119
+ # Implemented in INST/lib/block_conversion.rb:
120
+ require 'openc3/conversions/conversion'
121
+ module OpenC3
122
+ class BlockConversion < Conversion
123
+ def call(value, packet, buffer)
124
+ # Use the packet.given_values hash to access user provided values to the command
125
+ byte = packet.given_values['BYTE'] || 0x55
126
+ length = packet.given_values['LENGTH'] || 0
127
+ [byte].pack('C') * length
128
+ end
129
+ end
130
+ end
109
131
  python_example: |
110
- WRITE_CONVERSION openc3/conversions/ip_write_conversion.py
132
+ # Example command with a WRITE_CONVERSION that sets a command parameter
133
+ # based on the given values of other parameters
134
+ COMMAND INST BLOCK BIG_ENDIAN "Send variable block of data"
135
+ APPEND_PARAMETER BYTE 8 UINT MIN MAX 0x55 "Byte to duplicate"
136
+ FORMAT_STRING "0x%0X"
137
+ APPEND_PARAMETER LENGTH 32 UINT MIN MAX 0 "Length of data"
138
+ APPEND_PARAMETER DATA 0 BLOCK "" "Variable block of data"
139
+ WRITE_CONVERSION block_conversion.py
140
+ HIDDEN # Because we're filling it in with a conversion
141
+
142
+ # Implemented in INST/lib/block_conversion.py:
143
+ from openc3.conversions.conversion import Conversion
144
+ class BlockConversion(Conversion):
145
+ def call(self, value, packet, buffer):
146
+ # Use the packet.given_values hash to access user provided values to the command
147
+ byte = packet.given_values.get('BYTE', 0x55)
148
+ length = packet.given_values.get('LENGTH', 0)
149
+ return bytes([byte]) * length
150
+
111
151
  POLY_WRITE_CONVERSION:
112
152
  summary: Adds a polynomial conversion factor to the current command parameter
113
153
  description: See [Polynomial Conversion](/docs/configuration/conversions#polynomial_conversion) for more information.
@@ -127,14 +167,15 @@ GENERIC_WRITE_CONVERSION_START:
127
167
  value. The GENERIC_WRITE_CONVERSION_END keyword specifies that all lines of
128
168
  code for the conversion have been given.
129
169
 
130
- :::info Multiple write conversions on command parameters
170
+ :::info[Multiple write conversions on command parameters]
131
171
  When a command is built, each item gets written (and write conversions are run)
132
172
  to set the default value. Then items are written (again write conversions are run)
133
173
  with user provided values. Thus write conversions can be run twice. Also there are
134
174
  no guarantees which parameters have already been written. The packet itself has a
135
- given_values() method which can be used to retrieve a hash of the user provided
175
+ `given_values` attribute which can be used to retrieve a hash of the user provided
136
176
  values to the command. That can be used to check parameter values passed in.
137
177
  :::
178
+
138
179
  warning: Generic conversions are not a good long term solution. Consider creating
139
180
  a conversion class and using WRITE_CONVERSION instead. WRITE_CONVERSION is easier
140
181
  to debug and higher performance.
@@ -171,4 +212,5 @@ HIDDEN:
171
212
  summary: Hides this parameter from all the OpenC3 tools
172
213
  description: This item will not appear in CmdSender.
173
214
  It also hides this item from appearing in the Script Runner popup helper
174
- when writing scripts. The parameter should not be provided to commands.
215
+ when writing scripts. The parameter should not be provided to commands.
216
+ since: 6.10.0
@@ -3,7 +3,9 @@ HIDDEN:
3
3
  summary: Indicates that the parameter should not be shown to the user in the Table Manager GUI
4
4
  description: Hidden parameters still exist and will be saved to the resulting
5
5
  binary. This is useful for padding and other essential but non-user editable fields.
6
+ since: 6.10.1
6
7
  UNEDITABLE:
7
8
  summary: Indicates that the parameter should be shown to the user but not editable.
8
- description: Uneditable parameters are useful for control fields which the user
9
+ description:
10
+ Uneditable parameters are useful for control fields which the user
9
11
  may be interested in but should not be able to edit.
@@ -159,3 +159,14 @@ TARGET:
159
159
  values: \d+
160
160
  example: |
161
161
  SHARD 0
162
+ DB_SHARD:
163
+ summary: Shard for target database database if sharding Redis/TSDB
164
+ description: DB Shard. Only used if running multiple database shards typically in Kubernetes
165
+ since: 7.1.0
166
+ parameters:
167
+ - name: DB Shard
168
+ required: true
169
+ description: DB Shard number starting from 0
170
+ values: \d+
171
+ example: |
172
+ DB_SHARD 0
@@ -2,8 +2,12 @@
2
2
  LANGUAGE:
3
3
  summary: Programming language of the target interfaces and microservices
4
4
  description: The target language must be either Ruby or Python. The language
5
- determines how the target's interfaces and microservices are run. Note that
6
- both Ruby and Python still use ERB to perform templating.
5
+ determines how the target's interfaces and microservices are run. A target
6
+ must pick one language for its interfaces and microservices &mdash; you cannot
7
+ mix Ruby and Python interfaces/microservices within the same target. Scripts
8
+ executed in Script Runner are independent of this setting and may be written
9
+ in either Ruby or Python regardless of the target's LANGUAGE. Note that both
10
+ Ruby and Python still use ERB to perform templating.
7
11
  example: LANGUAGE python
8
12
  parameters:
9
13
  - language: Programming language
@@ -53,6 +53,9 @@ module OpenC3
53
53
  return nil if item.data_type == :DERIVED
54
54
  configure()
55
55
 
56
+ # No template items to read (e.g. command with fixed template string)
57
+ return nil if @item_keys.empty?
58
+
56
59
  # Scan the response for all the variables in brackets <VARIABLE>
57
60
  values = buffer.scan(@read_regexp)[0]
58
61
  if !values || (values.length != @item_keys.length)
@@ -73,6 +76,12 @@ module OpenC3
73
76
  result = {}
74
77
  configure()
75
78
 
79
+ # No template items to read (e.g. command with fixed template string)
80
+ if @item_keys.empty?
81
+ items.each { |item| result[item.name] = nil }
82
+ return result
83
+ end
84
+
76
85
  # Scan the response for all the variables in brackets <VARIABLE>
77
86
  values = buffer.scan(@read_regexp)[0]
78
87
  if !values || (values.length != @item_keys.length)
@@ -183,7 +183,8 @@ module OpenC3
183
183
  authorize(permission: 'cmd_info', target_name: target_name, packet_name: command_name, manual: manual, scope: scope, token: token)
184
184
  TargetModel.packet(target_name, command_name, type: :CMD, scope: scope)
185
185
  topic = "#{scope}__COMMAND__{#{target_name}}__#{command_name}"
186
- msg_id, msg_hash = Topic.get_newest_message(topic)
186
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
187
+ msg_id, msg_hash = Topic.get_newest_message(topic, db_shard: db_shard)
187
188
  if msg_id
188
189
  msg_hash['buffer'] = msg_hash['buffer'].b
189
190
  return msg_hash
@@ -28,9 +28,11 @@ module OpenC3
28
28
  DELAY_METRICS['log_topic_delta_seconds'] = 0.0
29
29
  DELAY_METRICS['router_topic_delta_seconds'] = 0.0
30
30
  DELAY_METRICS['text_log_topic_delta_seconds'] = 0.0
31
+ DELAY_METRICS['tsdb_ingest_topic_delta_seconds'] = 0.0
31
32
 
32
33
  DURATION_METRICS = {}
33
34
  DURATION_METRICS['decom_duration_seconds'] = 0.0
35
+ DURATION_METRICS['tsdb_ingest_duration_seconds'] = 0.0
34
36
 
35
37
  SUM_METRICS = {}
36
38
  SUM_METRICS['cleanup_total'] = 0
@@ -48,6 +50,8 @@ module OpenC3
48
50
  SUM_METRICS['router_directive_total'] = 0
49
51
  SUM_METRICS['text_log_total'] = 0
50
52
  SUM_METRICS['text_log_error_total'] = 0
53
+ SUM_METRICS['tsdb_ingest_total'] = 0
54
+ SUM_METRICS['tsdb_ingest_error_total'] = 0
51
55
 
52
56
  def get_metrics(manual: false, scope: $openc3_scope, token: $openc3_token)
53
57
  authorize(permission: 'system', manual: manual, scope: scope, token: token)
@@ -79,7 +83,13 @@ module OpenC3
79
83
  result.merge!(duration_metrics)
80
84
  result.merge!(sum_metrics)
81
85
 
82
- result.merge!(MetricModel.redis_metrics)
86
+ redis_metrics = MetricModel.redis_metrics
87
+ redis_metrics.each do |_db_shard, values|
88
+ values.each do |key, value|
89
+ existing = result[key]
90
+ result[key] = value if existing.nil? or value > existing
91
+ end
92
+ end
83
93
 
84
94
  return result
85
95
  end
@@ -117,7 +117,7 @@ module OpenC3
117
117
  # @param packet_name [String] Packet name of the packet
118
118
  # @param item_hash [Hash] Hash of item_name and value for each item you want to change from the current value table
119
119
  # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED
120
- def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, manual: false, scope: $openc3_scope, token: $openc3_token)
120
+ def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, manual: false, scope: $openc3_scope, token: $openc3_token)
121
121
  authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
122
122
  type = type.to_s.intern
123
123
  target_name = target_name.upcase
@@ -155,9 +155,9 @@ module OpenC3
155
155
 
156
156
  # Use an interface microservice if it exists, other use the decom microservice
157
157
  if interface_name
158
- InterfaceTopic.inject_tlm(interface_name, target_name, packet_name, item_hash, type: type, scope: scope)
158
+ InterfaceTopic.inject_tlm(interface_name, target_name, packet_name, item_hash, type: type, stored: stored, scope: scope)
159
159
  else
160
- DecomInterfaceTopic.inject_tlm(target_name, packet_name, item_hash, type: type, scope: scope)
160
+ DecomInterfaceTopic.inject_tlm(target_name, packet_name, item_hash, type: type, stored: stored, scope: scope)
161
161
  end
162
162
  end
163
163
 
@@ -221,7 +221,8 @@ module OpenC3
221
221
  return msg_hash
222
222
  else
223
223
  topic = "#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}"
224
- msg_id, msg_hash = Topic.get_newest_message(topic)
224
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
225
+ msg_id, msg_hash = Topic.get_newest_message(topic, db_shard: db_shard)
225
226
  if msg_id
226
227
  msg_hash['buffer'] = msg_hash['buffer'].b
227
228
  return msg_hash
@@ -446,7 +447,8 @@ module OpenC3
446
447
  packet_name = packet_name.upcase
447
448
  authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
448
449
  topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}"
449
- id, _ = Topic.get_newest_message(topic)
450
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
451
+ id, = Topic.get_newest_message(topic, db_shard: db_shard)
450
452
  results[topic] = id ? id : '0-0'
451
453
  end
452
454
  results.to_a.join(SUBSCRIPTION_DELIMITER)
@@ -463,7 +465,20 @@ module OpenC3
463
465
  authorize(permission: 'tlm', manual: manual, scope: scope, token: token)
464
466
  # Split the list of topic, ID values and turn it into a hash for easy updates
465
467
  lookup = Hash[*id.split(SUBSCRIPTION_DELIMITER)]
466
- xread = Topic.read_topics(lookup.keys, lookup.values, nil, count) # Always don't block
468
+ # Group topics by db_shard for multi-shard support
469
+ db_shard_groups = {}
470
+ lookup.each do |topic, offset|
471
+ target_name = topic.match(/__\{?([^}_]+)\}?__/)[1] rescue nil
472
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
473
+ db_shard_groups[db_shard] ||= { topics: [], offsets: [] }
474
+ db_shard_groups[db_shard][:topics] << topic
475
+ db_shard_groups[db_shard][:offsets] << offset
476
+ end
477
+ xread = {}
478
+ db_shard_groups.each do |db_shard, group|
479
+ result = Topic.read_topics(group[:topics], group[:offsets], nil, count, db_shard: db_shard) # Always don't block
480
+ xread.merge!(result) if result
481
+ end
467
482
  # Return the original ID and and empty array if we didn't get anything
468
483
  packets = []
469
484
  return [id, packets] if xread.empty?
@@ -1,6 +1,6 @@
1
1
  # Remove warnings in CGI
2
2
  saved_verbose = $VERBOSE
3
- $VERBOSE = false
3
+ $VERBOSE = nil
4
4
  require 'faraday'
5
5
  $VERBOSE = saved_verbose
6
6
 
@@ -295,17 +295,12 @@ module OpenC3
295
295
  else
296
296
  data, extra = protocol.read_data(data)
297
297
  end
298
- protocol.read_protocol_output_base(data, extra) unless blank_test
298
+ protocol.read_protocol_output_base(data, extra) unless blank_test or data == :STOP or data == :DISCONNECT
299
299
  if data == :DISCONNECT
300
300
  Logger.info("#{@name}: Protocol #{protocol.class} read_data requested disconnect")
301
301
  return nil
302
302
  end
303
303
  break if data == :STOP
304
- if blank_test
305
- # This means the blank test returned something so we can log
306
- protocol.read_protocol_input_base('', nil)
307
- protocol.read_protocol_output_base(data, extra)
308
- end
309
304
  end
310
305
  next if data == :STOP
311
306
 
@@ -65,7 +65,7 @@ module OpenC3
65
65
 
66
66
  def _request(*method_params, **kw_params)
67
67
  kw_params[:scope] = $openc3_scope unless kw_params[:scope]
68
- kw_params[:json] = true unless kw_params[:json]
68
+ kw_params[:json] = true # This is JsonApi so should always be speaking json
69
69
  @json_api.request(*method_params, **kw_params)
70
70
  end
71
71
  end
@@ -237,7 +237,9 @@ module OpenC3
237
237
  # Now that the file is in S3, trim the Redis stream up until the previous file.
238
238
  # This keeps one minute of data in Redis
239
239
  instance.cleanup_offsets[index].each do |redis_topic, cleanup_offset|
240
- Topic.trim_topic(redis_topic, cleanup_offset)
240
+ target_match = redis_topic.match(/__\{?([^}_]+)\}?__/)
241
+ db_shard = target_match ? Store.db_shard_for_target(target_match[1]) : 0
242
+ Topic.trim_topic(redis_topic, cleanup_offset, db_shard: db_shard)
241
243
  end
242
244
  indexes_to_clear << index
243
245
  end