Skip to content

Commit

Permalink
Adds schedule table support (#80)
Browse files Browse the repository at this point in the history
* Adds schedule table classes and schedule table field to LDF
* FIxes missing unassign frame id command to JSON schema
* Adds schedule table documentation
* Schedule entry delay type is now float
* Event triggered frames have reference to Schedule
* Updates changelog
* Bumps software version
* Updates disclaimers, examples and features in README
* Fixes missing links in index.md
  • Loading branch information
c4deszes authored Feb 5, 2022
1 parent 91712f0 commit 6d95fe1
Show file tree
Hide file tree
Showing 13 changed files with 523 additions and 24 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.13.0] - 2022-02-05

### Added

- Schedule tables are now parsed into Python objects

### Changed

- The delay of schedule entries in the dictionary now have a floating point type and their unit
has been normalized to seconds

### Fixed

- Added missing `UnassignFrameId` command to JSON schema

### Migration guide for 0.13.0

- Any reference to `ldf['schedule_tables'][id]['schedule'][entry]['delay']` that assumes that
milliseconds are used as the unit has to be updated to either multiply the current value by `1000`
or somehow change the assumption about the unit to seconds.
All numeric values that have an associated unit have their SI prefix removed during parsing, for
example `kbps` is converted into `bps`. The schedule entry delay was one case where it wasn't
handled accordingly.

## [0.12.0] - 2021-11-21

### Added
Expand Down
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,22 @@
---

## Disclaimer
## Disclaimers

The library is still in a pre-release state, therefore features may break between minor versions.
For this reason it's recommended that productive environments pin to the exact version of the
library and do an integration test or review when updating the version. Breaking changes and how to
migrate to the new version will be documented in the
[changelog](https://github.com/c4deszes/ldfparser/blob/master/CHANGELOG.md) and on the
[Github releases page](https://github.com/c4deszes/ldfparser/releases).

The tool has been written according the LIN standards 1.3, 2.0, 2.1 and 2.2A, but due to errors in
the documentation there's no guarantee that the library will be able to parse your LDF. In such
cases if possible first verify the LDF with a commercial tool such as Vector LDF Explorer or the
tool that was used to create the LDF. If the LDF seems to be correct then open a new issue.
I also recommend trying the LDF to JSON conversion mechanism, see if that succeeds.

Also the LIN standard is now known as [ISO 17987](https://www.iso.org/standard/61222.html) which
The LIN standard is now known as [ISO 17987](https://www.iso.org/standard/61222.html) which
clears up some of the confusing parts in the 2.2A specification. Since this new standard is not
freely available **this library won't support the modifications present in ISO 17987**. I don't
think it's going to a huge problem because the LIN 2.2A released in 2010 has overall better adoption.
Expand Down Expand Up @@ -63,11 +70,11 @@ print(binascii.hexlify(message))

# Decode message into dictionary of signal names and values
received = bytearray([0x7B, 0x00])
print(frame.decode(received, ldf.converters))
print(frame.decode(received))
>>> {"Signal_1": 123, "Signal_2": 0}

# Encode signal values through converters
message = frame.encode({"MotorRPM": 100, "FanState": "ON"}, ldf.converters)
message = frame.encode({"MotorRPM": 100, "FanState": "ON"})
print(binascii.hexlify(message))
>>> 0xFE01
```
Expand All @@ -94,23 +101,31 @@ Documentation is published to [Github Pages](https://c4deszes.github.io/ldfparse

+ Retrieve Node attributes

+ Retrieve schedule table information

+ Command Line Interface

+ Capturing comments

### Currently not supported
+ Encode and decode standard diagnostic frames

### Known issues / missing features

+ Certain parsing related errors are unintuitive

+ Checksum calculation for frames

+ Scheduling table
+ Saving LDF objects as an `.ldf` file

+ Diagnostics
+ Token information is not preserved

---

## Development

Install the requirements via `pip install -r requirements.txt`

Install the library locally by running `python setup.py install`
Install the library locally by running `pip install -e .`

[Pytest](https://pytest.org/) is used for testing, to execute all tests run `pytest -m 'not snapshot'`

Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ production use cases you pin the version to a minor release in your requirements

[Using the command line interface](commandline.md)

[Using diagnostic frames](diagnostics.md)

[Accessing schedule tables](schedules.md)

## License

Distributed under the terms of
Expand Down
33 changes: 33 additions & 0 deletions docs/schedules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
layout: default
title: LDF Parser - Schedules
---

## Accessing schedules

### Retrieving schedule information

After parsing the LDF into objects the schedule tables defined in the LDF will
be accessible.

```python
ldf = parse_ldf('network.ldf')

configuration_schedule = ldf.get_schedule('Configuration_Schedule')
print(configuration_schedule.name)
>>> 'Configuration_Schedule'
for entry in configuration_schedule.schedule:
print(f"{type(entry).__name__} - {entry.delay * 1000} ms")
>>> 'AssignNadEntry - 15 ms'
>>> 'AssignFrameIdRangeEntry - 15 ms'
>>> 'AssignFrameIdEntry - 15 ms'
>>> 'AssignFrameIdEntry - 15 ms'
>>> 'AssignFrameIdEntry - 15 ms'
```

The objects referenced in the table entries are also linked.

```python
print(configuration_schedule.schedule[0].node.name)
>>> 'LSM'
```
2 changes: 1 addition & 1 deletion ldfparser/diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Iterable, Dict

from ldfparser.frame import LinUnconditionalFrame
from .frame import LinUnconditionalFrame

# LIN Diagnostic Frame IDs
LIN_MASTER_REQUEST_FRAME_ID = 0x3C
Expand Down
25 changes: 14 additions & 11 deletions ldfparser/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
LIN Frame utilities
"""
import warnings
from typing import Dict, List, Tuple, Union
from typing import Dict, List, Tuple, Union, TYPE_CHECKING

import bitstruct

from .signal import LinSignal
from .encoding import LinSignalEncodingType
if TYPE_CHECKING:
from .signal import LinSignal
from .encoding import LinSignalEncodingType
from .schedule import ScheduleTable

class LinFrame():
# pylint: disable=too-few-public-methods
Expand Down Expand Up @@ -42,7 +44,7 @@ class LinUnconditionalFrame(LinFrame):
:type signals: Dict[int, LinSignal]
"""

def __init__(self, frame_id: int, name: str, length: int, signals: Dict[int, LinSignal]):
def __init__(self, frame_id: int, name: str, length: int, signals: Dict[int, 'LinSignal']):
super().__init__(frame_id, name)
self.publisher = None
self.length = length
Expand All @@ -52,7 +54,7 @@ def __init__(self, frame_id: int, name: str, length: int, signals: Dict[int, Lin
@staticmethod
def _frame_pattern(
frame_size: int,
signals: List[Tuple[int, LinSignal]]) -> bitstruct.CompiledFormat:
signals: List[Tuple[int, 'LinSignal']]) -> bitstruct.CompiledFormat:
"""
Converts a frame layout into a bitstructure formatting string
Expand Down Expand Up @@ -109,7 +111,7 @@ def _flip_bytearray(data: bytearray) -> bytearray:

def encode(self,
data: Dict[str, Union[str, int, float]],
encoding_types: Dict[str, LinSignalEncodingType] = None) -> bytearray:
encoding_types: Dict[str, 'LinSignalEncodingType'] = None) -> bytearray:
"""
Encodes signal values into the LIN frame content
Expand Down Expand Up @@ -194,7 +196,7 @@ def encode_raw(self, data: Union[Dict[str, int], List[int]]) -> bytearray:

def decode(self,
data: bytearray,
encoding_types: Dict[str, LinSignalEncodingType] = None,
encoding_types: Dict[str, 'LinSignalEncodingType'] = None,
keep_unit: bool = False) -> Dict[str, Union[str, int, float]]:
"""
Decodes a LIN frame into the signals that it contains
Expand Down Expand Up @@ -268,7 +270,7 @@ def raw(self, data: Dict[str, int]) -> bytearray:

def data(self,
data: Dict[str, Union[str, int, float]],
converters: Dict[str, LinSignalEncodingType]) -> bytearray:
converters: Dict[str, 'LinSignalEncodingType']) -> bytearray:
"""
Returns a bytearray (frame content) by using the human readable signal values
Expand Down Expand Up @@ -299,7 +301,7 @@ def parse_raw(self, data: bytearray) -> Dict[str, int]:

def parse(self,
data: bytearray,
converters: Dict[str, LinSignalEncodingType]) -> Dict[str, Union[str, int, float]]:
converters: Dict[str, 'LinSignalEncodingType']) -> Dict[str, Union[str, int, float]]:
"""
Returns a mapping between Signal names and their human readable value
Expand All @@ -323,8 +325,9 @@ class LinEventTriggeredFrame(LinFrame):
LinEventTriggeredFrame is LinFrame in the schedule table that can contain different
unconditional frames from different nodes
"""
# TODO: add schedule table reference

def __init__(self, frame_id: int, name: str, frames: List[LinUnconditionalFrame]) -> None:
def __init__(self, frame_id: int, name: str, frames: List[LinUnconditionalFrame],
collision_resolving_schedule_table: 'ScheduleTable' = None) -> None:
super().__init__(frame_id, name)
self.frames = frames
self.collision_resolving_schedule_table = collision_resolving_schedule_table
26 changes: 26 additions & 0 deletions ldfparser/ldf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .signal import LinSignal
from .encoding import LinSignalEncodingType
from .node import LinMaster, LinSlave
from .schedule import ScheduleTable

class LDF():
# pylint: disable=too-many-instance-attributes,too-many-public-methods
Expand All @@ -33,6 +34,7 @@ def __init__(self):
self._signal_representations: Dict[LinSignal, LinSignalEncodingType] = {}
self._master_request_frame: LinDiagnosticRequest = None
self._slave_response_frame: LinDiagnosticResponse = None
self._schedule_tables: Dict[str, ScheduleTable] = {}
self._comments: List[str] = []

def get_protocol_version(self) -> LinVersion:
Expand Down Expand Up @@ -232,6 +234,30 @@ def get_diagnostic_signals(self) -> List[LinSignal]:
"""
return self._diagnostic_signals.values()

def get_schedule_table(self, name: str) -> ScheduleTable:
"""
Returns the schedule table with the given name
:param name: Name of the schedule table to find
:type name: str
:returns: Schedule table
:rtype: ScheduleTable
:raises: LookupError if the given schedule table is not found
"""
schedule = self._schedule_tables.get(name)
if schedule is None:
raise LookupError(f"No schedule table named '{name}' found!")
return schedule

def get_schedule_tables(self) -> List[ScheduleTable]:
"""
Returns all schedule tables
:returns: List of Schedule tables
:rtype: List[ScheduleTable]
"""
return self._schedule_tables.values()

@property
def master_request_frame(self) -> LinDiagnosticRequest:
return self._master_request_frame
Expand Down
Loading

0 comments on commit 6d95fe1

Please sign in to comment.