openc3-cosmos-cfdp 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +18 -0
- data/README.md +181 -0
- data/Rakefile +40 -0
- data/lib/cfdp.rb +283 -0
- data/lib/cfdp_api.rb +204 -0
- data/microservices/CFDP/Gemfile +37 -0
- data/microservices/CFDP/Rakefile +6 -0
- data/microservices/CFDP/app/controllers/application_controller.rb +46 -0
- data/microservices/CFDP/app/controllers/cfdp_controller.rb +222 -0
- data/microservices/CFDP/app/models/cfdp_checksum.rb +52 -0
- data/microservices/CFDP/app/models/cfdp_crc_checksum.rb +41 -0
- data/microservices/CFDP/app/models/cfdp_mib.rb +613 -0
- data/microservices/CFDP/app/models/cfdp_model.rb +25 -0
- data/microservices/CFDP/app/models/cfdp_null_checksum.rb +29 -0
- data/microservices/CFDP/app/models/cfdp_pdu.rb +202 -0
- data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +590 -0
- data/microservices/CFDP/app/models/cfdp_source_transaction.rb +449 -0
- data/microservices/CFDP/app/models/cfdp_topic.rb +58 -0
- data/microservices/CFDP/app/models/cfdp_transaction.rb +188 -0
- data/microservices/CFDP/app/models/cfdp_user.rb +601 -0
- data/microservices/CFDP/bin/rails +4 -0
- data/microservices/CFDP/bin/rake +4 -0
- data/microservices/CFDP/bin/setup +25 -0
- data/microservices/CFDP/config/application.rb +55 -0
- data/microservices/CFDP/config/boot.rb +4 -0
- data/microservices/CFDP/config/credentials.yml.enc +1 -0
- data/microservices/CFDP/config/environment.rb +5 -0
- data/microservices/CFDP/config/environments/development.rb +53 -0
- data/microservices/CFDP/config/environments/production.rb +75 -0
- data/microservices/CFDP/config/environments/test.rb +50 -0
- data/microservices/CFDP/config/initializers/application_controller_renderer.rb +8 -0
- data/microservices/CFDP/config/initializers/backtrace_silencers.rb +7 -0
- data/microservices/CFDP/config/initializers/cfdp_initializer.rb +26 -0
- data/microservices/CFDP/config/initializers/cors.rb +16 -0
- data/microservices/CFDP/config/initializers/filter_parameter_logging.rb +8 -0
- data/microservices/CFDP/config/initializers/inflections.rb +16 -0
- data/microservices/CFDP/config/initializers/mime_types.rb +4 -0
- data/microservices/CFDP/config/initializers/wrap_parameters.rb +9 -0
- data/microservices/CFDP/config/locales/en.yml +29 -0
- data/microservices/CFDP/config/puma.rb +38 -0
- data/microservices/CFDP/config/routes.rb +16 -0
- data/microservices/CFDP/config.ru +5 -0
- data/microservices/CFDP/init.sh +9 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_ack.rb +82 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_enum.rb +237 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_eof.rb +87 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_file_data.rb +98 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_finished.rb +114 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_keep_alive.rb +65 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_metadata.rb +116 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_nak.rb +91 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_prompt.rb +60 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_tlv.rb +291 -0
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_user_ops.rb +749 -0
- data/microservices/CFDP/public/robots.txt +1 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_ack_spec.rb +114 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_eof_spec.rb +159 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_file_data_spec.rb +76 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_finished_spec.rb +192 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_keep_alive_spec.rb +69 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_metadata_spec.rb +346 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_nak_spec.rb +126 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_prompt_spec.rb +94 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_spec.rb +111 -0
- data/microservices/CFDP/spec/rails_helper.rb +71 -0
- data/microservices/CFDP/spec/requests/cfdp_spec.rb +1965 -0
- data/microservices/CFDP/spec/spec_helper.rb +200 -0
- data/plugin.txt +67 -0
- data/targets/CFDPTEST/cmd_tlm/cmd.txt +5 -0
- data/targets/CFDPTEST/cmd_tlm/tlm.txt +4 -0
- data/targets/CFDPTEST/procedures/cfdp_test_suite.rb +130 -0
- data/targets/CFDPTEST/target.txt +4 -0
- 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
|