Skip to content

Commit

Permalink
Create as installable module
Browse files Browse the repository at this point in the history
  • Loading branch information
pjb304 committed Aug 28, 2018
0 parents commit 7da2b16
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 0 deletions.
104 changes: 104 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include README.md
include LICENSE.txt
include dragino.ini.default

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Plantower Particulate Sensor Python interface
1 change: 1 addition & 0 deletions plantower/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .plantower import PlantowerReading, Plantower, PlantowerException
135 changes: 135 additions & 0 deletions plantower/plantower.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Wrapper classes for the Plantower PMS5003.
Philip Basford
12/02/2018
"""

import logging
from datetime import datetime, timedelta
from serial import Serial, SerialException

DEFAULT_SERIAL_PORT = "/dev/ttyUSB0" # Serial port to use if no other specified
DEFAULT_BAUD_RATE = 9600 # Serial baud rate to use if no other specified
DEFAULT_SERIAL_TIMEOUT = 2 # Serial timeout to use if not specified
DEFAULT_READ_TIMEOUT = 1 #How long to sit looking for the correct character sequence.

DEFAULT_LOGGING_LEVEL = logging.DEBUG

MSG_CHAR_1 = b'\x42' # First character to be recieved in a valid packet
MSG_CHAR_2 = b'\x4d' # Second character to be recieved in a valid packet

class PlantowerReading(object):
"""
Describes a single reading from the PMS5003 sensor
"""
def __init__(self, line):
"""
Takes a line from the Plantower serial port and converts it into
an object containing the data
"""
self.timestamp = datetime.utcnow()
self.pm10_cf1 = line[4] * 256 + line[5]
self.pm25_cf1 = line[6] * 256 + line[7]
self.pm100_cf1 = line[8] * 256 + line[9]
self.pm10_std = line[10] * 256 + line[11]
self.pm25_std = line[12] * 256 + line[13]
self.pm100_std = line[14] * 256 + line[15]
self.gr03um = line[16] * 256 + line[17]
self.gr05um = line[18] * 256 + line[19]
self.gr10um = line[20] * 256 + line[21]
self.gr25um = line[22] * 256 + line[23]
self.gr50um = line[24] * 256 + line[25]
self.gr100um = line[26] * 256 + line[27]

def __str__(self):
return (
"%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s," %
(self.pm10_cf1, self.pm10_std, self.pm25_cf1, self.pm25_std,
self.pm100_cf1, self.pm100_std, self.gr03um, self.gr05um,
self.gr10um, self.gr25um, self.gr50um, self.gr100um))

class PlantowerException(Exception):
"""
Exception to be thrown if any problems occur
"""
pass

class Plantower(object):
"""
Actual interface to the PMS5003 sensor
"""
def __init__(
self, port=DEFAULT_SERIAL_PORT, baud=DEFAULT_BAUD_RATE,
serial_timeout=DEFAULT_SERIAL_TIMEOUT,
read_timeout=DEFAULT_READ_TIMEOUT,
log_level=DEFAULT_LOGGING_LEVEL):
"""
Setup the interface for the sensor
"""
self.logger = logging.getLogger("PMS5003 Interface")
logging.basicConfig(
format='%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
self.logger.setLevel(log_level)
self.port = port
self.logger.info("Serial port: %s", self.port)
self.baud = baud
self.logger.info("Baud rate: %s", self.baud)
self.serial_timeout = serial_timeout
self.logger.info("Serial Timeout: %s", self.serial_timeout)
self.read_timeout = read_timeout
self.logger.info("Read Timeout: %s", self.read_timeout)
try:
self.serial = Serial(
port=self.port, baudrate=self.baud,
timeout=self.serial_timeout)
self.logger.debug("Port Opened Successfully")
except SerialException as exp:
self.logger.error(str(exp))
raise PlantowerException(str(exp))

def set_log_level(self, log_level):
"""
Enables the class logging level to be changed after it's created
"""
self.logger.setLevel(log_level)

def _verify(self, recv):
"""
Uses the last 2 bytes of the data packet from the Plantower sensor
to verify that the data recived is correct
"""
calc = 0
ord_arr = []
for c in bytearray(recv[:-2]): #Add all the bytes together except the checksum bytes
calc += c
ord_arr.append(c)
self.logger.debug(str(ord_arr))
sent = (recv[-2] << 8) | recv[-1] # Combine the 2 bytes together
if sent != calc:
self.logger.error("Checksum failure %d != %d", sent, calc)
raise PlantowerException("Checksum failure")

def read(self, perform_flush=True):
"""
Reads a line from the serial port and return
if perform_flush is set to true it will flush the serial buffer
before performing the read, otherwise, it'll just read the first
item in the buffer
"""
recv = b''
start = datetime.utcnow() #Start timer
if perform_flush:
self.serial.flush() #Flush any data in the buffer
while(
datetime.utcnow() <
(start + timedelta(seconds=self.read_timeout))):
inp = self.serial.read() # Read a character from the input
if inp == MSG_CHAR_1: # check it matches
recv += inp # if it does add it to recieve string
inp = self.serial.read() # read the next character
if inp == MSG_CHAR_2: # check it's what's expected
recv += inp # att it to the recieve string
recv += self.serial.read(30) # read the remaining 30 bytes
self._verify(recv) # verify the checksum
return PlantowerReading(recv) # convert to reading object
#If the character isn't what we are expecting loop until timeout
23 changes: 23 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import setuptools

with open("README.md", "r") as fh:
long_description = fh.read()

setuptools.setup(
name="plantower",
version="0.0.1",
author="Philip Basford",
author_email="[email protected]",
description="An interface for plantower particulate matter sensors",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/FEEprojects/plantower",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.3, <4',
install_requires=['pyserial']
)

0 comments on commit 7da2b16

Please sign in to comment.