First commit

This commit is contained in:
Duncan Tourolle 2025-05-24 12:23:08 +01:00
parent 671953c98c
commit 513e06487b
10 changed files with 626 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
build
bdist*
lib
dist
*.egg-info
*/__pycache__

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include LICENSE
include README.md
include pyproject.toml
recursive-include examples *.py

View File

@ -1,2 +1,49 @@
# PyBMA400 # PyBMA400
A Python library for the Bosch BMA400 accelerometer, providing easy access to accelerometer data and configuration.
## Installation
```bash
pip install pybma400
```
## Features
- Read acceleration data
- Read temperature data in Celsius
- Configure power modes, sampling rates, and ranges
- Low-level register access for advanced users
- Simple orientation detection functionality
## Requirements
- Python 3.6 or later
- Raspberry Pi or other Linux system with I2C enabled
- SMBus library (automatically installed as a dependency)
## Usage Example
```python
from pybma400 import BMA400
# Initialize the sensor
sensor = BMA400() # Default I2C bus=1, address=0x14
# Read acceleration data
x, y, z = sensor.acceleration
print(f"Acceleration: X={x:.2f}, Y={y:.2f}, Z={z:.2f} m/s²")
# Read temperature
temp = sensor.temperature
print(f"Temperature: {temp:.1f}°C")
# Configure the sensor
sensor.power_mode = BMA400.NORMAL_MODE
sensor.output_data_rate = BMA400.ACCEL_100HZ
sensor.acc_range = BMA400.ACC_RANGE_4 # ±4g range
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

42
examples/basic_usage.py Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
Basic example of using the PyBMA400 library to read
acceleration and temperature data from the BMA400 sensor.
"""
import time
from pybma400 import BMA400
def main():
# Initialize the sensor
try:
sensor = BMA400() # Default I2C bus=1, address=0x14
print("BMA400 sensor initialized successfully!")
# Configure the sensor
sensor.power_mode = BMA400.NORMAL_MODE
sensor.output_data_rate = BMA400.ACCEL_100HZ
sensor.acc_range = BMA400.ACC_RANGE_4 # ±4g range
print(f"Power mode: {sensor.power_mode}")
print(f"Output data rate: {sensor.output_data_rate}")
print(f"Acceleration range: {sensor.acc_range}")
print(f"Filter bandwidth: {sensor.filter_bandwidth}")
print(f"Oversampling rate: {sensor.oversampling_rate}")
# Read and display sensor data
for i in range(10):
# Read acceleration
x, y, z = sensor.acceleration
print(f"Acceleration: X={x:.2f}, Y={y:.2f}, Z={z:.2f} m/s²")
# Read temperature
temp = sensor.temperature
print(f"Temperature: {temp:.1f}°C")
time.sleep(0.5)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Example of using PyBMA400 to detect orientation changes.
This shows how to use the watcher module to detect flips between
landscape and portrait orientations.
"""
import asyncio
from pybma400 import BMA400, detect_orientation_flip, is_landscape
async def main():
try:
# Initialize the sensor
sensor = BMA400() # Default I2C bus=1, address=0x14
print("BMA400 sensor initialized successfully!")
# Start with the current orientation state
current_state = is_landscape(sensor.acceleration)
print(f"Initial orientation: {'Landscape' if current_state else 'Portrait'}")
# Monitor for orientation changes
while True:
print("Monitoring for orientation changes...")
# Call the detect_orientation_flip function with our sensor
# We'll use is_landscape as our evaluation function
new_state = await detect_orientation_flip(
current_state=current_state,
eval_func=is_landscape,
flip_delay=0.5, # Shorter delay for demonstration purposes
sensor=sensor
)
# Update our state
if new_state != current_state:
current_state = new_state
print(f"Orientation changed to: {'Landscape' if current_state else 'Portrait'}")
except KeyboardInterrupt:
print("\nMonitoring stopped by user")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
asyncio.run(main())

11
pyBMA400/__init__.py Normal file
View File

@ -0,0 +1,11 @@
"""
PyBMA400 - Python driver for the Bosch BMA400 accelerometer.
This package provides a Python interface for the Bosch BMA400 accelerometer,
allowing reading of acceleration data and configuration of sensor parameters.
"""
from .driver import BMA400
from .watcher import detect_orientation_flip, is_landscape, is_inverted
__version__ = "0.1.1"

363
pyBMA400/driver.py Normal file
View File

@ -0,0 +1,363 @@
# bma400.py - BMA400 Accelerometer Library for Python
import time
import struct
from typing import Tuple, Optional, Union, List
try:
import smbus
except ImportError:
pass # Allow for non-Raspberry Pi environments
class BMA400:
"""
Python driver for the Bosch BMA400 Accelerometer.
This library provides access to the BMA400 accelerometer over I2C,
allowing reading of acceleration data and configuration of sensor parameters.
:param i2c_bus: The I2C bus number (usually 1 on Raspberry Pi 3+)
:param address: The I2C device address, defaults to 0x14
"""
# Register addresses
_REG_WHOAMI = 0x00
_ACC_CONFIG0 = 0x19
_ACC_CONFIG1 = 0x1A
_ACC_CONFIG2 = 0x1B
_ACCEL_DATA = 0x04
_TEMP_DATA = 0x11
# Constants for conversion
_ACC_CONVERSION = 9.80665 # Convert to m/s²
# Power mode values
SLEEP_MODE = 0x00
LOW_POWER_MODE = 0x01
NORMAL_MODE = 0x02
SWITCH_TO_SLEEP = 0x03
# Output data rate values
ACCEL_12_5HZ = 0x05
ACCEL_25HZ = 0x06
ACCEL_50HZ = 0x07
ACCEL_100HZ = 0x08
ACCEL_200HZ = 0x09
ACCEL_400HZ = 0xA4
ACCEL_800HZ = 0xB8
# Filter bandwidth values
ACC_FILT_BW0 = 0x00 # 0.48 x ODR
ACC_FILT_BW1 = 0x01 # 0.24 x ODR
# Oversampling values
OVERSAMPLING_0 = 0x00
OVERSAMPLING_1 = 0x01
OVERSAMPLING_2 = 0x02
OVERSAMPLING_3 = 0x03
# Acceleration range values
ACC_RANGE_2 = 0x00 # ±2g
ACC_RANGE_4 = 0x01 # ±4g
ACC_RANGE_8 = 0x02 # ±8g
ACC_RANGE_16 = 0x03 # ±16g
# Range factors for converting raw values
_acc_range_factor = {
ACC_RANGE_2: 1024,
ACC_RANGE_4: 512,
ACC_RANGE_8: 256,
ACC_RANGE_16: 128
}
# Data source register values
ACC_FILT1 = 0x00
ACC_FILT2 = 0x01
ACC_FILT_LP = 0x02
def __init__(self, i2c_bus: int = 1, address: int = 0x14):
"""
Initialize the BMA400 sensor.
:param i2c_bus: The I2C bus number
:param address: The I2C device address
:raises RuntimeError: If the sensor is not found
"""
self._bus = smbus.SMBus(i2c_bus)
self._address = address
# Check device ID
device_id = self._read_byte(BMA400._REG_WHOAMI)
if device_id != 0x90:
raise RuntimeError(f"Failed to find BMA400, got device ID: 0x{device_id:02X}")
# Initialize with default settings
self._power_mode = self.NORMAL_MODE
self._acc_range = self.ACC_RANGE_2
self._output_data_rate = self.ACCEL_100HZ
self._oversampling_rate = self.OVERSAMPLING_3
self._filter_bandwidth = self.ACC_FILT_BW0
self._source_data_registers = self.ACC_FILT1
# Apply default settings
self._write_register_bits(self._ACC_CONFIG0, 0, 2, self._power_mode)
self._write_register_bits(self._ACC_CONFIG0, 7, 1, self._filter_bandwidth)
self._write_register_bits(self._ACC_CONFIG1, 0, 4, self._output_data_rate)
self._write_register_bits(self._ACC_CONFIG1, 4, 2, self._oversampling_rate)
self._write_register_bits(self._ACC_CONFIG1, 6, 2, self._acc_range)
self._write_register_bits(self._ACC_CONFIG2, 2, 2, self._source_data_registers)
def _read_byte(self, register: int) -> int:
"""Read a single byte from the specified register"""
return self._bus.read_byte_data(self._address, register)
def _read_bytes(self, register: int, length: int) -> List[int]:
"""Read multiple bytes from the specified register"""
return self._bus.read_i2c_block_data(self._address, register, length)
def _write_byte(self, register: int, value: int) -> None:
"""Write a single byte to the specified register"""
self._bus.write_byte_data(self._address, register, value)
def _write_register_bits(self, register: int, pos: int, length: int, value: int) -> None:
"""
Write specific bits in a register
:param register: Register address
:param pos: Position of the LSB (0-indexed)
:param length: Number of bits to modify
:param value: Value to write
"""
current = self._read_byte(register)
mask = (1 << length) - 1
current &= ~(mask << pos) # Clear the bits
current |= (value & mask) << pos # Set the bits
self._write_byte(register, current)
def _read_register_bits(self, register: int, pos: int, length: int) -> int:
"""
Read specific bits from a register
:param register: Register address
:param pos: Position of the LSB (0-indexed)
:param length: Number of bits to read
:return: Value of the specified bits
"""
value = self._read_byte(register)
mask = (1 << length) - 1
return (value >> pos) & mask
@property
def power_mode(self) -> str:
"""
Get the current power mode.
:return: String describing the current power mode
"""
mode = self._read_register_bits(self._ACC_CONFIG0, 0, 2)
modes = ["SLEEP_MODE", "LOW_POWER_MODE", "NORMAL_MODE", "SWITCH_TO_SLEEP"]
return modes[mode]
@power_mode.setter
def power_mode(self, value: int) -> None:
"""
Set the power mode.
:param value: Power mode value (use BMA400.XXX_MODE constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.SLEEP_MODE, self.LOW_POWER_MODE, self.NORMAL_MODE, self.SWITCH_TO_SLEEP]:
raise ValueError("Value must be a valid power mode setting")
self._write_register_bits(self._ACC_CONFIG0, 0, 2, value)
self._power_mode = value
@property
def output_data_rate(self) -> str:
"""
Get the current output data rate.
:return: String describing the current output data rate
"""
odr = self._read_register_bits(self._ACC_CONFIG1, 0, 4)
rates = {
0x05: "ACCEL_12_5HZ",
0x06: "ACCEL_25HZ",
0x07: "ACCEL_50HZ",
0x08: "ACCEL_100HZ",
0x09: "ACCEL_200HZ",
0xA4: "ACCEL_400HZ",
0xB8: "ACCEL_800HZ"
}
return rates.get(odr, f"UNKNOWN (0x{odr:02X})")
@output_data_rate.setter
def output_data_rate(self, value: int) -> None:
"""
Set the output data rate.
:param value: Output data rate value (use BMA400.ACCEL_XXX constants)
:raises ValueError: If the value is invalid
"""
valid_values = [self.ACCEL_12_5HZ, self.ACCEL_25HZ, self.ACCEL_50HZ,
self.ACCEL_100HZ, self.ACCEL_200HZ, self.ACCEL_400HZ,
self.ACCEL_800HZ]
if value not in valid_values:
raise ValueError("Value must be a valid output data rate setting")
self._write_register_bits(self._ACC_CONFIG1, 0, 4, value)
self._output_data_rate = value
@property
def oversampling_rate(self) -> str:
"""
Get the current oversampling rate.
:return: String describing the current oversampling rate
"""
osr = self._read_register_bits(self._ACC_CONFIG1, 4, 2)
rates = ["OVERSAMPLING_0", "OVERSAMPLING_1", "OVERSAMPLING_2", "OVERSAMPLING_3"]
return rates[osr]
@oversampling_rate.setter
def oversampling_rate(self, value: int) -> None:
"""
Set the oversampling rate.
:param value: Oversampling rate value (use BMA400.OVERSAMPLING_X constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.OVERSAMPLING_0, self.OVERSAMPLING_1,
self.OVERSAMPLING_2, self.OVERSAMPLING_3]:
raise ValueError("Value must be a valid oversampling rate setting")
self._write_register_bits(self._ACC_CONFIG1, 4, 2, value)
self._oversampling_rate = value
@property
def acc_range(self) -> str:
"""
Get the current acceleration range.
:return: String describing the current acceleration range
"""
range_val = self._read_register_bits(self._ACC_CONFIG1, 6, 2)
ranges = ["ACC_RANGE_2", "ACC_RANGE_4", "ACC_RANGE_8", "ACC_RANGE_16"]
return ranges[range_val]
@acc_range.setter
def acc_range(self, value: int) -> None:
"""
Set the acceleration range.
:param value: Acceleration range value (use BMA400.ACC_RANGE_X constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.ACC_RANGE_2, self.ACC_RANGE_4,
self.ACC_RANGE_8, self.ACC_RANGE_16]:
raise ValueError("Value must be a valid acceleration range setting")
self._write_register_bits(self._ACC_CONFIG1, 6, 2, value)
self._acc_range = value
@property
def filter_bandwidth(self) -> str:
"""
Get the current filter bandwidth.
:return: String describing the current filter bandwidth
"""
bw = self._read_register_bits(self._ACC_CONFIG0, 7, 1)
bandwidths = ["ACC_FILT_BW0", "ACC_FILT_BW1"]
return bandwidths[bw]
@filter_bandwidth.setter
def filter_bandwidth(self, value: int) -> None:
"""
Set the filter bandwidth.
:param value: Filter bandwidth value (use BMA400.ACC_FILT_BWx constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.ACC_FILT_BW0, self.ACC_FILT_BW1]:
raise ValueError("Value must be a valid filter bandwidth setting")
self._write_register_bits(self._ACC_CONFIG0, 7, 1, value)
self._filter_bandwidth = value
@property
def source_data_registers(self) -> str:
"""
Get the current source data register setting.
:return: String describing the current source data register
"""
src = self._read_register_bits(self._ACC_CONFIG2, 2, 2)
sources = ["ACC_FILT1", "ACC_FILT2", "ACC_FILT_LP"]
return sources[src] if src < len(sources) else f"UNKNOWN (0x{src:02X})"
@source_data_registers.setter
def source_data_registers(self, value: int) -> None:
"""
Set the source data register.
:param value: Source data register value (use BMA400.ACC_FILT_X constants)
:raises ValueError: If the value is invalid
"""
if value not in [self.ACC_FILT1, self.ACC_FILT2, self.ACC_FILT_LP]:
raise ValueError("Value must be a valid source data register setting")
self._write_register_bits(self._ACC_CONFIG2, 2, 2, value)
self._source_data_registers = value
@property
def acceleration(self) -> Tuple[float, float, float]:
"""
Get the current acceleration measurements in m/.
:return: Tuple of (x, y, z) acceleration values
"""
# Read 6 bytes (2 bytes per axis, x, y, z)
data = self._read_bytes(self._ACCEL_DATA, 6)
# Convert data to 16-bit signed integers
rawx = (data[1] << 8) | data[0]
rawy = (data[3] << 8) | data[2]
rawz = (data[5] << 8) | data[4]
# Apply two's complement if needed
if rawx > 2047:
rawx -= 4096
if rawy > 2047:
rawy -= 4096
if rawz > 2047:
rawz -= 4096
# Convert to m/s² based on range setting
factor = self._acc_range_factor[self._acc_range] * self._ACC_CONVERSION
return (rawx / factor, rawy / factor, rawz / factor)
@property
def temperature(self) -> float:
"""
Get the current temperature in Celsius.
The temperature sensor is calibrated with a precision of ±5°C.
:return: Temperature in Celsius
"""
raw_temp = self._read_byte(self._TEMP_DATA)
# Apply two's complement for 8-bit value
if raw_temp > 127:
raw_temp -= 256
# Convert to Celsius according to datasheet
return (raw_temp * 0.5) + 23.0
def soft_reset(self) -> None:
"""
Perform a soft reset of the sensor.
This resets all registers to their default values.
"""
# Command register and soft reset command
CMD_REG = 0x7E
SOFT_RESET_CMD = 0xB6
self._write_byte(CMD_REG, SOFT_RESET_CMD)
time.sleep(0.2) # Wait for reset to complete

54
pyBMA400/watcher.py Normal file
View File

@ -0,0 +1,54 @@
import asyncio
from .driver import BMA400
def is_landscape(accel):
x, y, z = accel
# Return True if device is in landscape orientation
return abs(x) > abs(y)
def is_inverted(accel):
x, y, z = accel
# Return True if device is in landscape orientation
return 0 > abs(y)
async def detect_orientation_flip(current_state: bool, eval_func, flip_delay: float = 1.0, sensor = None) -> bool:
"""
Asynchronously detect orientation flip using the BMA400 accelerometer.
Args:
current_state (bool): The current orientation state.
eval_func (Callable[[tuple], bool]): A function that evaluates the acceleration (x, y, z)
and returns True if flipped, False otherwise.
flip_delay (float): Delay in seconds after detecting a flip before confirming the state change.
Returns:
bool: The updated orientation state.
"""
try:
# Initialize sensor if not provided
if sensor is None:
sensor = BMA400()
sensor.power_mode = BMA400.LOW_POWER_MODE
sensor.output_data_rate = BMA400.ACCEL_12_5HZ
sensor.acc_range = BMA400.ACC_RANGE_2
sensor.oversampling_rate = BMA400.OVERSAMPLING_0
while True:
accel = sensor.acceleration
flipped = eval_func(accel)
if flipped != current_state:
# Possible orientation change detected, wait for confirmation
await asyncio.sleep(flip_delay)
# Check again to confirm
accel = sensor.acceleration
if eval_func(accel) == flipped:
return flipped
await asyncio.sleep(0.1) # Polling interval
except Exception as e:
print(f"Error: {e}")
return current_state

29
pyproject.toml Normal file
View File

@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pybma400"
description = "Python driver for the Bosch BMA400 accelerometer"
readme = "README.md"
requires-python = ">=3.6"
license = "MIT"
authors = [
{name = "Duncan Tourolle", email = "duncan@tourolle.paris"}
]
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"Topic :: System :: Hardware",
]
dependencies = [
"smbus-cffi; platform_system!='Windows'",
]
dynamic = ["version"]
[tool.setuptools]
packages = ["pybma400"]
package-dir = {"pybma400" = "pyBMA400"}
[tool.setuptools.dynamic]
version = {attr = "pyBMA400.__version__"}

26
setup.py Normal file
View File

@ -0,0 +1,26 @@
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="pybma400",
version="0.1.1",
author="Duncan Tourolle",
description="Python driver for the Bosch BMA400 accelerometer",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://gitea.tourolle.paris/dtourolle/PyBMA400",
packages=["pybma400"],
package_dir={"pybma400": "pyBMA400"},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: System :: Hardware",
],
python_requires=">=3.6",
install_requires=[
"smbus-cffi; platform_system!='Windows'",
],
)