commit 2e988a0df73217eb65e87b2eca90a258483205c8 Author: Kyle Isom Date: Sat Feb 26 18:31:17 2022 -0800 Initial import. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..1d4b1da --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ +Install https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py first. \ No newline at end of file diff --git a/chime/__init__.py b/chime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chime/database.py b/chime/database.py new file mode 100644 index 0000000..b152922 --- /dev/null +++ b/chime/database.py @@ -0,0 +1,47 @@ +import psycopg2 + + +class Database: + def __init__(self, creds): + keys = ['host', 'port', 'name', 'user', 'pass'] + for key in keys: + if not key in creds: + raise ValueError(f'missing cred key={key}') + self.creds = creds + self.conn = None + self.queued = [] + + def _connstr_(self): + connstr = f"host={self.creds['host']} port={self.creds['port']} " + connstr += f"dbname={self.creds['name']} user={self.creds['user']} " + connstr += f"password={self.creds['pass']}" + return connstr + + def close(self): + if self.conn: + self.conn.close() + self.conn = None + + def connect(self): + if self.conn: + self.close() + self.conn = psycopg2.connect(self._connstr_()) + + def store(self, reading): + try: + if not self.conn: + self.connect() + cur = self.conn.cursor() + + cur.execute("""INSERT INTO readings + (created, temp, press, humid) + VALUES (%s, %s, %s, %s) + """, (reading.time.timestamp(), reading.temp, reading.press, reading.hum)) + self.conn.commit() + cur.close() + + if len(self.queued) > 0: + self.store(self.queued.pop()) + except: + print('db error') + self.queued.append(reading) diff --git a/chime/main.py b/chime/main.py new file mode 100644 index 0000000..249d957 --- /dev/null +++ b/chime/main.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import datetime +import pickle +import sys +from chime.database import Database +from chime.pubsub import Publisher +from chime.sensor import MS8607 +from time import sleep + + +def show_temperature(reading): + print(reading) + + +# !/usr/bin/env python3 + +import datetime +import pickle +import sys +from chime.database import Database +from chime.pubsub import Publisher +from chime.sensor import MS8607 +from time import sleep + + +def show_temperature(reading): + print(reading) + + +def write_reading(logfile, reading): + logfile.write(str(reading) + '\n') + logfile.flush() + + +def main(credspath='/etc/chime/creds.dat', logpath='/var/lib/pht.csv'): + with open(credspath, 'rb') as credsfile: + creds = pickle.loads(credsfile.read()) + db = Database(creds) + sensor = MS8607() + publisher = Publisher('tcp://*:4000') + + logfile = open('/var/lib/pht.csv', 'at') + + while True: + reading = sensor.reading() + show_temperature(reading) + write_reading(logfile, reading) + publisher.publish(reading.json()) + db.store(reading) + sleep(120) + + +if __name__ == '__main__': + main() + + +def write_reading(logfile, reading): + logfile.write(str(reading) + '\n') + logfile.flush() + + +def main(credspath='/etc/chime/creds.dat', logpath='/var/lib/pht.csv'): + with open(credspath, 'rb') as credsfile: + creds = pickle.loads(credsfile.read()) + db = Database(creds) + sensor = MS8607() + publisher = Publisher('tcp://*:4000') + + logfile = open('/var/lib/pht.csv', 'at') + + while True: + reading = sensor.reading() + show_temperature(reading) + write_reading(logfile, reading) + publisher.publish(reading.json()) + db.store(reading) + sleep(120) + + +if __name__ == '__main__': + main() diff --git a/chime/pubsub.py b/chime/pubsub.py new file mode 100644 index 0000000..1d63949 --- /dev/null +++ b/chime/pubsub.py @@ -0,0 +1,55 @@ +import time +import zmq +import telemetry_pb2 as proto + + +class Subscriber: + def __init__(self, addr, topics=None, conflate=1): + self.ctx = zmq.Context().instance() + self.socket = self.ctx.socket(zmq.SUB) + self.socket.setsockopt(zmq.CONFLATE, conflate) + self._topics = set() + self.socket.connect(addr) + + if topics is not None: + for topic in topics: + self.subscribe(topic) + + def subscribe(self, topic): + if len(topic) == 0: + self.socket.subscribe('') + self._topics.add('') + return + + if topic not in self._topics: + self._topics.add(topic) + topic_bytes = bytearray([10, len(topic)]) + topic_bytes.extend(topic) + self.socket.subscribe(bytes(topic_bytes)) + + def topics(self): + return list(self._topics) + + def receive(self): + return self.socket.recv() + + def close(self): + return self.socket.close() + + +class Publisher: + def __init__(self, addr, conflate=1): + self.ctx = zmq.Context().instance() + self.socket = self.ctx.socket(zmq.PUB) + self.socket.setsockopt(zmq.CONFLATE, conflate) + self.socket.bind(addr) + + def publish(self, message, topic=b''): + message = proto.Packet(topic=topic, created=int(time.time()), payload=message) + return self.socket.send(message.SerializeToString()) + + def publish_json(self, message, topic=b''): + return self.publish(json.dumps(message).encode('UTF-8'), topic) + + def close(self): + return self.socket.close() diff --git a/chime/sensor.py b/chime/sensor.py new file mode 100644 index 0000000..09aee66 --- /dev/null +++ b/chime/sensor.py @@ -0,0 +1,176 @@ +import board +import busio +import json +import adafruit_ms8607 as ms8607 +from time import sleep + +DELAY = 0.1 + + +class Reading: + def __init__(self, t, p, h, normal=False): + if not normal: + if t < -40.0 or t > 85.0: + raise TemperatureError(t) + if h < 0.0 or h > 100.0: + raise HumidityError(h) + if p < 10 or p > 2000: + raise PressureError(p) + + self.time = datetime.datetime.now() + self.temp = t + self.press = p + self.hum = h + self.normal = normal + + def __repr__(self): + return f'Reading@{self.time.timestamp()}(t={self.temp},p={self.press},h={self.hum})' + + def __str__(self): + return f'{self.time.strftime("%F %T %Z")},{self.time.timestamp()},{self.temp},{self.press},{self.hum}' + + def list(self): + return [ + self.time.strftime("%F %T %Z"), + self.time.timestamp(), + self.temp, + self.press, + self.hum + ] + + def normalize(self): + """ + normal returns a normalised average of PHT. + """ + + if self.normal: + return self + return Reading( + normal_temp(self.temp), + normal_press(self.press), + normal_humidity(self.hum), + normal=True + ) + + def average(self): + if not self.normal: + return self.normalize().average() + return average([self.temp, self.press, self.hum]) + + def json(self) -> bytes: + return bytes(json.dumps({ + 'timestamp': self.time.timestamp, + 'temperature': self.temp, + 'pressure': self.press, + 'relative_humidity': self.hum, + })) + + +class SensorError(Exception): + def __init__(self, component, value): + self.component = component + self.value = value + + def __repr__(self): + return f'SensorError: {self.component} out of range: {self.value}' + + def __str__(self): + return repr(self) + + +class TemperatureError(SensorError): + def __init__(self, value): + super().__init__('temperature', value) + + +class PressureError(SensorError): + def __init__(self, value): + super().__init__('pressure', value) + + +class HumidityError(SensorError): + def __init__(self, value): + super().__init__('humidity', value) + + +def clamp(val): + return min(1.0, max(0.0, val)) + + +def normalize(minval, maxval, val): + return clamp((val - minval) / maxval) + + +def normal_temp(temperature): + return normalize(0.0, 45.0, temperature) + + +def normal_press(pressure): + return normalize(1000.0, 1050.0, pressure) + + +def normal_humidity(hum): + return normalize(20, 90, hum) + + +def average(values): + return sum(values) / len(values) + + +class Sensor: + def __init__(self): + pass + + def reading(self) -> Reading: + return Reading(0, 0, 0) + + def json(self) -> bytes: + pass + + +class TestSensor(Sensor): + def __init__(self, t, h, p): + super().__init__() + self.temperature = t + self.relative_humidity = h + self.pressure = p + + def json(self) -> bytes: + return bytes(json.dumps({ + 'temperature': self.temperature, + 'pressure': self.pressure, + 'relative_humidity': self.relative_humidity, + })) + + +class MS8607(Sensor): + + def __init__(self, i2c=None): + super().__init__() + if i2c is None: + i2c = busio.I2C(board.SCL, board.SDA) + self.sensor = get_sensor(i2c) + + def reading(self) -> Reading: + while True: + try: + reading = Reading(self.sensor.temperature, self.sensor.pressure, self.sensor.relative_humidity) + except (OSError, ValueError, SensorError) as err: + print(err) + sleep(DELAY) + else: + return reading + + +def get_sensor(i2c): + while True: + try: + sensor = ms8607.MS8607(i2c) + except OSError as os_error: + print(os_error) + sleep(DELAY) + except ValueError as i2c_error: + print(i2c_error) + sleep(DELAY) + else: + return sensor diff --git a/chime/telemetry_pb2.py b/chime/telemetry_pb2.py new file mode 100644 index 0000000..d8fc68f --- /dev/null +++ b/chime/telemetry_pb2.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: telemetry.proto + + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + +DESCRIPTOR = _descriptor.FileDescriptor( + name='telemetry.proto', + package='telemetrypb', + syntax='proto3', + serialized_options=b'Z\r.;telemetrypb', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x0ftelemetry.proto\x12\x0btelemetrypb\";\n\x06Packet\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07payload\x18\x03 \x01(\x0c\x42\x0fZ\r.;telemetrypbb\x06proto3' +) + +_PACKET = _descriptor.Descriptor( + name='Packet', + full_name='telemetrypb.Packet', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='topic', full_name='telemetrypb.Packet.topic', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='timestamp', full_name='telemetrypb.Packet.timestamp', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='payload', full_name='telemetrypb.Packet.payload', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=32, + serialized_end=91, +) + +DESCRIPTOR.message_types_by_name['Packet'] = _PACKET +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Packet = _reflection.GeneratedProtocolMessageType('Packet', (_message.Message,), { + 'DESCRIPTOR': _PACKET, + '__module__': 'telemetry_pb2' + # @@protoc_insertion_point(class_scope:telemetrypb.Packet) +}) +_sym_db.RegisterMessage(Packet) + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9ac50f3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +adafruit-circuitpython-ms8607 +protobuf +psycopg2 +zmq diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..58f6440 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + +setup( + name='chime', + version='0.1.0', + packages=find_packages(include=['chime', 'chime.*']), + entry_points={ + 'console_scripts': ['my-command=exampleproject.example:main.py'] + } +)