Skip to content

Commit 7da2b16

Browse files
committed
Create as installable module
0 parents  commit 7da2b16

File tree

6 files changed

+268
-0
lines changed

6 files changed

+268
-0
lines changed

.gitignore

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
MANIFEST
27+
28+
# PyInstaller
29+
# Usually these files are written by a python script from a template
30+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
31+
*.manifest
32+
*.spec
33+
34+
# Installer logs
35+
pip-log.txt
36+
pip-delete-this-directory.txt
37+
38+
# Unit test / coverage reports
39+
htmlcov/
40+
.tox/
41+
.coverage
42+
.coverage.*
43+
.cache
44+
nosetests.xml
45+
coverage.xml
46+
*.cover
47+
.hypothesis/
48+
.pytest_cache/
49+
50+
# Translations
51+
*.mo
52+
*.pot
53+
54+
# Django stuff:
55+
*.log
56+
local_settings.py
57+
db.sqlite3
58+
59+
# Flask stuff:
60+
instance/
61+
.webassets-cache
62+
63+
# Scrapy stuff:
64+
.scrapy
65+
66+
# Sphinx documentation
67+
docs/_build/
68+
69+
# PyBuilder
70+
target/
71+
72+
# Jupyter Notebook
73+
.ipynb_checkpoints
74+
75+
# pyenv
76+
.python-version
77+
78+
# celery beat schedule file
79+
celerybeat-schedule
80+
81+
# SageMath parsed files
82+
*.sage.py
83+
84+
# Environments
85+
.env
86+
.venv
87+
env/
88+
venv/
89+
ENV/
90+
env.bak/
91+
venv.bak/
92+
93+
# Spyder project settings
94+
.spyderproject
95+
.spyproject
96+
97+
# Rope project settings
98+
.ropeproject
99+
100+
# mkdocs documentation
101+
/site
102+
103+
# mypy
104+
.mypy_cache/

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include README.md
2+
include LICENSE.txt
3+
include dragino.ini.default
4+

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Plantower Particulate Sensor Python interface

plantower/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .plantower import PlantowerReading, Plantower, PlantowerException

plantower/plantower.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Wrapper classes for the Plantower PMS5003.
3+
Philip Basford
4+
12/02/2018
5+
"""
6+
7+
import logging
8+
from datetime import datetime, timedelta
9+
from serial import Serial, SerialException
10+
11+
DEFAULT_SERIAL_PORT = "/dev/ttyUSB0" # Serial port to use if no other specified
12+
DEFAULT_BAUD_RATE = 9600 # Serial baud rate to use if no other specified
13+
DEFAULT_SERIAL_TIMEOUT = 2 # Serial timeout to use if not specified
14+
DEFAULT_READ_TIMEOUT = 1 #How long to sit looking for the correct character sequence.
15+
16+
DEFAULT_LOGGING_LEVEL = logging.DEBUG
17+
18+
MSG_CHAR_1 = b'\x42' # First character to be recieved in a valid packet
19+
MSG_CHAR_2 = b'\x4d' # Second character to be recieved in a valid packet
20+
21+
class PlantowerReading(object):
22+
"""
23+
Describes a single reading from the PMS5003 sensor
24+
"""
25+
def __init__(self, line):
26+
"""
27+
Takes a line from the Plantower serial port and converts it into
28+
an object containing the data
29+
"""
30+
self.timestamp = datetime.utcnow()
31+
self.pm10_cf1 = line[4] * 256 + line[5]
32+
self.pm25_cf1 = line[6] * 256 + line[7]
33+
self.pm100_cf1 = line[8] * 256 + line[9]
34+
self.pm10_std = line[10] * 256 + line[11]
35+
self.pm25_std = line[12] * 256 + line[13]
36+
self.pm100_std = line[14] * 256 + line[15]
37+
self.gr03um = line[16] * 256 + line[17]
38+
self.gr05um = line[18] * 256 + line[19]
39+
self.gr10um = line[20] * 256 + line[21]
40+
self.gr25um = line[22] * 256 + line[23]
41+
self.gr50um = line[24] * 256 + line[25]
42+
self.gr100um = line[26] * 256 + line[27]
43+
44+
def __str__(self):
45+
return (
46+
"%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s," %
47+
(self.pm10_cf1, self.pm10_std, self.pm25_cf1, self.pm25_std,
48+
self.pm100_cf1, self.pm100_std, self.gr03um, self.gr05um,
49+
self.gr10um, self.gr25um, self.gr50um, self.gr100um))
50+
51+
class PlantowerException(Exception):
52+
"""
53+
Exception to be thrown if any problems occur
54+
"""
55+
pass
56+
57+
class Plantower(object):
58+
"""
59+
Actual interface to the PMS5003 sensor
60+
"""
61+
def __init__(
62+
self, port=DEFAULT_SERIAL_PORT, baud=DEFAULT_BAUD_RATE,
63+
serial_timeout=DEFAULT_SERIAL_TIMEOUT,
64+
read_timeout=DEFAULT_READ_TIMEOUT,
65+
log_level=DEFAULT_LOGGING_LEVEL):
66+
"""
67+
Setup the interface for the sensor
68+
"""
69+
self.logger = logging.getLogger("PMS5003 Interface")
70+
logging.basicConfig(
71+
format='%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
72+
self.logger.setLevel(log_level)
73+
self.port = port
74+
self.logger.info("Serial port: %s", self.port)
75+
self.baud = baud
76+
self.logger.info("Baud rate: %s", self.baud)
77+
self.serial_timeout = serial_timeout
78+
self.logger.info("Serial Timeout: %s", self.serial_timeout)
79+
self.read_timeout = read_timeout
80+
self.logger.info("Read Timeout: %s", self.read_timeout)
81+
try:
82+
self.serial = Serial(
83+
port=self.port, baudrate=self.baud,
84+
timeout=self.serial_timeout)
85+
self.logger.debug("Port Opened Successfully")
86+
except SerialException as exp:
87+
self.logger.error(str(exp))
88+
raise PlantowerException(str(exp))
89+
90+
def set_log_level(self, log_level):
91+
"""
92+
Enables the class logging level to be changed after it's created
93+
"""
94+
self.logger.setLevel(log_level)
95+
96+
def _verify(self, recv):
97+
"""
98+
Uses the last 2 bytes of the data packet from the Plantower sensor
99+
to verify that the data recived is correct
100+
"""
101+
calc = 0
102+
ord_arr = []
103+
for c in bytearray(recv[:-2]): #Add all the bytes together except the checksum bytes
104+
calc += c
105+
ord_arr.append(c)
106+
self.logger.debug(str(ord_arr))
107+
sent = (recv[-2] << 8) | recv[-1] # Combine the 2 bytes together
108+
if sent != calc:
109+
self.logger.error("Checksum failure %d != %d", sent, calc)
110+
raise PlantowerException("Checksum failure")
111+
112+
def read(self, perform_flush=True):
113+
"""
114+
Reads a line from the serial port and return
115+
if perform_flush is set to true it will flush the serial buffer
116+
before performing the read, otherwise, it'll just read the first
117+
item in the buffer
118+
"""
119+
recv = b''
120+
start = datetime.utcnow() #Start timer
121+
if perform_flush:
122+
self.serial.flush() #Flush any data in the buffer
123+
while(
124+
datetime.utcnow() <
125+
(start + timedelta(seconds=self.read_timeout))):
126+
inp = self.serial.read() # Read a character from the input
127+
if inp == MSG_CHAR_1: # check it matches
128+
recv += inp # if it does add it to recieve string
129+
inp = self.serial.read() # read the next character
130+
if inp == MSG_CHAR_2: # check it's what's expected
131+
recv += inp # att it to the recieve string
132+
recv += self.serial.read(30) # read the remaining 30 bytes
133+
self._verify(recv) # verify the checksum
134+
return PlantowerReading(recv) # convert to reading object
135+
#If the character isn't what we are expecting loop until timeout

setup.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="plantower",
8+
version="0.0.1",
9+
author="Philip Basford",
10+
author_email="[email protected]",
11+
description="An interface for plantower particulate matter sensors",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
url="https://github.com/FEEprojects/plantower",
15+
packages=setuptools.find_packages(),
16+
classifiers=[
17+
"Programming Language :: Python :: 3",
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: OS Independent",
20+
],
21+
python_requires='>=3.3, <4',
22+
install_requires=['pyserial']
23+
)

0 commit comments

Comments
 (0)