Skip to content

Commit

Permalink
Merge pull request #12 from seemoo-lab/disable_status_ack
Browse files Browse the repository at this point in the history
Implement support for disabling ack before status messages
  • Loading branch information
lumagi authored Sep 6, 2023
2 parents fe0833d + 7b8faed commit 9ab6009
Show file tree
Hide file tree
Showing 25 changed files with 1,166 additions and 757 deletions.
40 changes: 25 additions & 15 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@ pyshimmer: Unofficial Python API for Shimmer Sensor devices
.. image:: https://github.com/seemoo-lab/pyshimmer/actions/workflows/build.yml/badge.svg
:target: https://github.com/seemoo-lab/pyshimmer

.. image:: https://www.codefactor.io/repository/github/seemoo-lab/pyshimmer/badge/master
:target: https://www.codefactor.io/repository/github/seemoo-lab/pyshimmer/overview/master
:alt: CodeFactor

.. image:: https://codecov.io/gh/seemoo-lab/pyshimmer/branch/master/graph/badge.svg?token=EHK1ISJH7Z
:target: https://codecov.io/gh/seemoo-lab/pyshimmer

.. contents::

General Information
Expand Down Expand Up @@ -75,23 +68,40 @@ You can then run the tests from the repository root by simply issuing:
Shimmer Firmware
^^^^^^^^^^^^^^^^

The vanilla version of the `Shimmer3 firmware <https://github.com/ShimmerResearch/shimmer3>`_ exhibits several
unfixed bugs (see the `issues page <https://github.com/ShimmerResearch/shimmer3/issues>`_ for more information).
Depending on the firmware you intend to use, you will need to compile and run a custom patched version of the firmware.
In the following table, we list the tested firmware versions and their compatibility.
As of version v0.15.4 of the `Shimmer3 firmware <https://github.com/ShimmerResearch/shimmer3>`_ the Python API is
fully compatible to the firmware. Older versions of the vanilla firmware exhibit several bugs and are incompatible.
If you intend to use a firmware version older than 0.15.4, you will need to compile and run a custom patched version of
the firmware. In the following table, the firmware versions and their compatibility are listed.

Compatibility Table
"""""""""""""""""""

============= ========= ============= ======================================================================
Firmware Type Version Compatibility Issues
============= ========= ============= ======================================================================
LogAndStream v0.15.4 Compatible You will need to use pyshimmer v0.4 or newer
to v0.4.0
LogAndStream v0.11.0 Incompatible - `Issue 7 <https://github.com/ShimmerResearch/shimmer3/issues/7>`_
- `Issue 10 <https://github.com/ShimmerResearch/shimmer3/issues/10>`_
SDLog v0.21.0 Compatible Untested
SDLog v0.19.0 Compatible
============= ========= ============= ======================================================================

If you want to use the *LogAndStream* firmware with the pyshimmer library, you will need to compile and program a
patched version of the firmware. We provide a forked repository which features the necessary fixes
`here <https://github.com/seemoo-lab/shimmer3/>`_. It also contains instructions on how to compile and program the
firmware.
It is recommended to use the newest *LogAndStream* firmware that is compatible to the API. If you want to use an older
version with the pyshimmer library, you will need to compile and program a patched version of the firmware. We provide
a forked repository which features the necessary fixes `here <https://github.com/seemoo-lab/shimmer3/>`_.
It also contains instructions on how to compile and program the firmware.

Notes on Firmware Version 0.15.4
""""""""""""""""""""""""""""""""
Starting with Firmware version v0.15.4,
`the race condition issue <https://github.com/ShimmerResearch/shimmer3/issues/7>`_ in the Bluetooth stack has been
fixed. Additionally, the Shimmer3 now supports an additional command to disable the unsolicited status acknowledgment
byte (see `Issue 10 <https://github.com/ShimmerResearch/shimmer3/issues/10>`_). The pyshimmer Bluetooth API tries to
automatically detect if the Shimmer3 runs a firmware newer or equal to v0.15.4 and automatically issues the command
to disable the unsolicited status acknowledgment at startup. You can optionally disable this feature in the constructor.
With this new command, the state machine in the Bluetooth API of pyshimmer is compatible to the vanilla firmware
version.

Creating udev rules for persistent device filenames
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
10 changes: 6 additions & 4 deletions pyshimmer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from .bluetooth.bt_api import ShimmerBluetooth
from .bluetooth.bt_commands import DataPacket
from .uart.dock_api import ShimmerDock
from .reader.shimmer_reader import ShimmerReader
from .dev.base import DEFAULT_BAUDRATE
from .dev.channels import ChannelDataType, EChannelType
from .dev.exg import ExGMux, ExGRLDLead, ERLDRef, ExGRegister
from .dev.fw_version import EFirmwareType
from .reader.binary_reader import ShimmerBinaryReader
from .reader.shimmer_reader import ShimmerReader
from .uart.dock_api import ShimmerDock
from .util import fmt_hex
from .device import EChannelType, DEFAULT_BAUDRATE, ChannelDataType, ExGRegister, EFirmwareType, ERLDRef, ExGRLDLead, \
ExGMux
98 changes: 81 additions & 17 deletions pyshimmer/bluetooth/bt_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from queue import Queue, Empty
from threading import Event, Thread
from typing import List, Tuple, Callable, Iterable
from typing import List, Tuple, Callable, Iterable, Optional

from serial import Serial

Expand All @@ -24,11 +24,12 @@
GetFirmwareVersionCommand, InquiryCommand, StartStreamingCommand, StopStreamingCommand, DataPacket, \
GetEXGRegsCommand, SetEXGRegsCommand, StartLoggingCommand, StopLoggingCommand, GetExperimentIDCommand, \
SetExperimentIDCommand, GetDeviceNameCommand, SetDeviceNameCommand, DummyCommand, GetBatteryCommand, \
SetSamplingRateCommand, SetSensorsCommand
SetSamplingRateCommand, SetSensorsCommand, SetStatusAckCommand
from pyshimmer.bluetooth.bt_const import ACK_COMMAND_PROCESSED, DATA_PACKET, FULL_STATUS_RESPONSE, INSTREAM_CMD_RESPONSE
from pyshimmer.bluetooth.bt_serial import BluetoothSerial
from pyshimmer.device import EChannelType, ChDataTypeAssignment, ExGRegister, EFirmwareType, ChannelDataType, \
ESensorGroup
from pyshimmer.dev.channels import ChDataTypeAssignment, ChannelDataType, EChannelType, ESensorGroup
from pyshimmer.dev.exg import ExGRegister
from pyshimmer.dev.fw_version import EFirmwareType, FirmwareVersion, FirmwareCapabilities
from pyshimmer.serial_base import ReadAbort
from pyshimmer.util import fmt_hex, PeekQueue

Expand Down Expand Up @@ -255,31 +256,80 @@ def clear_queues(self) -> None:


class ShimmerBluetooth:
"""Main API for communicating with the Shimmer via Bluetooth

:arg serial: The serial interface to use for communication
"""
def __init__(self, serial: Serial, disable_status_ack: bool = True):
"""API for communicating with the Shimmer via Bluetooth
This class implements support for talking to the Shimmer LogAndStream firmware via Bluetooth.
Each command is encapsulated as a method that can be called to invoke the corresponding command.
All commands are executed synchronously. This means that the method call will block until the
Shimmer has processed the request and responded.
def __init__(self, serial: Serial):
:param serial: The serial channel that encapsulates the rfcomm Bluetooth connection to the Shimmer
:param disable_status_ack: Starting with LogAndStream firmware version 0.15.4, the vanilla firmware
supports disabling the acknowledgment byte before status messages. This removes the need for
running a custom firmware version on the Shimmer. If this flag is set to True, the API will
query the firmware version of the Shimmer and automatically send a command to disable the status
acknowledgment byte at startup. You can set it to True if you don't want this or if it causes
trouble with your firmware version.
"""
self._serial = BluetoothSerial(serial)
self._bluetooth = BluetoothRequestHandler(self._serial)

self._thread = Thread(target=self._run_readloop, daemon=True)

self._initialized = False
self._disable_ack = disable_status_ack

self._fw_version: Optional[FirmwareVersion] = None
self._fw_caps: Optional[FirmwareCapabilities] = None

@property
def initialized(self) -> bool:
"""Specifies if the connection was initialized
This property helps to determine if the capabilities property will return a valid value.
:return: True if initialize() was called, otherwise False
"""
return self._initialized

@property
def capabilities(self) -> FirmwareCapabilities:
"""Return the capabilities of the device firmware
This property shall only be accessed after invoking initialize().
:return: A FirmwareCapabilities instance representing the version and capabilities of the firmware
"""
return self._fw_caps

def __enter__(self):
self.initialize()
return self

def __exit__(self, exc_type, exc_value, exc_traceback):
self.shutdown()

def _set_fw_capabilities(self) -> None:
fw_type, fw_ver = self.get_firmware_version()
self._fw_caps = FirmwareCapabilities(fw_type, fw_ver)

def initialize(self) -> None:
"""Initialize the reading loop of the API
"""Initialize the Bluetooth connection
Initialize the reading loop by starting a new thread to handle all reads asynchronously
This method must be invoked before sending commands to the Shimmer. It queries the Shimmer version,
optionally disables the status acknowledgment and starts the read loop.
"""
self._thread.start()

self._set_fw_capabilities()

if self.capabilities.supports_ack_disable and self._disable_ack:
self.set_status_ack(enabled=False)

self._initialized = True

def shutdown(self) -> None:
"""Shutdown the read loop
Expand Down Expand Up @@ -351,6 +401,7 @@ def set_sampling_rate(self, sr: float) -> None:

def get_battery_state(self, in_percent: bool) -> float:
"""Retrieve the battery state of the device
:param in_percent: True: calculate battery state in percent; False: calculate battery state in Volt
:return: The battery state in percent / Volt
"""
Expand Down Expand Up @@ -403,16 +454,16 @@ def get_status(self) -> List[bool]:
"""
return self._process_and_wait(GetStatusCommand())

def get_firmware_version(self) -> Tuple[EFirmwareType, int, int, int]:
def get_firmware_version(self) -> Tuple[EFirmwareType, FirmwareVersion]:
"""Get the version of the running firmware
:return: A tuple of four values:
- The firmware type as enum, i.e. SDLog, LogAndStream, ...
- the major version as int
- the minor version as int
- the patch level as int
:return: The firmware type as enum, i.e. SDLog or LogAndStream
and the numeric firmware version
"""
return self._process_and_wait(GetFirmwareVersionCommand())
fw_type, major, minor, rel = self._process_and_wait(GetFirmwareVersionCommand())
fw_version = FirmwareVersion(major, minor, rel)

return fw_type, fw_version

def get_exg_register(self, chip_id: int) -> ExGRegister:
"""Get the current configuration of one of the two ExG registers of the device
Expand Down Expand Up @@ -522,3 +573,16 @@ def send_ping(self) -> None:
The command can be used to test the connection. It does not return anything.
"""
self._process_and_wait(DummyCommand())

def set_status_ack(self, enabled: bool) -> None:
"""Send a command to enable or disable the status acknowledgment
This command should normally not be called directly. If enabled in the constructor, the command
will automatically be sent to the Shimmer if the firmware supports it. It can be used to make
vanilla firmware versions compatible with the state machine of the Python API.
:param enabled: If set to True, enable status acknowledgment byte. This will make the
firmware incompatible to the Python API. If set to False, disable sending the status ack.
In this state, the firmware is compatible to the Python API.
"""
self._process_and_wait(SetStatusAckCommand(enabled))
28 changes: 26 additions & 2 deletions pyshimmer/bluetooth/bt_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@

from pyshimmer.bluetooth.bt_const import *
from pyshimmer.bluetooth.bt_serial import BluetoothSerial
from pyshimmer.device import dr2sr, EChannelType, ChannelDataType, sec2ticks, ticks2sec, ExGRegister, \
get_firmware_type, sr2dr, ESensorGroup, serialize_sensorlist

from pyshimmer.dev.base import dr2sr, sr2dr, sec2ticks, ticks2sec
from pyshimmer.dev.channels import ChannelDataType, EChannelType, ESensorGroup, serialize_sensorlist
from pyshimmer.dev.exg import ExGRegister
from pyshimmer.dev.fw_version import get_firmware_type

from pyshimmer.util import bit_is_set, resp_code_to_bytes, calibrate_u12_adc_value, battery_voltage_to_percent


Expand Down Expand Up @@ -477,6 +481,26 @@ def __init__(self, dev_name: str):
super().__init__(SET_SHIMMERNAME_COMMAND, dev_name)


class SetStatusAckCommand(ShimmerCommand):

def __init__(self, enabled: bool):
"""Command to enable/disable the ACK byte before status messages
By default, the Shimmer firmware sends an acknowledgment byte before
sending unsolicited status messages to the host. This confuses the state
machine of the Python API but is always expected by the official Shimmer
software. This command is used by the Python API to automatically disable
the acknowledgment when connecting to a Shimmer.
:param enabled: If set to True, the acknowledgment is sent. If set to False,
the acknowledgment is not sent.
"""
self._enabled = enabled

def send(self, ser: BluetoothSerial) -> None:
ser.write_command(ENABLE_STATUS_ACK_COMMAND, "<B", int(self._enabled))


class StartLoggingCommand(OneShotCommand):
"""Begin logging data to the SD card
Expand Down
4 changes: 3 additions & 1 deletion pyshimmer/bluetooth/bt_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from pyshimmer.device import EChannelType
from pyshimmer.dev.channels import EChannelType

ACK_COMMAND_PROCESSED = 0xFF
INSTREAM_CMD_RESPONSE = 0x8A
Expand Down Expand Up @@ -82,6 +82,8 @@
START_LOGGING_COMMAND = 0x92
STOP_LOGGING_COMMAND = 0x93

ENABLE_STATUS_ACK_COMMAND = 0xA3

"""
The Bluetooth LogAndStream API assigns a numerical index to each channel type. This dictionary maps each index to the
corresponding channel type.
Expand Down
15 changes: 15 additions & 0 deletions pyshimmer/dev/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# pyshimmer - API for Shimmer sensor devices
# Copyright (C) 2023 Lukas Magel

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
Loading

0 comments on commit 9ab6009

Please sign in to comment.