openc3 7.0.1 → 7.1.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +50 -3
  3. data/data/config/interface_modifiers.yaml +3 -1
  4. data/data/config/item_modifiers.yaml +1 -1
  5. data/data/config/microservice.yaml +15 -2
  6. data/data/config/parameter_modifiers.yaml +49 -7
  7. data/data/config/plugins.yaml +1 -0
  8. data/data/config/target.yaml +11 -0
  9. data/data/config/target_config.yaml +6 -2
  10. data/lib/openc3/api/api.rb +1 -0
  11. data/lib/openc3/api/calendar_api.rb +183 -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/io/json_api.rb +1 -1
  17. data/lib/openc3/logs/log_writer.rb +3 -1
  18. data/lib/openc3/microservices/decom_common.rb +128 -0
  19. data/lib/openc3/microservices/decom_microservice.rb +30 -97
  20. data/lib/openc3/microservices/interface_decom_common.rb +6 -2
  21. data/lib/openc3/microservices/interface_microservice.rb +10 -8
  22. data/lib/openc3/microservices/log_microservice.rb +1 -1
  23. data/lib/openc3/microservices/microservice.rb +3 -2
  24. data/lib/openc3/microservices/queue_microservice.rb +1 -1
  25. data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
  26. data/lib/openc3/microservices/text_log_microservice.rb +1 -2
  27. data/lib/openc3/models/cvt_model.rb +24 -13
  28. data/lib/openc3/models/db_sharded_model.rb +110 -0
  29. data/lib/openc3/models/interface_model.rb +9 -0
  30. data/lib/openc3/models/interface_status_model.rb +33 -3
  31. data/lib/openc3/models/metric_model.rb +96 -37
  32. data/lib/openc3/models/microservice_model.rb +7 -0
  33. data/lib/openc3/models/microservice_status_model.rb +30 -3
  34. data/lib/openc3/models/plugin_model.rb +20 -8
  35. data/lib/openc3/models/queue_model.rb +36 -46
  36. data/lib/openc3/models/reingest_job_model.rb +153 -0
  37. data/lib/openc3/models/scope_model.rb +3 -2
  38. data/lib/openc3/models/script_status_model.rb +4 -20
  39. data/lib/openc3/models/target_model.rb +113 -100
  40. data/lib/openc3/models/trigger_model.rb +1 -1
  41. data/lib/openc3/packets/packet_config.rb +4 -1
  42. data/lib/openc3/packets/parsers/xtce_parser.rb +23 -1
  43. data/lib/openc3/script/script.rb +6 -4
  44. data/lib/openc3/script/script_runner.rb +4 -4
  45. data/lib/openc3/script/telemetry.rb +3 -3
  46. data/lib/openc3/script/web_socket_api.rb +29 -22
  47. data/lib/openc3/system/system.rb +20 -3
  48. data/lib/openc3/topics/command_decom_topic.rb +4 -2
  49. data/lib/openc3/topics/command_topic.rb +9 -5
  50. data/lib/openc3/topics/decom_interface_topic.rb +15 -10
  51. data/lib/openc3/topics/interface_topic.rb +71 -29
  52. data/lib/openc3/topics/limits_event_topic.rb +62 -41
  53. data/lib/openc3/topics/router_topic.rb +61 -21
  54. data/lib/openc3/topics/system_events_topic.rb +18 -1
  55. data/lib/openc3/topics/telemetry_decom_topic.rb +3 -1
  56. data/lib/openc3/topics/telemetry_topic.rb +4 -2
  57. data/lib/openc3/topics/topic.rb +77 -5
  58. data/lib/openc3/utilities/aws_bucket.rb +2 -0
  59. data/lib/openc3/utilities/cli_generator.rb +10 -2
  60. data/lib/openc3/utilities/metric.rb +15 -1
  61. data/lib/openc3/utilities/questdb_client.rb +173 -37
  62. data/lib/openc3/utilities/reingest_job.rb +377 -0
  63. data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
  64. data/lib/openc3/utilities/running_script.rb +8 -10
  65. data/lib/openc3/utilities/store_autoload.rb +78 -52
  66. data/lib/openc3/utilities/store_queued.rb +20 -12
  67. data/lib/openc3/version.rb +5 -5
  68. data/templates/microservice/microservices/TEMPLATE/microservice.py +9 -0
  69. data/templates/plugin/plugin.gemspec +13 -1
  70. data/templates/tool_angular/package.json +2 -2
  71. data/templates/tool_react/package.json +1 -1
  72. data/templates/tool_svelte/package.json +1 -1
  73. data/templates/tool_vue/package.json +3 -3
  74. data/templates/tool_vue/src/router.js +2 -2
  75. data/templates/widget/package.json +2 -2
  76. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f12f777f56b7d64a19cfd89b8886352b6f0fb06bbda59d95d5643b8d4c71f6
4
- data.tar.gz: 1ebffbb1399279e391a47dde42e6f8b9492be10b389c8e63d148aab402ee614c
3
+ metadata.gz: 719b55747a7004cc44da1976c3f15eec6c67a3fca702b56792862f59f02cb8b5
4
+ data.tar.gz: c80082e75c059dac71d15a56264cba9c968f82c494e564e33b598c75ac01a6c2
5
5
  SHA512:
6
- metadata.gz: ee971acf5bfd378f5a238263b467385286997900bea7fe549b8458c707568fb3a2265b261ee8794601bd42db7d0c2e8a983cf3d81a52c0c5069b9be0b41c4966
7
- data.tar.gz: 5ba0ea7fb684a6e3e029864289612975db9555d1754f30f65baf4cf3193208516ea74029bb889b4575014b7953c45990c7e515dcf8f43d292bf8fc844de15a6b
6
+ metadata.gz: 5b3c1ca82717f1debd25ab4a6ac0239aa9baee2b5f736f61b7f52103397b608ab3de8fd0d50e10f0a4481b507b2a519987a4c4c68f7d0f3770cd48f3e165b8eb
7
+ data.tar.gz: d2bc96742a2555be7c8149dd728155a76ba7c1a16474eb26a36388bdedaacc1fba9c447ab373beebd8fd58ac81248541531de1a163bc156be195c76e34b515f2
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
 
@@ -443,6 +444,9 @@ def unload_plugin(plugin_name, scope:)
443
444
  plugin_model = OpenC3::PluginModel.get_model(name: plugin_name, scope: scope)
444
445
  plugin_model.destroy
445
446
  OpenC3::LocalMode.remove_local_plugin(plugin_name, scope: scope)
447
+ # Remove the backing gem now that no PluginModel references it,
448
+ # so it disappears from the admin Packages tab.
449
+ OpenC3::PluginModel.cleanup_gem(plugin_name, scope: scope)
446
450
  OpenC3::Logger.info("PluginModel destroyed: #{plugin_name}", scope: scope)
447
451
  rescue => e
448
452
  abort("Error uninstalling plugin: #{scope}: #{plugin_name}: #{e.formatted}")
@@ -1143,6 +1147,48 @@ if not ARGV[0].nil? # argument(s) given
1143
1147
  end
1144
1148
  cli_pkg_uninstall(ARGV[1], scope: ARGV[2])
1145
1149
 
1150
+ when 'reingest'
1151
+ # Internal command spawned by StorageController via ProcessManager so the
1152
+ # reingest runs in its own process and System singleton resets cannot
1153
+ # collide with the cmd-tlm-api Rails server.
1154
+ if ARGV[1].nil? || ARGV[2].nil? || ARGV[1] == '--help' || ARGV[1] == '-h'
1155
+ puts "Usage: cli reingest JOB_ID SCOPE"
1156
+ exit(ARGV[1].nil? ? 1 : 0)
1157
+ end
1158
+ require 'openc3/utilities/reingest_job'
1159
+ require 'openc3/models/reingest_job_model'
1160
+ job_id = ARGV[1]
1161
+ scope = ARGV[2]
1162
+ job = OpenC3::ReingestJobModel.get_model(name: job_id, scope: scope)
1163
+ if job.nil?
1164
+ OpenC3::Logger.error("Reingest job #{job_id} not found in scope #{scope}")
1165
+ exit(1)
1166
+ end
1167
+ begin
1168
+ OpenC3::ReingestJob.new(
1169
+ job_id: job_id,
1170
+ files: job.files,
1171
+ path: job.path,
1172
+ bucket: job.bucket,
1173
+ scope: scope,
1174
+ target_version: job.target_version,
1175
+ ).run
1176
+ rescue Exception => e
1177
+ # ReingestJob#run already marks Crashed for errors raised during the run.
1178
+ # This catches failures from the constructor itself (or anything before
1179
+ # run gets its rescue installed) so the model doesn't sit in Queued forever.
1180
+ OpenC3::Logger.error("Reingest job #{job_id} crashed before run: #{e.formatted}")
1181
+ begin
1182
+ job.state = 'Crashed'
1183
+ job.error = e.message
1184
+ job.finished_at = Time.now.utc.iso8601
1185
+ job.update
1186
+ rescue => e2
1187
+ OpenC3::Logger.error("Reingest job #{job_id} failed to mark Crashed: #{e2.message}")
1188
+ end
1189
+ exit(1)
1190
+ end
1191
+
1146
1192
  when 'generate'
1147
1193
  # To test against a local copy call this file from the root cosmos directory like this:
1148
1194
  # ruby -Iopenc3/lib openc3/bin/openc3cli generate ...
@@ -1390,8 +1436,9 @@ if not ARGV[0].nil? # argument(s) given
1390
1436
  end
1391
1437
  end
1392
1438
  end
1393
- # Unless explicitly disabled, ensure the tools bucket is public
1394
- unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false)
1439
+ # Unless explicitly disabled, ensure the tools bucket is public.
1440
+ # OPENC3_TOOLS_BUCKET_PRIVATE keeps the tools bucket private; the cmd-tlm-api proxies reads via ToolsController.
1441
+ unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false) || ENV.fetch("OPENC3_TOOLS_BUCKET_PRIVATE", false)
1395
1442
  client.ensure_public(ENV['OPENC3_TOOLS_BUCKET'])
1396
1443
  end
1397
1444
  # Always ensure the scriptrunner policy is in place since it is required for script execution
@@ -212,7 +212,9 @@ WORK_DIR:
212
212
  WORK_DIR '/openc3/lib/openc3/microservices'
213
213
  PORT:
214
214
  summary: Open port for the microservice
215
- description: Kubernetes needs a Service to be applied to open a port so this is required for Kubernetes support
215
+ description:
216
+ Kubernetes needs a Service to be applied to open a port so this is required for Kubernetes support
217
+ See [Exposing Microservices](/docs/guides/exposing-microservices) for more information.
216
218
  since: 5.7.0
217
219
  parameters:
218
220
  - name: Number
@@ -193,4 +193,4 @@ HIDDEN:
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
195
  when writing scripts. The item will also not be included in decom data.
196
- since: 6.10.1
196
+ since: 6.10.0
@@ -40,7 +40,9 @@ MICROSERVICE:
40
40
  WORK_DIR .
41
41
  PORT:
42
42
  summary: Open port for the microservice
43
- description: Kubernetes needs a Service to be applied to open a port so this is required for Kubernetes support
43
+ description:
44
+ Kubernetes needs a Service to be applied to open a port so this is required for Kubernetes support.
45
+ See [Exposing Microservices](/docs/guides/exposing-microservices) for more information.
44
46
  since: 5.0.10
45
47
  parameters:
46
48
  - name: Number
@@ -168,11 +170,22 @@ MICROSERVICE:
168
170
  since: 6.0.0
169
171
  parameters:
170
172
  - name: Shard
171
- required: false
173
+ required: true
172
174
  description: Shard number starting from 0
173
175
  values: \d+
174
176
  example: |
175
177
  SHARD 0
178
+ DB_SHARD:
179
+ summary: Shard for target database database if sharding Redis/TSDB
180
+ description: DB Shard. Only used if running multiple database shards typically in Kubernetes
181
+ since: 7.1.0
182
+ parameters:
183
+ - name: DB Shard
184
+ required: true
185
+ description: DB Shard number starting from 0
186
+ values: \d+
187
+ example: |
188
+ DB_SHARD 0
176
189
  STOPPED:
177
190
  summary: Initially creates the microservice in a stopped state (not enabled)
178
191
  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
@@ -38,6 +38,7 @@ VARIABLE_STATE:
38
38
  VARIABLE log_retain_time 172800
39
39
  VARIABLE_STATE "24 hours" 86400
40
40
  VARIABLE_STATE "48 hours" 172800
41
+ VARIABLE_STATE 60 # Both description and value are set to 60
41
42
  parameters:
42
43
  - name: State Description
43
44
  required: false
@@ -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
@@ -17,6 +17,7 @@
17
17
 
18
18
  require 'openc3/script/extract'
19
19
  require 'openc3/script/api_shared'
20
+ require 'openc3/api/calendar_api'
20
21
  require 'openc3/api/cmd_api'
21
22
  require 'openc3/api/config_api'
22
23
  require 'openc3/api/interface_api'
@@ -0,0 +1,183 @@
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 'date'
15
+ require 'openc3/models/timeline_model'
16
+ require 'openc3/models/activity_model'
17
+ require 'openc3/topics/timeline_topic'
18
+
19
+ module OpenC3
20
+ module Api
21
+ # NOTE: These methods are intentionally NOT added to WHITELIST. Their signatures
22
+ # match openc3/lib/openc3/script/calendar.rb (no manual:/token: kwargs), so they
23
+ # cannot be dispatched via JSON-RPC (which auto-injects manual/token from headers).
24
+ # The script-side calendar methods reach the server through the timeline/activity
25
+ # HTTP controllers, which call these helpers after performing their own
26
+ # authorization.
27
+
28
+ # Returns an array of all timelines for the given scope.
29
+ def list_timelines(scope: $openc3_scope)
30
+ ret = []
31
+ TimelineModel.all.each do |timeline, value|
32
+ if scope == timeline.split('__')[0]
33
+ ret << value
34
+ end
35
+ end
36
+ ret
37
+ end
38
+
39
+ # Creates a new timeline and deploys its microservice.
40
+ # @return [Hash] the created timeline as a hash
41
+ def create_timeline(name, color: nil, scope: $openc3_scope)
42
+ model = TimelineModel.new(name: name, color: color, scope: scope)
43
+ model.create()
44
+ model.deploy()
45
+ model.as_json()
46
+ end
47
+
48
+ # @return [Hash, nil] the timeline as a hash, or nil if not found
49
+ def get_timeline(name, scope: $openc3_scope)
50
+ model = TimelineModel.get(name: name, scope: scope)
51
+ return nil if model.nil?
52
+ model.as_json()
53
+ end
54
+
55
+ # Updates the color of an existing timeline.
56
+ # @return [Hash, nil] the updated timeline as a hash, or nil if not found
57
+ def set_timeline_color(name, color, scope: $openc3_scope)
58
+ model = TimelineModel.get(name: name, scope: scope)
59
+ return nil if model.nil?
60
+ model.color = color
61
+ model.update()
62
+ model.notify(kind: 'updated')
63
+ model.as_json()
64
+ end
65
+
66
+ # Updates the execute flag of an existing timeline.
67
+ # @return [Hash, nil] the updated timeline as a hash, or nil if not found
68
+ def set_timeline_execute(name, enable, scope: $openc3_scope)
69
+ model = TimelineModel.get(name: name, scope: scope)
70
+ return nil if model.nil?
71
+ model.execute = enable
72
+ model.update()
73
+ model.notify(kind: 'updated')
74
+ model.as_json()
75
+ end
76
+
77
+ # Deletes a timeline (and optionally all of its activities when force is true).
78
+ # @return [Hash, nil] {'name' => name}, or nil if not found
79
+ def delete_timeline(name, force: false, scope: $openc3_scope)
80
+ model = TimelineModel.get(name: name, scope: scope)
81
+ return nil if model.nil?
82
+ TimelineModel.delete(name: name, scope: scope, force: force)
83
+ model.undeploy()
84
+ model.notify(kind: 'deleted')
85
+ { 'name' => name }
86
+ end
87
+
88
+ # Creates a new activity on the specified timeline.
89
+ # username is read from data['username'] if present and is used for the audit event.
90
+ # @return [Hash] the created activity as a hash
91
+ def create_timeline_activity(name, kind:, start:, stop:, data: {}, recurring: nil, scope: $openc3_scope)
92
+ data ||= {}
93
+ hash = {
94
+ kind: kind,
95
+ start: _cal_to_epoch(start),
96
+ stop: _cal_to_epoch(stop),
97
+ data: data,
98
+ }
99
+ if recurring
100
+ recurring = recurring.dup
101
+ if recurring['end']
102
+ recurring['end'] = _cal_to_epoch(recurring['end'])
103
+ end
104
+ hash[:recurring] = recurring
105
+ end
106
+ model = ActivityModel.from_json(hash, name: name, scope: scope)
107
+ model.create(username: data['username'])
108
+ model.as_json()
109
+ end
110
+
111
+ # Updates an existing activity on the specified timeline.
112
+ # @return [Hash, nil] the updated activity as a hash, or nil if not found
113
+ def update_timeline_activity(name, id:, kind:, start:, stop:, uuid:, data: {}, scope: $openc3_scope)
114
+ data ||= {}
115
+ model = ActivityModel.score(name: name, score: id.to_i, uuid: uuid, scope: scope)
116
+ return nil if model.nil?
117
+ model.update(
118
+ start: _cal_to_epoch(start),
119
+ stop: _cal_to_epoch(stop),
120
+ kind: kind,
121
+ data: data,
122
+ username: data['username'],
123
+ )
124
+ model.as_json()
125
+ end
126
+
127
+ # @return [Hash, nil] the activity as a hash, or nil if not found
128
+ def get_timeline_activity(name, start, uuid, scope: $openc3_scope)
129
+ model = ActivityModel.score(name: name, score: start.to_i, uuid: uuid, scope: scope)
130
+ return nil if model.nil?
131
+ model.as_json()
132
+ end
133
+
134
+ # Returns activities on the timeline in the given window.
135
+ # When start/stop are nil, defaults to a window of [now - 7 days, now + 7 days].
136
+ # When limit is nil, defaults to one event per minute over the window.
137
+ # @return [Array<Hash>] the matching activities
138
+ def get_timeline_activities(name, start: nil, stop: nil, limit: nil, scope: $openc3_scope)
139
+ now = DateTime.now.new_offset(0)
140
+ start_score = start.nil? ? (now - 7).strftime('%s').to_i : _cal_to_epoch(start)
141
+ stop_score = stop.nil? ? (now + 7).strftime('%s').to_i : _cal_to_epoch(stop)
142
+ limit ||= ((stop_score - start_score) / 60).to_i
143
+ ActivityModel.get(name: name, scope: scope, start: start_score, stop: stop_score, limit: limit)
144
+ end
145
+
146
+ # Removes an activity (or all members of its recurring group when recurring is truthy).
147
+ # @return [Integer] number of activities removed (0 indicates not found)
148
+ def delete_timeline_activity(name, start, uuid, recurring: nil, scope: $openc3_scope)
149
+ ActivityModel.destroy(name: name, scope: scope, score: start.to_i, uuid: uuid, recurring: recurring)
150
+ end
151
+
152
+ # @return [Integer] count of activities on the timeline
153
+ def count_timeline_activities(name, scope: $openc3_scope)
154
+ ActivityModel.count(name: name, scope: scope)
155
+ end
156
+
157
+ # Commits an event to an existing activity.
158
+ # @return [Hash, nil] the activity as a hash, or nil if not found
159
+ def commit_timeline_activity(name, start, uuid, status:, message: nil, scope: $openc3_scope)
160
+ model = ActivityModel.score(name: name, score: start.to_i, uuid: uuid, scope: scope)
161
+ return nil if model.nil?
162
+ model.commit(status: status, message: message)
163
+ model.as_json()
164
+ end
165
+
166
+ # Convert a value to an epoch integer. Accepts Integer/Numeric (treated as already-epoch),
167
+ # numeric strings, and ISO-style date/time strings or DateTime/Time objects.
168
+ def _cal_to_epoch(value)
169
+ case value
170
+ when Integer
171
+ value
172
+ when Numeric
173
+ value.to_i
174
+ when DateTime, Time, Date
175
+ value.to_datetime.strftime('%s').to_i
176
+ else
177
+ s = value.to_s
178
+ return s.to_i if s.match?(/\A-?\d+\z/)
179
+ DateTime.parse(s).strftime('%s').to_i
180
+ end
181
+ end
182
+ end
183
+ end
@@ -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
 
@@ -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