Skip to content

Commit

Permalink
Use PyPMS reader instead of low-level serial cmds
Browse files Browse the repository at this point in the history
This is viable since [^1].

There are some minor differences to note:

- SensorReader._cmd uses Serial.flush(), but this shouldn't be an
issue in newer versions of Python [^2].

- SensorReader._cmd reads all available input into the buffer and
only calls Serial.reset after a "_read", or if the buffer turns out
to be invalid. This is less robust than just calling Serial.reset
before each command, but it seems to work.

- SensorReader.open does an additional check to verify the sensor.

[^1]: avaldebe/PyPMS#33
[^2]: python/cpython#97001
  • Loading branch information
benthorner committed Dec 9, 2022
1 parent b823612 commit 19a981b
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 52 deletions.
53 changes: 23 additions & 30 deletions src/snsary/contrib/pypms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"""
import dataclasses

from pms.core import Sensor
from serial import Serial
from pms.core.reader import SensorReader

from snsary.models import Reading
from snsary.sources import PollingSensor
Expand All @@ -19,6 +18,19 @@ class PyPMSSensor(PollingSensor):
``warm_up_seconds`` is necessary for some sensors e.g. for the PMSA003 the first two samples always `raise an InconsistentObservation exception <https://github.com/avaldebe/PyPMS/blob/04ff8edede7d780018cd00a7fcf78ffed43c0de4/src/pms/sensors/plantower/pmsx003.py#L63>`_.
"""

class SnsaryReader(SensorReader):
"""
PyPMS SensorReader customisations.
"""

def _pre_heat(self):
"""
Disable pre-heat to avoid blocking samples.
Sensors will raise an exception if they are not ready.
"""
pass

def __init__(
self,
*,
Expand All @@ -27,12 +39,13 @@ def __init__(
warm_up_seconds=10,
timeout=5,
):
self.__sensor = Sensor[sensor_name]
self.warm_up_seconds = warm_up_seconds

self.__serial = Serial(
port,
baudrate=self.__sensor.baud,
self.__reader = self.SnsaryReader(
sensor=sensor_name,
port=port,
samples=1,
max_retries=0,
timeout=timeout,
)

Expand All @@ -43,36 +56,17 @@ def __init__(

@property
def name(self):
return self.__sensor.name

def __cmd(self, command):
cmd = self.__sensor.command(command)
self.logger.debug(f"Sending {command} => {cmd}")
# clear any stray inbound data
self.__serial.reset_input_buffer()
# no need to flush() - this can
# cause the execution to hang
self.__serial.write(cmd.command)
buffer = self.__serial.read(cmd.answer_length)

self.logger.debug(f"Received {buffer}")
return buffer
return self.__reader.sensor.name

def start(self):
self.__cmd("wake")
buffer = self.__cmd("passive_mode")

if not self.__sensor.check(buffer, "passive_mode"):
raise RuntimeError("Serial port not connected.")

self.__reader.open()
PollingSensor.start(self)

def stop(self):
PollingSensor.stop(self)

try:
self.__cmd("sleep")
self.__serial.close()
self.__reader.close()
except Exception as e:
self.logger.exception(e)

Expand All @@ -81,8 +75,7 @@ def sample(self, timestamp, elapsed_seconds, **kwargs):
self.logger.info("Still warming up, no data yet.")
return []

buffer = self.__cmd("passive_read")
obs = self.__sensor.decode(buffer)
obs = next(self.__reader())

return [
Reading(
Expand Down
44 changes: 22 additions & 22 deletions tests/contrib/test_pypms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pms
import pytest
from pms.core import reader

from snsary.contrib.pypms import PyPMSSensor

Expand Down Expand Up @@ -71,6 +72,12 @@ def sensor(mock_sensor):
)


@pytest.fixture
def started_sensor(sensor, mock_start):
sensor.start()
return sensor


def test_name(sensor):
assert sensor.name == "PMSx003"

Expand Down Expand Up @@ -105,25 +112,25 @@ def test_start_bad_response(
send_bytes=send_bytes,
)

with pytest.raises(RuntimeError) as einfo:
with pytest.raises(reader.UnableToRead) as einfo:
sensor.start()

assert str(einfo.value) == "Serial port not connected."
assert str(einfo.value) == "Sensor failed validation"


def test_stop(
mock_sensor,
sensor,
started_sensor,
mock_stop,
):
sensor.stop()
started_sensor.stop()
assert mock_sensor.stubs["sleep"].called
mock_stop.assert_called_once()


def test_stop_bad_response(
mock_sensor,
sensor,
started_sensor,
mock_stop,
):
mock_sensor.stub(
Expand All @@ -132,26 +139,24 @@ def test_stop_bad_response(
send_bytes=b"123",
)

sensor.stop()
started_sensor.stop()
assert mock_sensor.stubs["sleep"].called
mock_stop.assert_called_once()


def test_stop_already_closed(
mock_sensor,
sensor,
caplog,
mock_stop,
):
sensor.stop()
sensor.stop()
assert "Attempting to use a port that is not open" in caplog.text


def test_sample(
sensor,
started_sensor,
):
readings = sensor.sample(timestamp="now", elapsed_seconds=0)
readings = started_sensor.sample(timestamp="now", elapsed_seconds=0)
assert len(readings) == 12

pm10_reading = next(r for r in readings if r.name == "pm10")
Expand All @@ -160,23 +165,22 @@ def test_sample(


def test_sample_warm_up(
sensor,
started_sensor,
):
sensor.warm_up_seconds = 5
readings = sensor.sample(timestamp="now", elapsed_seconds=0)
started_sensor.warm_up_seconds = 5
readings = started_sensor.sample(timestamp="now", elapsed_seconds=0)
assert len(readings) == 0

readings = sensor.sample(timestamp="now", elapsed_seconds=5)
readings = started_sensor.sample(timestamp="now", elapsed_seconds=5)
assert len(readings) == 12

pm10_reading = next(r for r in readings if r.name == "pm10")
assert pm10_reading.value == 11822
assert pm10_reading.timestamp == "now"


def test_sample_bad_response(
mock_sensor,
sensor,
started_sensor,
):
mock_sensor.stub(
name="passive_read",
Expand All @@ -185,16 +189,12 @@ def test_sample_bad_response(
)

with pytest.raises(pms.WrongMessageFormat):
sensor.sample(timestamp="now", elapsed_seconds=0)
started_sensor.sample(timestamp="now", elapsed_seconds=0)


def test_sample_already_closed(
sensor,
mock_stop,
):
sensor.stop()

with pytest.raises(Exception) as einfo:
with pytest.raises(StopIteration):
sensor.sample(timestamp="now", elapsed_seconds=0)

assert str(einfo.value) == "Attempting to use a port that is not open"

0 comments on commit 19a981b

Please sign in to comment.