Skip to content

Commit 2ab7b80

Browse files
committed
Run as a service
we add a watchdog to restart if things break. we add a unit file. modified readme to document this mode, as well as running with telegraf.
1 parent 92e1b9b commit 2ab7b80

File tree

6 files changed

+141
-30
lines changed

6 files changed

+141
-30
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,55 @@ $ pip install poetry
1414

1515
## Running
1616

17+
Use poetry to install dependencies:
18+
19+
```bash
20+
$ poetry install
21+
```
22+
1723
Invoke with poetry:
1824

1925
```bash
2026
$ poetry run ./main.py
2127
```
28+
29+
You can get help by passing `--help`.
30+
31+
### Options
32+
33+
By default, this program will scan a few possible `tty` ports to find a PMS7003.
34+
You may wish to pass the location explicitly, by using `--port <port>`.
35+
36+
Also, by default, the program will print quality measurements to the terminal.
37+
It will also print them, in [influxdb line protocol format](https://docs.influxdata.com/influxdb/v1.8/write_protocols/line_protocol_tutorial/), to `measurements.log`.
38+
You may wish to disable printing to the terminal with `--log-only`, and customize the location of the log file with `--log-path`.
39+
40+
## Running as a `systemd` service
41+
42+
On a recent Linux, you can run this as a service.
43+
Modify the included `mini-aqm.service` file, and edit the `WorkingDirectory` and `ExecStart` variables.
44+
`WorkingDirectory` should point at the location where you have this repo checked out.
45+
`ExecStart` (and `ExecStartPre`) should have the path to your `poetry` binary -- find it with `which poetry`.
46+
You may also wish to customize the arguments to `main.py`, for instance to set `--log-path`.
47+
48+
To install the service:
49+
50+
```bash
51+
cat mini-aqm.service | sudo tee /etc/systemd/system/mini-aqm.service
52+
sudo systemctl daemon-reload
53+
sudo systemctl start mini-aqm
54+
```
55+
56+
## With `telegraf`
57+
58+
You might want to pull the measurements into a time series database using [`telegraf`](https://github.com/influxdata/telegraf).
59+
You can do so by using the [`tail`](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/tail) plugin.
60+
The configuration looks like this:
61+
62+
```
63+
[[inputs.tail]]
64+
files = ["/home/igor47/repos/mini-aqm/measurements.log"]
65+
```
66+
67+
Customize the path where your `measurements.log` file is found.
68+
You may wish to pass an explicit path to `main.py` using `--log-path <path>`.

main.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from colorama import Fore, Style
55
import logging
66
import os
7-
import time
8-
from typing import Tuple
7+
import systemd_watchdog
8+
from typing import Tuple, Optional
99

1010
from influxdb_logger import InfluxdbLogger
1111
from pms7003 import PMS7003, PMSData
@@ -71,10 +71,9 @@ def print_pm(data: PMSData) -> None:
7171
@click.command()
7272
@click.option(
7373
"--port",
74-
default="/dev/ttyUSB0",
74+
default=None,
7575
help="Location of PMS7003 TTY device",
76-
required=True,
77-
show_default=True,
76+
show_default="scans possible ports for devices",
7877
)
7978
@click.option(
8079
"--debug/--no-debug",
@@ -94,36 +93,45 @@ def print_pm(data: PMSData) -> None:
9493
show_default=True,
9594
)
9695
def main(
97-
port: str, debug: bool, log_only: bool, log_path: str
96+
port: Optional[str], debug: bool, log_only: bool, log_path: str
9897
) -> None:
99-
if not os.access(port, mode=os.R_OK, follow_symlinks=True):
98+
devs = PMS7003.get_all(only=port)
99+
if not devs:
100100
click.echo(
101-
f"{Fore.RED}cannot access {port}; check path and permissions", err=True
101+
f"{Fore.RED}"
102+
f"cannot find PMS7003 on any checked port; check path and permissions"
103+
f"{Style.RESET_ALL}",
104+
err=True
102105
)
103106
return
104107

105108
logger = InfluxdbLogger(log_path)
106-
tags = {"type": "PMS7003", "id": port}
107109
click.echo(
108110
f"{Fore.BLUE}"
109111
f"writing influxdb measurement {logger.MEASUREMENT} to {logger.path}"
110112
f"{Style.RESET_ALL}"
111113
)
112114

113-
dev = PMS7003(port)
114-
click.echo(f"{Fore.GREEN}beginning to read data from {port}...{Style.RESET_ALL}")
115+
for dev in devs:
116+
click.echo(f"{Fore.GREEN}beginning to read data from {dev.id}...{Style.RESET_ALL}")
117+
118+
# systemd watchdog, in case this is running as a systemd service
119+
wd = systemd_watchdog.watchdog()
120+
wd.ready()
115121

116122
while True:
117-
data = dev.read()
118-
if debug:
119-
print_verbose(data)
120-
else:
121-
logger.emit(
122-
fields={k: v for k, v in data._asdict().items() if k.startswith("pm")},
123-
tags=tags,
124-
)
125-
if not log_only:
126-
print_pm(data)
123+
wd.ping()
124+
for dev in devs:
125+
data = dev.read()
126+
if debug:
127+
print_verbose(data)
128+
else:
129+
logger.emit(
130+
fields={k: v for k, v in data._asdict().items() if k.startswith("pm")},
131+
tags={"type": "PMS7003", "id": dev.id},
132+
)
133+
if not log_only:
134+
print_pm(data)
127135

128136
if __name__ == "__main__":
129137
main()

mini-aqm.service

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[Unit]
2+
Description=Monitors and collects data from PMS7003
3+
Documentation=https://github.com/igor47/mini-aqm https://igor.moomers.org/minimal-viable-air-quality
4+
Requires=local-fs.target
5+
After=local-fs.target
6+
7+
[Service]
8+
WorkingDirectory=/home/igor47/repos/mini-aqm
9+
ExecStartPre=/home/igor47/.asdf/installs/python/3.8.3/bin/poetry install
10+
ExecStart=/home/igor47/.asdf/installs/python/3.8.3/bin/poetry run ./main.py
11+
12+
Type=notify
13+
WatchdogSec=10
14+
RestartSec=10
15+
Restart=always
16+
17+
User=igor47
18+
Group=igor47
19+
20+
[Install]
21+
WantedBy=multi-user.target

pms7003.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
http://eleparts.co.kr/data/_gextends/good-pdf/201803/good-pdf-4208690-1.pdf
44
"""
55
import logging
6+
import os
67
import serial
78
import struct
89
import time
9-
from typing import Any, Dict, NamedTuple
10+
from typing import Any, Dict, NamedTuple, List, Optional
1011

1112

1213
class PMSData(NamedTuple):
@@ -44,20 +45,41 @@ class PMS7003(object):
4445
HEADER_LOW = int("0x4d", 16)
4546

4647
# UART / USB Serial : 'dmesg | grep ttyUSB'
47-
USB0 = "/dev/ttyUSB0"
48-
UART = "/dev/ttyAMA0"
49-
S0 = "/dev/serial0"
50-
51-
# USE PORT
52-
DEFAULT_PORT = S0
48+
POSSIBLE_PORTS = {
49+
'USB0': "/dev/ttyUSB0",
50+
'USB1': "/dev/ttyUSB1",
51+
'USB2': "/dev/ttyUSB2",
52+
'UART': "/dev/ttyAMA0",
53+
'S0': "/dev/serial0",
54+
}
5355

5456
# Baud Rate
5557
SERIAL_SPEED = 9600
5658

5759
# give up after trying to read for this long
5860
READ_TIMEOUT_SEC = 2
5961

60-
def __init__(self, port: str = DEFAULT_PORT):
62+
@classmethod
63+
def get_all(cls, only: Optional[str] = None) -> List['PMS7003']:
64+
"""checks several possible locations for PMS7003 devices
65+
66+
returns all valid locations
67+
"""
68+
devs = []
69+
70+
possible = [only] if only else cls.POSSIBLE_PORTS.values()
71+
for port in possible:
72+
if os.access(port, mode=os.R_OK, follow_symlinks=True):
73+
try:
74+
dev = PMS7003(port)
75+
if dev.read():
76+
devs.append(dev)
77+
except:
78+
pass
79+
80+
return devs
81+
82+
def __init__(self, port: str = POSSIBLE_PORTS["S0"]):
6183
self.port = port
6284
self.buffer: bytes = b""
6385
self.log = logging.getLogger(str(self))

poetry.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ python = "^3.8"
1010
pyserial = "^3.4"
1111
click = "^7.1.2"
1212
colorama = "^0.4.3"
13+
systemd-watchdog = "^0.9.0"
1314

1415
[tool.poetry.dev-dependencies]
1516
black = "^20.8b1"

0 commit comments

Comments
 (0)