openc3-cosmos-cfdp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +18 -0
  3. data/README.md +181 -0
  4. data/Rakefile +40 -0
  5. data/lib/cfdp.rb +283 -0
  6. data/lib/cfdp_api.rb +204 -0
  7. data/microservices/CFDP/Gemfile +37 -0
  8. data/microservices/CFDP/Rakefile +6 -0
  9. data/microservices/CFDP/app/controllers/application_controller.rb +46 -0
  10. data/microservices/CFDP/app/controllers/cfdp_controller.rb +222 -0
  11. data/microservices/CFDP/app/models/cfdp_checksum.rb +52 -0
  12. data/microservices/CFDP/app/models/cfdp_crc_checksum.rb +41 -0
  13. data/microservices/CFDP/app/models/cfdp_mib.rb +613 -0
  14. data/microservices/CFDP/app/models/cfdp_model.rb +25 -0
  15. data/microservices/CFDP/app/models/cfdp_null_checksum.rb +29 -0
  16. data/microservices/CFDP/app/models/cfdp_pdu.rb +202 -0
  17. data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +590 -0
  18. data/microservices/CFDP/app/models/cfdp_source_transaction.rb +449 -0
  19. data/microservices/CFDP/app/models/cfdp_topic.rb +58 -0
  20. data/microservices/CFDP/app/models/cfdp_transaction.rb +188 -0
  21. data/microservices/CFDP/app/models/cfdp_user.rb +601 -0
  22. data/microservices/CFDP/bin/rails +4 -0
  23. data/microservices/CFDP/bin/rake +4 -0
  24. data/microservices/CFDP/bin/setup +25 -0
  25. data/microservices/CFDP/config/application.rb +55 -0
  26. data/microservices/CFDP/config/boot.rb +4 -0
  27. data/microservices/CFDP/config/credentials.yml.enc +1 -0
  28. data/microservices/CFDP/config/environment.rb +5 -0
  29. data/microservices/CFDP/config/environments/development.rb +53 -0
  30. data/microservices/CFDP/config/environments/production.rb +75 -0
  31. data/microservices/CFDP/config/environments/test.rb +50 -0
  32. data/microservices/CFDP/config/initializers/application_controller_renderer.rb +8 -0
  33. data/microservices/CFDP/config/initializers/backtrace_silencers.rb +7 -0
  34. data/microservices/CFDP/config/initializers/cfdp_initializer.rb +26 -0
  35. data/microservices/CFDP/config/initializers/cors.rb +16 -0
  36. data/microservices/CFDP/config/initializers/filter_parameter_logging.rb +8 -0
  37. data/microservices/CFDP/config/initializers/inflections.rb +16 -0
  38. data/microservices/CFDP/config/initializers/mime_types.rb +4 -0
  39. data/microservices/CFDP/config/initializers/wrap_parameters.rb +9 -0
  40. data/microservices/CFDP/config/locales/en.yml +29 -0
  41. data/microservices/CFDP/config/puma.rb +38 -0
  42. data/microservices/CFDP/config/routes.rb +16 -0
  43. data/microservices/CFDP/config.ru +5 -0
  44. data/microservices/CFDP/init.sh +9 -0
  45. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_ack.rb +82 -0
  46. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_enum.rb +237 -0
  47. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_eof.rb +87 -0
  48. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_file_data.rb +98 -0
  49. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_finished.rb +114 -0
  50. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_keep_alive.rb +65 -0
  51. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_metadata.rb +116 -0
  52. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_nak.rb +91 -0
  53. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_prompt.rb +60 -0
  54. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_tlv.rb +291 -0
  55. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_user_ops.rb +749 -0
  56. data/microservices/CFDP/public/robots.txt +1 -0
  57. data/microservices/CFDP/spec/models/cfdp_pdu_ack_spec.rb +114 -0
  58. data/microservices/CFDP/spec/models/cfdp_pdu_eof_spec.rb +159 -0
  59. data/microservices/CFDP/spec/models/cfdp_pdu_file_data_spec.rb +76 -0
  60. data/microservices/CFDP/spec/models/cfdp_pdu_finished_spec.rb +192 -0
  61. data/microservices/CFDP/spec/models/cfdp_pdu_keep_alive_spec.rb +69 -0
  62. data/microservices/CFDP/spec/models/cfdp_pdu_metadata_spec.rb +346 -0
  63. data/microservices/CFDP/spec/models/cfdp_pdu_nak_spec.rb +126 -0
  64. data/microservices/CFDP/spec/models/cfdp_pdu_prompt_spec.rb +94 -0
  65. data/microservices/CFDP/spec/models/cfdp_pdu_spec.rb +111 -0
  66. data/microservices/CFDP/spec/rails_helper.rb +71 -0
  67. data/microservices/CFDP/spec/requests/cfdp_spec.rb +1965 -0
  68. data/microservices/CFDP/spec/spec_helper.rb +200 -0
  69. data/plugin.txt +67 -0
  70. data/targets/CFDPTEST/cmd_tlm/cmd.txt +5 -0
  71. data/targets/CFDPTEST/cmd_tlm/tlm.txt +4 -0
  72. data/targets/CFDPTEST/procedures/cfdp_test_suite.rb +130 -0
  73. data/targets/CFDPTEST/target.txt +4 -0
  74. metadata +118 -0
@@ -0,0 +1,449 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2023 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # Licensed for Evaluation and Educational Use
7
+ #
8
+ # This file may only be used commercially under the terms of a commercial license
9
+ # purchased from OpenC3, Inc.
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.
14
+ #
15
+ # The development of this software was funded in-whole or in-part by MethaneSAT LLC.
16
+
17
+ require_relative 'cfdp_transaction'
18
+
19
+ class CfdpSourceTransaction < CfdpTransaction
20
+
21
+ attr_reader :filestore_responses
22
+
23
+ def initialize(source_entity: nil)
24
+ super()
25
+ @source_entity = source_entity
26
+ @source_entity = CfdpMib.source_entity unless source_entity
27
+ raise "No source entity defined" unless @source_entity
28
+ @transaction_seq_num = CfdpModel.get_next_transaction_seq_num
29
+ @id = CfdpTransaction.build_transaction_id(@source_entity['id'], @transaction_seq_num)
30
+ CfdpMib.transactions[@id] = self
31
+ @finished_pdu_hash = nil
32
+ @destination_entity = nil
33
+ @eof_count = 0
34
+ @filestore_responses = []
35
+ @metadata_pdu_hash = {} # non-nil to avoid cfdp_user thinking it needs to be set
36
+ end
37
+
38
+ def put(
39
+ destination_entity_id:,
40
+ source_file_name: nil,
41
+ destination_file_name: nil,
42
+ segmentation_control: "NOT_PRESERVED", # Not supported
43
+ fault_handler_overrides: [],
44
+ flow_label: nil, # Not supported
45
+ transmission_mode: nil,
46
+ closure_requested: nil,
47
+ messages_to_user: [],
48
+ filestore_requests: [])
49
+
50
+ raise "destination_entity_id is required" if destination_entity_id.nil?
51
+ destination_entity_id = Integer(destination_entity_id)
52
+
53
+ @source_file_name = source_file_name
54
+ @destination_file_name = destination_file_name
55
+ @segmentation_control = segmentation_control
56
+ @segmentation_control = "NOT_PRESERVED" unless @segmentation_control
57
+ fault_handler_overrides = [] unless fault_handler_overrides
58
+ messages_to_user = [] unless messages_to_user
59
+ filestore_requests = [] unless filestore_requests
60
+
61
+ begin
62
+ transaction_start_notification()
63
+ copy_file(
64
+ transaction_seq_num: @transaction_seq_num,
65
+ transaction_id: @id,
66
+ destination_entity_id: destination_entity_id,
67
+ source_file_name: @source_file_name,
68
+ destination_file_name: @destination_file_name,
69
+ fault_handler_overrides: fault_handler_overrides,
70
+ flow_label: flow_label, # Not supported
71
+ transmission_mode: transmission_mode,
72
+ closure_requested: closure_requested,
73
+ messages_to_user: messages_to_user,
74
+ filestore_requests: filestore_requests
75
+ )
76
+ rescue => err
77
+ abandon()
78
+ raise err
79
+ end
80
+ end
81
+
82
+ def transaction_start_notification
83
+ # Issue Transaction.indication
84
+ CfdpTopic.write_indication("Transaction", transaction_id: @id)
85
+ end
86
+
87
+ def handle_suspend
88
+ while @state == "SUSPENDED" or @freeze
89
+ sleep(1)
90
+ end
91
+ end
92
+
93
+ def update
94
+ if @state != "SUSPENDED"
95
+ if @eof_ack_timeout and Time.now > @eof_ack_timeout and @destination_entity['enable_acks']
96
+ # Resend eof pdu
97
+ cmd_params = {}
98
+ cmd_params[@item_name] = @eof_pdu
99
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
100
+ @eof_count += 1
101
+ if @eof_count > @destination_entity['ack_timer_expiration_limit']
102
+ # Positive ACK Limit Reached Fault
103
+ @condition_code = "ACK_LIMIT_REACHED"
104
+ handle_fault()
105
+ @eof_ack_timeout = nil
106
+ else
107
+ @eof_ack_timeout = Time.now + @destination_entity['ack_timer_interval']
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def copy_file(
114
+ transaction_seq_num:,
115
+ transaction_id:,
116
+ destination_entity_id:,
117
+ source_file_name:,
118
+ destination_file_name:,
119
+ fault_handler_overrides:,
120
+ flow_label: nil, # Not supported
121
+ transmission_mode:,
122
+ closure_requested:,
123
+ messages_to_user:,
124
+ filestore_requests:)
125
+
126
+ # Lookup outgoing PDU command
127
+ @source_entity = CfdpMib.source_entity
128
+ @destination_entity = CfdpMib.entity(destination_entity_id)
129
+ raise "Unknown destination entity: #{destination_entity_id}" unless @destination_entity
130
+ @transmission_mode = transmission_mode
131
+ @transmission_mode = @destination_entity['default_transmission_mode'].upcase unless @transmission_mode
132
+ @target_name, @packet_name, @item_name = @destination_entity["cmd_info"]
133
+ raise "cmd_info not configured for destination_entity: #{destination_entity_id}" unless @target_name and @packet_name and @item_name
134
+
135
+ if source_file_name and destination_file_name
136
+ # Prepare file
137
+ if StringIO === source_file_name
138
+ source_file = source_file_name
139
+ source_file_name = destination_file_name
140
+ else
141
+ source_file = CfdpMib.get_source_file(source_file_name)
142
+ end
143
+ unless source_file
144
+ abandon()
145
+ raise "Source file: #{source_file_name} does not exist"
146
+ end
147
+
148
+ file_size = source_file.size
149
+ read_size = @destination_entity['maximum_file_segment_length']
150
+ else
151
+ source_file = nil
152
+ file_size = 0
153
+ end
154
+
155
+ # Prepare options, ordered by 4.6.1.1.3 c.
156
+ options = []
157
+ fault_handler_overrides = [] unless fault_handler_overrides
158
+ fault_handler_overrides.each do |fho|
159
+ tlv = {}
160
+ tlv["TYPE"] = "FAULT_HANDLER_OVERRIDE"
161
+ tlv["CONDITION_CODE"] = fho[0].to_s.upcase
162
+ tlv["HANDLER_CODE"] = fho[1].to_s.upcase
163
+ options << tlv
164
+ @fault_handler_overrides[tlv["CONDITION_CODE"]] = tlv["HANDLER_CODE"]
165
+ end
166
+
167
+ messages_to_user = [] unless messages_to_user
168
+ messages_to_user.each do |mtu|
169
+ tlv = {}
170
+ tlv["TYPE"] = "MESSAGE_TO_USER"
171
+ tlv["MESSAGE_TO_USER"] = mtu
172
+ options << tlv
173
+ end
174
+
175
+ filestore_requests = [] unless filestore_requests
176
+ filestore_requests.each do |fsr|
177
+ tlv = {}
178
+ tlv["TYPE"] = "FILESTORE_REQUEST"
179
+ tlv["ACTION_CODE"] = fsr[0].to_s.upcase
180
+ tlv["FIRST_FILE_NAME"] = fsr[1]
181
+ tlv["SECOND_FILE_NAME"] = fsr[2] if fsr[2]
182
+ options << tlv
183
+ end
184
+
185
+ if flow_label
186
+ tlv = {}
187
+ tlv["TYPE"] = "FLOW_LABEL"
188
+ tlv["FLOW_LABEL"] = flow_label
189
+ options << tlv
190
+ end
191
+
192
+ handle_suspend()
193
+ return if @state == "ABANDONED"
194
+
195
+ # Send Metadata PDU
196
+ @metadata_pdu = CfdpPdu.build_metadata_pdu(
197
+ source_entity: @source_entity,
198
+ transaction_seq_num: @transaction_seq_num,
199
+ destination_entity: @destination_entity,
200
+ closure_requested: closure_requested,
201
+ file_size: file_size,
202
+ source_file_name: source_file_name,
203
+ destination_file_name: destination_file_name,
204
+ options: options,
205
+ segmentation_control: @segmentation_control,
206
+ transmission_mode: @transmission_mode)
207
+ cmd_params = {}
208
+ cmd_params[@item_name] = @metadata_pdu
209
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
210
+
211
+ if source_file
212
+ checksum = get_checksum(@destination_entity['default_checksum_type'])
213
+ unless checksum
214
+ # Unsupported algorithm - Use modular instead
215
+ @condition_code = "UNSUPPORTED_CHECKSUM_TYPE"
216
+ handle_fault()
217
+ checksum = CfdpChecksum.new
218
+ end
219
+
220
+ # Send File Data PDUs
221
+ offset = 0
222
+ while true
223
+ break if @state == "CANCELED"
224
+ handle_suspend()
225
+ return if @state == "ABANDONED"
226
+ file_data = source_file.read(read_size)
227
+ break if file_data.nil? or file_data.length <= 0
228
+ file_data_pdu = CfdpPdu.build_file_data_pdu(
229
+ offset: offset,
230
+ file_data: file_data,
231
+ file_size: file_size,
232
+ source_entity: @source_entity,
233
+ transaction_seq_num: @transaction_seq_num,
234
+ destination_entity: @destination_entity,
235
+ segmentation_control: @segmentation_control,
236
+ transmission_mode: @transmission_mode)
237
+ cmd_params = {}
238
+ cmd_params[@item_name] = file_data_pdu
239
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
240
+ checksum.add(offset, file_data)
241
+ offset += file_data.length
242
+ @progress = offset
243
+ end
244
+ end
245
+
246
+ handle_suspend()
247
+ return if @state == "ABANDONED"
248
+
249
+ # Send EOF PDU
250
+ if source_file
251
+ file_checksum = checksum.checksum(source_file, false)
252
+ else
253
+ file_checksum = 0
254
+ end
255
+ if @canceling_entity_id
256
+ @condition_code = "CANCEL_REQUEST_RECEIVED"
257
+ eof_file_size = @progress
258
+ else
259
+ eof_file_size = file_size
260
+ end
261
+ begin
262
+ @eof_pdu = CfdpPdu.build_eof_pdu(
263
+ source_entity: @source_entity,
264
+ transaction_seq_num: @transaction_seq_num,
265
+ destination_entity: @destination_entity,
266
+ file_size: eof_file_size,
267
+ file_checksum: file_checksum,
268
+ condition_code: @condition_code,
269
+ segmentation_control: @segmentation_control,
270
+ transmission_mode: @transmission_mode,
271
+ canceling_entity_id: @canceling_entity_id)
272
+ cmd_params = {}
273
+ cmd_params[@item_name] = @eof_pdu
274
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
275
+ rescue => err
276
+ abandon() if @canceling_entity_id
277
+ raise err
278
+ end
279
+
280
+ # Issue EOF-Sent.indication
281
+ CfdpTopic.write_indication("EOF-Sent", transaction_id: transaction_id) if CfdpMib.source_entity['eof_sent_indication']
282
+
283
+ @eof_ack_timeout = Time.now + @destination_entity['ack_timer_interval'] if @transmission_mode == "ACKNOWLEDGED"
284
+
285
+ @file_status = "UNREPORTED"
286
+ @delivery_code = "DATA_COMPLETE"
287
+
288
+ # Wait for Finished if Closure Requested or Acknowledged Mode
289
+ if @destination_entity['enable_finished'] and (closure_requested == "CLOSURE_REQUESTED" or @transmission_mode == "ACKNOWLEDGED")
290
+ start_time = Time.now
291
+ while (Time.now - start_time) < @source_entity['check_interval']
292
+ sleep(1)
293
+ break if @finished_pdu_hash
294
+ end
295
+ if @finished_pdu_hash
296
+ @file_status = @finished_pdu_hash['FILE_STATUS']
297
+ @delivery_code = @finished_pdu_hash['DELIVERY_CODE']
298
+ @condition_code = @finished_pdu_hash['CONDITION_CODE'] unless @canceling_entity_id
299
+ else
300
+ unless @canceling_entity_id
301
+ @condition_code = "CHECK_LIMIT_REACHED"
302
+ handle_fault()
303
+ end
304
+ end
305
+ end
306
+
307
+ # Complete use of source file
308
+ CfdpMib.complete_source_file(source_file) if source_file
309
+
310
+ notice_of_completion()
311
+ end
312
+
313
+ def notice_of_completion
314
+ # Cancel all timeouts
315
+ @eof_ack_timeout = nil
316
+
317
+ if @finished_pdu_hash
318
+ tlvs = @finished_pdu_hash["TLVS"]
319
+ if tlvs
320
+ tlvs.each do |tlv|
321
+ case tlv['TYPE']
322
+ when 'FILESTORE_RESPONSE'
323
+ @filestore_responses << tlv.except('TYPE')
324
+ end
325
+ end
326
+ end
327
+ end
328
+ @state = "FINISHED" unless @state == "CANCELED" or @state == "ABANDONED"
329
+ @transaction_status = "TERMINATED"
330
+ OpenC3::Logger.info("CFDP Finished Source Transaction #{@id}, #{@condition_code}", scope: ENV['OPENC3_SCOPE'])
331
+
332
+ if CfdpMib.source_entity['transaction_finished_indication']
333
+ if @filestore_responses.length > 0
334
+ CfdpTopic.write_indication("Transaction-Finished",
335
+ transaction_id: @id, condition_code: @condition_code,
336
+ file_status: @file_status, delivery_code: @delivery_code, status_report: @state,
337
+ filestore_responses: @filestore_responses)
338
+ else
339
+ CfdpTopic.write_indication("Transaction-Finished",
340
+ transaction_id: @id, condition_code: @condition_code,
341
+ file_status: @file_status, status_report: @state, delivery_code: @delivery_code)
342
+ end
343
+ end
344
+ @proxy_response_needed = true if @proxy_response_info
345
+ end
346
+
347
+ def handle_pdu(pdu_hash)
348
+ case pdu_hash["DIRECTIVE_CODE"]
349
+ when "EOF", "METADATA", "PROMPT"
350
+ # Unexpected - Ignore
351
+
352
+ when "FINISHED"
353
+ @finished_pdu_hash = pdu_hash
354
+
355
+ if @finished_pdu_hash["CONDITION_CODE"] == "CANCEL_REQUEST_RECEIVED" and @state != "CANCELED"
356
+ cancel(@destination_entity.id)
357
+ end
358
+
359
+ if @transmission_mode == "ACKNOWLEDGED" and @destination_entity['enable_acks']
360
+ # Ack Finished PDU
361
+ ack_pdu = CfdpPdu.build_ack_pdu(
362
+ source_entity: @source_entity,
363
+ transaction_seq_num: @transaction_seq_num,
364
+ destination_entity: @destination_entity,
365
+ segmentation_control: @segmentation_control,
366
+ transmission_mode: @transmission_mode,
367
+ condition_code: @finished_pdu_hash["CONDITION_CODE"],
368
+ ack_directive_code: "FINISHED",
369
+ transaction_status: @transaction_status)
370
+ cmd_params = {}
371
+ cmd_params[@item_name] = ack_pdu
372
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
373
+ end
374
+
375
+ when "ACK"
376
+ # EOF Ack
377
+ @eof_ack_pdu_hash = pdu_hash
378
+ @eof_ack_timeout = nil
379
+
380
+ when "NAK"
381
+ handle_nak(pdu_hash)
382
+
383
+ when "KEEP_ALIVE"
384
+ @keep_alive_pdu_hash = pdu_hash
385
+ if (@progress - @keep_alive_pdu_hash['PROGRESS']) > @destination_entity['keep_alive_discrepancy_limit']
386
+ @condition_code = "KEEP_ALIVE_LIMIT_REACHED"
387
+ handle_fault()
388
+ end
389
+ else # File Data
390
+ # Unexpected - Ignore
391
+ end
392
+ end
393
+
394
+ def handle_nak(pdu_hash)
395
+ if StringIO === @source_file_name
396
+ source_file = StringIO.new(@source_file_name.string)
397
+ else
398
+ source_file = CfdpMib.get_source_file(@source_file_name)
399
+ end
400
+
401
+ # TODO: Not sure how valid this is in real life
402
+ # but test code can delete the file from under us
403
+ return unless source_file
404
+ file_size = source_file.size
405
+ max_read_size = @destination_entity['maximum_file_segment_length']
406
+
407
+ pdu_hash["SEGMENT_REQUESTS"].each do |request|
408
+ start_offset = request["START_OFFSET"]
409
+ end_offset = request["END_OFFSET"]
410
+
411
+ if start_offset == 0 and end_offset == 0
412
+ # Send Metadata PDU
413
+ cmd_params = {}
414
+ cmd_params[@item_name] = @metadata_pdu
415
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
416
+ else
417
+ # Send File Data PDU(s)
418
+ offset = start_offset
419
+ source_file.seek(offset, IO::SEEK_SET)
420
+ while true
421
+ bytes_remaining = end_offset - offset
422
+ break if bytes_remaining <= 0
423
+ if bytes_remaining >= max_read_size
424
+ read_size = max_read_size
425
+ else
426
+ read_size = bytes_remaining
427
+ end
428
+ file_data = source_file.read(read_size)
429
+ break if file_data.nil? or file_data.length <= 0
430
+ file_data_pdu = CfdpPdu.build_file_data_pdu(
431
+ offset: offset,
432
+ file_data: file_data,
433
+ file_size: file_size,
434
+ source_entity: @source_entity,
435
+ transaction_seq_num: @transaction_seq_num,
436
+ destination_entity: @destination_entity,
437
+ segmentation_control: @segmentation_control,
438
+ transmission_mode: @transmission_mode)
439
+ cmd_params = {}
440
+ cmd_params[@item_name] = file_data_pdu
441
+ cmd(@target_name, @packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
442
+ offset += file_data.length
443
+ end
444
+ end
445
+ end
446
+
447
+ CfdpMib.complete_source_file(source_file) if source_file
448
+ end
449
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2023 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # Licensed for Evaluation and Educational Use
7
+ #
8
+ # This file may only be used commercially under the terms of a commercial license
9
+ # purchased from OpenC3, Inc.
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.
14
+ #
15
+ # The development of this software was funded in-whole or in-part by MethaneSAT LLC.
16
+
17
+ require 'openc3/topics/topic'
18
+ require 'openc3/core_ext'
19
+
20
+ class CfdpTopic < OpenC3::Topic
21
+ def self.write_indication(indication_type, transaction_id:, **kw_args)
22
+ msg_hash = {
23
+ :time => Time.now.to_nsec_from_epoch,
24
+ :indication_type => indication_type,
25
+ :transaction_id => transaction_id
26
+ }
27
+ kw_args.each do |key, value|
28
+ msg_hash[key] = value
29
+ end
30
+
31
+ data_hash = {"data" => JSON.generate(msg_hash.as_json(allow_nan: true), allow_nan: true)}
32
+ OpenC3::Topic.write_topic("#{ENV['OPENC3_MICROSERVICE_NAME']}__CFDP", data_hash, '*', 1000)
33
+ end
34
+
35
+ def self.read_indications(transaction_id: nil, continuation: nil, limit: 1000)
36
+ continuation = '0-0' unless continuation
37
+ limit = 1000 unless limit
38
+ xread = OpenC3::Topic.read_topics(["#{ENV['OPENC3_MICROSERVICE_NAME']}__CFDP"], [continuation], nil, limit) # Always don't block
39
+ # Return the original continuation and and empty array if we didn't get anything
40
+ indications = []
41
+ return {continuation: continuation, indications: indications} if xread.empty?
42
+ xread.each do |topic, data|
43
+ data.each do |id, msg_hash|
44
+ continuation = id
45
+ msg_hash = JSON.parse(msg_hash["data"], :allow_nan => true, :create_additions => true)
46
+ if !transaction_id or (transaction_id and msg_hash['transaction_id'] == transaction_id)
47
+ indications << msg_hash
48
+ end
49
+ end
50
+ end
51
+ return {continuation: continuation, indications: indications}
52
+ end
53
+
54
+ def self.subscribe_indications
55
+ id, _ = OpenC3::Topic.get_newest_message("#{ENV['OPENC3_MICROSERVICE_NAME']}__CFDP")
56
+ return id || '0-0'
57
+ end
58
+ end
@@ -0,0 +1,188 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2023 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # Licensed for Evaluation and Educational Use
7
+ #
8
+ # This file may only be used commercially under the terms of a commercial license
9
+ # purchased from OpenC3, Inc.
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.
14
+ #
15
+ # The development of this software was funded in-whole or in-part by MethaneSAT LLC.
16
+
17
+ require 'openc3/api/api'
18
+ require_relative 'cfdp_model'
19
+ require_relative 'cfdp_mib'
20
+ require_relative 'cfdp_topic'
21
+ require_relative 'cfdp_pdu'
22
+ require_relative 'cfdp_checksum'
23
+ require_relative 'cfdp_null_checksum'
24
+ require_relative 'cfdp_crc_checksum'
25
+ require 'tempfile'
26
+
27
+ class CfdpTransaction
28
+ include OpenC3::Api
29
+ attr_reader :id
30
+ attr_reader :frozen
31
+ attr_reader :state
32
+ attr_reader :transaction_status
33
+ attr_reader :progress
34
+ attr_reader :transaction_seq_num
35
+ attr_reader :condition_code
36
+ attr_reader :delivery_code
37
+ attr_reader :file_status
38
+ attr_reader :metadata_pdu_hash
39
+ attr_reader :metadata_pdu_count
40
+ attr_accessor :proxy_response_info
41
+ attr_accessor :proxy_response_needed
42
+
43
+ def self.build_transaction_id(source_entity_id, transaction_seq_num)
44
+ "#{source_entity_id}__#{transaction_seq_num}"
45
+ end
46
+
47
+ def initialize
48
+ @frozen = false
49
+ @state = "ACTIVE" # ACTIVE, FINISHED, CANCELED, SUSPENDED, ABANDONED
50
+ @transaction_status = "ACTIVE" # UNDEFINED, ACTIVE, TERMINATED, UNRECOGNIZED
51
+ @progress = 0
52
+ @condition_code = "NO_ERROR"
53
+ @delivery_code = nil
54
+ @canceling_entity_id = nil
55
+ @fault_handler_overrides = {}
56
+ @metadata_pdu_hash = nil
57
+ @metadata_pdu_count = 0
58
+ @proxy_response_info = nil
59
+ @proxy_response_needed = false
60
+ @source_file_name = nil
61
+ @destination_file_name = nil
62
+ end
63
+
64
+ def as_json(*args)
65
+ return {
66
+ "id" => @id,
67
+ "frozen" => @frozen,
68
+ "state" => @state,
69
+ "transaction_status" => @transaction_status,
70
+ "progress" => @progress,
71
+ "condition_code" => @condition_code,
72
+ "source_file_name" => @source_file_name,
73
+ "destination_file_name" => @destination_file_name
74
+ }
75
+ end
76
+
77
+ def suspend
78
+ OpenC3::Logger.info("CFDP Suspend Transaction #{@id}", scope: ENV['OPENC3_SCOPE'])
79
+ if @state == "ACTIVE"
80
+ @condition_code = "SUSPEND_REQUEST_RECEIVED"
81
+ @state = "SUSPENDED"
82
+ CfdpTopic.write_indication("Suspended", transaction_id: @id, condition_code: @condition_code) if CfdpMib.source_entity['suspended_indication']
83
+ end
84
+ end
85
+
86
+ def resume
87
+ OpenC3::Logger.info("CFDP Resume Transaction #{@id}", scope: ENV['OPENC3_SCOPE'])
88
+ if @state == "SUSPENDED"
89
+ @state = "ACTIVE"
90
+ @condition_code = "NO_ERROR"
91
+ @inactivity_timeout = Time.now + CfdpMib.source_entity['keep_alive_interval']
92
+ CfdpTopic.write_indication("Resumed", transaction_id: @id, progress: @progress) if CfdpMib.source_entity['resume_indication']
93
+ end
94
+ end
95
+
96
+ def cancel(canceling_entity_id = nil)
97
+ OpenC3::Logger.info("CFDP Cancel Transaction #{@id}", scope: ENV['OPENC3_SCOPE'])
98
+ if @state != "FINISHED"
99
+ @condition_code = "CANCEL_REQUEST_RECEIVED" if @condition_code == "NO_ERROR"
100
+ if canceling_entity_id
101
+ @canceling_entity_id = canceling_entity_id
102
+ else
103
+ @canceling_entity_id = CfdpMib.source_entity['id']
104
+ end
105
+ @state = "CANCELED"
106
+ @transaction_status = "TERMINATED"
107
+ end
108
+ end
109
+
110
+ def abandon
111
+ OpenC3::Logger.info("CFDP Abandon Transaction #{@id}", scope: ENV['OPENC3_SCOPE'])
112
+ if @state != "FINISHED"
113
+ @state = "ABANDONED"
114
+ @transaction_status = "TERMINATED"
115
+ CfdpTopic.write_indication("Abandoned", transaction_id: @id, condition_code: @condition_code, progress: @progress)
116
+ end
117
+ end
118
+
119
+ def report
120
+ CfdpTopic.write_indication("Report", transaction_id: @id, status_report: build_report())
121
+ end
122
+
123
+ def freeze
124
+ OpenC3::Logger.info("CFDP Freeze Transaction #{@id}", scope: ENV['OPENC3_SCOPE'])
125
+ @freeze = true
126
+ end
127
+
128
+ def unfreeze
129
+ OpenC3::Logger.info("CFDP Unfreeze Transaction #{@id}", scope: ENV['OPENC3_SCOPE'])
130
+ @freeze = false
131
+ end
132
+
133
+ def build_report
134
+ JSON.generate(as_json(allow_nan: true), allow_nan: true)
135
+ end
136
+
137
+ def handle_fault
138
+ OpenC3::Logger.error("CFDP Fault Transaction #{@id}, #{@condition_code}", scope: ENV['OPENC3_SCOPE'])
139
+ if @fault_handler_overrides[@condition_code]
140
+ case @fault_handler_overrides[@condition_code]
141
+ when "ISSUE_NOTICE_OF_CANCELLATION"
142
+ cancel()
143
+ when "ISSUE_NOTICE_OF_SUSPENSION"
144
+ suspend()
145
+ when "IGNORE_ERROR"
146
+ ignore_fault()
147
+ when "ABANDON_TRANSACTION"
148
+ abandon()
149
+ end
150
+ else
151
+ case CfdpMib.source_entity['fault_handler'][@condition_code]
152
+ when "ISSUE_NOTICE_OF_CANCELLATION"
153
+ cancel()
154
+ when "ISSUE_NOTICE_OF_SUSPENSION"
155
+ suspend()
156
+ when "IGNORE_ERROR"
157
+ ignore_fault()
158
+ when "ABANDON_TRANSACTION"
159
+ abandon()
160
+ end
161
+ end
162
+ end
163
+
164
+ def ignore_fault
165
+ CfdpTopic.write_indication("Fault", transaction_id: @id, condition_code: @condition_code, progress: @progress)
166
+ end
167
+
168
+ def update
169
+ # Default do nothing
170
+ end
171
+
172
+ def get_checksum(checksum_type)
173
+ case checksum_type
174
+ when 0 # Modular Checksum
175
+ return CfdpChecksum.new
176
+ when 1 # Proximity-1 CRC-32 - Poly: 0x00A00805 - Reference CCSDS-211.2-B-3 - Unsure of correct xor/reflect
177
+ return CfdpCrcChecksum.new(0x00A00805, 0x00000000, false, false)
178
+ when 2 # CRC-32C - Poly: 0x1EDC6F41 - Reference RFC4960
179
+ return CfdpCrcChecksum.new(0x1EDC6F41, 0xFFFFFFFF, true, true)
180
+ when 3 # CRC-32 - Poly: 0x04C11DB7 - Reference Ethernet Frame Check Sequence
181
+ return CfdpCrcChecksum.new(0x04C11DB7, 0xFFFFFFFF, true, true)
182
+ when 15
183
+ return CfdpNullChecksum.new
184
+ else # Unsupported
185
+ return nil
186
+ end
187
+ end
188
+ end