PyDIABAS 1.2.0
Python access to EDIABAS API
Requires Python: >=3.10

# pydiabas
**pydiabas** is a Python module for communication with ECUs (Electronic Control Units) in cars using **EDIABAS** as diagnosis system (eg. BMW, Volkswagen).
The module has only been tested on BMW cars.
Using this module makes it possible to make advantage of the comfort features of Python by still allowing communication to a car using **EDIABAS** on a very low level.

## Table of contents
1. [General Description](#1-general-description)
2. [System Requirements](#2-system-requirements)
3. [Installation](#3-installation)
4. [Quick Start](#4-quick-start)
5. [Module Documentation](#5-module-documentation)
    - [pydiabas module](#pydiabas-module)
        - [PyDIABAS class](#pydiabas-class)
        - [Result class](#result-class)
        - [Set class](#set-class)
        - [Row class](#row-class)
    - [ediabas module](#ediabas-module)
        - [EDIABAS class](#ediabas-class)
        - [statics](#statics)
        - [utils](#utils)
        - [api32](#api32)
    - [ecu module](#ecu-module)
        - [ECU class](#ecu-class)
        - [MSD80 class](#msd80-class)
    - [simulation module](#simulation-module)
        - [SimulatedPyDIABAS class](#simulatedpydiabas-class)
        - [CapturedJob class](#capturedjob-class)
        - [capture_jobs decorator](#capture_jobs-decorator)
6. [Tests](#6-tests)
7. [Limitations](#7-limitations)
8. [Future Development](#8-future-development)
9. [EDIABAS Troubleshooting](#9-ediabas-troubleshooting)
10. [License](#10-license)
11. [Change Log](#11-change-log)

## 1 General Description
The **pydiabas** module has two sub modules:
- **ecu**: For ECU specific tasks
- **ediabas**: For direct access to **EDIABAS** without using **pydiabas**
> **Note**
> For a better distinction between this documentation uses the following formatting:
> - **EDIABAS**: EDIABAS system installed separately on your pc
> - **ediabas**: ediabas Python module.
> - *EDIABAS*: EDIABAS class located in the ediabas module.

### Architecture
1. **EDIABAS**: It's the basis for any communication via the OBD cable tour your car, respective your cars ECUs. It is installed separately and not part of this Python module.
2. **ediabas**: Serves mainly as a wrapper to provide a more pythonic API for **EDIABAS** and  has an utility to facilitate result retrieval. This module can be used even without **pydiabas**.
3. **pydiabas**: Serves as an additional abstraction layer on top of the **ediabas** module to make working with **EDIABAS** more flawless and to hide some complexity from the user. This is the main abstraction layer to be used.
4. **ecu**: Can be used in addition to **pydiabas**. It contains a generic ECU class which can be used with any ECU to extract available jobs and tables.
Furthermore the are classes for single ECUs with specific functionality related to these ECUs.

## 2 System Requirements
To use **pydiabas** there are some system requirements to observe.
As far as I know, **EDIABAS** is only running on Windows.

### EDIABAS
A working **EDIABAS** (version >= 7.0.0) is required. The collection and installation of **EDIABAS** ist neither part of this module nor covered in this documentation. There are a lot of sources, instructions and tutorials out there.
Under [8. EDIABAS Troubleshooting](#8-ediabas-troubleshooting) you can finde some help regarding common problems using **EDIABAS**, focussing on configuring **EDIABAS** correctly to establish a communication via the OBD cable.

### OBD Cable
I'm using the [MaxDia Diag 2+](https://www.obdexpert.de/shopware/diagnose-artikel/fuer-bmw-fahrzeuge/33/maxdia-diag-2-diagnose-interface-fuer-bmw-fahrzeuge-bj.-2007-2016-ohne-software?c=20) cable from [obdexpert.de](https://www.obdexpert.de). This cable is usable for BMW from 2007 to 2016. If you need to connect to older BMWs, you may want to use this [Pin7 - Pin8 - Connector für MaxDia Diag2+](https://www.obdexpert.de/shopware/diagnose-artikel/fuer-bmw-fahrzeuge/28/pin7-pin8-connector-fuer-maxdia-diag2?c=20).

> **Note**
> I don't get payed by obdexpert.de, its just as personal recommendation based on my experience. There may be lots of other cables out there which will be as good or even better as my suggestion, but I've never used them.

### Python Version and dependencies
This module has been developed using `Python 3.12 32bit` and tested on `Python 3.13 32bit`. The minimum required version is `Python 3.10 32bit`.
As **EDIABAS** uses 32bits memory addresses, a 32bit Python version is necessary to load the **EDIABAS** dynamic library ("api32.dll"). Running this package on a 64bit Python version will fail!


## 3 Installation
You can use *pip* to install pydiabas using the following command
```
pip install pydiabas
```

## 4 Quick Start

To get your first data out of **pydiabas** you just need a few lines of code.
It's not necessary to have your OBD cable connected to your PC as `TMODE` is a simulated ECU which can be accessed without being connected to a car.

```py
# Make sure to use a 32bit Python version!

# Import the PyDIABAS class from the pydiabas module
from pydiabas import PyDIABAS

# Start the session
# Using Pythons context manager ensured proper closing of the EDIABAS session
with PyDIABAS() as pydiabas:

    # Ask the ECU named "TMODE" to execute a job named "LESE_INTERFACE_TYP"
    result = pydiabas.job("TMODE", "LESE_INTERFACE_TYP")

    # Access result data
    print(result["TYP"]) # prints: b'OBD'
```
> __Info:__ If you get the following error you are most probably using a 64bit Python version.  
> `OSError: [WinError 193] %1 is not a valid Win32 application`


## 5 Module Documentation

### pydiabas module
This is the main module to be used. It provides all functionality usually required to use **EDIABAS** for communication with ECUs. The **ediabas** sub-module offers more control but needs a deeper understanding about the **EDIABAS** system to work with.

#### PyDIABAS class
The *PyDIABAS* class provides a simple and comfortable API for communication via **EDIABAS**.

##### Starting and ending the **EDIABAS** session
After creating an instance of *PyDIABAS* the *start()* method must be called to set up everything for subsequent communication. This can be done either manually, or automatically by using Pythons context manager. Calling *start()* multiple times will usually not cause any problems or memory leaks.
After finishing communications the *end()* method must be called to stop the connection to **EDIABAS** and free the used resources. This is done automatically by using Pythons context manager, or must be done manually otherwise.
Forgetting to call *end()* usually doesn't cause any major problems, even for possible succeeding **EDIABAS** sessions on the same machine but it should always be done as a good habit.
During any time the present state of **EDIABAS** can be checked using the property *ready*. It returns *True* if **EDIABAS** is ready or *False* if not. Getting *False* as return value does not necessary mean that **EDIABAS** is not able to execute a Job. *False* may be returned if *start()* has not been called or failed, or if the previous job failed for any reason. In the last case a succeeding job may run successful.

```py
# Starting and ending an EDIABAS session manually
from pydiabas import PyDIABAS

# Create an PyDIABAS instance
pydiabas = PyDIABAS()

# Start the session
pydiabas.start()

# All the work will be done here
# Make sure to catch possible exceptions to make sure the session is being closed properly

# End the session
pydiabas.end()
```
```py
# Starting and ending an EDIABAS session automatically using Pythons context manager
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:
    # All the work will be done here
    # The session will be started and ended automatically even if exception occur
```
```py
# Using ready property
from pydiabas import PyDIABAS

pydiabas = PyDIABAS()
print(pydiabas.ready) # prints: False

pydiabas.start()
print(pydiabas.ready) # prints: True

pydiabas.end()
print(pydiabas.ready) # prints: False
```

##### Configurations
Initial **EDIABAS** configurations will be loaded from the respective `EDIABAS.ini` file in the `/bin` folder of your **EDIABAS** installation. Please refer to the documentation of your **EDIABAS** installation for further details.
Values not configured via this file will be set to hardcoded default values by **EDIABAS**.
Basically there is no need to change any configurations for routine usage of **pydiabas**.
If required, configuration can be changed using the *config()* method.
Changes to the configuration will be passed as keyword arguments to the *config()* method.
Any changes done via an *PyDIABAS* object will be stored inside the object and will be returned as a dict with lower lettered keys any time the *config()* method is called.
Accessing this dict without changing the configuration can be done by calling *config()* without passing any parameters.
A complete list of configuration parameters can be found in the documentation (usually placed in the `/Doku` folder) of your **EDIABAS** installation or in the docstring of the **EDIABAS** class.

```py
# Setting and reading configuration changes
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # Changing the value if "apiTrace" to 1 and getting current configuration changes made by pydiabas
    current_config = pydiabas.config(apiTrace=1, traceSize=4096)
    print(current_config) # prints {"apitrace: 1, "tracesize": 4096}
```

##### Job
Jobs are used for the actual communication.
They have to be addressed to a specific ECU and must pass the jobs name as argument.
Further data can be send as parameters to the ECU. Some ECUs can apply a result filter to return only the results with the names listet in this filter. If a result filter is set for ECUs not supporting this function, all results will be returned instead.
If the size of the data returned by the ECU is expected to be very large and only a small part is actually needed, the parameter *fetchall* can be set to *False* to skip the retrieval of all result values after job execution. The values must then be fetched manually using the functionality coming with the returned *Result* object. 
Calling the *job()* method will block the program until the results have been returned from the ECU and returns these as a *Result* object.
If the job fails, a *StateError* will be raised.

```py
# Executing a job
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # Executing a job on ECU "TMODE" which is possible even if no car or OBD cable is connected to the computer
    # The job "_JOBS" gets the names of all jobs available in the ECU
    result = pydiabas.job(ecu="TMODE", job="_JOBS")
```

##### Direct access to the *EDIABAS* instance
To enable direct access to the *EDIABAS* object, the property *ediabas* can be used. This might become necessary to perform very specific jobs or to get detailed information about the current state or error codes and description of the **EDIABAS** system.

```py
# Direct access to the EDIABAS object
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # Get the EDIABAS object
    ediabas = pydiabas.ediabas

    # Get current error description
    print(ediabas.errorText()) # prints: NO_ERROR in this case
```

##### Functions and Properties
> **PyDIABAS()** - > None
> 
> Creates a new PyDIABAS() instance without starting an **EDIABAS** session.
> ```
> >>> pydiabas = PyDIABAS()
> ```

> **start()** - > None
>
> Starts the **EDIABAS** server if necessary and sets up a new session. 
>
>If this operation is not successful a *StateError* will be raised. If *start()* is called after a configuration changes has been made using this PyDIABAS object, these configurations will automatically be set again after starting the new session.
> 
> **Raises** a *StateError* if failed.
> ```
> >>> pydiabas.start()
> ```

> **end()** -> None
>
> Current **EDIABAS** session is stopped and used resources will be released.
> ```
> >>> pydiabas.end()
> ```

> **reset()** -> None
>
> Has the same effect as calling *end()* and *start()* consecutive.
> ```
> >>> pydiabas.reset()
> ```

> **ready** -> *bool*
>
> Checks if the **EDIABAS** session has been started and the previous job (if any) ran successful.
>
> **Returns** *True* if **EDIABAS** is running properly.
> ```
> >>> pydiabas.ready
> True
>```

> **ediabas** -> *EDIABAS*
>
> Allows access to the *EDIABAS* object (in this case not the **EDIABAS** system installed on your pc but the Python *EDIABAS* object being part og the **ediabas** sub-module of **pydiabas** currently used by this *PyDIABAS* object).  
>
> **Returns** the *EDIABAS* instance being used by this PyDIABAS object.
> ```
> >>> pydiabas.ediabas
> <pydiabas.ediabas.ediabas.EDIABAS object at 0x.....>
> ```

> **config([\*\*kwargs])** -> dict
>
> Sets new configuration values (if passed as arguments) and returns applied configuration changes since creating this PyDIABAS instance.
>
> Optional parameter **\*\*kwargs** can be of any type but must match the required type depending on the configuration value to be changed.
> **Returns** currently applied configuration changes as a dict.
> ```
> >>> pydiabas.config(apiTrace=1)
> {"apitrace: 1}
>
> >>> pydiabas.config(traceSize=4096)
> {"apitrace: 1, "tracesize": 4096}
>
> >>> pydiabas.config()
> {"apitrace: 1, "tracesize": 4096}
> ```

> **job(ecu, job [parameters="", result_filter="", fetchall=*True*])** -> Result
>
> This method actually executes communication with the specified ECU. 
>
> The name of the ECU which should run this job and the jobs name must be passed at least.
> Optional job parameters can be send to the ECU either as *str* or *bytes*, using a semicolon as separator between possible multiple parameters. As a second option, the parameters can be passed as list of *str* or *bytes*. The semicolon as separator will be added automatically in this case before transmission to the ECU.
>
> >**Note:**:
> > If parameters are passed as a list, they must be all of the same type. So either as list only containing *str* objects or only containing *bytes* objects. A mix of types is not supported.
>
> A result_filter can be passed as *str* with semicolon as separator or list of *str* similar to the parameters but *bytes* are not supported here.
>
> Parameter **ecu** must be a *str*.  
> Parameter **job** must be a *str*.  
> Optional parameter **parameters** must be *str*, *bytes* or *list*.  
> Optional parameter **result_filter** must be *str* or *list*.  
> Optional parameter **fetchall** must be a *bool*.  
> **Returns** the *Result* object.
> ```
> >>> pydiabas.job("TMODE", "LESE_INTERFACE_TYP")
> <pydiabas.result.Result object at 0x.....>
> ```

#### Result class
Represents the result coming back from the ECU after a job has been executed.
An instance of this class will be returned by the method *job()* after the job has been executed by the ECU.
The class can be used to manually create *Result* objects. Data can be loaded into these object by fetching them, using on of the various fetch methods.

```py
# Getting Result object from a job
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # The Result object will be returned after the job has been executed
    result = pydiabas.job(ecu="TMODE", job="INFO")
```

```py
# Manually creating a Result object
from pydiabas import Result

# Creating an empty result object
result = Result()

# Fetch data from EDIABAS
# The returned data will always be the one of the last job executed if not manually changed to a previously saved result set
result.fetchall()
```
##### Data Structure
Data returned by an ECU via **EDIABAS** follows a specific pattern. The *Result* object uses the same structure to be consistent.

Each job execution creates as new result in **EDIABAS** and is only accessible until the next job is executed. There are possibilities to save result data inside **EDIABAS** but they are not discussed here. Refer to the respective documentation if required.

After the job has been finished, aborted or failed by the ECU, there will be some data available to be fetched. This data is called **EDIABAS** result.
The result comprises of one ore more sets, wich finally hold the data as key value pairs.
Set #0 always contains data about the job execution and is called *systemSet* in the *Result* object.
Sets #1-n will contain the data returned by the executed job. In some cases there might be no such data at all. These sets are called *jobSets* in the *Result* object.
Each *Set* contains a ist of *Row* objects which themselves have the attributes *name* and *value*.

Result
- *Set* #0: (*systemSet*)
    - *Row* #0: *name* = VARIANTE, *value* = TMODE
    - *Row* #1: *name* = SAETZE, *value* = 25
    - *Row* #2: *name* = JOBNAME, *value* = _JOBS
    - ...
- *Set* #1: (*jobSet*)
    - *Row* #0: *name* = JOBNAME, *value* = INFO
- *Set* #2: (*jobSet*)
    - *Row* #0: *name* = JOBNAME, *value* = INITIALISIERUNG
- *Set* #3: (*jobSet*)
    - *Row* #0: *name* = JOBNAME, *value* = SETZE_INTERFACE_ZURUECK
    - ... *Sets* can have more than one *Row*
- ...


##### Fetching Data from **EDIABAS**
If the *Result* object has been returned by a **pydiabas** job, data will automatically be fetched and no further action is required before accessing it through the *Result* object.

```py
# Automatically fetch all data together with executing the job
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # All data will be fetched automatically and will be available in the Result object
    result = pydiabas.job(ecu="TMODE", job="INFO")
```

Data can be manually fetched if necessary using one of he following methods.
This can be useful if a job is expected to return a lot of data and only a small part of this data is of interest. Inhibiting fetching of all of the data will reduce execution time.
Manual fetching methods will modify the *Result* object in place as well as returning the modified object to use this functions in both of the following ways:
```py
# Manually fetching in the same line together with the job execution
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # Automatic fetching of all data is inhibited by the fetchall parameter being set to False
    # Manual fetching is done by calling 'fetchsystemset'
    result = pydiabas.job(ecu="TMODE", job="INFO", fetchall=False).fetchsystemset()
```

```py
# Manually fetching after the Result object has been created modifying the object in place
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # Automatic fetching of all data is inhibited by the fetchall parameter being set to False
    # No data will be available
    result = pydiabas.job(ecu="TMODE", job="INFO", fetchall=False)

    # Modifying the Result object in place
    result.fetchsystemset()
```
##### Functions and Properties
For all examples in this chapter we assume the following data to be present in the *Result* object:

result:
- *Set* #0: (*systemSet*)
    - *Row* #0: *name* = VARIANTE, *value* = TMODE
    - *Row* #1: *name* = SAETZE, *value* = 25
    - *Row* #2: *name* = JOBNAME, *value* = _JOBS
- *Set* #1: (*jobSet*)
    - *Row* #0: *name* = JOBNAME, *value* = INFO
    - *Row* #1: *name* = INFO, *value* = "INFOTEXT"
- *Set* #2: (*jobSet*)
    - *Row* #0: *name* = JOBNAME, *value* = INITIALISIERUNG

<br>

```py
# Using Python standart functionality with a Result object

# Get number of jobSets
len(result)

# Check if there is a jobSet available
bool(result)

# Get a string representation
str(result)

# Check if there is a Row with "ECU" as name in any jobSet
"ECU" in result

# Get the value of the first Row with name "ECU" in any jobSet
result["ECU"] # Raises a KeyError of no such Row found

# Get the jobSet with the given index
result[0] # Raises an IndexError if out ouf range

# Get a Result object containing only the sliced jobSets and the systemSet
result[0:5:2]

# Iterate trough all jobSets in the Result object
for job_set in result:
    pass
```
> **clear()** -> None
>
> Clears all data from the *Result* object.
> ```
> >>> result.clear()
> >>> bool(result)
> False
>```

> **fetchsystemset()** -> *Result*
>
> Fetches only set #0 from **EDIABAS** containing all system related data.
>
> **Returns** the *Result* object after fetching.
> ```
> >>> result.fetchsystemset()
> <pydiabas.result.Result object at 0x.....>
>```

> **fetchjobsets()** -> *Result*
>
> Fetches all job sets (#1-n) from **EDIABAS**.
>
> **Returns** the *Result* object after fetching.
> ```
> >>> result.fetchjobsets()
> <pydiabas.result.Result object at 0x.....>
>```

> **fetchall()** -> *Result*
>
> Fetches all sets from **EDIABAS**.
>
> **Returns** the *Result* object after fetching.
> ```
> >>> result.fetchall()
> <pydiabas.result.Result object at 0x.....>
>```

> **fetchname(name)** -> *Result*
>
> Fetches all data from the **EDIABAS** job sets (#1-n) where the name of the data matches the given name.
>
> Parameter **name** must be a *str*.  
> **Returns** the *Result* object after fetching.
> ```
> >>> result.fetchname("JOBNAME")
> <pydiabas.result.Result object at 0x.....>
>```

> **fetchnames(names)** -> *Result*
>
> Fetches all data from the **EDIABAS** job sets (#1-n)  where the name of the data matches one of the names in the list of names.
>
> Parameter **name**s must be a list of *str*.  
> **Returns** the *Result* object after fetching.
> ```
> >>> result.fetchnames(["JOBNAME", "INFO"])
> <pydiabas.result.Result object at 0x.....>
>```

> **systemSet** -> *Set*
>
> **EDIABAS** set #0 contains information about system status and job execution.
>
> **Returns** *Set* #0.
> ```
> >>> result.systemSet
> <pydiabas.result.Set object at 0x.....>
>```

> **jobSets** -> *list*[*Set*]
>
> **EDIABAS** set #1-n contain data returned by the executed job.
>
> **Returns** *Sets* #1-n as a *list* of *Sets*. If not such *Sets* are available, an empty *list* is returned.
> ```
> >>> result.jobSets
> [<pydiabas.result.Set object at 0x.....>, <pydiabas.result.Set object at 0x.....>]
>```

> **ecu** -> *str*
>
> **Returns** the name of the ECU as stated in the result.
> ```
> >>> result.ecu
> 'TMODE'
>```

> **jobname** -> *str*
>
> **Returns** the name of the Job as stated in the result.
> ```
> >>> result.jobname
> '_JOBS'
>```

> **jobstatus** -> *str*
>
> **Returns** the status of the job as stated in the result.
> ```
> >>> result.jobstatus
> 'OK'
>```

> **as_dicts()** -> *list*[*dict*]
>
> Generates a *dict* containing all *Rows* as key value pairs for each *Set* in the *Result*. The *systemSet* will always be at index 0 of this list.
>
> **Return**s the data contained in the *Result* as *dict*.
> ```
> >>> result.as_dicts()
> [{'VARIANTE': 'TMODE', 'SAETZE': 25, 'JOBNAME': '_JOBS'}, {'JOBNAME': 'INFO', 'INFO': 'INFOTEXT'}, {'JOBNAME': 'INITIALISIERUNG'}]
>```

> **count(name)** -> *int*
>
> Count the number of *Rows* matching the given name in all *jobSets* of the *Result*.
>
> Parameter **name** must be a *str*.  
> **Returns** the number of occurrences.
> ```
> >>> result.count("JOBNAME")
> 2
>
> >>> result.count("INFO")
> 1
>```

> **index(name, [start=0, end=None])** -> *int*
>
> Looks for the first *jobSet* which contains a *Row* matching the given name.
> First and last *jobSet* to be searched can be defined using the start and end parameter.
> 
> Parameter **name** must be a *str*.  
> Optional parameter **start** must be an *int*.  
> Optional parameter **end** must be an *int*.  
> **Returns** the index of the first *jobSet* containing a *Row* with the given name.  
> **Raises** a ValueError if no *Row* matching the given name has been found.
> ```
> >>> result.index("INFO")
> 1
> 
> >>> result.index("INFO", start=1)
> 1
> 
> >>> result.index("INFO", end=2)
> 1
> 
> >>> result.index("INFO", start=1, end=2)
> 1
> 
> >>> result.index("INFO", end=1)
> ValueError: 'INFO' not in set
> 
> >>> result.index("TEST")
> ValueError: 'TEST' not in set
>```

> **get(name, [default=None])** -> *int* | *str* | *bytes* | *float* | *None*
>
> Gets the value of the first *Row* matching the given name starting from the first *jobSet* to the last.
> Any further occurrences of the name in other *jobSets* will be ignored.
> A default value can be set to be returned in case no *Row* with the given name can be found instead of returning *None*.
>
> Parameter **name** must be a *str*.  
> Optional parameter **default** can be of any type.  
> **Returns** the value if the *Row* or the default value.
> ```
> >>> result.get("JOBNAME")
> '_JOBS'
>
> >>> result.get("SAETZE")
> 25
>
> >>> result.get("INFO")
> 'INFOTEXT'
>
> >>> result.get("TEST")
> None
>
> >>> result.get("TEST", default=0)
> 0
>```

> **get_in(pattern, [default=None])** -> *int* | *str* | *bytes* | *float* | *None*
>
> Gets the value of the first *Row* having the pattern in its name from the first *jobSet* to the last.
> Any further occurrences of the name in other *jobSets* will be ignored.
> A default value can be set to be returned in case no matching *Row* can be found instead of returning *None*.
>
> Parameter **pattern** must be a *str*.  
> Optional parameter **default** can be of any type.  
> **Returns** the value if the *Row* or the default value.
> ```
> >>> result.get_in("JOBNAME")
> '_JOBS'
>
> >>> result.get_in("JOB")
> '_JOBS'
>
> >>> result.get_in("NAME")
> '_JOBS'
>
> >>> result.get_in("JOBNAMES")
> None
>
> >>> result.get_in("JOBNAMES", default=0)
> 0
>```

> **get_fn(fn, [default=None])** -> *int* | *str* | *bytes* | *float* | *None*
>
> Gets the value of the first *Row* where the given function returns True when the *Rows* name is passed as parameter starting from the first *Row* to the last.
> Any further occurrences of the name in other *jobSets* will be ignored.
> A default value can be set to be returned in case no matching *Row* can be found instead of returning *None*.
>
> Parameter **fn** must be a *Callable*.  
> Optional parameter **default** can be of any type.  
> **Returns** the value if the *Row* or the default value.
> ```
> >>> result.get_fn(lambda name: name.startswith("JOB))
> '_JOBS'
>
> >>> result.get_fn(lambda name: len(name) == 4)
> 'INFO'
>
> >>> result.get_fn(lambda name: name == "TEST")
> None
>```


#### Set class
Is part of a *Result's* data structure and represents a set of data coming back from the **EDIABAS** job.  
A *Set* contains *Row* objects for each row in the respective set of the **EDIABAS** data.

```py
# Get a Set from a Result object
from pydiabas import PyDIABAS

with PyDIABAS() as pydiabas:

    # Getting the result from a job
    result = pydiabas.job(ecu="TMODE", job="INFO")

    # Getting the system set from a Result object
    system_set = result.systemSet
```

##### Functions and Properties
For all examples in this chapter we assume the following data to be present in the *Set* object:

system_set:
- *Row* #0: *name* = VARIANTE, *value* = TMODE
- *Row* #1: *name* = SAETZE, *value* = 25
- *Row* #2: *name* = JOBNAME, *value* = _JOBS

<br>

```py
# Using Python standart functionality with a Set

# Get number of Rows
len(system_set)

# Check if Set contains at least one Row
bool(system_set)

# Get a string representation
str(system_set)

# Check if there is a Row with "ECU" as name
"ECU" in system_set

# Get the value of the Row with name "ECU" or raises a KeyError
system_set["ECU"]

# Get the Row at index 0 or raises and IndexError
system_set[0]

# Get a Set object containing only the sliced Rows
system_set[0:5:2]

# Iterate trough all Rows in the Set
for row in system_set:
    pass
```

> **all** -> *list*[*Row*]
>
> Generates a *list* containing all *Rows* in this *Set*.
>
> **Returns** a *list* containing all *Rows*.
> ```
> >>> system_set.all
> [Row(name='VARIANTE', value='TMODE'), Row(name='SAETZE', value=25), Row(name='JOBNAME', value='_JOBS')]
>```

> **as_dict()** -> *dict*
>
> Generates a *dict* containing all *Rows* of this *Set* with the *Rows* name as key. As there are no name duplicates in all *Rows* of a *Set*, not data will be lost.
>
> **Returns** all *Rows* as a *dict*.
> ```
> >>> system_set.as_dict()
> {'VARIANTE': 'TMODE', 'SAETZE': 25, 'JOBNAME': '_JOBS'}
>```

> **index(name, [start=0, end=None])** -> *int*
>
> Looks for the first *Row* matching the given name.
> First and last *Row* to be searched can be defined using the start and end parameter.
> 
> Parameter **name** must be a *str*.  
> Optional parameter **start** must be an *int*.  
> Optional parameter **end** must be an *int*.  
> **Returns** the index of the first *Row* with the given name.  
> **Raises** a ValueError if no *Row* matching the given name has been found.
> ```
> >>> system_set.index("SAETZE")
> 1
> 
> >>> system_set.index("SAETZE", start=1)
> 1
> 
> >>> system_set.index("SAETZE", end=2)
> 1
> 
> >>> system_set.index("SAETZE", start=1, end=2)
> 1
> 
> >>> system_set.index("SAETZE", end=1)
> ValueError: 'SAETZE' not in set
> 
> >>> system_set.index("TEST")
> ValueError: 'TEST' not in set
>```

> **keys()** -> *list*
>
> **Returns** a *list* containing the name of all *Rows* in this *Set*.
> ```
> >>> system_set.keys()
> ['VARIANTE', 'SAETZE', '_JOBS']
>```

> **values()** -> *list*
>
> **Returns** a *list* containing the values of all *Rows* in this *Set*.
> ```
> >>> system_set.values()
> ['TMODE', 25, '_JOBS']
>```

> **items()** -> *list*[*tuple*]
>
> **Returns** a *list* containing *tuples* (name, value) for all *Rows* in this *Set*.
> ```
> >>> system_set.items()
> [('VARIANTE', 'TMODE'), ('SAETZE', 25), ('JOBNAME', '_JOBS')]
>```

> **get(name, [default=None])** -> *int* | *str* | *bytes* | *float* | *None*
>
> Gets the value of the *Row* matching the given name starting from the first *Row* to the last.
> A default value can be set to be returned in case no *Row* with the given name can be found instead of returning *None*.
>
> Parameter **name** must be a *str*.  
> Optional parameter **default** can be of any type.  
> **Returns** the value if the *Row* or the default value.
> ```
> >>> system_set.get("JOBNAME")
> '_JOBS'
>
> >>> system_set.get("SAETZE")
> 25
>
> >>> system_set.get("TEST")
> None
>
> >>> system_set.get("TEST", default=0)
> 0
>```

> **get_in(pattern, [default=None])** -> *int* | *str* | *bytes* | *float* | *None*
>
> Gets the value of the first *Row* having the pattern in its name from the first *Row* to the last.
> A default value can be set to be returned in case no matching *Row* can be found instead of returning *None*.
>
> Parameter **pattern** must be a *str*.  
> Optional parameter **default** can be of any type.  
> **Returns** the value if the *Row* or the default value.
> ```
> >>> system_set.get_in("JOBNAME")
> '_JOBS'
>
> >>> system_set.get_in("JOB")
> '_JOBS'
>
> >>> system_set.get_in("NAME")
> '_JOBS'
>
> >>> system_set.get_in("JOBNAMES")
> None
>
> >>> system_set.get_in("JOBNAMES", default=0)
> 0
>```

> **get_fn(fn, [default=None])** -> *int* | *str* | *bytes* | *float* | *None*
>
> Gets the value of the first *Row* where the given function returns True when the *Rows* name is passed as parameter starting from the first *Row* to the last.
> A default value can be set to be returned in case no matching *Row* can be found instead of returning *None*.
>
> Parameter **fn** must be a *Callable*.  
> Optional parameter **default** can be of any type.  
> **Returns** the value if the *Row* or the default value.
> ```
> >>> system_set.get_fn(lambda name: name.startswith("JOB))
> '_JOBS'
>
> >>> system_set.get_fn(lambda name: len(name) == 6)
> 25
>
> >>> system_set.get_fn(lambda name: name == "TEST")
> None
>```


#### Row class
Represents a row in an EDIABAS result set. Name and value of the Row can be accessed via the respective attributes (Row.name and Row.value).

If we have a row with name="SAETZE" and value=25 we can use the Row object as follows
```
>>> row.name
'SAETZE'

>>> row.value
25
```


### ediabas module
This module is used by **pydiabas** behind the scenes. Most of the functionality is being a wrapper around the **EDIABAS** API to provide a pythonic way of accessing it.

As using this module is not necessary for most of the users, a detailed documentation is not provided at this place. Class and methods are documented in the respective source code file.

A detailed description about handling the **EDIABAS** API can be found in the `/Doku` folder of your **EDIABAS** installation. 

The *EDIABAS* class provides methods with the same names and effects as the functions available in the **EDIABAS** API with some general differences.
As the **EDIABAS** API uses the 'C-style' of function signatures by providing a reference to a variable in which the return value of the job should be stored and returning the status of the job as return value of the function like:
```C
// C-style function signature used in EDIABAS

int value;

// Value will be set to the referenced variable trough side-effect of the function
// Return value of the function gives information about the job status
bool job_sts = arbitrary_ediabas_function(&value);
```
This is translated in to the 'python-style' of function signatures as follows:
```py
# Python-style function signature used in ediabas

try:
    # Return value of the function is the values asked for
    value = arbitrary_ediabas_function()
except JobFailedError:
    # A JobFailedError exception is raised if the job fails
```
Besides these changes, the **ediabas** module works the same as the **EDIABAS** API.
A details description of **EDIABAS** can be found in the `/Doku` folder of your **EDIABAS** installation. Details about the **ediabas** module can be found in the source code file itself.

#### EDIABAS class
Class representing the **EDIABAS** API. Can be used in very much the same way as the **EDIABAS** API itself.

#### statics
Contains all static constants used by this **ediabas** module

#### utils
Provides helper functions for a more comfortable and clean way of interaction with the **ediabas** module.

> **getResult(ediabas, name, [set=1])** -> *str* | *bytes* | *int* | *float* | *None*
> 
> Accesses the **EDIABAS** result and searches the given result set (defaults to 1, as this is the first set containing data returned by the job) data with the given name.  
> Checks the format of the data, gets the data and casts it to the most appropriate Python data type. If no data with the name is found, *None* will be returned.
>
> Parameter **ediabas** must be an *EDIABAS* instance.  
> Parameter **name** must be a *str*.  
> Optional parameter **set** must be an *int*.  
> **Returns** the value of the data or *None*.

#### api32
Is just a wrapper around the `api32.dll` library, loading this library and extracting all the functions.


### ecu module
This module facilitates the execution of tasks that are frequently used when working with ECUs. There are some generic functions that can be used on all ECUs in this class and functions more specific to a single ECU which can be found in the respective class. For now, only specific tasks for MSD80 are implemented.  
A *PyDIABAS* instance need to be set up separately to provide connection to the ECU via **EDIABAS**.

```py
# Get available jobs by using the ECU class
from pydiabas import PyDIABAS
from pydiabas.ecu import ECU

# Create a new ECU object with its name set to TMODE
tmode = ECU("TMODE")

# Start pydiabas communication
with PyDIABAS() as pydiabas:

    # Get all available jobs
    # A PyDIABAS object must always be passed to enable communication
    jobs = tmode.get_jobs(pydiabas)

# Print all jobs
for job in jobs:
    print(job)
```

#### ECU class
Offers generic functions that can be used on all ECUs. 

##### Initialization
To be able to use the *ECU* object, the name of the ECU to communicate with must be set first. This can either be done by passing the name as argument when creating the object or it can be set (or even changed) later via the *name* instance variable.
```py
# Setting the name of the ECU
from pydiabas.ecu import ECU

# Passing the name as arguments when creating the object
tmode = ECU("TMODE")

# Settings the name after creating the object
frm = ECU()
frm.name = "FRM"
```
```py
# Executing a job in an ECU
from pydiabas import PyDIABAS
from pydiabas.ecu import ECU

# Create an ECU object
tmode = ECU("TMODE")

# Start a PyDIABAS session
with PyDIABAS() as pydiabas:

    # Jobs can be executed using the ECU object in the same way as using the PyDIABAS instance by just changing the ecu parameter with the pydiabas parameter.
    tmode.job(pydiabas, "LESE_INTERFACE_TYP")
```

##### Functions
> **job(_pydiabas_, job [parameters="", result_filter="", fetchall=*True*])** -> Result
>
> This method actually executes communication with the ECU. 
>
>This method is just a utility to be able to execute jobs directly on the ECU object instead of using a PyDIABAS object to do so. It just calls the *job()* method of the given PyDIABAS instance with the parameter **ecu** set to the name ob this ECU object.  
> Consult the documentation of the *PyDIABAS.job()* method for further details.
>
> Parameter **pydiabas** must be a *PyDIABAS* object.  
> Parameter **job** must be a *str*.  
> Optional parameter **parameters** must be *str*, *bytes* or *list*.  
> Optional parameter **result_filter** must be *str* or *list*.  
> Optional parameter **fetchall** must be a *bool*.  
> **Returns** the *Result* object.
> ```
> >>> ecu.job(pydiabas, "LESE_INTERFACE_TYP")
> <pydiabas.result.Result object at 0x.....>
> ```

> **get_jobs(_pydiabas_, [details=*True*, verbose=*False*])** -> *dict*[*dict*]
>
> Gets the names of all available jobs in the ECU and adds them as keys to a *dict* with an empty *dict* set as the value. If details is set to *True* these empty *dicts* will be filled with additional information about the job if available in the ECU.  
> Settings *verbose* to *True* will print some information concerning the execution progress to the terminal as extracting a lot of data from the an ECU may take a few minutes.
> 
> Parameter **pydiabas** must be a *PyDIABAS* instance.  
> Optional parameter **details** must be a *bool*.  
> Optional parameter **verbose** must be a *bool*.  
> **Returns** a *dict* containing information about the available jobs.
> ```
> >>> ecu.get_jobs(pydiabas, details=False)
> {'INFO': {}, 'INITIALISIERUNG': {}, 'SETZE_INTERFACE_ZURUECK': {}, ...}
> 
> >>> ecu.get_jobs(pydiabas, details=True)
> {
>     'INFO': {
>         'comments': ['Information SGBD'],
>         'arguments': [],
>         'results': [
>             {
>             'name': 'ECU',
>             'type': 'string',
>             'comments': ['Steuergerät im Klartext']
>             }, {
>             'name': 'ORIGIN',
>             'type': 'string',
>             'comments': ['Steuergeräte-Verantwortlicher']
>             }, {
>             'name': 'REVISION',
>             'type': 'string',
>             'comments': ['