Compare commits

...

50 commits
v1.0 ... master

Author SHA1 Message Date
297605b81e Improved readability 2024-08-10 02:53:23 +02:00
5e673e0c0e Self implemented co2 again 2024-08-10 02:50:05 +02:00
3f083c0aed Simplified co2 2024-08-10 02:44:33 +02:00
d1bb240080 debugging 2024-08-10 02:42:20 +02:00
b0a5903c6e Merge branch 'master' of code.giller.dev:m.giller/matrix-clock 2024-08-10 02:40:40 +02:00
6066af50ac debuging 2024-08-10 02:40:33 +02:00
3cb59cc688 debuging 2024-08-10 02:40:29 +02:00
d3b2cc4c89 Fixed co2 reading 2024-08-10 02:39:16 +02:00
cb50f230b6 Implemented co2 library 2024-08-10 02:36:09 +02:00
e2fa06d94e Some small improvements 2024-08-10 01:51:00 +02:00
d2825a32ae Removed dead code 2024-08-10 01:41:16 +02:00
8d625264d1 Not using start.sh anymore 2024-08-10 01:28:18 +02:00
df102d8e4e Revert "Added temporary contrast to pattern endpoint"
This reverts commit 9715339c43.
2024-08-10 01:24:21 +02:00
aff797bcb5 Implemented co2 sensor and silent climate endpoint 2024-08-10 00:57:37 +02:00
9715339c43 Added temporary contrast to pattern endpoint 2024-05-06 22:01:20 +02:00
e44a78742e Fixed Uart serial stuff 2024-05-02 22:10:52 +02:00
7f4eae67f6 Added serial code for mh_z19c 2024-05-02 22:03:54 +02:00
ea73b3702c Simplified format for patterns 2024-03-03 18:57:53 +01:00
31072baf97 Fixed climate logger not starting 2024-03-03 18:44:03 +01:00
de66a88fda Removed separate off and on ms for pattern 2024-03-03 17:32:10 +01:00
2bcc99a893 sanity check for pattern parser 2024-03-03 16:17:57 +01:00
d10359f8d1 Simplified pattern handling 2024-03-03 16:17:38 +01:00
6e42689a91 Some fixes 2024-03-03 16:15:04 +01:00
49c53b0f93 Fixed relative imports 2024-03-03 16:12:16 +01:00
cef3904bd0 Added missing package init 2024-03-03 16:10:48 +01:00
e088e7db32 Added missing package init 2024-03-03 16:08:13 +01:00
eab3a825f2 Fixed missing code from removing abstraction 2024-03-03 16:00:18 +01:00
075e7d277a Removed unnecessary abstraction; Implemented pattern endpoint 2024-03-03 15:58:12 +01:00
30571d2439 Implemented start up actions and tasks 2024-01-30 20:43:25 +01:00
91648b739f Allowing non-awaitable idle actions as well 2024-01-30 20:31:32 +01:00
da780894d0 Allowing non-awaitable actions 2024-01-30 20:25:48 +01:00
0ab2d6996e More None checks 2024-01-30 20:13:11 +01:00
fc904f42bf Fixes 2024-01-30 20:08:32 +01:00
44890aced6 Hmm 2024-01-30 20:04:01 +01:00
1094fce783 Max fixes Dinos problems 2024-01-30 20:01:20 +01:00
1854c3ee27 Dino0040 fixes everthing 2024-01-30 19:58:21 +01:00
931e7c1dc0 Removed duplicate endpoint 2024-01-30 19:31:47 +01:00
1f1020c53a Some changes 2023-12-29 22:51:03 +01:00
051f0e8ed0 Added api to tests 2023-12-29 22:50:40 +01:00
c25d099f0c Improved exception handling 2023-12-29 22:50:13 +01:00
Max
6a85e24096 Ignoring climate file 2023-12-25 00:14:53 +01:00
Max
d9cd7a8688 Implemented matrix and climate abstractions, and fixed startup 2023-12-24 15:07:26 +01:00
Max
393b14374b Implemented custom contrast for flashes 2023-12-23 20:28:35 +01:00
Max
f90bfe5090 Implemented full-screen endpoint and contrast setting 2023-12-23 19:19:17 +01:00
Max
fa16909572 Fixed action queue 2023-12-23 18:57:55 +01:00
123ee48529 Testing action queue 2023-12-22 19:09:18 +01:00
fb98e38e00 Merge branch 'master' of code.giller.dev:m.giller/matrix-clock 2023-12-22 16:32:57 +01:00
e94f2368d1 Added potentially missing origin for CORS 2023-12-22 16:32:08 +01:00
Max
f860108b52 Improved code structure and implemented improved async action handling with queue and idle actions 2023-12-18 00:21:37 +01:00
Max
feefd55e20 Improved code structure 2023-12-17 21:04:39 +01:00
12 changed files with 282 additions and 137 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
__pycache__
src/climate.csv

View file

@ -4,4 +4,5 @@ fastapi
uvicorn
fastapi-cors
Adafruit_DHT
requests
requests
mh_z19

48
src/actions.py Normal file
View file

@ -0,0 +1,48 @@
import asyncio
from datetime import datetime
import os
from config import climate_log_file, dht22_pin
from handler.dht22_climate import Dht22Climate
from handler.matrix_display import MatrixDisplay
from handler.mhz19_co2 import Mhz19Co2
climate_sensor = Dht22Climate(dht22_pin)
co2_sensor = Mhz19Co2()
matrix_display = MatrixDisplay()
async def log_climate(delay_sec: int = 60):
# If file does not exist, create it and write header
if not os.path.isfile(climate_log_file):
with open(climate_log_file, "w") as f:
f.write("timestamp,temperature,humidity,co2\n")
while True:
measurements = climate_sensor.read()
co2_density = co2_sensor.read()
if measurements is not None:
with open(climate_log_file, "a") as f:
f.write(
"{},{},{},{}\n".format(
datetime.now().isoformat(),
measurements["temperature"],
measurements["humidity"],
co2_density,
)
)
await asyncio.sleep(delay_sec)
async def display_time():
while True:
matrix_display.show_current_time()
seconds_until_next_minute = 60 - datetime.now().second
await asyncio.sleep(seconds_until_next_minute)
async def display_pattern(*args, **kwargs):
while True:
await matrix_display.pattern(*args, **kwargs)

2
src/config.py Normal file
View file

@ -0,0 +1,2 @@
dht22_pin = 17
climate_log_file = "./climate.csv"

0
src/handler/__init__.py Normal file
View file

View file

@ -0,0 +1,48 @@
import asyncio
import logging
class ActionQueue:
def __init__(self, idle_action = None, *args, **kwargs) -> None:
self.queued_actions: asyncio.Queue = asyncio.Queue()
self.idle_action: tuple = (idle_action, args, kwargs)
self.queue_task = asyncio.create_task(self.run_queue())
self.idle_action_task = None
def __del___(self):
self.queue_task.cancel()
if self.idle_action_task is not None:
self.idle_action_task.cancel()
async def run_queue(self):
while True:
try:
if self.queued_actions.empty() and self.idle_action[0] is not None:
self.idle_action_task = asyncio.create_task(self.run_action(self.idle_action))
action = await self.queued_actions.get()
if self.idle_action_task is not None:
self.idle_action_task.cancel()
self.idle_action_task = None
await self.run_action(action)
except Exception as ex:
logging.exception("Something went wrong in queue task.", ex)
async def run_action(self, action):
if action is None or action[0] is None:
return
awaitable = action[0](*(action[1]), **(action[2]))
if awaitable is not None:
await awaitable
async def add_action_to_queue(self, action, *args, **kwargs):
await self.queued_actions.put((action, args, kwargs))
async def set_idle_action(self, action, *args, **kwargs):
self.idle_action = (action, args, kwargs)
await self.queued_actions.put(None)

View file

@ -1,20 +1,22 @@
import Adafruit_DHT
class Dht22Sensor:
def __init__(self, pin):
class Dht22Climate:
def __init__(self, pin: int):
self.sensor = Adafruit_DHT.AM2302
self.pin = pin
self.last_read = None
def read(self):
def get_last_read(self) -> dict | None:
if self.last_read is None:
return self.read()
return self.last_read
def read(self) -> dict | None:
humidity, temperature = Adafruit_DHT.read_retry(self.sensor, self.pin)
if humidity is not None and temperature is not None:
self.last_read = {'temperature': temperature, 'humidity': humidity}
self.last_read = {"temperature": temperature, "humidity": humidity}
return self.last_read
else:
return None
def get_last_read(self):
if self.last_read is None:
self.read()
return self.last_read

View file

@ -1,3 +1,4 @@
import asyncio
import time
from datetime import datetime
from luma.core.interface.serial import spi, noop
@ -5,7 +6,7 @@ from luma.core.render import canvas
from luma.led_matrix.device import max7219
from luma.core import legacy
from luma.core.virtual import viewport
from luma.core.legacy.font import proportional, CP437_FONT, LCD_FONT
from luma.core.legacy.font import proportional, CP437_FONT
class MatrixDisplay:
@ -19,7 +20,17 @@ class MatrixDisplay:
self.device.contrast(self.contrast)
def set_contrast(self, contrast: int):
"""Set contrast for all actions.
Args:
contrast (int): [0, 255]
"""
self.contrast = contrast
self.device.contrast(self.contrast)
def show_text(self, text):
self.device.contrast(self.contrast)
width = len(text) * 8 + 4 * 8
virtual = viewport(self.device, width + 4 * 8, height=8)
with canvas(virtual) as draw:
@ -31,8 +42,12 @@ class MatrixDisplay:
virtual.set_position((offset, 0))
time.sleep(self.text_speed)
def flash(self, count=1):
self.device.contrast(255)
def flash(self, count=1, contrast=None):
if contrast:
self.device.contrast(contrast)
else:
self.device.contrast(self.contrast)
while count > 0:
with canvas(self.device) as draw:
draw.rectangle((0, 0, 31, 7), outline="white", fill="white")
@ -43,15 +58,29 @@ class MatrixDisplay:
time.sleep(0.1)
count -= 1
self.device.contrast(self.contrast)
def turn_off(self):
self.device.contrast(0)
with canvas(self.device) as draw:
draw.rectangle((0, 0, 31, 7), outline="black", fill="black")
def turn_full(self):
self.device.contrast(self.contrast)
with canvas(self.device) as draw:
draw.rectangle((0, 0, 31, 7), outline="white", fill="white")
async def pattern(self, pattern: str = "01", step_ms: int = 500):
# Parse
pattern_steps = [step.strip() == "1" for step in pattern]
# Execute
for step in pattern_steps:
if step:
self.turn_full()
else:
self.turn_off()
await asyncio.sleep(step_ms / 1000)
def show_current_time(self):
self.device.contrast(self.contrast)
hour = str(datetime.now().hour).rjust(2, "0")
minute = str(datetime.now().minute).rjust(2, "0")
with canvas(self.device) as draw:

48
src/handler/mhz19_co2.py Normal file
View file

@ -0,0 +1,48 @@
import serial
class Mhz19Co2:
def __init__(self):
self.last_read = None
self.serial_port = "/dev/serial0"
self.baud_rate = 9600
self.byte_size = 8
self.parity = "N"
self.stop_bits = 1
self.timeout = None
def get_last_read(self) -> int | None:
if self.last_read is None:
return self.read()
return self.last_read
def read(self) -> int | None:
ser = None
try:
ser = serial.Serial(
port=self.serial_port,
baudrate=self.baud_rate,
bytesize=self.byte_size,
parity=self.parity,
stopbits=self.stop_bits,
timeout=self.timeout,
)
# send "Read CO2" command
command_data = bytes([0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79])
ser.write(command_data)
# read "Return Value (CO2 concentration)"
data = ser.read(9)
concentration = data[2] * 256 + data[3]
except Exception as e:
print(f"Error reading data: {e}")
finally:
if ser:
ser.close()
ser = None
self.last_read = concentration
return concentration

View file

@ -1,20 +1,35 @@
import os
from fastapi import FastAPI, HTTPException
from datetime import datetime
import requests
from history import get_recent_entries
from matrix import MatrixDisplay
from fastapi.middleware.cors import CORSMiddleware
import asyncio
from climate import Dht22Sensor
import logging
from typing import Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from actions import (
climate_sensor,
display_time,
matrix_display,
display_pattern,
co2_sensor,
)
from config import climate_log_file
from handler.action_queue import ActionQueue
from handler.history import get_recent_entries
logging.getLogger().setLevel(logging.INFO)
# Start services
queue = ActionQueue(matrix_display.show_current_time)
app = FastAPI()
origins = [
"http://localhost",
"http://localhost:8000",
"http://raspberrypi",
"http://192.168.178.84"
"http://192.168.178.84:8000",
"http://192.168.178.84",
]
app.add_middleware(
@ -25,110 +40,75 @@ app.add_middleware(
allow_headers=["*"],
)
should_run_time_loop = True
dht22_pin = 17
climate_log_file = "./climate.csv"
matrix_display = MatrixDisplay()
dht22_sensor = Dht22Sensor(dht22_pin)
# Start background service to log temperature and humidity every minute
async def log_temperature():
# If file does not exist, create it and write header
if not os.path.isfile(climate_log_file):
with open(climate_log_file, "w") as f:
f.write("timestamp,temperature,humidity\n")
while True:
measurements = dht22_sensor.read()
if measurements is not None:
with open(climate_log_file, "a") as f:
f.write("{},{},{}\n".format(
datetime.now().isoformat(),
measurements["temperature"],
measurements["humidity"]
))
await asyncio.sleep(60)
async def display_time():
while should_run_time_loop:
try:
matrix_display.show_current_time()
except requests.exceptions.RequestException as e:
raise HTTPException(
status_code=500,
detail=f"Failed to display time on the matrix display: {e}",
)
seconds_until_next_minute = 60 - datetime.now().second
await asyncio.sleep(seconds_until_next_minute)
asyncio.create_task(display_time())
asyncio.create_task(log_temperature())
@app.post("/time")
async def start_time_loop():
global should_run_time_loop
should_run_time_loop = True
asyncio.create_task(display_time())
await queue.set_idle_action(display_time)
return {"message": "Time loop started"}
@app.post("/full")
async def turn_full():
await queue.set_idle_action(matrix_display.turn_full)
return {"message": "Full screen turned on"}
@app.post("/off")
async def turn_off():
global should_run_time_loop
should_run_time_loop = False
matrix_display.turn_off()
await queue.set_idle_action(matrix_display.turn_off)
return {"message": "Display turned off"}
@app.post("/temperature")
async def temperature():
measurements = dht22_sensor.get_last_read()
measurements = climate_sensor.read()
if measurements is None:
return {"message": "Failed to read temperature"}
global should_run_time_loop
was_clock_runnign = should_run_time_loop
should_run_time_loop = False
matrix_display.show_text("{0:0.1f}*C".format(measurements["temperature"]))
if was_clock_runnign:
should_run_time_loop = True
asyncio.create_task(display_time())
await queue.add_action_to_queue(
matrix_display.show_text, "{0:0.1f}*C".format(measurements["temperature"])
)
return measurements
@app.post("/humidity")
async def humidity():
measurements = dht22_sensor.get_last_read()
measurements = climate_sensor.read()
if measurements is None:
return {"message": "Failed to read humidity"}
global should_run_time_loop
was_clock_runnign = should_run_time_loop
should_run_time_loop = False
matrix_display.show_text("{0:0.1f}%".format(measurements["humidity"]))
if was_clock_runnign:
should_run_time_loop = True
asyncio.create_task(display_time())
await queue.add_action_to_queue(
matrix_display.show_text, "{0:0.1f}%".format(measurements["humidity"])
)
return measurements
@app.post("/co2")
async def co2():
co2 = co2_sensor.read()
if co2 is None:
return {"message": "Failed to read co2"}
await queue.add_action_to_queue(matrix_display.show_text, f"{co2} ppm")
return {"co2": co2}
@app.post("/climate")
async def climate():
measurements = climate_sensor.read()
if measurements is None:
return {"message": "Failed to read humidy and temperature"}
co2 = co2_sensor.read()
if co2 is None:
return {"message": "Failed to read co2"}
return {"co2": co2, **measurements}
@app.post("/history")
async def history():
day_entry_count = 24 * 60
@ -136,41 +116,29 @@ async def history():
@app.post("/flash")
async def flash(count: int = 1):
global should_run_time_loop
was_clock_runnign = should_run_time_loop
should_run_time_loop = False
matrix_display.flash(count)
if was_clock_runnign:
should_run_time_loop = True
asyncio.create_task(display_time())
async def flash(count: int = 1, contrast: Optional[int] = None):
await queue.add_action_to_queue(
matrix_display.flash, count=count, contrast=contrast
)
return {"message": "Display flashed"}
@app.post("/pattern")
async def flash(pattern: str = "01", step_ms: int = 500):
await queue.set_idle_action(display_pattern, pattern=pattern, step_ms=step_ms)
return {"message": "Activated pattern."}
@app.post("/contrast")
async def contrast(contrast: Optional[int] = None):
if contrast:
matrix_display.set_contrast(contrast)
return {"contrast": matrix_display.contrast}
@app.post("/message")
async def display_message(body: dict):
global should_run_time_loop
was_clock_runnign = should_run_time_loop
should_run_time_loop = False
message_text = body.get("message")
try:
matrix_display.show_text(message_text)
except requests.exceptions.RequestException as e:
raise HTTPException(
status_code=500,
detail=f"Failed to display message on the matrix display: {e}",
)
finally:
if was_clock_runnign:
should_run_time_loop = True
asyncio.create_task(display_time())
await queue.add_action_to_queue(matrix_display.show_text, message_text)
return {"message": "Message displayed"}
@app.post("/stop")
async def stop_time_loop():
global should_run_time_loop
should_run_time_loop = False
return {"message": "Time loop stopped"}

View file

@ -1,3 +0,0 @@
cd /home/pi/matrix-clock/src
uvicorn main:app --reload --host 0.0.0.0