Compare commits

...

35 Commits
v0.1 ... main

Author SHA1 Message Date
a97dd88a67
Added a cover image 2024-11-16 17:38:38 +01:00
92ebbd2ef8
Small improvements 2024-11-16 17:00:54 +01:00
68a588de58
Removed the compatibility note with 4x20 displays as this could be done with some tweaks, but is unnecessary for me 2024-11-16 16:42:13 +01:00
91a58cca61
Restructured main.py for better readability 2024-11-16 16:38:54 +01:00
002b01d572
Added license texts at the beginning of all programs started from the main menu 2024-11-16 16:38:09 +01:00
aba5927221
Did a huge refactoring and outsouring for modularity and a small performance improvement 2024-11-16 16:31:33 +01:00
a6b531c394
Fixed typos, improved understandability 2024-11-16 15:43:07 +01:00
72bc662cb8
Improved the welcome screen enable/disable dialog 2024-11-16 15:41:10 +01:00
e1075f8d84
Small improvements in the settings menu 2024-11-16 15:27:55 +01:00
908a5e39be
Changed the log level default to 1 (info) 2024-11-16 15:11:13 +01:00
5ce8e592c3
Added another demo, this one showing off the lcd driver functionality 2024-11-16 15:09:04 +01:00
ddde9480ff
Fixed bug in the logging method 2024-11-16 15:08:18 +01:00
c7a8cda7d1
Added support for changing the log level in the settings menu 2024-11-16 12:58:35 +01:00
7adcdb1624
Implemented reset functionality in the settings menu 2024-11-16 12:47:09 +01:00
f0d6b49d6e
Improved the timers menu with timer numbering 2024-11-16 12:23:06 +01:00
a4adaac9b4
Implemented support for setting the timer durations, fixed some minor issues and cleaned up / streamlined the menus 2024-11-16 12:14:43 +01:00
78ced9e7ea
Updated submodules, implemented timer functionality, fixed configuration issue 2024-11-15 23:13:03 +01:00
eb31a92cea
removed unnecessary doubles in the links section 2024-11-15 19:54:21 +01:00
fd2a4117b3
Added useful links section to README.md 2024-11-15 19:52:37 +01:00
864a833a43
[README.md] Fixed wrong displayment of some data in the previously changed table 2024-11-15 19:41:43 +01:00
678ebfe7aa
Added new config variables for timer functions 2024-11-15 19:39:32 +01:00
9d395bd2b8
Fixed small bugs, and added fundamental settings 2024-11-15 19:32:25 +01:00
78933f6090
Fixed some small issues with some programs still using the old configuration way 2024-11-15 18:38:33 +01:00
f48315f18e
Fixed small typo in README.md 2024-11-15 18:26:58 +01:00
2aac36e15c
Added implementation of config changes in the utils.Config class 2024-11-15 18:26:05 +01:00
aa3110d564
Added small instructions on the setting of configuration changes 2024-11-15 18:24:22 +01:00
f03cc4a44c
Added note in the installation instructions to clone the repository properly, making sure submodules get populated 2024-11-15 17:50:09 +01:00
cbc5298cda
Added lcdMenu as a submodule 2024-11-15 17:47:19 +01:00
98353475ea
Preparing for including lcdMenu as a submodule 2024-11-15 17:46:43 +01:00
7443af2095
Fixed the 'used libraries' section and made it up to date 2024-11-15 16:01:18 +01:00
20331b81bc
Added a configuration list to README.md 2024-11-15 15:57:07 +01:00
eef6f8e2cc
Changed the times sign in README.md to an x for improved readibility / consistency 2024-11-15 15:34:23 +01:00
3c97b96435
Moved to json as the configuration format 2024-11-15 15:31:37 +01:00
533deb2833
Now using lcdMenu as the ProgramChooser library has been retired 2024-11-15 13:23:20 +01:00
355821a6a9 Added the WelcomeScreen library 2024-10-28 21:09:23 +01:00
18 changed files with 909 additions and 287 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lib/lcdMenu"]
path = lib/lcdMenu
url = https://git.privacynerd.de/BlueFox/lcdMenu.git

View File

@ -2,18 +2,20 @@
A collection of programs run on a Raspberry Pi Pico to control a uv exposure unit (mainly used for exposing PCBs)
![The front of the exposure unit](cover.png)
## Hardware
This program is strongly customized to my needs, and my DIY exposure unit has **two buttons** and **one switch** to interact with the software (and a power switch FWIW). Also, a **16*2 display** (maybe 20*4 or others do also work, but these are not tested) can show information to the user.
This software is strongly customized to my needs, and my DIY exposure unit has **two buttons** and **one switch** to interact with the software (and a power switch FWIW). Also, a **16x2 display** can show information to the user.
A **relais** is used for switching all the LEDs.
| Device Pin | Pi Pico Pin |
| ------------------ | ----------- |
| BTN_1 Pin 1 | 3.3V |
| BTN_1 Pin 2 | GPIO15 |
| BTN_2 Pin 1 | 3.3V |
| BTN_2 Pin 2 | GPIO14 |
| BTN\_1 Pin 1 | 3.3V |
| BTN\_1 Pin 2 | GPIO15 |
| BTN\_2 Pin 1 | 3.3V |
| BTN\_2 Pin | GPIO14 |
| SWITCH Pin 1 | 3.3V |
| SWITCH Pin 2 | GPIO13 |
| LCD SDA | GPIO8 |
@ -23,26 +25,98 @@ A **relais** is used for switching all the LEDs.
| Relais control pin | GPIO21 |
## Installation
## Software installation
To install this, use [Thonny](https://thonny.org/), open all the files present in this repository and then save them onto a [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/) (or [Pico 2](https://www.raspberrypi.com/products/raspberry-pi-pico-2/), but it's not tested on this platform yet) running [MicroPython](https://micropython.org/). Then, given you followed above wiring, it should just be running! Then you can jump over the configuration section.
To install this software on your Pi Pico, first clone the repository with `git clone --recurse-submodules` to populate the submodules also (some libraries are included as submodules). Then use [Thonny](https://thonny.org/), open all the files present in this repository and then save them onto a [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/) (or [Pico 2](https://www.raspberrypi.com/products/raspberry-pi-pico-2/), but it's not tested on this platform yet) running [MicroPython](https://micropython.org/). Then, given you followed above wiring, it should just be running! Then you can jump over the configuration section.
## Configuration
## First configuration
All the configuration can be done in the [config.py](config.py) file, just have a look around there. Some hints for editing this file:
All the configuration can be done in the [config.json](config.json) file in JSON format just have a look around there. Some hints for editing this file:
- When editing the startup section strings, make sure the "STARTUP_PROJECT_NAME" and the "STARTUP_MESSAGE_FINISHED" values have the same length as your display can show (likely 16 characters). Otherwise it could be that some things won't be displayed correctly.
- For a description of all of the attributes, see below
- When editing the startup section strings, make sure the "STARTUP\_PROJECT\_NAME" and the "STARTUP\_MESSAGE\_FINISHED" values have the same length as your display can show (likely 16 characters). Otherwise it could be that some things won't be displayed correctly.
- When changing Pins in the Pinout section, make sure to follow the pinout assignment of your Pi Pico board (e.g. the i2c sda and scl pins)
- If your display doesn't work properly - the first issue might be a wrong i2c address. To find the address of your display, just follow some of the tutorials on the internet on scanning for i2c devices (e.g. [here](https://randomnerdtutorials.com/raspberry-pi-pico-i2c-scanner-micropython/)).
- The most basic configuration changes can be made directly from the device, without the need of connecting it to a PC, essentially making it a kind-of standalone device once flashed!
### Attribute table
| Attribute name (on top level in config.json) | Type | Description | Default |
| -------------------------------------------- | ---- | ----------- | ------- |
| `"LOG_LEVEL"` | int | defines up to which log level to show log messages in the serial console: warn (0), info (1), debug (2) | `1` |
| `"STARTUP_WELCOME_SHOW"` | bool | show the startup screen? | `true` |
| `"STARTUP_PROJECT_NAME"` | str | the name shown at the welcome/startup screen | `" UV-Belichter "` |
| `"STARTUP_MESSAGE_STARTING"` | str | the message shown at startup when starting | `"Starting..."` |
| `"STARTUP_PROJECT_FINISHED"` | str | the message shown at startup when finished | `" Started! "` |
| `"STARTUP_WELCOME_CYCLES"` | int | how often the starting string shall go over the welcome screen | `1` |
| `"PIN_IN_BTN_1"` | dict | the dict must contain the "pin" and "pull" keys with respective values | `{"pin": 15, "pull": "down"}` |
| `"PIN_IN_BTN_2"` | dict | as above | `{"pin": 14, "pull": "down"}` |
| `"PIN_IN_SWITCH"` | dict | as above | `{"pin": 13, "pull": "down"}` |
| `"PIN_OUT_RELAIS"` | int | pin number where the relais is connected | `21` |
| `"PIN_SDA"` | int | the pin number of the sda wire connected to the lcd | `8` |
| `"PIN_SCL"` | int | the pin number of the scl wire connected to the lcd | `9` |
| `"LCD_I2C_CH"` | int | the channel of the i2c bus used | `0` |
| `"LCD_I2C_ADDR"` | int | the i2c address of the lcd; make sure to convert hexadecimal to decimal numbers | `38` |
| `"LCD_I2C_NUM_ROWS"` | int | how many rows for character display has the display? | `2` |
| `"LCD_I2C_NUM_COLS"` | int | and how many characters can it display per row? | `16` |
| `"TIMER_1_DURATION"` | int | the timer duration of the first timer; IN SECONDS | `60` (1min) |
| `"TIMER_2_DURATION"` | int | as above, but of the seconds timer; IN SECONDS | `2400` (40min) |
| `"TIMER_3_DURATION"` | int | as above, but of the third timer; IN SECONDS | `2700` (45min) |
Note that this software has it's own small wrapper for the config file, e.g. to have instant access to an LCD object generated on the fly. These are all documented in the [utils.py](utils.py) file! When setting configuration options from your custom code, keep in mind that doing this via the `Config().<ATTR_NAME>` way just means writing the value directly to the file, while getting it goes through the wrapper to make e.g. the pin a machine.Pin object. But you just can't write a pin back into an attribute.
This will NOT work:
```python
from utils import Config
from machine import Pin
cfg = Config()
btn1 = cfg.PIN_IN_BTN_1
# Now we don't like this setting anymore
new_btn1 = Pin(10, Pin.IN, Pin.PULL_DOWN)
cfg.PIN_IN_BTN_1 = new_btn1
```
Instead, you have to do it that way:
```python
from utils import Config
from machine import Pin
cfg = Config
btn1 = cfg.PIN_IN_BTN_1
# Now we don't like it so we write the new pin in our json format
cfg.PIN_IN_BTN_1 = {"pin": 10, "pull": "down"}
```
So, as the `Config` class uses python's magic function for getting and setting attributes, the process of changing some config is not completely intuitive, but when keeping that in mind, it's very handy!
## Used libraries
- PCF8574T - a driver for the i2c multiplexer used to address the 2x16 lcd display
- [ProgramChooserAdapted](https://git.privacynerd.de/BlueFox/ProgramChooser) - an adapted version of ProgramChooser (but I'm planning to merge my changes to the upstream Programchooser)
- [PCF8574T](https://git.privacynerd.de/BlueFox/micropython-libraries/src/branch/main/PCF8574T) - a driver for the i2c multiplexer used to address the 2x16 lcd display
- [WelcomeScreen](https://git.privacynerd.de/BlueFox/WelcomeScreen) - used to display a small welcome message in the beginning
- [lcdMenu](https://git.privacynerd.de/BlueFox/lcdMenu) - used for the menus
## Learning curve
Here are some of the websites I learnt a lot from while programming this project - mainly here for documentation reasons.
- [A handy guide into configuration files in json](https://bhave.sh/micropython-json-config/)
- [The official micropython wiki page about the same topic](https://docs.micropython.org/en/latest/library/json.html)
- [StackOverflow thread about how to format a json file (used by this project to make it a bit more readable)](https://stackoverflow.com/questions/16311562/python-json-without-whitespaces)
- [A guide about magic functions for settings and getting attributes](https://staskoltsov.medium.com/python-magic-methods-to-get-set-attributes-716a12d0b106)
- [The official guide into git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
## License

21
config.json Normal file
View File

@ -0,0 +1,21 @@
{
"LOG_LEVEL": 1,
"STARTUP_WELCOME_SHOW": true,
"STARTUP_PROJECT_NAME":" UV-Belichter ",
"STARTUP_MESSAGE_STARTING": "Starting...",
"STARTUP_MESSAGE_FINISHED": " Started! ",
"STARTUP_WELCOME_CYCLES": 1,
"PIN_IN_BTN_1": {"pin": 15, "pull": "down"},
"PIN_IN_BTN_2": {"pin": 14, "pull": "down"},
"PIN_IN_SWITCH": {"pin": 13, "pull": "down"},
"PIN_OUT_RELAIS": 21,
"PIN_SDA": 8,
"PIN_SCL": 9,
"LCD_I2C_CH": 0,
"LCD_I2C_ADDR": 39,
"LCD_I2C_NUM_ROWS": 2,
"LCD_I2C_NUM_COLS": 16,
"TIMER_1_DURATION": 60,
"TIMER_2_DURATION": 2400,
"TIMER_3_DURATION": 2700,
}

View File

@ -1,65 +0,0 @@
"""
uv-belichter-software - Configuration file
Copyright (C) 2024 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
"""
---------------------------
----- LOGGING SECTION -----
---------------------------
"""
LOG_LEVEL = 2 # there are three log levels: warn (0), info (1), debug (2)
# this value defines which log messages to show
# e.g. 2 means show [debug], [warn] and [info] messages
"""
---------------------------
----- STARTUP SECTION -----
---------------------------
"""
STARTUP_PROJECT_NAME = " UV-Belichter " # the name to show at startup
STARTUP_MESSAGE_STARTING = "Starting..." # the message to show at startup
STARTUP_MESSAGE_FINISHED = " Started! " # the message to show at startup
STARTUP_WELCOME_SHOW = True # show the name and a startup message
STARTUP_WELCOME_CYCLES = 1 # how often shall "Starting..." run over the screen
"""
--------------------------
----- PINOUT SECTION -----
--------------------------
"""
from machine import Pin
BTN_1 = Pin(15, Pin.IN, Pin.PULL_DOWN) # input of the first btn
BTN_2 = Pin(14, Pin.IN, Pin.PULL_DOWN) # input of the second btn
SWITCH = Pin(13, Pin.IN, Pin.PULL_DOWN) # input of switch
LCD_SDA = Pin(8) # just some standard I2C serial data (SDA) outputs (on I2C channel 0 on Pi Pico)
LCD_SCL = Pin(9) # just some standard I2C serial clock (SCL) outputs (on I2C channel 0 on Pi Pico)
#LCD_SDA = Pin(16) # another pinout (soldered on the original project's circuit board)
#LCD_SCL = Pin(17) # another pinout (soldered on the original project's circuit board)
RELAIS = Pin(21, Pin.OUT) # where the relais is connected (for the UV lights)
"""
-----------------------
----- LCD SECTION -----
-----------------------
"""
from machine import I2C, Pin
from lib.PCF8574T import I2C_LCD
LCD_I2C_ADDR = 0x27 # the i2c adress of the display (yours might be different to this one)
LCD_I2C_NUM_ROWS = 2 # how many rows for character display has the display?
LCD_I2C_NUM_COLS = 16 # and how many characters can it display per row?
LCD = I2C_LCD(I2C(0, sda=LCD_SDA, scl=LCD_SCL, freq=400000),
LCD_I2C_ADDR,
LCD_I2C_NUM_ROWS,
LCD_I2C_NUM_COLS)

BIN
cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -9,11 +9,25 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import config as cfg
from utils import Config
from time import sleep
cfg = Config()
def run(endless_loop: bool = True, serial_output: bool = True):
while endless_loop:
if cfg.PIN_IN_BTN_1.value() and cfg.PIN_IN_BTN_2.value() and cfg.PIN_IN_SWITCH.value():
cfg.LCD.move_to(0,0)
cfg.LCD.putstr("In: Y1 | G1 | S1 Exiting! ")
if serial_output:
print("All configured inputs pressed! Exiting...")
sleep(0.2)
break
cfg.LCD.move_to(0,0)
cfg.LCD.putstr(f"In: Y{cfg.PIN_IN_BTN_1.value()} | G{cfg.PIN_IN_BTN_2.value()} | S{cfg.PIN_IN_SWITCH.value()}Push all to exit")
if serial_output:
print(f"Y_BTN: {cfg.PIN_IN_BTN_1.value()}; G_BTN: {cfg.PIN_IN_BTN_2.value()}; Lever: {cfg.PIN_IN_SWITCH.value()}")
sleep(0.05)
if __name__ == "__main__":
while True:
print(f"Y_BTN: {cfg.BTN_1.value()}; G_BTN: {cfg.BTN_2.value()}; Lever: {cfg.SWITCH.value()}")
sleep(0.1)
run() # run the program

View File

@ -1,5 +1,5 @@
"""
An example "program" which can be used with the ProgramChooser library, see also main.py
An example "program" which can be used with the lcdMenu library, see also main.py
Copyright (C) 2024 Benjamin Burkhardt <bluefox@privacynerd.de>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -14,26 +14,13 @@ You should have received a copy of the GNU General Public License along with thi
@ Feature: Fades "HELLO" over two lines in and out
"""
import machine
import time
import config
lcd = config.LCD
lcd.custom_char(0, bytearray([0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03])) # right
lcd.custom_char(1, bytearray([0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18])) # left
lcd.custom_char(2, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0x1F])) # 2-down
lcd.custom_char(3, bytearray([0x1F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00])) # 2-up
lcd.custom_char(4, bytearray([0x1F,0x1F,0x00,0x00,0x00,0x00,0x00,0x1F])) # e-up
lcd.custom_char(5, bytearray([0x1F,0x00,0x00,0x00,0x00,0x00,0x1F,0x1F])) # e-down
lcd.custom_char(6, bytearray([0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0x1F])) # l-down
lcd.custom_char(7, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18])) # l-right
line1 = str(chr(0) + chr(2) + chr(1) + chr(0) + chr(4) + " " + chr(1) + " " + chr(1) + " " + chr(0) + chr(3) + chr(1))
line2 = str(chr(0) + chr(3) + chr(1) + chr(0) + chr(5) + " " + chr(6) + chr(7) + chr(6) + chr(7) + chr(0) + chr(2) + chr(1))
def right2left(line1, line2, speed=0.3):
def right2left(lcd, line1, line2, speed=0.3):
padding = " " # 16 spaces
line2 = padding + line2 + padding
line1 = padding + line1 + padding
@ -44,7 +31,7 @@ def right2left(line1, line2, speed=0.3):
time.sleep(speed)
def top2bottom(line1, line2, speed=0.2):
def top2bottom(lcd, line1, line2, speed=0.2):
lcd.clear()
time.sleep(speed)
lcd.putstr(line2)
@ -61,7 +48,7 @@ def top2bottom(line1, line2, speed=0.2):
lcd.clear()
time.sleep(speed)
def showAll(waitAfter=0.5):
def showAll(lcd, waitAfter=0.5):
lcd.clear()
lcd.putstr(line1)
lcd.move_to(0,1)
@ -69,11 +56,21 @@ def showAll(waitAfter=0.5):
time.sleep(waitAfter)
def run(): # for the ProgramChooser as callback
right2left(line1, line2, 0.2)
top2bottom(line1, line2, 0.2)
showAll()
def run(lcd): # for the ProgramChooser as callback
lcd.custom_char(0, bytearray([0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03])) # right
lcd.custom_char(1, bytearray([0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18])) # left
lcd.custom_char(2, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0x1F])) # 2-down
lcd.custom_char(3, bytearray([0x1F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00])) # 2-up
lcd.custom_char(4, bytearray([0x1F,0x1F,0x00,0x00,0x00,0x00,0x00,0x1F])) # e-up
lcd.custom_char(5, bytearray([0x1F,0x00,0x00,0x00,0x00,0x00,0x1F,0x1F])) # e-down
lcd.custom_char(6, bytearray([0x18,0x18,0x18,0x18,0x18,0x18,0x1F,0x1F])) # l-down
lcd.custom_char(7, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18])) # l-right
right2left(lcd, line1, line2, 0.2)
top2bottom(lcd, line1, line2, 0.2)
showAll(lcd)
if __name__ == "__main__":
run() # run once
from utils import Config
cfg = Config()
run(cfg.LCD) # run once

64
demos/lcd_driver_demo.py Normal file
View File

@ -0,0 +1,64 @@
"""
An example "program" which can be used with the lcdMenu library on a 2x16 display (with some tweaks also on 4x20 displays)
Copyright (C) 2024 Benjamin Burkhardt <bluefox@privacynerd.de>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
"""
@ Feature: Demonstrates the lcd driver's functionality
"""
from time import sleep
from utils import log
# lcd is an object of the I2C_LCD class (https://git.privacynerd.de/BlueFox/micropython-libraries/src/branch/main/PCF8574T)
def run(lcd):
# Show off basic functionality of the lcd driver
log(2, "Running the lcd driver demo")
lcd.clear()
lcd.putstr("Driver demo".center(16)+"running...".center(16))
sleep(1)
lcd.clear()
lcd.putstr("Lorem ipsum dolor sit amet")
sleep(1)
lcd.show_cursor()
sleep(1)
lcd.hide_cursor()
sleep(1)
lcd.blink_cursor_on()
sleep(1)
lcd.blink_cursor_off()
sleep(1)
lcd.backlight_off()
sleep(1)
lcd.backlight_on()
sleep(1)
lcd.display_off()
sleep(1)
lcd.display_on()
sleep(1)
lcd.clear()
sleep(1)
s = ""
for x in range(32, 127):
s += chr(x)
while len(s) > 0:
lcd.clear()
lcd.move_to(0,0)
lcd.putstr(s[:32])
s = s[32:]
sleep(1)
lcd.clear()
return True
if __name__ == "__main__":
import utils
run(utils.Config().LCD)

View File

@ -1,93 +0,0 @@
"""
ProgramChooser - a program launcher for microPython (adapted to the UV Belichter needs)
Copyright (C) 2024 Benjamin Burkhardt <bluefox@privacynerd.de>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import utils, config
from PCF8574T import I2C_LCD
import time
class ProgramChooser:
def __init__(self, programs, debug=False, run_directly=False):
self.lcd = config.LCD
self.ok_btn = config.BTN_1
self.next_btn = config.BTN_2
self.lcd.move_to(0,0)
self.lcd.putstr("[ PROGRAMS ]< >")
self.current_selection = None # no selection
self.programs = programs # a dictionary of programs and it's callbacks e.g. {"lora_test": some_callback}
self.show_selection()
if run_directly: self.run()
def log(self, msg, is_debug=False):
if is_debug:
utils.log(2, f"[ProgramChooser] {msg}")
else:
utils.log(1, f"[ProgramChooser] {msg}")
def show_selection(self):
self.lcd.move_to(1,1)
if len(self.programs) == 0:
self.lcd.putstr(" No programs!")
return True
if self.current_selection == None: # set it initially
self.current_selection = 0
# the actual displaying process
to_show = list(self.programs.keys())[self.current_selection]
if len(to_show) > 14:
to_show = to_show[:13] + chr(0)
else:
to_show = to_show[:14]
to_show = to_show.center(14)
self.lcd.putstr(to_show)
return True
def run(self):
while True:
if self.next_btn.value() == 1:
former_program_name = list(self.programs.keys())[self.current_selection]
self.current_selection = (self.current_selection+1)%len(list(self.programs.keys()))
self.show_selection()
now_program_name = list(self.programs.keys())[self.current_selection]
self.log(f"Selected next program (\"{former_program_name}\" -> \"{now_program_name}\")")
while self.next_btn.value() == 1: time.sleep(0.01) # wait till release
if self.ok_btn.value() == 1:
program_name = list(self.programs.keys())[self.current_selection]
self.log(f"Running selected program! (\"{program_name}\")")
# shorten the name for displaying (if too long)
if len(program_name) > 14:
program_name = program_name[:13] + chr(0)
else:
program_name = program_name[:14]
program_name = program_name.center(14)
self.lcd.move_to(0,0)
self.lcd.putstr(f" {program_name} Executing... ")
self.execute_selection()
while self.ok_btn.value() == 1: time.sleep(0.01) # wait till release (e.g. if the "program" is a simple send action)
self.lcd.putstr(f" {program_name} Closing... ")
time.sleep(1)
self.lcd.move_to(0,0)
self.lcd.putstr("[ PROGRAMS ]< >")
self.show_selection()
time.sleep(0.01)
def execute_selection(self): # execute the current selected program's callback
self.programs[list(self.programs.keys())[self.current_selection]]()

View File

@ -0,0 +1,126 @@
"""
WelcomeScreen: A simple library providing a customizable welcome screen fading over an LCD
Copyright (C) 2024 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from time import sleep
class WelcomeScreen:
# __init__() - the constructor
# lcd: an object of I2C_LCD (from the PCF8574T library - https://git.privacynerd.de/BlueFox/micropython-libraries/src/branch/main/PCF8574T)
# ---
# interrupt_pins: a list containing machine.Pin objects exclusively (but if you give these you can still turn interrupting off
# via a parameter in the show method (see below!)
# ---
# subtitle: the text shown below the cycling text (e.g. the device's name, ...) (Lorem ipsum. by default)
# ---
# starting_msg: the text shown while cycling (default: Starting...)
# ---
# started_msg: the text shown while cycling (default: Started!)
def __init__(self, lcd, interrupt_pins=None, subtitle="Lorem ipsum.", starting_msg="Starting...", started_msg="Started!"):
self.lcd = lcd
self.columns = self.lcd.num_columns
self.lines = self.lcd.num_lines
self.interrupt_pins = interrupt_pins
self.subtitle = subtitle
self.starting_msg = starting_msg
self.started_msg = started_msg
# show() - Display the actual message
# ---
# cycles says how often the Starting text goes through
# ---
# wait_after_cycles is a number in seconds (can be float) defining how long to wait before returning OR
# if fading out is activated, the time to wait between end of the cycling animation and the fade out animation
# ---
# fade_down is a dict with following keys:
# - "enabled" - REQUIRED - wether fading is enabled (default: true)
# - "wait_between" - OPTIONAL - the time in seconds to wait between each line fade
# - "wait_after" - OPTIONAL - the time in seconds to wait after the fade out
# ---
# interruptable influences wether the program can be interrupted by pins in the self.interrupt_pins list
# if this list is empty, even when interruptable is set to true, nothing will be able to interrupt!
def show(self, cycles=1, wait_after_cycles=1, fade_down={"enabled": True}, interruptable=True):
if cycles < 1: cycles = 1
padding = " " * self.columns # as much spaces as padding as one display line is long
padding_hyphen = "-" * self.columns # as much hyphens as padding as one display line is long
# mechanism for centering on a 4*20 display
y_offset = 0
if self.lines == 4:
y_offset = 1
# also clear the top and bottom with ----
self.lcd.move_to(0,0)
self.lcd.putstr(padding_hyphen)
self.lcd.move_to(0,4)
self.lcd.putstr(padding_hyphen)
# get the current pin values (only if there are pins specified) (when something changes, the interrupt happens and the cycle stops)
break_flag = False
if self.interrupt_pins:
pin_values = []
for p in self.interrupt_pins:
pin_values.append(p.value())
# cycle the text 'cycles' times and listen for changes on interrupt pins (if any given)
for i in range(cycles):
line1 = padding + self.starting_msg + padding
line2 = self.subtitle.center(self.columns)
for i in range(self.columns + len(self.starting_msg)):
self.lcd.move_to(0,y_offset)
self.lcd.putstr(line1[0:self.columns])
self.lcd.move_to(0,y_offset+1)
self.lcd.putstr(line2[0:self.columns])
line1 = line1[1:]
if self.interrupt_pins:
for i, p in enumerate(self.interrupt_pins):
if pin_values[i] != p.value():
break_flag = True
if break_flag:
break
if break_flag:
break
self.lcd.move_to(0,y_offset)
self.lcd.putstr(self.started_msg.center(16))
sleep(wait_after_cycles)
# now fade down if enabled via the params
if fade_down["enabled"]:
# get all the waiting times
if "wait_between" in fade_down.keys():
wait_between = fade_down["wait_between"]
else:
wait_between = 0.1
if "wait_after" in fade_down.keys():
wait_after = fade_down["wait_after"]
else:
wait_after = 0.3
if self.lines == 4:
old_display = padding_hyphen + self.started_msg.center(16) + self.subtitle.center(self.columns) + padding_hyphen
else:
old_display = self.started_msg.center(16) + self.subtitle.center(self.columns)
self.lcd.move_to(0,0) # move to the start of the lcd
while old_display != "":
old_display = old_display[:-self.columns]
lines_before = " " * (self.columns*self.lines-len(old_display))
self.lcd.putstr(lines_before + old_display)
sleep(wait_between)
sleep(wait_after)

1
lib/lcdMenu Submodule

@ -0,0 +1 @@
Subproject commit 4b7e5723ca1c2d4b18e452561b2c5a025e489eb2

101
main.py
View File

@ -10,61 +10,62 @@ You should have received a copy of the GNU General Public License along with thi
"""
import config, utils
from lib.ProgramChooserAdapted import ProgramChooser
from time import sleep
import gc # garbage collector for better memory performance
import utils
from lcdMenu import lcdMenu
from WelcomeScreen import WelcomeScreen
from gc import collect # garbage collector for better memory performance
config = utils.Config()
btn_mapping = {"ok_btn": config.PIN_IN_BTN_1, "next_btn": config.PIN_IN_BTN_2} # the btn mapping for all lcdMenus
# extra functions to access the garbage collector
def timers():
import programs.timers as t
t.run(config, btn_mapping, utils.log, lcdMenu)
del t
collect()
return True
def manual():
config.LCD.clear()
set_value = config.RELAIS.value()
config.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
while True:
if set_value != config.SWITCH.value():
config.RELAIS.value(config.SWITCH.value())
set_value = config.RELAIS.value()
config.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
if config.BTN_1.value() == 1 or config.BTN_2.value() == 1:
return # exit on press of these buttons
def timer():
# display WIP
config.LCD.clear()
config.LCD.putstr(" Still work-in-progress")
sleep(3)
def uv_on():
config.RELAIS.value(1)
config.LCD.clear()
config.LCD.putstr(" UV turned on ")
sleep(1)
def uv_off():
config.RELAIS.value(0)
config.LCD.clear()
config.LCD.putstr("------ UV ------ turned off ")
sleep(1)
def lcd_big_hello():
import lcd_big_hello
lcd_big_hello.run()
gc.collect()
import programs.manual as m
m.run(config)
del m
collect()
return True
def demos():
import programs.demos as d
d.run(config, btn_mapping, utils.log, lcdMenu)
del d
collect()
return True
def settings():
# display WIP
config.LCD.clear()
config.LCD.putstr(" Still work-in-progress")
sleep(3)
# create a programs dict, where the items are callables (functions)
programs = {
"Settings": settings,
"LCD Demo": lcd_big_hello,
"UV off": uv_off,
"UV on": uv_on,
"Timer": timer,
"Manual": manual,
}
import programs.settings as s
s.run(config, btn_mapping, utils.log, lcdMenu)
del s
collect()
return True
# create the main menu
main_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="PROGRAMS")
main_programs = [("Timers", timers),
("Manual", manual),
("Demos", demos),
("Settings", settings)]
main_menu.setup(main_programs) # give it the callback list
if config.STARTUP_WELCOME_SHOW: utils.show_welcome()
# -------
pc = ProgramChooser(programs) # initialize the ProgramChooser
pc.run() # and run it (will be an endless loop)
# run the welcome screen as defined in the config file
if config.STARTUP_WELCOME_SHOW:
ws = WelcomeScreen(config.LCD,
interrupt_pins=[config.PIN_IN_BTN_1, config.PIN_IN_BTN_2, config.PIN_IN_SWITCH],
subtitle=config.STARTUP_PROJECT_NAME,
starting_msg=config.STARTUP_MESSAGE_STARTING,
started_msg=config.STARTUP_MESSAGE_FINISHED)
ws.show(cycles=config.STARTUP_WELCOME_CYCLES)
del ws
collect()
# and run the main menu (will be an endless loop)
main_menu.run()

52
programs/demos.py Normal file
View File

@ -0,0 +1,52 @@
"""
uv-belichter-software - the DEMOS submenu
Copyright (C) 2024 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from gc import collect
# All the arguments this method takes are there for one reason: reduce the amount of libraries loaded into the picos memory and thus improving performance
# config: utils.Config object
# btn_mapping: a dict containing the btn mapping for the menu operation (given to the lcdMenu object)
# log: the utils.log function
# lcdMenu: the lcdMenu class (not an object of that class!)
def run(config, btn_mapping, log, lcdMenu):
def lcd_driver_demo():
import demos.lcd_driver_demo as ldd
ldd.run(config.LCD)
del ldd
collect()
return True
def lcd_big_hello():
import demos.lcd_big_hello as lbh
lbh.run(config.LCD)
del lbh
collect()
return True
def input_tests():
import demos.input_tests as it
it.run(serial_output=False)
del it
collect()
return True
demo_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="DEMOS")
demo_programs = [("LCD Demo", lcd_driver_demo),
("Hello world", lcd_big_hello),
("Input tests", input_tests),
("Exit", demo_menu.stop)]
demo_menu.setup(demo_programs) # give it the callback list
demo_menu.run()
return True
if __name__ == "__main__":
from utils import Config, log
from lcdMenu import lcdMenu
config = Config()
btn_mapping = {"ok_btn": config.PIN_IN_BTN_1, "next_btn": config.PIN_IN_BTN_2} # the btn mapping for all lcdMenus
run(Config(), btn_mapping, log, lcdMenu)

28
programs/manual.py Normal file
View File

@ -0,0 +1,28 @@
"""
uv-belichter-software - the MANUAL program (started from the main menu)
Copyright (C) 2024 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def run(config):
config.LCD.clear()
set_value = config.PIN_OUT_RELAIS.value()
config.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
while True:
if set_value != config.PIN_IN_SWITCH.value():
config.PIN_OUT_RELAIS.value(config.PIN_IN_SWITCH.value())
set_value = config.PIN_OUT_RELAIS.value()
config.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
if config.PIN_IN_BTN_1.value() == 1 or config.PIN_IN_BTN_2.value() == 1:
return True # exit on press of these buttons; True to disable the Quitting message from lcdMenu
if __name__ == "__main__":
from utils import Config, log
config = Config()
btn_mapping = {"ok_btn": config.PIN_IN_BTN_1, "next_btn": config.PIN_IN_BTN_2} # the btn mapping for all lcdMenus
run(Config(), btn_mapping, log)

254
programs/settings.py Normal file
View File

@ -0,0 +1,254 @@
"""
uv-belichter-software - the SETTINGS submenu
Copyright (C) 2024 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from time import sleep, time_ns
# All the arguments this method takes are there for one reason: reduce the amount of libraries loaded into the picos memory and thus improving performance
# config: utils.Config object
# btn_mapping: a dict containing the btn mapping for the menu operation (given to the lcdMenu object)
# log: the utils.log function
# lcdMenu: the lcdMenu class (not an object of that class!)
def run(config, btn_mapping, log, lcdMenu):
settings_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="SETTINGS")
def toggle_show_welcome():
current_value = config.STARTUP_WELCOME_SHOW
config.LCD.clear()
config.LCD.putstr(f"Currently {'on' if current_value else 'off'}".center(16))
config.LCD.putstr("< keep change >")
keep_btn = config.PIN_IN_BTN_1
change_btn = config.PIN_IN_BTN_2
while True:
if keep_btn.value() == 1:
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Stay {'on' if current_value else 'off'}!".center(16))
sleep(0.5)
return True
if change_btn.value() == 1:
config.STARTUP_WELCOME_SHOW = not current_value
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Turned {'on' if not current_value else 'off'}!".center(16))
sleep(0.5)
return True
def welcome_cycles():
config.LCD.clear()
current_cycles = config.STARTUP_WELCOME_CYCLES
option_down = [" ", "v"][current_cycles>1]
config.LCD.putstr(f" Cycles \n{option_down} {str(current_cycles).center(12)} ^")
btn_left = config.PIN_IN_BTN_1
btn_right = config.PIN_IN_BTN_2
while True:
if btn_left.value() == 1:
sleep(0.1) # this value is a good compromise between being able to press both buttons and a fast up/down speed
if btn_right.value() == 1:
config.STARTUP_WELCOME_CYCLES = current_cycles
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Saved!".center(16)) # show a little info that it is now set
sleep(0.5)
while btn_right.value() == 1 or btn_left.value() == 1: # wait till both btns are released
pass
return True
current_cycles -= 1
if current_cycles < 1: current_cycles = 1
option_down = [" ", "v"][current_cycles>1]
config.LCD.putstr(f" Cycles \n{option_down} {str(current_cycles).center(12)} ^")
if btn_right.value() == 1:
sleep(0.1)
if btn_left.value() == 1:
config.STARTUP_WELCOME_CYCLES = current_cycles
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Saved!".center(16)) # show a little info that it is now set
sleep(0.5)
while btn_right.value() == 1 or btn_left.value() == 1: # wait till both btns are released
pass
return True
current_cycles += 1
option_down = [" ", "v"][current_cycles>1]
config.LCD.putstr(f" Cycles \n{option_down} {str(current_cycles).center(12)} ^")
# with n being the number of the timer (starting at 0)
def set_n_timer(n: int):
# a small helper function to save the value of the n'th timer...
# as you can't programatically access TIMER_n_DURATION
def set_timer_helper(n, value):
if n == 0:
config.TIMER_1_DURATION = value
elif n == 1:
config.TIMER_2_DURATION = value
elif n == 2:
config.TIMER_3_DURATION = value
else:
log(0, "There are only 3 timers at all. Trying to set the timer number {n} failed.")
config.LCD.clear()
timer_values = [config.TIMER_1_DURATION, config.TIMER_2_DURATION, config.TIMER_3_DURATION]
current_timer = timer_values[n] # get the n'th timer
current_timer_div = lambda: divmod(current_timer, 60)
config.LCD.putstr(f"Timer {n+1}".center(16))
config.LCD.putstr(f"{'v' if current_timer > 1 else ' '} " + f"{current_timer_div()[0]:02d}:{current_timer_div()[1]:02d}".center(12) + " ^")
btn_left = config.PIN_IN_BTN_1
btn_right = config.PIN_IN_BTN_2
left_was_released = True
right_was_released = True
while True:
if btn_left.value() == 1:
if left_was_released:
time_press_start = time_ns()
left_was_released = False
sleep(0.1) # this value is a good compromise between being able to press both buttons and a fast up/down speed
if btn_right.value() == 1: # exit if both btns are pressed
set_timer_helper(n, current_timer)
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Saved!".center(16)) # show a little info that it is now set
sleep(0.5)
while btn_right.value() == 1 or btn_left.value() == 1: # wait till both btns are released
pass
return True
# define the step width
time_now = time_ns()
if (time_now - time_press_start) <= 1*(10**9): # max. 1 seconds pressed
current_timer -= 1
elif (time_now - time_press_start) <= 2*(10**9): # max. 2 seconds pressed
current_timer -= 5
elif (time_now - time_press_start) <= 3*(10**9): # max. 3 seconds pressed
current_timer -= 10
elif (time_now - time_press_start) <= 4*(10**9): # max. 4 seconds pressed
current_timer -= 30
elif (time_now - time_press_start) <= 5*(10**9): # max. 5 seconds pressed
current_timer -= 60
else: # longer than 5s pressed
current_timer -= 300
if current_timer < 1: current_timer = 5999
config.LCD.move_to(0,1)
config.LCD.putstr("v " + f"{current_timer_div()[0]:02d}:{current_timer_div()[1]:02d}".center(12) + " ^")
else:
left_was_released = True
if btn_right.value() == 1:
if right_was_released:
time_press_start = time_ns()
right_was_released = False
sleep(0.1)
if btn_left.value() == 1: # exit if both btns are pressed
set_timer_helper(n, current_timer)
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Saved!".center(16)) # show a little info that it is now set
sleep(0.5)
while btn_right.value() == 1 or btn_left.value() == 1: # wait till both btns are released
pass
return True
# define the step width
time_now = time_ns()
if (time_now - time_press_start) <= 1*(10**9): # max. 1 seconds pressed
current_timer += 1
elif (time_now - time_press_start) <= 2*(10**9): # max. 2 seconds pressed
current_timer += 5
elif (time_now - time_press_start) <= 3*(10**9): # max. 3 seconds pressed
current_timer += 10
elif (time_now - time_press_start) <= 4*(10**9): # max. 4 seconds pressed
current_timer += 30
elif (time_now - time_press_start) <= 5*(10**9): # max. 5 seconds pressed
current_timer += 60
else: # longer than 5s pressed
current_timer += 300
if current_timer > 5999: current_timer = 1
config.LCD.move_to(0,1)
config.LCD.putstr("v " + f"{current_timer_div()[0]:02d}:{current_timer_div()[1]:02d}".center(12) + " ^")
else:
right_was_released = True
def set_log_level():
config.LCD.clear()
current_level = config.LOG_LEVEL
log_levels = ["WARN", "INFO", "DEBUG"]
config.LCD.putstr(f"Log level".center(16))
config.LCD.putstr("v " + f"{log_levels[current_level]} ({current_level})".center(12) + " ^") # show the log level and it's name in the second row
btn_left = config.PIN_IN_BTN_1
btn_right = config.PIN_IN_BTN_2
while True:
if btn_left.value() == 1:
sleep(0.1) # this value is a good compromise between being able to press both buttons and a fast up/down speed
if btn_right.value() == 1:
config.LOG_LEVEL = current_level
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Saved!".center(16)) # show a little info that it is now set
sleep(0.5)
while btn_right.value() == 1 or btn_left.value() == 1: # wait till both btns are released
pass
return True
current_level -= 1
if current_level < 0: current_level = 2
config.LCD.move_to(0,0)
config.LCD.putstr(f"Log level".center(16))
config.LCD.putstr("v " + f"{log_levels[current_level]} ({current_level})".center(12) + " ^") # show the log level and it's name in the second row
if btn_right.value() == 1:
sleep(0.1)
if btn_left.value() == 1:
config.LOG_LEVEL = current_level
config.LCD.move_to(0,1) # move to the second row
config.LCD.putstr(f"Saved!".center(16)) # show a little info that it is now set
sleep(0.5)
while btn_right.value() == 1 or btn_left.value() == 1: # wait till both btns are released
pass
return True
current_level += 1
if current_level > 2: current_level = 0
config.LCD.move_to(0,0)
config.LCD.putstr(f"Log level".center(16))
config.LCD.putstr("v " + f"{log_levels[current_level]} ({current_level})".center(12) + " ^") # show the log level and it's name in the second row
def reset(): # reset all user-settable configuration to the default values
config.LCD.clear()
config.LCD.putstr("Sure about that?")
config.LCD.putstr("< no yes >")
no_btn = config.PIN_IN_BTN_1
yes_btn = config.PIN_IN_BTN_2
while True:
if no_btn.value() == 1:
return None
if yes_btn.value() == 1:
config.LCD.putstr("Resetting...".center(16))
config.LCD.putstr("Welcome Screen".center(16))
config.STARTUP_WELCOME_SHOW = True
config.STARTUP_WELCOME_CYCLES = 1
sleep(0.5)
config.LCD.move_to(0,1)
config.LCD.putstr("Timers".center(16))
config.TIMER_1_DURATION = 60
config.TIMER_2_DURATION = 2400
config.TIMER_3_DURATION = 2700
sleep(0.5)
config.LCD.move_to(0,1)
config.LCD.putstr("Logging".center(16))
config.LOG_LEVEL = 1
sleep(0.5)
return True
settings_programs = [("Show welcome", toggle_show_welcome),
("Welcome cycles", welcome_cycles),
("Timer 1", lambda: set_n_timer(0)),
("Timer 2", lambda: set_n_timer(1)),
("Timer 3", lambda: set_n_timer(2)),
("Log level", set_log_level),
("Reset", reset),
("Exit", settings_menu.stop)]
settings_menu.setup(settings_programs) # give it the callback list
settings_menu.run() # run the menu until it's closed
if __name__ == "__main__":
from utils import Config, log
from lcdMenu import lcdMenu
config = Config()
btn_mapping = {"ok_btn": config.PIN_IN_BTN_1, "next_btn": config.PIN_IN_BTN_2} # the btn mapping for all lcdMenus
run(Config(), btn_mapping, log, lcdMenu)

73
programs/timers.py Normal file
View File

@ -0,0 +1,73 @@
"""
uv-belichter-software - the TIMER submenu
Copyright (C) 2024 Benjamin Burkhardt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from time import sleep, time_ns
# All the arguments this method takes are there for one reason: reduce the amount of libraries loaded into the picos memory and thus improving performance
# config: utils.Config object
# btn_mapping: a dict containing the btn mapping for the menu operation (given to the lcdMenu object)
# log: the utils.log function
# lcdMenu: the lcdMenu class (not an object of that class!)
def run(config, btn_mapping, log, lcdMenu):
timer_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="TIMERS")
timer_values_original = [config.TIMER_1_DURATION, config.TIMER_2_DURATION, config.TIMER_3_DURATION]
timer_values = timer_values_original.copy() # here, the current timers time is stored when interrupting via the interrupt_pin (see below)
timer_splits = [lambda: divmod(round(timer_values[0]), 60), lambda: divmod(round(timer_values[1]), 60), lambda: divmod(round(timer_values[2]), 60)]
interrupt_pin = config.PIN_IN_BTN_1 # the interrupt btn stops the timer, saves the current time and goes back to the menu
reset_pin = config.PIN_IN_BTN_2 # the reset btn restores the default value
start_stop_pin = config.PIN_IN_SWITCH # the start_stop switch starts/stops the timer
# timer_number is the number of the timer that will be run, starting at 1
# the _timer variable is needed because otherwise python will throw crazy errors regarding "variable accessed before assignment"...
# ...just see it as a copy of the timer_number-1'th elemnt of the timer_values list (see above)
def run_timer(timer_number: int, _timer: int):
config.LCD.clear()
config.LCD.putstr(f"Timer {timer_number}".center(16))
last_start_stop_value = start_stop_pin.value()
config.PIN_OUT_RELAIS.value(last_start_stop_value)
last_time_ns = time_ns()
while True: # now run the timer (if the switch is high)
config.LCD.move_to(0,1)
_timer_div = divmod(round(_timer), 60)
config.LCD.putstr(f"{_timer_div[0]:02d}:{_timer_div[1]:02d}".center(16))
if interrupt_pin.value() == 1:
timer_values[timer_number-1] = _timer # save the current state
last_start_stop_value = 0 # turn the LEDs off!
config.PIN_OUT_RELAIS.value(last_start_stop_value)
return None
if reset_pin.value() == 1:
_timer = timer_values_original[timer_number-1] # reset the timers
if _timer <= 0:
config.PIN_OUT_RELAIS.off()
return True
sleep(0.05)
if last_start_stop_value != (new_value := start_stop_pin.value()):
last_start_stop_value = new_value
config.PIN_OUT_RELAIS.value(new_value)
last_time_ns = time_ns()
if start_stop_pin.value() == 1:
_timer -= (time_ns() - last_time_ns) / 1000**3
last_time_ns = time_ns()
timer_programs = [(f"T1 - {timer_splits[0]()[0]:02d}:{timer_splits[0]()[1]:02d}", lambda: run_timer(1, timer_values[0])),
(f"T2 - {timer_splits[1]()[0]:02d}:{timer_splits[1]()[1]:02d}", lambda: run_timer(2, timer_values[1])),
(f"T3 - {timer_splits[2]()[0]:02d}:{timer_splits[2]()[1]:02d}", lambda: run_timer(3, timer_values[2])),
("Exit", timer_menu.stop)]
timer_menu.setup(timer_programs) # give it the callback list
timer_menu.run()
return True # disable the "Quitting" message from lcdMenu
if __name__ == "__main__":
from utils import Config, log
from lcdMenu import lcdMenu
config = Config()
btn_mapping = {"ok_btn": config.PIN_IN_BTN_1, "next_btn": config.PIN_IN_BTN_2} # the btn mapping for all lcdMenus
run(Config(), btn_mapping, log, lcdMenu)

150
utils.py
View File

@ -9,9 +9,116 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from gc import collect
import config as cfg
from time import sleep
"""
A small wrapper class as storing machine.Pin, LCD and machine.I2C objects in a file is not that cool :)
Now only pin numbers and strings etc. are stored on the uC, and the complex objects are generated on the fly
"""
class Config:
"""
The initializer method
- config_file: the path to the config file laying on the uC
"""
def __init__(self, config_file: str = "config.json"):
self._attr_list = ["LOG_LEVEL", # there are three log levels: warn (0), info (1), debug (2)
# this value defines which log messages to show
# e.g. 2 means show [debug], [warn] and [info] messages
"STARTUP_WELCOME_SHOW", # show the name and a startup message
"STARTUP_PROJECT_NAME", # the name to show at startup
"STARTUP_MESSAGE_STARTING", # the message to show at startup
"STARTUP_MESSAGE_FINISHED", # the message to show at startup
"STARTUP_WELCOME_CYCLES", # how often shall "Starting..." run over the screen
"PIN_IN_BTN_1", # input of the first btn
"PIN_IN_BTN_2", # input of the second btn
"PIN_IN_SWITCH", # input of the switch
"PIN_OUT_RELAIS", # where the relais is connected (for the UV lights)
"PIN_SDA", # just some standard I2C serial data (SDA) output
"PIN_SCL", # just some standard I2C serial clock (SCL) output
"LCD_I2C_CH", # where the relais is connected (for the UV lights)
"LCD_I2C_ADDR", # the i2c adress of the display
"LCD_I2C_NUM_ROWS", # how many rows for character display has the display?
"LCD_I2C_NUM_COLS", # and how many characters can it display per row?
"LCD", # the actual lcd object (of the PCF8574T I2C_LCD class, see libraries)
"TIMER_1_DURATION", # the duration of the first timer in seconds
"TIMER_2_DURATION", # the duration of the second first timer in seconds
"TIMER_3_DURATION"] # the duration of the third timer in seconds
self._config_file = config_file
self.load_config()
def load_config(self):
# prepare the class
with open(self._config_file, "r") as f:
from json import load
self._config = load(f)
del load
collect()
def save_config(self):
with open(self._config_file, "w") as f:
from json import dump
dump(self._config, f, separators=(',\n', ': '))
del dump
collect()
def __getattr__(self, name):
if name.startswith("_"): # make private attributes unaccessible
raise AttributeError(f"'Access to the private attribute '{name}' of the object '{self.__class__.__name__}' is forbidden")
elif name in self._attr_list: # valid attributes (only capital letters and -_ etc. are allowed)
try:
# now some if statements to check if the lcd or some pin object is asked
if name.startswith("PIN_"):
from machine import Pin
if name.startswith("PIN_IN"):
if self._config[name]["pull"].lower() == "down":
p = Pin(self._config[name]["pin"], Pin.IN, Pin.PULL_DOWN)
elif self._config[name]["pull"].lower() == "up":
p = Pin(self._config[name]["pin"], Pin.IN, Pin.PULL_UP)
else:
p = Pin(self._config[name]["pin"], Pin.IN)
elif name.startswith("PIN_OUT"):
p = Pin(self._config[name], Pin.OUT)
else:
p = Pin(self._config[name])
del Pin
collect()
return p
elif name == "LCD":
try:
return self._lcd
except:
from machine import I2C, Pin
from PCF8574T import I2C_LCD
self._lcd = I2C_LCD(I2C(self.LCD_I2C_CH, sda=self.PIN_SDA, scl=self.PIN_SCL, freq=400000),
self.LCD_I2C_ADDR,
self.LCD_I2C_NUM_ROWS,
self.LCD_I2C_NUM_COLS)
del I2C, Pin, I2C_LCD
collect()
return self._lcd
return self._config[name]
except KeyError:
raise AttributeError(f"Attribute '{name}' does not exist in the config file '{self._config_file}'")
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
def __setattr__(self, name, value):
if name.startswith("_"): # make private attributes settable as normal
object.__setattr__(self, name, value)
elif name in self._attr_list: # valid attributes (only capital letters and -_ etc. are allowed)
try:
self._config[name] = value
self.save_config()
except KeyError:
raise AttributeError(f"Attribute '{name}' does not exist in the config file '{self._config_file}'")
else:
raise AttributeError(f"Can't set attribute '{name}' for a '{self.__class__.__name__}' object: forbidden")
def __delattr__(self, name):
raise AttributeError(f"You may not delete any attribute of the '{self.__class__.__name__}' object")
"""
@ -24,40 +131,5 @@ def log(log_level: int, message: str):
if log_level not in [0, 1, 2]:
print(f"[LOGGER] Got a message of unknown log level ({log_level}). Original message is printed below.")
print(f"{message}")
elif cfg.LOG_LEVEL >= log_level: # if log level is valid
print(f"[{log_mapping[log_level]}] {message}")
"""
Simple function that displays a startup "welcome" screen
Configurable in config.py
"""
def show_welcome(): # cycles says how often the startup text goes through
cycles = cfg.STARTUP_WELCOME_CYCLES
if cycles < 1:
cycles = 1
padding = " "*cfg.LCD_I2C_NUM_COLS
started_str = cfg.STARTUP_MESSAGE_FINISHED
starting_str = cfg.STARTUP_MESSAGE_STARTING
# slide the first line over the display (animated from right to left)
for i in range(cycles):
line1 = padding + starting_str + padding
line2 = cfg.STARTUP_PROJECT_NAME
for i in range(cfg.LCD_I2C_NUM_COLS + len(starting_str)):
cfg.LCD.putstr(line1[0:16])
cfg.LCD.move_to(0,1)
cfg.LCD.putstr(line2[0:16])
line1 = line1[1:]
cfg.LCD.move_to(0,0)
cfg.LCD.putstr(started_str)
# now fade down
sleep(2)
cfg.LCD.move_to(0,0)
cfg.LCD.putstr(padding + started_str)
sleep(0.1)
cfg.LCD.clear()
elif Config().LOG_LEVEL >= log_level: # if log level is valid
print(f"[{log_mapping[log_level]}] {message}")