openc3 7.4.1 → 7.5.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/bin/pipinstall +18 -5
  3. data/bin/pipuninstall +1 -1
  4. data/bin/uvinstall +36 -8
  5. data/data/config/default_plugins.yaml +43 -0
  6. data/data/config/interface_modifiers.yaml +22 -7
  7. data/data/config/item_modifiers.yaml +14 -4
  8. data/data/config/plugins.yaml +1 -1
  9. data/data/config/widgets.yaml +1 -1
  10. data/lib/openc3/api/cmd_api.rb +10 -13
  11. data/lib/openc3/api/interface_api.rb +1 -2
  12. data/lib/openc3/api/limits_api.rb +1 -1
  13. data/lib/openc3/api/metrics_api.rb +26 -1
  14. data/lib/openc3/api/router_api.rb +1 -2
  15. data/lib/openc3/api/tlm_api.rb +6 -10
  16. data/lib/openc3/config/config_parser.rb +3 -1
  17. data/lib/openc3/core_ext/tempfile.rb +2 -0
  18. data/lib/openc3/interfaces/file_interface.rb +6 -1
  19. data/lib/openc3/interfaces/http_client_interface.rb +14 -6
  20. data/lib/openc3/interfaces/interface.rb +27 -3
  21. data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
  22. data/lib/openc3/interfaces/stream_interface.rb +22 -3
  23. data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
  24. data/lib/openc3/interfaces/udp_interface.rb +16 -2
  25. data/lib/openc3/interfaces.rb +1 -0
  26. data/lib/openc3/logs/log_writer.rb +13 -4
  27. data/lib/openc3/microservices/decom_microservice.rb +7 -4
  28. data/lib/openc3/microservices/interface_microservice.rb +146 -50
  29. data/lib/openc3/microservices/log_microservice.rb +1 -3
  30. data/lib/openc3/microservices/microservice.rb +25 -1
  31. data/lib/openc3/microservices/plugin_microservice.rb +32 -0
  32. data/lib/openc3/microservices/text_log_microservice.rb +1 -3
  33. data/lib/openc3/models/interface_model.rb +1 -1
  34. data/lib/openc3/models/interface_status_model.rb +6 -0
  35. data/lib/openc3/models/metric_model.rb +52 -4
  36. data/lib/openc3/models/microservice_model.rb +7 -3
  37. data/lib/openc3/models/plugin_model.rb +12 -12
  38. data/lib/openc3/models/python_package_model.rb +3 -13
  39. data/lib/openc3/models/target_model.rb +83 -9
  40. data/lib/openc3/operators/microservice_operator.rb +5 -0
  41. data/lib/openc3/operators/operator.rb +69 -0
  42. data/lib/openc3/packets/packet.rb +10 -4
  43. data/lib/openc3/packets/packet_config.rb +12 -1
  44. data/lib/openc3/packets/packet_item.rb +27 -14
  45. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  46. data/lib/openc3/packets/structure_item.rb +3 -2
  47. data/lib/openc3/script/api_shared.rb +58 -84
  48. data/lib/openc3/script/extract.rb +215 -19
  49. data/lib/openc3/script/plugins.rb +4 -11
  50. data/lib/openc3/script/storage.rb +81 -10
  51. data/lib/openc3/system/system.rb +9 -6
  52. data/lib/openc3/topics/command_decom_topic.rb +1 -1
  53. data/lib/openc3/topics/command_topic.rb +2 -1
  54. data/lib/openc3/topics/router_topic.rb +2 -1
  55. data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
  56. data/lib/openc3/topics/telemetry_topic.rb +3 -2
  57. data/lib/openc3/topics/topic.rb +154 -2
  58. data/lib/openc3/utilities/cli_generator.rb +67 -15
  59. data/lib/openc3/utilities/config_overlay.rb +35 -17
  60. data/lib/openc3/utilities/default_plugins.rb +33 -0
  61. data/lib/openc3/utilities/local_mode.rb +82 -46
  62. data/lib/openc3/utilities/metric.rb +16 -0
  63. data/lib/openc3/utilities/process_stats.rb +160 -0
  64. data/lib/openc3/utilities/pypi_url.rb +65 -0
  65. data/lib/openc3/utilities/questdb_client.rb +14 -0
  66. data/lib/openc3/utilities/running_script.rb +22 -18
  67. data/lib/openc3/utilities/store_autoload.rb +20 -3
  68. data/lib/openc3/utilities/store_queued.rb +37 -9
  69. data/lib/openc3/utilities/target_file.rb +57 -12
  70. data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
  71. data/lib/openc3/version.rb +6 -6
  72. data/templates/conversion/conversion.py +6 -2
  73. data/templates/conversion/conversion.rb +5 -0
  74. data/templates/plugin/.github/workflows/playwright.yml +62 -0
  75. data/templates/plugin/.github/workflows/release.yml +45 -0
  76. data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
  77. data/templates/plugin/.gitignore +6 -0
  78. data/templates/plugin/README.md +114 -2
  79. data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
  80. data/templates/plugin/plugin.gemspec +10 -1
  81. data/templates/plugin/pyproject.toml +119 -0
  82. data/templates/tool_angular/package.json +2 -2
  83. data/templates/tool_angular/pnpm-workspace.yaml +16 -0
  84. data/templates/tool_react/package.json +1 -1
  85. data/templates/tool_react/pnpm-workspace.yaml +16 -0
  86. data/templates/tool_svelte/package.json +2 -1
  87. data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
  88. data/templates/tool_vue/package.json +3 -3
  89. data/templates/tool_vue/pnpm-workspace.yaml +16 -0
  90. data/templates/widget/package.json +2 -2
  91. data/templates/widget/pnpm-workspace.yaml +16 -0
  92. metadata +16 -1
@@ -0,0 +1,331 @@
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 'timeout' # For Timeout::Error
15
+ require 'openc3/interfaces/interface'
16
+ require 'openc3/core_ext/exception'
17
+ require 'openc3/core_ext/time'
18
+ require 'openc3/utilities/logger'
19
+
20
+ module OpenC3
21
+ # Interface base class which reads an underlying socket / stream (the reader)
22
+ # in a dedicated thread and buffers the results on a queue. This keeps the
23
+ # operating system receive buffers drained as fast as possible so data isn't
24
+ # dropped while the packet reading thread is busy processing protocols and
25
+ # writing to the database.
26
+ #
27
+ # Subclasses call start_read_queue_thread with the object they read from
28
+ # (anything with a read method) once it is connected and stop_read_queue_thread
29
+ # once it is disconnected. While the thread is running the reader's read
30
+ # method is redirected to the queue, so the subclass (and anything else such
31
+ # as derived classes or protocols) keeps calling reader.read exactly as before
32
+ # and receives the data in the order it was read rather than competing with
33
+ # the read thread for it.
34
+ #
35
+ # Setting READ_QUEUE_MAX_SIZE to 0 disables the read thread so the reader is
36
+ # read inline, which is how interfaces read before the queue existed.
37
+ class ReadQueueInterface < Interface
38
+ # Maximum number of bytes buffered on the queue before the read thread
39
+ # blocks and lets the operating system do the buffering instead
40
+ DEFAULT_READ_QUEUE_MAX_SIZE = 20 * 1024 * 1024 # 20MB
41
+
42
+ # Approximate memory each queued read costs on top of the data itself (the
43
+ # String object plus the queue entry). Small reads are dominated by this so
44
+ # it is charged against the budget, otherwise a stream delivering a few
45
+ # bytes at a time would hold many times the budget in actual memory.
46
+ READ_QUEUE_ENTRY_OVERHEAD = 64
47
+
48
+ # How long to wait before re-checking whether the interface has been
49
+ # disconnected while blocked waiting for room on the queue
50
+ QUEUE_POLL_TIMEOUT = 0.1
51
+
52
+ # How long to wait for the read thread to exit when disconnecting
53
+ THREAD_JOIN_TIMEOUT = 2
54
+
55
+ # @return [Integer] Maximum number of bytes buffered on the queue
56
+ attr_accessor :read_queue_max_size
57
+
58
+ # @return [Time, nil] When the read thread read the data most recently
59
+ # returned by the reader (nil when the read queue is disabled)
60
+ attr_reader :read_queue_data_time
61
+
62
+ def initialize
63
+ super()
64
+ initialize_read_queue()
65
+ end
66
+
67
+ # Initialize (or reset) the read queue attributes. Called by initialize.
68
+ def initialize_read_queue(max_size = DEFAULT_READ_QUEUE_MAX_SIZE)
69
+ @raw_read_queue = nil
70
+ @raw_read_thread = nil
71
+ @raw_reader = nil
72
+ @raw_reader_read = nil
73
+ @raw_read_bytes = 0
74
+ @raw_read_budget = 0
75
+ @raw_read_timeout_queued = false
76
+ @read_queue_data_time = nil
77
+ # Guards @raw_read_queue / @raw_read_bytes / @raw_read_budget and wakes the
78
+ # read thread when the bytes it queued are consumed and there is room to
79
+ # queue more. Each read thread is cancelled by closing its own queue under
80
+ # this mutex, so a thread which outlives stop_read_queue_thread (and a new
81
+ # thread starting) keeps seeing its old queue closed and can never touch
82
+ # the byte counters or read again.
83
+ @raw_read_mutex = Mutex.new
84
+ @raw_read_condition = ConditionVariable.new
85
+ @read_queue_max_size = max_size
86
+ end
87
+
88
+ # Supported Options
89
+ # READ_QUEUE_MAX_SIZE - Maximum number of bytes buffered on the read queue
90
+ # or 0 to disable the read thread and read inline (see Interface#set_option)
91
+ def set_option(option_name, option_values)
92
+ super(option_name, option_values)
93
+ if option_name.upcase == 'READ_QUEUE_MAX_SIZE'
94
+ max_size = Integer(option_values[0])
95
+ raise "READ_QUEUE_MAX_SIZE must be 0 (disabled) or a positive integer but was #{max_size}" if max_size < 0
96
+
97
+ @read_queue_max_size = max_size
98
+ end
99
+ end
100
+
101
+ # @return [Boolean] Whether reads are buffered by a read thread (false when
102
+ # READ_QUEUE_MAX_SIZE is 0 and reads happen inline)
103
+ def read_queue_enabled?
104
+ @read_queue_max_size > 0
105
+ end
106
+
107
+ # @return [Boolean] Whether a read thread has been started (and not stopped)
108
+ def read_queue_started?
109
+ !@raw_read_queue.nil?
110
+ end
111
+
112
+ # @return [Integer] The number of reads waiting on the read queue
113
+ def read_queue_size
114
+ queue = @raw_read_queue
115
+ queue ? queue.size : 0
116
+ end
117
+
118
+ # @return [Integer] The number of bytes waiting on the read queue
119
+ def read_queue_bytes
120
+ return 0 unless @raw_read_queue
121
+
122
+ @raw_read_mutex.synchronize { @raw_read_bytes }
123
+ end
124
+
125
+ # Start the thread which continuously reads and queues data from the reader
126
+ # and redirect the reader's read method to the queue. Does nothing beyond
127
+ # stopping any existing thread if the read queue is disabled.
128
+ #
129
+ # @param reader [#read] Object to read from, e.g. a Stream or socket
130
+ # @param read_args [Array] Arguments the read thread passes to reader.read.
131
+ # Arguments passed to the redirected read are ignored.
132
+ def start_read_queue_thread(reader, *read_args)
133
+ stop_read_queue_thread()
134
+ return unless read_queue_enabled?
135
+
136
+ queue = Queue.new
137
+ @raw_read_mutex.synchronize do
138
+ @raw_read_bytes = 0
139
+ @raw_read_budget = 0
140
+ @raw_read_timeout_queued = false
141
+ @raw_read_queue = queue
142
+ end
143
+ begin
144
+ raw_read = redirect_reader(reader)
145
+ rescue FrozenError, TypeError
146
+ Logger.warn "#{@name}: Can't redirect #{reader.class}#read so disabling the read queue"
147
+ @raw_read_mutex.synchronize { @raw_read_queue = nil }
148
+ @read_queue_max_size = 0
149
+ return
150
+ end
151
+ @raw_read_thread = Thread.new do
152
+ read_queue_thread_body(queue, raw_read, read_args)
153
+ rescue Exception => e
154
+ Logger.error "#{@name}: Read queue thread unexpectedly died: #{e.formatted}"
155
+ end
156
+ end
157
+
158
+ # Stop the read thread, restore the reader's read method and discard anything
159
+ # left on the queue. The reader should be disconnected first so the thread
160
+ # isn't blocked reading.
161
+ def stop_read_queue_thread
162
+ # Allow subclasses to set their reader before initialize_read_queue
163
+ return unless @raw_read_mutex
164
+
165
+ @raw_read_mutex.synchronize do
166
+ # Closing the queue under the mutex cancels the read thread (it either
167
+ # already charged its bytes, which are reset below, or never will) and
168
+ # unblocks read_queue_pop
169
+ @raw_read_queue.close if @raw_read_queue
170
+ @raw_read_queue = nil
171
+ @raw_read_bytes = 0
172
+ @raw_read_budget = 0
173
+ @raw_read_timeout_queued = false
174
+ @read_queue_data_time = nil
175
+ # Unblock the read thread if it is waiting for room on the queue
176
+ @raw_read_condition.broadcast
177
+ end
178
+ restore_reader()
179
+ thread = @raw_read_thread
180
+ @raw_read_thread = nil
181
+ # The read source should already be disconnected which unblocks the read
182
+ if thread and thread != Thread.current and !thread.join(THREAD_JOIN_TIMEOUT)
183
+ OpenC3.kill_thread(nil, thread)
184
+ end
185
+ end
186
+
187
+ # Retrieve the next data off the read queue, blocking until it is available.
188
+ # This is what the reader's read method returns while it is redirected.
189
+ #
190
+ # @return [String, nil] Data read or nil if the read source is done
191
+ def read_queue_pop
192
+ queue = @raw_read_queue
193
+ return nil unless queue
194
+
195
+ loop do
196
+ data = queue.pop
197
+ if data.kind_of?(Array)
198
+ data, time = data
199
+ @raw_read_mutex.synchronize do
200
+ # Counters were reset if the queue was stopped / replaced
201
+ if queue.equal?(@raw_read_queue)
202
+ @raw_read_bytes -= data.length
203
+ @raw_read_budget -= data.length + READ_QUEUE_ENTRY_OVERHEAD
204
+ @read_queue_data_time = time
205
+ # Tell the read thread there is room for more data
206
+ @raw_read_condition.broadcast
207
+ end
208
+ end
209
+ return data
210
+ end
211
+
212
+ if data.kind_of?(Timeout::Error)
213
+ @raw_read_mutex.synchronize { @raw_read_timeout_queued = false if queue.equal?(@raw_read_queue) }
214
+ # Data queued behind the timeout arrived while nobody was waiting on
215
+ # it, so an inline read would have returned it rather than timing out
216
+ next unless queue.empty?
217
+ end
218
+ # Exceptions raised by the read thread are re-raised here so they are
219
+ # handled exactly as they were when the read happened inline
220
+ raise data if data.kind_of?(Exception)
221
+
222
+ return data
223
+ end
224
+ end
225
+
226
+ # @param data [String, nil] Data returned by reader.read
227
+ # @return [Boolean] Whether the data indicates the reader is done (closed) so
228
+ # the read thread should stop. Subclasses can override this.
229
+ def read_queue_done?(data)
230
+ data.nil? or data.length <= 0
231
+ end
232
+
233
+ # protected
234
+
235
+ def read_queue_thread_body(queue, raw_read, read_args)
236
+ until queue.closed?
237
+ begin
238
+ data = raw_read.call(*read_args)
239
+ data_time = Time.now.sys
240
+ rescue Timeout::Error => e
241
+ # A read timeout doesn't end the reader so pass it along and keep
242
+ # reading. Only one is queued at a time so a reader which times out
243
+ # quickly can't fill the queue with them.
244
+ queue_timeout = @raw_read_mutex.synchronize do
245
+ next false if queue.closed? or @raw_read_timeout_queued
246
+
247
+ @raw_read_timeout_queued = true
248
+ end
249
+ queue.push(e) if queue_timeout
250
+ next
251
+ rescue Exception => e
252
+ queue.push(e)
253
+ break
254
+ end
255
+ # Tell whoever reads next that the reader is done and stop reading
256
+ if read_queue_done?(data)
257
+ queue.push(data)
258
+ break
259
+ end
260
+ # Charge the bytes against the budget before the push so
261
+ # read_queue_bytes never goes negative if the data is dequeued
262
+ # before we get back here
263
+ break unless reserve_read_queue_bytes(queue, data.length)
264
+
265
+ # Queue the time of the read so packets are timestamped when the data
266
+ # arrived rather than when it was processed
267
+ queue.push([data, data_time])
268
+ end
269
+ rescue ClosedQueueError
270
+ # Interface disconnected while we were pushing
271
+ ensure
272
+ # Closing tells read_queue_pop there will never be more data so it
273
+ # returns nil rather than blocking forever
274
+ queue.close
275
+ end
276
+
277
+ # Wait until the data fits in the byte budget and charge it against the
278
+ # budget. A read larger than the entire budget is queued on its own rather
279
+ # than never fitting, so the budget can be exceeded by at most one read.
280
+ #
281
+ # @return [Boolean] Whether the bytes were reserved (false if disconnected)
282
+ def reserve_read_queue_bytes(queue, length)
283
+ cost = length + READ_QUEUE_ENTRY_OVERHEAD
284
+ @raw_read_mutex.synchronize do
285
+ while @raw_read_budget > 0 and (@raw_read_budget + cost) > @read_queue_max_size
286
+ return false if queue.closed?
287
+
288
+ @raw_read_condition.wait(@raw_read_mutex, QUEUE_POLL_TIMEOUT)
289
+ end
290
+ return false if queue.closed?
291
+
292
+ @raw_read_bytes += length
293
+ @raw_read_budget += cost
294
+ end
295
+ return true
296
+ end
297
+
298
+ # Redirect reader.read to the read queue so everything which reads the
299
+ # reader directly gets the queued data in order.
300
+ #
301
+ # @return [Method] The original read method for the read thread to call
302
+ def redirect_reader(reader)
303
+ raw_read = reader.method(:read)
304
+ # Remember a read already defined on the reader itself (e.g. a test stub)
305
+ # so it can be put back rather than removed
306
+ reader_read = reader.singleton_methods(false).include?(:read) ? raw_read : nil
307
+ interface = self
308
+ reader.define_singleton_method(:read) { |*_args| interface.read_queue_pop }
309
+ @raw_reader = reader
310
+ @raw_reader_read = reader_read
311
+ return raw_read
312
+ end
313
+
314
+ # Undo redirect_reader so the reader reads inline again
315
+ def restore_reader
316
+ reader = @raw_reader
317
+ return unless reader
318
+
319
+ if @raw_reader_read
320
+ reader.define_singleton_method(:read, @raw_reader_read)
321
+ else
322
+ reader.singleton_class.send(:remove_method, :read)
323
+ end
324
+ rescue NameError
325
+ # The redirect was already removed
326
+ ensure
327
+ @raw_reader = nil
328
+ @raw_reader_read = nil
329
+ end
330
+ end
331
+ end
@@ -15,12 +15,12 @@
15
15
  # This file may also be used under the terms of a commercial license
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
- require 'openc3/interfaces/interface'
18
+ require 'openc3/interfaces/read_queue_interface'
19
19
 
20
20
  module OpenC3
21
21
  # Base class for interfaces that act read and write from a stream
22
- class StreamInterface < Interface
23
- attr_accessor :stream
22
+ class StreamInterface < ReadQueueInterface
23
+ attr_reader :stream
24
24
 
25
25
  def initialize(protocol_type = nil, protocol_args = [])
26
26
  super()
@@ -34,9 +34,20 @@ module OpenC3
34
34
  end
35
35
  end
36
36
 
37
+ # Replacing the stream stops the read thread which is reading the old one
38
+ def stream=(stream)
39
+ stop_read_queue_thread()
40
+ @stream = stream
41
+ end
42
+
37
43
  def connect
38
44
  super() # Reset the protocols
39
45
  @stream.connect if @stream
46
+ # The read thread continuously drains the stream so the operating system
47
+ # buffers don't fill up while we're busy processing the previous read.
48
+ # @stream.read is redirected to the queue so everything reading the
49
+ # stream (including subclasses and protocols) still sees all the data.
50
+ start_read_queue_thread(@stream) if @stream and read_allowed?
40
51
  end
41
52
 
42
53
  def connected?
@@ -48,11 +59,19 @@ module OpenC3
48
59
  end
49
60
 
50
61
  def disconnect
62
+ # Disconnect the stream first to unblock the read thread
51
63
  @stream.disconnect if @stream
64
+ stop_read_queue_thread()
52
65
  super()
53
66
  end
54
67
 
55
68
  def read_interface
69
+ # The read thread is normally started by connect but interfaces can also
70
+ # be handed an already connected stream so start it on demand
71
+ if @stream and !read_queue_started? and read_queue_enabled? and read_allowed? and connected?
72
+ start_read_queue_thread(@stream)
73
+ end
74
+
56
75
  timeout = false
57
76
  begin
58
77
  data = @stream.read
@@ -257,6 +257,15 @@ module OpenC3
257
257
  @read_queue ? @read_queue.size : 0
258
258
  end
259
259
 
260
+ # @return [Integer] The number of bytes waiting on the read queues of the
261
+ # connected clients. The server itself has no stream so the raw reads are
262
+ # buffered by each client connection.
263
+ def read_queue_bytes
264
+ @connection_mutex.synchronize do
265
+ @read_interface_infos.sum { |rii| rii.interface.read_queue_bytes }
266
+ end
267
+ end
268
+
260
269
  # @return [Integer] The number of packets waiting on the write queue
261
270
  def write_queue_size
262
271
  @write_queue ? @write_queue.size : 0
@@ -400,6 +409,10 @@ module OpenC3
400
409
  end
401
410
 
402
411
  interface = StreamInterface.new
412
+ # Only the read side of the connection needs a read thread
413
+ interface.read_allowed = listen_read
414
+ # Each connection buffers its own reads so give it the configured limit
415
+ interface.read_queue_max_size = @read_queue_max_size
403
416
  interface.target_names = @target_names
404
417
  interface.cmd_target_names = @cmd_target_names
405
418
  interface.tlm_target_names = @tlm_target_names
@@ -15,13 +15,13 @@
15
15
  # This file may also be used under the terms of a commercial license
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
- require 'openc3/interfaces/interface'
18
+ require 'openc3/interfaces/read_queue_interface'
19
19
  require 'openc3/io/udp_sockets'
20
20
  require 'openc3/config/config_parser'
21
21
 
22
22
  module OpenC3
23
23
  # Base class for interfaces that send and receive messages over UDP
24
- class UdpInterface < Interface
24
+ class UdpInterface < ReadQueueInterface
25
25
  HOST_127_0_0_1 = '127.0.0.1'
26
26
  HOST_0_0_0_0 = '0.0.0.0'
27
27
 
@@ -140,6 +140,11 @@ module OpenC3
140
140
  end
141
141
  @thread_sleeper = nil
142
142
  super()
143
+ # The read thread continuously drains the socket so the operating system
144
+ # buffers don't fill up (and drop datagrams) while we're busy processing
145
+ # the previous read. @read_socket.read is redirected to the queue so
146
+ # everything reading the socket (including subclasses) still sees all the data.
147
+ start_read_queue_thread(@read_socket, @read_timeout) if @read_socket
143
148
  end
144
149
 
145
150
  # @return [Boolean] Whether the active ports (read and/or write) have
@@ -163,6 +168,8 @@ module OpenC3
163
168
  OpenC3.close_socket(@read_socket)
164
169
  @write_socket = nil
165
170
  @read_socket = nil
171
+ # The sockets are closed above which unblocks the read thread
172
+ stop_read_queue_thread()
166
173
  @thread_sleeper.cancel if @thread_sleeper
167
174
  @thread_sleeper = nil
168
175
  super()
@@ -177,9 +184,16 @@ module OpenC3
177
184
  return nil
178
185
  end
179
186
 
187
+ # Zero length datagrams are valid so only nil means the socket is done
188
+ def read_queue_done?(data)
189
+ data.nil?
190
+ end
191
+
180
192
  # Reads from the socket if the read_port is defined
181
193
  def read_interface
182
194
  data = @read_socket.read(@read_timeout)
195
+ return nil unless data # Disconnected
196
+
183
197
  Logger.info "#{@name}: Udp read returned 0 bytes (stream closed)" if data.length <= 0
184
198
  extra = nil
185
199
  read_interface_base(data, extra)
@@ -22,6 +22,7 @@ module OpenC3
22
22
  autoload(:Interface, 'openc3/interfaces/interface.rb')
23
23
  autoload(:MqttInterface, 'openc3/interfaces/mqtt_interface.rb')
24
24
  autoload(:MqttStreamInterface, 'openc3/interfaces/mqtt_stream_interface.rb')
25
+ autoload(:ReadQueueInterface, 'openc3/interfaces/read_queue_interface.rb')
25
26
  autoload(:SerialInterface, 'openc3/interfaces/serial_interface.rb')
26
27
  autoload(:SimulatedTargetInterface, 'openc3/interfaces/simulated_target_interface.rb')
27
28
  autoload(:StreamInterface, 'openc3/interfaces/stream_interface.rb')
@@ -63,7 +63,10 @@ module OpenC3
63
63
  CYCLE_TIME_INTERVAL = 10
64
64
 
65
65
  # Delay in seconds before trimming Redis streams
66
- CLEANUP_DELAY = 60
66
+ # This is the primary trimmer of the TELEMETRY__ and COMMAND__ streams. Writers
67
+ # also apply a safety MINID cap (see Topic.stream_safety_minid) which depends on this
68
+ # value, so it is defined in Topic.
69
+ CLEANUP_DELAY = Topic::LOG_CLEANUP_DELAY_SECONDS
67
70
 
68
71
  # Mutex protecting class variables
69
72
  @@mutex = Mutex.new
@@ -77,6 +80,14 @@ module OpenC3
77
80
  # Sleeper used to delay cycle thread
78
81
  @@cycle_sleeper = nil
79
82
 
83
+ # Returns the Redis db_shard holding a target stream such as
84
+ # SCOPE__TELEMETRY__{TARGET}__PACKET, or 0 if no target can be determined
85
+ def self.db_shard_for_topic(redis_topic)
86
+ target_match = redis_topic.match(/\{([^{}]+)\}/)
87
+ return 0 unless target_match
88
+ Store.db_shard_for_target(target_match[1], scope: redis_topic.split('__')[0])
89
+ end
90
+
80
91
  # @param remote_log_directory [String] The path to store the log files
81
92
  # @param logging_enabled [Boolean] Whether to start with logging enabled
82
93
  # @param cycle_time [Integer] The amount of time in seconds before creating
@@ -245,9 +256,7 @@ module OpenC3
245
256
  # Now that the file is in S3, trim the Redis stream up until the previous file.
246
257
  # This keeps one minute of data in Redis
247
258
  instance.cleanup_offsets[index].each do |redis_topic, cleanup_offset|
248
- target_match = redis_topic.match(/__\{?([^}_]+)\}?__/)
249
- db_shard = target_match ? Store.db_shard_for_target(target_match[1]) : 0
250
- Topic.trim_topic(redis_topic, cleanup_offset, db_shard: db_shard)
259
+ Topic.trim_topic(redis_topic, cleanup_offset, db_shard: LogWriter.db_shard_for_topic(redis_topic))
251
260
  end
252
261
  indexes_to_clear << index
253
262
  end
@@ -84,6 +84,11 @@ module OpenC3
84
84
  include InterfaceDecomCommon
85
85
  LIMITS_STATE_INDEX = { RED_LOW: 0, YELLOW_LOW: 1, YELLOW_HIGH: 2, RED_HIGH: 3, GREEN_LOW: 4, GREEN_HIGH: 5 }
86
86
 
87
+ # Decom never uses descriptions so don't keep them in memory
88
+ def load_descriptions?
89
+ false
90
+ end
91
+
87
92
  def initialize(*args)
88
93
  super(*args)
89
94
  # Should only be one target, but there might be multiple decom microservices for a given target
@@ -156,11 +161,9 @@ module OpenC3
156
161
  @limits_response_thread = nil
157
162
  end
158
163
 
159
- def decom_packet(_topic, msg_id, msg_hash, _redis)
164
+ def decom_packet(topic, msg_id, msg_hash, _redis)
160
165
  OpenC3.in_span("decom_packet") do
161
- msgid_seconds_from_epoch = msg_id.split('-')[0].to_i / 1000.0
162
- delta = Time.now.to_f - msgid_seconds_from_epoch
163
- @metric.set(name: 'decom_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and decom start')
166
+ update_topic_lag(topic, msg_id, metric_name: 'decom_topic_delta_seconds', help: 'Delta time between data written to stream and decom start')
164
167
 
165
168
  #######################################
166
169
  # Build packet object from topic data