Skip to main content

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 communication
  • multipledispatch - For tag-based dispatch

Development dependencies (optional):

  • pytest - Test framework
  • pytest-mock - Mocking for tests
  • pytest-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:

  1. Look at existing sensor implementations (GMP252 or AlphaTRACER)
  2. Copy the structure for your sensor
  3. Write simple tests (see test files for examples)
  4. Test with real hardware if possible
  5. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

atmospyre-0.1.0a1.tar.gz (48.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

atmospyre-0.1.0a1-py3-none-any.whl (56.9 kB view details)

Uploaded Python 3

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

Hashes for atmospyre-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 2c0b6a5713f53da6f7ebfc2fab35de5f176d572b82bbf84f12b39b0e771f82a8
MD5 e7da1091d019a3e9d3c205a3e110f402
BLAKE2b-256 375a8904eb5d0f260c475213b0f5798479e3adc096e37483228d2ba4d07870a7

See more details on using hashes here.

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

Hashes for atmospyre-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 5f68063a6c3eb2e2325c73a927965d7322949f3c5950db1f131f8461dda16597
MD5 0dc66248f349ae3d62d5660d0d9e8fa9
BLAKE2b-256 3db55593fc4c6218741c06c45327eedbbcdf7d75eb7802a9022b0605ed78a69d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page