Add light detection, fixes and tweaks

This commit is contained in:
Phil Howard
2020-09-01 19:03:44 +01:00
parent d778a8a779
commit e3c7869f40

View File

@@ -9,6 +9,7 @@ import threading
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
import ST7735 import ST7735
import ltr559
from fonts.ttf import RobotoMedium as UserFont from fonts.ttf import RobotoMedium as UserFont
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
@@ -47,8 +48,9 @@ def icon(image, icon, position, color):
class View: class View:
def __init__(self): def __init__(self, image):
pass self._image = image
self._draw = ImageDraw.Draw(image)
def button_a(self): def button_a(self):
return False return False
@@ -65,12 +67,11 @@ class View:
def update(self): def update(self):
pass pass
def render(self, canvas): def render(self):
pass pass
def label( def label(
self, self,
canvas,
position="X", position="X",
text="", text="",
bgcolor=(0, 0, 0), bgcolor=(0, 0, 0),
@@ -80,8 +81,7 @@ class View:
if position not in ["A", "B", "X", "Y"]: if position not in ["A", "B", "X", "Y"]:
raise ValueError(f"Invalid label position {position}") raise ValueError(f"Invalid label position {position}")
draw = ImageDraw.Draw(canvas) text_w, text_h = self._draw.textsize(text, font=font)
text_w, text_h = draw.textsize(text, font=font)
text_h = 11 text_h = 11
text_w += margin * 2 text_w += margin * 2
text_h += margin * 2 text_h += margin * 2
@@ -97,17 +97,16 @@ class View:
x2, y2 = x + text_w, y + text_h x2, y2 = x + text_w, y + text_h
draw.rectangle((x, y, x2, y2), bgcolor) self._draw.rectangle((x, y, x2, y2), bgcolor)
draw.text((x + margin, y + margin - 1), text, font=font, fill=textcolor) self._draw.text((x + margin, y + margin - 1), text, font=font, fill=textcolor)
class MainView(View): class MainView(View):
def __init__(self, channels=None): def __init__(self, image, channels=None):
self.channels = channels self.channels = channels
View.__init__(self) View.__init__(self, image)
def render_channel(self, canvas, channel, font): def render_channel(self, channel, font):
draw = ImageDraw.Draw(image)
x = [21, 61, 101][channel.channel - 1] x = [21, 61, 101][channel.channel - 1]
# Saturation amounts from each sensor # Saturation amounts from each sensor
@@ -116,65 +115,92 @@ class MainView(View):
if active: if active:
# Draw background bars # Draw background bars
draw.rectangle( self._draw.rectangle(
(x, int((1.0 - saturation) * HEIGHT), x + 37, HEIGHT), (x, int((1.0 - saturation) * DISPLAY_HEIGHT), x + 37, DISPLAY_HEIGHT),
channel.indicator_color(saturation) if active else (229, 229, 229), channel.indicator_color(saturation) if active else (229, 229, 229),
) )
# Channel selection icons # Channel selection icons
x += 15 x += 15
col = channel.indicator_color(saturation, channel.label_colours) col = channel.indicator_color(saturation, channel.label_colours)
draw.rectangle( if channel.alarm:
icon(
self._image,
icon_snooze,
(x - 2, 0),
col if active else (129, 129, 129)
)
else:
self._draw.rectangle(
(x, 2, x + 15, 17), (x, 2, x + 15, 17),
col if active else (129, 129, 129), col if active else (129, 129, 129),
) )
# TODO: replace number text with graphic # TODO: replace number text with graphic
tw, th = font.getsize("{}".format(channel.channel)) tw, th = font.getsize("{}".format(channel.channel))
draw.text( self._draw.text(
(x + int(math.ceil(8 - (tw / 2.0))), 2), (x + int(math.ceil(8 - (tw / 2.0))), 2),
"{}".format(channel.channel), "{}".format(channel.channel),
font=font, font=font,
fill=(255, 255, 255) if active else (200, 200, 200), fill=(255, 255, 255) if active else (200, 200, 200),
) )
def render(self, canvas): def render(self):
draw = ImageDraw.Draw(canvas) self._draw.rectangle((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), (255, 255, 255))
width, height = canvas.size
draw.rectangle((0, 0, width, height), (255, 255, 255))
for channel in self.channels: for channel in self.channels:
self.render_channel(canvas, channel, font) self.render_channel(channel, font)
# Icon backdrops # Icon backdrops
draw.rectangle((0, 0, 19, 19), (32, 138, 251)) self._draw.rectangle((0, 0, 19, 19), (32, 138, 251))
# Icons # Icons
icon(image, icon_rightarrow, (0, 0), (255, 255, 255)) icon(self._image, icon_rightarrow, (0, 0), (255, 255, 255))
alarm.render(canvas, (0, DISPLAY_HEIGHT - 19)) alarm.render((0, DISPLAY_HEIGHT - 19))
class DetailView(View): class ChannelView(View):
def __init__(self, channel=None): def draw_status(self, subtle=False):
self._draw.rectangle((0, 20, DISPLAY_WIDTH, 40), (50, 50, 50))
self._draw.text(
(3, 23),
f"{self.channel.sensor.saturation * 100:.2f}% ({self.channel.sensor.moisture:.2f}Hz)",
font=font,
fill=(150, 150, 150) if subtle else (255, 255, 255),
)
def draw_next_button(self, disabled=False):
if disabled:
# Draw disabled "Next" button
self._draw.rectangle((0, 0, 19, 19), (138, 138, 138))
icon(self._image, icon_rightarrow, (0, 0), (150, 150, 150))
else:
self._draw.rectangle((0, 0, 19, 19), COLOR_BLUE)
icon(self._image, icon_rightarrow, (0, 0), COLOR_WHITE)
class DetailView(ChannelView):
def __init__(self, image, channel=None):
self.channel = channel self.channel = channel
View.__init__(self) View.__init__(self, image)
def render(self, canvas): def render(self):
draw = ImageDraw.Draw(canvas) width, height = self._image.size
width, height = canvas.size self._draw.rectangle((0, 0, width, height), (255, 255, 255))
draw.rectangle((0, 0, width, height), (255, 255, 255))
draw.text( self._draw.text(
(23, 3), (23, 3),
"{}".format(self.channel.title), "{}".format(self.channel.title),
font=font, font=font,
fill=(0, 0, 0), fill=(0, 0, 0),
) )
graph_height = DISPLAY_HEIGHT - 20 graph_height = DISPLAY_HEIGHT - 40
draw.rectangle((0, 20, DISPLAY_WIDTH, DISPLAY_HEIGHT), (60, 60, 60)) self._draw.rectangle((0, 40, DISPLAY_WIDTH, 40 + graph_height), (10, 10, 10))
self.draw_status()
offset_x = 20 offset_x = 20
offset_y = 20 offset_y = 20
@@ -182,47 +208,42 @@ class DetailView(View):
for x, value in enumerate(self.channel.sensor.history[:DISPLAY_WIDTH]): for x, value in enumerate(self.channel.sensor.history[:DISPLAY_WIDTH]):
color = self.channel.indicator_color(value) color = self.channel.indicator_color(value)
h = value * graph_height h = value * graph_height
draw.rectangle((x, DISPLAY_HEIGHT - h, x + 1, DISPLAY_HEIGHT), color) x = DISPLAY_WIDTH - x - 1
self._draw.rectangle((x, DISPLAY_HEIGHT - h, x + 1, DISPLAY_HEIGHT), color)
alarm_line = self.channel.alarm_level * graph_height alarm_line = int(self.channel.alarm_level * graph_height)
draw.rectangle( r = 255
if self.channel.alarm:
r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 128) + 127
self._draw.rectangle(
( (
0, 0,
DISPLAY_HEIGHT - alarm_line, DISPLAY_HEIGHT - alarm_line,
DISPLAY_WIDTH - 40,
DISPLAY_HEIGHT - alarm_line + 1,
),
(r, 0, 0),
)
self._draw.rectangle(
(
DISPLAY_WIDTH - 20,
DISPLAY_HEIGHT - alarm_line,
DISPLAY_WIDTH, DISPLAY_WIDTH,
DISPLAY_HEIGHT - alarm_line + 1, DISPLAY_HEIGHT - alarm_line + 1,
), ),
(255, 0, 0), (r, 0, 0),
)
draw.rectangle(
(
DISPLAY_WIDTH - 50,
DISPLAY_HEIGHT - alarm_line - 16,
DISPLAY_WIDTH,
DISPLAY_HEIGHT - alarm_line + 1,
),
(255, 0, 0),
) )
draw.text( icon(self._image, icon_snooze, (DISPLAY_WIDTH - 40, DISPLAY_HEIGHT - alarm_line - 10), (r, 0, 0))
(DISPLAY_WIDTH - 47, DISPLAY_HEIGHT - alarm_line - 15),
"Alarm",
font=font,
fill=(255, 255, 255),
)
# Icon backdrops self.draw_next_button()
draw.rectangle((0, 0, 19, 19), (32, 138, 251))
# Icons
icon(image, icon_rightarrow, (0, 0), (255, 255, 255))
# Edit # Edit
self.label(canvas, "X", "Edit", textcolor=(255, 255, 255), bgcolor=COLOR_RED) self.label("X", "Edit", textcolor=(255, 255, 255), bgcolor=COLOR_RED)
class EditView(View): class EditView(ChannelView):
def __init__(self, channel=None): def __init__(self, image, channel=None):
self._options = [ self._options = [
{ {
"title": "Alarm Level", "title": "Alarm Level",
@@ -258,21 +279,16 @@ class EditView(View):
self._current_option = 0 self._current_option = 0
self._change_mode = False self._change_mode = False
self.channel = channel self.channel = channel
View.__init__(self) View.__init__(self, image)
def render(self, canvas): def render(self):
draw = ImageDraw.Draw(canvas) graph_height = DISPLAY_HEIGHT - 40
width, height = canvas.size
draw.rectangle((0, 0, width, height), (255, 255, 255))
draw.text((23, 3), "{}".format(self.channel.title), font=font, fill=(0, 0, 0)) self._draw.rectangle((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), (255, 255, 255))
draw.text( self._draw.text((23, 3), "{}".format(self.channel.title), font=font, fill=(0, 0, 0))
(5, 25),
f"Now: {self.channel.sensor.saturation * 100:.2f}% {self.channel.sensor.moisture:.2f}Hz", self.draw_status(True)
font=font,
fill=(0, 0, 0),
)
option = self._options[self._current_option] option = self._options[self._current_option]
title = option["title"] title = option["title"]
@@ -280,23 +296,18 @@ class EditView(View):
value = getattr(self.channel, prop) value = getattr(self.channel, prop)
text = option["format"](value) text = option["format"](value)
mode = option.get("mode", "int") mode = option.get("mode", "int")
draw.text((5, 40), f"{title} : {text}", font=font, fill=(0, 0, 0)) self._draw.text((3, 43), f"{title} : {text}", font=font, fill=(0, 0, 0))
draw.rectangle((0, 0, 19, 19), (138, 138, 138)) self.draw_next_button(True)
# Icons
icon(image, icon_rightarrow, (0, 0), (255, 255, 255))
if self._change_mode: if self._change_mode:
self.label(canvas, "Y", "Yes" if mode == "bool" else "++", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW) self.label("Y", "Yes" if mode == "bool" else "++", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW)
self.label(canvas, "B", "No" if mode == "bool" else "--", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE) self.label("B", "No" if mode == "bool" else "--", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE)
else: else:
self.label( self.label("Y", "Change", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW)
canvas, "Y", "Change", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW self.label("B", "Next", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE)
)
self.label(canvas, "B", "Next", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE)
self.label(canvas, "X", "Done", textcolor=COLOR_WHITE, bgcolor=COLOR_RED) self.label("X", "Done", textcolor=COLOR_WHITE, bgcolor=COLOR_RED)
def button_a(self): def button_a(self):
pass pass
@@ -320,6 +331,7 @@ class EditView(View):
else: else:
self._current_option += 1 self._current_option += 1
self._current_option %= len(self._options) self._current_option %= len(self._options)
return True
def button_x(self): def button_x(self):
if self._change_mode: if self._change_mode:
@@ -388,17 +400,43 @@ class Channel:
self.pump_speed = pump_speed self.pump_speed = pump_speed
self.pump_time = pump_time self.pump_time = pump_time
self.watering_delay = watering_delay self.watering_delay = watering_delay
self.wet_point = wet_point self._wet_point = wet_point
self.dry_point = dry_point self._dry_point = dry_point
self.last_dose = time.time() self.last_dose = time.time()
self.icon = icon self.icon = icon
self.enabled = enabled self._enabled = enabled
self.alarm = False self.alarm = False
self.title = f"Channel {display_channel}" if title is None else title self.title = f"Channel {display_channel}" if title is None else title
self.sensor.set_wet_point(wet_point) self.sensor.set_wet_point(wet_point)
self.sensor.set_dry_point(dry_point) self.sensor.set_dry_point(dry_point)
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, enabled):
self._enabled = enabled
@property
def wet_point(self):
return self._wet_point
@property
def dry_point(self):
return self._dry_point
@wet_point.setter
def wet_point(self, wet_point):
self._wet_point = wet_point
self.sensor.set_wet_point(wet_point)
@dry_point.setter
def dry_point(self, dry_point):
self._dry_point = dry_point
self.sensor.set_dry_point(dry_point)
def indicator_color(self, value, r=None): def indicator_color(self, value, r=None):
value = 1.0 - value value = 1.0 - value
@@ -444,7 +482,16 @@ Delay: {watering_delay}
Wet point: {wet_point} Wet point: {wet_point}
Dry point: {dry_point} Dry point: {dry_point}
""".format( """.format(
**self.__dict__ channel=self.channel,
enabled=self.enabled,
alarm_level=self.alarm_level,
auto_water=self.auto_water,
water_level=self.water_level,
pump_speed=self.pump_speed,
pump_time=self.pump_time,
watering_delay=self.watering_delay,
wet_point=self.wet_point,
dry_point=self.dry_point
) )
def water(self): def water(self):
@@ -470,17 +517,22 @@ Dry point: {dry_point}
self.channel, self.pump_speed, self.pump_time self.channel, self.pump_speed, self.pump_time
) )
) )
if sat < self.alarm_level and not self.alarm: if sat < self.alarm_level:
if not self.alarm:
logging.warning( logging.warning(
"Alarm on Channel: {} - saturation is {:.2f}% (warn level {:.2f}%)".format( "Alarm on Channel: {} - saturation is {:.2f}% (warn level {:.2f}%)".format(
self.channel, sat * 100, self.alarm_level * 100 self.channel, sat * 100, self.alarm_level * 100
) )
) )
self.alarm = True self.alarm = True
else:
self.alarm = False
class Alarm: class Alarm:
def __init__(self, enabled=True, interval=10.0, beep_frequency=440): def __init__(self, image, enabled=True, interval=10.0, beep_frequency=440):
self._image = image
self._draw = ImageDraw.Draw(image)
self.piezo = Piezo() self.piezo = Piezo()
self.enabled = enabled self.enabled = enabled
self.interval = interval self.interval = interval
@@ -494,13 +546,14 @@ class Alarm:
self.enabled = config.get("alarm_enable", self.enabled) self.enabled = config.get("alarm_enable", self.enabled)
self.interval = config.get("alarm_interval", self.interval) self.interval = config.get("alarm_interval", self.interval)
def update(self): def update(self, lights_out=False):
if self._sleep_until is not None: if self._sleep_until is not None:
if self._sleep_until > time.time(): if self._sleep_until > time.time():
return return
if ( if (
self.enabled self.enabled
and not lights_out
and self._triggered and self._triggered
and time.time() - self._time_last_beep > self.interval and time.time() - self._time_last_beep > self.interval
): ):
@@ -519,19 +572,18 @@ class Alarm:
).start() ).start()
self._time_last_beep = time.time() self._time_last_beep = time.time()
def render(self, canvas, position=(0, 0)): def render(self, position=(0, 0)):
draw = ImageDraw.Draw(canvas)
x, y = position x, y = position
# Draw the snooze icon- will be pulsing red if the alarm state is True # Draw the snooze icon- will be pulsing red if the alarm state is True
draw.rectangle((x, y, x + 19, y + 19), (255, 255, 255)) self._draw.rectangle((x, y, x + 19, y + 19), (255, 255, 255))
r = 129 r = 129
if self._triggered: if self._triggered:
r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 255) r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 128) + 127
icon(image, icon_snooze, (x, y - 1), (r, 129, 129)) icon(self._image, icon_snooze, (x, y - 1), (r, 129, 129))
if self._sleep_until is not None: # TODO maybe sleeping alarm icon? if self._sleep_until is not None: # TODO maybe sleeping alarm icon?
if self._sleep_until > time.time(): if self._sleep_until > time.time():
draw.text((x, y), "zZ", font=font, fill=(255, 255, 255)) self._draw.text((x, y), "zZ", font=font, fill=(255, 255, 255))
def trigger(self): def trigger(self):
self._triggered = True self._triggered = True
@@ -579,22 +631,24 @@ class ViewController:
def update(self): def update(self):
self.view.update() self.view.update()
def render(self, canvas): def render(self):
self.view.render(canvas) self.view.render()
def button_a(self): def button_a(self):
if not self.view.button_a(): if not self.view.button_a():
self.next_view() self.next_view()
def button_b(self): def button_b(self):
self.view.button_b() return self.view.button_b()
def button_x(self): def button_x(self):
if not self.view.button_x(): if not self.view.button_x():
self.next_subview() self.next_subview()
return True
return True
def button_y(self): def button_y(self):
self.view.button_y() return self.view.button_y()
def handle_button(pin): def handle_button(pin):
@@ -620,10 +674,12 @@ display = ST7735.ST7735(
port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=80000000 port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=80000000
) )
display.begin() display.begin()
WIDTH, HEIGHT = display.width, display.height
# Set up light sensor
light = ltr559.LTR559()
# Set up our canvas and prepare for drawing # Set up our canvas and prepare for drawing
image = Image.new("RGBA", (WIDTH, HEIGHT), color=(255, 255, 255)) image = Image.new("RGBA", (DISPLAY_WIDTH, DISPLAY_HEIGHT), color=(255, 255, 255))
font = ImageFont.truetype(UserFont, 14) font = ImageFont.truetype(UserFont, 14)
@@ -636,14 +692,14 @@ channels = [
viewcontroller = ViewController( viewcontroller = ViewController(
[ [
MainView(channels=channels), MainView(image, channels=channels),
(DetailView(channel=channels[0]), EditView(channel=channels[0])), (DetailView(image, channel=channels[0]), EditView(image, channel=channels[0])),
(DetailView(channel=channels[1]), EditView(channel=channels[1])), (DetailView(image, channel=channels[1]), EditView(image, channel=channels[1])),
(DetailView(channel=channels[2]), EditView(channel=channels[2])), (DetailView(image, channel=channels[2]), EditView(image, channel=channels[2])),
] ]
) )
alarm = Alarm() alarm = Alarm(image)
def main(): def main():
@@ -695,10 +751,10 @@ Alarm Interval: {:.2f}s
if channel.alarm: if channel.alarm:
alarm.trigger() alarm.trigger()
alarm.update() alarm.update(light.get_lux() < 4.0)
viewcontroller.update() viewcontroller.update()
viewcontroller.render(image) viewcontroller.render()
display.display(image.convert("RGB")) display.display(image.convert("RGB"))
# 5 FPS # 5 FPS