Merge pull request #2 from pimoroni/new-ui

New UI
This commit is contained in:
Philip Howard
2020-09-15 10:03:11 +01:00
committed by GitHub
8 changed files with 176 additions and 149 deletions

View File

@@ -1,21 +1,12 @@
# Monitoring and/or Watering Your Plants
# Monitoring Your Plants
The `grow-monitor-and-water.py` example can monitor and, optionally, automatically water all three Grow channels.
The example `monitor.py` monitors the moisture level of your soil and sounds an alarm when it drops below a defined threshold.
By default auto-watering is disabled and an alarm will sound every 1s if the `warn_level` is reached.
Run it with `python3 grow-monitor-and-water.py`.
## Monitoring
Grow can monitor the moisture level of your soil, sounding an alarm when it dries out.
Grow is configured using `settings.yml`. Your settings for monitoring only will look something like this:
It's configured using `settings.yml`. Your settings for monitoring will look something like this:
```yaml
channel1:
warn_level: 0.2
icon: icons/flat-4.png
channel2:
warn_level: 0.2
channel3:
@@ -25,6 +16,16 @@ general:
alarm_interval: 1.0
```
`monitor.py` includes a main view showing the moisture status of each channel and the level beyond which the alarm will sound.
The controls from the main view are as follows:
* `A` - cycle through the main screen and each channel
* `B` - snooze the alarm
* `X` - configure global settings or the selected channel
The warning moisture level can be configured for each channel, along with the Wet and Dry points that store the frequency expected from the sensor when soil is fully wet/dry.
## Watering
If you've got pumps attached to Grow and want to automatically water your plants, you'll need some extra configuration options.
@@ -39,8 +40,7 @@ channel1:
pump_time: 0.7
wet_point: 0.7
dry_point: 27.6
auto_water: False
icon: icons/flat-4.png
auto_water: True
channel2:
water_level: 0.8
warn_level: 0.2
@@ -48,7 +48,7 @@ channel2:
pump_time: 0.7
wet_point: 0.7
dry_point: 27.6
auto_water: False
auto_water: True
channel3:
water_level: 0.8
warn_level: 0.2
@@ -56,7 +56,7 @@ channel3:
pump_time: 0.7
wet_point: 0.7
dry_point: 27.6
auto_water: False
auto_water: True
general:
alarm_enable: True
alarm_interval: 1.0
@@ -73,7 +73,6 @@ Grow has three channels which are separated into the sections `channel1`, `chann
* `auto_water` - Whether to run the attached pump (True to auto-water, False for manual watering)
* `wet_point` - Value for the sensor in saturated soil (in Hz)
* `dry_point` - Value for the sensor in totally dry soil (in Hz)
* `icon` - Optional icon image for the channel, see the icons directory for images.
## General Settings

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -28,10 +28,11 @@ DISPLAY_WIDTH = 160
DISPLAY_HEIGHT = 80
COLOR_WHITE = (255, 255, 255)
COLOR_BLUE = (32, 137, 251)
COLOR_GREEN = (100, 255, 124)
COLOR_BLUE = (31, 137, 251)
COLOR_GREEN = (99, 255, 124)
COLOR_YELLOW = (254, 219, 82)
COLOR_RED = (254, 82, 82)
COLOR_RED = (247, 0, 63)
COLOR_BLACK = (0, 0, 0)
# Only the ALPHA channel is used from these images
@@ -42,6 +43,9 @@ icon_alarm = Image.open("icons/icon-alarm.png").convert("RGBA")
icon_snooze = Image.open("icons/icon-snooze.png").convert("RGBA")
icon_help = Image.open("icons/icon-help.png").convert("RGBA")
icon_settings = Image.open("icons/icon-settings.png").convert("RGBA")
icon_channel = Image.open("icons/icon-channel.png").convert("RGBA")
icon_backdrop = Image.open("icons/icon-backdrop.png").convert("RGBA")
icon_return = Image.open("icons/icon-return.png").convert("RGBA")
class View:
@@ -71,7 +75,7 @@ class View:
pass
def clear(self):
self._draw.rectangle((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), (255, 255, 255))
self._draw.rectangle((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), (0, 0, 0))
def icon(self, icon, position, color):
col = Image.new("RGBA", icon.size, color=color)
@@ -80,7 +84,7 @@ class View:
def label(
self,
position="X",
text="",
text=None,
bgcolor=(0, 0, 0),
textcolor=(255, 255, 255),
margin=4,
@@ -109,16 +113,16 @@ class View:
(x + margin, y + margin - 1), text, font=self.font, fill=textcolor
)
def overlay(self, text):
def overlay(self, text, top=0):
"""Draw an overlay with some auto-sized text."""
self._draw.rectangle(
(0, 20, DISPLAY_WIDTH, DISPLAY_HEIGHT), fill=(192, 225, 254)
(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT), fill=(192, 225, 254)
) # Overlay backdrop
self._draw.rectangle((0, 20, DISPLAY_WIDTH, 21), fill=COLOR_BLUE) # Top border
self._draw.rectangle((0, top, DISPLAY_WIDTH, top + 1), fill=COLOR_BLUE) # Top border
self.text_in_rect(
text,
self.font,
(3, 20, DISPLAY_WIDTH - 3, DISPLAY_HEIGHT - 2),
(3, top, DISPLAY_WIDTH - 3, DISPLAY_HEIGHT - 2),
line_spacing=1,
)
@@ -186,45 +190,48 @@ class MainView(View):
View.__init__(self, image)
def render_channel(self, channel):
x = [21, 61, 101][channel.channel - 1]
bar_x = 33
bar_margin = 2
bar_width = 30
label_width = 16
label_height = 16
label_y = 0
x = [
bar_x,
bar_x + ((bar_width + bar_margin) * 1),
bar_x + ((bar_width + bar_margin) * 2),
][channel.channel - 1]
# Saturation amounts from each sensor
saturation = channel.sensor.saturation
active = channel.sensor.active and channel.enabled
warn_level = channel.warn_level
self._draw.rectangle((x, 0, x + 37, DISPLAY_HEIGHT), (230, 230, 230))
if active:
# Draw background bars
self._draw.rectangle(
(x, int((1.0 - saturation) * DISPLAY_HEIGHT), x + 37, DISPLAY_HEIGHT),
(x, int((1.0 - saturation) * DISPLAY_HEIGHT), x + bar_width - 1, DISPLAY_HEIGHT),
channel.indicator_color(saturation) if active else (229, 229, 229),
)
y = int((1.0 - warn_level) * DISPLAY_HEIGHT)
self._draw.rectangle(
(x, y, x + 37, y), (255, 0, 0) if channel.alarm else (0, 0, 0)
(x, y, x + bar_width - 1, y), (255, 0, 0) if channel.alarm else (0, 0, 0)
)
# Channel selection icons
x += 11
col = channel.indicator_color(saturation, channel.label_colours)
if channel.alarm:
self.icon(icon_alarm, (x - 2, 0), col if active else (129, 129, 129))
else:
self._draw.rectangle(
(x, 2, x + 15, 17),
col if active else (129, 129, 129),
)
x += (bar_width - label_width) // 2
self.icon(icon_channel, (x, label_y), (200, 200, 200) if active else (64, 64, 64))
# TODO: replace number text with graphic
tw, th = self.font.getsize("{}".format(channel.channel))
tw, th = self.font.getsize(str(channel.channel))
self._draw.text(
(x + int(math.ceil(8 - (tw / 2.0))), 2),
"{}".format(channel.channel),
(x + int(math.ceil(8 - (tw / 2.0))), label_y + 1),
str(channel.channel),
font=self.font,
fill=(255, 255, 255) if active else (200, 200, 200),
fill=(55, 55, 55) if active else (100, 100, 100),
)
def render(self):
@@ -233,15 +240,14 @@ class MainView(View):
for channel in self.channels:
self.render_channel(channel)
# Icon backdrops
self._draw.rectangle((0, 0, 19, 19), (32, 138, 251))
# Icons
self.icon(icon_rightarrow, (0, 0), (255, 255, 255))
self.icon(icon_backdrop, (0, 0), COLOR_WHITE)
self.icon(icon_rightarrow, (3, 3), (55, 55, 55))
self.alarm.render((0, DISPLAY_HEIGHT - 19))
self.alarm.render((3, DISPLAY_HEIGHT - 23))
self.icon(icon_settings, (DISPLAY_WIDTH - 19, 0), COLOR_RED)
self.icon(icon_backdrop.rotate(180), (DISPLAY_WIDTH - 26, 0), COLOR_WHITE)
self.icon(icon_settings, (DISPLAY_WIDTH - 19 - 3, 3), (55, 55, 55))
class EditView(View):
@@ -257,7 +263,8 @@ class EditView(View):
View.__init__(self, image)
def render(self):
self.label("X", "Done", textcolor=COLOR_WHITE, bgcolor=COLOR_RED)
self.icon(icon_backdrop.rotate(180), (DISPLAY_WIDTH - 26, 0), COLOR_WHITE)
self.icon(icon_return, (DISPLAY_WIDTH - 19 - 3, 3), (55, 55, 55))
option = self._options[self._current_option]
title = option["title"]
@@ -272,25 +279,27 @@ class EditView(View):
self.label(
"Y",
"Yes" if mode == "bool" else "++",
textcolor=COLOR_WHITE,
bgcolor=COLOR_YELLOW,
textcolor=COLOR_BLACK,
bgcolor=COLOR_WHITE,
)
self.label(
"B",
"No" if mode == "bool" else "--",
textcolor=COLOR_WHITE,
bgcolor=COLOR_BLUE,
textcolor=COLOR_BLACK,
bgcolor=COLOR_WHITE,
)
else:
self.label("B", "Next", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE)
self.label("Y", "Change", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW)
self.label("B", "Next", textcolor=COLOR_BLACK, bgcolor=COLOR_WHITE)
self.label("Y", "Change", textcolor=COLOR_BLACK, bgcolor=COLOR_WHITE)
self.icon(icon_help, (0, 0), COLOR_BLUE)
self._draw.text((3, 43), f"{title} : {text}", font=self.font, fill=(0, 0, 0))
self._draw.text((3, 36), f"{title} : {text}", font=self.font, fill=COLOR_WHITE)
if self._help_mode:
self.overlay(help)
self.icon(icon_backdrop.rotate(90), (0, 0), COLOR_BLUE)
self._draw.rectangle((7, 3, 23, 19), COLOR_BLACK)
self.overlay(help, top=26)
self.icon(icon_help, (0, 0), COLOR_BLUE)
def button_a(self):
self._help_mode = not self._help_mode
@@ -366,10 +375,10 @@ class SettingsView(EditView):
def render(self):
self.clear()
self._draw.text(
(23, 3),
(28, 5),
"Settings",
font=self.font,
fill=(0, 0, 0),
fill=COLOR_WHITE,
)
EditView.render(self)
@@ -381,16 +390,14 @@ class ChannelView(View):
self.channel = channel
View.__init__(self, image)
def draw_status(self, subtle=False):
status = f"{self.channel.sensor.saturation * 100:.2f}% ({self.channel.sensor.moisture:.2f}Hz)"
self._draw.rectangle((0, 20, DISPLAY_WIDTH, 40), (50, 50, 50))
def draw_status(self, position):
status = f"Sat: {self.channel.sensor.saturation * 100:.2f}%"
self._draw.text(
(3, 23),
position,
status,
font=self.font,
fill=(150, 150, 150) if subtle else (255, 255, 255),
fill=(255, 255, 255),
)
@@ -402,65 +409,87 @@ class DetailView(ChannelView):
"""
def render(self):
width, height = self._image.size
self._draw.rectangle((0, 0, width, height), (255, 255, 255))
self.clear()
self._draw.text(
(23, 3),
"{}".format(self.channel.title),
font=self.font,
fill=(0, 0, 0),
)
if self.channel.enabled:
graph_height = DISPLAY_HEIGHT - 8 - 20
graph_width = DISPLAY_WIDTH - 64
self.draw_status()
graph_x = (DISPLAY_WIDTH - graph_width) // 2
graph_y = 8
graph_height = DISPLAY_HEIGHT - 40
self._draw.rectangle(
(0, DISPLAY_HEIGHT - graph_height, DISPLAY_WIDTH, DISPLAY_HEIGHT),
(10, 10, 10),
)
self.draw_status((graph_x, graph_y + graph_height + 4))
for x, value in enumerate(self.channel.sensor.history[:DISPLAY_WIDTH]):
self._draw.rectangle((graph_x, graph_y, graph_x + graph_width, graph_y + graph_height), (50, 50, 50))
for x, value in enumerate(self.channel.sensor.history[:graph_width]):
color = self.channel.indicator_color(value)
h = value * graph_height
x = DISPLAY_WIDTH - x - 1
self._draw.rectangle((x, DISPLAY_HEIGHT - h, x + 1, DISPLAY_HEIGHT), color)
x = graph_x + graph_width - x - 1
self._draw.rectangle((x, graph_y + graph_height - h, x + 1, graph_y + graph_height), color)
alarm_line = int(self.channel.warn_level * graph_height)
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,
DISPLAY_HEIGHT - alarm_line,
graph_height + 8 - alarm_line,
DISPLAY_WIDTH - 40,
DISPLAY_HEIGHT - alarm_line,
graph_height + 8 - alarm_line,
),
(r, 0, 0),
)
self._draw.rectangle(
(
DISPLAY_WIDTH - 20,
DISPLAY_HEIGHT - alarm_line,
graph_height + 8 - alarm_line,
DISPLAY_WIDTH,
DISPLAY_HEIGHT - alarm_line,
graph_height + 8 - alarm_line,
),
(r, 0, 0),
)
self.icon(
icon_alarm,
(DISPLAY_WIDTH - 40, DISPLAY_HEIGHT - alarm_line - 10),
(DISPLAY_WIDTH - 40, graph_height + 8 - alarm_line - 10),
(r, 0, 0),
)
# Channel icons
x_positions = [40, 72, 104]
label_x = x_positions[self.channel.channel - 1]
label_y = 0
active = self.channel.sensor.active and self.channel.enabled
for x in x_positions:
self.icon(icon_channel, (x, label_y - 10), (16, 16, 16))
self.icon(icon_channel, (label_x, label_y), (200, 200, 200))
tw, th = self.font.getsize(str(self.channel.channel))
self._draw.text(
(label_x + int(math.ceil(8 - (tw / 2.0))), label_y + 1),
str(self.channel.channel),
font=self.font,
fill=(55, 55, 55) if active else (100, 100, 100),
)
# Next button
self._draw.rectangle((0, 0, 19, 19), COLOR_BLUE)
self.icon(icon_rightarrow, (0, 0), COLOR_WHITE)
self.icon(icon_backdrop, (0, 0), COLOR_WHITE)
self.icon(icon_rightarrow, (3, 3), (55, 55, 55))
# Prev button
# self.icon(icon_backdrop, (0, DISPLAY_HEIGHT - 26), COLOR_WHITE)
# self.icon(icon_return, (3, DISPLAY_HEIGHT - 26 + 3), (55, 55, 55))
# Edit
self.label("X", "Edit", textcolor=(255, 255, 255), bgcolor=COLOR_RED)
self.icon(icon_backdrop.rotate(180), (DISPLAY_WIDTH - 26, 0), COLOR_WHITE)
self.icon(icon_settings, (DISPLAY_WIDTH - 19 - 3, 3), (55, 55, 55))
class ChannelEditView(ChannelView, EditView):
@@ -515,29 +544,15 @@ class ChannelEditView(ChannelView, EditView):
def render(self):
self.clear()
self._draw.text(
(23, 3), "{}".format(self.channel.title), font=self.font, fill=(0, 0, 0)
)
EditView.render(self)
if not self._help_mode:
self.draw_status(True)
class Channel:
bar_colours = [
(192, 225, 254), # Blue
(196, 255, 209), # Green
(255, 243, 192), # Yellow
(254, 192, 192), # Red
]
label_colours = [
colors = [
COLOR_BLUE,
COLOR_GREEN,
COLOR_YELLOW,
COLOR_RED,
COLOR_RED
]
def __init__(
@@ -603,22 +618,23 @@ class Channel:
self._dry_point = dry_point
self.sensor.set_dry_point(dry_point)
def indicator_color(self, value, r=None):
def warn_color(self):
value = self.sensor.moisture
def indicator_color(self, value):
value = 1.0 - value
if r is None:
r = self.bar_colours
if value == 1.0:
return r[-1]
return self.colors[-1]
if value == 0.0:
return r[0]
return self.colors[0]
value *= len(r) - 1
value *= len(self.colors) - 1
a = int(math.floor(value))
b = a + 1
blend = float(value - a)
r, g, b = [int(((r[b][i] - r[a][i]) * blend) + r[a][i]) for i in range(3)]
r, g, b = [int(((self.colors[b][i] - self.colors[a][i]) * blend) + self.colors[a][i]) for i in range(3)]
return (r, g, b)
@@ -716,6 +732,7 @@ class Alarm(View):
if self._sleep_until is not None:
if self._sleep_until > time.time():
return
self._sleep_until = None
if (
self.enabled
@@ -743,7 +760,7 @@ class Alarm(View):
def render(self, position=(0, 0)):
x, y = position
# Draw the snooze icon- will be pulsing red if the alarm state is True
self._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
if self._triggered and self._sleep_until is None:
r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 128) + 127
@@ -778,6 +795,10 @@ class ViewController:
self._current_view = 0
self._current_subview = 0
@property
def home(self):
return self._current_view == 0 and self._current_subview == 0
def next_subview(self):
view = self.views[self._current_view]
if isinstance(view, tuple):
@@ -790,6 +811,12 @@ class ViewController:
self._current_view %= len(self.views)
self._current_subview = 0
def prev_view(self):
if self._current_subview == 0:
self._current_view -= 1
self._current_view %= len(self.views)
self._current_subview = 0
def get_current_view(self):
view = self.views[self._current_view]
if isinstance(view, tuple):
@@ -812,7 +839,7 @@ class ViewController:
self.next_view()
def button_b(self):
return self.view.button_b()
self.view.button_b()
def button_x(self):
if not self.view.button_x():
@@ -904,6 +931,7 @@ def main():
if label == "B": # Sleep Alarm
if not viewcontroller.button_b():
if viewcontroller.home:
if alarm.sleeping():
alarm.cancel_sleep()
else: