openc3 7.1.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa824943ce72cce586cdfe03c1a5a8395120801bd845174c25433a15bf986306
4
- data.tar.gz: 620cc66576e14e7e696419e5be0b5704190452d29efc35a315a489099b20f82e
3
+ metadata.gz: 719b55747a7004cc44da1976c3f15eec6c67a3fca702b56792862f59f02cb8b5
4
+ data.tar.gz: c80082e75c059dac71d15a56264cba9c968f82c494e564e33b598c75ac01a6c2
5
5
  SHA512:
6
- metadata.gz: 1f392d51b2e5870128a995aa9e5717f9a4ed3bf0b8f55952b9141ec34898406734a2a834cf18d09b78d6e82f5675c2c0f3269e2ef62964ec58f2b96b8933ce80
7
- data.tar.gz: a92a073ff672da55a44b85b700cb5b6a908208e9a10ddcad77828f02de0f877c3db2252d7d93857ab5b65d55b39594dfc109d72028a82cbfeaa6e468878eef94
6
+ metadata.gz: 5b3c1ca82717f1debd25ab4a6ac0239aa9baee2b5f736f61b7f52103397b608ab3de8fd0d50e10f0a4481b507b2a519987a4c4c68f7d0f3770cd48f3e165b8eb
7
+ data.tar.gz: d2bc96742a2555be7c8149dd728155a76ba7c1a16474eb26a36388bdedaacc1fba9c447ab373beebd8fd58ac81248541531de1a163bc156be195c76e34b515f2
data/bin/openc3cli CHANGED
@@ -444,6 +444,9 @@ def unload_plugin(plugin_name, scope:)
444
444
  plugin_model = OpenC3::PluginModel.get_model(name: plugin_name, scope: scope)
445
445
  plugin_model.destroy
446
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)
447
450
  OpenC3::Logger.info("PluginModel destroyed: #{plugin_name}", scope: scope)
448
451
  rescue => e
449
452
  abort("Error uninstalling plugin: #{scope}: #{plugin_name}: #{e.formatted}")
@@ -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
@@ -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
@@ -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
@@ -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
@@ -94,13 +94,15 @@ module OpenC3
94
94
  @limits_event_topic = "#{@scope}__openc3_limits_events"
95
95
  @topics << @limits_event_topic
96
96
  Topic.update_topic_offsets(@topics, db_shard: @db_shard)
97
+ # Initialize before assigning limits_change_callback - sync_system below
98
+ # can fire the callback synchronously for any persisted-disabled items.
99
+ @limits_response_queue = Queue.new
100
+ @limits_response_thread = nil
97
101
  System.telemetry.limits_change_callback = method(:limits_change_callback)
98
102
  LimitsEventTopic.sync_system(scope: @scope)
99
103
  @error_count = 0
100
104
  @metric.set(name: 'decom_total', value: @count, type: 'counter')
101
105
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
102
- @limits_response_queue = Queue.new
103
- @limits_response_thread = nil
104
106
  end
105
107
 
106
108
  def run
@@ -158,12 +158,12 @@ module OpenC3
158
158
  variables[current_variable_name]['description'] = params[0]
159
159
  when 'VARIABLE_STATE'
160
160
  usage = "#{keyword} <Display Text> <Value>"
161
- parser.verify_num_parameters(2, 2, usage)
161
+ parser.verify_num_parameters(1, 2, usage)
162
162
  unless current_variable_name
163
163
  raise "VARIABLE_STATE must follow a VARIABLE definition"
164
164
  end
165
165
  variables[current_variable_name]['options'] ||= []
166
- option = { 'value' => params[1], 'text' => params[0] }
166
+ option = { 'value' => params[1] || params[0], 'text' => params[0] }
167
167
  variables[current_variable_name]['options'] << option
168
168
  end
169
169
  end
@@ -229,12 +229,13 @@ module OpenC3
229
229
  plugin_model.homepage = pkg.spec.homepage
230
230
  plugin_model.repository = pkg.spec.metadata['source_code_uri'] # this key because it's in the official gemspec examples
231
231
  plugin_model.keywords = pkg.spec.metadata['openc3_store_keywords']&.split(/, ?/)
232
- img_path = pkg.spec.metadata['openc3_store_image']
233
- unless img_path
234
- default_img_path = 'public/store_img.png'
235
- full_default_path = File.join(gem_path, default_img_path)
236
- img_path = default_img_path if File.exist? full_default_path
237
- end
232
+ # Resolve the plugin's store image path. The gemspec generated by
233
+ # `cli generate plugin` sets `openc3_store_image` to `public/store_img.png`
234
+ # but does not create that file, so we must verify existence regardless
235
+ # of whether the path came from metadata or the default — otherwise the
236
+ # frontend hits a 500 when fetching the missing file.
237
+ img_path = pkg.spec.metadata['openc3_store_image'] || 'public/store_img.png'
238
+ img_path = nil unless File.exist?(File.join(gem_path, img_path))
238
239
  package_name = "#{pkg.spec.name}-#{pkg.spec.version}"
239
240
  plugin_model.img_path = File.join('gems', package_name, img_path) if img_path # convert this filesystem path to volumes mount path
240
241
  plugin_model.update() unless validate_only
@@ -525,5 +526,16 @@ module OpenC3
525
526
  end
526
527
  return result.sort
527
528
  end
529
+
530
+ # Remove the backing gem for an unloaded plugin so it disappears from
531
+ # GemModel.names (and the admin Packages tab). Skips the removal if any
532
+ # other PluginModel (any scope, any counter) still references the gem,
533
+ # via the existing PluginModel.gem_names check inside GemModel.destroy.
534
+ def self.cleanup_gem(plugin_name, scope:)
535
+ gem_filename = plugin_name.split("__")[0]
536
+ OpenC3::GemModel.destroy(gem_filename, log_and_raise_needed_errors: false)
537
+ rescue => e
538
+ Logger.warn("Could not remove gem #{gem_filename}: #{e.message}", scope: scope)
539
+ end
528
540
  end
529
541
  end
@@ -44,34 +44,37 @@ module OpenC3
44
44
  model = get_model(name: name, scope: scope)
45
45
  raise QueueError, "Queue '#{name}' not found in scope '#{scope}'" unless model
46
46
 
47
- if model.state != 'DISABLE'
48
- result = Store.zrevrange("#{scope}:#{name}", 0, 0, with_scores: true)
49
- if result.empty?
50
- id = 1.0
51
- else
52
- id = result[0][1].to_f + 1
53
- end
54
-
55
- # Build command data with support for both formats
56
- command_data = { username: username, timestamp: Time.now.to_nsec_from_epoch, validate: validate, timeout: timeout }
57
- if target_name && cmd_name
58
- # New format: store target_name, cmd_name, and cmd_params separately
59
- command_data[:target_name] = target_name
60
- command_data[:cmd_name] = cmd_name
61
- command_data[:cmd_params] = JSON.generate(cmd_params.as_json, allow_nan: true) if cmd_params
62
- elsif command
63
- # Legacy format: store command string for backwards compatibility
64
- command_data[:value] = command
65
- else
66
- raise QueueError, "Must provide either command string or target_name/cmd_name parameters"
67
- end
68
-
69
- Store.zadd("#{scope}:#{name}", id, command_data.to_json)
70
- model.notify(kind: 'command')
71
- else
47
+ if model.state == 'DISABLE'
72
48
  error_msg = command || "#{target_name} #{cmd_name}"
73
49
  raise QueueError, "Queue '#{name}' is disabled. Command '#{error_msg}' not queued."
74
50
  end
51
+
52
+ result = Store.zrevrange("#{scope}:#{name}", 0, 0, with_scores: true)
53
+ id = result.empty? ? 1.0 : result[0][1].to_f + 1
54
+
55
+ command_data = build_command_data(username: username, command: command, target_name: target_name,
56
+ cmd_name: cmd_name, cmd_params: cmd_params, validate: validate, timeout: timeout)
57
+ Store.zadd("#{scope}:#{name}", id, command_data.to_json)
58
+ model.notify(kind: 'command')
59
+ end
60
+
61
+ # Build the hash that gets serialized into Redis. Always uses symbol keys so
62
+ # downstream code in this class can access values consistently. cmd_params is
63
+ # JSON-encoded as a string so binary data survives the round-trip via as_json.
64
+ def self.build_command_data(username:, command: nil, target_name: nil, cmd_name: nil, cmd_params: nil, validate: nil, timeout: nil)
65
+ command_data = { username: username, timestamp: Time.now.to_nsec_from_epoch }
66
+ if target_name && cmd_name
67
+ command_data[:target_name] = target_name
68
+ command_data[:cmd_name] = cmd_name
69
+ command_data[:cmd_params] = JSON.generate(cmd_params.as_json, allow_nan: true) if cmd_params
70
+ elsif command
71
+ command_data[:value] = command
72
+ else
73
+ raise QueueError, "Must provide either command string or target_name/cmd_name parameters"
74
+ end
75
+ command_data[:validate] = validate unless validate.nil?
76
+ command_data[:timeout] = timeout unless timeout.nil?
77
+ command_data
75
78
  end
76
79
 
77
80
  attr_accessor :name, :state
@@ -115,49 +118,36 @@ module OpenC3
115
118
  QueueTopic.write_notification(notification, scope: @scope)
116
119
  end
117
120
 
118
- def insert_command(id, command_data)
121
+ def insert_command(id: nil, username:, command: nil, target_name: nil, cmd_name: nil, cmd_params: nil, validate: nil, timeout: nil)
119
122
  if @state == 'DISABLE'
120
- if command_data['value']
121
- command_name = command_data['value']
122
- else
123
- command_name = "#{command_data['target_name']} #{command_data['cmd_name']}"
124
- end
123
+ command_name = command || "#{target_name} #{cmd_name}"
125
124
  raise QueueError, "Queue '#{@name}' is disabled. Command '#{command_name}' not queued."
126
125
  end
127
126
 
128
127
  unless id
129
128
  result = Store.zrevrange("#{@scope}:#{@name}", 0, 0, with_scores: true)
130
- if result.empty?
131
- id = 1.0
132
- else
133
- id = result[0][1].to_f + 1
134
- end
129
+ id = result.empty? ? 1.0 : result[0][1].to_f + 1
135
130
  end
136
131
 
137
- # Convert cmd_params values to JSON-safe format if present
138
- if command_data['cmd_params']
139
- command_data['cmd_params'] = JSON.generate(command_data['cmd_params'].as_json, allow_nan: true)
140
- end
132
+ command_data = self.class.build_command_data(username: username, command: command, target_name: target_name,
133
+ cmd_name: cmd_name, cmd_params: cmd_params, validate: validate, timeout: timeout)
141
134
  Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
142
135
  notify(kind: 'command')
143
136
  end
144
137
 
145
- def update_command(id:, command:, username:, validate: nil, timeout: nil)
138
+ def update_command(id:, username:, command: nil, target_name: nil, cmd_name: nil, cmd_params: nil, validate: nil, timeout: nil)
146
139
  if @state == 'DISABLE'
147
140
  raise QueueError, "Queue '#{@name}' is disabled. Command at id #{id} not updated."
148
141
  end
149
142
 
150
- # Check if command exists at the given id
151
143
  existing = Store.zrangebyscore("#{@scope}:#{@name}", id, id)
152
144
  if existing.empty?
153
145
  raise QueueError, "No command found at id #{id} in queue '#{@name}'"
154
146
  end
155
147
 
156
- # Remove the existing command and add the new one at the same id
157
148
  Store.zremrangebyscore("#{@scope}:#{@name}", id, id)
158
- command_data = { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }
159
- command_data[:validate] = validate unless validate.nil?
160
- command_data[:timeout] = timeout unless timeout.nil?
149
+ command_data = self.class.build_command_data(username: username, command: command, target_name: target_name,
150
+ cmd_name: cmd_name, cmd_params: cmd_params, validate: validate, timeout: timeout)
161
151
  Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
162
152
  notify(kind: 'command')
163
153
  end
@@ -107,7 +107,7 @@ module OpenC3
107
107
  group:,
108
108
  left:,
109
109
  operator:,
110
- right:,
110
+ right: nil,
111
111
  state: false,
112
112
  enabled: true,
113
113
  dependents: nil,
@@ -709,6 +709,26 @@ module OpenC3
709
709
  end
710
710
  end
711
711
 
712
+ def set_item_range_and_default(item)
713
+ return if item.range
714
+ return if item.data_type == :STRING || item.data_type == :BLOCK
715
+
716
+ if item.data_type == :INT
717
+ item.range = (-(2**(item.bit_size - 1)))..((2**(item.bit_size - 1)) - 1)
718
+ item.default = 0 if item.default.nil?
719
+ elsif item.data_type == :UINT
720
+ item.range = 0..((2**item.bit_size) - 1)
721
+ item.default = 0 if item.default.nil?
722
+ elsif item.data_type == :FLOAT
723
+ if item.bit_size == 32
724
+ item.range = -3.402823e38..3.402823e38
725
+ else
726
+ item.range = -Float::MAX..Float::MAX
727
+ end
728
+ item.default = 0.0 if item.default.nil?
729
+ end
730
+ end
731
+
712
732
  def set_limits(item, type)
713
733
  return unless @current_cmd_or_tlm == PacketConfig::TELEMETRY
714
734
 
@@ -756,7 +776,9 @@ module OpenC3
756
776
  @current_packet.get_item(item.name)
757
777
  rescue
758
778
  # Item hasn't already been added so define it
759
- @current_packet.define(item.clone)
779
+ cloned_item = item.clone
780
+ @current_packet.define(cloned_item)
781
+ set_item_range_and_default(cloned_item) if @current_cmd_or_tlm == PacketConfig::COMMAND
760
782
  count += 1
761
783
  end
762
784
  end
@@ -206,10 +206,12 @@ module OpenC3
206
206
  _file_dialog(title, message, filter)
207
207
  end
208
208
 
209
- def open_bucket_dialog(title, message = "Open Bucket File")
209
+ def open_bucket_dialog(title, message = "Open Bucket File", default_path: nil, filter: nil)
210
210
  answer = ''
211
+ hint = default_path ? " [default: #{default_path}]" : ''
212
+ hint += " filter: #{filter}" if filter
211
213
  while answer.empty?
212
- print "#{title}\n#{message}\n<Type bucket file path (e.g. BUCKET/path/to/file)>:"
214
+ print "#{title}\n#{message}\n<Type bucket file path (e.g. BUCKET/path/to/file)>#{hint}:"
213
215
  answer = gets
214
216
  answer.chomp!
215
217
  end
@@ -33,6 +33,7 @@ module OpenC3
33
33
  received_count: packet.received_count,
34
34
  stored: packet.stored.to_s,
35
35
  buffer: packet.buffer(false) }
36
+ msg_hash[:extra] = JSON.generate(packet.extra.as_json, allow_nan: true) if packet.extra
36
37
  db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
37
38
  EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
38
39
  end
@@ -43,6 +43,7 @@ module OpenC3
43
43
  :received_count => packet.received_count,
44
44
  :json_data => json_data,
45
45
  }
46
+ msg_hash[:extra] = JSON.generate(packet.extra.as_json, allow_nan: true) if packet.extra
46
47
  db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
47
48
  Topic.write_topic("#{scope}__DECOM__{#{packet.target_name}}__#{packet.packet_name}", msg_hash, id, db_shard: db_shard)
48
49
 
@@ -293,6 +293,13 @@ RUBY
293
293
  if File.exist?(microservice_path)
294
294
  abort("Microservice #{microservice_path} already exists!")
295
295
  end
296
+ # plugin.txt is checked separately because the user may have deleted the
297
+ # microservices/NAME directory without cleaning up the plugin.txt entry.
298
+ # A duplicate MICROSERVICE entry causes plugin install to fail with
299
+ # "openc3_microservices:...already exists at create".
300
+ if File.exist?('plugin.txt') && File.read('plugin.txt') =~ /^MICROSERVICE\s+#{Regexp.escape(microservice_name)}\b/
301
+ abort("plugin.txt already declares MICROSERVICE #{microservice_name}. Remove that entry before regenerating.")
302
+ end
296
303
  microservice_filename = "#{microservice_name.downcase}.#{@@language}"
297
304
  microservice_class = microservice_filename.filename_to_class_name
298
305
  microservice_class.inspect # Remove unused variable warning. These are used in binding for generator
@@ -1280,18 +1280,16 @@ class RunningScript
1280
1280
  # Start Output Thread
1281
1281
  @@output_thread = Thread.new { output_thread() } unless @@output_thread
1282
1282
 
1283
- if @script_engine
1284
- if @script_status.start_line_no != 1 or !@script_status.end_line_no.nil?
1285
- if @script_status.end_line_no.nil?
1286
- # Goto line
1287
- start(@script_status.filename, line_no: @script_status.start_line_no, complete: true)
1288
- else
1289
- # Execute selection
1290
- start(@script_status.filename, line_no: @script_status.start_line_no, end_line_no: @script_status.end_line_no, complete: true)
1291
- end
1283
+ if @script_status.start_line_no != 1 or !@script_status.end_line_no.nil?
1284
+ if @script_status.end_line_no.nil?
1285
+ # Run From Line / Goto line
1286
+ start(@script_status.filename, line_no: @script_status.start_line_no, complete: true)
1292
1287
  else
1293
- @script_engine.run_text(text, filename: @script_status.filename)
1288
+ # Execute Selection
1289
+ start(@script_status.filename, line_no: @script_status.start_line_no, end_line_no: @script_status.end_line_no, complete: true)
1294
1290
  end
1291
+ elsif @script_engine
1292
+ @script_engine.run_text(text, filename: @script_status.filename)
1295
1293
  else
1296
1294
  if initial_filename == 'SCRIPTRUNNER'
1297
1295
  # Don't instrument pseudo scripts
@@ -1,14 +1,14 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- OPENC3_VERSION = '7.1.0'
3
+ OPENC3_VERSION = '7.1.1'
4
4
  module OpenC3
5
5
  module Version
6
6
  MAJOR = '7'
7
7
  MINOR = '1'
8
- PATCH = '0'
8
+ PATCH = '1'
9
9
  OTHER = ''
10
- BUILD = '1074049d7a87d4b4d8cdc31e3512ab495b7492bb'
10
+ BUILD = '1c377442111a2b1801fb5c6b49a27d604e2cce55'
11
11
  end
12
- VERSION = '7.1.0'
13
- GEM_VERSION = '7.1.0'
12
+ VERSION = '7.1.1'
13
+ GEM_VERSION = '7.1.1'
14
14
  end
@@ -1,4 +1,13 @@
1
1
  import time
2
+
3
+ # Add any plugin lib directories to the Python search path so this microservice
4
+ # can import helpers from `/lib` folders inside installed plugins (Ruby gets this
5
+ # for free via gem `$LOAD_PATH`; Python does not).
6
+ import glob
7
+ from openc3.top_level import add_to_search_path
8
+ for path in glob.glob("/gems/gems/**/lib"):
9
+ add_to_search_path(path, True)
10
+
2
11
  from openc3.microservices.microservice import Microservice
3
12
  from openc3.utilities.sleeper import Sleeper
4
13
  from openc3.api import *
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= tool_name %>",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "scripts": {
5
5
  "ng": "ng",
6
6
  "start": "ng serve",
@@ -23,7 +23,7 @@
23
23
  "@angular/platform-browser-dynamic": "^18.2.6",
24
24
  "@angular/router": "^18.2.6",
25
25
  "@astrouxds/astro-web-components": "^7.24.0",
26
- "@openc3/js-common": "7.1.0",
26
+ "@openc3/js-common": "7.1.1",
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": "7.1.0",
19
+ "@openc3/js-common": "7.1.1",
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": "7.1.0",
15
+ "@openc3/js-common": "7.1.1",
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": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -11,8 +11,8 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@astrouxds/astro-web-components": "^7.24.0",
14
- "@openc3/js-common": "7.1.0",
15
- "@openc3/vue-common": "7.1.0",
14
+ "@openc3/js-common": "7.1.1",
15
+ "@openc3/vue-common": "7.1.1",
16
16
  "axios": "^1.7.7",
17
17
  "date-fns": "^4.1.0",
18
18
  "lodash": "^4.17.21",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= widget_name %>",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@astrouxds/astro-web-components": "^7.24.0",
11
- "@openc3/vue-common": "7.1.0",
11
+ "@openc3/vue-common": "7.1.1",
12
12
  "vuetify": "^3.7.1"
13
13
  },
14
14
  "devDependencies": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openc3
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Melton
@@ -973,6 +973,7 @@ files:
973
973
  - lib/openc3/api/README.md
974
974
  - lib/openc3/api/api.rb
975
975
  - lib/openc3/api/authorized_api.rb
976
+ - lib/openc3/api/calendar_api.rb
976
977
  - lib/openc3/api/cmd_api.rb
977
978
  - lib/openc3/api/config_api.rb
978
979
  - lib/openc3/api/interface_api.rb