Protocols
Protocols process data on behalf of an Interface. They can modify the data being written, data being read, or both. Protocols can also mark a packet as stored instead of real-time which means COSMOS will not update the current value table with the packet data. Protocols can be layered and will be processed in order. For example, if you have a low-level encryption layer that must be first removed before processing a higher level buffer length protocol.
Read protocols execute in the order specified (first specified runs first). Write protocols execute in the reverse order (last specified executes first).
This ordering is critical when combining packet delineation protocols (e.g. Length, Terminated) with helper protocols (e.g. CRC, Ignore). Delineation protocols must run before helper protocols on reads so they can assemble complete packets first. On writes, delineation protocols must run after helper protocols so fields like the length are filled in before the CRC is calculated. Since write protocols execute in reverse order, the helper protocol must be listed before the delineation protocol for writes.
To satisfy both directions, you may need to define a helper protocol twice — once for WRITE (before the delineation protocol) and once for READ (after it). For example, with a Length protocol and CRC protocol:
PROTOCOL WRITE CrcProtocol ...
PROTOCOL READ_WRITE LengthProtocol ...
PROTOCOL READ CrcProtocol ...
On read: Length runs first (assembles a complete packet from the byte stream), then CRC runs (verifies the CRC over the complete packet). On write: CRC is listed before Length, so since write order is reversed, Length runs first (fills in the length field) and then CRC runs (calculates the CRC over the final packet including the correct length).
Protocols are typically used to define the logic to delineate packets and manipulate data as it written to and read from Interfaces. COSMOS includes Interfaces for TCP/IP Client, TCP/IP Server, Udp Client / Server, and Serial connections. For 99% of use cases these Interfaces should not require any changes as they universally handle the low-level details of reading and writing from these types of connections. All unique behavior should now be defined in Protocols.
At a minimum, any byte stream based Interface will require a Protocol to delineate packets. TCP/IP and Serial are examples of byte stream based Interfaces. A byte stream is just a simple stream of bytes and thus you need some way to know where packets begin and end within the stream.
TCP/IP is a friendly byte stream. Unless you are dealing with a very poorly written system, the first byte received on a TCP/IP connection will always be the start of a packet. Also, TCP/IP is a reliable connection in that it ensures that all data is received in the correct order, that no data is lost, and that the data is not corrupted (TCP/IP is protected by a CRC32 which is pretty good for avoiding unrecognized data corruption).
Serial is a much less friendly byte stream. With serial connections, it is very likely that when you open a serial port and start receiving data you will receive the middle of a message. (This problem is only avoided when interfacing with a system that only writes to the serial port in response to a command). For this reason, sync patterns are highly beneficial for serial interfaces. Additionally, serial interfaces may use some method to protect against unrecognized data corruption (Checksums, CRCs, etc.)
UDP is an inherently packet based connection. If you read from a UDP socket, you will always receive back an entire packet. The best UDP based Protocols take advantage of this fact. Some implementations try to make UDP act like a byte stream, but this is a misuse of the protocol because it is highly likely that you will lose data and have no way to recover.
For more information about how Protocols fit with Interfaces and Accessors see Interoperability Without Standards.
Packet Delineation Protocols
COSMOS provides the following packet delineation protocols: COBS, SLIP, Burst, Fixed, Length, Terminated and Preidentified. Each of these protocols has the primary purpose of separating out packets from a byte stream.
COSMOS Enterprise provides the GEMS packet delineation protocol as well as the CCSDS TCTF, CLTU, TMTF, USLP and AOS framing protocols. It also provides COP-1 (a reliability layer on top of TC or USLP framing) and SDLS (a security engine used by the framing protocols), which do not delineate packets themselves. See CCSDS Protocols for details.
Note that all protocols take a final parameter called "Allow Empty Data". This indicates whether the protocol will allow an empty string to be passed down to later Protocols (instead of returning :STOP). Can be true, false, or nil, where nil is interpreted as true unless the Protocol is the last Protocol of the chain. End users of a protocol will almost always simply leave off this parameter. For more information read the Custom Protocols documentation.
Note the first parameter after the PROTOCOL keyword is how to apply the protocol: READ, WRITE, or READ_WRITE. Read applies the protocol on incoming packets (telemetry) and write on outgoing packets (commands). The next parameter is the protocol filename or class name. All other parameters are protocol specific.
Maximum Buffer Size
Protocols which delineate packets have to accumulate bytes until a complete packet has arrived. The Length and Preidentified protocols buffer until the number of bytes named by a length field on the wire has been received; the Terminated protocol buffers until it sees the termination characters. In all three cases the peer on the other end controls how much COSMOS buffers, so a peer which declares an enormous length, or never sends a terminator, can drive an interface microservice to memory exhaustion.
COSMOS bounds this with a maximum buffer size, which defaults to 100,000,000 bytes (100 MB). This is far larger than any realistic packet, so it does not affect normal configurations. When the limit is hit the interface logs an error and disconnects, which drops the offending peer and clears the buffer. The Length and Preidentified protocols additionally reject a declared length larger than the limit immediately, without buffering the declared number of bytes first.
Set the OPENC3_PROTOCOL_MAX_BUFFER_SIZE environment variable to change the limit for all protocols. Note that the Length and Preidentified protocols also take a Max Length parameter which bounds the value of the length field itself. Max Length is checked before the buffer size and gives a tighter, per-interface bound, so it is worth setting on those protocols in addition to the global limit.
COBS Protocol
The Consistent Overhead Byte Stuffing (COBS) Protocol is an algorithm for encoding data bytes that results in efficient, reliable, unambiguous packet framing regardless of packet content, thus making it easy for receiving applications to recover from malformed packets. It employs the zero byte value to serve as a packet delimiter (a special value that indicates the boundary between packets). The algorithm replaces each zero data byte with a non-zero value so that no zero data bytes will appear in the packet and thus be misinterpreted as packet boundaries (See https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing for more).
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ openc3/interfaces/protocols/cobs_protocol.py
Source code for cobs_protocol.py
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ CobsProtocol
Source code for cobs_protocol.rb
SLIP Protocol
The Serial Line IP (SLIP) Protocol defines a sequence of characters that frame IP packets on a serial line. It defines two special characters: END and ESC. END is 0xC0 and ESC is 0xDB. To send a packet, a SLIP host simply starts sending the data in the packet. If a data byte is the same code as END character, a two byte sequence of ESC and 0xDC is sent instead. If a data bytes is the same as an ESC character, an two byte sequence of ESC and 0xDD is sent instead. When the last byte in the packet has been sent, an END character is then transmitted (See https://datatracker.ietf.org/doc/html/rfc1055 for more).
| Parameter | Description | Required | Default |
|---|---|---|---|
| Start Char | Character to place at the start of frames | No | nil (no character) |
| Read Strip Characters | Strip off start_char and end_char from reads | No | true |
| Read Enable Escaping | Whether to enable character escaping on reads | No | true |
| Write Enable Escaping | Whether to enable character escaping on writes | No | true |
| End Char | Character to place at the end of frames | No | 0xC0 |
| Esc Char | Escape character | No | 0xDB |
| Escape End Char | Character to escape End character | No | 0xDC |
| Escape Esc Char | Character to escape Esc character | No | 0xDD |
- Python
- Ruby
Usage in plugin.txt:
INTERFACE INTERFACE_NAME <parameters>
# Since we're using the defaults, none of the parameters are actually required
PROTOCOL READ openc3/interfaces/protocols/slip_protocol.py None True True True 0xC0 0xDB 0xDC 0xDD
Source code for slip_protocol.py
Usage in plugin.txt:
INTERFACE INTERFACE_NAME <parameters>
# Since we're using the defaults, none of the parameters are actually required
PROTOCOL READ SlipProtocol nil true true true 0xC0 0xDB 0xDC 0xDD
Source code for slip_protocol.rb
Burst Protocol
The Burst Protocol simply reads as much data as it can from the interface before returning the data as a COSMOS Packet (It returns a packet for each burst of data read). This Protocol relies on regular bursts of data delimited by time and thus is not very robust. However, it can utilize a sync pattern which does allow it to re-sync if necessary. It can also discard bytes from the incoming data to remove the sync pattern. Finally, it can add sync patterns to data being written out of the Interface.
Note that Burst Protocol is the base class for many of the other protocols.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Discard Leading Bytes | The number of bytes to discard from the binary data after reading. Note that this applies to bytes starting with the sync pattern if the sync pattern is being used. | No | 0 (do not discard bytes) |
| Sync Pattern | Hex string representing a byte pattern that will be searched for in the raw data. This pattern represents a packet delimiter and all data found including the sync pattern will be returned | No | nil (no sync pattern) |
| Fill Fields | Whether to fill in the sync pattern on outgoing packets | No | false |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
# Use a sync pattern but discard it
PROTOCOL READ openc3/interfaces/protocols/burst_protocol.py 4 0x1ACFFC1D
Source code for burst_protocol.py
INTERFACE INTERFACE_NAME <parameters>
# Use a sync pattern but discard it
PROTOCOL READ BurstProtocol 4 0x1ACFFC1D
Source code for burst_protocol.rb
Fixed Protocol
The Fixed Protocol reads a preset minimum amount of data which is necessary to properly identify all the defined packets using the interface. It then identifies the packet and proceeds to read as much data from the interface as necessary to create the packet which it then returns. This protocol relies on all the packets on the interface being fixed in length. For example, all the packets using the interface are a fixed size and contain a simple header with a 32-bit sync pattern followed by a 16 bit ID. The Fixed Protocol would elegantly handle this case with a minimum read size of 6 bytes. The Fixed Protocol also supports a sync pattern, discarding leading bytes, and filling the sync pattern similar to the Burst Protocol.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Minimum ID Size | The minimum number of bytes needed to identify a packet. All the packet definitions must declare their ID_ITEM(s) within this given number of bytes. | Yes | |
| Discard Leading Bytes | The number of bytes to discard from the binary data after reading. Note that this applies to bytes starting with the sync pattern if the sync pattern is being used. | No | 0 (do not discard bytes) |
| Sync Pattern | Hex string representing a byte pattern that will be searched for in the raw data. This pattern represents a packet delimiter and all data found including the sync pattern will be returned. | No | nil (no sync pattern) |
| Telemetry | Whether the data is telemetry | No | true (false means command) |
| Fill Fields | Whether to fill in the sync pattern on outgoing packets | No | false |
| Unknown Raise | Whether to raise an exception for an unknown packet | No | false |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ openc3/interfaces/protocols/fixed_protocol.py 6 4 0x1ACFFC1D True
Source code for fixed_protocol.py
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ FixedProtocol 6 4 0x1ACFFC1D true
Source code for fixed_protocol.rb
Length Protocol
The Length Protocol depends on a length field at a fixed location in the defined packets using the interface. It then reads enough data to grab the length field, decodes it, and reads the remaining length of the packet. For example, all the packets using the interface contain a CCSDS header with a length field. The Length Protocol can be set up to handle the length field and even the length offset CCSDS uses. The Length Protocol also supports a sync pattern, discarding leading bytes, and filling the length and sync pattern similar to the Burst Protocol.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Length Bit Offset | The bit offset from the start of the packet to the length field. Every packet using this interface must have the same structure such that the length field is the same size at the same location. Be sure to account for the length of the Sync Pattern in this value (if present). | No | 0 bits |
| Length Bit Size | The size in bits of the length field | No | 16 bits |
| Length Value Offset | The offset to apply to the length field value. The actual value of the length field plus this offset should equal the exact number of bytes required to read all data for the packet (including the length field itself, sync pattern, etc). For example, if the length field indicates packet length minus one, this value should be one. Be sure to account for the length of the Sync Pattern in this value (if present). | No | 0 |
| Bytes per Count | The number of bytes per each length field 'count'. This is used if the units of the length field is something other than bytes, e.g. if the length field count is in words. | No | 1 byte |
| Length Endianness | The endianness of the length field. Must be either 'BIG_ENDIAN' or 'LITTLE_ENDIAN'. | No | 'BIG_ENDIAN' |
| Discard Leading Bytes | The number of bytes to discard from the binary data after reading. Note that this applies to bytes including the sync pattern if the sync pattern is being used. Discarding is one of the very last steps so any size and offsets above need to account for all the data before discarding. | No | 0 (do not discard bytes) |
| Sync Pattern | Hex string representing a byte pattern that will be searched for in the raw data. This pattern represents a packet delimiter and all data found including the sync pattern will be returned. | No | nil (no sync pattern) |
| Max Length | The maximum allowed value in the length field. Strongly recommended: it is the only per-interface bound on how much data a peer can make COSMOS buffer. For a UDP-backed interface 65535 is a natural value since a UDP payload cannot exceed 65507 bytes. See Maximum Buffer Size. | No | nil (no maximum length) |
| Fill Length and Sync Pattern | Setting this flag to true causes the length field and sync pattern (if present) to be filled automatically on outgoing packets. | No | false |
The most confusing aspect of the Length Protocol is calculating the Length Value Offset. This is especially true in the commonly used CCSDS Space Packet Protocol. The best way to illustrate this is with an example. Suppose you have CCSDS Space Packets prepended with a Sync Pattern of 0x1ACFFC1D. This would look like the following:
| Sync (4 bytes) | Header (4 bytes) | Length (2 bytes) | Data (4 bytes) |
|---|---|---|---|
| 0x1ACFFC1D | 0x0001CADB | 0x0003 | 0xDEADBEEF |
In this case the total length of the packet is 14 bytes: 4 + 4 + 2 + 4 = 14. With 4 bytes of data, the length field is 3 because in CCSDS the length field is calculated as (data length - 1). So how would we calculate the Length Value Offset? COSMOS reads all the bytes in the packet (including the Sync Pattern) so the total length is 14 bytes. The length field is 3 so the Length Value Offset (offset to apply to the length field value) should be 11 (3 + 11 = 14).
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
# Example length protocol parameters for a CCSDS packet
PROTOCOL READ openc3/interfaces/protocols/length_protocol.py 64 16 11 1 BIG_ENDIAN 4 0x1ACFFC1D None True
Source code for length_protocol.py
INTERFACE INTERFACE_NAME <parameters>
# Example length protocol parameters for a CCSDS packet
PROTOCOL READ LengthProtocol 64 16 11 1 BIG_ENDIAN 4 0x1ACFFC1D nil true
Source code for length_protocol.rb
Terminated Protocol
The Terminated Protocol delineates packets using termination characters found at the end of every packet. It continuously reads data until the termination characters are found at which point it returns the packet data. For example, all the packets using the interface are followed by 0xABCD. This data can either be a part of each packet that is kept or something which is known only by the Terminated Protocol and simply thrown away.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Write Termination Characters | The data to write after writing a command packet. Given as a hex string such as 0xABCD. | Yes | |
| Read Termination Characters | The characters which delineate the end of a telemetry packet. Given as a hex string such as 0xABCD. | Yes | |
| Strip Read Termination | Whether to remove the read termination characters before returning the telemetry packet | No | true |
| Discard Leading Bytes | The number of bytes to discard from the binary data after reading. Note that this applies to bytes including the sync pattern if the sync pattern is being used. | No | 0 (do not discard bytes) |
| Sync Pattern | Hex string representing a byte pattern that will be searched for in the raw data. This pattern represents a packet delimiter and all data found including the sync pattern will be returned. | No | nil (no sync pattern) |
| Fill Fields | Whether to fill in the sync pattern on outgoing packets | No | false |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
# Example newline (0x0A) delimiter and sync pattern of 'DATA' (0x44415441 is ascii for 'DATA')
PROTOCOL READ openc3/interfaces/protocols/terminated_protocol.py 0x0A 0x0A True 4 0x44415441
Source code for terminated_protocol.py
INTERFACE INTERFACE_NAME <parameters>
# Example newline (0x0A) delimiter and sync pattern of 'DATA' (0x44415441 is ascii for 'DATA')
PROTOCOL READ TerminatedProtocol 0x0A 0x0A True 4 0x44415441
Source code for terminated_protocol.rb
GEMS Protocol (Enterprise)
The GEMS Protocol implements the Ground Equipment Monitoring Service protocol. It is added along with the Terminated Protocol which delineates packets using '|END'. The GEMS Interface is currently only implemented in Ruby.
The GEMS protocol doesn't take any parameters but should be added to an interface after the Terminated Protocol and Command Response Protocol.
- Ruby
INTERFACE GEMS_INT tcpip_client_interface.rb openc3-operator 8080 8080 10.0 nil nil
# TerminatedProtocol 0x7C454E44 0x7C454E44 false 0 0x7C47454D53 false ... means:
# wtc rtc strip discard sync fill
# where wtc = write termination characters, end of the gems protocol: 0x7C454E44 == '|END'
# rtc = read termination characters, end of the gems protocol: 0x7C454E44 == '|END'
# strip = strip read termination (false)
# discard = 0 bytes
# sync pattern = beginning of the GEMS protocol: 0x7C47454D53 == '|GEMS'
# fill = whether to fill in the sync pattern (false as we specify fill in our cmd/tlm definitions)
PROTOCOL READ TerminatedProtocol 0x7C454E44 0x7C454E44 false 0 0x7C47454D53 false
# CmdResponseProtocol 5.0 0.2 true means:
# 5 sec response timeout, 0.2 sec response polling,
# and true to raise exceptions when protocol errors occur
PROTOCOL READ_WRITE CmdResponseProtocol 5.0 0.2 true
PROTOCOL READ_WRITE GemsProtocol
Source code for gems_protocol.rb
For a full example, please see openc3-cosmos-gems-interface.
Preidentified Protocol
The Preidentified Protocol delineates packets using the COSMOS header. This Protocol was created to allow tools to connect and receive the entire packet stream. It can be used with the FileInterface to process COSMOS 4 log files. It can also be used to chain COSMOS instances together although that should rarely be needed with the new web native implementation.
Every target named in the incoming packet headers must be mapped to the interface with MAP_TARGET or MAP_TLM_TARGET. Packets for unmapped targets are dropped, and the interface logs a warning the first time it sees each unmapped packet.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Sync Pattern | Hex string representing a byte pattern that will be searched for in the raw data. This pattern represents a packet delimiter and all data found AFTER the sync pattern will be returned. The sync pattern itself is discarded. | No | nil (no sync pattern) |
| Max Length | The maximum allowed value in the length field. Strongly recommended, see Maximum Buffer Size. | No | nil (no maximum length) |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ openc3/interfaces/protocols/preidentified_protocol.py
Source code for preidentified_protocol.py
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ PreidentifiedProtocol
Source code for preidentified_protocol.rb
For an example of using Preidentified Protocol see FileInterface.
Helper Protocols
COSMOS provides the following helper protocols: Command Response, Crc and Ignore. These protocols provide helper functionality to Interfaces.
Command Response Protocol
The Command Response Protocol waits for a response for any commands with a defined response packet.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Response Timeout | Number of seconds to wait before timing out when waiting for a response | No | 5 |
| Response Polling Period | Number of seconds to wait between polling for a response | No | 0.02 |
| Raise Exceptions | Whether to raise exceptions when errors occur in the protocol like unexpected responses or response timeouts | No | false |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ openc3/interfaces/protocols/cmd_response_protocol.py 5.0 0.1 True
Source code for cmd_response_protocol.py
INTERFACE INTERFACE_NAME <parameters>
PROTOCOL READ CmdResponseProtocol 5.0 0.1 true
Source code for cmd_response_protocol.rb
Packet Definitions
The Command Response Protocol utilizes the RESPONSE keyword in the command definition to determine which telemetry packet should be expected when the given command is sent. This will block the command until the response is received.
COMMAND SCPI_PS GET_STATUS BIG_ENDIAN "Gets status"
ACCESSOR TemplateAccessor
TEMPLATE ":MEAS:VOLT? (@1:2)"
RESPONSE SCPI_PS STATUS
The Response packet (STATUS) should be defined to contain the response data.
TELEMETRY SCPI_PS STATUS BIG_ENDIAN "Status"
ACCESSOR TemplateAccessor
TEMPLATE "<MEAS_VOLTAGE_1>,<MEAS_VOLTAGE_2>"
APPEND_ITEM MEAS_VOLTAGE_1 32 FLOAT "Voltage Reading for Channel 1"
UNITS VOLTS V
FORMAT_STRING %0.3f
APPEND_ITEM MEAS_VOLTAGE_2 32 FLOAT "Voltage Reading for Channel 2"
UNITS VOLTS V
FORMAT_STRING %0.3f
For a full example, please see openc3-cosmos-scpi-power-supply.
CRC Protocol
The CRC protocol can add CRCs to outgoing commands and verify CRCs on incoming telemetry packets. Note: You either have to give all the parameters for Poly, Seed, Xor, Reflect or just use the defaults. You can't mix and match setting some and not others.
The CRC Protocol must run after packet delineation protocols (e.g. Length, Terminated) for both reads and writes. On reads, the delineation protocol must assemble a complete packet before the CRC can be verified — otherwise the CRC protocol may operate on partial or multiple packets. On writes, the delineation protocol must fill in fields like the length before the CRC is calculated — otherwise the CRC will be computed over incorrect data. Because read and write protocols execute in opposite orders, this typically requires defining the CRC protocol twice — once for WRITE before the delineation protocol, and once for READ after it. See "Protocol Run Order" for a full example.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Write Item Name | Item to fill with calculated CRC value for outgoing packets (nil = don't fill) | No | nil |
| Strip CRC | Whether to remove the CRC from incoming packets | No | false |
| Bad Strategy | How to handle CRC errors on incoming packets. ERROR = Just log the error, DISCONNECT = Disconnect interface | No | "ERROR" |
| Bit Offset | Bit offset of the CRC in the data. Can be negative to indicate distance from end of packet | No | -32 |
| Bit Size | Bit size of the CRC - Must be 16, 32, or 64 | No | 32 |
| Endianness | Endianness of the CRC (BIG_ENDIAN/LITTLE_ENDIAN) | No | "BIG_ENDIAN" |
| Poly | Polynomial to use when calculating the CRC expressed as an integer | No | nil (use default polynomial - 16-bit=0x1021, 32-bit=0x04C11DB7, 64-bit=0x42F0E1EBA9EA3693) |
| Seed | Seed value to start the calculation | No | nil (use default seed - 16-bit=0xFFFF, 32-bit=0xFFFFFFFF, 64-bit=0xFFFFFFFFFFFFFFFF) |
| Xor | Whether to XOR the CRC result with 0xFFFF | No | nil (use default value - 16-bit=false, 32-bit=true, 64-bit=true) |
| Reflect | Whether to bit reverse each byte of data before calculating the CRC | No | nil (use default value - 16-bit=false, 32-bit=true, 64-bit=true) |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
# Handle a trailing 32 bit CRC on telemetry by stripping it off
# In this example I changed the Reflect default to false so I had to explicitly specify all the values
PROTOCOL READ openc3/interfaces/protocols/crc_protocol.py None True ERROR -32 32 BIG_ENDIAN 0x04C11DB7 0xFFFFFFFF True False
Source code for crc_protocol.py
INTERFACE INTERFACE_NAME <parameters>
# Handle a trailing 32 bit CRC on telemetry by stripping it off
# In this example I changed the Reflect default to false so I had to explicitly specify all the values
PROTOCOL READ CrcProtocol nil true ERROR -32 32 BIG_ENDIAN 0x04C11DB7 0xFFFFFFFF true false
Source code for crc_protocol.rb
Ignore Packet Protocol
The Ignore Packet protocol drops specified command packets sent by COSMOS or drops incoming telemetry packets.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Target Name | Target name of the packet to ignore | Yes | nil |
| Packet Name | Packet name of the packet to ignore | Yes | nil |
- Python
- Ruby
INTERFACE INTERFACE_NAME <parameters>
# Ignore the INST MECH packet
PROTOCOL READ openc3/interfaces/protocols/ignore_packet_protocol.py INST MECH
Source code for ignore_packet_protocol.py
INTERFACE INTERFACE_NAME <parameters>
# Ignore the INST MECH packet
PROTOCOL READ IgnorePacketProtocol INST MECH
Source code for ignore_packet_protocol.rb
CCSDS Protocols (Enterprise)
COSMOS Enterprise implements the CCSDS Space Data Link protocols as Interface Protocols in the openc3-cosmos-ccsds-protocols plugin. To use them, install the openc3-cosmos-ccsds-protocols plugin in Admin - Plugins. The plugin repository and the source code links below require COSMOS Enterprise credentials.
Every protocol ships in both Ruby and Python with identical class names and parameters. In Ruby use the class name (e.g. CcsdsTmtfProtocol) and in Python use the filename (e.g. ccsds_tmtf_protocol.py) on the PROTOCOL line.
| Protocol | CCSDS Standard | Purpose | Direction |
|---|---|---|---|
| CCSDS TCTF | 232.0-B-3 | TC Transfer Frame (command uplink framing) | WRITE (+READ) |
| CCSDS CLTU | 231.0-B-4 | CLTU wrap (BCH codeblocks) around TC frames | WRITE |
| CCSDS TMTF | 132.0-B-2 | TM Transfer Frame (telemetry downlink deframing) | READ (+WRITE) |
| CCSDS USLP | 732.1-B-3 | Unified Space Data Link Protocol (uplink and downlink) | READ / WRITE |
| CCSDS AOS | 732.0-B-4 | AOS Transfer Frame (high-rate return link) | READ / WRITE |
| CCSDS COP-1 FOP | 232.1-B-2 | COP-1 FOP-1 (reliable AD service, ground sender) | READ_WRITE |
| CCSDS COP-1 FARM | 232.1-B-2 | COP-1 FARM-1 (spacecraft-side simulator) | READ_WRITE |
| SDLS | 355.0-B-2 | Space Data Link Security engine used by the framing protocols | Any framing |
| SLE Interfaces | 911.1 / 911.2 / 912.1 / 913.1 | SLE user: RAF / RCF return, FCLTU forward (Python only) | Interface |
Every parameter which can be written as a string is positional and can be given on a PROTOCOL line. Use nil (Ruby) or None (Python) to select the default for a parameter you want to skip over. The one exception is the SDLS engine which is an object and must be passed programmatically (see SDLS).
The classic ground station setup wraps a TCP/IP interface with TM deframing on READ and TC + CLTU framing on WRITE:
- Python
- Ruby
INTERFACE INST_INT tcpip_client_interface.py host.docker.internal 8080 8080 10.0 None None
PROTOCOL READ ccsds_tmtf_protocol.py 100
PROTOCOL WRITE ccsds_cltu_protocol.py
PROTOCOL WRITE ccsds_tctf_protocol.py True False 1 100
INTERFACE INST_INT tcpip_client_interface.rb host.docker.internal 8080 8080 10.0 nil nil
PROTOCOL READ CcsdsTmtfProtocol 100
PROTOCOL WRITE CcsdsCltuProtocol
PROTOCOL WRITE CcsdsTctfProtocol true false 1 100
Since write protocols execute in reverse order, CLTU is declared before TCTF so the TCTF protocol builds the transfer frame first and the CLTU protocol then wraps it.
CCSDS TCTF Protocol (Enterprise)
The CCSDS TCTF Protocol builds TC Transfer Frames (CCSDS 232.0-B-3) on the command uplink. It can also deframe TC frames on read (for example in a spacecraft simulator), returning VCID, SEQ_NUM, BYPASS, CTRL_CMD_FLAG and (if a MAP ID is configured) MAP_ID and SEQUENCE_FLAGS in the packet extra. Type-BC control frames can be built per command by setting BYPASS and CTRL_CMD_FLAG in the command extra.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Randomization | Whether to randomize the transfer frame | No | true |
| Error Control | Whether to add the Frame Error Control Field (16 bit CRC) | No | false |
| Bypass | Bypass flag where 0 is Type-A and 1 is Type-B (bypass frame acceptance checks) | No | 1 |
| SCID | Spacecraft Identifier (10 bits) | No | 0 |
| VCID | Virtual Channel Identifier (6 bits) | No | 0 |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
| MAP ID | Multiplexer Access Point ID (0-63). Enables the 1 byte Segment Header and MAP multiplexing | No | nil |
- Python
- Ruby
INTERFACE INTERFACE_NAME <params>
# TC uplink framing (SCID 100, VC 0, randomized, with FECF)
PROTOCOL WRITE ccsds_tctf_protocol.py True True 1 100 0
# Same, plus the MAP service on MAP ID 3
# PROTOCOL WRITE ccsds_tctf_protocol.py True True 1 100 0 None 3
Source code for ccsds_tctf_protocol.py
INTERFACE INTERFACE_NAME <params>
# TC uplink framing (SCID 100, VC 0, randomized, with FECF)
PROTOCOL WRITE CcsdsTctfProtocol true true 1 100 0
# Same, plus the MAP service on MAP ID 3
# PROTOCOL WRITE CcsdsTctfProtocol true true 1 100 0 nil 3
Source code for ccsds_tctf_protocol.rb
TC frames carry no sync marker so a desynchronized read stream cannot reliably re-anchor. A Frame Length too small to hold the configured fields is treated as a lost frame boundary: the byte is dropped and deframing retries. The CLTU protocol does not decode on read, so if you need true resynchronization decode the CLTUs externally (or use another delimiter that supports READ) ahead of it. Frames are assumed to carry a single MAP SDU (no MAP packet blocking or segmentation beyond the Segment Header sequence flags).
CCSDS CLTU Protocol (Enterprise)
The CCSDS CLTU Protocol wraps an already-built TC Transfer Frame in a Communications Link Transmission Unit (CCSDS 231.0-B-4): a start sequence, the frame split into BCH(63,56) codeblocks (7 data bytes + 1 parity byte each), and a tail sequence. It must be declared before the CCSDS TCTF Protocol on the WRITE chain so that, with write protocols running in reverse order, it wraps the finished frame. The defaults are the CCSDS standard values so it is normally used with no parameters. The CLTU is used for the forward link only; there is no CLTU decoding on read.
| Parameter | Description | Required | Default |
|---|---|---|---|
| Header | Start sequence before the BCH encoded data | No | 0xEB90 |
| Footer | Tail sequence after the BCH encoded data | No | 0xC5C5C5C5C5C5C579 |
| Fill Byte | BCH encoding fill byte | No | 0x55 |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
- Python
- Ruby
INTERFACE INTERFACE_NAME <params>
PROTOCOL WRITE ccsds_cltu_protocol.py
PROTOCOL WRITE ccsds_tctf_protocol.py
Source code for ccsds_cltu_protocol.py
INTERFACE INTERFACE_NAME <params>
PROTOCOL WRITE CcsdsCltuProtocol
PROTOCOL WRITE CcsdsTctfProtocol
Source code for ccsds_cltu_protocol.rb
CCSDS TMTF Protocol (Enterprise)
The CCSDS TMTF Protocol deframes TM Transfer Frames (CCSDS 132.0-B-2) on the telemetry downlink, extracting packets which span frames using the First Header Pointer. It adds VCID, MC_FRM_CNT and VC_FRM_CNT to the packet extra which will be included in the Decom data. If the frame has a Transfer Frame Secondary Header it is returned in SECONDARY_HEADER and if OCF is enabled the Operational Control Field is returned in OCF (used by COP-1). It can also build TM frames on write (for example in a spacecraft simulator) including the MC/VC frame counts, idle fill, and optional OCF, FECF and randomization.
| Parameter | Description | Required | Default |
|---|---|---|---|
| SCID | Spacecraft Identifier (10 bits) | Yes | |
| Frame Length | Length of the transfer frame in bytes (not including the sync pattern) | No | 2048 |
| Randomization | Whether the transfer frame was randomized | No | true |
| Discard Leading Bytes | The number of bytes to discard from the binary data after reading. Note that this applies to bytes including the sync pattern if the sync pattern is being used. | No | 0 (do not discard bytes) |
| Sync Pattern | Hex string representing a byte pattern that will be searched for in the raw data. This pattern represents a packet delimiter and all data found including the sync pattern will be returned. | No | 0x1ACFFC1D |
| Fill Fields | Whether to fill in the sync pattern on outgoing frames | No | true |
| Error Control | Whether the frame has a Frame Error Control Field (16 bit CRC) | No | false |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
| VCID | Virtual Channel Identifier (0-7) used when writing frames | No | 0 |
| Secondary Header | Transfer Frame Secondary Header data to emit when writing frames (max 63 bytes) | No | nil |
| Lenient | Whether to downgrade non-fatal header mismatches (Sync Flag, Packet Order, Segment Length ID) to warnings | No | false |
| OCF | Whether the frame has the 4 byte Operational Control Field (CLCW) | No | false |
- Python
- Ruby
INTERFACE INTERFACE_NAME <params>
# TM downlink deframing (SCID 100, 1115 byte frames, randomized)
PROTOCOL READ ccsds_tmtf_protocol.py 100 1115 True
# Same, but tolerate non-standard header bits
# PROTOCOL READ ccsds_tmtf_protocol.py 100 1115 True 0 0x1ACFFC1D True False None 0 None True
Source code for ccsds_tmtf_protocol.py
INTERFACE INTERFACE_NAME <params>
# TM downlink deframing (SCID 100, 1115 byte frames, randomized)
PROTOCOL READ CcsdsTmtfProtocol 100 1115 true
# Same, but tolerate non-standard header bits
# PROTOCOL READ CcsdsTmtfProtocol 100 1115 true 0 0x1ACFFC1D true false nil 0 nil true
Source code for ccsds_tmtf_protocol.rb
The Operational Control Field is treated as a CLCW (Type-1 report). A non-CLCW OCF (Type-2 / PLCW variants) is passed through as raw bytes but not interpreted.
Without a sync pattern there is nothing to resynchronize on, so a frame that fails its FECF or header checks is discarded whole and parsing continues at the next frame boundary. A gap in the Virtual Channel Frame Count means frames were lost, so any packet partially reassembled on that Virtual Channel is discarded rather than completed with unrelated bytes.
CCSDS USLP Protocol (Enterprise)
The CCSDS USLP Protocol implements the Unified Space Data Link Protocol (CCSDS 732.1-B-3). USLP uses a single Transfer Frame format for both the forward (command) and return (telemetry) links so the same protocol serves a READ or a WRITE chain. On read it returns VCID, MAP_ID, VC_FRM_CNT and, when present, INSERT_ZONE and OCF in the packet extra.
| Primary Header | [Insert Zone] | [Security Header] | TFDF | [Security Trailer] | [OCF] | [FECF] |
| Parameter | Description | Required | Default |
|---|---|---|---|
| SCID | Spacecraft Identifier (16 bits) | Yes | |
| Frame Length | Fixed frame length in bytes (not including the sync pattern). Leave nil for variable length frames which are delimited by reading the Frame Length field. | No | nil |
| Max Frame Length | Largest frame accepted on read when using the Frame Length field. Set this to bound how long a corrupt length stalls the reader. | No | 65536 |
| Randomization | Whether the transfer frame is randomized | No | false |
| Randomization Mode | Randomizer to use: TM or TC | No | TM |
| Sync Pattern | Hex string sync pattern. Set to nil to disable (e.g. when wrapped by CLTU). | No | 0x1ACFFC1D |
| Error Control | Whether the frame has a Frame Error Control Field (16 bit CRC) | No | false |
| Insert Zone Length | Length of the Insert Zone in bytes | No | 0 |
| OCF | Whether the frame has the 4 byte Operational Control Field (CLCW) | No | false |
| VC Count Length | Number of bytes in the VC Frame Count (0-7) | No | 1 |
| Source or Dest | Source or Destination Identifier flag | No | 0 |
| VCID | Virtual Channel Identifier | No | 0 |
| MAP ID | Multiplexer Access Point ID | No | 0 |
| Bypass | Bypass / Sequence Control flag where 0 is sequence controlled (Type-A) and 1 is expedited (Type-B) | No | 1 |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
- Python
- Ruby
INTERFACE INTERFACE_NAME tcpip_client_interface.py host.docker.internal 8080 8080 10.0 None None
PROTOCOL READ ccsds_uslp_protocol.py 100
PROTOCOL WRITE ccsds_uslp_protocol.py 100
Source code for ccsds_uslp_protocol.py
INTERFACE INTERFACE_NAME tcpip_client_interface.rb host.docker.internal 8080 8080 10.0 nil nil
PROTOCOL READ CcsdsUslpProtocol 100
PROTOCOL WRITE CcsdsUslpProtocol 100
Source code for ccsds_uslp_protocol.rb
The non-truncated Primary Header is supported along with TFDZ Construction Rules 000 (packets spanning frames, used with a fixed Frame Length) and 111 (no segmentation, used with variable length frames). Only Idle Data frames (VCID 63) are discarded on read but cannot be written. The truncated Primary Header, other TFDZ construction rules, and MAP multiplexing beyond a single MAP ID are not implemented.
Protocol Control Command (Type-BC) frames carry COP-1 control commands. Select one per command by setting PCC to 1 in the command extra (with BYPASS 1), or programmatically with the keyword-only pcc parameter. Such a frame is written under Construction Rule 111 with UPID 1 (COP-1 Control Commands), so it requires variable length frames: PCC 1 with a Frame Length set raises an error. It is never secured by SDLS. On read, the USLP protocol discards every Protocol Control Command frame and logs a warning, because its contents are protocol control information for the FARM rather than user data (CCSDS 732.1-B-3 4.1.2.8.2.2). On a COP-1 receiver the FARM-1 protocol handles these frames before they reach the deframer.
On read, a frame whose VC Frame Count length disagrees with the configured VC Count Length is rejected, and a VC Frame Count discontinuity discards any partial packet held for that Virtual Channel. Without a sync pattern there is nothing to resynchronize on, so a frame that fails its CRC or header checks is discarded whole.
CCSDS AOS Protocol (Enterprise)
The CCSDS AOS Protocol implements the AOS ("Version-2") Transfer Frame (CCSDS 732.0-B-4) used for high-rate return links. Like USLP the frame format is direction independent, so the same protocol serves a READ or a WRITE chain. AOS frames have no length field so the Frame Length is required. On read it returns VCID, REPLAY, VC_FRM_CNT and, when present, INSERT_ZONE and OCF in the packet extra. The Insert Zone and OCF are returned on every frame including Only Idle Data frames, so an AOS downlink can carry a CLCW for COP-1 even when no telemetry is queued.
| Primary Header (6 or 8) | [Insert Zone] | Data Field (M_PDU) | [OCF 4] | [FECF 2] |
| Parameter | Description | Required | Default |
|---|---|---|---|
| SCID | Spacecraft Identifier (8 bits) | Yes | |
| Frame Length | Fixed frame length in bytes (not including the sync pattern) | Yes | |
| Randomization | Whether the transfer frame is randomized | No | false |
| Sync Pattern | Hex string sync pattern. Set to nil to disable. | No | 0x1ACFFC1D |
| Error Control | Whether the frame has a Frame Error Control Field (16 bit CRC) | No | false |
| FHEC | Whether the primary header has the Frame Header Error Control field (Reed-Solomon (10,6) code) | No | false |
| Insert Zone Length | Length of the Insert Zone in bytes | No | 0 |
| OCF | Whether the frame has the 4 byte Operational Control Field (CLCW) | No | false |
| VCID | Virtual Channel Identifier used when writing frames. 63 writes Only Idle Data frames. | No | 0 |
| Replay Flag | Replay Flag used when writing frames | No | 0 |
| VC Count Cycle | Virtual Channel Frame Count Cycle used when writing frames | No | 0 |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
The Frame Length must exceed the overhead implied by the other parameters (primary header, M_PDU header, Insert Zone, OCF, FECF and any SDLS header and trailer) or the protocol raises an error on startup.
- Python
- Ruby
INTERFACE INTERFACE_NAME <params>
# AOS return link: 1024 byte frames, randomized, FECF + FHEC + OCF, VC 1
PROTOCOL READ ccsds_aos_protocol.py 0xAB 1024 True 0x1ACFFC1D True True 0 True 1
Source code for ccsds_aos_protocol.py
INTERFACE INTERFACE_NAME <params>
# AOS return link: 1024 byte frames, randomized, FECF + FHEC + OCF, VC 1
PROTOCOL READ CcsdsAosProtocol 0xAB 1024 true 0x1ACFFC1D true true 0 true 1
Source code for ccsds_aos_protocol.rb
The M_PDU (packet) service is implemented with the First Header Pointer, and packets may span frames in both directions: on write a packet that does not fit in one frame is split across as many frames as needed, and on read it is reassembled per Virtual Channel. The optional FHEC is generated on write and, on read, corrects up to two symbol errors in the primary header before the FECF is verified. Only Idle Data frames (VCID 63) are discarded on read. The B_PDU (bitstream) and VCA_SDU (Virtual Channel Access) services are not implemented.
CCSDS COP-1 Protocols (Enterprise)
The Communications Operation Procedure-1 (CCSDS 232.1-B-2) provides a reliable, retransmitting data link service on top of the TC or USLP transfer frames. It has two halves:
- FOP-1 (
CcsdsCop1FopProtocol) - the ground sender. It stamps each Type-AD frame's sequence number N(S), buffers it, retransmits on a timeout or when requested by the CLCW, and consumes the CLCW carried in the downlink Operational Control Field. - FARM-1 (
CcsdsCop1FarmProtocol) - the spacecraft receiver, for building a COP-1 aware simulator. It accepts in-sequence frames, discards duplicates and gaps, handles the Type-BC Unlock / Set V(R) control commands, and generates the CLCW.
Both work with either TC or USLP framing (Frame Type parameter). Because the CLCW arrives in the downlink frame's OCF, you must enable OCF on the return link TMTF or USLP protocol. The frame geometry parameters (Frame Length, Insert Zone Length, and the security lengths) must match the framing protocol on the same interface. On a secured link pass the same SDLS engine to both and the security geometry is taken from the engine.
FARM-1 validates each frame completely (FECF, Transfer Frame Version, SCID, VCID and, with an SDLS engine, authentication) before running FARM-1 acceptance. Acknowledging a frame the deframer then drops would advance V(R) past data that never arrived, which COP-1 cannot recover from since the sender sees it acknowledged and never retransmits.
FOP-1 Parameters
| Parameter | Description | Required | Default |
|---|---|---|---|
| SCID | Spacecraft Identifier | Yes | |
| Frame Type | TC or USLP | No | USLP |
| VCID | Virtual Channel Identifier | No | 0 |
| Randomization | Whether the transfer frame is randomized | No | false |
| Error Control | Whether the frame has a Frame Error Control Field | No | false |
| Sync Pattern | Hex string sync pattern | No | nil |
| VC Count Length | Number of bytes in the USLP VC Frame Count | No | 1 |
| OCF | Whether the frame has an Operational Control Field | No | false |
| MAP ID | Multiplexer Access Point ID | No | 0 |
| Source or Dest | USLP Source or Destination Identifier flag | No | 0 |
| Sliding Window | FOP sliding window width (K) | No | 1 |
| Transmission Limit | Maximum number of transmissions of a frame | No | 1 |
| Timeout Type | Action on T1 timer expiry when the transmission limit is reached | No | 0 |
| T1 Initial | Retransmission timer in seconds | No | 5.0 |
| Frame Length | Fixed frame length in bytes | No | nil |
| Insert Zone Length | Length of the USLP Insert Zone in bytes | No | 0 |
| Security Header Length | Length of the SDLS Security Header in bytes | No | 0 |
| Security Trailer Length | Length of the SDLS Security Trailer in bytes | No | 0 |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
FARM-1 Parameters
| Parameter | Description | Required | Default |
|---|---|---|---|
| SCID | Spacecraft Identifier | Yes | |
| Frame Length | Fixed frame length in bytes (not including the sync pattern). Use nil for variable length frames sized by their Frame Length field, which is required to receive USLP Type-BC frames. | Yes | |
| Sync Pattern | Hex string sync pattern | No | 0x1ACFFC1D |
| Randomization | Whether the transfer frame is randomized | No | false |
| Error Control | Whether the frame has a Frame Error Control Field | No | false |
| VC Count Length | Number of bytes in the USLP VC Frame Count | No | 1 |
| OCF | Must be true since FARM reports the CLCW in the OCF | No | true |
| VCID | Virtual Channel Identifier | No | 0 |
| MAP ID | Multiplexer Access Point ID | No | 0 |
| Source or Dest | USLP Source or Destination Identifier flag | No | 0 |
| Positive Window | FARM positive window width (W) | No | 64 |
| Negative Window | FARM negative window width | No | 64 |
| Insert Zone Length | Length of the USLP Insert Zone in bytes | No | 0 |
| Security Header Length | Length of the SDLS Security Header in bytes | No | 0 |
| Security Trailer Length | Length of the SDLS Security Trailer in bytes | No | 0 |
| Allow Empty Data | See Packet Delineation Protocols | No | nil |
FOP must run after the framing protocol in both directions: it edits a built transfer frame on write, and needs the OCF from the deframer on read. Since read protocols run in declaration order and write protocols run in reverse order, declare the framing protocol as a separate READ line before the FOP protocol and a separate WRITE line after it. A physical layer protocol such as CLTU is declared on its own WRITE line ahead of the FOP line, which places it after FOP in the write chain.
- Python
- Ruby
INTERFACE INTERFACE_NAME tcpip_client_interface.py host.docker.internal 8080 8080 10.0 None None
# Return link (telemetry) - OCF enabled so the CLCW is surfaced to FOP
PROTOCOL READ ccsds_uslp_protocol.py 100 None None False TM None False 0 True
PROTOCOL READ_WRITE ccsds_cop1_fop_protocol.py 100 USLP
# Forward link (commanding) - builds the transfer frame that FOP then stamps
PROTOCOL WRITE ccsds_uslp_protocol.py 100
Source code for ccsds_cop1_fop_protocol.py and ccsds_cop1_farm_protocol.py
INTERFACE INTERFACE_NAME tcpip_client_interface.rb host.docker.internal 8080 8080 10.0 nil nil
# Return link (telemetry) - OCF enabled so the CLCW is surfaced to FOP
PROTOCOL READ CcsdsUslpProtocol 100 nil nil false TM nil false 0 true
PROTOCOL READ_WRITE CcsdsCop1FopProtocol 100 USLP
# Forward link (commanding) - builds the transfer frame that FOP then stamps
PROTOCOL WRITE CcsdsUslpProtocol 100
Source code for ccsds_cop1_fop_protocol.rb and ccsds_cop1_farm_protocol.rb
For a spacecraft simulator interface, declare the FARM protocol ahead of the USLP protocol so it runs before the deframer on read and after the framer on write:
PROTOCOL READ_WRITE CcsdsCop1FarmProtocol 100 1024
PROTOCOL READ_WRITE CcsdsUslpProtocol 100 1024 nil false TM 0x1ACFFC1D false 0 true
Operators drive FOP-1 with directives sent via interface_protocol_cmd or the cop1_api Script Runner helpers included in the plugin:
- Python
- Ruby
from cop1_api import *
cop1_initiate_ad('CMD_INT', mode='unlock') # bring up reliable AD service
cmd('TGT COMMAND') # framed and sent reliably (retransmitted if lost)
cop1_set_transmission_limit('CMD_INT', 5)
cop1_terminate('CMD_INT')
require 'cop1_api'
cop1_initiate_ad('CMD_INT', mode: :unlock) # bring up reliable AD service
cmd('TGT COMMAND') # framed and sent reliably (retransmitted if lost)
cop1_set_transmission_limit('CMD_INT', 5)
cop1_terminate('CMD_INT')
The available directives are COP1_INITIATE_AD_WITHOUT_CLCW, COP1_INITIATE_AD_WITH_CLCW, COP1_INITIATE_AD_WITH_UNLOCK, COP1_INITIATE_AD_WITH_SET_VR, COP1_TERMINATE, COP1_RESUME, COP1_SET_VS, COP1_SET_SLIDING_WINDOW, COP1_SET_T1, COP1_SET_TRANSMISSION_LIMIT, COP1_SET_TIMEOUT_TYPE and COP1_STATUS. Per-command AD vs BD (bypass) selection is done by setting COP1_TYPE to AD or BD in the command extra.
The full FOP-1 state machine and FARM-1 (Open / Wait / Lockout, Unlock / Set V(R)) are implemented. COP-P (the Proximity-1 variant) is not implemented and the FARM protocol receives USLP frames only (there is no FARM for TC Transfer Frames).
CCSDS SDLS (Enterprise)
The Space Data Link Security Protocol (CCSDS 355.0-B-2) adds a Security Header before the Transfer Frame Data Field and a Security Trailer after it, providing authentication, encryption, or authenticated encryption per a Security Association (SA). It is implemented as an engine (Sdls + SecurityAssociation) which is passed to the TCTF, TMTF, USLP, AOS and COP-1 protocols via the keyword-only sdls parameter. The frame's primary header is authenticated as additional data and only the Transfer Frame Data Field is encrypted.
| Service | Algorithms |
|---|---|
| Authenticated encryption | AES-GCM (the CCSDS baseline for TM / AOS / USLP: 256 bit key, 96 bit IV, 128 bit tag) |
| Authentication only | AES-CMAC (the CCSDS baseline for TC), HMAC-SHA-256, GMAC |
| Encryption only | AES-CTR |
Since the SDLS engine is an object it cannot be written on a PROTOCOL line. Build the Security Association(s) and engine in a small custom interface and pass the engine to the framing protocol. Keys should be stored in COSMOS Secrets rather than hard coded.
- Python
- Ruby
from sdls import Sdls
from security_association import SecurityAssociation
# Baseline SAs
sa = SecurityAssociation.aes_gcm(spi=1, key=key_bytes) # AES-256-GCM (TM / AOS / USLP)
sa = SecurityAssociation.aes_cmac(spi=2, key=key_bytes) # AES-CMAC (TC)
# Non-baseline services use the full constructor, e.g. HMAC-SHA-256 authentication
sa = SecurityAssociation(
spi=3,
service_type=SecurityAssociation.AUTHENTICATION,
algorithm=SecurityAssociation.HMAC_SHA256,
key=key_bytes, length_sn=4, length_mac=16, sequence_window=64)
# Counter mode IVs must not restart at zero (see below)
sa = SecurityAssociation.aes_gcm(spi=1, key=key_bytes, counter_store=my_store)
sa = SecurityAssociation.aes_gcm(spi=1, key=key_bytes, iv_mode=SecurityAssociation.RANDOM_IV)
sdls = Sdls(sa) # or Sdls([sa1, sa2], apply_spi=1) for multiple SAs
# Then instantiate the framing protocol with sdls=sdls and add it to the interface
require 'sdls'
require 'security_association'
# Baseline SAs
sa = OpenC3::SecurityAssociation.aes_gcm(spi: 1, key: key_bytes) # AES-256-GCM (TM / AOS / USLP)
sa = OpenC3::SecurityAssociation.aes_cmac(spi: 2, key: key_bytes) # AES-CMAC (TC)
# Non-baseline services use the full keyword constructor, e.g. HMAC-SHA-256 authentication
sa = OpenC3::SecurityAssociation.new(spi: 3,
service_type: OpenC3::SecurityAssociation::AUTHENTICATION,
algorithm: OpenC3::SecurityAssociation::HMAC_SHA256,
key: key_bytes, length_sn: 4, length_mac: 16, sequence_window: 64)
# Counter mode IVs must not restart at zero (see below)
sa = OpenC3::SecurityAssociation.aes_gcm(spi: 1, key: key_bytes, counter_store: my_store)
sa = OpenC3::SecurityAssociation.aes_gcm(spi: 1, key: key_bytes,
iv_mode: OpenC3::SecurityAssociation::RANDOM_IV)
sdls = OpenC3::Sdls.new(sa) # or Sdls.new([sa1, sa2], apply_spi: 1) for multiple SAs
# Then instantiate the framing protocol with sdls: sdls and add it to the interface
The SPI in each frame's Security Header selects the SA at the receiver. A monotonic IV / sequence counter provides anti-replay protection and tampered or replayed frames are rejected.
A counter mode IV must never repeat under the same key. AES-GCM leaks its authentication subkey on IV reuse (allowing frame forgery) and AES-CTR leaks plaintext. A SecurityAssociation starts its send counter at zero, so a restarted process re-emits IV 0 unless you do one of the following:
- Persist the counters - pass
counter_store, any object implementingload_counters(spi)andsave_counters(spi, send_counter, receive_counter). The SA loads on construction and saves on every counter change. - Use a random IV - set
iv_modeto random to draw a cryptographically random IV per frame. This requires a Sequence Number field to carry the anti-replay counter; theaes_gcmfactory adds a 4 byte one automatically in this mode. - Rekey every boot - acceptable only if the key is genuinely fresh each time.
When the counter space is exhausted the SA raises CounterExhaustedError rather than wrapping, and must be rekeyed.
The receive counter also needs a trusted baseline after a restart, otherwise authenticated frames are rejected. A counter_store restores it automatically (recommended), sa.resynchronize(counter) sets it from an operator action, and allow_resync adopts the first authenticated frame's counter (convenient for simulators but accepts a replay of a pre-restart frame). sequence_window bounds how far ahead a subsequent counter may jump (0 disables the bound).
Additional SDLS details:
- Channel binding - pass
gvcids(e.g.[{'scid': 42, 'vcid': 0}]) so a valid SA on one Virtual Channel cannot be replayed onto another. TC and USLP also support binding to amap_id. A mismatch raisesChannelMismatchError. - Authentication bit mask - each framing protocol supplies a baseline mask of the header bits covered by the MAC, excluding the fields a sender fills in after the MAC is computed. TM excludes the Master and Virtual Channel Frame Counts, AOS excludes the Virtual Channel Frame Count, its cycle in the Signaling Field and the FHEC, while TC and USLP exclude only their spare bits (USLP's VC Frame Count stays authenticated). The SCID and VCID are always authenticated. Set the SA's
auth_maskto replace the protocol baseline entirely. - Encryption only - AES-CTR provides confidentiality only. It has no MAC, so frame contents can be modified undetected, and it has no replay protection. Prefer AES-GCM unless an external authentication layer covers the frame.
- Type-C frames - TC control command frames carry COP-1 directives and are never secured (CCSDS 232.0-B-4 6.3.4.1).
- USLP Protocol Control Command frames - USLP frames with the Protocol Control Command flag set are never secured either (CCSDS 732.1-B-3 6.1). Since they carry no MAC, the USLP protocol discards them on read rather than delivering their contents, so a frame forged without the key cannot inject a packet.
- COP-1 + SDLS - COP-1 stamps the frame sequence number after the MAC is computed, so those bytes must be excluded from the authenticated region. Build the SA's mask with
SecurityAssociation.uslp_auth_mask(vc_count_length: 1, length_iv: 12)(USLP) orSecurityAssociation.tc_auth_mask(length_sn: 4)(TC), and pass the same engine to both the framing protocol and the COP-1 protocol.
The SDLS Extended Procedures (CCSDS 355.1-B: key management / OTAR, SA management, monitoring & control) are not implemented. The Pad Length field is honored on receive but always sent as 0 since none of the supported algorithms require padding.
CCSDS SLE Interfaces (Enterprise)
The Space Link Extension services are used for agency cross-support (NASA DSN, ESA ESTRACK, SSC, KSAT) to move transfer frames (downlink) and CLTUs (uplink) between a ground station (the SLE provider) and a mission control center (the SLE user). The plugin implements the SLE user side as two Python-only COSMOS interfaces which compose with the framing protocols above:
sle_return_interface.py- RAF Return All Frames (CCSDS 911.1) and RCF Return Channel Frames (CCSDS 911.2)sle_forward_interface.py- FCLTU Forward CLTU (CCSDS 912.1)
Transport is ISP1 (CCSDS 913.1) with protected simple authentication (auth levels none, bind or all). ISP1 has no transport confidentiality so the association should be run over a VPN.
# Downlink: SLE RAF -> TM deframer -> packets
INTERFACE TM_INT sle_return_interface.py <host> <port> <service_instance_id> RAF
PROTOCOL READ ccsds_tmtf_protocol.py 100
# Downlink filtered to one virtual channel with RCF (gvcid = "scid,version,vcid")
INTERFACE TM_INT sle_return_interface.py <host> <port> <service_instance_id> RCF <initiator_id> <responder_id> default <password> <peer_password> none 4 "0xAB,0,1"
PROTOCOL READ ccsds_tmtf_protocol.py 100
# Uplink: TC frame + CLTU encoders -> SLE FCLTU
INTERFACE CMD_INT sle_forward_interface.py <host> <port> <service_instance_id>
PROTOCOL WRITE ccsds_cltu_protocol.py
PROTOCOL WRITE ccsds_tctf_protocol.py True False 1 100
SLE Return Interface Parameters
| Parameter | Description | Required | Default |
|---|---|---|---|
| Hostname | SLE provider host | Yes | |
| Port | SLE provider port | Yes | |
| Inst ID | Provider-issued service instance identifier, e.g. sagr=1.spack=1.rsl-fg=1.raf=onlc1 | Yes | |
| Service | RAF or RCF | No | RAF |
| Initiator ID | Initiator (user) identifier | No | "" |
| Responder ID | Responder (provider) identifier. Required when Peer Password is set. | No | "" |
| Responder Port | Responder port identifier | No | default |
| Password | User password as a hex string | No | None |
| Peer Password | Provider password as a hex string. Enables verification of the provider's credentials. | No | None |
| Auth Level | none, bind or all | No | none |
| Version | SLE service version | No | 4 |
| GVCID | Global Virtual Channel ID as "scid,version,vcid" (omit vcid for the master channel). Required for RCF. | No | None |
SLE Forward Interface Parameters
| Parameter | Description | Required | Default |
|---|---|---|---|
| Hostname | SLE provider host | Yes | |
| Port | SLE provider port | Yes | |
| Inst ID | Provider-issued service instance identifier | Yes | |
| Initiator ID | Initiator (user) identifier | No | "" |
| Responder ID | Responder (provider) identifier. Required when Peer Password is set. | No | "" |
| Responder Port | Responder port identifier | No | default |
| Password | User password as a hex string | No | None |
| Peer Password | Provider password as a hex string. Enables verification of the provider's credentials. | No | None |
| Auth Level | none, bind or all | No | none |
| Version | SLE service version (4 also selects the v4 PDU definitions) | No | 5 |
| Transfer Data Timeout | Seconds to wait for the provider to acknowledge each CLTU. 0 sends without waiting (fire and forget) | No | 10 |
With a positive Transfer Data Timeout, each CLTU write blocks until the provider returns the matching TRANSFER-DATA result and raises if the provider rejected it, so a refused CLTU is never counted as radiated. With a timeout of 0 the CLTU is sent fire and forget, so provider acceptance is not confirmed. The association is kept alive with ISP1 heartbeats and COSMOS reconnects if the provider goes silent.
Provider credentials are verified but the provider's time and random number are not checked against an acceptance window, so a captured provider credential remains replayable. Treat provider authentication as proof the peer knows the password, not as a live session guarantee.
The ROCF and FSP services, SLE service management, the provider (ground station) side, a Ruby implementation, and the CSTS framework are not implemented.
If additional features of these protocols are needed for your mission, please reach out to support@openc3.com.
Encryption Protocols
COSMOS supports implementing encryption at the protocol layer using OpenSSL. This allows you to encrypt and decrypt data transparently as it flows through the interface. Encryption protocols are typically placed at the beginning of the protocol chain (for reads) so they can decrypt data before other protocols process it.
For reading, the encryption protocol should be first so it decrypts the raw data before other protocols process it. For writing, protocols execute in reverse order, so the encryption protocol (listed first) will encrypt the data last, just before it's sent.
Creating an Encryption Protocol
Below is an example of a custom encryption protocol using OpenSSL's AES-256-GCM cipher. This protocol encrypts outgoing data and decrypts incoming data.
- Python
- Ruby
# encryption_protocol.py
# Place in targets/TARGET_NAME/lib/encryption_protocol.py
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
from openc3.interfaces.protocols.protocol import Protocol
from openc3.system.system import System
from openc3.utilities.logger import Logger
class EncryptionProtocol(Protocol):
# key_secret_name: Name of the secret containing the encryption key
# iv_secret_name: Name of the secret containing the initialization vector (optional)
# allow_empty_data: See Protocol.__init__
def __init__(self, key_secret_name, iv_secret_name=None, allow_empty_data=None):
super().__init__(allow_empty_data)
self.key_secret_name = key_secret_name
self.iv_secret_name = iv_secret_name
self.key = None
self.iv = None
# AESGCM instance created in connect_reset after key is loaded
self.aesgcm = None
def connect_reset(self):
# Retrieve the encryption key from COSMOS secrets
secrets = System.secrets
self.key = secrets.get(self.key_secret_name, scope=self.interface.scope)
# IV can be from secrets or generated per message
if self.iv_secret_name:
self.iv = secrets.get(self.iv_secret_name, scope=self.interface.scope)
# Create the AESGCM instance with the key (reused for encrypt/decrypt)
self.aesgcm = AESGCM(self.key)
def read_data(self, data, extra=None):
if len(data) == 0:
return super().read_data(data, extra)
try:
# Extract IV and ciphertext from the data
# Format: [12-byte IV][ciphertext with auth tag]
iv = data[0:12]
ciphertext = data[12:]
plaintext = self.aesgcm.decrypt(iv, ciphertext, None)
return (plaintext, extra)
except Exception as e:
Logger.error(f"Decryption failed: {e}")
return "DISCONNECT"
def write_data(self, data, extra=None):
if len(data) == 0:
return super().write_data(data, extra)
# Generate a random IV for each message (recommended for GCM)
iv = self.iv if self.iv else os.urandom(12)
# encrypt() returns ciphertext with auth tag appended
ciphertext = self.aesgcm.encrypt(iv, data, None)
# Prepend IV to ciphertext
# Format: [12-byte IV][ciphertext with auth tag]
encrypted_data = iv + ciphertext
return (encrypted_data, extra)
# encryption_protocol.rb
# Place in targets/TARGET_NAME/lib/encryption_protocol.rb
require 'openssl'
require 'openc3/interfaces/protocols/protocol'
module OpenC3
class EncryptionProtocol < Protocol
# @param key_secret_name [String] Name of the secret containing the encryption key
# @param iv_secret_name [String] Name of the secret containing the initialization vector
# @param allow_empty_data [true/false/nil] See Protocol#initialize
def initialize(key_secret_name, iv_secret_name = nil, allow_empty_data = nil)
super(allow_empty_data)
@key_secret_name = key_secret_name
@iv_secret_name = iv_secret_name
@key = nil
@iv = nil
# Create cipher instances once and reuse them
@cipher = OpenSSL::Cipher.new('aes-256-gcm')
@decipher = OpenSSL::Cipher.new('aes-256-gcm')
end
def connect_reset
# Retrieve the encryption key from COSMOS secrets
secrets = OpenC3::System.secrets
@key = secrets.get(@key_secret_name, scope: @interface.scope)
# IV can be from secrets or generated per message
if @iv_secret_name
@iv = secrets.get(@iv_secret_name, scope: @interface.scope)
end
end
def read_data(data, extra = nil)
return super(data, extra) if data.empty?
begin
@decipher.reset
@decipher.decrypt
@decipher.key = @key
# Extract IV and auth tag from the data
# Format: [12-byte IV][16-byte auth tag][ciphertext]
iv = data[0..11]
auth_tag = data[12..27]
ciphertext = data[28..-1]
@decipher.iv = iv
@decipher.auth_tag = auth_tag
plaintext = @decipher.update(ciphertext) + @decipher.final
return plaintext, extra
rescue OpenSSL::Cipher::CipherError => e
Logger.error("EncryptionProtocol: Decryption failed: #{e.message}")
return :DISCONNECT
end
end
def write_data(data, extra = nil)
return super(data, extra) if data.empty?
@cipher.reset
@cipher.encrypt
@cipher.key = @key
# Generate a random IV for each message (recommended for GCM)
iv = @iv || @cipher.random_iv
@cipher.iv = iv
ciphertext = @cipher.update(data) + @cipher.final
auth_tag = @cipher.auth_tag
# Prepend IV and auth tag to ciphertext
# Format: [12-byte IV][16-byte auth tag][ciphertext]
encrypted_data = iv + auth_tag + ciphertext
return encrypted_data, extra
end
end
end
Using the Encryption Protocol
To use the encryption protocol, first store your encryption key as a secret in COSMOS Admin under the Secrets tab. Then reference it in your plugin.txt:
INTERFACE ENCRYPTED_INT tcpip_client_interface.rb myhost.com 12345 12345 10.0 nil LENGTH 0 16 0 1 BIG_ENDIAN 0 nil true
# The encryption protocol should be first for reading (decrypts before other protocols)
# For writing, it will be last (encrypts after other protocols build the packet)
PROTOCOL READ_WRITE encryption_protocol.rb my_encryption_key_secret
TARGET ENCRYPTED_TARGET
Available Ciphers
OpenSSL supports many encryption algorithms. Common choices include:
| Cipher | Description | Key Size | IV Size |
|---|---|---|---|
| aes-256-gcm | AES-256 in GCM mode (authenticated encryption) | 32 bytes | 12 bytes |
| aes-128-gcm | AES-128 in GCM mode (authenticated encryption) | 16 bytes | 12 bytes |
| aes-256-cbc | AES-256 in CBC mode | 32 bytes | 16 bytes |
| aes-128-cbc | AES-128 in CBC mode | 16 bytes | 16 bytes |
We recommend using authenticated encryption modes like GCM (Galois/Counter Mode) which provide both confidentiality and integrity. Non-authenticated modes like CBC are vulnerable to certain attacks if not combined with a separate MAC (Message Authentication Code).
Key Management Best Practices
- Never hardcode keys: Always store encryption keys in COSMOS Secrets or an external secrets manager
- Use unique IVs: For GCM mode, always generate a random IV for each message
- Key rotation: Implement a key rotation strategy for long-running systems
- Key derivation: Consider using a Key Derivation Function (KDF) if deriving keys from passwords
Custom Protocols
Creating a custom protocol is easy and should be the default solution for customizing COSMOS Interfaces (rather than creating a new Interface class). However, creating custom Interfaces is still useful for defaulting parameters to values that always are fixed for your target and for including the necessary Protocols. The COSMOS Interfaces take a lot of parameters that can be confusing to your end users. Thus you may want to create a custom Interface just to hard coded these values and cut the available parameters down to something like the hostname and port to connect to.
Custom protocols that handle non-realtime data (e.g. recorded files, back-orbit data, or store-and-forward playback) should set packet.stored = true in the read_packet() method. Stored packets are fully processed through the COSMOS pipeline (identification, decommutation, logging) but do not update the Current Value Table (CVT). This prevents historical data from overwriting real-time values in displays like Packet Viewer and Telemetry Viewer. See Stored Packets for more details. The Preidentified Protocol is one example that encodes and decodes the stored flag in its packet header.
All custom Protocols should derive from the Protocol class openc3/interfaces/protocols/protocol.rb (Ruby) and openc3/interfaces/protocols/protocol.py (Python). This class defines the 9 methods that are relevant to writing your own protocol. The base class implementation for each method is included below as well as a discussion as to how the methods should be overridden and used in your own Protocols.
Protocols should not require 'openc3/script' since they are part of a COSMOS interface. They should use the COSMOS library code directly like System, Packet, Bucket, BinaryAccessor, etc. When in doubt, consult the existing COSMOS protocol classes.
Protocols should not from openc3.script import * since they are part of a COSMOS interface. They should use the COSMOS library code directly like System, Packet, Bucket, BinaryAccessor, etc. When in doubt, consult the existing COSMOS protocol classes.
To really understand how Protocols work, you first must understand the logic within the base Interface class read and write methods.
Let's first discuss the read method.
In the following discussions an all caps word is a symbol in Ruby and a string in Python. So a reference to STOP means :STOP in Ruby and "STOP" in Python.

On every call to read, an empty string "" is first passed down to each of the read Protocol's read_data() method before new raw data is attempted to be read using the Interface's read_interface() method. This is a signal to Protocols that have cached up more than one packet worth of data to output those cached packets before any new data is read from the Interface. Typically no data will be cached up and one of the Protocols read_data() methods will return STOP in response to the empty string, indicating that more data is required to generate a packet. Each Protocol's read_data() method can return one of three things: data that will be passed down to any additional Protocols or turned into a Packet, STOP which means more data is required from the Interface for the Protocol to continue, or DISCONNECT which means that something has happened that requires disconnecting the Interface (and by default trying to reconnect). Each Protocol's read_data() method is passed the data that will eventually be turned into a packet and returns a possibly modified set of data. If the data passes through all Protocol's read_data() methods it is then converted into a COSMOS packet using the Interface's convert_data_to_packet() method. This packet is then run in a similar fashion through each Read Protocol's read_packet() method. This method has essentially the same return possibilities: a Packet (instead of data as in read_data()), STOP, or DISCONNECT. If the Packet makes it through all read_packet() methods then the Interface packet read counter is incremented and the Packet is returned to the Interface.

The Interface write() method works very similarly to read. (It should be mentioned that by default write protocols run in the reverse order of read protocols. This makes sense because when reading you're typically stripping layers of data and when writing you're typically adding on layers in reverse order.)
First, the packet write counter is incremented. Then each write Protocol is given a chance to modify the packet by its write_packet() method being called. This method can either return a potentially modified packet, STOP, or DISCONNECT. If a write Protocol returns STOP no data will be written out the Interface and it is assumed that more packets are necessary before a final packet can be output. DISCONNECT will disconnect the Interface. If the packet makes it through all the write Protocol's write_packet() methods, then it is converted to binary data using the Interface's convert_packet_to_data() method. Next the write_data() method is called for each write Protocol giving it a chance to modify the lower level data. The same return options are available except a Ruby string or Python bytes object is returned instead of a COSMOS packet. If the data makes it through all write_data() methods, then it is written out on the Interface using the write_interface() method. Afterwards, each Protocol's post_write_interface() method is called with both the final modified Packet, and the actual data written out to the Interface. This method allows follow-up such as waiting for a response after writing out a message.
Method discussions
initialize or init
This is the constructor for your custom Protocol. It should always call super(allow_empty_data) to initialize the base Protocol class.
Base class implementation:
- Python
- Ruby
def __init__(self, allow_empty_data=None):
self.interface = None
self.allow_empty_data = ConfigParser.handle_true_false_none(allow_empty_data)
self.reset()
# @param allow_empty_data [true/false/nil] Whether or not this protocol will allow an empty string
# to be passed down to later Protocols (instead of returning :STOP). Can be true, false, or nil, where
# nil is interpreted as true unless the Protocol is the last Protocol of the chain.
def initialize(allow_empty_data = nil)
@interface = nil
@allow_empty_data = ConfigParser.handle_true_false_nil(allow_empty_data)
reset()
end
As you can see, every Protocol maintains state on at least two items. The interface variable holds the Interface class instance that the protocol is associated with. This is sometimes necessary to introspect details that only the Interface knows. allow_empty_data is a flag used by the read_data(data) method that is discussed later in this document.
reset
The reset method is used to reset internal protocol state when the Interface is connected and/or disconnected. This method should be used for common resetting logic. Connect and Disconnect specific logic are handled in the next two methods.
Base class implementation:
- Python
- Ruby
def reset(self):
self.read_data_input_time = None
self.read_data_input = b""
self.read_data_output_time = None
self.read_data_output = b""
self.write_data_input_time = None
self.write_data_input = b""
self.write_data_output_time = None
self.write_data_output = b""
self.extra = None
def reset
@read_data_input_time = nil
@read_data_input = ''
@read_data_output_time = nil
@read_data_output = ''
@write_data_input_time = nil
@write_data_input = ''
@write_data_output_time = nil
@write_data_output = ''
@extra = nil
end
The base class reset implementation only clears the protocol's own state: the extra field and the data captured for the read_details / write_details methods. Subclasses which define reset must call super to keep this state cleared.
connect_reset
The connect_reset method is used to reset internal Protocol state each time the Interface is connected.
Base class implementation:
- Python
- Ruby
def connect_reset(self):
self.reset()
def connect_reset
reset()
end
The base class connect_reset implementation just calls the reset method to ensure common reset logic is run.
disconnect_reset
The disconnect_reset method is used to reset internal Protocol state each time the Interface is disconnected.
Base class implementation:
- Python
- Ruby
def disconnect_reset(self):
self.reset()
def disconnect_reset
reset()
end
The base class disconnect_reset implementation just calls the reset method to ensure common reset logic is run.
read_data
The read_data method is used to analyze and potentially modify any raw data read by an Interface. It takes one parameter as the current state of the data to be analyzed. It can return either a string of data, STOP, or DISCONNECT. If it returns a string, then it believes that data may be ready to be a full packet, and is ready for processing by any following Protocols. If STOP is returned then the Protocol believes it needs more data to complete a full packet. If DISCONNECT is returned then the Protocol believes the Interface should be disconnected (and typically automatically reconnected).
Base class implementation:
- Python
- Ruby
def read_data(self, data, extra=None):
if len(data) <= 0:
if self.allow_empty_data is None:
if self.interface and self.interface.read_protocols[-1] == self:
# Last read interface in chain with auto self.allow_empty_data
return ("STOP", extra)
elif not self.allow_empty_data:
# Don't self.allow_empty_data means STOP
return ("STOP", extra)
return (data, extra)
def read_data(data, extra = nil)
if (data.length <= 0)
if @allow_empty_data.nil?
if @interface and @interface.read_protocols[-1] == self # Last read interface in chain with auto @allow_empty_data
return :STOP
end
elsif !@allow_empty_data # Don't @allow_empty_data means STOP
return :STOP
end
end
return data, extra
end
The base class implementation does nothing except return the data it was given. The only exception to this is when handling an empty string. If the allow_empty_data flag is false / False or if it is nil / None and the Protocol is the last in the chain, then the base implementation will return STOP to indicate that it is time to call the Interface read_interface() method to get more data. Blank strings are used to signal Protocols that they have an opportunity to return a cached packet.
read_packet
The read_packet method is used to analyze and potentially modify a COSMOS packet before it is returned by the Interface. It takes one parameter as the current state of the packet to be analyzed. It can return either a COSMOS packet, STOP, or DISCONNECT. If it returns a COSMOS packet, then it believes that the packet is valid, should be returned, and is ready for processing by any following Protocols. If STOP is returned then the Protocol believes the packet should be silently dropped. If DISCONNECT is returned then the Protocol believes the Interface should be disconnected (and typically automatically reconnected). This method is where a Protocol would set the stored flag on a packet if it determines that the packet is stored telemetry instead of real-time telemetry.
Base class implementation:
- Python
- Ruby
def read_packet(self, packet):
return packet
def read_packet(packet)
return packet
end
The base class always just returns the packet given.
write_packet
The write_packet method is used to analyze and potentially modify a COSMOS packet before it is output by the Interface. It takes one parameter as the current state of the packet to be analyzed. It can return either a COSMOS packet, STOP, or DISCONNECT. If it returns a COSMOS packet, then it believes that the packet is valid, should be written out the Interface, and is ready for processing by any following Protocols. If STOP is returned then the Protocol believes the packet should be silently dropped. If DISCONNECT is returned then the Protocol believes the Interface should be disconnected (and typically automatically reconnected).
Base class implementation:
- Python
- Ruby
def write_packet(self, packet):
return packet
def write_packet(packet)
return packet
end
The base class always just returns the packet given.
write_data
The write_data method is used to analyze and potentially modify data before it is written out by the Interface. It takes one parameter as the current state of the data to be analyzed and sent. It can return either a string of data, STOP, or DISCONNECT. If it returns a string of data, then it believes that the data is valid, should be written out the Interface, and is ready for processing by any following Protocols. If STOP is returned then the Protocol believes the data should be silently dropped. If DISCONNECT is returned then the Protocol believes the Interface should be disconnected (and typically automatically reconnected).
Base class implementation:
- Python
- Ruby
def write_data(self, data, extra=None):
return (data, extra)
def write_data(data)
return data
end
The base class always just returns the data given.
post_write_interface
The post_write_interface method is called after data has been written out the Interface. The typical use of this method is to provide a hook to implement command/response type interfaces where a response is always immediately expected in response to a command. It takes two parameters, the packet after all modifications by write_packet() and the data that was actually written out the Interface. It can return either the same pair of packet/data, STOP, or DISCONNECT. If it returns a packet/data pair then they are passed on to any other Protocols. If STOP is returned then the Interface write() call completes and no further Protocols post_write_interface() methods are called. If DISCONNECT is returned then the Protocol believes the Interface should be disconnected (and typically automatically reconnected). Note that only the first parameter "packet", is checked to be STOP, or DISCONNECT on the return.
Base class implementation:
- Python
- Ruby
def post_write_interface(self, packet, data, extra=None):
return (packet, data, extra)
def post_write_interface(packet, data)
return packet, data
end
The base class always just returns the packet/data given.
protocol_cmd
The protocol_cmd method is used to send commands to the protocol itself. This is useful to change protocol behavior during runtime. See interface_protocol_cmd for more information.
Base class implementation:
- Python
- Ruby
def protocol_cmd(self, cmd_name, *cmd_args):
# Default do nothing - Implemented by subclasses
return False
def protocol_cmd(cmd_name, *cmd_args)
# Default do nothing - Implemented by subclasses
return false
end
The base class does nothing as this is special functionality implemented by subclasses.
read_details
The read_details method returns a hash/dictionary containing diagnostic information about the most recent read operation through the protocol. This method is called by the Interface's as_json() method to provide insight into protocol state for debugging and monitoring purposes.
Base class implementation:
- Python
- Ruby
def read_details(self):
result = {"name": self.__class__.__name__}
if self.read_data_input_time:
result["read_data_input_time"] = self.read_data_input_time.strftime(self.DATETIME_FORMAT)
else:
result["read_data_input_time"] = None
result["read_data_input"] = self.read_data_input
if self.read_data_output_time:
result["read_data_output_time"] = self.read_data_output_time.strftime(self.DATETIME_FORMAT)
else:
result["read_data_output_time"] = None
result["read_data_output"] = self.read_data_output
return result
def read_details
result = {'name' => self.class.name.to_s.split("::")[-1]}
if @read_data_input_time
result['read_data_input_time'] = @read_data_input_time.iso8601
else
result['read_data_input_time'] = nil
end
result['read_data_input'] = @read_data_input
if @read_data_output_time
result['read_data_output_time'] = @read_data_output_time.iso8601
else
result['read_data_output_time'] = nil
end
result['read_data_output'] = @read_data_output
return result
end
The base class implementation returns a hash/dictionary with the following keys:
| Key | Description |
|---|---|
| name | The protocol class name (e.g., "LengthProtocol") |
| read_data_input_time | ISO8601 timestamp of when data was received by the protocol's read_data() method, or nil/None if no data has been received |
| read_data_input | The raw data that was passed into the protocol's read_data() method |
| read_data_output_time | ISO8601 timestamp of when data was output from the protocol's read_data() method, or nil/None if no data has been output |
| read_data_output | The data that was returned from the protocol's read_data() method |
Custom protocols can override this method to include additional protocol-specific diagnostic information by calling super() and adding to the returned hash/dictionary.
Example from BurstProtocol:
- Python
- Ruby
def read_details(self):
result = super().read_details()
result["discard_leading_bytes"] = self.discard_leading_bytes
result["sync_pattern"] = repr(self.sync_pattern)
result["fill_fields"] = self.fill_fields
return result
def read_details
result = super()
result['discard_leading_bytes'] = @discard_leading_bytes
result['sync_pattern'] = @sync_pattern.inspect
result['fill_fields'] = @fill_fields
return result
end
write_details
The write_details method returns a hash/dictionary containing diagnostic information about the most recent write operation through the protocol. This method is called by the Interface's as_json() method to provide insight into protocol state for debugging and monitoring purposes.
Base class implementation:
- Python
- Ruby
def write_details(self):
result = {"name": self.__class__.__name__}
if self.write_data_input_time:
result["write_data_input_time"] = self.write_data_input_time.strftime(self.DATETIME_FORMAT)
else:
result["write_data_input_time"] = None
result["write_data_input"] = self.write_data_input
if self.write_data_output_time:
result["write_data_output_time"] = self.write_data_output_time.strftime(self.DATETIME_FORMAT)
else:
result["write_data_output_time"] = None
result["write_data_output"] = self.write_data_output
return result
def write_details
result = {'name' => self.class.name.to_s.split("::")[-1]}
if @write_data_input_time
result['write_data_input_time'] = @write_data_input_time.iso8601
else
result['write_data_input_time'] = nil
end
result['write_data_input'] = @write_data_input
if @write_data_output_time
result['write_data_output_time'] = @write_data_output_time.iso8601
else
result['write_data_output_time'] = nil
end
result['write_data_output'] = @write_data_output
return result
end
The base class implementation returns a hash/dictionary with the following keys:
| Key | Description |
|---|---|
| name | The protocol class name (e.g., "LengthProtocol") |
| write_data_input_time | ISO8601 timestamp of when data was received by the protocol's write_data() method, or nil/None if no data has been received |
| write_data_input | The raw data that was passed into the protocol's write_data() method |
| write_data_output_time | ISO8601 timestamp of when data was output from the protocol's write_data() method, or nil/None if no data has been output |
| write_data_output | The data that was returned from the protocol's write_data() method |
Custom protocols can override this method to include additional protocol-specific diagnostic information by calling super() and adding to the returned hash/dictionary.
Example from BurstProtocol:
- Python
- Ruby
def write_details(self):
result = super().write_details()
result["discard_leading_bytes"] = self.discard_leading_bytes
result["sync_pattern"] = repr(self.sync_pattern)
result["fill_fields"] = self.fill_fields
return result
def write_details
result = super()
result['discard_leading_bytes'] = @discard_leading_bytes
result['sync_pattern'] = @sync_pattern.inspect
result['fill_fields'] = @fill_fields
return result
end
Examples
Please see the linked Ruby Protocol and Python Protocol code for examples of the above methods in action.