cosmos-ccsds_transfer_frames 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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