├── .gitignore ├── .readthedocs.yaml ├── CONTRIBUTING.rst ├── LICENSE.txt ├── README.rst ├── azure-pipelines.yml ├── chapter10 ├── __init__.py ├── analog.py ├── arinc429.py ├── c10.py ├── computer.py ├── discrete.py ├── ethernet.py ├── i1394.py ├── image.py ├── message.py ├── ms1553.py ├── packet.py ├── parallel.py ├── pcm.py ├── time.py ├── uart.py ├── util.py └── video.py ├── conftest.py ├── docs ├── Makefile ├── make.bat └── source │ ├── api │ ├── datatypes.rst │ ├── index.rst │ └── util.rst │ ├── conf.py │ ├── index.rst │ └── samples │ ├── allbus.rst │ ├── index.rst │ └── stat.rst ├── pdm.lock ├── pyproject.toml └── tests ├── conftest.py ├── discrete.c10 ├── ethernet.c10 ├── event.c10 ├── fixtures.py ├── pcm.c10 ├── performance_test.py ├── sample.c10 ├── test_sanity.py └── unit ├── test_analog.py ├── test_arinc429.py ├── test_c10.py ├── test_computer.py ├── test_discrete.py ├── test_ethernet.py ├── test_message.py ├── test_ms1553.py ├── test_packet.py ├── test_pcm.py ├── test_time.py ├── test_uart.py └── test_video.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.c10 4 | build 5 | dist 6 | scripts/mplayer/* 7 | bin/* 8 | .tox/* 9 | .ropeproject 10 | MANIFEST 11 | .DS_store 12 | .cache 13 | junit* 14 | .coverage 15 | coverage.xml 16 | htmlcov 17 | deps 18 | docs/html 19 | docs/doctrees 20 | .pdm.toml 21 | .vscode 22 | *.xml 23 | __pypackages__ 24 | .pdm-python 25 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.8" 7 | 8 | python: 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - docs 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | 2 | Contributing 3 | ============ 4 | 5 | Questions or Comments 6 | --------------------- 7 | 8 | Commits, issues, and pull requests can all host comments & discussion to better 9 | coordinate efforts. Constructive discussion is always welcome. 10 | 11 | Code Standards 12 | -------------- 13 | 14 | Adhere to relevant code style & testing constraints. For Python refer to 15 | PEP8_. Automated tools are available for the various editors to autorun style 16 | checking, etc. 17 | 18 | Issues 19 | ------ 20 | 21 | To report bugs or suggest features, create an issue. Issues are the "to do" 22 | list for repos and allow for triage of priorities and clarity of development 23 | focus. 24 | 25 | Pull Requests 26 | ------------- 27 | 28 | When working on an issue (see above), the workflow should be as follows: 29 | 30 | 1. Create a branch named issue-# (ie. issue-4) 31 | 2. Make the edits needed to resolve the issue 32 | 3. Commit the changes and push the issue branch to a personal fork or the main 33 | repo 34 | 4. Create a pull request from the uploaded branch into the master branch 35 | 5. Once reviewed the pull request will be merged into the master branch 36 | 37 | When working on a feature involving several commits, though not tied to an 38 | issue, use the same workflow, but name the branch "" (ie. docopt) 39 | 40 | Also viable, make changes on a personal fork of the repo and submit a PR from 41 | there. 42 | 43 | 44 | .. _PEP8: https://www.python.org/dev/peps/pep-0008/ 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Avionics Test and Analysis Corporation 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | PyChapter10 3 | =========== 4 | 5 | |StatusImage|_ 6 | |CoverageImage| 7 | |DocsImage|_ 8 | |LicenseImage| 9 | |PyPiVersion|_ 10 | |PythonVersion| 11 | 12 | PyChapter10 is an open source pure Python library for reading and writing IRIG 106 13 | Chapter 10 (now 11) files. Tested on all 3 major platforms with Python 3.6+. 14 | 15 | Installation 16 | ------------ 17 | 18 | Install the latest version with pip:: 19 | 20 | pip install pychapter10 21 | 22 | To install offline from "full" zip, install the included dependencies and the library with:: 23 | 24 | pip install dependencies/* . 25 | 26 | **Note:** you may also install cbitstruct for a performance improvement. 27 | 28 | Basic Usage 29 | ----------- 30 | 31 | PyChapter10 provides a pythonic API to read, write, and update Chapter 10 data. 32 | 33 | .. code-block:: python 34 | 35 | from chapter10 import C10 36 | 37 | # Find all 1553 messages in a file 38 | for packet in C10(''): 39 | if packet.data_type == 0x19: 40 | for msg in packet: 41 | # do something with the message 42 | 43 | Supported Datatypes 44 | ------------------- 45 | ==== ================================================== ========= 46 | Type Name Supported 47 | ==== ================================================== ========= 48 | 0x00 Computer-Generated F0 - User-Defined User-Defined 49 | 0x01 Computer-Generated F1 - Setup Record (TMATS) Yes 50 | 0x02 Computer-Generated F2 - Recording Events Yes 51 | 0x03 Computer-Generated F3 - Recording Index Yes 52 | 0x04 Computer-Generated F4 - Streaming Config (TMATS) No 53 | 0x09 PCM F1 Yes 54 | 0x11 Time Data F1 Yes 55 | 0x12 Time Data F2 No 56 | 0x19 1553 F1 Yes 57 | 0x1A 1553 F2 - 16PP194 Yes 58 | 0x21 Analog F1 Yes 59 | 0x29 Discrete F1 Yes 60 | 0x30 Message F0 Yes 61 | 0x38 ARINC-429 F0 Yes 62 | 0x40 Video F0 Yes 63 | 0x41 Video F1 Yes 64 | 0x42 Video F2 Yes 65 | 0x43 Video F3 No 66 | 0x44 Video F4 No 67 | 0x48 Image F0 Yes (untested) 68 | 0x49 Image F1 Yes (untested) 69 | 0x4A Image F2 Yes (untested) 70 | 0x50 UART F0 Yes 71 | 0x58 IEEE 1394 F0 Yes (untested) 72 | 0x59 IEEE 1394 F1 Yes (untested) 73 | 0x60 Parallel F0 Yes (untested) 74 | 0x68 Ethernet F0 - Ethernet Data Yes 75 | 0x69 Ethernet F1 - UDP Payload Yes 76 | 0x70 TSPI/CTS F0 - GPS NMEA-RTCM No 77 | 0x71 TSPI/CTS F1 - EAG ACMI No 78 | 0x72 TSPI/CTS F2 - ACTTS No 79 | 0x78 Controller Area Network Bus No 80 | 0x79 Fibre Channel F0 No 81 | 0x7A Fibre Channel F1 No 82 | ==== ================================================== ========= 83 | 84 | Running Tests 85 | ------------- 86 | 87 | To run the included test suite install pdm and dev dependencies with pip:: 88 | 89 | pip install pdm 90 | pdm install --dev 91 | 92 | Then run:: 93 | 94 | pytest 95 | 96 | Building the Documentation 97 | -------------------------- 98 | 99 | After installing dependencies (or just "pip install sphinx") Build the docs with:: 100 | 101 | python setup.py build_docs 102 | 103 | The generated HTML will be in docs/html 104 | 105 | **Note:** "Full" zip includes built documentation already. 106 | 107 | Contributing 108 | ------------ 109 | 110 | See CONTRIBUTING_ 111 | 112 | .. _Python: http://python.org 113 | .. |StatusImage| image:: https://img.shields.io/azure-devops/build/atac-bham/7e6b2ae2-5609-49c9-9ded-f108e80d8949/7 114 | .. _StatusImage: https://dev.azure.com/atac-bham/pychapter10/_build/latest?definitionId=7&branchName=master 115 | .. |DocsImage| image:: https://readthedocs.org/projects/pychapter10/badge/?version=latest 116 | .. _DocsImage: https://pychapter10.readthedocs.io/en/latest/?badge=latest 117 | .. |CoverageImage| image:: https://img.shields.io/azure-devops/coverage/atac-bham/pychapter10/7 118 | .. |LicenseImage| image:: https://img.shields.io/github/license/atac/pychapter10 119 | .. _PyPiVersion: https://pypi.org/project/pychapter10/ 120 | .. |PyPiVersion| image:: https://img.shields.io/pypi/v/pychapter10 121 | .. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/pychapter10 122 | .. _CONTRIBUTING: CONTRIBUTING.rst 123 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | 2 | trigger: 3 | - master 4 | 5 | name: '1.1.19' 6 | 7 | jobs: 8 | 9 | - job: Test 10 | strategy: 11 | maxParallel: 5 12 | matrix: 13 | Ubuntu-py3.7: 14 | imageName: 'ubuntu-latest' 15 | python.version: '3.7' 16 | Ubuntu-py3.8: 17 | imageName: 'ubuntu-latest' 18 | python.version: '3.8' 19 | Ubuntu-py3.9: 20 | imageName: 'ubuntu-latest' 21 | python.version: '3.9' 22 | Ubuntu-py3.10: 23 | imageName: 'ubuntu-latest' 24 | python.version: '3.10' 25 | # Ubuntu-py3.11: 26 | # imageName: 'ubuntu-latest' 27 | # python.version: '3.11' 28 | macOS-py3.7: 29 | imageName: 'macos-latest' 30 | python.version: '3.7' 31 | macOS-py3.8: 32 | imageName: 'macos-latest' 33 | python.version: '3.8' 34 | macOS-py3.9: 35 | imageName: 'macos-latest' 36 | python.version: '3.9' 37 | macOS-py3.10: 38 | imageName: 'macos-latest' 39 | python.version: '3.10' 40 | # macOS-py3.11: 41 | # imageName: 'macos-latest' 42 | # python.version: '3.11' 43 | Windows-py3.7: 44 | imageName: 'windows-latest' 45 | python.version: '3.7' 46 | Windows-py3.8: 47 | imageName: 'windows-latest' 48 | python.version: '3.8' 49 | Windows-py3.9: 50 | imageName: 'windows-latest' 51 | python.version: '3.9' 52 | Windows-py3.10: 53 | imageName: 'windows-latest' 54 | python.version: '3.10' 55 | # Windows-py3.11: 56 | # imageName: 'windows-latest' 57 | # python.version: '3.11' 58 | 59 | pool: 60 | vmImage: $(imageName) 61 | 62 | steps: 63 | - task: UsePythonVersion@0 64 | inputs: 65 | versionSpec: "$(python.version)" 66 | architecture: 'x64' 67 | 68 | - script: python -m pip install -U pip pdm 69 | displayName: 'Install pdm' 70 | 71 | - script: pdm install --dev 72 | displayName: 'Install dependencies' 73 | 74 | - script: pdm run pytest --junitxml=junit-$(imageName)-$(python.version).xml --cov chapter10 --cov-report xml --cov-report html --cov-append 75 | displayName: 'Run tests' 76 | 77 | - task: PublishTestResults@2 78 | inputs: 79 | testResultsFormat: 'JUnit' 80 | testResultsFiles: 'junit-*.xml' 81 | testRunTitle: 'Python $(python.version)' 82 | condition: succeededOrFailed() 83 | 84 | - task: PublishCodeCoverageResults@1 85 | inputs: 86 | codeCoverageTool: Cobertura 87 | summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' 88 | 89 | # Create dist files for PyPI release 90 | - job: Build 91 | dependsOn: 'Test' 92 | pool: 93 | vmImage: 'ubuntu-latest' 94 | steps: 95 | - task: UsePythonVersion@0 96 | inputs: 97 | versionSpec: "3.11" 98 | architecture: 'x64' 99 | 100 | - script: python -m pip install -U pip pdm 101 | displayName: 'Install pdm' 102 | 103 | - script: pdm install --dev 104 | displayName: 'Install dependencies' 105 | 106 | - script: "pdm build" 107 | displayName: "Build source & wheel distributions" 108 | 109 | - task: PublishBuildArtifacts@1 110 | inputs: 111 | pathToPublish: 'dist' 112 | artifactName: dist 113 | 114 | # Package dependencies and docs with source code 115 | - job: Bundle 116 | dependsOn: 'Test' 117 | pool: 118 | vmImage: 'ubuntu-latest' 119 | steps: 120 | - task: UsePythonVersion@0 121 | inputs: 122 | versionSpec: "3.11" 123 | architecture: 'x64' 124 | 125 | - script: python -m pip install -U pip pdm 126 | displayName: 'Install pdm' 127 | 128 | - script: pdm install --dev 129 | displayName: 'Install dependencies' 130 | 131 | - script: "pdm run sphinx-build -b html docs/source docs/html" 132 | displayName: 'Build documentation' 133 | 134 | - task: ArchiveFiles@2 135 | displayName: 'Zip full package' 136 | inputs: 137 | rootFolderOrFile: '$(Build.SourcesDirectory)' 138 | includeRootFolder: false 139 | archiveType: 'zip' 140 | archiveFile: 'pychapter10-$(Build.BuildNumber)-full.zip' 141 | replaceExistingArchive: true 142 | 143 | - task: PublishBuildArtifacts@1 144 | inputs: 145 | pathToPublish: 'pychapter10-$(Build.BuildNumber)-full.zip' 146 | artifactName: complete-library 147 | -------------------------------------------------------------------------------- /chapter10/__init__.py: -------------------------------------------------------------------------------- 1 | from .c10 import C10, TYPES 2 | from .packet import Packet, InvalidPacket 3 | version = '1.1.19' 4 | -------------------------------------------------------------------------------- /chapter10/analog.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | from bitstring import Bits 6 | 7 | 8 | class AnalogF1(packet.Packet): 9 | """ 10 | .. py:attribute:: mode 11 | 12 | Indicates packing and aligmnent mode. 13 | 14 | * 0 - Packed 15 | * 1 - Unpacked, lsb padded 16 | * 3 - Unpacked, msb padded 17 | 18 | .. py:attribute:: length 19 | 20 | Bit length of samples. 21 | 22 | .. py:attribute:: subchannel 23 | 24 | Subchannel ID 25 | 26 | .. py:attribute:: subchannel_count 27 | 28 | Number of subchannels (and CSDWs) in the packet. 29 | 30 | .. py:attribute:: factor 31 | 32 | Exponent of power of 2 sampling rate factor denominator. 33 | 34 | .. py:attribute:: same 35 | 36 | Indicates whether this CSDW applies to all subchannels. 37 | 38 | """ 39 | 40 | csdw_format = BitFormat(''' 41 | u6 length 42 | u2 mode 43 | 44 | u8 subchannel 45 | u8 subchannel_count 46 | 47 | p3 48 | u1 same 49 | u4 factor 50 | ''') 51 | 52 | class Message(packet.Message): 53 | def __repr__(self): 54 | return '' % len(self.length) 55 | 56 | @classmethod 57 | def from_packet(cls, packet): 58 | subchannel = packet._subchannel 59 | if packet.same: 60 | csdw = packet.subchannels[0] 61 | else: 62 | csdw = packet.subchannels[subchannel] 63 | 64 | # Find the sample size in bytes (16 bit aligned). 65 | length = csdw['length'] or 64 # Length 0 = 64 bits 66 | 67 | if packet.bit_offset > len(packet.data): 68 | raise EOFError 69 | 70 | data = packet.data[packet.bit_offset:packet.bit_offset + length] 71 | packet.bit_offset += length 72 | 73 | packet._subchannel += 1 74 | if packet._subchannel >= len(packet.subchannels): 75 | packet._subchannel = 0 76 | 77 | return packet.Message(data.bytes, parent=packet, **csdw) 78 | 79 | def __bytes__(self): 80 | return self.data 81 | 82 | def __init__(self, *args, **kwargs): 83 | packet.Packet.__init__(self, *args, **kwargs) 84 | 85 | self._subchannel = 0 86 | 87 | if self.length == 0: 88 | self.length = 16 89 | 90 | keys = ( 91 | 'mode', 92 | 'length', 93 | 'subchannel', 94 | 'subchannel_count', 95 | 'factor', 96 | 'same') 97 | self.subchannels = [{k: getattr(self, k) for k in keys}] 98 | 99 | # Read CSDWs for additional subchannels if applicable. 100 | if not self.same: 101 | for i in range(self.subchannel_count - 1): 102 | self.subchannels.append( 103 | self.csdw_format.unpack(self.buffer.read(4))) 104 | 105 | # Get the raw bytes of data since samples are specified in bit length 106 | self.data = Bits() 107 | if self.buffer: 108 | raw = self.buffer.read( 109 | self.data_length - (4 * len(self.subchannels))) 110 | self.data = Bits(raw) 111 | self.bit_offset = 0 112 | 113 | def _raw_body(self): 114 | raw = bytes() 115 | for channel in self.subchannels: 116 | raw += self.csdw_format.pack(channel) 117 | raw += b''.join(bytes(msg) for msg in self._messages) 118 | return raw 119 | -------------------------------------------------------------------------------- /chapter10/arinc429.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class ARINC429F0(packet.Packet): 7 | """ 8 | .. py:attribute:: count 9 | 10 | Number of ARINC 429 words in packet body. 11 | """ 12 | 13 | csdw_format = BitFormat(''' 14 | u16 count 15 | p16''') 16 | 17 | class Message(packet.Message): 18 | """ 19 | .. py:attribute:: gap_time 20 | 21 | Gap time from the beginning of the preceding bus word (regardless \ 22 | of bus) to the beginning of the current bus word in 0.1-us increments. 23 | .. py:attribute:: bus_speed 24 | 25 | * 0 - low-speed bus (12.5kHz) 26 | * 1 - high-speed bus (100kHz) 27 | 28 | .. py:attribute:: parity_error 29 | .. py:attribute:: format_error 30 | .. py:attribute:: bus 31 | 32 | ARINC bus number (0-255) 33 | """ 34 | FORMAT = BitFormat(''' 35 | u16 gap_time 36 | 37 | u1 format_error 38 | u1 parity_error 39 | u1 bus_speed 40 | p1 41 | 42 | u4 gap_upper 43 | 44 | u8 bus 45 | ''', '211') 46 | length = 4 47 | 48 | # TODO: there should be a neater way to do this with bitstruct, but I 49 | # haven't found it yet. 50 | def __init__(self, *args, **kwargs): 51 | packet.Message.__init__(self, *args, **kwargs) 52 | self.gap_time += (self.gap_upper << 16) 53 | 54 | def __bytes__(self): 55 | old_gap = self.gap_time 56 | self.gap_upper = self.gap_time >> 16 57 | self.gap_time &= 0xffff 58 | b = packet.Message.__bytes__(self) 59 | self.gap_time = old_gap 60 | return b 61 | 62 | def __repr__(self): 63 | return '' 64 | -------------------------------------------------------------------------------- /chapter10/c10.py: -------------------------------------------------------------------------------- 1 | 2 | from io import BytesIO 3 | import os 4 | 5 | from .packet import Packet, InvalidPacket 6 | from .util import Buffer 7 | from .computer import ComputerF0, ComputerF1, ComputerF2, ComputerF3 8 | from .pcm import PCMF1 9 | from .time import TimeF1 10 | from .ms1553 import MS1553F1, MS1553F2 11 | from .analog import AnalogF1 12 | from .discrete import DiscreteF1 13 | from .message import MessageF0 14 | from .arinc429 import ARINC429F0 15 | from .video import VideoF0, VideoF1, VideoF2 16 | from .image import ImageF0, ImageF1, ImageF2 17 | from .uart import UARTF0 18 | from .i1394 import I1394F0, I1394F1 19 | from .parallel import ParallelF0 20 | from .ethernet import EthernetF0, EthernetF1 21 | 22 | __all__ = ('TYPES', 'C10') 23 | 24 | # Top level data types. 25 | TYPES = { 26 | 0x00: ComputerF0, 27 | 0x01: ComputerF1, # TMATS 28 | 0x02: ComputerF2, # Event 29 | 0x03: ComputerF3, # Index 30 | 0x09: PCMF1, 31 | 0x11: TimeF1, 32 | 0x19: MS1553F1, 33 | 0x1A: MS1553F2, 34 | 0x21: AnalogF1, 35 | 0x29: DiscreteF1, 36 | 0x30: MessageF0, 37 | 0x38: ARINC429F0, 38 | 0x40: VideoF0, 39 | 0x41: VideoF1, 40 | 0x42: VideoF2, 41 | 0x48: ImageF0, 42 | 0x49: ImageF1, 43 | 0x4A: ImageF2, 44 | 0x50: UARTF0, 45 | 0x58: I1394F0, 46 | 0x59: I1394F1, 47 | 0x60: ParallelF0, 48 | 0x68: EthernetF0, 49 | 0x69: EthernetF1, 50 | } 51 | 52 | 53 | class C10(object): 54 | """A Chapter 10/11 parser. 55 | 56 | :param f: A file like object or file path to read from. 57 | :type f: file or str 58 | 59 | .. py:attribute:: last_time 60 | :type: time.TimeF1 61 | 62 | Most recently parsed (or generated) time packet. 63 | """ 64 | 65 | def __init__(self, f): 66 | if isinstance(f, str): 67 | f = open(f, 'rb') 68 | self.file = Buffer(f) 69 | self.last_time = None 70 | 71 | @classmethod 72 | def from_string(cls, s): 73 | """Create a C10 object from a string or bytes.""" 74 | 75 | return cls(BytesIO(s)) 76 | 77 | def __next__(self): 78 | """Walk a chapter 10 file using python's iterator protocol and return 79 | a chapter10.packet.Packet object for each valid packet found. 80 | """ 81 | 82 | while True: 83 | pos = self.file.tell() 84 | try: 85 | header = Packet.FORMAT.unpack(self.file.read(24)) 86 | handler = TYPES.get(header['data_type'], None) 87 | if handler: 88 | packet = handler(self.file, parent=self, **header) 89 | if packet.data_type == 0x11: 90 | self.last_time = packet 91 | return packet 92 | 93 | raise NotImplementedError('Type %s not implemented' % 94 | hex(header['data_type'])) 95 | except EOFError: 96 | raise StopIteration 97 | except InvalidPacket: 98 | self.file.seek(pos + 1) 99 | 100 | def __repr__(self): 101 | return ''.format(os.path.basename(self.file.io.name)) 102 | 103 | def __iter__(self): 104 | return self 105 | -------------------------------------------------------------------------------- /chapter10/computer.py: -------------------------------------------------------------------------------- 1 | 2 | from collections import OrderedDict 3 | 4 | from .util import BitFormat, bitstruct 5 | from . import packet 6 | 7 | 8 | class ComputerF0(packet.Packet): 9 | """User-defined""" 10 | 11 | def __init__(self, *args, **kwargs): 12 | self.data = bytes() 13 | packet.Packet.__init__(self, *args, **kwargs) 14 | if self.buffer: 15 | self.data = self.buffer.read(self.data_length - 4) 16 | 17 | def _raw_body(self): 18 | return (b'\0' * 4) + self.data 19 | 20 | 21 | class ComputerF1(packet.Packet): 22 | """Setup record (TMATS) 23 | 24 | Using dictionary lookup syntax returns an OrderedDict of values that start 25 | with the given string:: 26 | 27 | >> tmats_packet['G'] 28 | OrderedDict({'G\\COM': 'Comment'}) 29 | 30 | .. py:attribute:: version 31 | 32 | Irig106 release: 33 | 34 | * 7 = 2007 35 | * 8 = 2009 36 | * 9 = 2010 37 | * 10 = 2013 38 | * 11 = 2015 39 | * 12 = 2017 40 | 41 | .. py:attribute:: configuration_change 42 | 43 | When streaming, indicates if the TMATS configuration has changed from 44 | the previous one. 45 | 46 | .. py:attribute:: format 47 | 48 | Indicates ASCII (0) or XML (1) 49 | """ 50 | 51 | csdw_format = BitFormat(''' 52 | u8 version 53 | u1 configuration_change 54 | u1 format 55 | p22''') 56 | 57 | _MAX_PACKET_BYTES = 134_217_728 58 | 59 | def __init__(self, *args, **kwargs): 60 | packet.Packet.__init__(self, *args, **kwargs) 61 | if self.buffer: 62 | self.data = self.buffer.read(self.data_length - 4) 63 | elif not getattr(self, 'data', None): 64 | self.data = kwargs.get('data', bytes()) 65 | 66 | def _raw_body(self): 67 | csdw = self.csdw_format.pack(self.__dict__) 68 | if isinstance(self.data, str): 69 | return csdw + bytes(self.data, 'utf8') 70 | return csdw + self.data 71 | 72 | def __getitem__(self, key): 73 | key = bytearray(key, 'utf-8') 74 | d = OrderedDict() 75 | for line in self.data.splitlines(): 76 | if not line.strip() or b':' not in line: 77 | continue 78 | line = line.strip()[:-1].split(b':', 1) # Strip the semicolon. 79 | if line[0].startswith(key): 80 | d[line[0]] = line[1] 81 | return d 82 | 83 | 84 | class ComputerF2(packet.Packet): 85 | """Recording Event 86 | 87 | .. py:attribute:: count 88 | .. py:attribute:: ipdh 89 | """ 90 | 91 | csdw_format = BitFormat(''' 92 | u12 count 93 | p19 94 | u1 ipdh''') 95 | 96 | def __init__(self, *args, **kwargs): 97 | packet.Packet.__init__(self, *args, **kwargs) 98 | 99 | fmt = 'u64 ipts' 100 | if self.ipdh: 101 | fmt += '\nu64 ipdh' 102 | fmt += ''' 103 | u12 number 104 | u16 count 105 | u1 occurrence 106 | p3''' 107 | self.Message.FORMAT = BitFormat(fmt) 108 | 109 | class Message(packet.Message): 110 | """ 111 | .. py:attribute:: ipts 112 | .. py:attribute:: ipdh 113 | 114 | If present (see CSDW), contains the absolute time of the event. 115 | 116 | .. py:attribute:: number 117 | 118 | Event type number 119 | 120 | .. py:attribute:: count 121 | 122 | Number of events of this type as of the packet being written. 123 | 124 | .. py:attribute:: occurrence 125 | 126 | 1 if event occurred during .RECORD mode. 127 | """ 128 | def __repr__(self): 129 | return '' 130 | 131 | 132 | class ComputerF3(packet.Packet): 133 | """Index Packet 134 | 135 | .. py:attribute:: count 136 | .. py:attribute:: ipdh 137 | .. py:attribute:: file_size_present 138 | .. py:attribute:: index_type 139 | 140 | * 0 - Root Index 141 | * 1 - Node Index 142 | 143 | .. py:attribute::file_size 144 | 145 | If enabled (see CSDW) indicates file size when packet was written. 146 | 147 | .. py:attribute::root_offset 148 | 149 | For root index, indicates offset to previous root index packet. 150 | """ 151 | 152 | csdw_format = BitFormat(''' 153 | u16 count 154 | p8 155 | u1 index_type 156 | u1 file_size_present 157 | u1 ipdh 158 | p5 159 | ''') 160 | 161 | class Message(packet.Message): 162 | """ 163 | .. py:attribute:: ipts 164 | .. py:attribute:: ipdh 165 | .. py:attribute:: offset 166 | 167 | Offset to packet from beginning of file. 168 | 169 | **Node Index Only** 170 | 171 | .. py:attribute:: channel_id 172 | .. py:attribute:: data_type 173 | """ 174 | 175 | def __repr__(self): 176 | return '<%s Index>' % ( 177 | 'Node' if self.parent.index_type else 'Root') 178 | 179 | def __init__(self, *args, **kwargs): 180 | packet.Packet.__init__(self, *args, **kwargs) 181 | 182 | if self.file_size_present and self.buffer: 183 | self.file_size, = bitstruct.unpack('u64<', self.buffer.read(8)) 184 | 185 | fmt = 'u64 ipts' 186 | if self.ipdh: 187 | fmt += '\nu64 ipdh' 188 | 189 | if self.index_type == 0: 190 | fmt += '\nu64 offset' 191 | elif self.index_type == 1: 192 | fmt += ''' 193 | u16 channel_id 194 | u8 data_type 195 | p8 196 | u64 offset''' 197 | 198 | self.Message.FORMAT = BitFormat(fmt) 199 | 200 | if self.index_type == 0 and self.buffer: 201 | pos = self.buffer.tell() 202 | self.buffer.seek(self.data_length - 8) 203 | self.root_offset, = bitstruct.unpack('u64<', self.buffer.read(8)) 204 | self.buffer.seek(pos) 205 | 206 | def _read_messages(self): 207 | if self.buffer: 208 | offset = 36 if self.secondary_header else 24 209 | if self.file_size_present: 210 | offset += 8 211 | self.buffer.seek(offset + 4) 212 | self._messages = list(self) 213 | 214 | def _raw_body(self): 215 | raw = self.csdw_format.pack(self.__dict__) 216 | if self.file_size_present: 217 | raw += bitstruct.pack('u64<', self.file_size) 218 | raw += b''.join(bytes(m) for m in self._messages) 219 | if self.index_type == 0: 220 | raw += bitstruct.pack('u64<', self.root_offset) 221 | return raw 222 | -------------------------------------------------------------------------------- /chapter10/discrete.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class DiscreteF1(packet.Packet): 7 | """Discrete Format 1 8 | 9 | .. py:attribute:: mode 10 | 11 | * 0 - data is recorded when the state changes 12 | * 1 - data is recorded at a timed interval 13 | 14 | .. py:attribute:: length 15 | 16 | Bit length of data or 0 (32 bits) 17 | """ 18 | 19 | csdw_format = BitFormat(''' 20 | u3 mode 21 | u5 length 22 | p24''') 23 | 24 | class Message(packet.Message): 25 | """.. py:attribute:: ipts""" 26 | 27 | length = 4 28 | FORMAT = BitFormat('u64 ipts') 29 | 30 | def __repr__(self): 31 | return '' % len(self.data) 32 | -------------------------------------------------------------------------------- /chapter10/ethernet.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class EthernetF0(packet.Packet): 7 | """Ethernet data 8 | 9 | .. py:attribute:: count 10 | .. py:attribute:: ttb 11 | 12 | Time tag bits: 13 | 14 | * 0 - First bit of the frame destination address 15 | * 1 - Last bit of the frame check sequence 16 | * 2 - First bit of the frame payload data 17 | * 3 - Last bit of the frame payload data 18 | 19 | .. py:attribute:: format 20 | 21 | Should be 0 (IEEE 802.3 MAC frame) 22 | """ 23 | 24 | csdw_format = BitFormat(''' 25 | u16 count 26 | p9 27 | u3 ttb 28 | u4 format''') 29 | 30 | class Message(packet.Message): 31 | """ 32 | .. py:attribute:: ipts 33 | .. py:attribute:: length 34 | 35 | Payload length (bytes) 36 | 37 | .. py:attribute:: data_length_error 38 | .. py:attribute:: data_crc_error 39 | .. py:attribute:: network_id 40 | .. py:attribute:: crc_error 41 | .. py:attribute:: frame_error 42 | .. py:attribute:: content 43 | 44 | * 0 - Full MAC frame 45 | * 1 - Payload only (14 bytes from the destination address) 46 | 47 | .. py:attribute:: ethernet_speed 48 | 49 | * 0 - Auto 50 | * 1 - 10 Mbps 51 | * 2 - 100 Mbps 52 | * 3 - 1 Gbps 53 | * 4 - 10 Gbps 54 | """ 55 | 56 | def __repr__(self): 57 | return '' % len(self.data) 58 | 59 | # Note: bitfields may need to be listed in reverse of expected 60 | # order. 61 | FORMAT = BitFormat(''' 62 | u64 ipts 63 | 64 | u16 length 65 | 66 | u8 network_id 67 | u1 crc_error 68 | u1 frame_error 69 | u2 content 70 | u4 ethernet_speed 71 | ''') 72 | 73 | def __init__(self, *args, **kwargs): 74 | packet.Message.__init__(self, *args, **kwargs) 75 | self.length_error = self.length >> 14 & 0b1 76 | self.data_error = self.length >> 15 77 | self.length &= 0b11111111111111 78 | 79 | def __bytes__(self): 80 | self.length |= (self.length_error << 14) | (self.data_error << 15) 81 | return packet.Message.__bytes__(self) 82 | 83 | 84 | class EthernetF1(packet.Packet): 85 | """ARINC-664 86 | 87 | .. py:attribute:: count 88 | .. py:attribute:: iph_length 89 | 90 | Fixed at 28 91 | """ 92 | 93 | csdw_format = BitFormat(''' 94 | u16 count 95 | u16 iph_length''') 96 | 97 | class Message(packet.Message): 98 | """ 99 | .. py:attribute:: ipts 100 | .. py:attribute:: flags 101 | 102 | * 0 - Actual data 103 | * 1 - Simulated data 104 | 105 | .. py:attribute:: error 106 | .. py:attribute:: length 107 | 108 | Message length (bytes) 109 | 110 | .. py:attribute:: virtual_link 111 | 112 | Lower 16 bits of the destination MAC address 113 | 114 | .. py:attribute:: src_ip 115 | 116 | Source IP address from ARINC IP header 117 | 118 | .. py:attribute:: dst_ip 119 | 120 | Destination IP address from ARINC IP header 121 | 122 | .. py:attribute:: dst_port 123 | 124 | Destination port from the ARINC UDP header 125 | 126 | .. py:attribute:: src_port 127 | 128 | Source port from the ARINC UDP header 129 | """ 130 | 131 | def __repr__(self): 132 | return '' % len(self.data) 133 | 134 | FORMAT = BitFormat(''' 135 | u64 ipts 136 | u8 flags 137 | u8 error 138 | u16 length 139 | u16 virtual_link 140 | p16 141 | u32 src_ip 142 | u32 dst_ip 143 | u16 dst_port 144 | u16 src_port''') 145 | -------------------------------------------------------------------------------- /chapter10/i1394.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class I1394F0(packet.Packet): 7 | """ 8 | .. py:attribute:: count 9 | .. py:attribute:: sync 10 | 11 | Synchronization code 12 | 13 | .. py:attribute:: packet_body_type 14 | """ 15 | 16 | csdw_format = BitFormat(''' 17 | u16 count 18 | p9 19 | u4 sync 20 | u3 packet_body_type''') 21 | 22 | class Message(packet.Message): 23 | """ 24 | .. py:attribute:: reset 25 | 26 | If packet type 0 (bus status) indicates if bus reset has occurred 27 | 28 | .. py:attribute:: ipts 29 | 30 | Present for packet type = 2 (general purpose) 31 | """ 32 | 33 | def __repr__(self): 34 | return '' 35 | 36 | def __init__(self, *args, **kwargs): 37 | packet.Packet.__init__(self, *args, **kwargs) 38 | 39 | # Bus Status 40 | if self.packet_body_type == 0: 41 | self.Message.FORMAT = BitFormat(''' 42 | p31 43 | u1 reset''') 44 | 45 | # Data Streaming 46 | elif self.packet_body_type == 1: 47 | self.Message.length = self.packet.data_length - 4 48 | 49 | # General Purpose 50 | elif self.packet_body_type == 2: 51 | self.Message.length = ( 52 | (self.packet.data_length - 4) / self.transaction_count) - 8 53 | self.Message.FORMAT = BitFormat('u64 ipts') 54 | 55 | 56 | class I1394F1(packet.Packet): 57 | """IEEE 1394 Physical Layer 58 | 59 | .. py:attribute:: count 60 | """ 61 | 62 | csdw_format = BitFormat(''' 63 | u16 count 64 | p16''') 65 | 66 | class Message(packet.Message): 67 | """ 68 | .. py:attribute:: ipts 69 | .. py:attribute:: length 70 | 71 | Transfer length (bytes) 72 | 73 | .. py:attribute:: buffer_overflow 74 | .. py:attribute:: overflow_error 75 | 76 | * 0 - no overflow 77 | * 1 - transfer started correctly, but is longer than standard \ 78 | transfer length 79 | * 2 - previous transfer was a type 1 overflow, this transfer \ 80 | ended correctly 81 | * 3 - previous transfer was a type 1 overflow and this transfer \ 82 | did not end correctly 83 | .. py:attribute:: speed 84 | 85 | See chapter 10 standard 86 | 87 | .. py:attribute:: status 88 | """ 89 | 90 | FORMAT = BitFormat(''' 91 | u64 ipts 92 | u16 length 93 | p1 94 | u1 buffer_overflow 95 | u2 overflow_error 96 | u4 speed 97 | u8 status''') 98 | -------------------------------------------------------------------------------- /chapter10/image.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | __all__ = ('ImageF0', 'ImageF1', 'ImageF2') 7 | 8 | 9 | class ImageMessage: 10 | 11 | def __repr__(self): 12 | return '' 13 | 14 | 15 | class ImageF0(packet.Packet): 16 | """Image data 17 | 18 | .. py:attribute:: segment_length 19 | .. py:attribute:: iph 20 | .. py:attribute:: sum 21 | 22 | * 0 - Less than one complete image 23 | * 1 - One complete image 24 | * 2 - Multiple complete images 25 | * 3 - Multiple incomplete images 26 | 27 | .. py:attribute:: parts 28 | 29 | Indicates which piece[s] are of the frame are contained in the packet: 30 | """ 31 | 32 | csdw_format = BitFormat(''' 33 | u27 length 34 | u1 iph 35 | u3 sum 36 | u3 parts''') 37 | 38 | class Message(packet.Message, ImageMessage): 39 | """ 40 | .. py:attribute:: ipts 41 | 42 | If IPH is true (see above), containts intra-packet timestamp 43 | """ 44 | 45 | def __init__(self, *args, **kwargs): 46 | packet.Packet.__init__(self, *args, **kwargs) 47 | 48 | if self.iph: 49 | self.Message.FORMAT = BitFormat('u64 ipts') 50 | 51 | 52 | class ImageF1(packet.Packet): 53 | """Still imagery 54 | 55 | .. py:attribute:: format 56 | 57 | * 0 - MIL-STD-2500 National Imagery Transmission Format 58 | * 1 - JPEG File Interchange Format 59 | * 2 - JPEG 2000 (ISO/IEC 154444-1) 60 | * 3 - Portable Network Graphics Format (PNG) 61 | 62 | .. py:attribute:: iph 63 | .. py:attribute:: sum 64 | 65 | * 0 - Contains less than one complete image 66 | * 1 - Contains one complete image 67 | * 2 - Contains multiple complete images 68 | * 3 - Contains multiple incomplete messages 69 | 70 | .. py:attribute:: parts 71 | 72 | * 0 - Doesn't contain first or last segment of the image 73 | * 1 - Contains first segment of image 74 | * 2 - Contains multiple complete messages 75 | * 3 - Contains both first and last segment of image 76 | """ 77 | 78 | csdw_format = BitFormat(''' 79 | p23 80 | u4 format 81 | u1 iph 82 | u2 sum 83 | u2 parts''') 84 | 85 | class Message(packet.Message, ImageMessage): 86 | """ 87 | .. py:attribute:: ipts 88 | 89 | If IPH is true (see above), containts intra-packet timestamp 90 | 91 | .. py:attribute:: length 92 | 93 | Length of image or segment (bytes) 94 | """ 95 | 96 | def __init__(self, *args, **kwargs): 97 | packet.Packet.__init__(self, *args, **kwargs) 98 | fmt = '' 99 | if self.iph: 100 | fmt = 'u64 ipts\n' 101 | self.Message.FORMAT = BitFormat(fmt + 'u32 length') 102 | 103 | 104 | class ImageF2(packet.Packet): 105 | """Dynamic Imagery 106 | 107 | .. py:attribute:: format 108 | 109 | Refer to chapter 10 standard 110 | 111 | .. py:attribute:: iph 112 | .. py:attribute:: sum 113 | 114 | * 0 - Contains less than one complete image (segment) 115 | * 1 - Contains one complete image 116 | * 2 - Contains multiple complete images 117 | 118 | .. py:attribute:: parts 119 | 120 | * 0 - Doesn't contain first or last segment of the image 121 | * 1 - Contains first segment of image 122 | * 2 - Contains last segment of image 123 | """ 124 | 125 | csdw_format = BitFormat(''' 126 | p21 127 | u6 format 128 | u1 iph 129 | u2 sum 130 | u2 parts''') 131 | 132 | class Message(packet.Message, ImageMessage): 133 | """ 134 | .. py:attribute:: ipts 135 | 136 | If IPH is true (see above), containts intra-packet timestamp 137 | 138 | .. py:attribute:: length 139 | 140 | Length of image or segment (bytes) 141 | """ 142 | 143 | def __init__(self, *args, **kwargs): 144 | packet.Packet.__init__(self, *args, **kwargs) 145 | fmt = '' 146 | if self.iph: 147 | fmt = 'u64 ipts\n' 148 | self.Message.FORMAT = BitFormat(fmt + 'u32 length') 149 | -------------------------------------------------------------------------------- /chapter10/message.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class MessageF0(packet.Packet): 7 | """ 8 | .. py:attribute:: count 9 | 10 | Segment number of packet (for non-zero packet_type) or a count of \ 11 | messages contained in the packet. 12 | 13 | .. py:attribute:: packet_type 14 | 15 | * 0 - One or more complete messages 16 | * 1 - Beginning of a long message 17 | * 2 - Last part of a long message 18 | * 3 - Middle part of a long message 19 | """ 20 | 21 | csdw_format = BitFormat(''' 22 | u16 count 23 | u2 packet_type 24 | p14''') 25 | 26 | class Message(packet.Message): 27 | """ 28 | .. py:attribute:: ipts 29 | .. py:attribute:: length 30 | 31 | Message length (bytes) 32 | 33 | .. py:attribute:: subchannel 34 | .. py:attribute:: format_error 35 | .. py:attribute:: data_error 36 | """ 37 | 38 | FORMAT = BitFormat(''' 39 | u64 ipts 40 | u16 length 41 | u14 subchannel 42 | u1 format_error 43 | u1 data_error''') 44 | 45 | def __repr__(self): 46 | return '' % len(self.data) 47 | -------------------------------------------------------------------------------- /chapter10/ms1553.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class MS1553F1(packet.Packet): 7 | """MIL-STD-1553B bus data 8 | 9 | .. py:attribute:: count 10 | 11 | Count of 1553 messages in the packet body. 12 | 13 | .. py:attribute:: time_tag_bits 14 | 15 | Indicates which bit of the message is time tagged: 16 | 17 | * 0 - Last bit of the last word of the message 18 | * 1 - First bit of the first word of the message 19 | * 2 - Last bit of the first (command) word of the message 20 | """ 21 | 22 | csdw_format = BitFormat(''' 23 | u24 count 24 | p6 25 | u2 time_tag_bits''') 26 | 27 | class Message(packet.Message): 28 | """ 29 | .. py:attribute:: ipts 30 | .. py:attribute:: le 31 | 32 | Length error 33 | 34 | .. py:attribute:: se 35 | 36 | Sync type error 37 | 38 | .. py:attribute:: we 39 | 40 | Invalid word error 41 | 42 | .. py:attribute:: bus 43 | 44 | Bus ID (A/B) 45 | 46 | .. py:attribute:: me 47 | 48 | Message error 49 | 50 | .. py:attribute:: rt2rt 51 | 52 | Boolean: indicates RT to RT message 53 | 54 | .. py:attribute:: fe 55 | 56 | Format error 57 | 58 | .. py:attribute:: timeout 59 | .. py:attribute:: gap_time 60 | 61 | GAP1 indicates time (in tenths of us) between the command or data \ 62 | word and the first (and only) status word. GAP2 measures time between the \ 63 | last data word and the second status word. 64 | 65 | .. py:attribute:: length 66 | 67 | Message length (bytes) 68 | """ 69 | 70 | def __repr__(self): 71 | return '<1553F1 message %s words>' % (len(self.data) // 2) 72 | 73 | # Note: bitfields from status word are listed in different order than 74 | # shown in the standard. bitstruct doesn't allow for specifying bit 75 | # order across multiple fields. 76 | FORMAT = BitFormat(''' 77 | u64 ipts 78 | 79 | p2 80 | u1 le 81 | u1 se 82 | u1 we 83 | p3 84 | 85 | p2 86 | u1 bus 87 | u1 me 88 | u1 rt2rt 89 | u1 fe 90 | u1 timeout 91 | p1 92 | 93 | u16 gap_time 94 | u16 length''') 95 | 96 | 97 | class MS1553F2(packet.Packet): 98 | """16PP194 Weapons Bus Data 99 | 100 | **Note:** the specifics of the data word format (bus ID, etc.) are not \ 101 | implemented here (yet). 102 | 103 | .. py:attribute:: count 104 | """ 105 | 106 | csdw_format = BitFormat('u32 count') 107 | 108 | class Message(packet.Message): 109 | """ 110 | .. py:attribute:: ipts 111 | .. py:attribute:: length 112 | 113 | IPDH length. Fixed at 24 bytes. 114 | 115 | .. py:attribute:: se 116 | 117 | Status error 118 | 119 | .. py:attribute:: ee 120 | 121 | Echo error 122 | 123 | .. py:attribute:: te 124 | 125 | Transaction error 126 | 127 | .. py:attribute:: re 128 | 129 | Master reset 130 | 131 | .. py:attribute:: tm 132 | 133 | Timeout 134 | """ 135 | 136 | FORMAT = BitFormat(''' 137 | u64 ipts 138 | u16 length 139 | u1 se 140 | p1 141 | u1 ee 142 | p3 143 | u1 te 144 | u1 re 145 | u1 tm 146 | p6''') 147 | 148 | def __repr__(self): 149 | return '<16PP194 Message %s words>' % (len(self.data) // 2) 150 | -------------------------------------------------------------------------------- /chapter10/packet.py: -------------------------------------------------------------------------------- 1 | 2 | from array import array 3 | from datetime import timedelta, datetime 4 | from io import BytesIO 5 | import struct 6 | 7 | from .util import BitFormat 8 | 9 | 10 | class InvalidPacket(Exception): 11 | pass 12 | 13 | 14 | class Packet: 15 | """Base class for the various datatypes. May be created from a file-like 16 | object or started from scratch. 17 | 18 | :param file: Source file to read from. 19 | :type file: file-like 20 | :param kwargs: Initial header values. Read from file if not provided. 21 | 22 | **Chapter 10 Header Attributes** 23 | 24 | - sync_pattern 25 | - channel_id 26 | - packet_length 27 | - data_length 28 | - header_version 29 | - sequence_number 30 | - secondary_header 31 | - ipts_source 32 | - rtc_sync_error 33 | - data_overflow_error 34 | - secondary_format 35 | - data_checksum 36 | - data_type 37 | - rtc 38 | - header_checksum 39 | 40 | **Secondary Header (if present)** 41 | 42 | - secondary_time 43 | - secondary_checksum 44 | 45 | **Other Members** 46 | 47 | .. py:attribute:: FORMAT 48 | :type: BitFormat 49 | 50 | Description of the chapter 10 header 51 | 52 | .. py:attribute:: SECONDARY_FORMAT 53 | :type: BitFormat 54 | 55 | Describes the secondary header 56 | 57 | .. py:attribute:: csdw_format 58 | :type: BitFormat 59 | :value: None 60 | 61 | Describes a datatype's channel specific data word (CSDW) 62 | """ 63 | 64 | FORMAT = BitFormat(''' 65 | u16 sync_pattern 66 | u16 channel_id 67 | u32 packet_length 68 | u32 data_length 69 | u8 header_version 70 | u8 sequence_number 71 | u1 secondary_header 72 | u1 ipts_source 73 | u1 rtc_sync_error 74 | u1 data_overflow_error 75 | u2 secondary_format 76 | u2 data_checksum 77 | u8 data_type 78 | u48 rtc 79 | u16 header_checksum''') 80 | 81 | SECONDARY_FORMAT = BitFormat(''' 82 | u64 secondary_time 83 | p16 reserved 84 | u16 secondary_checksum''') 85 | 86 | csdw_format = BitFormat('u32 csdw') 87 | 88 | _MAX_PACKET_BYTES = 524288 89 | 90 | def __init__(self, file=None, parent=None, **kwargs): 91 | 92 | self.parent = parent 93 | 94 | # Set defaults 95 | for fmt in (self.FORMAT, self.SECONDARY_FORMAT, self.csdw_format): 96 | for name in fmt.names: 97 | setattr(self, name, 0) 98 | self.sync_pattern = 0xeb25 99 | 100 | self.__dict__.update(kwargs) 101 | self._messages = [] 102 | self.buffer = None 103 | 104 | # If file is not given, start from scratch. 105 | if not file: 106 | self.clear() 107 | return 108 | 109 | # Read header if not done already. 110 | if not kwargs: 111 | raw_header = file.read(24) 112 | kwargs = self.FORMAT.unpack(raw_header) 113 | self.__dict__.update(kwargs) 114 | else: 115 | raw_header = self.FORMAT.pack(kwargs) 116 | 117 | # Compute checksum and update attributes with header values. 118 | self.header_sums = sum(array('H', raw_header)[:-1]) & 0xffff 119 | 120 | # Read the secondary header (if any). 121 | secondary = bytes() 122 | if self.secondary_header: 123 | secondary = file.read(12) 124 | self.secondary_sums = sum(array('H', secondary)[:-1]) & 0xffff 125 | self.__dict__.update(self.SECONDARY_FORMAT.unpack(file.read(12))) 126 | 127 | # Read into our own personal buffer. 128 | header_size = 24 + len(secondary) 129 | body = file.read(self.packet_length - header_size) 130 | self.buffer = BytesIO(raw_header + secondary + body) 131 | self.buffer.seek(header_size) 132 | 133 | self.validate() 134 | 135 | # Read channel specific data word (CSDW) 136 | self.__dict__.update(self.csdw_format.unpack(self.buffer.read(4))) 137 | 138 | def get_header_sum(self): 139 | raw_header = self.FORMAT.pack(self.__dict__) 140 | self.header_sums = sum(array('H', raw_header)[:-1]) & 0xffff 141 | return self.header_sums 142 | 143 | def get_time(self): 144 | """Return a timestamp for this packet. Depends on parent C10 object.""" 145 | 146 | if self.parent and self.parent.last_time is not None: 147 | t = self.parent.last_time.time 148 | rtc = self.rtc - self.parent.last_time.rtc 149 | mask = 0xffffffffffff 150 | while rtc > mask: 151 | rtc -= mask 152 | return t + timedelta(seconds=rtc / 10_000_000) 153 | else: 154 | return datetime.now() 155 | 156 | def _read_messages(self): 157 | """Internal: ensure all messages are read in order to manipulate data 158 | in an internal list. 159 | """ 160 | 161 | if self.buffer: 162 | header_size = 36 if self.secondary_header else 24 163 | self.buffer.seek(header_size + 4) 164 | self._messages = list(self) 165 | 166 | def __next__(self): 167 | """Return the next message until the end, then raise StopIteration.""" 168 | 169 | if self.buffer is None: 170 | if not getattr(self, '_message_iterator', None): 171 | self._message_iterator = iter(self._messages) 172 | return next(self._message_iterator) 173 | 174 | if not getattr(self, 'Message', None): 175 | raise StopIteration 176 | 177 | try: 178 | msg = self.Message.from_packet(self) 179 | self._messages.append(msg) 180 | return msg 181 | except EOFError: 182 | self.buffer = None 183 | raise StopIteration 184 | 185 | def __iter__(self): 186 | if self.buffer: 187 | return self 188 | else: 189 | return iter(self._messages) 190 | 191 | def validate(self, silent=False): 192 | """Validate the packet using checksums and verifying fields. If silent 193 | = False raises InvalidPacket. 194 | """ 195 | 196 | err = None 197 | if self.sync_pattern != 0xeb25: 198 | err = InvalidPacket('Incorrect sync pattern!') 199 | elif self.get_header_sum() != self.header_checksum: 200 | err = InvalidPacket('Header checksum mismatch!') 201 | elif self.secondary_header: 202 | if self.secondary_sums != self.secondary_checksum: 203 | err = InvalidPacket('Secondary header checksum mismatch!') 204 | if self.data_length > self._MAX_PACKET_BYTES: 205 | err = InvalidPacket( 206 | f'Data length {self.data_length} larger than allowed!') 207 | 208 | if err: 209 | if not silent: 210 | raise err 211 | return False 212 | return True 213 | 214 | def __len__(self): 215 | """Return length if we can find one, else raise TypeError.""" 216 | 217 | if self.buffer is None and self._messages is not None: 218 | return len(self._messages) 219 | if hasattr(self, 'count'): 220 | return self.count 221 | elif getattr(self, 'Message', None) and self.Message.length: 222 | msg_size = self.Message.length 223 | if getattr(self.Message, 'FORMAT', None): 224 | msg_size += self.Message.FORMAT.calcsize() // 8 225 | return (self.data_length - 4) // msg_size 226 | raise TypeError("object of type '%s' has no len()" % self.__class__) 227 | 228 | def _raw_body(self): 229 | """Returns the raw bytes of the packet body, including the CSDW.""" 230 | 231 | # Pack messages into body 232 | if getattr(self, 'Message', None): 233 | body = b''.join(bytes(m) for m in self._messages) 234 | 235 | # Pull raw bytes if not a message-based packet 236 | else: 237 | self.buffer.seek(36 if self.secondary_header else 24) 238 | body = self.buffer.read(self.data_length - 4) 239 | return self.csdw_format.pack(self.__dict__) + body 240 | 241 | def __bytes__(self): 242 | """Returns the entire packet as raw bytes.""" 243 | 244 | self._read_messages() 245 | 246 | body = self._raw_body() 247 | self.data_length = len(body) 248 | 249 | # Add filler to maintain 32 bit alignment 250 | checksum_size = (0, 1, 2, 4)[self.data_checksum] 251 | while (checksum_size + len(body)) % 4: 252 | body += b'\0' 253 | 254 | # Compute the data checksum 255 | if self.data_checksum: 256 | fmt = 'xBHI'[self.data_checksum] 257 | checksum = sum(array(fmt, body)) 258 | checksum &= (0, 0xff, 0xffff, 0xffffffff)[self.data_checksum] 259 | body += struct.pack(fmt, checksum) 260 | 261 | # Pack header (with updated checksum) and secondary header if needed. 262 | header_length = 36 if self.secondary_header else 24 263 | self.packet_length = header_length + len(body) 264 | self.header_checksum = self.get_header_sum() 265 | raw = self.FORMAT.pack(self.__dict__) 266 | if self.secondary_header: 267 | raw += self.SECONDARY_FORMAT.pack(self.__dict__) 268 | 269 | # Add body and filler 270 | return raw + body 271 | 272 | def __repr__(self): 273 | return '<{} {} bytes>'.format( 274 | self.__class__.__name__, getattr(self, 'packet_length', '?')) 275 | 276 | def append(self, *messages): 277 | """Add one or more messages to the packet.""" 278 | 279 | self._read_messages() 280 | for message in messages: 281 | self._messages.append(message) 282 | 283 | def clear(self): 284 | """Remove all messages.""" 285 | 286 | self._read_messages() 287 | self._messages = [] 288 | 289 | def copy(self): 290 | """Duplicate this packet.""" 291 | 292 | return self.__class__(**self.__dict__.copy()) 293 | 294 | def remove(self, i): 295 | """Remove message at index 'i'""" 296 | 297 | self._read_messages() 298 | del self._messages[i] 299 | 300 | # Pickle compatability. 301 | def __setstate__(self, state): 302 | self.__dict__.update(state) 303 | 304 | def __getstate__(self): 305 | self._read_messages() 306 | return {k: v for k, v in self.__dict__.items() if not callable(v)} 307 | 308 | 309 | class Message: 310 | """The base class for packet message data. Subclasses define FORMAT, 311 | length, etc. to give the format and can be instantiated empty or from a 312 | packet using the from_packet method. 313 | 314 | :param bytes data: The binary data to be stored. May be empty (for 315 | instance, if FORMAT fully describes the format). 316 | :param Packet parent: The Packet object this message belongs to. 317 | :param kwargs: Arbitrary key-value pairs to add as attributes to the item 318 | instance. Used for IPH values. 319 | 320 | **Class Attributes** 321 | 322 | .. py:attribute:: FORMAT 323 | :type: BitFormat 324 | :value: None 325 | 326 | Describes the intra-packet header (IPH) or message format in general. 327 | 328 | .. py:attribute:: length 329 | :type: int 330 | :value: 0 331 | 332 | Byte size of message body. If not specified will look for a 'length' 333 | field in the IPH. 334 | 335 | **Instance Attributes** 336 | 337 | .. py:attribute:: parent 338 | :type: Packet or None 339 | 340 | The Packet object this message is attached to. 341 | 342 | .. py:attribute:: data 343 | :type: bytes 344 | 345 | Raw data not identified in FORMAT 346 | """ 347 | 348 | FORMAT = None 349 | length = 0 350 | 351 | def __init__(self, data=b'', parent=None, **kwargs): 352 | if self.FORMAT: 353 | self.__dict__.update({name: 0 for name in self.FORMAT.names}) 354 | self.__dict__.update(kwargs) 355 | self.parent = parent 356 | self.data = data 357 | 358 | def get_time(self): 359 | """Return a timestamp for this message based on parent packet.""" 360 | 361 | if self.parent is None or not hasattr(self, 'ipts'): 362 | raise AttributeError( 363 | 'Must have parent packet and IPTS to get time.') 364 | 365 | rtc = self.ipts - self.parent.rtc 366 | mask = 0xffffffffffff 367 | while rtc > mask: 368 | rtc -= mask 369 | return self.parent.get_time() + timedelta(seconds=rtc / 10_000_000) 370 | 371 | @classmethod 372 | def from_packet(cls, packet): 373 | """Helper method to read a message from packet.""" 374 | 375 | # Exit if we've read all messages. 376 | if hasattr(packet, 'count') and packet.count == len(packet._messages): 377 | raise EOFError 378 | 379 | # Exit when we reach the end of the packet body 380 | end = packet.data_length + (packet.secondary_header and 36 or 24) 381 | if packet.buffer.tell() >= end: 382 | raise EOFError 383 | 384 | # Read and parse the IPH 385 | iph = {} 386 | if getattr(cls, 'FORMAT', None): 387 | iph_size = cls.FORMAT.calcsize() // 8 388 | raw = packet.buffer.read(iph_size) 389 | if len(raw) < iph_size: 390 | raise EOFError 391 | iph = cls.FORMAT.unpack(raw) 392 | 393 | # Read the message data and account for filler if length is odd. 394 | length = iph.get('length', cls.length) 395 | data = packet.buffer.read(length) 396 | if length % 2: 397 | packet.buffer.seek(1, 1) 398 | 399 | return packet.Message(data, parent=packet, **iph) 400 | 401 | def __bytes__(self): 402 | """Return bytes() containing any IPH and data.""" 403 | 404 | raw = bytes() 405 | if self.FORMAT is not None: 406 | raw += self.FORMAT.pack(self.__dict__) 407 | raw += self.data 408 | if len(raw) % 2: 409 | raw += b'\x00' 410 | return raw 411 | -------------------------------------------------------------------------------- /chapter10/parallel.py: -------------------------------------------------------------------------------- 1 | 2 | from .packet import Packet 3 | from .util import BitFormat 4 | 5 | 6 | class ParallelF0(Packet): 7 | """ 8 | .. py:attribute:: scan_number 9 | 10 | If present contains the scan number of the first scan (for DCRsi data) 11 | 12 | .. py:attribute:: type 13 | 14 | If between 0x10 and 0x80 contains the number of bits of recorded data 15 | """ 16 | 17 | csdw_format = BitFormat(''' 18 | u24 scan_number 19 | u8 type 20 | ''') 21 | -------------------------------------------------------------------------------- /chapter10/pcm.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class PCMF1(packet.Packet): 7 | """IRIG 106 Chapter 4/8 PCM data. 8 | 9 | .. py:attribute:: sync_offset 10 | 11 | Offset into the major frame for the first data word in the packet. Not 12 | applicable for packed or throughput modes. 13 | 14 | .. py:attribute:: alignment 15 | 16 | Indicates 32 bit alignment if 1 or 16 bit by default. 17 | 18 | .. py:attribute:: throughput 19 | 20 | Indicates throughput mode (which would make most state flags 21 | unapplicable) 22 | 23 | .. py:attribute:: packed 24 | 25 | Indicates presence of packed data. 26 | 27 | .. py:attribute:: unpacked 28 | 29 | Indicates presense of unpacked data. 30 | 31 | .. py:attribute:: major_frame_status 32 | 33 | Combination major frame check flag and major frame lock 34 | 35 | .. py:attribute:: minor_frame_status 36 | 37 | Combination minor frame check flag and minor frame lock 38 | 39 | .. py:attribute:: minor_frame_indicator 40 | 41 | Indicates if the first data word in the packjet is the beginning of a 42 | minor frame. 43 | 44 | .. py:attribute:: major_frame_indicator 45 | 46 | Indicates if the first data word in the packet is the beginning of a 47 | major frame. 48 | 49 | .. py:attribute:: iph 50 | 51 | Indicates presence of Intra-Packet Header (IPTS and IPDH) 52 | """ 53 | 54 | csdw_format = BitFormat(''' 55 | u18 sync_offset 56 | u1 alignment 57 | u1 throughput 58 | u1 packed 59 | u1 unpacked 60 | p2 61 | u2 major_frame_status 62 | u2 minor_frame_status 63 | u1 minor_frame_indicator 64 | u1 major_frame_indicator 65 | u1 iph 66 | p1''') 67 | 68 | class Message(packet.Message): 69 | """ 70 | .. py:attribute:: ipts 71 | 72 | Provided if IPH is true (see above). 73 | 74 | .. py:attribute:: lock_status 75 | """ 76 | 77 | length = 12 # Two words sync, four data. 78 | 79 | def __repr__(self): 80 | return '' 81 | 82 | def __init__(self, *args, **kwargs): 83 | packet.Packet.__init__(self, *args, **kwargs) 84 | 85 | # Throughput basically means we don't need to do anything. 86 | if self.throughput: 87 | self.Message = None 88 | 89 | else: 90 | fmt = ''' 91 | u64 ipts 92 | p12 93 | u4 lock_status''' 94 | 95 | # Extra IPH word in 32 bit alignment. 96 | if self.iph and self.alignment: 97 | fmt += '\np16' 98 | 99 | self.Message.FORMAT = BitFormat(fmt) 100 | -------------------------------------------------------------------------------- /chapter10/time.py: -------------------------------------------------------------------------------- 1 | 2 | from datetime import datetime, timedelta 3 | 4 | from .util import BitFormat 5 | from .packet import Packet 6 | 7 | 8 | class TimeF1(Packet): 9 | """Time Packets 10 | 11 | **Note:** Low level fields such as HSn, TSn, etc. are not documented here 12 | nor are they intended to be used directly. Use the .time attribute instead. 13 | 14 | .. py:attribute:: time_source 15 | 16 | * 0 - Internal (recorder clock) 17 | * 1 - External 18 | * 2 - Internal from RMM 19 | * 15 - None 20 | 21 | .. py:attribute:: time_format 22 | 23 | Indicates time data packet format 24 | 25 | * 0 - IRIG-B 26 | * 1 - IRIG-A 27 | * 2 - IRIG-G 28 | * 3 - Real-Time clock 29 | * 4 - UTC time from GPS 30 | * 5 - Native GPS time 31 | * 15 - None (payload invalid) 32 | 33 | .. py:attribute:: leap 34 | 35 | Indicates if this is a leap year. 36 | 37 | .. py:attribute:: date_format 38 | 39 | 0 for IRIG day format, 1 for month and year format. 40 | 41 | .. py:attribute:: irig_source 42 | 43 | IRIG time source 44 | 45 | * 0 - TCG freewheeling (no/lost time source) 46 | * 1 - TCG freewheeling from .TIME command 47 | * 2 - TCG freewheeling from RMM time 48 | * 3 - TCG locked to external IRIG time signal 49 | * 4 - TCG locked to external GPS 50 | * 5 - TCG locked to external Network Time Protocol (NTP) 51 | * 6 - TCG locked to external Precision Time Protocol (PTP) 52 | * 7 - TCG locked to external embedded time from input track/channel\ 53 | such as PCM or 1553 54 | 55 | .. py:attribute:: time 56 | 57 | Python datetime object representing the packet payload. 58 | """ 59 | 60 | csdw_format = BitFormat(''' 61 | u4 time_format 62 | u4 time_source 63 | 64 | u4 irig_source 65 | 66 | p2 67 | u1 date_format 68 | u1 leap 69 | 70 | p16''') 71 | 72 | def __init__(self, *args, **kwargs): 73 | Packet.__init__(self, *args, **kwargs) 74 | 75 | self.data_format = ''' 76 | u4 Hmn 77 | u4 Tmn 78 | u4 TSn 79 | u4 Sn 80 | 81 | u4 TMn 82 | u4 Mn 83 | u4 THn 84 | u4 Hn 85 | 86 | u4 TDn 87 | u4 Dn 88 | ''' 89 | 90 | if not self.date_format: 91 | self.data_format += 'u8 HDn' 92 | else: 93 | self.data_format += ''' 94 | p3 95 | u1 TOn 96 | u4 On 97 | 98 | u4 TYn 99 | u4 Yn 100 | p2 101 | u2 OYn 102 | u4 HYn 103 | ''' 104 | 105 | self.data_format = BitFormat(self.data_format) 106 | 107 | if not self.buffer: 108 | for name in self.data_format.names: 109 | setattr(self, name, 0) 110 | self._initial_time = 0 111 | if getattr(self, "time", True): 112 | self.time = datetime.now() 113 | return 114 | 115 | raw = self.buffer.read(self.data_length - 4) 116 | self.__dict__.update(self.data_format.unpack(raw)) 117 | 118 | ms = (self.Hmn * 100) + (self.Tmn * 10) 119 | seconds = self.Sn + (self.TSn * 10) 120 | minutes = self.Mn + (self.TMn * 10) 121 | hours = self.Hn + (self.THn * 10) 122 | 123 | # IRIG day format 124 | if not self.date_format: 125 | day = self.Dn + (self.HDn * 100) + (self.TDn * 10) 126 | 127 | today = datetime.today() 128 | self.time = datetime(today.year, 1, 1) + timedelta(day - 1) 129 | 130 | # Month and Year Format 131 | else: 132 | month = self.On + (self.TOn * 10) 133 | day = self.Dn + (self.TDn * 10) 134 | year = self.Yn + (self.TYn * 10) + (self.HYn * 100) + ( 135 | self.OYn * 1000) 136 | self.time = datetime(year, month, day) 137 | 138 | self.time = self.time.replace( 139 | microsecond=ms * 1000, 140 | second=seconds, 141 | minute=minutes, 142 | hour=hours, 143 | tzinfo=None) 144 | self._initial_time = self.time 145 | 146 | def _raw_body(self): 147 | ms = self.time.microsecond // 1000 148 | self.Hmn = ms // 100 149 | self.Tmn = (ms - (self.Hmn * 100)) // 10 150 | 151 | self.TSn = self.time.second // 10 152 | self.Sn = self.time.second - (self.TSn * 10) 153 | self.TMn = self.time.minute // 10 154 | self.Mn = self.time.minute - (self.TMn * 10) 155 | self.THn = self.time.hour // 10 156 | self.Hn = self.time.hour - (self.THn * 10) 157 | 158 | # Month and year format 159 | if self.date_format: 160 | day = self.time.day 161 | self.TDn = day // 10 162 | day -= self.TDn * 10 163 | self.Dn = day 164 | 165 | month = self.time.month 166 | self.TOn = month // 10 167 | month -= self.TOn * 10 168 | self.On = month 169 | 170 | year = self.time.year 171 | self.OYn = year // 1000 172 | year -= self.OYn * 1000 173 | self.HYn = year // 100 174 | year -= self.HYn * 100 175 | self.TYn = year // 10 176 | year -= self.TYn * 10 177 | self.Yn = year 178 | 179 | else: 180 | day = int(self.time.strftime('%j')) 181 | self.HDn = day // 100 182 | day -= self.HDn * 100 183 | self.TDn = day // 10 184 | day -= self.TDn * 10 185 | self.Dn = day 186 | 187 | body = self.data_format.pack(self.__dict__) 188 | if len(body) % 2: 189 | body += b'\0' 190 | 191 | # Add CSDW and body 192 | return self.csdw_format.pack(self.__dict__) + body 193 | 194 | def __bool__(self): 195 | return bool(self.time) 196 | -------------------------------------------------------------------------------- /chapter10/uart.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | class UARTF0(packet.Packet): 7 | """ 8 | .. py:attribute:: iph 9 | """ 10 | 11 | csdw_format = BitFormat('''' % len(self.data) 29 | 30 | def __init__(self, *args, **kwargs): 31 | packet.Packet.__init__(self, *args, **kwargs) 32 | 33 | fmt = 'u64 ipts' if self.iph else '' 34 | fmt += ''' 35 | u16 length 36 | u14 subchannel 37 | p1 38 | u1 parity_error''' 39 | self.Message.FORMAT = BitFormat(fmt) 40 | -------------------------------------------------------------------------------- /chapter10/util.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | import cbitstruct as bitstruct 4 | except ImportError: 5 | import bitstruct 6 | 7 | 8 | class BitFormat: 9 | """Bitstruct wrapper that allows for compiling from readable format 10 | strings. 11 | 12 | :param str src: "struct" like format with newline separated pairs of , 13 | where fmt is a bitstruct format string (generally as 14 | "") and name is the attribute name to use. 15 | :param str byteswap: Optional bitstruct style byteswap description. See 16 | bitstruct docs 17 | :value byteswap: None 18 | 19 | .. py:attribute:: fmt_str 20 | :type: string 21 | 22 | The original format string. 23 | 24 | .. py:attribute:: names 25 | :type: list 26 | 27 | A list of the field names from the format string. 28 | """ 29 | 30 | def __init__(self, src, byteswap=None): 31 | self.byteswap = byteswap 32 | self.fmt_str = '' 33 | self.names = [] 34 | for line in src.strip().splitlines(): 35 | line = line.strip().split() 36 | if not line: 37 | continue 38 | if len(line) == 1 or line[-1].lower().endswith('reserved'): 39 | fmt = line[0] 40 | else: 41 | fmt, name = line 42 | self.names.append(name) 43 | self.fmt_str += fmt 44 | 45 | # Default to little endian if we're not explicitly defining swapping 46 | if not byteswap and self.fmt_str[-1] not in '<>': 47 | self.fmt_str += '<' 48 | 49 | self._compiled = bitstruct.compile(self.fmt_str, names=self.names) 50 | 51 | def __getattr__(self, name, default=None): 52 | if name in ('byteswap', 'pack', 'unpack', 'raw', 'fmt_str', 'names'): 53 | return object.__getattr__(self, name, default) 54 | return getattr(self._compiled, name, default) 55 | 56 | def unpack(self, data): 57 | """Use compiled format to unpack data. Uses self.byteswap if provided. 58 | 59 | :param bytes data: Raw data to unpack 60 | :returns dict: Unpacked values based on compiled format. 61 | """ 62 | 63 | if self.byteswap: 64 | data = bitstruct.byteswap(self.byteswap, data) 65 | self.raw = data 66 | return self._compiled.unpack(data) 67 | 68 | def pack(self, values): 69 | raw = self._compiled.pack(values) 70 | if self.byteswap: 71 | raw = bitstruct.byteswap(self.byteswap, raw) 72 | return raw 73 | 74 | 75 | # TODO: is this needed any more? 76 | class Buffer(object): 77 | """File wrapper that raises EOF on a short read.""" 78 | 79 | def __init__(self, io): 80 | self.io = io 81 | self.tell = self.io.tell 82 | self.seek = self.io.seek 83 | 84 | def read(self, size=None): 85 | value = self.io.read(size) 86 | if size and len(value) != size: 87 | raise EOFError 88 | return value 89 | -------------------------------------------------------------------------------- /chapter10/video.py: -------------------------------------------------------------------------------- 1 | 2 | from .util import BitFormat 3 | from . import packet 4 | 5 | 6 | __all__ = ('VideoF0', 'VideoF1', 'VideoF2') 7 | 8 | 9 | class Video(packet.Packet): 10 | """Generic video superclass.""" 11 | 12 | class Message(packet.Message): 13 | length = 188 14 | 15 | def __repr__(self): 16 | return '' % len(self.data) 17 | 18 | def __init__(self, *args, **kwargs): 19 | packet.Packet.__init__(self, *args, **kwargs) 20 | 21 | if self.iph: 22 | self.Message.FORMAT = BitFormat('u64 ipts') 23 | 24 | 25 | class VideoF0(Video): 26 | """MPEG2/H.264 27 | 28 | .. py:attribute:: byte_alignment 29 | 30 | * 0 - Little endian 31 | * 1 - Big endian 32 | 33 | .. py:attribute:: payload 34 | 35 | * 0 - MPEG-2 MP @ ML 36 | * 1 - H.264 MP @ L2.1 37 | * 2 - H.264 MP @ L2.2 38 | * 3 - H.264 MP @ L3 39 | 40 | .. py:attribute:: key_length 41 | 42 | Indicates if key-length-value metadata is present 43 | 44 | .. py:attribute:: scr_rtc_sync 45 | 46 | Indicates if MPEG-2 SCR is RTC 47 | 48 | .. py:attribute:: iph 49 | .. py:attribute:: embedded_time 50 | 51 | Indicates time embedded in MPEG-2 stream. 52 | """ 53 | 54 | csdw_format = BitFormat(''' 55 | p23 56 | u1 byte_alignment 57 | u4 payload 58 | u1 key_length 59 | u1 scr_rtc_sync 60 | u1 iph 61 | u1 embedded_time''') 62 | 63 | class Message(Video.Message): 64 | """ 65 | .. py:attribute:: ipts 66 | 67 | If indicated by iph flag in CSDW (see above) 68 | """ 69 | 70 | 71 | class VideoF1(Video): 72 | """ISO 13818-1 MPEG-2 bit stream 73 | 74 | .. py:attribute:: count 75 | .. py:attribute:: type 76 | 77 | * 0 - Transport data bit stream 78 | * 1 - Program data bit stream 79 | 80 | .. py:attribute:: mode 81 | 82 | * 0 - CBR stream 83 | * 1 - Variable bit rate stream 84 | 85 | .. py:attribute:: embedded_time 86 | 87 | Indicates time embedded in MPEG-2 stream. 88 | 89 | .. py:attribute:: encoding_profile 90 | 91 | * 0 - Simple profile @ main level 92 | * 1 - Main profile @ low level 93 | * 2 - Main profile @ main level 94 | * 3 - Main profile @ high-1440 level 95 | * 4 - Main profile @ high level 96 | * 5 - SNR profile @ low level 97 | * 6 - SNR profile @ main level 98 | * 7 - Spatial profile @ high-1440 level 99 | * 8 - High profile @ main level 100 | * 9 - High profile @ high-1440 level 101 | * 10 - High profile @ high level 102 | * 11 - 4:2:2 profile @ main level 103 | 104 | .. py:attribute:: iph 105 | .. py:attribute:: scr_rtc_sync 106 | 107 | Indicates if MPEG-2 SCR is RTC 108 | 109 | .. py:attribute:: key_length 110 | 111 | Indicates if key length value metadata is present. 112 | 113 | """ 114 | 115 | csdw_format = BitFormat(''' 116 | u12 count 117 | u1 type 118 | u1 mode 119 | u1 embedded_time 120 | u4 encoding_profile 121 | u1 iph 122 | u1 scr_rtc_sync 123 | u1 key_length 124 | p10''') 125 | 126 | class Message(Video.Message): 127 | """ 128 | .. py:attribute:: ipts 129 | 130 | If indicated by iph flag in CSDW (see above) 131 | """ 132 | 133 | 134 | class VideoF2(Video): 135 | csdw_format = BitFormat(''' 136 | u12 count 137 | u1 type 138 | u1 mode 139 | u1 embedded_time 140 | u4 encoding_profile 141 | u1 iph 142 | u1 scr_rtc_sync 143 | u1 key_length_value 144 | u4 encoding_level 145 | u1 audio_encoding_type 146 | p5''') 147 | 148 | class Message(Video.Message): 149 | """ 150 | .. py:attribute:: ipts 151 | 152 | If indicated by iph flag in CSDW (see above) 153 | """ 154 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/conftest.py -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/api/datatypes.rst: -------------------------------------------------------------------------------- 1 | 2 | Datatypes 3 | ========= 4 | 5 | Computer 6 | -------- 7 | 8 | .. automodule:: chapter10.computer 9 | :members: 10 | 11 | PCM 12 | --- 13 | 14 | .. automodule:: chapter10.pcm 15 | :members: 16 | 17 | Time 18 | ---- 19 | 20 | .. automodule:: chapter10.time 21 | :members: 22 | 23 | Mil-Std-1553 24 | ------------ 25 | 26 | .. automodule:: chapter10.ms1553 27 | :members: 28 | 29 | Analog 30 | ------ 31 | 32 | .. automodule:: chapter10.analog 33 | :members: 34 | 35 | Discrete 36 | -------- 37 | 38 | .. automodule:: chapter10.discrete 39 | :members: 40 | 41 | Message 42 | ------- 43 | 44 | .. automodule:: chapter10.message 45 | :members: 46 | 47 | Arinc 429 48 | --------- 49 | 50 | .. automodule:: chapter10.arinc429 51 | :members: 52 | 53 | Video 54 | ----- 55 | 56 | .. automodule:: chapter10.video 57 | :members: 58 | 59 | Image 60 | ----- 61 | 62 | .. automodule:: chapter10.image 63 | :members: 64 | 65 | UART 66 | ---- 67 | 68 | .. automodule:: chapter10.uart 69 | :members: 70 | 71 | I1394 72 | ----- 73 | 74 | .. automodule:: chapter10.i1394 75 | :members: 76 | 77 | Parallel 78 | -------- 79 | 80 | .. automodule:: chapter10.parallel 81 | :members: 82 | 83 | Ethernet 84 | -------- 85 | 86 | .. automodule:: chapter10.ethernet 87 | :members: 88 | 89 | -------------------------------------------------------------------------------- /docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:module:: chapter10 3 | :synopsis: Pure-python library for parsing Chapter 10 data. 4 | 5 | API Reference 6 | ============= 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | datatypes 12 | util 13 | 14 | .. automodule:: chapter10.c10 15 | :members: 16 | 17 | .. autoclass:: chapter10.packet.Packet 18 | :members: 19 | 20 | .. autoexception:: chapter10.packet.InvalidPacket 21 | 22 | .. autoclass:: chapter10.packet.Message 23 | :members: 24 | -------------------------------------------------------------------------------- /docs/source/api/util.rst: -------------------------------------------------------------------------------- 1 | 2 | Utilities 3 | ========= 4 | 5 | .. automodule:: chapter10.util 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | from datetime import datetime 14 | from chapter10 import version as release 15 | 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = 'PyChapter10' 20 | copyright = datetime.now().strftime('%Y, ATAC') 21 | author = 'Micah Ferrill - ATAC' 22 | 23 | # The full version, including alpha/beta/rc tags 24 | # release = '1.1' 25 | 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = [] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'nature' 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ['_static'] 56 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | PyChapter10 3 | =========== 4 | 5 | PyChapter10 is an open source pure Python library for reading and writing `IRIG 6 | 106 Chapter 10 (now 11)`_ files. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | api/index 12 | samples/index 13 | 14 | 15 | Library Overview 16 | ================ 17 | 18 | Basic Structure & Usage 19 | ----------------------- 20 | 21 | PyChapter10 makes every effort to provide a pythonic interface to Chapter 10 22 | data as in the following example:: 23 | 24 | for packet in C10('file.c10'): 25 | for message in packet: 26 | print(message.rtc) 27 | 28 | The top-level C10 object represents a given Chapter 10 file or stream and can 29 | be initialized from file or filename (default) or from bytes/string using the 30 | from_string method. C10 objects contain Packet objects of various data types 31 | (see API reference above). Packets attributes containing parsed/generated packet 32 | information and generally consist of a number of messages which can be iterated 33 | over and modified in turn. 34 | 35 | All of these types and classes respond to the usual python introspection 36 | resources such as help() and dir(). 37 | 38 | Modifying Data 39 | -------------- 40 | 41 | Calling bytes() on a packet will compile the packet header, CSDW 42 | (channel specific data word), and body (including messages) to raw bytes. You 43 | can modify packets and messages before dumping to bytes and those changes will 44 | be reflected in the resulting bytes object. For example:: 45 | 46 | for msg in ethernet_packet: 47 | msg.ethernet_speed = 3 48 | 49 | out_file.write(bytes(ethernet_packet)) 50 | 51 | Generating Data from Scratch 52 | ---------------------------- 53 | 54 | You can also generate your own data. By creating a packet object of the 55 | intended type and passing header values (or even CSDW and data) as keyword 56 | arguments:: 57 | 58 | messages = ... 59 | p = EthernetF0(count=10) 60 | for message in messages: 61 | p.append(EthernetF0.Message(data=message, length=len(message))) 62 | 63 | Defining Data Types 64 | ------------------- 65 | 66 | Data formats are specified using a wrapper around bitstruct_. Every data type 67 | has a channel specific data word (CSDW) that may look something like this 68 | (Ethernet given as an example):: 69 | 70 | class EthernetF0(packet.Packet): 71 | # ... 72 | 73 | csdw_format = BitFormat(''' 74 | u16 count 75 | p9 76 | u3 ttb 77 | u4 format''') 78 | 79 | Similar to a C struct fields are specified with a type (uint by default) and 80 | bit length. "p" signifies padding or "reserved" as the chapter 10 standard 81 | may call it. Various data types will also specify the format of the messages 82 | they contain be that 1553, ethernet, etc. Going back to the Ethernet 83 | example:: 84 | 85 | class EthernetF0(packet.Packet): 86 | # ... 87 | 88 | class Message(packet.Message): 89 | # ... 90 | 91 | def __repr__(self): 92 | return '' % len(self.data) 93 | 94 | FORMAT = BitFormat(''' 95 | u64 ipts 96 | u14 length 97 | u1 data_length_error 98 | u1 data_crc_error 99 | u8 network_id 100 | u1 crc_error 101 | u1 frame_error 102 | u2 content 103 | u4 ethernet_speed''') 104 | 105 | Now we have the intra-packet/message header defined and since it includes a 106 | "length" field the appropriate number of bytes are read each time we get a new 107 | message. We also define a "\__repr\__" function to help with debugging. 108 | 109 | Contributing 110 | ============ 111 | 112 | Feedback, issues, and pull requests are all welcome on the `main repo`_. See 113 | the CONTRIBUTING document in github for more details. If you're not sure where 114 | to get started check out the issue tracker on github. 115 | 116 | 117 | Indices and tables 118 | ================== 119 | 120 | * :ref:`genindex` 121 | * :ref:`modindex` 122 | * :ref:`search` 123 | 124 | 125 | .. _IRIG 106 Chapter 10 (now 11): https://www.wsmr.army.mil/RCCsite/Documents/106-20_Telemetry_Standards/chapter11.pdf 126 | .. _bitstruct: https://bitstruct.readthedocs.io/en/latest/ 127 | .. _main repo: https://github.com/atac/pychapter10 128 | -------------------------------------------------------------------------------- /docs/source/samples/allbus.rst: -------------------------------------------------------------------------------- 1 | 2 | Make 1553 Data Single-Bus 3 | ========================= 4 | 5 | :: 6 | 7 | #!/usr/bin/env python 8 | 9 | """usage: allbus.py [-b] 10 | 11 | Switch 1553 format 1 messages to the same bus. Defaults to A unless "-b" flag 12 | is present. 13 | """ 14 | 15 | import sys 16 | 17 | from chapter10 import C10 18 | 19 | 20 | if __name__ == '__main__': 21 | 22 | # Parse args 23 | if len(sys.argv) < 3: 24 | print(__doc__) 25 | raise SystemExit 26 | bus = int(sys.argv[-1].lower() == '-b') 27 | if bus: 28 | src, dst = sys.argv[-3:-1] 29 | else: 30 | src, dst = sys.argv[-2:] 31 | 32 | # Walk through the source file and write data to the output file. 33 | with open(dst, 'wb') as out: 34 | for packet in C10(src): 35 | 36 | # Write non-1553 out as-is. 37 | if packet.data_type == 0x19: 38 | for msg in packet: 39 | msg.bus = bus 40 | 41 | out.write(bytes(packet)) 42 | -------------------------------------------------------------------------------- /docs/source/samples/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Code Samples 3 | ============ 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: Contents: 8 | 9 | stat 10 | allbus 11 | -------------------------------------------------------------------------------- /docs/source/samples/stat.rst: -------------------------------------------------------------------------------- 1 | 2 | Scan Channels and Data Types 3 | ============================ 4 | 5 | Don't mind the heavy formatting in the printing section. It's simply whitespace 6 | handling:: 7 | 8 | #!/usr/bin/env python 9 | 10 | import sys 11 | 12 | from chapter10 import C10, TYPES 13 | 14 | 15 | if __name__ == '__main__': 16 | 17 | # Get commandline args. 18 | if len(sys.argv) < 2: 19 | print('usage: stat.py ') 20 | raise SystemExit 21 | filename = sys.argv[-1] 22 | 23 | # Scan channel info 24 | channels = {} 25 | for packet in C10(filename): 26 | key = (packet.channel_id, packet.data_type) 27 | if key not in channels: 28 | channels[key] = { 29 | 'packets': 0, 30 | 'size': 0, 31 | 'type': packet.data_type, 32 | 'id': packet.channel_id} 33 | 34 | channels[key]['packets'] += 1 35 | channels[key]['size'] += packet.packet_length 36 | 37 | # Print details for each channel. 38 | print('{} {:>13} {:>38} {:>16}'.format( 39 | 'Channel ID', 'Data Type', 'Packets', 'Size')) 40 | print('-' * 80) 41 | packets, size = 0, 0 42 | for key, channel in sorted(channels.items()): 43 | print('Channel {:<7}'.format(channel['id']), end='') 44 | hextype = hex(channel['type'])[2:] 45 | label = TYPES[channel['type']].__name__ 46 | print('{:>2} - {:<30}'.format(hextype, label), end='') 47 | print('{:,}'.format(channel['packets']).rjust(13), end='') 48 | print('{:>16,}b'.format(channel['size'])) 49 | packets += channel['packets'] 50 | size += channel['size'] 51 | 52 | # Print file summary. 53 | print('-' * 80) 54 | print('''Summary for {}: 55 | Channels: {} 56 | Packets: {:,} 57 | Size: {:,} bytes'''.format(filename, len(channels), packets, size)) 58 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "docs", "dev"] 6 | cross_platform = true 7 | static_urls = false 8 | lock_version = "4.3" 9 | content_hash = "sha256:2c5e01d303075ec38248fb8570d714f698f18aca1742ff0bf557eb07b673cf16" 10 | 11 | [[package]] 12 | name = "alabaster" 13 | version = "0.7.12" 14 | summary = "A configurable sidebar-enabled Sphinx theme" 15 | files = [ 16 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 17 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 18 | ] 19 | 20 | [[package]] 21 | name = "atomicwrites" 22 | version = "1.4.0" 23 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | summary = "Atomic file writes." 25 | files = [ 26 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 27 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 28 | ] 29 | 30 | [[package]] 31 | name = "attrs" 32 | version = "21.4.0" 33 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 34 | summary = "Classes Without Boilerplate" 35 | files = [ 36 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 37 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 38 | ] 39 | 40 | [[package]] 41 | name = "babel" 42 | version = "2.9.1" 43 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 44 | summary = "Internationalization utilities" 45 | dependencies = [ 46 | "pytz>=2015.7", 47 | ] 48 | files = [ 49 | {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, 50 | {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, 51 | ] 52 | 53 | [[package]] 54 | name = "bitstring" 55 | version = "3.1.9" 56 | summary = "Simple construction, analysis and modification of binary data." 57 | files = [ 58 | {file = "bitstring-3.1.9-py3-none-any.whl", hash = "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578"}, 59 | {file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"}, 60 | ] 61 | 62 | [[package]] 63 | name = "bitstruct" 64 | version = "8.11.0" 65 | summary = "This module performs conversions between Python values and C bit field structs represented as Python byte strings." 66 | files = [ 67 | {file = "bitstruct-8.11.0.tar.gz", hash = "sha256:2b13f2c3e76b49e8cd854f7a1da590bb73ecbc6cbfacc2d479eacf2b88282d5d"}, 68 | ] 69 | 70 | [[package]] 71 | name = "certifi" 72 | version = "2021.10.8" 73 | summary = "Python package for providing Mozilla's CA Bundle." 74 | files = [ 75 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 76 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 77 | ] 78 | 79 | [[package]] 80 | name = "charset-normalizer" 81 | version = "2.0.11" 82 | requires_python = ">=3.5.0" 83 | summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 84 | files = [ 85 | {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, 86 | {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, 87 | ] 88 | 89 | [[package]] 90 | name = "colorama" 91 | version = "0.4.5" 92 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 93 | summary = "Cross-platform colored terminal text." 94 | files = [ 95 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 96 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 97 | ] 98 | 99 | [[package]] 100 | name = "coverage" 101 | version = "6.2" 102 | requires_python = ">=3.6" 103 | summary = "Code coverage measurement for Python" 104 | files = [ 105 | {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, 106 | {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, 107 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, 108 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, 109 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, 110 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, 111 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, 112 | {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, 113 | {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, 114 | {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, 115 | {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, 116 | {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, 117 | {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, 118 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, 119 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, 120 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, 121 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, 122 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, 123 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, 124 | {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, 125 | {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, 126 | {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, 127 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, 128 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, 129 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, 130 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, 131 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, 132 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, 133 | {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, 134 | {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, 135 | {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, 136 | {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, 137 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, 138 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, 139 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, 140 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, 141 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, 142 | {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, 143 | {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, 144 | {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, 145 | {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, 146 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, 147 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, 148 | {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, 149 | {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, 150 | {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, 151 | {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, 152 | ] 153 | 154 | [[package]] 155 | name = "coverage" 156 | version = "6.2" 157 | extras = ["toml"] 158 | requires_python = ">=3.6" 159 | summary = "Code coverage measurement for Python" 160 | dependencies = [ 161 | "coverage==6.2", 162 | "tomli", 163 | ] 164 | files = [ 165 | {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, 166 | {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, 167 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, 168 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, 169 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, 170 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, 171 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, 172 | {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, 173 | {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, 174 | {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, 175 | {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, 176 | {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, 177 | {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, 178 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, 179 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, 180 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, 181 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, 182 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, 183 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, 184 | {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, 185 | {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, 186 | {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, 187 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, 188 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, 189 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, 190 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, 191 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, 192 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, 193 | {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, 194 | {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, 195 | {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, 196 | {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, 197 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, 198 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, 199 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, 200 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, 201 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, 202 | {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, 203 | {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, 204 | {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, 205 | {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, 206 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, 207 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, 208 | {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, 209 | {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, 210 | {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, 211 | {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, 212 | ] 213 | 214 | [[package]] 215 | name = "docutils" 216 | version = "0.17.1" 217 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 218 | summary = "Docutils -- Python Documentation Utilities" 219 | files = [ 220 | {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, 221 | {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, 222 | ] 223 | 224 | [[package]] 225 | name = "editables" 226 | version = "0.2" 227 | requires_python = ">=3.5" 228 | summary = "Editable installations" 229 | files = [ 230 | {file = "editables-0.2-py3-none-any.whl", hash = "sha256:fe9afdc51df78ac3b20a2c2dd763b36ac412a70c59ca39fc782d45797dcb619a"}, 231 | {file = "editables-0.2.tar.gz", hash = "sha256:6918f16225258f24ef9800c2327e14eded42ddac344e77982380749464024f35"}, 232 | ] 233 | 234 | [[package]] 235 | name = "idna" 236 | version = "3.3" 237 | requires_python = ">=3.5" 238 | summary = "Internationalized Domain Names in Applications (IDNA)" 239 | files = [ 240 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 241 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 242 | ] 243 | 244 | [[package]] 245 | name = "imagesize" 246 | version = "1.3.0" 247 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 248 | summary = "Getting image size from png/jpeg/jpeg2000/gif file" 249 | files = [ 250 | {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, 251 | {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, 252 | ] 253 | 254 | [[package]] 255 | name = "importlib-metadata" 256 | version = "4.8.2" 257 | requires_python = ">=3.6" 258 | summary = "Read metadata from Python packages" 259 | dependencies = [ 260 | "typing-extensions>=3.6.4; python_version < \"3.8\"", 261 | "zipp>=0.5", 262 | ] 263 | files = [ 264 | {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, 265 | {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, 266 | ] 267 | 268 | [[package]] 269 | name = "importlib-resources" 270 | version = "5.4.0" 271 | requires_python = ">=3.6" 272 | summary = "Read resources from Python packages" 273 | dependencies = [ 274 | "zipp>=3.1.0; python_version < \"3.10\"", 275 | ] 276 | files = [ 277 | {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, 278 | {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, 279 | ] 280 | 281 | [[package]] 282 | name = "iniconfig" 283 | version = "1.1.1" 284 | summary = "iniconfig: brain-dead simple config-ini parsing" 285 | files = [ 286 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 287 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 288 | ] 289 | 290 | [[package]] 291 | name = "jinja2" 292 | version = "3.0.3" 293 | requires_python = ">=3.6" 294 | summary = "A very fast and expressive template engine." 295 | dependencies = [ 296 | "MarkupSafe>=2.0", 297 | ] 298 | files = [ 299 | {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, 300 | {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, 301 | ] 302 | 303 | [[package]] 304 | name = "markupsafe" 305 | version = "2.0.1" 306 | requires_python = ">=3.6" 307 | summary = "Safely add untrusted strings to HTML/XML markup." 308 | files = [ 309 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, 310 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, 311 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, 312 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, 313 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, 314 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, 315 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, 316 | {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, 317 | {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, 318 | {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, 319 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 320 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 321 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 322 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 323 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 324 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 325 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, 326 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, 327 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, 328 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, 329 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, 330 | {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, 331 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 332 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 333 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 334 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 335 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 336 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 337 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 338 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 339 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, 340 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, 341 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, 342 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, 343 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, 344 | {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, 345 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 346 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 347 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, 348 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 349 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 350 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 351 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 352 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 353 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 354 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, 355 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, 356 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, 357 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, 358 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, 359 | {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, 360 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 361 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 362 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 363 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 364 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 365 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 366 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 367 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 368 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 369 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, 370 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, 371 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, 372 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, 373 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, 374 | {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, 375 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 376 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 377 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 378 | ] 379 | 380 | [[package]] 381 | name = "packaging" 382 | version = "21.3" 383 | requires_python = ">=3.6" 384 | summary = "Core utilities for Python packages" 385 | dependencies = [ 386 | "pyparsing!=3.0.5,>=2.0.2", 387 | ] 388 | files = [ 389 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 390 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 391 | ] 392 | 393 | [[package]] 394 | name = "pluggy" 395 | version = "0.13.1" 396 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 397 | summary = "plugin and hook calling mechanisms for python" 398 | dependencies = [ 399 | "importlib-metadata>=0.12; python_version < \"3.8\"", 400 | ] 401 | files = [ 402 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 403 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 404 | ] 405 | 406 | [[package]] 407 | name = "py" 408 | version = "1.11.0" 409 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 410 | summary = "library with cross-python path, ini-parsing, io, code, log facilities" 411 | files = [ 412 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 413 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 414 | ] 415 | 416 | [[package]] 417 | name = "pygments" 418 | version = "2.14.0" 419 | requires_python = ">=3.6" 420 | summary = "Pygments is a syntax highlighting package written in Python." 421 | files = [ 422 | {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, 423 | {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, 424 | ] 425 | 426 | [[package]] 427 | name = "pyparsing" 428 | version = "3.0.6" 429 | requires_python = ">=3.6" 430 | summary = "Python parsing module" 431 | files = [ 432 | {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, 433 | {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, 434 | ] 435 | 436 | [[package]] 437 | name = "pytest" 438 | version = "7.0.1" 439 | requires_python = ">=3.6" 440 | summary = "pytest: simple powerful testing with Python" 441 | dependencies = [ 442 | "atomicwrites>=1.0; sys_platform == \"win32\"", 443 | "attrs>=19.2.0", 444 | "colorama; sys_platform == \"win32\"", 445 | "importlib-metadata>=0.12; python_version < \"3.8\"", 446 | "iniconfig", 447 | "packaging", 448 | "pluggy<2.0,>=0.12", 449 | "py>=1.8.2", 450 | "tomli>=1.0.0", 451 | ] 452 | files = [ 453 | {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, 454 | {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, 455 | ] 456 | 457 | [[package]] 458 | name = "pytest-azurepipelines" 459 | version = "1.0.5" 460 | requires_python = ">=3.5" 461 | summary = "Formatting PyTest output for Azure Pipelines UI" 462 | dependencies = [ 463 | "importlib-resources", 464 | "pytest-nunit<2.0.0,>=1.0.0", 465 | "pytest>=5.0.0", 466 | ] 467 | files = [ 468 | {file = "pytest-azurepipelines-1.0.5.tar.gz", hash = "sha256:fd08f089bca0fad3c2b6fde9dbea69b975492ffaa8cb637cc987e64af8c18b9c"}, 469 | {file = "pytest_azurepipelines-1.0.5-py3-none-any.whl", hash = "sha256:58b65a7a47710cea46a310f6c0222b0b2ecaa35c9eabbb9a027666726c52f914"}, 470 | ] 471 | 472 | [[package]] 473 | name = "pytest-cov" 474 | version = "4.0.0" 475 | requires_python = ">=3.6" 476 | summary = "Pytest plugin for measuring coverage." 477 | dependencies = [ 478 | "coverage[toml]>=5.2.1", 479 | "pytest>=4.6", 480 | ] 481 | files = [ 482 | {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, 483 | {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, 484 | ] 485 | 486 | [[package]] 487 | name = "pytest-nunit" 488 | version = "1.0.4" 489 | requires_python = ">=3.6" 490 | summary = "A pytest plugin for generating NUnit3 test result XML output" 491 | dependencies = [ 492 | "attrs", 493 | "pytest>=4.6.0", 494 | ] 495 | files = [ 496 | {file = "pytest-nunit-1.0.4.tar.gz", hash = "sha256:9debc9294f278e5b4aea66f6e1f6fe72f690339b73e81e25b33e615fc75083bf"}, 497 | ] 498 | 499 | [[package]] 500 | name = "pytz" 501 | version = "2021.3" 502 | summary = "World timezone definitions, modern and historical" 503 | files = [ 504 | {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, 505 | {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, 506 | ] 507 | 508 | [[package]] 509 | name = "requests" 510 | version = "2.27.1" 511 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 512 | summary = "Python HTTP for Humans." 513 | dependencies = [ 514 | "certifi>=2017.4.17", 515 | "charset-normalizer~=2.0.0; python_version >= \"3\"", 516 | "idna<4,>=2.5; python_version >= \"3\"", 517 | "urllib3<1.27,>=1.21.1", 518 | ] 519 | files = [ 520 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 521 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 522 | ] 523 | 524 | [[package]] 525 | name = "snowballstemmer" 526 | version = "2.2.0" 527 | summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 528 | files = [ 529 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 530 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 531 | ] 532 | 533 | [[package]] 534 | name = "sphinx" 535 | version = "5.3.0" 536 | requires_python = ">=3.6" 537 | summary = "Python documentation generator" 538 | dependencies = [ 539 | "Jinja2>=3.0", 540 | "Pygments>=2.12", 541 | "alabaster<0.8,>=0.7", 542 | "babel>=2.9", 543 | "colorama>=0.4.5; sys_platform == \"win32\"", 544 | "docutils<0.20,>=0.14", 545 | "imagesize>=1.3", 546 | "importlib-metadata>=4.8; python_version < \"3.10\"", 547 | "packaging>=21.0", 548 | "requests>=2.5.0", 549 | "snowballstemmer>=2.0", 550 | "sphinxcontrib-applehelp", 551 | "sphinxcontrib-devhelp", 552 | "sphinxcontrib-htmlhelp>=2.0.0", 553 | "sphinxcontrib-jsmath", 554 | "sphinxcontrib-qthelp", 555 | "sphinxcontrib-serializinghtml>=1.1.5", 556 | ] 557 | files = [ 558 | {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, 559 | {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, 560 | ] 561 | 562 | [[package]] 563 | name = "sphinxcontrib-applehelp" 564 | version = "1.0.2" 565 | requires_python = ">=3.5" 566 | summary = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" 567 | files = [ 568 | {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, 569 | {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, 570 | ] 571 | 572 | [[package]] 573 | name = "sphinxcontrib-devhelp" 574 | version = "1.0.2" 575 | requires_python = ">=3.5" 576 | summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 577 | files = [ 578 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, 579 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, 580 | ] 581 | 582 | [[package]] 583 | name = "sphinxcontrib-htmlhelp" 584 | version = "2.0.0" 585 | requires_python = ">=3.6" 586 | summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 587 | files = [ 588 | {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, 589 | {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, 590 | ] 591 | 592 | [[package]] 593 | name = "sphinxcontrib-jsmath" 594 | version = "1.0.1" 595 | requires_python = ">=3.5" 596 | summary = "A sphinx extension which renders display math in HTML via JavaScript" 597 | files = [ 598 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 599 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 600 | ] 601 | 602 | [[package]] 603 | name = "sphinxcontrib-qthelp" 604 | version = "1.0.3" 605 | requires_python = ">=3.5" 606 | summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 607 | files = [ 608 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, 609 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, 610 | ] 611 | 612 | [[package]] 613 | name = "sphinxcontrib-serializinghtml" 614 | version = "1.1.5" 615 | requires_python = ">=3.5" 616 | summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 617 | files = [ 618 | {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, 619 | {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, 620 | ] 621 | 622 | [[package]] 623 | name = "tomli" 624 | version = "1.2.2" 625 | requires_python = ">=3.6" 626 | summary = "A lil' TOML parser" 627 | files = [ 628 | {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, 629 | {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, 630 | ] 631 | 632 | [[package]] 633 | name = "typing-extensions" 634 | version = "4.1.1" 635 | requires_python = ">=3.6" 636 | summary = "Backported and Experimental Type Hints for Python 3.6+" 637 | files = [ 638 | {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, 639 | {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, 640 | ] 641 | 642 | [[package]] 643 | name = "urllib3" 644 | version = "1.26.8" 645 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 646 | summary = "HTTP library with thread-safe connection pooling, file post, and more." 647 | files = [ 648 | {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, 649 | {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, 650 | ] 651 | 652 | [[package]] 653 | name = "zipp" 654 | version = "3.5.1" 655 | requires_python = ">=3.6" 656 | summary = "Backport of pathlib-compatible object wrapper for zip files" 657 | files = [ 658 | {file = "zipp-3.5.1-py3-none-any.whl", hash = "sha256:8dc6c4d5a809d659067cc713f76bcf42fae8ae641db12fddfa93694a15abc96b"}, 659 | {file = "zipp-3.5.1.tar.gz", hash = "sha256:1fc9641b26f3bd81069b7738b039f2819cab6e3fc3399a953e19d92cc81eff4d"}, 660 | ] 661 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pychapter10" 3 | version = "1.1.19" 4 | description = "A parser library for the IRIG 106 Chapter 10 data format." 5 | authors = [ 6 | {name = "Micah Ferrill", email = "ferrillm@avtest.com"}, 7 | ] 8 | dependencies = [ 9 | "bitstruct==8.11.0", 10 | "bitstring>=3.1.7", 11 | "editables>=0.2", 12 | ] 13 | readme = "README.rst" 14 | requires-python = ">=3.6" 15 | license = {file = "LICENSE.txt"} 16 | classifiers = [ 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Topic :: Software Development :: Libraries", 23 | "Development Status :: 5 - Production/Stable", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: BSD License", 26 | "Operating System :: OS Independent", 27 | ] 28 | 29 | [project.urls] 30 | homepage = "https://github.com/atac/pychapter10" 31 | repository = "https://github.com/atac/pychapter10" 32 | documentation = "https://pychapter10.readthedocs.io/" 33 | 34 | [project.optional-dependencies] 35 | docs = [ 36 | "sphinx>=2.4.4", 37 | ] 38 | 39 | [tool] 40 | [tool.pdm] 41 | [[tool.pdm.source]] 42 | name = "pypi" 43 | url = "https://pypi.org/simple" 44 | verify_ssl = true 45 | 46 | [build-system] 47 | requires = ["pdm-pep517"] 48 | build-backend = "pdm.pep517.api" 49 | 50 | [tool.pdm.dev-dependencies] 51 | dev = [ 52 | "sphinx>=2.4.4", 53 | "pytest>=6.0.0", 54 | "pytest-cov>=2.10.1", 55 | "pytest-azurepipelines", 56 | ] 57 | 58 | [tool.pytest.ini_options] 59 | minversion = "6.0" 60 | testpaths = [ 61 | "tests", 62 | ] -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/tests/conftest.py -------------------------------------------------------------------------------- /tests/discrete.c10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/tests/discrete.c10 -------------------------------------------------------------------------------- /tests/ethernet.c10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/tests/ethernet.c10 -------------------------------------------------------------------------------- /tests/event.c10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/tests/event.c10 -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | 2 | from array import array 3 | import os 4 | import struct 5 | 6 | 7 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 8 | ETHERNET = os.path.join(BASEDIR, 'ethernet.c10') 9 | UART = ETHERNET 10 | EVENTS = os.path.join(BASEDIR, 'event.c10') 11 | PCM = os.path.join(BASEDIR, 'pcm.c10') 12 | SAMPLE = os.path.join(BASEDIR, 'sample.c10') 13 | DISCRETE = os.path.join(BASEDIR, 'discrete.c10') 14 | ANALOG = PCM 15 | INDEX = DISCRETE 16 | 17 | 18 | def dummy_packet(type, size): 19 | """Create a dummy packet of given type and size. Returns bytes.""" 20 | 21 | header = [ 22 | 0xeb25, 23 | 0, 24 | size + 24, 25 | size, 26 | 1, 27 | 0, 28 | 0, 29 | type, 30 | 0, 31 | 0 32 | ] 33 | header[2] += 4 - (header[2] % 4) 34 | checksum = sum(array('H', struct.pack('=HHIIBBBBIH', *header))) & 0xffff 35 | header.append(checksum) 36 | header = struct.pack('=HHIIBBBBIHH', *header) 37 | return header + (b'\x00' * size) 38 | -------------------------------------------------------------------------------- /tests/pcm.c10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/tests/pcm.c10 -------------------------------------------------------------------------------- /tests/performance_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Script to benchmark Chapter 10 parsing.""" 4 | 5 | from datetime import timedelta 6 | import sys 7 | import os 8 | import time 9 | 10 | try: 11 | from i106 import C10 12 | except ImportError: 13 | from chapter10 import C10 14 | 15 | if os.environ.get('LIBRARY', None) == 'c10': 16 | from chapter10 import C10 17 | 18 | try: 19 | from tqdm import tqdm 20 | except ImportError: 21 | tqdm = None 22 | 23 | RUNS = 100 24 | SOURCE_FILE = 'tests/ethernet.c10' 25 | 26 | file_size = os.stat(SOURCE_FILE).st_size 27 | total = file_size * RUNS 28 | bytes_to_go = total 29 | 30 | 31 | def show_progress(percent): 32 | """Takes a percentage (as a decimal fraction) and shows progress.""" 33 | 34 | bar = ('=' * int(74 * percent)).ljust(74) 35 | sys.stdout.write('[{}] {}%\r'.format(bar, round(percent * 100))) 36 | sys.stdout.flush() 37 | 38 | 39 | if __name__ == '__main__': 40 | start_time = time.perf_counter() 41 | print('Running with %s' % (os.environ.get('LIBRARY', 'i106'))) 42 | if tqdm: 43 | progress = tqdm( 44 | total=total, unit='bytes', unit_scale=True, leave=False) 45 | for i in range(RUNS): 46 | for packet in C10(SOURCE_FILE): 47 | 48 | if tqdm: 49 | progress.update(packet.packet_length) 50 | else: 51 | bytes_to_go -= packet.packet_length 52 | percent = (total - bytes_to_go) / total 53 | show_progress(percent) 54 | 55 | for msg in packet: 56 | bus = getattr(msg, 'bus', None) 57 | print('\nCompleted in %s' % 58 | timedelta(seconds=time.perf_counter() - start_time)) 59 | -------------------------------------------------------------------------------- /tests/sample.c10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atac/pychapter10/23b154e7aecef6864c6c1ee99db3aeede3385b9d/tests/sample.c10 -------------------------------------------------------------------------------- /tests/test_sanity.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from chapter10 import C10 5 | from chapter10.packet import InvalidPacket 6 | from fixtures import dummy_packet 7 | 8 | 9 | @pytest.mark.parametrize('size', (6, 8, 10, 12, 16)) 10 | @pytest.mark.parametrize('data_type', range(120)) 11 | def test_sanity(data_type, size): 12 | """Test default constructors for every data type and 5 base sizes.""" 13 | 14 | raw = dummy_packet(data_type, size) 15 | try: 16 | C10.from_string(raw) 17 | except (NotImplementedError, InvalidPacket, TypeError): 18 | return 19 | -------------------------------------------------------------------------------- /tests/unit/test_analog.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from chapter10 import C10, analog 5 | from fixtures import ANALOG 6 | 7 | 8 | @pytest.fixture 9 | def packet(): 10 | for packet in C10(ANALOG): 11 | if isinstance(packet, analog.AnalogF1): 12 | return packet 13 | 14 | 15 | def test_csdw(packet): 16 | assert packet.mode == 0 17 | assert packet.length == 8 18 | assert packet.subchannel == 0 19 | assert packet.factor == 0 20 | assert packet.same == 1 21 | 22 | 23 | def test_count(packet): 24 | assert len(packet.subchannels) == packet.subchannel_count 25 | 26 | 27 | def test_next(packet): 28 | for i, sample in enumerate(packet): 29 | assert sample.data == b'\xfc' 30 | if i >= 2: 31 | break 32 | 33 | 34 | def test_bytes(packet): 35 | assert packet.buffer.getvalue() == bytes(packet) 36 | 37 | 38 | def test_generate(): 39 | a = analog.AnalogF1(same=1) 40 | msg1 = analog.AnalogF1.Message(length=2, data=b'\xbe\xef') 41 | a.append(msg1) 42 | 43 | assert b'\xbe\xef' in bytes(a) -------------------------------------------------------------------------------- /tests/unit/test_arinc429.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from chapter10 import C10, arinc429 5 | from fixtures import SAMPLE 6 | 7 | 8 | @pytest.fixture 9 | def packet(): 10 | for packet in C10(SAMPLE): 11 | if isinstance(packet, arinc429.ARINC429F0): 12 | return packet 13 | 14 | 15 | def test_count(packet): 16 | assert len(list(packet)) == len(packet) == 221 17 | 18 | 19 | def test_gap(packet): 20 | expected = ( 21 | (0, 2, 1), 22 | (2489, 4, 1), 23 | (1131, 2, 1), 24 | (2489, 4, 1), 25 | (1131, 2, 1), 26 | (1221, 5, 0), 27 | ) 28 | for i, msg in enumerate(packet): 29 | if i < len(expected): 30 | assert (msg.gap_time, msg.bus, msg.bus_speed) == expected[i] 31 | else: 32 | break 33 | 34 | 35 | def test_bytes(packet): 36 | for msg in packet: 37 | packet.buffer.seek(-8, 1) 38 | raw = packet.buffer.read(8) 39 | assert bytes(msg) == raw 40 | -------------------------------------------------------------------------------- /tests/unit/test_c10.py: -------------------------------------------------------------------------------- 1 | 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | 6 | from chapter10 import c10 7 | from fixtures import SAMPLE 8 | 9 | 10 | def test_next(): 11 | with open(SAMPLE, 'rb') as f: 12 | assert next(c10.C10(f)).packet_length == 6680 13 | 14 | 15 | def test_next_stop(): 16 | with pytest.raises(StopIteration): 17 | next(c10.C10(Mock(read=Mock(side_effect=EOFError)))) 18 | 19 | 20 | @pytest.mark.parametrize('func', (repr, str)) 21 | def test_repr(func): 22 | assert func(c10.C10(SAMPLE)) == '' 23 | -------------------------------------------------------------------------------- /tests/unit/test_computer.py: -------------------------------------------------------------------------------- 1 | 2 | from chapter10 import C10 3 | from fixtures import SAMPLE, EVENTS, INDEX 4 | 5 | 6 | def test_tmats(): 7 | for packet in C10(SAMPLE): 8 | if packet.data_type == 1: 9 | break 10 | assert list(packet['V-1'].items()) == [ 11 | (b'V-1\\ID', b'DATASOURCE'), 12 | (b'V-1\\VN', b'HDS'), 13 | (b'V-1\\HDS\\SYS', b'sov2')] 14 | 15 | 16 | def test_events(): 17 | for packet in C10(EVENTS): 18 | if packet.data_type == 2: 19 | assert len(packet) == packet.count 20 | 21 | 22 | def test_index_csdw(): 23 | for packet in C10(INDEX): 24 | if packet.data_type == 3: 25 | break 26 | assert (packet.count, 27 | packet.ipdh, 28 | packet.file_size_present, 29 | packet.index_type) == (5, 0, 1, 1) 30 | 31 | 32 | def test_index_node(): 33 | for packet in C10(INDEX): 34 | if packet.data_type == 3: 35 | if packet.index_type: 36 | break 37 | for node in packet: 38 | break 39 | assert node.ipts == 28892518346 40 | assert node.channel_id == 1 41 | assert node.data_type == 17 42 | assert node.offset == 28160 43 | 44 | 45 | def test_index_root(): 46 | for packet in C10(INDEX): 47 | if packet.data_type == 3: 48 | if not packet.index_type: 49 | break 50 | for part in packet: 51 | break 52 | assert part.ipts == 28892518346 53 | assert part.offset == 952252 54 | 55 | 56 | def test_index_bytes(): 57 | for packet in C10(INDEX): 58 | if packet.data_type == 3: 59 | break 60 | assert packet.buffer.getvalue() == bytes(packet) 61 | -------------------------------------------------------------------------------- /tests/unit/test_discrete.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from chapter10 import C10, discrete 5 | from fixtures import DISCRETE 6 | 7 | 8 | @pytest.fixture 9 | def packet(): 10 | for packet in C10(DISCRETE): 11 | if isinstance(packet, discrete.DiscreteF1): 12 | return packet 13 | 14 | 15 | def test_packet(packet): 16 | assert packet.mode == 0 17 | assert packet.length == 0 18 | 19 | 20 | def test_msg(packet): 21 | for msg in packet: 22 | assert msg.ipts == 28894167514 23 | assert msg.data == b'\xff\xff\xff\xff' 24 | break 25 | -------------------------------------------------------------------------------- /tests/unit/test_ethernet.py: -------------------------------------------------------------------------------- 1 | 2 | from chapter10 import C10 3 | from fixtures import ETHERNET 4 | 5 | 6 | def test_format_0(): 7 | for packet in C10(ETHERNET): 8 | if packet.data_type == 0x68: 9 | for msg in packet: 10 | assert msg.ethernet_speed == 2 11 | break 12 | assert len(packet) == packet.count 13 | 14 | 15 | def test_format_1_count(): 16 | for packet in C10(ETHERNET): 17 | if packet.data_type == 0x69: 18 | for i, msg in enumerate(packet): 19 | if i == 0: 20 | assert msg.dst_port == 9311 21 | assert len(msg.data) == msg.length 22 | break 23 | assert i+1 == len(packet) 24 | assert len(packet) == 2 25 | 26 | 27 | def test_bytes(): 28 | for packet in C10(ETHERNET): 29 | if packet.data_type == 0x68: 30 | break 31 | assert packet.buffer.getvalue() == bytes(packet) 32 | -------------------------------------------------------------------------------- /tests/unit/test_message.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from chapter10 import C10, message 5 | from fixtures import SAMPLE 6 | 7 | 8 | @pytest.fixture 9 | def packet(): 10 | for packet in C10(SAMPLE): 11 | if isinstance(packet, message.MessageF0): 12 | return packet 13 | 14 | 15 | @pytest.fixture 16 | def msg(packet): 17 | return next(packet) 18 | 19 | 20 | def test_csdw(packet): 21 | assert packet.packet_type == 0 22 | assert packet.count == 94 23 | 24 | 25 | def test_msg(msg): 26 | assert msg.ipts == 604324042154 27 | assert msg.length == 350 == len(msg.data) 28 | assert msg.subchannel == 1 29 | assert msg.format_error == 0 30 | assert msg.data_error == 0 31 | -------------------------------------------------------------------------------- /tests/unit/test_ms1553.py: -------------------------------------------------------------------------------- 1 | 2 | from chapter10 import C10, ms1553 3 | from fixtures import SAMPLE 4 | 5 | 6 | def test_count(): 7 | for packet in C10(SAMPLE): 8 | if isinstance(packet, ms1553.MS1553F1): 9 | for i, msg in enumerate(packet): 10 | if i == 0: 11 | assert msg.bus == 1 12 | assert len(msg.data) == msg.length 13 | break 14 | assert len(packet) == 82 15 | assert i+1 == len(packet) 16 | -------------------------------------------------------------------------------- /tests/unit/test_packet.py: -------------------------------------------------------------------------------- 1 | 2 | import pickle 3 | from unittest.mock import Mock 4 | from io import BytesIO 5 | 6 | import pytest 7 | 8 | from chapter10 import packet, computer 9 | from chapter10.util import BitFormat 10 | from fixtures import SAMPLE 11 | 12 | 13 | class TestPacket: 14 | @pytest.fixture 15 | def p(self): 16 | class Dummy(packet.Packet): 17 | Message = Mock() 18 | return Dummy(data_length=20) 19 | 20 | # Constructor 21 | def test_construct_file(self): 22 | with open(SAMPLE, 'rb') as f: 23 | assert packet.Packet(f).data_type == 1 24 | 25 | def test_construct_blank(self, p): 26 | assert p._messages == [] 27 | 28 | # __iter__ 29 | def test_iter_messages(self, p): 30 | p._messages = [0, 1, 2, 3] 31 | for i, msg in enumerate(p): 32 | assert i == msg 33 | 34 | def test_iter_stop(self): 35 | count = 0 36 | for msg in packet.Packet(): 37 | count += 1 38 | assert count == 0 39 | 40 | # __next__ 41 | def test_next_from_packet(self, p): 42 | for msg in p: 43 | pass 44 | assert p.Message.called_with(p) 45 | 46 | def test_next_from_list(self, p): 47 | p._messages = [0, 1, 2] 48 | assert list(p) == [0, 1, 2] 49 | assert not p.Message.called 50 | 51 | # __bytes__ 52 | def test_bytes_file(self): 53 | with open(SAMPLE, 'rb') as f: 54 | p = computer.ComputerF1(f) 55 | f.seek(0) 56 | assert f.read(p.packet_length) == bytes(p) 57 | 58 | def test_bytes_generated(self): 59 | class Dummy(packet.Packet): 60 | class Message(packet.Message): 61 | length = 1 62 | p = Dummy() 63 | p.csdw = 0 64 | p._messages = [packet.Message(b'\01'), packet.Message(b'\02')] 65 | result = bytes(p) 66 | 67 | assert len(result) == 32 68 | assert len(p.__class__(BytesIO(result))) == 4 69 | 70 | # __len__ 71 | def test_len_messages(self, p): 72 | p._messages = [0, 1, 2] 73 | assert len(p) == 3 74 | 75 | def test_len_count(self, p): 76 | p.count = 2 77 | p._messages = None 78 | assert len(p) == 2 79 | 80 | def test_len_message_no_format(self, p): 81 | p._messages = None 82 | p.data_length = 24 83 | p.Message.length = 5 84 | p.Message.FORMAT = None 85 | assert len(p) == 4 86 | 87 | def test_len_message_format(self, p): 88 | p._messages = None 89 | p.data_length = 24 90 | p.Message.length = 4 91 | p.Message.FORMAT = BitFormat('u8 one') 92 | assert len(p) == 4 93 | 94 | def test_append(self, p): 95 | p.append(1, 2, 3) 96 | assert p._messages == [1, 2, 3] 97 | 98 | def test_clear(self, p): 99 | p._messages = [3, 1, 2] 100 | p.clear() 101 | assert p._messages == [] 102 | 103 | def test_clear_file(self): 104 | with open(SAMPLE, 'rb') as f: 105 | p = packet.Packet(f) 106 | p.clear() 107 | assert p._messages == [] 108 | 109 | def test_copy(self, p): 110 | p.channel_id = 12 111 | p2 = p.copy() 112 | p.channel_id = 11 113 | assert p2.channel_id == 12 114 | 115 | def remove(self, p): 116 | p._messages = [0, 1, 2] 117 | p.remove(1) 118 | assert p._messages == [0, 2] 119 | 120 | def test_pickle(self): 121 | p = packet.Packet(channel_id=12) 122 | s = pickle.dumps(p) 123 | assert pickle.loads(s).__dict__ == p.__dict__ 124 | 125 | 126 | class TestMessage: 127 | @pytest.fixture 128 | def fake(self): 129 | class FakePacket: 130 | buffer = BytesIO(b'0' * 1000) 131 | data_length = 5 132 | secondary_header = 0 133 | 134 | class Message(packet.Message): 135 | length = 3 136 | 137 | return FakePacket() 138 | 139 | def test_from_packet_eof(self, fake): 140 | fake.buffer.seek(1000) 141 | with pytest.raises(EOFError): 142 | fake.Message.from_packet(fake) 143 | 144 | def test_from_packet_iph(self, fake): 145 | fake.Message.FORMAT = BitFormat('u8 ipts') 146 | fake.buffer = BytesIO(b'\xff\x00\x00\x00') 147 | msg = fake.Message.from_packet(fake) 148 | assert msg.ipts == 255 149 | 150 | def test_from_packet_no_format(self, fake): 151 | msg = fake.Message.from_packet(fake) 152 | assert len(msg.data) == fake.Message.length 153 | 154 | def test_blank(self, fake): 155 | msg = fake.Message('') 156 | assert msg.data == '' 157 | 158 | def test_new(self, fake): 159 | msg = fake.Message('', hello='World') 160 | assert msg.hello == 'World' 161 | 162 | def test_bytes(self, fake): 163 | fake.Message.FORMAT = BitFormat('u4 one\nu4 two') 164 | msg = fake.Message(b'\xff\x00', one=1, two=2) 165 | assert bytes(msg) == b'\x12\xff\x00\x00' 166 | -------------------------------------------------------------------------------- /tests/unit/test_pcm.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from chapter10 import C10, pcm 5 | from fixtures import PCM 6 | 7 | 8 | @pytest.fixture 9 | def packet(): 10 | for packet in C10(PCM): 11 | if isinstance(packet, pcm.PCMF1): 12 | return packet 13 | 14 | 15 | @pytest.fixture 16 | def throughput(): 17 | for packet in C10(PCM): 18 | if isinstance(packet, pcm.PCMF1): 19 | if packet.throughput: 20 | return packet 21 | 22 | 23 | def test_csdw(packet): 24 | assert packet.sync_offset == 0 25 | assert packet.alignment == 0 26 | assert packet.throughput == 0 27 | assert packet.packed == 1 28 | assert packet.iph == 1 29 | 30 | 31 | def test_packed(packet): 32 | assert len(list(packet)) == 2974 33 | 34 | 35 | def test_throughput(throughput): 36 | with pytest.raises(StopIteration): 37 | next(throughput) 38 | 39 | 40 | def test_throughput_len(throughput): 41 | with pytest.raises(TypeError): 42 | len(throughput) -------------------------------------------------------------------------------- /tests/unit/test_time.py: -------------------------------------------------------------------------------- 1 | 2 | from datetime import datetime 3 | import io 4 | 5 | from chapter10 import time, C10 6 | from fixtures import SAMPLE 7 | 8 | 9 | def test_time(): 10 | for packet in C10(SAMPLE): 11 | if isinstance(packet, time.TimeF1): 12 | break 13 | assert packet.time.strftime('%j %H:%M:%S') == '343 16:47:12' 14 | 15 | 16 | def test_time_bytes(): 17 | for packet in C10(SAMPLE): 18 | if isinstance(packet, time.TimeF1): 19 | break 20 | raw = bytes(packet) 21 | assert time.TimeF1(io.BytesIO(raw)).time == packet.time 22 | 23 | 24 | def test_time_bytes_with_ms(): 25 | t0 = time.TimeF1(date_format=1) 26 | 27 | # Note trailing 0, IRIG 106-15 Time F1 only allows precision 28 | # to tenths of ms, but fromisoformat requires specifying to 1-ms. 29 | t0.time = datetime.fromisoformat('2022-12-05 01:02:03.450') 30 | 31 | raw = bytes(t0) 32 | 33 | assert time.TimeF1(io.BytesIO(raw)).time == t0.time 34 | -------------------------------------------------------------------------------- /tests/unit/test_uart.py: -------------------------------------------------------------------------------- 1 | 2 | from chapter10 import C10, uart 3 | from fixtures import UART 4 | 5 | 6 | def test_count(): 7 | for packet in C10(UART): 8 | if isinstance(packet, uart.UARTF0): 9 | assert packet.iph 10 | assert len(list(packet)) == 2 11 | break 12 | 13 | 14 | def test_message(): 15 | expected = [ 16 | (561182982, 55, 0), 17 | (561754950, 33, 0), 18 | ] 19 | for packet in C10(UART): 20 | if isinstance(packet, uart.UARTF0): 21 | for i, msg in enumerate(packet): 22 | assert (msg.ipts, msg.length, msg.subchannel) == expected[i] 23 | break 24 | -------------------------------------------------------------------------------- /tests/unit/test_video.py: -------------------------------------------------------------------------------- 1 | 2 | from chapter10 import video, C10 3 | from fixtures import SAMPLE 4 | 5 | 6 | def test_videof0(): 7 | for packet in C10(SAMPLE): 8 | if isinstance(packet, video.VideoF0): 9 | for msg in packet: 10 | assert len(msg.data) == 188 11 | break 12 | assert len(packet) == 83 13 | 14 | 15 | def test_videof1(): 16 | for packet in C10(SAMPLE): 17 | if isinstance(packet, video.VideoF1): 18 | for msg in packet: 19 | assert len(msg.data) == 188 20 | break 21 | assert len(packet) == 83 22 | 23 | 24 | def test_videof2(): 25 | for packet in C10(SAMPLE): 26 | if isinstance(packet, video.VideoF2): 27 | for msg in packet: 28 | assert len(msg.data) == 188 29 | break 30 | assert len(packet) == 83 31 | --------------------------------------------------------------------------------