Modbus sensor communication library
Project description
AtmosPyre
A simple, unified Python interface for Modbus-based environmental sensors. Write sensor drivers once, use them everywhere.
Why AtmosPyre?
Instead of buying expensive vendor software for each sensor, write your own sensor interface in ~100 lines of Python. AtmosPyre provides a clean, consistent API that makes adding new sensors straightforward.
Key Benefits
- 💰 Save money - No need for proprietary software licenses
- 🔧 Easy to extend - Add new sensors in minutes
- 🎯 Simple interface - One API for all sensors
- ✅ Tested - Comprehensive test coverage
- 📝 Well documented - Clear examples and patterns
Currently Supported Sensors
- Vaisala GMP252 - CO₂ and temperature sensor
- RadonTech AlphaTRACER - Radon concentration sensor
More sensors coming soon!
Installation
# Clone the repository
git clone <repository-url>
cd atmospyre
# Install the package
pip install .
# OR install in development mode (recommended for development)
pip install -e .
# Install with development dependencies (for testing)
pip install -e ".[dev]"
Requirements
- Python 3.8+
minimalmodbus- For Modbus RTU communicationmultipledispatch- For tag-based dispatch
Development dependencies (optional):
pytest- Test frameworkpytest-mock- Mocking for testspytest-cov- Test coverage reports
Quick Start
Reading from a CO₂ Sensor (GMP252)
from atmospyre.sensors.co2.vaisala.gmp252 import GMP252, CO2, TEMPERATURE
# Create sensor instance
sensor = GMP252(port='COM3', slave_address=1)
# Read CO₂ concentration
result = sensor.read(CO2)
print(f"CO₂: {result[CO2]} ppm")
# Read multiple values at once
result = sensor.read([CO2, TEMPERATURE])
print(f"CO₂: {result[CO2]} ppm")
print(f"Temperature: {result[TEMPERATURE]} °C")
Reading from a Radon Sensor (AlphaTRACER)
from atmospyre.sensors.radon.alphatracer import AlphaTRACER, RADON_LIVE, RADON_24H
# Create sensor instance
sensor = AlphaTRACER(port='COM4', slave_address=1)
# Read live radon concentration
result = sensor.read(RADON_LIVE)
print(f"Live radon: {result[RADON_LIVE]} Bq/m³")
# Read 24-hour average
result = sensor.read([RADON_LIVE, RADON_24H])
print(f"Live: {result[RADON_LIVE]} Bq/m³")
print(f"24h avg: {result[RADON_24H]} Bq/m³")
Custom Serial Settings
All sensors support custom serial port settings for challenging installations:
# For long cables or noisy environments
sensor = GMP252(
port='COM3',
slave_address=1,
baudrate=9600, # Lower baudrate for reliability
timeout=2.0 # Longer timeout
)
# For fast polling applications
sensor = GMP252(
port='COM3',
slave_address=1,
timeout=0.2 # Shorter timeout
)
Adding Your Own Sensor
Adding a new sensor is straightforward. Here's the complete process:
1. Create Your Sensor File
# atmospyre/sensors/your_category/your_sensor.py
from atmospyre.sensors.sensor import Sensor
from atmospyre.sensors.tags import ReadTag
from multipledispatch import dispatch
from minimalmodbus import Instrument, BYTEORDER_LITTLE_SWAP
# Define tags for your sensor's measurements
class HumidityTag(ReadTag):
"""Tag for humidity measurement."""
pass
class PressureTag(ReadTag):
"""Tag for pressure measurement."""
pass
# Create tag instances
HUMIDITY = HumidityTag()
PRESSURE = PressureTag()
# Create dispatch namespace
your_sensor_namespace = {}
# Define read functions for each tag
@dispatch(object, HumidityTag, namespace=your_sensor_namespace)
def _read(instrument: Instrument, tag: HumidityTag) -> float:
"""Read humidity from register 10."""
return instrument.read_float(10, byteorder=BYTEORDER_LITTLE_SWAP)
@dispatch(object, PressureTag, namespace=your_sensor_namespace)
def _read(instrument: Instrument, tag: PressureTag) -> float:
"""Read pressure from register 12."""
return instrument.read_float(12, byteorder=BYTEORDER_LITTLE_SWAP)
# Create your sensor class
class YourSensor(Sensor):
"""Your sensor description.
Available tags:
- HUMIDITY: Relative humidity (%)
- PRESSURE: Atmospheric pressure (hPa)
"""
def __init__(
self,
port: str,
slave_address: int = 1,
baudrate: int = 9600,
stopbits: int = 1,
bytesize: int = 8,
parity: str = 'N',
timeout: float = 0.5
):
"""Initialize sensor."""
super().__init__(
port=port,
valid_tags=[HUMIDITY, PRESSURE],
namespace=your_sensor_namespace,
slave_address=slave_address,
baudrate=baudrate,
stopbits=stopbits,
bytesize=bytesize,
parity=parity,
timeout=timeout
)
2. Write Tests (Optional but Recommended)
# tests/test_your_sensor.py
import pytest
from atmospyre.sensors.your_category.your_sensor import YourSensor, HUMIDITY, PRESSURE
class TestYourSensorConstructor:
"""Test constructor."""
def test_constructor_with_defaults(self, mock_instrument):
sensor = YourSensor(port='COM3', slave_address=1)
assert sensor.instrument.serial.baudrate == 9600
class TestYourSensorRead:
"""Test reading values."""
def test_read_humidity(self, mock_instrument):
mock_instrument.read_float.return_value = 45.5
sensor = YourSensor(port='COM3', slave_address=1)
result = sensor.read(HUMIDITY)
assert result[HUMIDITY] == 45.5
3. Use Your Sensor
from atmospyre.sensors.your_category.your_sensor import YourSensor, HUMIDITY, PRESSURE
sensor = YourSensor(port='COM3', slave_address=1)
result = sensor.read([HUMIDITY, PRESSURE])
print(f"Humidity: {result[HUMIDITY]} %")
print(f"Pressure: {result[PRESSURE]} hPa")
That's it! Your sensor works exactly like the built-in ones.
Finding Register Addresses
You'll need your sensor's Modbus register map from the manual. Look for:
- Register address - Where the value is stored (e.g., 0, 256, 2048)
- Data type - Float (32-bit), Integer (16-bit), or Long (32-bit)
- Byte order - Big-endian or little-endian (usually specified in manual)
Common Modbus Read Functions
# For 16-bit integers (most common)
value = instrument.read_register(256)
# For 32-bit floats
value = instrument.read_float(0, byteorder=BYTEORDER_LITTLE_SWAP)
# For 32-bit integers
value = instrument.read_long(20, byteorder=BYTEORDER_BIG)
Project Structure
atmospyre/
├── sensors/
│ ├── sensor.py # Base Sensor class
│ ├── tags.py # Tag base class
│ ├── co2/
│ │ └── vaisala/
│ │ └── gmp252.py # GMP252 implementation
│ └── radon/
│ └── alphatracer.py # AlphaTRACER implementation
└── tests/
├── conftest.py # Shared test fixtures
├── test_gmp252.py # GMP252 tests
└── test_alphatracer.py # AlphaTRACER tests
Testing
The project uses pytest for testing. All sensors have comprehensive test coverage.
Running Tests
# Run all tests
pytest
# Run tests for a specific sensor
pytest tests/test_gmp252.py
# Run with verbose output
pytest -v
# Run with coverage
pytest --cov=atmospyre
Test Fixtures (conftest.py)
Tests use a global mock_instrument fixture that automatically mocks Modbus communication:
# tests/conftest.py
import pytest
@pytest.fixture
def mock_instrument(mocker):
"""Mock minimalmodbus.Instrument globally."""
mock_class = mocker.patch('minimalmodbus.Instrument')
mock_instance = mocker.Mock()
mock_class.return_value = mock_instance
# Configure default properties
mock_instance.serial.baudrate = None
mock_instance.serial.stopbits = None
mock_instance.serial.bytesize = None
mock_instance.serial.parity = None
mock_instance.serial.timeout = None
mock_instance.mode = None
mock_instance.close_port_after_each_call = None
return mock_instance
This fixture is automatically available to all test files.
Roadmap
Planned Features
- More sensor drivers (humidity, pressure, particle counters)
- Modbus TCP support (for network-connected sensors)
- Modbus controller support (write operations)
- Data logging utilities
- Automatic sensor discovery
- Configuration file support
- Web dashboard for monitoring
Contributing
Want to add your own sensor? Great! Follow these steps:
- Look at existing sensor implementations (GMP252 or AlphaTRACER)
- Copy the structure for your sensor
- Write simple tests (see test files for examples)
- Test with real hardware if possible
- Submit a pull request
We welcome contributions, especially from those adding new sensors!
Support
- Documentation: Check the docstrings in the code
- Examples: See the
examples/directory (coming soon) - Issues: Report bugs or request features on GitHub
- Questions: Open a discussion on GitHub
License
[Add your license information here]
Acknowledgments
- Built with MinimalModbus for Modbus communication
- Inspired by the need for cost-effective sensor integration
- Thanks to all contributors!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file atmospyre-0.1.0a1.tar.gz.
File metadata
- Download URL: atmospyre-0.1.0a1.tar.gz
- Upload date:
- Size: 48.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c0b6a5713f53da6f7ebfc2fab35de5f176d572b82bbf84f12b39b0e771f82a8
|
|
| MD5 |
e7da1091d019a3e9d3c205a3e110f402
|
|
| BLAKE2b-256 |
375a8904eb5d0f260c475213b0f5798479e3adc096e37483228d2ba4d07870a7
|
File details
Details for the file atmospyre-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: atmospyre-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 56.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f68063a6c3eb2e2325c73a927965d7322949f3c5950db1f131f8461dda16597
|
|
| MD5 |
0dc66248f349ae3d62d5660d0d9e8fa9
|
|
| BLAKE2b-256 |
3db55593fc4c6218741c06c45327eedbbcdf7d75eb7802a9022b0605ed78a69d
|