10 Commits

6 changed files with 201 additions and 61 deletions

View File

@@ -45,24 +45,57 @@ All the configuration can be done in the [config.json](config.json) file in JSON
| Attribute name (on top level in config.json) | Type | Description | Default | | 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) | 2 | | `"LOG_LEVEL"` | int | defines up to which log level to show log messages in the serial console: warn (0), info (1), debug (2) | `2` |
| "STARTUP\_WELCOME\_SHOW" | bool | show the startup screen? | true | | `"STARTUP_WELCOME_SHOW"` | bool | show the startup screen? | `true` |
| "STARTUP\_PROJECT\_NAME" | str | the name shown at the welcome/startup screen | " UV-Belichter " | | `"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_MESSAGE_STARTING"` | str | the message shown at startup when starting | `"Starting..."` |
| "STARTUP\_PROJECT\_FINISHED" | str | the message shown at startup when finished | " Started! " | | `"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 | | `"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_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_BTN_2"` | dict | as above | `{"pin": 14, "pull": "down"}` |
| "PIN\_IN\_SWITCH" | dict | as above | {"pin": 13, "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_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_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 | | `"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_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_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\_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 | | `"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!
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!
## Used libraries ## Used libraries
@@ -71,6 +104,17 @@ Note that this software has it's own small wrapper for the config file, e.g. to
- [lcdMenu](https://git.privacynerd.de/BlueFox/lcdMenu) - used for the menus - [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 ## License
This project is licensed under GPL-3.0-or-later. See [LICENSE](LICENSE). This project is licensed under GPL-3.0-or-later. See [LICENSE](LICENSE).

View File

@@ -15,4 +15,7 @@
"LCD_I2C_ADDR": 39, "LCD_I2C_ADDR": 39,
"LCD_I2C_NUM_ROWS": 2, "LCD_I2C_NUM_ROWS": 2,
"LCD_I2C_NUM_COLS": 16, "LCD_I2C_NUM_COLS": 16,
"TIMER_1_DURATION": 60,
"TIMER_2_DURATION": 2400,
"TIMER_3_DURATION": 2700,
} }

View File

@@ -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 @ Feature: Fades "HELLO" over two lines in and out
""" """
import machine
import time 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)) 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)) 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 padding = " " # 16 spaces
line2 = padding + line2 + padding line2 = padding + line2 + padding
line1 = padding + line1 + padding line1 = padding + line1 + padding
@@ -44,7 +31,7 @@ def right2left(line1, line2, speed=0.3):
time.sleep(speed) time.sleep(speed)
def top2bottom(line1, line2, speed=0.2): def top2bottom(lcd, line1, line2, speed=0.2):
lcd.clear() lcd.clear()
time.sleep(speed) time.sleep(speed)
lcd.putstr(line2) lcd.putstr(line2)
@@ -61,7 +48,7 @@ def top2bottom(line1, line2, speed=0.2):
lcd.clear() lcd.clear()
time.sleep(speed) time.sleep(speed)
def showAll(waitAfter=0.5): def showAll(lcd, waitAfter=0.5):
lcd.clear() lcd.clear()
lcd.putstr(line1) lcd.putstr(line1)
lcd.move_to(0,1) lcd.move_to(0,1)
@@ -69,11 +56,21 @@ def showAll(waitAfter=0.5):
time.sleep(waitAfter) time.sleep(waitAfter)
def run(): # for the ProgramChooser as callback def run(lcd): # for the ProgramChooser as callback
right2left(line1, line2, 0.2) lcd.custom_char(0, bytearray([0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03])) # right
top2bottom(line1, line2, 0.2) lcd.custom_char(1, bytearray([0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18])) # left
showAll() 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__": if __name__ == "__main__":
run() # run once from utils import Config
cfg = Config()
run(cfg.LCD) # run once

109
main.py
View File

@@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with thi
import utils import utils
from lcdMenu import lcdMenu from lcdMenu import lcdMenu
from WelcomeScreen import WelcomeScreen from WelcomeScreen import WelcomeScreen
from time import sleep from time import sleep, time_ns
from gc import collect # garbage collector for better memory performance from gc import collect # garbage collector for better memory performance
config = utils.Config() config = utils.Config()
@@ -30,28 +30,69 @@ def manual():
set_value = config.PIN_OUT_RELAIS.value() set_value = config.PIN_OUT_RELAIS.value()
config.LCD.putstr(f"---- MANUAL ---- State: {set_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: if config.PIN_IN_BTN_1.value() == 1 or config.PIN_IN_BTN_2.value() == 1:
return True # exit on press of these buttons return True # exit on press of these buttons; True to disable the Quitting message from lcdMenu
def timer(): def timer():
# display WIP timer_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="TIMERS")
timer_values = [config.TIMER_1_DURATION, config.TIMER_2_DURATION, config.TIMER_3_DURATION]
timer_splits = [lambda: divmod(round(timer_values[0]), 60), lambda: divmod(round(timer_values[1]), 60), lambda: divmod(round(timer_values[2]), 60)]
def run_timer(timer_number: int, _timer: int):
interrupt_pin = config.PIN_IN_BTN_1
reset_pin = config.PIN_IN_BTN_2
start_stop_pin = config.PIN_IN_SWITCH
original = _timer
config.LCD.clear() config.LCD.clear()
config.LCD.putstr(" Still work-in-progress") config.LCD.putstr(f"Timer {timer_number}".center(16))
sleep(3) last_start_stop_value = start_stop_pin.value()
config.PIN_OUT_RELAIS.value(last_start_stop_value)
last_time_ns = time_ns()
while True:
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 = original # 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"{timer_splits[0]()[0]:02d}:{timer_splits[0]()[1]:02d}", lambda: run_timer(1, timer_values[0])),
(f"{timer_splits[1]()[0]:02d}:{timer_splits[1]()[1]:02d}", lambda: run_timer(2, timer_values[1])),
(f"{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()
del timer_menu, timer_programs
collect()
return True # disable the "Quitting" message from lcdMenu return True # disable the "Quitting" message from lcdMenu
def uv_on(): def uv_on():
config.RELAIS.value(1) config.PIN_OUT_RELAIS.value(1)
config.LCD.clear() config.LCD.clear()
config.LCD.putstr("------ UV ------ turned on ") config.LCD.putstr("------ UV ------ turned on ")
sleep(1) sleep(1)
return True # disable the "Quitting" message from lcdMenu return True # disable the "Quitting" message from lcdMenu
def uv_off(): def uv_off():
config.RELAIS.value(0) config.PIN_OUT_RELAIS.value(0)
config.LCD.clear() config.LCD.clear()
config.LCD.putstr("------ UV ------ turned off ") config.LCD.putstr("------ UV ------ turned off ")
sleep(1) sleep(1)
return True # disable the "Quitting" message from lcdMenu return True # disable the "Quitting" message from lcdMenu
def lcd_big_hello(): def lcd_big_hello():
import lcd_big_hello as lbh import lcd_big_hello as lbh
lbh.run() lbh.run(config.LCD)
del lbh del lbh
collect() collect()
return True return True
@@ -61,10 +102,56 @@ def input_tests():
collect() collect()
return True return True
def settings(): def settings():
# display WIP settings_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="SETTINGS")
def swap_welcome():
config.STARTUP_WELCOME_SHOW = not config.STARTUP_WELCOME_SHOW
config.LCD.clear() config.LCD.clear()
config.LCD.putstr(" Still work-in-progress") config.LCD.putstr(f" Welcome Screen ")
sleep(3) if config.STARTUP_WELCOME_SHOW:
config.LCD.putstr("--- Enabled! ---")
else:
config.LCD.putstr("--- Disabled ---")
sleep(1)
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)} ^")
while True:
if config.PIN_IN_BTN_1.value() == 1:
time_ns_when_pressed = time_ns()
while config.PIN_IN_BTN_1.value() == 1:
if (time_ns() - time_ns_when_pressed) > 1000000000: # if the time passed is longer than a second
while config.PIN_IN_BTN_1.value() == 1:
pass # wait till release
config.STARTUP_WELCOME_CYCLES = current_cycles
return True
sleep(0.05)
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 config.PIN_IN_BTN_2.value() == 1:
time_ns_when_pressed = time_ns()
while config.PIN_IN_BTN_2.value() == 1:
if (time_ns() - time_ns_when_pressed) > 1000000000: # if the time passed is longer than a second
while config.PIN_IN_BTN_2.value() == 1:
pass # wait till release
config.STARTUP_WELCOME_CYCLES = current_cycles
return True
sleep(0.05)
current_cycles += 1
option_down = [" ", "v"][current_cycles>1]
config.LCD.putstr(f" Cycles \n{option_down} {str(current_cycles).center(12)} ^")
settings_programs = [("Show welcome", swap_welcome),
("Welcome cycles", welcome_cycles),
("Exit", settings_menu.stop)]
settings_menu.setup(settings_programs) # give it the callback list
settings_menu.run() # run the menu until it's closed
del settings_menu, settings_programs
collect()
return True return True
def run_demo_menu(): def run_demo_menu():
demo_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="DEMOS") demo_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="DEMOS")

View File

@@ -39,7 +39,10 @@ class Config:
"LCD_I2C_ADDR", # the i2c adress of the display "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_ROWS", # how many rows for character display has the display?
"LCD_I2C_NUM_COLS", # and how many characters can it display per row? "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) "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._config_file = config_file
self.load_config() self.load_config()
@@ -54,7 +57,7 @@ class Config:
def save_config(self): def save_config(self):
with open(self._config_file, "w") as f: with open(self._config_file, "w") as f:
from json import dump from json import dump
dump(self._config, f) dump(self._config, f, separators=(',\n', ': '))
del dump del dump
collect() collect()
@@ -102,14 +105,21 @@ class Config:
def __setattr__(self, name, value): def __setattr__(self, name, value):
#print(f"Someone tried to edit my poor attributes! Affected: '{name}' should be set to '{value}'") if name.startswith("_"): # make private attributes settable as normal
object.__setattr__(self, name, value) 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): def __delattr__(self, name):
raise AttributeError(f"You may not delete any attribute of the '{self.__class__.__name__}' object") raise AttributeError(f"You may not delete any attribute of the '{self.__class__.__name__}' object")
cfg = Config()
""" """
Very simple logging function Very simple logging function
@@ -123,4 +133,3 @@ def log(log_level: int, message: str):
print(f"{message}") print(f"{message}")
elif cfg.LOG_LEVEL >= log_level: # if log level is valid elif cfg.LOG_LEVEL >= log_level: # if log level is valid
print(f"[{log_mapping[log_level]}] {message}") print(f"[{log_mapping[log_level]}] {message}")