Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
7443af2095
|
|||
20331b81bc
|
|||
eef6f8e2cc
|
|||
3c97b96435
|
36
README.md
36
README.md
@@ -5,7 +5,7 @@ A collection of programs run on a Raspberry Pi Pico to control a uv exposure uni
|
||||
|
||||
## 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 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 **16x2 display** (maybe 20x4 or others do also work, but these are not tested) can show information to the user.
|
||||
A **relais** is used for switching all the LEDs.
|
||||
|
||||
| Device Pin | Pi Pico Pin |
|
||||
@@ -30,19 +30,45 @@ To install this, use [Thonny](https://thonny.org/), open all the files present i
|
||||
|
||||
## 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/)).
|
||||
|
||||
|
||||
### 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) | 2 |
|
||||
| "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 |
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
## License
|
||||
|
18
config.json
Normal file
18
config.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"LOG_LEVEL": 2,
|
||||
"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,
|
||||
}
|
65
config.py
65
config.py
@@ -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)
|
@@ -9,13 +9,14 @@ 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.BTN_1.value() and cfg.BTN_2.value() and cfg.SWITCH.value():
|
||||
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:
|
||||
@@ -23,9 +24,9 @@ def run(endless_loop: bool = True, serial_output: bool = True):
|
||||
sleep(0.2)
|
||||
break
|
||||
cfg.LCD.move_to(0,0)
|
||||
cfg.LCD.putstr(f"In: Y{cfg.BTN_1.value()} | G{cfg.BTN_2.value()} | S{cfg.SWITCH.value()}Push all to exit")
|
||||
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.BTN_1.value()}; G_BTN: {cfg.BTN_2.value()}; Lever: {cfg.SWITCH.value()}")
|
||||
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__":
|
||||
|
88
main.py
88
main.py
@@ -14,83 +14,87 @@ import utils
|
||||
from lcdMenu import lcdMenu
|
||||
from WelcomeScreen import WelcomeScreen
|
||||
from time import sleep
|
||||
import gc # garbage collector for better memory performance
|
||||
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 manual():
|
||||
utils.cfg.LCD.clear()
|
||||
set_value = utils.cfg.RELAIS.value()
|
||||
utils.cfg.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
|
||||
config.LCD.clear()
|
||||
set_value = config.PIN_OUT_RELAIS.value()
|
||||
config.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
|
||||
while True:
|
||||
if set_value != utils.cfg.SWITCH.value():
|
||||
utils.cfg.RELAIS.value(utils.cfg.SWITCH.value())
|
||||
set_value = utils.cfg.RELAIS.value()
|
||||
utils.cfg.LCD.putstr(f"---- MANUAL ---- State: {set_value} ")
|
||||
if utils.cfg.BTN_1.value() == 1 or utils.cfg.BTN_2.value() == 1:
|
||||
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
|
||||
def timer():
|
||||
# display WIP
|
||||
utils.cfg.LCD.clear()
|
||||
utils.cfg.LCD.putstr(" Still work-in-progress")
|
||||
config.LCD.clear()
|
||||
config.LCD.putstr(" Still work-in-progress")
|
||||
sleep(3)
|
||||
return True # disable the "Quitting" message from lcdMenu
|
||||
def uv_on():
|
||||
utils.cfg.RELAIS.value(1)
|
||||
utils.cfg.LCD.clear()
|
||||
utils.cfg.LCD.putstr("------ UV ------ turned on ")
|
||||
config.RELAIS.value(1)
|
||||
config.LCD.clear()
|
||||
config.LCD.putstr("------ UV ------ turned on ")
|
||||
sleep(1)
|
||||
return True # disable the "Quitting" message from lcdMenu
|
||||
def uv_off():
|
||||
utils.cfg.RELAIS.value(0)
|
||||
utils.cfg.LCD.clear()
|
||||
utils.cfg.LCD.putstr("------ UV ------ turned off ")
|
||||
config.RELAIS.value(0)
|
||||
config.LCD.clear()
|
||||
config.LCD.putstr("------ UV ------ turned off ")
|
||||
sleep(1)
|
||||
return True # disable the "Quitting" message from lcdMenu
|
||||
def lcd_big_hello():
|
||||
import lcd_big_hello
|
||||
lcd_big_hello.run()
|
||||
gc.collect()
|
||||
import lcd_big_hello as lbh
|
||||
lbh.run()
|
||||
del lbh
|
||||
collect()
|
||||
return True
|
||||
def input_tests():
|
||||
import input_tests as input_tests
|
||||
input_tests.run(serial_output=False)
|
||||
gc.collect()
|
||||
collect()
|
||||
return True
|
||||
def settings():
|
||||
# display WIP
|
||||
utils.cfg.LCD.clear()
|
||||
utils.cfg.LCD.putstr(" Still work-in-progress")
|
||||
config.LCD.clear()
|
||||
config.LCD.putstr(" Still work-in-progress")
|
||||
sleep(3)
|
||||
return True
|
||||
def run_demo_menu():
|
||||
demo_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="DEMOS")
|
||||
demo_programs = [("LCD Demo", lcd_big_hello),
|
||||
("Input tests", input_tests),
|
||||
("Exit", demo_menu.stop)]
|
||||
demo_menu.setup(demo_programs) # give it the callback list
|
||||
ret = demo_menu.run()
|
||||
del demo_menu, demo_programs
|
||||
collect()
|
||||
return ret
|
||||
|
||||
|
||||
if utils.cfg.STARTUP_WELCOME_SHOW:
|
||||
ws = WelcomeScreen(utils.cfg.LCD,
|
||||
interrupt_pins=[utils.cfg.BTN_1, utils.cfg.BTN_2, utils.cfg.SWITCH],
|
||||
subtitle=utils.cfg.STARTUP_PROJECT_NAME,
|
||||
starting_msg=utils.cfg.STARTUP_MESSAGE_STARTING,
|
||||
started_msg=utils.cfg.STARTUP_MESSAGE_FINISHED)
|
||||
ws.show(cycles=utils.cfg.STARTUP_WELCOME_CYCLES)
|
||||
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
|
||||
gc.collect()
|
||||
collect()
|
||||
|
||||
|
||||
# create the menus
|
||||
btn_mapping = {"ok_btn": utils.cfg.BTN_1, "next_btn": utils.cfg.BTN_2} # the btn mapping for all menus
|
||||
|
||||
demo_menu = lcdMenu(utils.cfg.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="DEMOS")
|
||||
demo_programs = [("LCD Demo", lcd_big_hello),
|
||||
("Input tests", input_tests),
|
||||
("Exit", demo_menu.stop)]
|
||||
demo_menu.setup(demo_programs) # give it the callback list
|
||||
|
||||
main_menu = lcdMenu(utils.cfg.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="PROGRAMS")
|
||||
main_menu = lcdMenu(config.LCD, btn_mapping, scroll_direction=True, cycle=True, hide_menu_name=False, name="PROGRAMS")
|
||||
main_programs = [("Timer", timer),
|
||||
("Manual", manual),
|
||||
("UV off", uv_off),
|
||||
("UV on", uv_on),
|
||||
("Demos", demo_menu.run),
|
||||
("Demos", run_demo_menu),
|
||||
("Settings", settings)]
|
||||
main_menu.setup(main_programs) # give it the callback list
|
||||
|
||||
|
101
utils.py
101
utils.py
@@ -9,9 +9,106 @@ 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)
|
||||
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)
|
||||
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):
|
||||
#print(f"Someone tried to edit my poor attributes! Affected: '{name}' should be set to '{value}'")
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
raise AttributeError(f"You may not delete any attribute of the '{self.__class__.__name__}' object")
|
||||
|
||||
cfg = Config()
|
||||
|
||||
|
||||
"""
|
||||
|
Reference in New Issue
Block a user