cosmos-ccsds_transfer_frames 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ # Cosmos::CcsdsTransferFrames
2
+
3
+ This gem contains a CCSDS transfer frame protocol for use with the Ball Aerospace COSMOS application.
4
+
5
+ The protocol extracts CCSDS space packets from CCSDS transfer frames, optionally prefixing each packet with the transfer frame headers of the frame where it started.
6
+
7
+ ## Installation
8
+
9
+ This gem is intended to be installed as a 'gem based target/tool' (see http://cosmosrb.com/docs/gemtargets/) in COSMOS and made available for use as a normal protocol in a target command and telemetry server configuration. Note that the protocol provided in this gem is neither a target nor a tool in the COSMOS sense.
10
+
11
+ In order to make this protocol available for use in your COSMOS targets, add this line to the Gemfile of your COSMOS project:
12
+
13
+ ```ruby
14
+ gem 'cosmos-ccsds_transfer_frames'
15
+ ```
16
+
17
+ and then execute
18
+
19
+ ```sh
20
+ $ bundle
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ In order to use this protocol in your COSMOS target first make sure to add the following to the `target.txt` file for your target:
26
+
27
+ ```
28
+ REQUIRE cosmos/ccsds_transfer_frames
29
+ ```
30
+
31
+ Then add the protocol in the command and telemetry server configuration in an interface definition as usual. The full explicit module namespace is necessary, for example:
32
+
33
+ ```
34
+ INTERFACE INTERFACE_NAME tcpip_client_interface.rb localhost 12345 12345 10.0 nil
35
+ PROTOCOL Cosmos::CcsdsTransferFrames::CcsdsTranferFrameProtocol 1115 0 true true
36
+ TARGET TARGET_NAME
37
+ ```
38
+
39
+ This would set up the protocol to expect transfer frames with:
40
+
41
+ * A total size of 1115 bytes.
42
+ * No secondary header.
43
+ * Operational control field.
44
+ * Frame error control field.
45
+ * No prefixing of packets (default).
46
+ * Discarding of idle packets (default).
47
+
48
+ For detailed information about the available configuration parameters for the protocol, please consult the yard inline source code documentation in `lib/cosmos/ccsds_transfer_frames/ccsds_transfer_frame_protocol.rb`
49
+
50
+ ## Development
51
+
52
+ [![Build Status](https://travis-ci.org/ienorand/cosmos-ccsds_transfer_frames.svg?branch=master)](https://travis-ci.org/ienorand/cosmos-ccsds_transfer_frames)
53
+
54
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
55
+
56
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in the gemspec file, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
57
+
58
+ ## Contributing
59
+
60
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ienorand/cosmos-ccsds_transfer_frames. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
61
+
62
+ ## License
63
+
64
+ This program is free software: you can redistribute it and/or modify
65
+ it under the terms of the GNU General Public License as published by
66
+ the Free Software Foundation, either version 3 of the License, or
67
+ (at your option) any later version.
68
+
69
+ This program is distributed in the hope that it will be useful,
70
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
71
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
72
+ GNU General Public License for more details.
73
+
74
+ You should have received a copy of the GNU General Public License
75
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
76
+
77
+ A copy of the GPLv3.0 is provided in [LICENSE.txt](LICENSE.txt).
78
+
79
+ ## Code of Conduct
80
+
81
+ Everyone interacting in the Cosmos::CcsdsTransferFrames project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ienorand/cosmos-ccsds_transfer_frames/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,35 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "cosmos/ccsds_transfer_frames/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cosmos-ccsds_transfer_frames"
8
+ spec.version = Cosmos::CcsdsTransferFrames::VERSION
9
+ spec.authors = ["Martin Erik Werner"]
10
+ spec.email = ["martinerikwerner@gmail.com"]
11
+
12
+ spec.summary = "CCSDS transfer frame protocol for use in COSMOS"
13
+ spec.description = <<-EOF
14
+ A Ball Aerospace COSMOS 'tool' gem which provides a read-only protocol
15
+ for extracting CCSDS space packets from CCSDS transfer frames, optionally
16
+ prefixing each packet, with the transfer frame headers of the frame where
17
+ it started.
18
+ EOF
19
+ spec.homepage = "https://github.com/ienorand/cosmos-ccsds_transfer_frames"
20
+ spec.license = "GPL-3.0"
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "cosmos", "~> 4"
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.16"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ spec.add_development_dependency "ruby-termios", "~> 0.9"
35
+ end
@@ -0,0 +1,2 @@
1
+ require "cosmos/ccsds_transfer_frames/ccsds_transfer_frame_protocol"
2
+ require "cosmos/ccsds_transfer_frames/version.rb"
@@ -0,0 +1,316 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2018 Fredrik Persson <u.fredrik.persson@gmail.com>
4
+ # Martin Erik Werner <martinerikwerner@gmail.com>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
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. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ require 'cosmos/config/config_parser'
20
+ require 'cosmos/packets/binary_accessor'
21
+ require 'cosmos/interfaces/protocols/protocol'
22
+ require 'thread'
23
+
24
+ module Cosmos
25
+ module CcsdsTransferFrames
26
+ # Given a stream of ccsds transfer frames, extract ccsds space packets based
27
+ # on the first header pointer and packet lengths.
28
+ #
29
+ # Only read is supported.
30
+ class CcsdsTransferFrameProtocol < Protocol
31
+ FRAME_PRIMARY_HEADER_LENGTH = 6
32
+ FIRST_HEADER_POINTER_OFFSET = 4
33
+ # last 11 bits
34
+ FIRST_HEADER_POINTER_MASK = [0b00000111, 0b11111111]
35
+ IDLE_FRAME_FIRST_HEADER_POINTER = 0b11111111110
36
+ NO_PACKET_START_FIRST_HEADER_POINTER = 0b11111111111
37
+ FRAME_VIRTUAL_CHANNEL_BIT_OFFSET = 12
38
+ FRAME_VIRTUAL_CHANNEL_BITS = 3
39
+ VIRTUAL_CHANNEL_COUNT = 8
40
+ FRAME_OPERATIONAL_CONTROL_FIELD_LENGTH = 4
41
+ FRAME_ERROR_CONTROL_FIELD_LENGTH = 2
42
+ SPACE_PACKET_HEADER_LENGTH = 6
43
+ SPACE_PACKET_LENGTH_BIT_OFFSET = 4 * 8
44
+ SPACE_PACKET_LENGTH_BITS = 2 * 8
45
+ SPACE_PACKET_APID_BITS = 14
46
+ SPACE_PACKET_APID_BIT_OFFSET = 2 * 8 - SPACE_PACKET_APID_BITS
47
+ IDLE_PACKET_APID = 0b11111111111111
48
+
49
+ # @param transfer_frame_length [Integer] Length of transfer frame in bytes
50
+ # @param transfer_frame_secondary_header_length [Integer] Length of
51
+ # transfer frame secondary header in bytes
52
+ # @param transfer_frame_has_operational_control_field [Boolean] Flag
53
+ # indicating if the transfer frame operational control field is
54
+ # present or not
55
+ # @param transfer_frame_has_frame_error_control_field [Boolean] Flag
56
+ # indicating if the transfer frame error control field is present or
57
+ # not
58
+ # @param prefix_packets [Boolean] Flag indicating if each space packet should
59
+ # be prefixed with the transfer frame headers from the frame where
60
+ # it started.
61
+ # @param include_idle_packets [Boolean] Flag indicating if idle packets
62
+ # should be included or discarded.
63
+ # @param allow_empty_data [true/false/nil] See Protocol#initialize
64
+ def initialize(
65
+ transfer_frame_length,
66
+ transfer_frame_secondary_header_length,
67
+ transfer_frame_has_operational_control_field,
68
+ transfer_frame_has_frame_error_control_field,
69
+ prefix_packets = false,
70
+ include_idle_packets = false,
71
+ allow_empty_data = nil)
72
+ super(allow_empty_data)
73
+
74
+ @frame_length = Integer(transfer_frame_length)
75
+
76
+ @frame_headers_length = FRAME_PRIMARY_HEADER_LENGTH + Integer(transfer_frame_secondary_header_length)
77
+
78
+ @frame_trailer_length = 0
79
+ has_ocf = ConfigParser.handle_true_false(transfer_frame_has_operational_control_field)
80
+ @frame_trailer_length += FRAME_OPERATIONAL_CONTROL_FIELD_LENGTH if has_ocf
81
+ has_fecf = ConfigParser.handle_true_false(transfer_frame_has_frame_error_control_field)
82
+ @frame_trailer_length += FRAME_ERROR_CONTROL_FIELD_LENGTH if has_fecf
83
+
84
+ @frame_data_field_length = @frame_length - @frame_headers_length - @frame_trailer_length
85
+
86
+ @packet_prefix_length = 0
87
+ @prefix_packets = ConfigParser.handle_true_false(prefix_packets)
88
+ @packet_prefix_length += @frame_headers_length if @prefix_packets
89
+
90
+ @include_idle_packets = ConfigParser.handle_true_false(include_idle_packets)
91
+ end
92
+
93
+ def reset
94
+ super()
95
+ @data = ''
96
+ @virtual_channels = Array.new(VIRTUAL_CHANNEL_COUNT) { VirtualChannel.new }
97
+ end
98
+
99
+ def read_data(data)
100
+ @data << data
101
+
102
+ if (@data.length >= @frame_length)
103
+ frame = @data.slice!(0, @frame_length)
104
+ process_frame(frame)
105
+ end
106
+
107
+ packet_data = get_packet()
108
+
109
+ # Potentially allow blank string to be sent to other protocols if no
110
+ # packet is ready in this one
111
+ if (Symbol === packet_data && packet_data == :STOP && data.length <= 0)
112
+ return super(data)
113
+ end
114
+
115
+ return packet_data
116
+ end
117
+
118
+ private
119
+
120
+ VirtualChannel = Struct.new(:packet_queue, :pending_incomplete_packet_bytes_left) do
121
+ def initialize(packet_queue: [], pending_incomplete_packet_bytes_left: 0)
122
+ super(packet_queue, pending_incomplete_packet_bytes_left)
123
+ end
124
+ end
125
+
126
+ # Get a packet from the virtual channel packet queues of stored packets
127
+ # from processed frames.
128
+ #
129
+ # If idle packets are not included, extracted idle packets are discarded
130
+ # and extraction is retried until a non-idle packet is found or no more
131
+ # complete packets are left in any of the virtual channel packet queues.
132
+ #
133
+ # @return [String] Packet data, if the queues contained at least one
134
+ # complete packet.
135
+ # @return [Symbol] :STOP, if the queues does not contain any complete
136
+ # packets.
137
+ def get_packet
138
+ @virtual_channels.each do |vc|
139
+ loop do
140
+ # Skip if there's only a single incomplete packet in the queue.
141
+ break if (vc.packet_queue.length == 1 &&
142
+ vc.pending_incomplete_packet_bytes_left > 0)
143
+
144
+ packet_data = vc.packet_queue.shift
145
+
146
+ break if packet_data.nil?
147
+
148
+ return packet_data if @include_idle_packets
149
+
150
+ apid = get_space_packet_apid(packet_data[@packet_prefix_length, SPACE_PACKET_HEADER_LENGTH])
151
+ return packet_data unless (apid == IDLE_PACKET_APID)
152
+ end
153
+ end
154
+ # If the packet queues contains any more whole packets they will be
155
+ # handled in subsequent calls to this method. Cosmos will ensure that
156
+ # read_data() is called until it returns :STOP, which allows us to
157
+ # clear all whole packets.
158
+
159
+ # no complete packet for any virtual channel
160
+ return :STOP
161
+ end
162
+
163
+ # Extract packets from a transfer frame and store them in the packet queue.
164
+ #
165
+ # First handles packet continuation of any incomplete packet and then
166
+ # handles the rest of the packets in the frame.
167
+ #
168
+ # @param frame [String] Transfer frame data.
169
+ def process_frame(frame)
170
+ first_header_pointer =
171
+ ((frame.bytes[FIRST_HEADER_POINTER_OFFSET] & FIRST_HEADER_POINTER_MASK[0]) << 8) |
172
+ (frame.bytes[FIRST_HEADER_POINTER_OFFSET + 1] & FIRST_HEADER_POINTER_MASK[1])
173
+
174
+ return if (first_header_pointer == IDLE_FRAME_FIRST_HEADER_POINTER)
175
+
176
+ virtual_channel = BinaryAccessor.read(
177
+ FRAME_VIRTUAL_CHANNEL_BIT_OFFSET,
178
+ FRAME_VIRTUAL_CHANNEL_BITS,
179
+ :UINT,
180
+ frame,
181
+ :BIG_ENDIAN)
182
+
183
+ frame_data_field = frame[@frame_headers_length, @frame_data_field_length]
184
+
185
+ status = handle_packet_continuation(virtual_channel, frame_data_field, first_header_pointer)
186
+ return if (Symbol === status && status == :STOP)
187
+
188
+ if (frame_data_field.length == @frame_data_field_length)
189
+ # No continuation packet was completed, and a packet starts in this
190
+ # frame. Utilise the first header pointer to re-sync to a packet start.
191
+ frame_data_field.replace(frame_data_field[first_header_pointer..-1])
192
+ end
193
+
194
+ frame_headers = frame[0, @frame_headers_length].clone
195
+ store_packets(virtual_channel, frame_headers, frame_data_field)
196
+ end
197
+
198
+ # Handle packet continuation when processing a transfer frame.
199
+ #
200
+ # Ensures that any incomplete packet first has enough data for the packet
201
+ # header to determine its length and then ensures that it has enough data
202
+ # to be complete based on its length.
203
+ #
204
+ # @param virtual_channel [Int] Transfer frame virtual channel.
205
+ # @param frame_data_field [String] Transfer frame data field.
206
+ # @param first_header_pointer [Int] First header pointer value.
207
+ def handle_packet_continuation(virtual_channel, frame_data_field, first_header_pointer)
208
+ vc = @virtual_channels[virtual_channel]
209
+ if (vc.packet_queue.length > 0 &&
210
+ vc.packet_queue[-1].length < @packet_prefix_length + SPACE_PACKET_HEADER_LENGTH)
211
+ # pending incomplete packet does not have header yet
212
+ rest_of_packet_header_length = @packet_prefix_length + SPACE_PACKET_HEADER_LENGTH - vc.packet_queue[-1].length
213
+ vc.packet_queue[-1] << frame_data_field.slice!(0, rest_of_packet_header_length)
214
+
215
+ space_packet_length = get_space_packet_length(vc.packet_queue[-1][@packet_prefix_length..-1])
216
+ throw "failed to get space packet length" if Symbol === space_packet_length && space_packet_length == :STOP
217
+
218
+ vc.pending_incomplete_packet_bytes_left = space_packet_length - SPACE_PACKET_HEADER_LENGTH
219
+ end
220
+
221
+ if (vc.pending_incomplete_packet_bytes_left >= frame_data_field.length)
222
+ # continuation of a packet
223
+ vc.packet_queue[-1] << frame_data_field
224
+ vc.pending_incomplete_packet_bytes_left -= frame_data_field.length
225
+ return :STOP
226
+ end
227
+
228
+ if (first_header_pointer == NO_PACKET_START_FIRST_HEADER_POINTER)
229
+ # This was not a continuation of a packet (or it was a continuation of
230
+ # an ignored idle packet), wait for another frame to find a packet
231
+ # start.
232
+ return :STOP
233
+ end
234
+
235
+ if (vc.pending_incomplete_packet_bytes_left > 0)
236
+ rest_of_packet = frame_data_field.slice!(0, vc.pending_incomplete_packet_bytes_left)
237
+ vc.packet_queue[-1] << rest_of_packet
238
+ vc.pending_incomplete_packet_bytes_left = 0
239
+ end
240
+ end
241
+
242
+ # Extract all packets from the remaining frame data field, and store them
243
+ # in the packet queue.
244
+ #
245
+ # It is assumed that packet continuation data from any previously
246
+ # unfinished packets has been removed from the frame data field prior, and
247
+ # hence that the given remaining frame data field starts at a space packet
248
+ # header.
249
+ #
250
+ # Handles both complete packets and unfinished packets which will be
251
+ # finished in a later frame via handle_packet_continuation().
252
+ #
253
+ # @param virtual_channel [Int] Transfer frame virtual channel.
254
+ # @param frame_headers [String] Transfer frame headers, only used if prefixing packets.
255
+ # @param frame_data_field [String] (Remaining) transfer frame data field.
256
+ def store_packets(virtual_channel, frame_headers, frame_data_field)
257
+ vc = @virtual_channels[virtual_channel]
258
+ while (frame_data_field.length > 0) do
259
+ if (@prefix_packets)
260
+ vc.packet_queue << frame_headers.clone
261
+ else
262
+ vc.packet_queue << ""
263
+ end
264
+
265
+ if (frame_data_field.length < SPACE_PACKET_HEADER_LENGTH)
266
+ vc.packet_queue[-1] << frame_data_field
267
+ vc.pending_incomplete_packet_bytes_left = SPACE_PACKET_HEADER_LENGTH - frame_data_field.length
268
+ return
269
+ end
270
+
271
+ space_packet_length = get_space_packet_length(frame_data_field)
272
+ throw "failed to get space packet length" if Symbol === space_packet_length && space_packet_length == :STOP
273
+
274
+ if (space_packet_length > frame_data_field.length)
275
+ vc.packet_queue[-1] << frame_data_field
276
+ vc.pending_incomplete_packet_bytes_left = space_packet_length - frame_data_field.length
277
+ return
278
+ end
279
+
280
+ vc.packet_queue[-1] << frame_data_field.slice!(0, space_packet_length)
281
+ end
282
+ end
283
+
284
+ def get_space_packet_length(space_packet)
285
+ # signal more data needed if we do not have enough to determine the
286
+ # length of the space packet
287
+ return :STOP if (space_packet.length < SPACE_PACKET_HEADER_LENGTH)
288
+
289
+ # actual length in ccsds space packet is stored value plus one
290
+ space_packet_data_field_length = BinaryAccessor.read(
291
+ SPACE_PACKET_LENGTH_BIT_OFFSET,
292
+ SPACE_PACKET_LENGTH_BITS,
293
+ :UINT,
294
+ space_packet,
295
+ :BIG_ENDIAN) + 1
296
+ space_packet_length = SPACE_PACKET_HEADER_LENGTH + space_packet_data_field_length
297
+ return space_packet_length
298
+ end
299
+
300
+ def get_space_packet_apid(space_packet)
301
+ # signal more data needed if we do not have enough of the header to
302
+ # determine the apid of the space packet
303
+ return :STOP if (space_packet.length < (SPACE_PACKET_APID_BIT_OFFSET + SPACE_PACKET_APID_BITS) / 8)
304
+
305
+ # actual length in ccsds space packet is stored value plus one
306
+ space_packet_apid = BinaryAccessor.read(
307
+ SPACE_PACKET_APID_BIT_OFFSET,
308
+ SPACE_PACKET_APID_BITS,
309
+ :UINT,
310
+ space_packet,
311
+ :BIG_ENDIAN)
312
+ return space_packet_apid
313
+ end
314
+ end
315
+ end
316
+ end