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,590 @@
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 CfdpReceiveTransaction < CfdpTransaction
20
+ def initialize(pdu_hash)
21
+ super()
22
+ @id = CfdpTransaction.build_transaction_id(pdu_hash["SOURCE_ENTITY_ID"], pdu_hash["SEQUENCE_NUMBER"])
23
+ @transaction_seq_num = pdu_hash["SEQUENCE_NUMBER"]
24
+ @transmission_mode = pdu_hash["TRANSMISSION_MODE"]
25
+ @messages_to_user = []
26
+ @filestore_requests = []
27
+ @tmp_file = nil
28
+ @segments = {}
29
+ @eof_pdu_hash = nil
30
+ @checksum = CfdpNullChecksum.new
31
+ @full_checksum_needed = false
32
+ @file_size = 0
33
+ @file_status = "UNREPORTED"
34
+ @delivery_code = "DATA_COMPLETE"
35
+ @filestore_responses = []
36
+ @nak_timeout = nil
37
+ @nak_timeout_count = 0
38
+ @check_timeout = nil
39
+ @check_timeout_count = 0
40
+ @progress = 0
41
+ @nak_start_of_scope = 0
42
+ @keep_alive_count = 0
43
+ @finished_count = 0
44
+ @source_entity_id = nil
45
+ @inactivity_timeout = nil
46
+ @inactivity_count = 0
47
+ @keep_alive_timeout = nil
48
+ CfdpMib.transactions[@id] = self
49
+ handle_pdu(pdu_hash)
50
+ @inactivity_timeout = Time.now + CfdpMib.entity(@source_entity_id)['keep_alive_interval']
51
+ @keep_alive_timeout = Time.now + CfdpMib.entity(@source_entity_id)['keep_alive_interval'] if @transmission_mode == 'ACKNOWLEDGED' and CfdpMib.entity(@source_entity_id)['enable_keep_alive']
52
+ end
53
+
54
+ def check_complete
55
+ return false unless @metadata_pdu_hash and @eof_pdu_hash
56
+ if @eof_pdu_hash["CONDITION_CODE"] != "NO_ERROR" # Canceled
57
+ @state = "CANCELED"
58
+ @transaction_status = "TERMINATED"
59
+ @condition_code = @eof_pdu_hash["CONDITION_CODE"]
60
+ @file_status = "FILE_DISCARDED"
61
+ @delivery_code = "DATA_INCOMPLETE"
62
+ if CfdpMib.entity(@source_entity_id)['incomplete_file_disposition'] == "DISCARD"
63
+ @tmp_file.unlink if @tmp_file
64
+ @tmp_file = nil
65
+ else
66
+ # Keep
67
+ if @tmp_file
68
+ @tmp_file.close
69
+ success = CfdpMib.put_destination_file(@destination_file_name, @tmp_file) # Unlink handled by CfdpMib
70
+ if success
71
+ @file_status = "FILESTORE_SUCCESS"
72
+ else
73
+ @file_status = "FILESTORE_REJECTION"
74
+ end
75
+ end
76
+ end
77
+ notice_of_completion()
78
+ return true
79
+ end
80
+
81
+ if @source_file_name and @destination_file_name
82
+ if complete_file_received?
83
+ @tmp_file ||= Tempfile.new('cfdp')
84
+
85
+ # Complete
86
+ if @checksum.check(@tmp_file, @eof_pdu_hash['FILE_CHECKSUM'], @full_checksum_needed)
87
+ # Move file to final destination
88
+ @tmp_file.close
89
+ success = CfdpMib.put_destination_file(@destination_file_name, @tmp_file) # Unlink handled by CfdpMib
90
+ if success
91
+ @file_status = "FILESTORE_SUCCESS"
92
+ else
93
+ @file_status = "FILESTORE_REJECTION"
94
+ @condition_code = "FILESTORE_REJECTION"
95
+ handle_fault()
96
+ end
97
+ @delivery_code = "DATA_COMPLETE"
98
+ else
99
+ @tmp_file.unlink
100
+ @file_status = "FILE_DISCARDED"
101
+ @condition_code = "FILE_CHECKSUM_FAILURE"
102
+ handle_fault()
103
+ @delivery_code = "DATA_INCOMPLETE"
104
+ end
105
+ @tmp_file = nil
106
+ else
107
+ # Still waiting on file data
108
+ return false
109
+ end
110
+ end
111
+
112
+ # Handle Filestore Requests
113
+ filestore_success = true
114
+ tlvs = @metadata_pdu_hash["TLVS"]
115
+ if tlvs and (@condition_code == "NO_ERROR" or @condition_code == "UNSUPPORTED_CHECKSUM_TYPE")
116
+ tlvs.each do |tlv|
117
+ case tlv['TYPE']
118
+ when 'FILESTORE_REQUEST'
119
+ action_code = tlv["ACTION_CODE"]
120
+ first_file_name = tlv["FIRST_FILE_NAME"]
121
+ second_file_name = tlv["SECOND_FILE_NAME"]
122
+ if filestore_success
123
+ status_code, filestore_message = CfdpMib.filestore_request(action_code, first_file_name, second_file_name)
124
+ filestore_response = {}
125
+ filestore_response['ACTION_CODE'] = action_code
126
+ filestore_response['STATUS_CODE'] = status_code
127
+ filestore_response['FIRST_FILE_NAME'] = first_file_name
128
+ filestore_response['SECOND_FILE_NAME'] = second_file_name
129
+ filestore_response['FILESTORE_MESSAGE'] = filestore_message
130
+ @filestore_responses << filestore_response
131
+ filestore_success = false if status_code != 'SUCCESSFUL'
132
+ else
133
+ filestore_response = {}
134
+ filestore_response['ACTION_CODE'] = action_code
135
+ filestore_response['STATUS_CODE'] = "NOT_PERFORMED"
136
+ filestore_response['FIRST_FILE_NAME'] = first_file_name
137
+ filestore_response['SECOND_FILE_NAME'] = second_file_name
138
+ @filestore_responses << filestore_response
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ notice_of_completion()
145
+ return true
146
+ end
147
+
148
+ def notice_of_completion
149
+ # Cancel all timeouts
150
+ @check_timeout = nil
151
+ @nak_timeout = nil
152
+ @keep_alive_timeout = nil
153
+ @inactivity_timeout = nil
154
+ @finished_ack_timeout = nil
155
+
156
+ destination_entity = CfdpMib.source_entity
157
+ source_entity = CfdpMib.entity(@source_entity_id)
158
+ if source_entity['enable_finished'] and (@metadata_pdu_hash["CLOSURE_REQUESTED"] == "CLOSURE_REQUESTED" or @transmission_mode == "ACKNOWLEDGED")
159
+ begin
160
+ # Lookup outgoing PDU command
161
+ raise "Unknown source entity: #{@metadata_pdu_hash['SOURCE_ENTITY_ID']}" unless source_entity
162
+ target_name, packet_name, item_name = source_entity["cmd_info"]
163
+ raise "cmd_info not defined for source entity: #{@metadata_pdu_hash['SOURCE_ENTITY_ID']}" unless target_name and packet_name and item_name
164
+ @finished_pdu = CfdpPdu.build_finished_pdu(
165
+ source_entity: source_entity,
166
+ transaction_seq_num: @transaction_seq_num,
167
+ destination_entity: destination_entity,
168
+ condition_code: @condition_code,
169
+ segmentation_control: "NOT_PRESERVED",
170
+ transmission_mode: @transmission_mode,
171
+ delivery_code: @delivery_code,
172
+ file_status: @file_status,
173
+ filestore_responses: @filestore_responses,
174
+ fault_location_entity_id: nil)
175
+ cmd_params = {}
176
+ cmd_params[item_name] = @finished_pdu
177
+ cmd(target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
178
+ rescue => err
179
+ abandon() if @state == "CANCELED"
180
+ raise err
181
+ end
182
+ @finished_ack_timeout = Time.now + source_entity['ack_timer_interval'] if @transmission_mode == "ACKNOWLEDGED" and source_entity['enable_acks']
183
+ end
184
+
185
+ @state = "FINISHED" unless @state == "CANCELED" or @state == "ABANDONED"
186
+ @transaction_status = "TERMINATED"
187
+ OpenC3::Logger.info("CFDP Finished Receive Transaction #{@id}, #{@condition_code}", scope: ENV['OPENC3_SCOPE'])
188
+
189
+ if CfdpMib.source_entity['transaction_finished_indication']
190
+ if @filestore_responses.length > 0
191
+ CfdpTopic.write_indication("Transaction-Finished", transaction_id: @id, condition_code: @condition_code, file_status: @file_status, delivery_code: @delivery_code, status_report: @state, filestore_responses: @filestore_responses)
192
+ else
193
+ CfdpTopic.write_indication("Transaction-Finished", transaction_id: @id, condition_code: @condition_code, file_status: @file_status, status_report: @state, delivery_code: @delivery_code)
194
+ end
195
+ end
196
+ end
197
+
198
+ def complete_file_received?
199
+ return false unless @file_size
200
+ offset = 0
201
+ while offset
202
+ next_offset = @segments[offset]
203
+ if next_offset
204
+ return true if next_offset == @file_size
205
+ else
206
+ # See if any segments cover the next offset
207
+ @segments.each do |segment_offset, segment_next_offset|
208
+ if offset > segment_offset and offset < segment_next_offset
209
+ # Found
210
+ next_offset = segment_next_offset
211
+ return true if next_offset == @file_size
212
+ break
213
+ end
214
+ end
215
+ end
216
+ offset = next_offset
217
+ end
218
+ return false
219
+ end
220
+
221
+ def cancel(canceling_entity_id = nil)
222
+ super(canceling_entity_id)
223
+ notice_of_completion()
224
+ end
225
+
226
+ def suspend
227
+ if @transmission_mode == "ACKNOWLEDGED"
228
+ super()
229
+ end
230
+ end
231
+
232
+ def update
233
+ if @state != "SUSPENDED"
234
+ if @check_timeout
235
+ if Time.now > @check_timeout
236
+ @check_timeout_count += 1
237
+ if @check_timeout_count < CfdpMib.entity(@source_entity_id)['check_limit']
238
+ @check_timeout = Time.now + CfdpMib.entity(@source_entity_id)['check_interval']
239
+ else
240
+ @condition_code = "CHECK_LIMIT_REACHED"
241
+ handle_fault()
242
+ @check_timeout = nil
243
+ end
244
+ end
245
+ end
246
+ if @nak_timeout
247
+ if Time.now > @nak_timeout
248
+ if complete_file_received?
249
+ @nak_timeout = nil
250
+ else
251
+ send_naks(true)
252
+ @nak_timeout_count += 1
253
+ if @nak_timeout_count < CfdpMib.entity(@source_entity_id)['nak_timer_expiration_limit']
254
+ @nak_timeout = Time.now + CfdpMib.entity(@source_entity_id)['nak_timer_interval']
255
+ else
256
+ @condition_code = "NAK_LIMIT_REACHED"
257
+ handle_fault()
258
+ @nak_timeout = nil
259
+ end
260
+ end
261
+ end
262
+ end
263
+ if @keep_alive_timeout
264
+ if @eof_pdu_hash
265
+ @keep_alive_timeout = nil
266
+ else
267
+ if Time.now > @keep_alive_timeout
268
+ send_keep_alive()
269
+ @keep_alive_count += 1
270
+ @keep_alive_timeout = Time.now + CfdpMib.entity(@source_entity_id)['keep_alive_interval']
271
+ end
272
+ end
273
+ end
274
+ if @inactivity_timeout
275
+ if @eof_pdu_hash
276
+ @inactivity_timeout = nil
277
+ else
278
+ if Time.now > @inactivity_timeout
279
+ @inactivity_count += 1
280
+ if @inactivity_count < CfdpMib.entity(@source_entity_id)['transaction_inactivity_limit']
281
+ @inactivity_timeout = Time.now + CfdpMib.entity(@source_entity_id)['keep_alive_interval']
282
+ else
283
+ @condition_code = "INACTIVITY_DETECTED"
284
+ handle_fault()
285
+ end
286
+ end
287
+ end
288
+ end
289
+ if @finished_ack_timeout
290
+ if @finished_ack_pdu_hash
291
+ @finished_ack_timeout = nil
292
+ else
293
+ if Time.now > @finished_ack_timeout
294
+ source_entity = CfdpMib.entity(@metadata_pdu_hash['SOURCE_ENTITY_ID'])
295
+ raise "Unknown source entity: #{@metadata_pdu_hash['SOURCE_ENTITY_ID']}" unless source_entity
296
+ target_name, packet_name, item_name = source_entity["cmd_info"]
297
+ raise "cmd_info not defined for source entity: #{@metadata_pdu_hash['SOURCE_ENTITY_ID']}" unless target_name and packet_name and item_name
298
+ cmd_params = {}
299
+ cmd_params[item_name] = @finished_pdu
300
+ cmd(target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
301
+ @finished_count += 1
302
+ if @finished_count > CfdpMib.entity(@source_entity_id)['ack_timer_expiration_limit']
303
+ # Positive ACK Limit Reached Fault
304
+ @condition_code = "ACK_LIMIT_REACHED"
305
+ handle_fault()
306
+ @finished_ack_timeout = nil
307
+ else
308
+ @finished_ack_timeout = Time.now + CfdpMib.entity(@source_entity_id)['ack_timer_interval']
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end
315
+
316
+ def send_keep_alive
317
+ source_entity = CfdpMib.entity(@metadata_pdu_hash['SOURCE_ENTITY_ID'])
318
+ destination_entity = CfdpMib.source_entity
319
+ target_name, packet_name, item_name = source_entity["cmd_info"]
320
+
321
+ keep_alive_pdu = CfdpPdu.build_keep_alive_pdu(
322
+ source_entity: source_entity,
323
+ transaction_seq_num: @transaction_seq_num,
324
+ destination_entity: destination_entity,
325
+ file_size: @file_size,
326
+ segmentation_control: "NOT_PRESERVED",
327
+ transmission_mode: @transmission_mode,
328
+ progress: @progress)
329
+ cmd_params = {}
330
+ cmd_params[item_name] = keep_alive_pdu
331
+ cmd(target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
332
+ end
333
+
334
+ def send_naks(force = false)
335
+ source_entity = CfdpMib.entity(@source_entity_id)
336
+ destination_entity = CfdpMib.source_entity
337
+ target_name, packet_name, item_name = source_entity["cmd_info"]
338
+
339
+ segment_requests = []
340
+ segment_requests << [0, 0] unless @metadata_pdu_hash
341
+
342
+ # TODO: I don't see the metadata_pdu_hash being used anywhere
343
+ # past this point. The Metadata holds the file size so how can
344
+ # we know if we haven't received segments if we never check
345
+ # the original request size?
346
+
347
+ if @eof_pdu_hash
348
+ final_end_of_scope = @file_size
349
+ else
350
+ final_end_of_scope = @progress
351
+ end
352
+
353
+ if force
354
+ offset = 0
355
+ else
356
+ offset = @nak_start_of_scope
357
+ end
358
+ sorted_segments = @segments.to_a.sort {|a,b| a[0] <=> b[0]}
359
+ index = 0
360
+ sorted_segments.each do |start_offset, end_offset|
361
+ break if end_offset > offset
362
+ index += 1
363
+ end
364
+ sorted_segments = sorted_segments[index..-1]
365
+ while (offset < final_end_of_scope) and sorted_segments.length > 0
366
+ found = false
367
+ sorted_segments.each do |start_offset, end_offset|
368
+ if offset >= start_offset and offset < end_offset
369
+ # Offset found - move to end offset
370
+ offset = end_offset
371
+ found = true
372
+ break
373
+ end
374
+ end
375
+ unless found
376
+ # Need a segment request up to first sorted segment
377
+ segment_requests << [offset, sorted_segments[0][0]]
378
+ offset = sorted_segments[0][1]
379
+ end
380
+ sorted_segments = sorted_segments[1..-1]
381
+ end
382
+ if offset < final_end_of_scope
383
+ segment_requests << [offset, final_end_of_scope]
384
+ end
385
+
386
+ # Calculate max number of segments in a single NAK PDU
387
+ if force
388
+ start_of_scope = 0
389
+ else
390
+ start_of_scope = @nak_start_of_scope
391
+ end
392
+ max_segments = (source_entity['maximum_file_segment_length'] / 8) - 2 # Minus 2 handles scope fields
393
+ while true
394
+ num_segments = segment_requests.length
395
+ if num_segments > max_segments
396
+ num_segments = max_segments
397
+ end
398
+ current_segment_requests = segment_requests[0..(num_segments - 1)]
399
+ if current_segment_requests.length == segment_requests.length
400
+ if @eof_pdu_hash
401
+ end_of_scope = @file_size
402
+ else
403
+ end_of_scope = @progress
404
+ end
405
+ @nak_start_of_scope = end_of_scope
406
+ else
407
+ end_of_scope = current_segment_requests[-1][1]
408
+ end
409
+ if start_of_scope != end_of_scope
410
+ nak_pdu = CfdpPdu.build_nak_pdu(
411
+ source_entity: source_entity,
412
+ transaction_seq_num: @transaction_seq_num,
413
+ destination_entity: destination_entity,
414
+ file_size: @file_size,
415
+ segmentation_control: "NOT_PRESERVED",
416
+ transmission_mode: @transmission_mode,
417
+ start_of_scope: start_of_scope,
418
+ end_of_scope: end_of_scope,
419
+ segment_requests: current_segment_requests)
420
+ segment_requests = segment_requests[num_segments..-1]
421
+ start_of_scope = end_of_scope
422
+ cmd_params = {}
423
+ cmd_params[item_name] = nak_pdu
424
+ cmd(target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
425
+ end
426
+ break if segment_requests.length <= 0
427
+ end
428
+ end
429
+
430
+ def handle_pdu(pdu_hash)
431
+ source_entity = CfdpMib.entity(@source_entity_id)
432
+ @inactivity_timeout = Time.now + source_entity['keep_alive_interval'] if source_entity
433
+
434
+ case pdu_hash["DIRECTIVE_CODE"]
435
+ when "METADATA"
436
+ @metadata_pdu_count += 1
437
+ return if @metadata_pdu_hash # Discard repeats
438
+ @metadata_pdu_hash = pdu_hash
439
+ @source_entity_id = @metadata_pdu_hash['SOURCE_ENTITY_ID']
440
+ kw_args = {}
441
+ tlvs = pdu_hash['TLVS']
442
+ if tlvs
443
+ tlvs.each do |tlv|
444
+ case tlv["TYPE"]
445
+ when "FILESTORE_REQUEST"
446
+ filestore_request = {}
447
+ filestore_request["ACTION_CODE"] = tlv["ACTION_CODE"]
448
+ filestore_request["FIRST_FILE_NAME"] = tlv["FIRST_FILE_NAME"]
449
+ filestore_request["SECOND_FILE_NAME"] = tlv["SECOND_FILE_NAME"] if tlv["SECOND_FILE_NAME"]
450
+ @filestore_requests << filestore_request
451
+
452
+ when "MESSAGE_TO_USER"
453
+ @messages_to_user << tlv["MESSAGE_TO_USER"]
454
+
455
+ when "FAULT_HANDLER_OVERRIDE"
456
+ @fault_handler_overrides[tlv["CONDITION_CODE"]] = tlv["HANDLER_CODE"]
457
+
458
+ when "FLOW_LABEL"
459
+ kw_args[:flow_label] = tlv["FLOW_LABEL"]
460
+ end
461
+ end
462
+ end
463
+ kw_args[:filestore_requests] = @filestore_requests unless @filestore_requests.empty?
464
+ kw_args[:messages_to_user] = @messages_to_user unless @messages_to_user.empty?
465
+ kw_args[:fault_handler_overrides] = @fault_handler_overrides unless @fault_handler_overrides.empty?
466
+
467
+ kw_args[:transaction_id] = @id
468
+ kw_args[:source_entity_id] = @metadata_pdu_hash['SOURCE_ENTITY_ID']
469
+
470
+ @file_size = @metadata_pdu_hash['FILE_SIZE']
471
+ kw_args[:file_size] = @file_size
472
+
473
+ @source_file_name = nil
474
+ if @metadata_pdu_hash['SOURCE_FILE_NAME'] and @metadata_pdu_hash['SOURCE_FILE_NAME'].length > 0
475
+ @source_file_name = @metadata_pdu_hash['SOURCE_FILE_NAME']
476
+ kw_args[:source_file_name] = @source_file_name
477
+ end
478
+
479
+ @destination_file_name = nil
480
+ if @metadata_pdu_hash['DESTINATION_FILE_NAME'] and @metadata_pdu_hash['DESTINATION_FILE_NAME'].length > 0
481
+ @destination_file_name = @metadata_pdu_hash['DESTINATION_FILE_NAME']
482
+ kw_args[:destination_file_name] = @destination_file_name
483
+ end
484
+
485
+ CfdpTopic.write_indication("Metadata-Recv", **kw_args)
486
+
487
+ @checksum = get_checksum(@metadata_pdu_hash["CHECKSUM_TYPE"])
488
+ unless @checksum
489
+ # Use Null checksum if checksum type not available
490
+ @condition_code = "UNSUPPORTED_CHECKSUM_TYPE"
491
+ handle_fault()
492
+ @checksum = CfdpNullChecksum.new
493
+ end
494
+
495
+ when "EOF"
496
+ @eof_pdu_hash = pdu_hash
497
+
498
+ # Check file size fault
499
+ @file_size = @eof_pdu_hash["FILE_SIZE"]
500
+ if @progress > @file_size
501
+ @condition_code = "FILE_SIZE_ERROR"
502
+ handle_fault()
503
+ end
504
+
505
+ CfdpTopic.write_indication("EOF-Recv", transaction_id: @id) if CfdpMib.source_entity['eof_recv_indication']
506
+
507
+ destination_entity = CfdpMib.source_entity
508
+ source_entity = CfdpMib.entity(@metadata_pdu_hash['SOURCE_ENTITY_ID'])
509
+ if @transmission_mode == "ACKNOWLEDGED" and source_entity['enable_acks']
510
+ target_name, packet_name, item_name = source_entity["cmd_info"]
511
+ # Ack EOF PDU
512
+ ack_pdu = CfdpPdu.build_ack_pdu(
513
+ source_entity: source_entity,
514
+ transaction_seq_num: @transaction_seq_num,
515
+ destination_entity: destination_entity,
516
+ segmentation_control: "NOT_PRESERVED",
517
+ transmission_mode: @transmission_mode,
518
+ condition_code: @eof_pdu_hash["CONDITION_CODE"],
519
+ ack_directive_code: "EOF",
520
+ transaction_status: "ACTIVE")
521
+ cmd_params = {}
522
+ cmd_params[item_name] = ack_pdu
523
+ cmd(target_name, packet_name, cmd_params, scope: ENV['OPENC3_SCOPE'])
524
+ end
525
+
526
+ # Note: This also handles canceled
527
+ complete = check_complete()
528
+ if complete
529
+ send_naks(true) if destination_entity['enable_eof_nak']
530
+ else
531
+ @check_timeout = Time.now + source_entity['check_interval']
532
+ @progress = @file_size
533
+ send_naks() if destination_entity['immediate_nak_mode'] or destination_entity['enable_eof_nak']
534
+ @nak_timeout = Time.now + source_entity['nak_timer_interval']
535
+ end
536
+
537
+ when "NAK", "FINISHED", "KEEP_ALIVE"
538
+ # Unexpected - Ignore
539
+
540
+ when "ACK"
541
+ @finished_ack_pdu_hash = pdu_hash
542
+ @finished_ack_timeout = nil
543
+
544
+ when "PROMPT"
545
+ @prompt_pdu_hash = pdu_hash
546
+ unless @eof_pdu_hash
547
+ if @prompt_pdu_hash['RESPONSE_REQUIRED'] == 'NAK'
548
+ send_naks()
549
+ else
550
+ send_keep_alive()
551
+ end
552
+ end
553
+
554
+ else # File Data
555
+ @source_entity_id = @metadata_pdu_hash['SOURCE_ENTITY_ID']
556
+
557
+ @tmp_file ||= Tempfile.new('cfdp')
558
+ offset = pdu_hash['OFFSET']
559
+ file_data = pdu_hash['FILE_DATA']
560
+ progress = offset + file_data.length
561
+
562
+ need_send_naks = false
563
+ if @transmission_mode == "ACKNOWLEDGED" and CfdpMib.entity(@source_entity_id)['immediate_nak_mode']
564
+ need_send_naks = true unless @metadata_pdu_hash
565
+ need_send_naks = true if offset != @progress and @progress < offset
566
+ end
567
+
568
+ @progress = progress if progress > @progress
569
+
570
+ # Ignore repeated segments
571
+ if !@segments[offset] or @segments[offset] != progress
572
+ if @file_size and progress > @file_size
573
+ @condition_code = "FILE_SIZE_ERROR"
574
+ handle_fault()
575
+ else
576
+ @full_checksum_needed = true unless @metadata_pdu_hash
577
+ @checksum.add(offset, file_data)
578
+ @segments[offset] = offset + file_data.length
579
+ @tmp_file.seek(offset, IO::SEEK_SET)
580
+ @tmp_file.write(file_data)
581
+ end
582
+ check_complete()
583
+
584
+ CfdpTopic.write_indication("File-Segment-Recv", transaction_id: @id, offset: offset, length: file_data.length) if CfdpMib.source_entity['file_segment_recv_indication']
585
+ end
586
+
587
+ send_naks() if need_send_naks
588
+ end
589
+ end
590
+ end