openc3 6.0.2 → 6.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/bin/pipinstall +1 -1
  3. data/bin/pipuninstall +10 -0
  4. data/data/config/parameter_modifiers.yaml +11 -0
  5. data/data/config/widgets.yaml +38 -1
  6. data/lib/openc3/api/settings_api.rb +35 -2
  7. data/lib/openc3/api/tlm_api.rb +11 -2
  8. data/lib/openc3/config/config_parser.rb +4 -2
  9. data/lib/openc3/io/json_api_object.rb +4 -4
  10. data/lib/openc3/microservices/interface_decom_common.rb +4 -0
  11. data/lib/openc3/microservices/periodic_microservice.rb +26 -1
  12. data/lib/openc3/migrations/20250108060000_news_feed.rb +15 -0
  13. data/lib/openc3/models/activity_model.rb +11 -4
  14. data/lib/openc3/models/news_model.rb +38 -0
  15. data/lib/openc3/models/python_package_model.rb +2 -1
  16. data/lib/openc3/models/scope_model.rb +3 -1
  17. data/lib/openc3/models/timeline_model.rb +3 -6
  18. data/lib/openc3/packets/commands.rb +2 -1
  19. data/lib/openc3/packets/packet.rb +1 -1
  20. data/lib/openc3/script/calendar.rb +21 -18
  21. data/lib/openc3/script/plugins.rb +10 -2
  22. data/lib/openc3/script/script.rb +11 -6
  23. data/lib/openc3/script/script_runner.rb +22 -11
  24. data/lib/openc3/script/storage.rb +3 -3
  25. data/lib/openc3/script/tables.rb +49 -0
  26. data/lib/openc3/script/web_socket_api.rb +2 -2
  27. data/lib/openc3/utilities/local_mode.rb +13 -2
  28. data/lib/openc3/utilities/target_file.rb +10 -2
  29. data/lib/openc3/version.rb +6 -6
  30. data/templates/tool_angular/package.json +2 -2
  31. data/templates/tool_react/package.json +1 -1
  32. data/templates/tool_svelte/package.json +1 -1
  33. data/templates/tool_vue/package.json +3 -3
  34. data/templates/widget/package.json +2 -2
  35. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7cb703367de9f45fc0ffb6d7fd9e2d4fbf075d3c2be95081468558a757df2f6
4
- data.tar.gz: 2580ce86c27ac4dc6a8dcdaa2e8c8531de1ff3ad5fb803516b70e4c8ac74c90f
3
+ metadata.gz: be3485e674a1fe4a236e3a71b57aff4d7d7398a0638a21d8069994739a53edab
4
+ data.tar.gz: 911ee7331cc9100dc1dfbb7abe3e7f38baaa5b201be2974d882f0ef01701e9da
5
5
  SHA512:
6
- metadata.gz: 43cb5951f41970eed0b27f355894d5c014b3f69132abe381640e6d9c1d9d04ab7eab8aecaf14c8e7ec1a51353ec953c68ee4e1ba7783f6d58014ae545dde48cf
7
- data.tar.gz: a146e2222479e99a6afe1b726bc0e5c08863db658203538d3781ba83d06e3944f74e41e85b49572f52ddd59d56b55428b937e4ab3a6902277a4dd34df2746e22
6
+ metadata.gz: eafafaac4ae90e0bb594446fefdfc7b9e6eb96be4b08c5798d827d60a2377fad9252bca9d2e4495b8d4073283aa957d89fab0214d998bd9c4bbbb5d9174000cc
7
+ data.tar.gz: ea90586ba1347a74964c3cbb181810463caf507bb22c88bed8253b2fc9164b7f1648300d35163aba95f9e96f29f31ccb6f4406063dfefa7d33fd8057acddc416
data/bin/pipinstall CHANGED
@@ -9,6 +9,6 @@ else
9
9
  echo "Command failed - retrying with --no-index"
10
10
  pip3 install --no-index "$@"
11
11
  if [ $? -eq 0 ]; then
12
- echo "ERROR: pip install failed"
12
+ echo "ERROR: pip3 install failed"
13
13
  fi
14
14
  fi
data/bin/pipuninstall ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ python3 -m venv $PYTHONUSERBASE
3
+ source $PYTHONUSERBASE/bin/activate
4
+ echo "pip3 uninstall $@"
5
+ pip3 uninstall "$@"
6
+ if [ $? -eq 0 ]; then
7
+ echo "Command succeeded"
8
+ else
9
+ echo "ERROR: pip3 uninstall failed"
10
+ fi
@@ -71,6 +71,17 @@ WRITE_CONVERSION:
71
71
  factor is applied to the value entered by the user before it is written into
72
72
  the binary command packet and sent.
73
73
 
74
+ When applying a write_conversion sometimes the data type changes,
75
+ e.g. creating a UINT from an input STRING (for an example of this see
76
+ [ip_write_conversion.rb](https://github.com/OpenC3/cosmos/blob/main/openc3/lib/openc3/conversions/ip_write_conversion.rb)
77
+ or [ip_write_conversion.py](https://github.com/OpenC3/cosmos/blob/main/openc3/python/openc3/conversions/ip_write_conversion.py)).
78
+ In this case, the command definition data type is UINT and the min, max values don't matter
79
+ (but must be given) so are typically set to MIN MAX. The default value is important
80
+ and should be specified as a string. For a full example see the IP_ADDRESS parameter
81
+ in the TIME_OFFSET command definition of the COSMOS Demo
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
+ 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
+
74
85
  :::info Multiple write conversions on command parameters
75
86
  When a command is built, each item gets written (and write conversions are run)
76
87
  to set the default value. Then items are written (again write conversions are run)
@@ -245,6 +245,40 @@ Telemetry Widgets:
245
245
  example: |
246
246
  ARRAY INST HEALTH_STATUS ARY 250 80 "0x%x" 6 FORMATTED
247
247
  ARRAY INST HEALTH_STATUS ARY2 200 100 nil 4 WITH_UNITS
248
+ ARRAYPLOT:
249
+ summary: Plot an array of values.
250
+ description:
251
+ The item can either be a simple array or a 2D array of x values and y values, e.g. [[x1, x2, x3], [y1, y2, y3]].
252
+ If the X_AXIS setting is not specified, the X axis starts with 0 and increments by 1.
253
+ If the X_AXIS setting is used the x values of a 2D array will be ignored.
254
+ settings:
255
+ TITLE:
256
+ summary: Title of the plot
257
+ parameters:
258
+ - name: Title
259
+ required: true
260
+ description: Title of the plot
261
+ values: .+
262
+ X_AXIS:
263
+ summary: Define the x-axis parameters for the plot
264
+ parameters:
265
+ - name: Start
266
+ required: true
267
+ description: Start value for the x-axis
268
+ values: .+
269
+ - name: Step
270
+ required: true
271
+ description: Step value for the x-axis
272
+ values: .+
273
+ # Inject the graph settings
274
+ <%= MetaConfigParser.load('graph_settings.yaml').to_meta_config_yaml(8) %>
275
+ example: |
276
+ ARRAYPLOT
277
+ SETTING TITLE "Array Data"
278
+ SETTING ITEM INST HEALTH_STATUS ARY
279
+ SETTING ITEM INST HEALTH_STATUS ARY2
280
+ SETTING SIZE 600 400
281
+ SETTING X_AXIS 10 10
248
282
  BLOCK:
249
283
  summary: Displays BLOCK data organized into rows and space separated
250
284
  parameters:
@@ -372,6 +406,7 @@ Telemetry Widgets:
372
406
  FORMATVALUE INST LATEST TEMP1 %.2f CONVERTED 20
373
407
  LABELLED:
374
408
  summary: Displays a LABEL followed by a LED
409
+ description: See the LED widget for more information
375
410
  parameters:
376
411
  - name: Target name
377
412
  required: true
@@ -688,6 +723,7 @@ Telemetry Widgets:
688
723
  Additional values can be added by using the LED_COLOR setting. For example
689
724
  LED INST PARAMS VALUE3 RAW can be followed by SETTING LED_COLOR 0 GREEN,
690
725
  SETTING LED_COLOR 1 RED, and SETTING LED_COLOR ANY ORANGE.
726
+ See LIMITSCOLOR for a widget that displays a circle depicting the limits color of an item.
691
727
  parameters:
692
728
  - name: Target name
693
729
  required: true
@@ -791,7 +827,8 @@ Telemetry Widgets:
791
827
  LIMITSCOLUMN INST HEALTH_STATUS TEMP1 CONVERTED 50 200
792
828
  LIMITSCOLUMN INST HEALTH_STATUS TEMP1
793
829
  LIMITSCOLOR:
794
- summary: Displays a circle depicting the limits color of an item
830
+ summary: Displays a circle depicting the limits color of an item.
831
+ See LED for a widget that displays a circle which changes to an arbitrary color based on telemetry values.
795
832
  parameters:
796
833
  - name: Target name
797
834
  required: true
@@ -14,13 +14,23 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
21
21
  # if purchased from OpenC3, Inc.
22
22
 
23
+ begin
24
+ require 'openc3-enterprise/version'
25
+ VERSION = OPENC3_ENTERPRISE_VERSION
26
+ ENTERPRISE = true
27
+ rescue LoadError
28
+ require 'openc3/version'
29
+ VERSION = OPENC3_VERSION
30
+ ENTERPRISE = false
31
+ end
23
32
  require 'openc3/models/setting_model'
33
+ require 'openc3/models/news_model'
24
34
 
25
35
  module OpenC3
26
36
  module Api
@@ -31,7 +41,8 @@ module OpenC3
31
41
  'get_setting',
32
42
  'get_settings',
33
43
  'set_setting',
34
- 'save_setting' # DEPRECATED
44
+ 'save_setting', # DEPRECATED
45
+ 'update_news',
35
46
  ])
36
47
 
37
48
  def list_settings(manual: false, scope: $openc3_scope, token: $openc3_token)
@@ -68,5 +79,27 @@ module OpenC3
68
79
  end
69
80
  # save_setting is DEPRECATED
70
81
  alias save_setting set_setting
82
+
83
+ # Update the news feed on demand to respond to frontend setting changes
84
+ def update_news(manual: false, scope: $openc3_scope, token: $openc3_token)
85
+ authorize(permission: 'admin', manual: manual, scope: scope, token: token)
86
+ conn = Faraday.new(
87
+ url: 'https://news.openc3.com',
88
+ params: {version: VERSION, enterprise: ENTERPRISE},
89
+ )
90
+ response = conn.get('/news')
91
+ if response.success?
92
+ NewsModel.set(response.body)
93
+ else
94
+ NewsModel.news_error(response)
95
+ end
96
+
97
+ # Test code to update the news feed with a dummy message
98
+ # data = NewsModel.all()
99
+ # json = JSON.parse(data)
100
+ # json.unshift( { date: Time.now.utc.iso8601, title: "News at #{Time.now}", body: "The news feed has been updated at #{Time.now}." })
101
+ # json.pop if json.length > 5
102
+ # NewsModel.set(json.to_json)
103
+ end
71
104
  end
72
105
  end
@@ -35,7 +35,7 @@ module OpenC3
35
35
  'tlm_raw',
36
36
  'tlm_formatted',
37
37
  'tlm_with_units',
38
- 'tlm_variable',
38
+ 'tlm_variable', # DEPRECATED
39
39
  'set_tlm',
40
40
  'inject_tlm',
41
41
  'override_tlm',
@@ -131,7 +131,16 @@ module OpenC3
131
131
  if item_hash
132
132
  item_hash = item_hash.transform_keys(&:upcase)
133
133
  # Check that the items exist ... exceptions are raised if not
134
- TargetModel.packet_items(target_name, packet_name, item_hash.keys, scope: scope)
134
+ items = TargetModel.packet_items(target_name, packet_name, item_hash.keys, scope: scope)
135
+ if type == :CONVERTED
136
+ # If the type is converted, check that the item states are valid
137
+ item_hash.each do |item_name, item_value|
138
+ item = items.find { |i| i['name'] == item_name.to_s.upcase }
139
+ if item['states'] && !item['states'][item_value]
140
+ raise "Unknown state '#{item_value}' for #{item['name']}, must be one of #{item['states'].keys.join(', ')}"
141
+ end
142
+ end
143
+ end
135
144
  else
136
145
  # Check that the packet exists ... exceptions are raised if not
137
146
  TargetModel.packet(target_name, packet_name, scope: scope)
@@ -380,9 +380,11 @@ module OpenC3
380
380
  return Float::INFINITY
381
381
  when 'NEG_INFINITY'
382
382
  return -Float::INFINITY
383
- else
384
- raise ArgumentError, "Could not convert constant: #{value}"
385
383
  end
384
+ # NOTE: No else case because of the following scenario:
385
+ # If the value type is a UINT but they have a WRITE_CONVERSION that takes a string
386
+ # then the default value will be a string. In that case we just want to return the string.
387
+ # For example, the IP_ADDRESS parameter in the TIME_OFFSET command in the Demo plugin.
386
388
  end
387
389
  return value
388
390
  end
@@ -67,6 +67,7 @@ module OpenC3
67
67
  @authentication = authentication.nil? ? generate_auth() : authentication
68
68
  @timeout = timeout
69
69
  @shutdown = false
70
+ # JsonDRb.debug = true # Enable for debugging
70
71
  end
71
72
 
72
73
  # generate the auth object
@@ -179,12 +180,11 @@ module OpenC3
179
180
  # NOTE: This is a helper method and should not be called directly
180
181
  def _generate_data(kwargs)
181
182
  data = kwargs[:data]
182
- if data.nil?
183
- data = kwargs[:data] = {}
184
- elsif data.is_a?(Hash) == false and data.is_a?(String) == false
183
+ # data can be nil but otherwise must be a Hash or String
184
+ if !data.nil? and !data.is_a?(Hash) and !data.is_a?(String)
185
185
  raise JsonApiError, "incorrect type for keyword 'data' MUST be Hash or String: #{data}"
186
186
  end
187
- return kwargs[:json] ? JSON.generate(kwargs[:data]) : kwargs[:data]
187
+ return kwargs[:json] ? JSON.generate(data) : data
188
188
  end
189
189
 
190
190
  # NOTE: This is a helper method and should not be called directly
@@ -36,6 +36,10 @@ module OpenC3
36
36
  packet.received_count += 1
37
37
  packet.received_time = Time.now.sys
38
38
  TelemetryTopic.write_packet(packet, scope: @scope)
39
+ # If the inject_tlm parameters are bad we rescue so
40
+ # interface_microservice and decom_microservice can continue
41
+ rescue => e
42
+ @logger.error "inject_tlm error due to #{e.message}"
39
43
  end
40
44
 
41
45
  def handle_build_cmd(build_cmd_json, msg_id)
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2022 OpenC3, Inc.
3
+ # Copyright 2025 OpenC3, Inc.
4
4
  # All Rights Reserved.
5
5
  #
6
6
  # This program is free software; you can modify and/or redistribute it
@@ -18,15 +18,39 @@
18
18
 
19
19
  require 'openc3/microservices/microservice'
20
20
  require 'openc3/models/offline_access_model'
21
+ require 'openc3/models/news_model'
22
+ # The VERSION and ENTERPRISE constants are set by settings_api.rb
23
+ require 'openc3/api/settings_api'
21
24
 
22
25
  module OpenC3
23
26
  class PeriodicMicroservice < Microservice
27
+ include Api
28
+
24
29
  STARTUP_DELAY_SECONDS = 2 * 60 # Two Minutes
25
30
  SLEEP_PERIOD_SECONDS = 24 * 60 * 60 # Run once per day
26
31
 
27
32
  def initialize(*args)
28
33
  super(*args)
29
34
  @metric.set(name: 'periodic_total', value: @count, type: 'counter')
35
+ @conn = nil # Faraday connection set by get_news
36
+ get_news()
37
+ end
38
+
39
+ def get_news
40
+ if get_setting('news_feed', scope: @scope)
41
+ unless @conn
42
+ @conn = Faraday.new(
43
+ url: 'https://news.openc3.com',
44
+ params: {version: VERSION, enterprise: ENTERPRISE},
45
+ )
46
+ end
47
+ response = @conn.get('/news')
48
+ if response.success?
49
+ NewsModel.set(response.body)
50
+ else
51
+ NewsModel.news_error(response)
52
+ end
53
+ end
30
54
  end
31
55
 
32
56
  def run
@@ -52,6 +76,7 @@ module OpenC3
52
76
  @metric.set(name: 'periodic_total', value: @count, type: 'counter')
53
77
  break if @cancel_thread
54
78
  break if @run_sleeper.sleep(SLEEP_PERIOD_SECONDS)
79
+ get_news()
55
80
  end
56
81
  end
57
82
 
@@ -0,0 +1,15 @@
1
+ require 'openc3/utilities/migration'
2
+ require 'openc3/models/scope_model'
3
+ require 'openc3/models/setting_model'
4
+
5
+ module OpenC3
6
+ class NewsFeed < Migration
7
+ def self.run
8
+ SettingModel.set({ name: 'news_feed', data: true }, scope: 'DEFAULT')
9
+ end
10
+ end
11
+ end
12
+
13
+ unless ENV['OPENC3_NO_MIGRATE']
14
+ OpenC3::NewsFeed.run
15
+ end
@@ -66,10 +66,17 @@ module OpenC3
66
66
  end
67
67
 
68
68
  # @return [String|nil] String of the saved json or nil if score not found under primary_key
69
- def self.score(name:, score:, scope:)
70
- value = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score, :limit => [0, 1]).first
71
- if value
72
- return ActivityModel.from_json(value, name: name, scope: scope)
69
+ def self.score(name:, score:, scope:, uuid: nil)
70
+ values = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score)
71
+ if values and values.length > 0
72
+ if uuid
73
+ values.each do |value|
74
+ activity = ActivityModel.from_json(value, name: name, scope: scope)
75
+ return activity if activity.uuid == uuid
76
+ end
77
+ else
78
+ return ActivityModel.from_json(values[0], name: name, scope: scope)
79
+ end
73
80
  end
74
81
  return nil
75
82
  end
@@ -0,0 +1,38 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'openc3/models/model'
20
+ require 'openc3/utilities/store'
21
+
22
+ module OpenC3
23
+ class NewsModel < Model
24
+ PRIMARY_KEY = 'openc3_news'
25
+
26
+ def self.set(news)
27
+ Store.set(PRIMARY_KEY, news)
28
+ end
29
+
30
+ def self.all()
31
+ Store.get(PRIMARY_KEY)
32
+ end
33
+
34
+ def self.news_error(response)
35
+ Store.set(PRIMARY_KEY, [{ date: Time.now.utc.iso8601, title: 'News Error', body: "Error contacting OpenC3 news feed (status: #{response.status})" }].to_json)
36
+ end
37
+ end
38
+ end
@@ -104,7 +104,8 @@ module OpenC3
104
104
  def self.destroy(name, scope:)
105
105
  package_name, version = self.extract_name_and_version(name)
106
106
  Logger.info "Uninstalling package: #{name}"
107
- result = OpenC3::ProcessManager.instance.spawn(["pip", "uninstall", package_name, "-y"], "package_uninstall", name, Time.now + 3600.0, scope: scope)
107
+ pip_args = ["-y", package_name]
108
+ result = OpenC3::ProcessManager.instance.spawn(["/openc3/bin/pipuninstall"] + pip_args, "package_uninstall", name, Time.now + 3600.0, scope: scope)
108
109
  return result.name
109
110
  end
110
111
 
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -400,6 +400,8 @@ module OpenC3
400
400
  SettingModel.set({ name: 'rubygems_url', data: ENV['RUBYGEMS_URL'] || 'https://rubygems.org' }, scope: @scope) unless setting
401
401
  setting = SettingModel.get(name: 'pypi_url')
402
402
  SettingModel.set({ name: 'pypi_url', data: ENV['PYPI_URL'] || 'https://pypi.org' }, scope: @scope) unless setting
403
+ # Set the news feed to true by default, don't bother checking if it's already set
404
+ SettingModel.set({ name: 'news_feed', data: true }, scope: @scope)
403
405
  end
404
406
  end
405
407
  end
@@ -94,13 +94,10 @@ module OpenC3
94
94
  if color.nil?
95
95
  color = '#%06x' % (rand * 0xffffff)
96
96
  end
97
- valid_color = color =~ /[0-9a-fA-F]{6}/
98
- if valid_color.nil?
99
- raise RuntimeError.new "invalid color but in hex format. #FF0000"
100
- end
101
- unless color.start_with?('#')
102
- color = "##{color}"
97
+ unless color =~ /#?([0-9a-fA-F]{6})/
98
+ raise TimelineInputError.new "invalid color, must be in hex format, e.g. #FF0000"
103
99
  end
100
+ color = "##{color}" unless color.start_with?('#')
104
101
  @color = color
105
102
  end
106
103
 
@@ -326,7 +326,8 @@ module OpenC3
326
326
  end
327
327
 
328
328
  range = item.range
329
- if range
329
+ # Don't range check a string default value
330
+ if range and !item.default.is_a?(String)
330
331
  # Perform Range Check on command parameter
331
332
  if not range.include?(range_check_value)
332
333
  range_check_value = "'#{range_check_value}'" if String === range_check_value
@@ -756,7 +756,7 @@ module OpenC3
756
756
  super(item, value, :RAW, buffer)
757
757
  rescue ArgumentError => e
758
758
  if item.states and String === value and e.message =~ /invalid value for/
759
- raise "Unknown state #{value} for #{item.name}, must be one of #{item.states.keys.join(', ')}"
759
+ raise "Unknown state '#{value}' for #{item.name}, must be one of #{item.states.keys.join(', ')}"
760
760
  else
761
761
  raise e
762
762
  end
@@ -23,12 +23,12 @@ module OpenC3
23
23
 
24
24
  private
25
25
 
26
- def list_timelines(scope: $openc3_scope, token: $openc3_token)
26
+ def list_timelines(scope: $openc3_scope)
27
27
  response = $api_server.request('get', "/openc3-api/timeline", scope: scope)
28
28
  return _handle_response(response, 'Failed to list timelines')
29
29
  end
30
30
 
31
- def create_timeline(name, color: nil, scope: $openc3_scope, token: $openc3_token)
31
+ def create_timeline(name, color: nil, scope: $openc3_scope)
32
32
  data = {}
33
33
  data['name'] = name
34
34
  data['color'] = color if color
@@ -36,19 +36,19 @@ module OpenC3
36
36
  return _handle_response(response, 'Failed to create timeline')
37
37
  end
38
38
 
39
- def get_timeline(name, scope: $openc3_scope, token: $openc3_token)
39
+ def get_timeline(name, scope: $openc3_scope)
40
40
  response = $api_server.request('get', "/openc3-api/timeline/#{name}", scope: scope)
41
41
  return _handle_response(response, 'Failed to get timeline')
42
42
  end
43
43
 
44
- def set_timeline_color(name, color, scope: $openc3_scope, token: $openc3_token)
44
+ def set_timeline_color(name, color, scope: $openc3_scope)
45
45
  post_data = {}
46
46
  post_data['color'] = color
47
47
  response = $api_server.request('post', "/openc3-api/timeline/#{name}/color", data: post_data, json: true, scope: scope)
48
48
  return _handle_response(response, 'Failed to set timeline color')
49
49
  end
50
50
 
51
- def delete_timeline(name, force: false, scope: $openc3_scope, token: $openc3_token)
51
+ def delete_timeline(name, force: false, scope: $openc3_scope)
52
52
  url = "/openc3-api/timeline/#{name}"
53
53
  if force
54
54
  url += "?force=true"
@@ -57,16 +57,7 @@ module OpenC3
57
57
  return _handle_response(response, 'Failed to delete timeline')
58
58
  end
59
59
 
60
- def get_timeline_activities(name, start: nil, stop: nil, scope: $openc3_scope, token: $openc3_token)
61
- url = "/openc3-api/timeline/#{name}/activities"
62
- if start and stop
63
- url += "?start=#{start}&stop=#{stop}"
64
- end
65
- response = $api_server.request('get', url, scope: scope)
66
- return _handle_response(response, 'Failed to get timeline activities')
67
- end
68
-
69
- def create_timeline_activity(name, kind:, start:, stop:, data: {}, scope: $openc3_scope, token: $openc3_token)
60
+ def create_timeline_activity(name, kind:, start:, stop:, data: {}, scope: $openc3_scope)
70
61
  kind = kind.to_s.downcase()
71
62
  kinds = %w(command script reserve)
72
63
  unless kinds.include?(kind)
@@ -81,12 +72,24 @@ module OpenC3
81
72
  return _handle_response(response, 'Failed to create timeline activity')
82
73
  end
83
74
 
84
- def get_timeline_activity(name, start: nil, scope: $openc3_scope, token: $openc3_token)
85
- response = $api_server.request('get', "/openc3-api/timeline/#{name}/activity/#{start}", scope: scope)
75
+ def get_timeline_activity(name, start, uuid, scope: $openc3_scope)
76
+ response = $api_server.request('get', "/openc3-api/timeline/#{name}/activity/#{start}/#{uuid}", scope: scope)
86
77
  return _handle_response(response, 'Failed to get timeline activity')
87
78
  end
88
79
 
89
- def delete_timeline_activity(name, start, uuid, scope: $openc3_scope, token: $openc3_token)
80
+ def get_timeline_activities(name, start: nil, stop: nil, limit: nil, scope: $openc3_scope)
81
+ url = "/openc3-api/timeline/#{name}/activities"
82
+ if start and stop
83
+ url += "?start=#{start}&stop=#{stop}"
84
+ end
85
+ if limit
86
+ url += "?limit=#{limit}"
87
+ end
88
+ response = $api_server.request('get', url, scope: scope)
89
+ return _handle_response(response, 'Failed to get timeline activities')
90
+ end
91
+
92
+ def delete_timeline_activity(name, start, uuid, scope: $openc3_scope)
90
93
  response = $api_server.request('delete', "/openc3-api/timeline/#{name}/activity/#{start}/#{uuid}", scope: scope)
91
94
  return _handle_response(response, 'Failed to delete timeline activity')
92
95
  end
@@ -20,7 +20,7 @@ module OpenC3
20
20
  module Script
21
21
  private
22
22
 
23
- def plugin_list(scope: $openc3_scope)
23
+ def plugin_list(default: false, scope: $openc3_scope)
24
24
  response_body = nil
25
25
  begin
26
26
  endpoint = "/openc3-api/plugins?scope=#{scope}"
@@ -35,7 +35,15 @@ module OpenC3
35
35
  http.request(request) do |response|
36
36
  response_body = response.body
37
37
  response.value() # Raises an HTTP error if the response is not 2xx (success)
38
- return JSON.parse(response.body, allow_nan: true, create_additions: true)
38
+ plugins = JSON.parse(response.body, allow_nan: true, create_additions: true)
39
+ if default
40
+ return plugins
41
+ else
42
+ return plugins.select do |plugin|
43
+ !plugin.include?('openc3-cosmos-tool-') and !plugin.include?('openc3-tool-base') and
44
+ !plugin.include?('openc3-cosmos-enterprise-tool-') and !plugin.include?('openc3-enterprise-tool-base')
45
+ end
46
+ end
39
47
  end
40
48
  end
41
49
  rescue => e
@@ -25,18 +25,23 @@ require 'openc3/api/api'
25
25
  require 'openc3/io/json_drb_object'
26
26
  require 'openc3/script/api_shared'
27
27
  require 'openc3/script/calendar'
28
- require 'openc3/script/metadata'
29
28
  require 'openc3/script/commands'
30
- require 'openc3/script/telemetry'
31
- require 'openc3/script/limits'
29
+ require 'openc3/script/critical_cmd'
32
30
  require 'openc3/script/exceptions'
31
+ # openc3/script/extract is just helper methods
32
+ require 'openc3/script/limits'
33
+ require 'openc3/script/metadata'
34
+ require 'openc3/script/packages'
35
+ require 'openc3/script/plugins'
33
36
  require 'openc3/script/screen'
34
37
  require 'openc3/script/script_runner'
35
38
  require 'openc3/script/storage'
39
+ # openc3/script/suite_results and suite_runner are used by
40
+ # running_script.rb and the script_runner_api
41
+ # openc3/script/suite is used by end user SR Suites
42
+ require 'openc3/script/tables'
43
+ require 'openc3/script/telemetry'
36
44
  require 'openc3/script/web_socket_api'
37
- require 'openc3/script/packages'
38
- require 'openc3/script/plugins'
39
- require 'openc3/script/critical_cmd'
40
45
  require 'openc3/utilities/authentication'
41
46
 
42
47
  $api_server = nil
@@ -34,22 +34,29 @@ module OpenC3
34
34
  if response.nil? || response.status != 200
35
35
  _script_response_error(response, "Script list request failed", scope: scope)
36
36
  else
37
- return JSON.parse(response.body, :allow_nan => true, :create_additions => true)
37
+ scripts = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
38
+ # Remove the '*' from the script names
39
+ return scripts.each { |script| script.gsub!(/\*$/, '') }
38
40
  end
39
41
  end
40
42
 
41
43
  def script_syntax_check(script, scope: $openc3_scope)
42
- endpoint = "/script-api/scripts/syntax"
43
- response = $script_runner_api_server.request('post', endpoint, json: true, data: script, scope: scope)
44
+ endpoint = "/script-api/scripts/temp.rb/syntax"
45
+ # Explicitly set the headers to plain/text so the request.body is set correctly
46
+ headers = {
47
+ 'Content-Type': 'plain/text',
48
+ }
49
+ response = $script_runner_api_server.request('post', endpoint, headers: headers, data: script, scope: scope)
44
50
  if response.nil? || response.status != 200
45
51
  _script_response_error(response, "Script syntax check request failed", scope: scope)
46
52
  else
47
53
  result = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
48
54
  if result['title'] == "Syntax Check Successful"
49
- return true
55
+ result['success'] = true
50
56
  else
51
- raise result.inspect
57
+ result['success'] = false
52
58
  end
59
+ return result
53
60
  end
54
61
  end
55
62
 
@@ -59,8 +66,8 @@ module OpenC3
59
66
  if response.nil? || response.status != 200
60
67
  _script_response_error(response, "Failed to get #{filename}", scope: scope)
61
68
  else
62
- script = response.body
63
- return script
69
+ result = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
70
+ return result['contents']
64
71
  end
65
72
  end
66
73
 
@@ -120,9 +127,13 @@ module OpenC3
120
127
  end
121
128
  end
122
129
 
123
- def script_instrumented(filename, script, scope: $openc3_scope)
124
- endpoint = "/script-api/scripts/#{filename}/instrumented"
125
- response = $script_runner_api_server.request('post', endpoint, json: true, data: script, scope: scope)
130
+ def script_instrumented(script, scope: $openc3_scope)
131
+ endpoint = "/script-api/scripts/temp.rb/instrumented"
132
+ # Explicitly set the headers to plain/text so the request.body is set correctly
133
+ headers = {
134
+ 'Content-Type': 'plain/text',
135
+ }
136
+ response = $script_runner_api_server.request('post', endpoint, headers: headers, data: script, scope: scope)
126
137
  if response.nil? || response.status != 200
127
138
  _script_response_error(response, "Script instrumented request failed", scope: scope)
128
139
  else
@@ -178,7 +189,7 @@ module OpenC3
178
189
 
179
190
  def _running_script_action(id, action_name, scope: $openc3_scope)
180
191
  endpoint = "/script-api/running-script/#{id}/#{action_name}"
181
- response = $script_runner_api_server.request('post', endpoint, scope: scope)
192
+ response = $script_runner_api_server.request('post', endpoint, json: true, scope: scope)
182
193
  if response.nil? || response.status != 200
183
194
  _script_response_error(response, "Running script #{action_name} request failed", scope: scope)
184
195
  else
@@ -127,7 +127,9 @@ module OpenC3
127
127
  end
128
128
  end
129
129
 
130
- def get_download_url(path, scope: $openc3_scope)
130
+ # These are helper methods ... should not be used directly
131
+
132
+ def _get_download_url(path, scope: $openc3_scope)
131
133
  targets = "targets_modified" # First try targets_modified
132
134
  response = $api_server.request('get', "/openc3-api/storage/exists/#{scope}/#{targets}/#{path}", query: { bucket: 'OPENC3_CONFIG_BUCKET' }, scope: scope)
133
135
  if response.status != 200
@@ -143,8 +145,6 @@ module OpenC3
143
145
  return result['url']
144
146
  end
145
147
 
146
- # These are helper methods ... should not be used directly
147
-
148
148
  def _get_storage_file(path, scope: $openc3_scope)
149
149
  # Create Tempfile to store data
150
150
  file = Tempfile.new('target', binmode: true)
@@ -0,0 +1,49 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ module OpenC3
20
+ module Script
21
+ private
22
+
23
+ def table_create_binary(definition, scope: $openc3_scope)
24
+ post_data = {}
25
+ post_data['definition'] = definition
26
+ response = $api_server.request('post', '/openc3-api/tables/generate', json: true, data: post_data, scope: scope)
27
+ return _handle_response(response, 'Failed to create binary')
28
+ end
29
+
30
+ def table_create_report(filename, definition, table_name: nil, scope: $openc3_scope)
31
+ post_data = {}
32
+ post_data['binary'] = filename
33
+ post_data['definition'] = definition
34
+ post_data['table_name'] = table_name if table_name
35
+ response = $api_server.request('post', '/openc3-api/tables/report', json: true, data: post_data, scope: scope)
36
+ return _handle_response(response, 'Failed to create report')
37
+ end
38
+
39
+ # Helper method to handle the response
40
+ def _handle_response(response, error_message)
41
+ return nil if response.nil?
42
+ if response.status >= 400
43
+ result = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
44
+ raise "#{error_message} due to #{result['message']}"
45
+ end
46
+ return JSON.parse(response.body, :allow_nan => true, :create_additions => true)
47
+ end
48
+ end
49
+ end
@@ -182,7 +182,7 @@ module OpenC3
182
182
  def generate_url
183
183
  schema = ENV['OPENC3_API_SCHEMA'] || 'http'
184
184
  hostname = ENV['OPENC3_API_HOSTNAME'] || (ENV['OPENC3_DEVEL'] ? '127.0.0.1' : 'openc3-cosmos-cmd-tlm-api')
185
- port = ENV['OPENC3_API_PORT'] || '2901'
185
+ port = ENV['OPENC3_API_CABLE_PORT'] || ENV['OPENC3_API_PORT'] || '3901'
186
186
  port = port.to_i
187
187
  return "#{schema}://#{hostname}:#{port}/openc3-api/cable"
188
188
  end
@@ -198,7 +198,7 @@ module OpenC3
198
198
  def generate_url
199
199
  schema = ENV['OPENC3_SCRIPT_API_SCHEMA'] || 'http'
200
200
  hostname = ENV['OPENC3_SCRIPT_API_HOSTNAME'] || (ENV['OPENC3_DEVEL'] ? '127.0.0.1' : 'openc3-cosmos-script-runner-api')
201
- port = ENV['OPENC3_SCRIPT_API_PORT'] || '2902'
201
+ port = ENV['OPENC3_SCRIPT_API_CABLE_PORT'] || ENV['OPENC3_SCRIPT_API_PORT'] || '3902'
202
202
  port = port.to_i
203
203
  return "#{schema}://#{hostname}:#{port}/script-api/cable"
204
204
  end
@@ -380,6 +380,7 @@ module OpenC3
380
380
 
381
381
  def self.put_target_file(path, io_or_string, scope:)
382
382
  full_folder_path = "#{OPENC3_LOCAL_MODE_PATH}/#{path}"
383
+ return unless File.expand_path(full_folder_path).start_with?(OPENC3_LOCAL_MODE_PATH)
383
384
  FileUtils.mkdir_p(File.dirname(full_folder_path))
384
385
  File.open(full_folder_path, 'wb') do |file|
385
386
  if String === io_or_string
@@ -393,7 +394,10 @@ module OpenC3
393
394
 
394
395
  def self.open_local_file(path, scope:)
395
396
  full_path = "#{OPENC3_LOCAL_MODE_PATH}/#{scope}/targets_modified/#{path}"
396
- return File.open(full_path, 'rb')
397
+ if File.expand_path(full_path).start_with?(OPENC3_LOCAL_MODE_PATH)
398
+ return File.open(full_path, 'rb')
399
+ end
400
+ nil
397
401
  rescue Errno::ENOENT
398
402
  nil
399
403
  end
@@ -446,6 +450,7 @@ module OpenC3
446
450
  def self.save_tool_config(scope, tool, name, data)
447
451
  json = JSON.parse(data, :allow_nan => true, :create_additions => true)
448
452
  config_path = "#{OPENC3_LOCAL_MODE_PATH}/#{scope}/tool_config/#{tool}/#{name}.json"
453
+ return unless File.expand_path(config_path).start_with?(OPENC3_LOCAL_MODE_PATH)
449
454
  FileUtils.mkdir_p(File.dirname(config_path))
450
455
  File.open(config_path, 'w') do |file|
451
456
  file.write(JSON.pretty_generate(json, :allow_nan => true))
@@ -453,7 +458,9 @@ module OpenC3
453
458
  end
454
459
 
455
460
  def self.delete_tool_config(scope, tool, name)
456
- FileUtils.rm_f("#{OPENC3_LOCAL_MODE_PATH}/#{scope}/tool_config/#{tool}/#{name}.json")
461
+ config_path = "#{OPENC3_LOCAL_MODE_PATH}/#{scope}/tool_config/#{tool}/#{name}.json"
462
+ return unless File.expand_path(config_path).start_with?(OPENC3_LOCAL_MODE_PATH)
463
+ FileUtils.rm_f(config_path)
457
464
  end
458
465
 
459
466
  def self.sync_settings()
@@ -471,6 +478,7 @@ module OpenC3
471
478
 
472
479
  def self.save_setting(scope, name, data)
473
480
  config_path = "#{OPENC3_LOCAL_MODE_PATH}/#{scope}/settings/#{name}.json"
481
+ return unless File.expand_path(config_path).start_with?(OPENC3_LOCAL_MODE_PATH)
474
482
  FileUtils.mkdir_p(File.dirname(config_path))
475
483
  # Anything can be stored as a setting so write it out directly
476
484
  File.write(config_path, data)
@@ -480,12 +488,14 @@ module OpenC3
480
488
 
481
489
  def self.sync_remote_to_local(bucket, key)
482
490
  local_path = "#{OPENC3_LOCAL_MODE_PATH}/#{key}"
491
+ return unless File.expand_path(local_path).start_with?(OPENC3_LOCAL_MODE_PATH)
483
492
  FileUtils.mkdir_p(File.dirname(local_path))
484
493
  bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, path: local_path)
485
494
  end
486
495
 
487
496
  def self.sync_local_to_remote(bucket, key)
488
497
  local_path = "#{OPENC3_LOCAL_MODE_PATH}/#{key}"
498
+ return unless File.expand_path(local_path).start_with?(OPENC3_LOCAL_MODE_PATH)
489
499
  File.open(local_path, 'rb') do |read_file|
490
500
  bucket.put_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: key, body: read_file)
491
501
  end
@@ -493,6 +503,7 @@ module OpenC3
493
503
 
494
504
  def self.delete_local(key)
495
505
  local_path = "#{OPENC3_LOCAL_MODE_PATH}/#{key}"
506
+ return unless File.expand_path(local_path).start_with?(OPENC3_LOCAL_MODE_PATH)
496
507
  File.delete(local_path) if File.exist?(local_path)
497
508
  nil
498
509
  end
@@ -94,7 +94,13 @@ module OpenC3
94
94
  # First try opening a potentially modified version by looking for the modified target
95
95
  if ENV['OPENC3_LOCAL_MODE']
96
96
  local_file = OpenC3::LocalMode.open_local_file(name, scope: scope)
97
- return local_file.read if local_file
97
+ if local_file
98
+ if File.extname(name) == ".bin"
99
+ return local_file.read
100
+ else
101
+ return local_file.read.force_encoding('UTF-8')
102
+ end
103
+ end
98
104
  end
99
105
 
100
106
  bucket = Bucket.getClient()
@@ -106,8 +112,10 @@ module OpenC3
106
112
  if resp && resp.body
107
113
  if File.extname(name) == ".bin"
108
114
  resp.body.binmode
115
+ return resp.body.read
116
+ else
117
+ return resp.body.read.force_encoding('UTF-8')
109
118
  end
110
- resp.body.read
111
119
  else
112
120
  nil
113
121
  end
@@ -1,14 +1,14 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- OPENC3_VERSION = '6.0.2'
3
+ OPENC3_VERSION = '6.1.0'
4
4
  module OpenC3
5
5
  module Version
6
6
  MAJOR = '6'
7
- MINOR = '0'
8
- PATCH = '2'
7
+ MINOR = '1'
8
+ PATCH = '0'
9
9
  OTHER = ''
10
- BUILD = '30bcab7b66f6a10baacea338b83fc5bfb89e68d6'
10
+ BUILD = '4db8ce5a1e1178bf7272dab85bd71299e5d6c3b7'
11
11
  end
12
- VERSION = '6.0.2'
13
- GEM_VERSION = '6.0.2'
12
+ VERSION = '6.1.0'
13
+ GEM_VERSION = '6.1.0'
14
14
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= tool_name %>",
3
- "version": "6.0.2",
3
+ "version": "6.1.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": "6.0.2",
26
+ "@openc3/js-common": "6.1.0",
27
27
  "rxjs": "~7.8.0",
28
28
  "single-spa": "^5.9.5",
29
29
  "single-spa-angular": "^9.2.0",
@@ -16,7 +16,7 @@
16
16
  "@emotion/react": "^11.13.3",
17
17
  "@emotion/styled": "^11.11.0",
18
18
  "@mui/material": "^6.1.1",
19
- "@openc3/js-common": "6.0.2",
19
+ "@openc3/js-common": "6.1.0",
20
20
  "react": "^18.2.0",
21
21
  "react-dom": "^18.2.0",
22
22
  "single-spa-react": "^5.1.4"
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@astrouxds/astro-web-components": "^7.24.0",
15
- "@openc3/js-common": "6.0.2",
15
+ "@openc3/js-common": "6.1.0",
16
16
  "@smui/button": "^7.0.0",
17
17
  "@smui/common": "^7.0.0",
18
18
  "@smui/card": "^7.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= tool_name %>",
3
- "version": "6.0.2",
3
+ "version": "6.1.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": "6.0.2",
15
- "@openc3/vue-common": "6.0.2",
14
+ "@openc3/js-common": "6.1.0",
15
+ "@openc3/vue-common": "6.1.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": "6.0.2",
3
+ "version": "6.1.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": "6.0.2",
11
+ "@openc3/vue-common": "6.1.0",
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: 6.0.2
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Melton
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-01-09 00:00:00.000000000 Z
12
+ date: 2025-02-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -791,6 +791,7 @@ files:
791
791
  - bin/cstol_converter
792
792
  - bin/openc3cli
793
793
  - bin/pipinstall
794
+ - bin/pipuninstall
794
795
  - bin/rubysloc
795
796
  - data/config/_array_params.yaml
796
797
  - data/config/_canvas_values.yaml
@@ -993,6 +994,7 @@ files:
993
994
  - lib/openc3/migrations/20231022000000_tlm_viewer_config.rb
994
995
  - lib/openc3/migrations/20241208080000_no_critical_cmd.rb
995
996
  - lib/openc3/migrations/20241208080001_no_trigger_group.rb
997
+ - lib/openc3/migrations/20250108060000_news_feed.rb
996
998
  - lib/openc3/models/activity_model.rb
997
999
  - lib/openc3/models/auth_model.rb
998
1000
  - lib/openc3/models/cvt_model.rb
@@ -1007,6 +1009,7 @@ files:
1007
1009
  - lib/openc3/models/microservice_status_model.rb
1008
1010
  - lib/openc3/models/migration_model.rb
1009
1011
  - lib/openc3/models/model.rb
1012
+ - lib/openc3/models/news_model.rb
1010
1013
  - lib/openc3/models/note_model.rb
1011
1014
  - lib/openc3/models/offline_access_model.rb
1012
1015
  - lib/openc3/models/ping_model.rb
@@ -1075,6 +1078,7 @@ files:
1075
1078
  - lib/openc3/script/suite.rb
1076
1079
  - lib/openc3/script/suite_results.rb
1077
1080
  - lib/openc3/script/suite_runner.rb
1081
+ - lib/openc3/script/tables.rb
1078
1082
  - lib/openc3/script/telemetry.rb
1079
1083
  - lib/openc3/script/web_socket_api.rb
1080
1084
  - lib/openc3/streams/mqtt_stream.rb