diff --git a/.gitignore b/.gitignore index d974ce94..982d4076 100755 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,15 @@ recipes/recipe/ backups/ -bin/ \ No newline at end of file +bin/ +settings-v1.8.0.json + +include/ + +lib/ + +share/ + +lib64 + +pyvenv.cfg diff --git a/README.md b/README.md index b48b4e11..cf846794 100755 --- a/README.md +++ b/README.md @@ -85,11 +85,11 @@ I recommend at least taking a peek at the PiFire overview video below. It cover [Link to our channel on YouTube](https://www.youtube.com/channel/UCYYs50U5QvHHhogx_rqs0Yg) -Here is a the latest version 2.0 of the hardware w/TFT screen and hardware buttons in a custom 3D printed enclosure. We've come a long way since v1.0. +Pictured below is version 2.0 of the hardware w/TFT screen and hardware buttons in a custom 3D printed enclosure. ![Hardware v2](docs/photos/HW-V2-Display.jpg) -And if you're interested in seeing more builds from other users, we have a discussions thread [here](https://github.com/nebhead/PiFire/discussions/28) where others have posted pictures of their unique builds. +And if you're interested in seeing more builds from other users, we have a discussions thread [here](https://github.com/nebhead/PiFire/discussions/28) or on [Discord](https://discord.gg/F9mbCrbrZS) (see below) where others have posted pictures of their unique builds. ## Full Documentation / Hardware and Software Installation @@ -109,12 +109,12 @@ I've added a discord server [here](https://discord.gg/F9mbCrbrZS) which can be a * 10/2022 - Release v1.3.5 - Bug fixes, feature refinements and brand new features galore in this latest release. Added a new ADS1115 module (using Adafruits Circuit Python), due to some reports of issues with the existing ADS1115 module. These can be optionally selected in the configuration wizard. PWM Fan Support and a boatload of code cleanup was introduced, thanks to contributor @weberbox. Support for saving cook files was introduced in this version, so that you can go back to older cooks, edit some of the information and add images and comments. Added a Prime Mode to allow you to prime the fire pot with pellets prior to a cook, and even prime & startup. Added estimated pellet usage to the pellet manager, which will attempt to track just how many pellets you have used since your last load of pellets. Added Apprise notification capability thanks to contributor @calonmerc. 320x200 displays have been update and added timers to specific modes. And even more! * 6/2023 - Release v1.5.0 - Arguably one of the biggest overhauls to PiFire since it's inception. The Probe system has been completely refactored to allow for multiple probe sensing devices (i.e. ADS1115, MAX31865, or even Virtual Probes to augment your inputs). This extension of the probe system, allows for any number of probe inputs to be tracked in PiFire, allowing from notifications and tracking of history for each probe. The sky is the limit! With this change the the probe architecture, a number of other things needed to be modified/updated, including the notification system, the history/charting, the dashboards, cookfiles and recipe modes. Note that if you are updating to this version, your settings will be upgraded in the process and you will not be able to roll back to a previous version (unless you restore from a backup of your settings). * 11/2023 - Release v1.6.0 - In this month comes another huge update with lots of new features and bug fixes. Many thanks to the users from discord that have been testing along the way (as well as submitting some bugs), what a great community we have! Many of these features have been deployed on our development branch for some time, so they should be relatively stable. Please do file issues on GitHub if you find any new bugs with the formal release. With that, enjoy and happy grilling/smoking! -* **5/2024** - Release v1.7.0 - Lot's of new updates in this release with the UI, new features (i.e. exit startup temp, etc.) and new device support. Some improvements to the tuning tools (including an auto-tuning tool). Under the covers improvements for stability and cleanup. As usual, submit issues to GitHub if you run into anything. Enjoy! - +* 5/2024 - Release v1.7.0 - Lot's of new updates in this release with the UI, new features (i.e. exit startup temp, etc.) and new device support. Some improvements to the tuning tools (including an auto-tuning tool). Under the covers improvements for stability and cleanup. As usual, submit issues to GitHub if you run into anything. Enjoy! +* **9/2024 - Release v1.8.0** - Overhaul of the configuration wizard and underlying platform pin definitions to support board selection and configuration versus the platform selection that was previously provided. This allows much more flexibility when it comes to custom pinouts. This version now supports the PCB v4.x modular design allowing the ability to mix and match probe devices, relay/fan hardware, etc. ### Credits -Web Application created by Ben Parmeter, copyright 2020-2023. Check out my other projects on [github](https://github.com/nebhead). If you enjoy this software and feel the need to donate a cup of coffee, a frosty beer or a bottle of wine to the developer you can click [here](https://paypal.me/benparmeter). +Web Application created by Ben Parmeter, copyright 2020-2024. Check out my other projects on [github](https://github.com/nebhead). If you enjoy this software and feel the need to donate a cup of coffee, a frosty beer or a bottle of wine to the developer you can click [here](https://paypal.me/benparmeter). Of course, none of this project would be available without the wonderful and amazing folks below. If I forgot anyone please don't hesitate to let me know. diff --git a/app.py b/app.py index 3b4289fe..6f8c326d 100755 --- a/app.py +++ b/app.py @@ -107,7 +107,7 @@ def dash_config(): dash_metadata = read_generic_json(f'./dashboard/{meta_data_filename}') if request.method == 'GET': - render_string = "{% from '_macro_dash_default.html' import render_config_card %}{{ render_config_card(dash_metadata, dash_data) }}" + render_string = "{% from '_macro_generic_config.html' import render_dash_config_card %}{{ render_dash_config_card(dash_metadata, dash_data) }}" return render_template_string(render_string, dash_metadata=dash_metadata, dash_data=dash_data) elif request.method == 'POST': dash_config_request = request.form @@ -958,11 +958,21 @@ def tuner_page(action=None): data = read_autotune() if len(data) > 10: + # If more than 10 datapoints, then calculate high / low / medium temp_list = [] tr_list = [] for datapoint in data: - temp_list.append(datapoint['ref_T']) - tr_list.append(datapoint['probe_Tr']) + ''' + Check if the ref_T value is already in the list and overwrite if so. + This assumes that the last temperature is the most recent and is likely + the most accurate resistance value to take. + ''' + if datapoint['ref_T'] in temp_list: + index = temp_list.index(datapoint['ref_T']) + tr_list[index] = datapoint['probe_Tr'] + else: + temp_list.append(datapoint['ref_T']) + tr_list.append(datapoint['probe_Tr']) # Determine High Temp / Tr status_data['high_temp'] = max(temp_list) @@ -1782,7 +1792,6 @@ def settings_page(action=None): 'name' : response['Name'], 'id' : UniqueID } - print(f'Response: {response}') if response.get('apply_profile', False): probe_selected = response['apply_profile'] for index, probe in enumerate(settings['probe_settings']['probe_map']['probe_info']): @@ -2366,7 +2375,6 @@ def admin_page(action=None): if control['system']['cpu_throttled'] or control['system']['cpu_under_voltage']: event = "CPU Throttled / Undervoltage event has occurred. Check your power supply for proper voltage." errors.append(event) - print(event) if 'check_cpu_temp' in supported_cmds: process_command(action='sys', arglist=['check_cpu_temp'], origin='admin') # Request supported commands @@ -2593,7 +2601,9 @@ def wizard(action=None): section = r['section'] if section in ['grillplatform', 'display', 'distance']: moduleData = wizardData['modules'][section][module] - moduleSettings = get_settings_dependencies_values(settings, moduleData) + moduleSettings = {} + moduleSettings['settings'] = get_settings_dependencies_values(settings, moduleData) + moduleSettings['config'] = {} if section != 'display' else settings['display']['config'][module] render_string = "{% from '_macro_wizard_card.html' import render_wizard_card %}{{ render_wizard_card(moduleData, moduleSection, moduleSettings) }}" return render_template_string(render_string, moduleData=moduleData, moduleSection=section, moduleSettings=moduleSettings) else: @@ -2601,7 +2611,7 @@ def wizard(action=None): ''' Create Temporary Probe Device/Port Structure for Setup, Use Existing unless First Time Setup ''' if settings['globals']['first_time_setup']: - wizardInstallInfo = wizardInstallInfoDefaults(wizardData) + wizardInstallInfo = wizardInstallInfoDefaults(wizardData, settings) else: wizardInstallInfo = wizardInstallInfoExisting(wizardData, settings) @@ -2621,47 +2631,55 @@ def get_settings_dependencies_values(settings, moduleData): for setting_name in setting_location: setting_value = setting_value[setting_name] moduleSettings[setting] = setting_value - print(moduleSettings) return moduleSettings -def wizardInstallInfoDefaults(wizardData): +def wizardInstallInfoDefaults(wizardData, settings): wizardInstallInfo = { 'modules' : { 'grillplatform' : { - 'module_selected' : [], - 'settings' : {} + 'profile_selected' : [], # Reference the profile in wizardData > wizard_manifest.json + 'settings' : {}, + 'config' : {} }, 'display' : { - 'module_selected' : [], - 'settings' : {} + 'profile_selected' : [], + 'settings' : {}, + 'config' : {} }, 'distance' : { - 'module_selected' : [], - 'settings' : {} + 'profile_selected' : [], + 'settings' : {}, + 'config' : {} }, 'probes' : { - 'module_selected' : [], + 'profile_selected' : [], 'settings' : { 'units' : 'F' - } + }, + 'config' : {} } }, - 'probe_map' : wizardData['boards']['PiFirev2x']['probe_map'] + 'probe_map' : {} } ''' Populate Modules Info with Defaults from Wizard Data including Settings ''' for component in ['grillplatform', 'display', 'distance']: for module in wizardData['modules'][component]: if wizardData['modules'][component][module]['default']: ''' Populate Module Filename''' - wizardInstallInfo['modules'][component]['module_selected'].append(wizardData['modules'][component][module]['filename']) + wizardInstallInfo['modules'][component]['profile_selected'].append(module) #TODO: Change wizard.py to reference the module filename instead, or in grill_platform use platform>system_type for setting in wizardData['modules'][component][module]['settings_dependencies']: ''' Populate all settings with default value ''' wizardInstallInfo['modules'][component]['settings'][setting] = list(wizardData['modules'][component][module]['settings_dependencies'][setting]['options'].keys())[0] + if module == 'display': + wizardInstallInfo['modules'][component]['config'] = settings['display']['config'][module] + + ''' Populate the default probe device / probe map from the default PCB Board ''' + wizardInstallInfo['probe_map'] = wizardData['boards'][wizardInstallInfo['modules']['grillplatform']['profile_selected'][0]]['probe_map'] ''' Populate Probes Module List with all configured probe devices ''' for device in wizardInstallInfo['probe_map']['probe_devices']: - wizardInstallInfo['modules']['probes']['module_selected'].append(device['module']) + wizardInstallInfo['modules']['probes']['profile_selected'].append(device['module']) return wizardInstallInfo @@ -2669,40 +2687,54 @@ def wizardInstallInfoExisting(wizardData, settings): wizardInstallInfo = { 'modules' : { 'grillplatform' : { - 'module_selected' : [settings['modules']['grillplat']], - 'settings' : {} + 'profile_selected' : [settings['platform']['current']], + 'settings' : {}, + 'config' : {} }, 'display' : { - 'module_selected' : [settings['modules']['display']], - 'settings' : {} + 'profile_selected' : [settings['modules']['display']], + 'settings' : {}, + 'config' : {} }, 'distance' : { - 'module_selected' : [settings['modules']['dist']], - 'settings' : {} + 'profile_selected' : [settings['modules']['dist']], + 'settings' : {}, + 'config' : {} }, 'probes' : { - 'module_selected' : [], + 'profile_selected' : [], 'settings' : { 'units' : settings['globals']['units'] - } + }, + 'config' : {} } }, 'probe_map' : settings['probe_settings']['probe_map'] } ''' Populate Probes Module List with all configured probe devices ''' for device in wizardInstallInfo['probe_map']['probe_devices']: - wizardInstallInfo['modules']['probes']['module_selected'].append(device['module']) + wizardInstallInfo['modules']['probes']['profile_selected'].append(device['module']) ''' Populate Modules Info with current Settings ''' for module in ['grillplatform', 'display', 'distance']: - selected = wizardInstallInfo['modules'][module]['module_selected'][0] + selected = wizardInstallInfo['modules'][module]['profile_selected'][0] + ''' Error condition if the item in settings doesn't match the wizard manifest ''' + if selected not in wizardData['modules'][module].keys(): + if module == 'grillplatform': + selected = 'custom' + settings['platform']['current'] = selected + else: + selected = 'none' + wizardInstallInfo['modules'][module]['profile_selected'] = selected + for setting in wizardData['modules'][module][selected]['settings_dependencies']: settingsLocation = wizardData['modules'][module][selected]['settings_dependencies'][setting]['settings'] settingsValue = settings.copy() for index in range(0, len(settingsLocation)): settingsValue = settingsValue[settingsLocation[index]] wizardInstallInfo['modules'][module]['settings'][setting] = str(settingsValue) - + if module == 'display': + wizardInstallInfo['modules'][module]['config'] = settings['display']['config'][settings['modules']['display']] return wizardInstallInfo def prepare_wizard_data(form_data): @@ -2712,27 +2744,31 @@ def prepare_wizard_data(form_data): wizardInstallInfo['modules'] = { 'grillplatform' : { - 'module_selected' : [form_data['grillplatformSelect']], - 'settings' : {} + 'profile_selected' : [form_data['grillplatformSelect']], + 'settings' : {}, + 'config' : {} }, 'display' : { - 'module_selected' : [form_data['displaySelect']], - 'settings' : {} + 'profile_selected' : [form_data['displaySelect']], + 'settings' : {}, + 'config' : {} }, 'distance' : { - 'module_selected' : [form_data['distanceSelect']], - 'settings' : {} + 'profile_selected' : [form_data['distanceSelect']], + 'settings' : {}, + 'config' : {} }, 'probes' : { - 'module_selected' : [], + 'profile_selected' : [], 'settings' : { 'units' : form_data['probes_units'] - } + }, + 'config' : {} } } for device in wizardInstallInfo['probe_map']['probe_devices']: - wizardInstallInfo['modules']['probes']['module_selected'].append(device['module']) + wizardInstallInfo['modules']['probes']['profile_selected'].append(device['module']) for module in ['grillplatform', 'display', 'distance']: module_ = module + '_' @@ -2742,6 +2778,9 @@ def prepare_wizard_data(form_data): settingName = module_ + setting if(settingName in form_data): wizardInstallInfo['modules'][module]['settings'][setting] = form_data[settingName] + for config, value in form_data.items(): + if config.startswith(module_ + 'config_'): + wizardInstallInfo['modules'][module]['config'][config.replace(module_ + 'config_', '')] = value return(wizardInstallInfo) @@ -3702,9 +3741,9 @@ def get_app_data(action=None, type=None): 'cpuinfo' : os.popen('cat /proc/cpuinfo').readlines(), 'ifconfig' : os.popen('ifconfig').readlines(), 'temp' : _check_cpu_temp(), - 'outpins' : settings['outpins'], - 'inpins' : settings['inpins'], - 'dev_pins' : settings['dev_pins'], + 'outpins' : settings['platform']['outputs'], + 'inpins' : settings['platform']['inputs'], + 'dev_pins' : settings['platform']['devices'], 'server_version' : settings['versions']['server'], 'server_build' : settings['versions']['build'] } diff --git a/auto-install/install.sh b/auto-install/install.sh index 1354f60e..dc07a022 100644 --- a/auto-install/install.sh +++ b/auto-install/install.sh @@ -81,10 +81,17 @@ echo "** Cloning PiFire from GitHub... **" echo "** **" echo "*************************************************************************" cd /usr/local/bin -# Use a shallow clone to reduce download size -$SUDO git clone --depth 1 https://github.com/nebhead/pifire -# Replace the below command to fetch development branch -#$SUDO git clone --depth 1 --branch development https://github.com/nebhead/pifire + +# Check if -dev option is used +if [ "$1" = "-dev" ]; then + echo "Cloning development branch..." + # Replace the below command to fetch development branch + $SUDO git clone --depth 1 --branch development https://github.com/nebhead/pifire +else + echo "Cloning main branch..." + # Use a shallow clone to reduce download size + $SUDO git clone --depth 1 https://github.com/nebhead/pifire +fi # Setup Python VENV & Install Python dependencies clear @@ -113,10 +120,6 @@ source bin/activate echo " - Installing module dependencies... " # Install module dependencies -python -m pip install "flask==2.3.3" -python -m pip install flask-mobility -python -m pip install flask-qrcode -python -m pip install flask-socketio if ! python -c "import sys; assert sys.version_info[:2] >= (3,11)" > /dev/null; then echo "System is running a python version lower than 3.11, installing eventlet==0.30.2"; python -m pip install "eventlet==0.30.2" @@ -124,48 +127,7 @@ else echo "System is running a python version 3.11 or greater, installing latest eventlet" python -m pip install eventlet fi -python -m pip install gunicorn -python -m pip install gpiozero -python -m pip install redis -python -m pip install uuid -python -m pip install influxdb-client[ciso] -python -m pip install apprise -python -m pip install scikit-fuzzy -python -m pip install "scikit-learn==1.4.2" -python -m pip install ratelimitingfilter -python -m pip install "pillow>=9.2.0" -python -m pip install paho-mqtt -python -m pip install psutil - -# Setup config.txt to enable busses -clear -echo "*************************************************************************" -echo "** **" -echo "** Configuring config.txt **" -echo "** **" -echo "*************************************************************************" - -# Enable SPI - Needed for some displays -$SUDO raspi-config nonint do_spi 0 - -# Enable I2C - Needed for some displays, ADCs, distance sensors -$SUDO raspi-config nonint do_i2c 0 - -# Enable Hardware PWM - Needed for hardware PWM support -if test -f /boot/firmware/config.txt; then - echo "dtoverlay=pwm,gpiopin=13,func=4" | $SUDO tee -a /boot/firmware/config.txt > /dev/null -else - echo "dtoverlay=pwm,gpiopin=13,func=4" | $SUDO tee -a /boot/config.txt > /dev/null -fi - -# Setup backlight / power permissions if a DSI screen is installed -clear -echo "*************************************************************************" -echo "** **" -echo "** Configuring Backlight UDEV Rules **" -echo "** **" -echo "*************************************************************************" -echo 'SUBSYSTEM=="backlight",RUN+="/bin/chmod 666 /sys/class/backlight/%k/brightness /sys/class/backlight/%k/bl_power"' | $SUDO tee -a /etc/udev/rules.d/backlight-permissions.rules > /dev/null +python -m pip install -r /usr/local/bin/pifire/auto-install/requirements.txt ### Setup nginx to proxy to gunicorn clear diff --git a/auto-install/requirements.txt b/auto-install/requirements.txt new file mode 100644 index 00000000..ff7ce4b4 --- /dev/null +++ b/auto-install/requirements.txt @@ -0,0 +1,17 @@ +flask==2.3.3 +flask_mobility +flask_qrcode +flask_socketio +gunicorn +gpiozero +redis +uuid +influxdb_client[ciso] +apprise +scikit-fuzzy +scikit-learn==1.4.2 +ratelimitingfilter +pillow>=9.2.0 +paho-mqtt +psutil +rpi_hardware_pwm \ No newline at end of file diff --git a/auto-install/supervisor/control.conf b/auto-install/supervisor/control.conf index e6531987..2e678180 100644 --- a/auto-install/supervisor/control.conf +++ b/auto-install/supervisor/control.conf @@ -4,5 +4,6 @@ directory=/usr/local/bin/pifire autostart=true autorestart=true startretries=3 +stopasgroup=true stderr_logfile=/usr/local/bin/pifire/logs/control.err.log stdout_logfile=/usr/local/bin/pifire/logs/control.out.log diff --git a/board-config.py b/board-config.py new file mode 100644 index 00000000..cf0ab16c --- /dev/null +++ b/board-config.py @@ -0,0 +1,494 @@ +''' +============================================================================== + PiFire Board Configuration Tool +============================================================================== + + Description: Tool to configure the board settings based on the settings.json + configuration. Currently supports only Raspberry Pi based platforms. + +============================================================================== +''' + +''' +============================================================================== + Imported Modules +============================================================================== +''' + +import argparse +import logging +import os +import json + +''' +============================================================================== + Globals +============================================================================== +''' + +log_level = logging.DEBUG + +''' +============================================================================== + Main Functions +============================================================================== +''' + +def set_pwm_gpio(): + result = 'Setting the PWM pin: ' + try: + settings = read_generic_json('settings.json') + pin = settings['platform']['outputs']['pwm'] + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all' or system_type == 'prototype': + # "dtoverlay=pwm,pin=13,func=4" + pin = int(pin) if pin != None else None + result += rpi_config_write('dtoverlay', 'pwm', add_config={'func' : '4'}, pin=pin, pin_type='pin') + else: + result += 'NA - No system defined' + except: + result += 'FAILED (error making the configuration change) ' + + return result + +def set_onewire_gpio(): + result = 'Setting the 1Wire pin: ' + try: + settings = read_generic_json('settings.json') + pin = settings['platform']['system']['1WIRE'] + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all' or system_type == 'prototype': + # "dtoverlay=w1-gpio,pin=6" + pin = int(pin) if pin != None else None + result += rpi_config_write('dtoverlay', 'w1-gpio', pin=pin, pin_type='gpio_pin') + else: + result += 'NA - No system defined' + except: + result += 'FAILED (error making the configuration change) ' + + return result + +def set_backlight(): + result = 'Enabling Backlight Control for DSI Touch Display: ' + try: + settings = read_generic_json('settings.json') + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all': + lines = [ 'SUBSYSTEM=="backlight",RUN+="/bin/chmod 666 /sys/class/backlight/%k/brightness /sys/class/backlight/%k/bl_power"\n' ] + file = '/etc/udev/rules.d/backlight-permissions.rules' + result += create_file(file, lines) + except: + result += 'FAILED (error making the configuration change) ' + + return result + +def enable_spi(): + result = 'Enabling SPI: ' + try: + settings = read_generic_json('settings.json') + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all' or system_type == 'prototype': + # "dtparam=spi=on" + result += rpi_config_write('dtparam', 'spi') + else: + result += 'NA - No system defined' + except: + result += 'FAILED (error making the configuration change) ' + + return result + +def enable_i2c(): + result = 'Enabling I2C: ' + try: + settings = read_generic_json('settings.json') + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all': + # dtparam=i2c_arm=on + result += rpi_config_write('dtparam', 'i2c_arm') + # To enable userspace access to I2C ensure that /etc/modules contains "12c-dev" + # echo "i2c-dev" | $SUDO tee -a /etc/modules + result += append_file('/etc/modules', 'i2c-dev\n') + else: + result += 'NA - No system defined' + + except: + result += 'FAILED (error making the configuration change) ' + + return result + +def set_i2c_speed(baud=100000): + result = f'Setting I2C speed ({baud} Baud): ' + try: + settings = read_generic_json('settings.json') + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all' or system_type == 'prototype': + # dtparam=i2c_arm_baudrate=100000 + result += rpi_config_write('dtparam', 'i2c_arm_baudrate', param=baud) + else: + result += 'NA - No system defined' + + except: + result += 'FAILED (error making the configuration change) ' + + return result + +def enable_gpio_shutdown(): + result = 'Enabling the GPIO Shutdown pin: ' + try: + settings = read_generic_json('settings.json') + pin = settings['platform']['inputs']['shutdown'] + system_type = settings['platform']['system_type'] + except: + result += 'FAILED (error getting settings.json data) ' + return result + + try: + if system_type == 'raspberry_pi_all' or system_type == 'prototype': + # dtoverlay=gpio-shutdown,gpio_pin=17,active_low=1,gpio_pull=up + add_config = { + 'active_low' : '1', + 'gpio_pull' : 'up' + } + pin = int(pin) if pin != None else None + result += rpi_config_write('dtoverlay', 'gpio-shutdown', add_config=add_config, pin=pin, pin_type='gpio_pin') + else: + result += 'NA - No system defined' + except: + result += 'FAILED (error making the configuration change) ' + + return result +''' +============================================================================== + Supporting Functions +============================================================================== +''' + +def rpi_config_write(config_type, feature, add_config={}, pin=0, param='', pin_type='gpio_pin'): + result = 'SUCCESS' + ''' Check OS version, so we can get the correct location of config.txt ''' + version = os_version() + if version == '12': + ''' Version 12 Bookworm ''' + config_filename = '/boot/firmware/config.txt' + elif version == '11': + ''' Version 11 Bullseye ''' + config_filename = '/boot/config.txt' + else: + ''' Test Mode ''' + config_filename = './local/config.txt' + + ''' Modify the configuration file ''' + try: + ''' Open the configuration file ''' + with open(config_filename, 'r+') as config_txt: + config_data = config_txt.readlines() + ''' Look for the configuration line if it exists already ''' + found = False + for index in range(0, len(config_data)): + if config_type in config_data[index] and feature in config_data[index]: + found = True + # Check for leading hashtag and remove + config_line = remove_hashtag(config_data[index]) + + # If the pin is marked as disabled / None, then comment out the line + if pin == None: + config_data[index] = f'#{config_line}' + else: + # Remove the preceding configuration type + config_line = config_line.replace(f'{config_type}=', '') + + # Get dictionary of the components + config_dict = parse_config_line(config_line) + + # For dtparams, turn on feature + if config_type == 'dtparam': + if param == '': + config_dict[feature] = 'on' + else: + config_dict[feature] = param + + # For dtoverlay, edit gpio-pin and additional features + elif config_type == 'dtoverlay': + # Modify pin number + if pin > 0: + for noun in ['gpio-pin', 'gpiopin', 'gpio_pin', 'pin']: + if noun in config_dict[feature].keys(): + config_dict[feature].pop(noun, None) + config_dict[feature][pin_type] = str(pin) + + # If function, add function number + if add_config != {}: + for key, value in add_config.items(): + config_dict[feature][key] = value + + ''' Create the modified configuration line ''' + config_data[index] = build_config_line(config_type, config_dict) + break + + if not found and pin is not None: + config_dict = {} + if config_type == 'dtoverlay': + config_dict[feature] = {} + config_dict[feature][pin_type] = pin + if add_config != {}: + for key, value in add_config.items(): + config_dict[feature][key] = value + elif config_type == 'dtparam': + config_dict[feature] = 'on' + + config_data.append(build_config_line(config_type, config_dict)) + + + ''' Write all data back to the file ''' + with open(config_filename, 'w') as config_txt: + config_txt.writelines(config_data) + + except: + result = 'FAILED ' + + return result + +def parse_config_line(config_line): + """ + (Format of the configuration line adheres to the Raspberry Pi config.txt formatting rules) + This function parses a configuration line into component options. + This function assumes that the preceding configuration option has been removed (i.e. dtparam=, dtoverlay=, etc.). + This function removes comments. + + Args: + config_line: The configuration line to be parsed + + Returns: + Dictionary of configuration keys and values, sub-keys/values + """ + if '#' in config_line: + config_line = config_line.split('#')[0] + + split_line = config_line.split(',') + config_dict = {} + feature = None + + for item in split_line: + item_split = item.split('=') + item_dict = {} + if len(item_split) > 1: + if feature is not None: + config_dict[feature][item_split[0]] = item_split[1] + else: + config_dict[item_split[0]] = item_split[1] + else: + config_dict[item_split[0]] = {} + feature = item_split[0] + return config_dict + +def build_config_line(config_type, config_dict): + """ + (Format of the configuration line adheres to the Raspberry Pi config.txt formatting rules) + This function parses a configuration dictionary into a configuration string/line. + + Args: + config_type: String of the type 'dtparam', 'dtoverlay', etc. + config_dict: The configuration dictionary to be parsed + + Returns: + String of the configuration line + """ + + config_line = f'{config_type}=' + comma = False + for key, value in config_dict.items(): + if comma: + config_line += ',' + if isinstance(value, dict): + config_line += f'{key}' + for subkey, subvalue in value.items(): + if subvalue is not None: + config_line += f',{subkey}={subvalue}' + else: + config_line += f',{subkey}' + else: + config_line += f'{key}={value}' + comma = True + + config_line += ' # Modified by PiFire Board Configuration Utility' + config_line += '\n' + + return config_line + +def os_version(): + version = None + try: + with open('/etc/os-release', "r+") as os_info_file: + os_info = os_info_file.readlines() + for index in range(0, len(os_info)): + if 'VERSION_ID=' in os_info[index]: + version = os_info[index].replace('VERSION_ID=', '').replace('"', '').replace('\n', '') + break + except: + version = None + + return version + +def create_file(filename, lines): + result = f'\n - Attempting to write data to {filename}: ' + try: + with open(filename, "w") as file: + for line in lines: + file.write(line) + result += f' SUCCESS (creating file {filename}) ' + except: + result += f' FAILED (creating file {filename}) ' + return result + +def append_file(filename, lines): + result = f'\n - Attempting to append data to {filename}: ' + try: + with open(filename, "a+") as file: + for line in lines: + file.write(line) + result += f' SUCCESS (appending file {filename}) ' + except: + result += f' FAILED (appending file {filename}) ' + return result + +def remove_hashtag(text): + """Removes a preceding hashtag character from a string if it exists, + including any leading spaces. + + Args: + text: The string to process. + + Returns: + The string with the hashtag and leading spaces removed if it existed, + otherwise the original string. + """ + if text: + # Strip leading spaces + stripped_text = text.lstrip() + if stripped_text and stripped_text[0] == "#": + return stripped_text[1:] + else: + return text + else: + return text + +def read_generic_json(filename): + try: + json_file = os.fdopen(os.open(filename, os.O_RDONLY)) + json_data = json_file.read() + dictionary = json.loads(json_data) + json_file.close() + except: + dictionary = {} + event = f'An error occurred loading {filename} ' + logger.error(event) + return dictionary + +def create_logger(name, filename='./logs/pifire.log', messageformat='%(asctime)s | %(levelname)s | %(message)s', level=logging.INFO): + '''Create or Get Existing Logger''' + logger = logging.getLogger(name) + ''' + If the logger does not exist, create one. Else return the logger. + Note: If the a log-level change is needed, the developer should directly set the log level on the logger, instead of using + this function. + ''' + if not logger.hasHandlers(): + logger.setLevel(level) + formatter = logging.Formatter(fmt=messageformat, datefmt='%Y-%m-%d %H:%M:%S') + # datefmt='%Y-%m-%d %H:%M:%S' + handler = logging.FileHandler(filename) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +''' +============================================================================== + Main +============================================================================== +''' +if __name__ == "__main__": + logger = create_logger('board_config', filename='./logs/board_config.log', level=log_level) + + print('PiFire Board Configuration Tool v1.0.0') + print('Ben Parmeter - 2024 - MIT License') + print(' --help, -h for command details\n') + + parser = argparse.ArgumentParser(description='This tool performs board specific configuration for certain system level features. Use the below options to enable/disable and configure these features. System settings are read from the settings.json file.') + parser.add_argument('-pwm', '--pwm', action='store_true', required=False, help="Set PWM GPIO.") + parser.add_argument('-ow', '--onewire', action='store_true', required=False, help="Set 1Wire GPIO.") + parser.add_argument('-bl', '--backlight', action='store_true', required=False, help="Enable backlight permissions.") + parser.add_argument('-ov', '--osversion', action='store_true', required=False, help="Get OS Version.") + parser.add_argument('-s', '--spi', action='store_true', required=False, help="Enable SPI.") + parser.add_argument('-i', '--i2c', action='store_true', required=False, help="Enable I2C.") + parser.add_argument('-is', '--i2cspeed', metavar='BAUD', type=int, required=False, help="Set the I2C baud rate. BAUD should be an integer, i.e. 100000") + parser.add_argument('-gs', '--gpioshutdown', action='store_true', required=False, help="Enable GPIO shutdown.") + + args = parser.parse_args() + + results = [] + + if args.pwm: + results.append(set_pwm_gpio()) + + if args.onewire: + results.append(set_onewire_gpio()) + + if args.backlight: + results.append(set_backlight()) + + if args.spi: + results.append(enable_spi()) + + if args.i2c: + results.append(enable_i2c()) + + if args.i2cspeed: + results.append(set_i2c_speed(baud=args.i2cspeed)) + + if args.gpioshutdown: + results.append(enable_gpio_shutdown()) + + if args.osversion: + version = os_version() + event = f'Detected OS version_id: {version}.' + results.append(event) + + if len(results) == 0: + print('No Arguments Found. Use --help to see available arguments') + else: + print('Results:') + for item in results: + print(f' - {item}') + logger.info(f'{item}') + diff --git a/common/common.py b/common/common.py index 57ca6a6e..f9ee1242 100644 --- a/common/common.py +++ b/common/common.py @@ -92,11 +92,7 @@ def default_settings(): 'grill_name' : '', 'debug_mode' : False, 'page_theme' : 'light', - 'triggerlevel' : 'LOW', - 'buttonslevel' : 'HIGH', 'disp_rotation' : 0, - 'dc_fan': False, - 'standalone': True, 'units' : 'F', 'augerrate' : 0.3, # (grams per second) default auger load rate is 10 grams / 30 seconds 'first_time_setup' : True, # Set to True on first setup, to run wizard on load @@ -106,38 +102,55 @@ def default_settings(): 'prime_ignition' : False, # Set to True to enable the igniter in prime & startup mode 'updated_message' : False, # Set to True to display a pop-up message after the system has been updated 'venv' : False, # Set to True if running in virtual environment (needed for Raspberry Pi OS Bookworm) - 'real_hw' : True # Set to True if running on real hardware (i.e. Raspberry Pi), False if running in a test environment } if os.path.exists('bin'): settings['globals']['venv'] = True - settings['outpins'] = { - 'power' : 4, - 'auger' : 14, - 'fan' : 15, - 'igniter' : 18, - 'dc_fan' : 26, - 'pwm' : 13 - } - - settings['inpins'] = { 'selector' : 17 } - - settings['dev_pins'] = { # Device Pin Assignment - 'input': { - 'up_clk': 16, # Up Button or CLK for encoder - 'enter_sw' : 21, # Enter Button or SW for encoder - 'down_dt' : 20 # Down Button or DT for encoder + """ The following are platform related settings, such as pin assignments, etc. """ + settings['platform'] = { + "devices": { + "display": { + "dc": 24, # SPI Display (ex. ILI9341) + "led": 5, # SPI Display (ex. ILI9341) + "rst": 25 # SPI Display (ex. ILI9341) + }, + "distance": { + "echo": 27, # HCSR04 Distance Sensor + "trig": 23 # HCSR04 Distance Sensor + }, + "input": { + "down_dt": 20, # Button (DOWN) or Encoder (DT) + "enter_sw": 21, # Button (ENTER) or Encoder (SW) + "up_clk": 16 # Button (UP) or Encoder (CLK) + } + }, + "inputs": { + "selector": 17, # Selector input to select between the OEM Controller or PiFire Controller + "shutdown" : 17 # Shutdown GPIO Pin if implemented }, - 'display': { - 'led' : 5, # ILI9341: LED - ST7789: BL - 'dc' : 24, # ILI9341: DC - ST7789: DC - 'rst' : 25 # ILI9341: RST - ST7789: RST + "outputs": { + "auger": 14, + "dc_fan": 26, + "fan": 15, + "igniter": 18, + "power": 4, + "pwm": 13 }, - 'distance': { - 'trig': 23, # For hcsr04 - 'echo' : 27 # For hcsr04 + "system" : { + "SPI0" : { + "CE0" : 8, # In case a non-standard CE/CS is utilized + "CE1" : 7, # In case a non-standard CE/CS is utilized + }, + "1WIRE" : 6 # 1WIRE is used for probe devices specifically the DS18B20 }, + "current" : "custom", + "dc_fan": False, # True if system has a DC Fan (Does not indicate PWM) + "triggerlevel": "LOW", # Active LOW / Active HIGH for the Relay Outputs + "buttonslevel": "HIGH", # Active LOW / Active HIGH for the button inputs + "standalone": True, # Standalone (without OEM controller present) + "real_hw" : True, # Set to True if running on real hardware (i.e. Raspberry Pi), False if running in a test environment + "system_type" : "prototype", # System type / core (i.e. Raspberry Pi Zero W, Zero 2W, 3A, 3B, 3B+, 4, 5) } settings['cycle_data'] = { @@ -505,8 +518,6 @@ def default_control(): 'pwm' : 100 } - control['errors'] = [] - control['smartstart'] = { 'startuptemp' : 0, 'profile_selected' : 0 @@ -518,6 +529,8 @@ def default_control(): control['system'] = {} + control['critical_error'] = False + return(control) def default_notify(settings): @@ -1144,6 +1157,48 @@ def upgrade_settings(prev_ver, settings, settings_default): settings['globals'].pop('shutdown_timer', None) settings['shutdown']['auto_power_off'] = settings['globals'].get('auto_power_off', settings_default['shutdown']['auto_power_off']) settings['globals'].pop('auto_power_off', None) + ''' Check if upgrading from v1.7.x ''' + if (prev_ver[0] <=1 and prev_ver[1] <= 7): + ''' Force running the configuration wizard again ''' + settings['globals']['first_time_setup'] = True + ''' Create platform section in settings with defaults ''' + settings['platform'] = settings_default['platform'] + ''' Move platform global variables to platform section ''' + if settings['globals'].get('buttonslevel', None) is not None: + settings['platform']['buttonslevel'] = settings['globals'].get('buttonslevel', 'HIGH') + settings['globals'].pop('buttonslevel') + if settings['globals'].get('dc_fan', None) is not None: + settings['platform']['dc_fan'] = settings['globals'].get('dc_fan', False) + settings['globals'].pop('dc_fan') + if settings['globals'].get('real_hw', None) is not None: + settings['platform']['real_hw'] = settings['globals'].get('real_hw', True) + settings['globals'].pop('real_hw') + if settings['globals'].get('standalone', None) is not None: + settings['platform']['standalone'] = settings['globals'].get('standalone', True) + settings['globals'].pop('standalone') + if settings['globals'].get('triggerlevel', None) is not None: + settings['platform']['triggerlevel'] = settings['globals'].get('triggerlevel', 'LOW') + settings['globals'].pop('triggerlevel') + ''' Move pin definitions to platform section''' + if settings.get('dev_pins', None) is not None: + updated_dict = deep_update(settings['platform']['devices'], settings['dev_pins']) + settings['platform']['devices'] = updated_dict + settings.pop('dev_pins') + if settings.get('inpins', None) is not None: + updated_dict = deep_update(settings['platform']['inputs'], settings['inpins']) + settings['platform']['inputs'] = updated_dict + settings.pop('inpins') + if settings.get('outpins', None) is not None: + updated_dict = deep_update(settings['platform']['outputs'], settings['outpins']) + settings['platform']['outputs'] = updated_dict + settings.pop('outpins') + ''' Migrate module settings for the appropriate module support ''' + settings['platform']['current'] = 'custom' # Since we do not know what PCB / System is installed on upgrade, set to custom + if settings['modules']['grillplat'] == 'prototype': + settings['platform']['system_type'] = 'prototype' + else: + settings['platform']['system_type'] = 'raspberry_pi_all' + settings['modules']['grillplat'] == 'raspberry_pi_all' ''' Import any new probe profiles ''' for profile in list(settings_default['probe_settings']['probe_profiles'].keys()): @@ -1654,7 +1709,7 @@ def is_real_hardware(settings=None): if settings == None: settings = read_settings() - return True if settings['globals']['real_hw'] else False + return True if settings['platform']['real_hw'] else False def restart_scripts(): """ @@ -1930,10 +1985,12 @@ def read_status(init=False): global cmdsts if init: + settings = read_settings() + pellet_db = read_pellet_db() status = { "s_plus": False, - "hopper_level": 100, - "units": "F", + "hopper_level": pellet_db['current']['hopper_level'], + "units": settings['globals']['units'], "mode": "Stop", "recipe": False, "startup_timestamp" : 0, @@ -2579,4 +2636,57 @@ def process_command(action=None, arglist=[], origin='unknown', direct_write=Fals data['result'] = 'ERROR' data['message'] = f'Action [{action}] not valid/recognized.' - return data \ No newline at end of file + return data + +def set_nested_key_value(data, key_list, value): + """ + Sets the value of a key in a nested dictionary and returns the modified dictionary. + + Args: + data: The dictionary to modify. + key_list: A list of keys representing the path to the nested key. + value: The value to assign to the nested key. + + Returns: + The modified dictionary. + + Raises: + KeyError: If any key in the path is not found in the dictionary. + """ + if not key_list: + return data # Reached the end of the key list, return the data + + current_key = key_list[0] + # Check if the key exists and is a dictionary (except for the last key) + if current_key not in data or (len(key_list) > 1 and not isinstance(data[current_key], dict)): + raise KeyError(f"Key '{current_key}' not found or not a dictionary") + + # Check if we reached the bottom level (last key in the list) + if len(key_list) == 1: + data[current_key] = value + else: + # Recursive call for nested dictionaries + data[current_key] = set_nested_key_value(data[current_key], key_list[1:], value) + + return data + +def read_generic_key(key): + """ + Read generic data from Redis DB + :param key: key name + """ + global cmdsts + + value = json.loads(cmdsts.get(key)) + + return value + +def write_generic_key(key, value): + """ + Write generic data to Redis DB + :param key: key name + :parma value: value to write + """ + global cmdsts + + cmdsts.set(key, json.dumps(value)) diff --git a/control.py b/control.py index 0ed83a4a..f0b245ec 100755 --- a/control.py +++ b/control.py @@ -21,6 +21,7 @@ ''' import logging import importlib +import atexit from common import * # Common Module for WebUI and Control Program from common.process_mon import Process_Monitor from common.redis_queue import RedisQueue @@ -35,9 +36,8 @@ Read and initialize Settings, Control, History, Metrics, and Error Data ============================================================================== ''' -# Read Settings & Wizard Manifest to Get Modules Configuration +# Read Settings to get Modules Configuration settings = read_settings(init=True) -wizard_data = read_wizard() # Setup logging log_level = logging.DEBUG if settings['globals']['debug_mode'] else logging.ERROR @@ -46,6 +46,9 @@ log_level = logging.DEBUG if settings['globals']['debug_mode'] else logging.INFO eventLogger = create_logger('events', filename='/tmp/events.log', messageformat='%(asctime)s [%(levelname)s] %(message)s', level=log_level) +eventLogger.info('Control Script Starting Up.') +controlLogger.info('Control Script Starting Up.') + # Flush Redis DB and create JSON structure control = read_control(flush=True) # Delete Redis DB for history / current @@ -57,6 +60,10 @@ eventLogger.info('Flushing Redis DB and creating new control structure') +platform_config = settings['platform'] +platform_config['frequency'] = settings['pwm']['frequency'] +units = settings['globals']['units'] + ''' Set up GrillPlatform Module ''' @@ -65,38 +72,29 @@ GrillPlatModule = importlib.import_module(f'grillplat.{grill_platform}') except: - controlLogger.exception(f'Error occurred loading grillplatform module ({settings["modules"]["grillplat"]}). Trace dump: ') + control['critical_error'] = True + write_control(control, direct_write=True, origin='control') + controlLogger.exception(f'Error occurred importing grillplatform module ({settings["modules"]["grillplat"]}). Trace dump: ') GrillPlatModule = importlib.import_module('grillplat.prototype') - error_event = f'An error occurred loading the [{settings["modules"]["grillplat"]}] platform module. The ' \ - f'prototype module has been loaded instead. This sometimes means that the hardware is not connected ' \ - f'properly, or the module is not configured. Please run the configuration wizard again from the admin ' \ + error_event = f'An error occurred importing the [{settings["modules"]["grillplat"]}] platform module. The ' \ + f'prototype module has been imported instead. This sometimes means that the module does not exist or is not ' \ + f'properly named. Please run the configuration wizard again from the admin ' \ f'panel to fix this issue.' errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) if settings['globals']['debug_mode']: raise -out_pins = settings['outpins'] -in_pins = settings['inpins'] -trigger_level = settings['globals']['triggerlevel'] -buttons_level = settings['globals']['buttonslevel'] -dc_fan = settings['globals']['dc_fan'] -frequency = settings['pwm']['frequency'] -standalone = settings['globals']['standalone'] -disp_rotation = settings['globals']['disp_rotation'] -units = settings['globals']['units'] -dev_pins = settings['dev_pins'] - try: - if dc_fan: - grill_platform = GrillPlatModule.GrillPlatform(out_pins, in_pins, trigger_level, dc_fan, frequency) - else: - grill_platform = GrillPlatModule.GrillPlatform(out_pins, in_pins, trigger_level) + grill_platform = GrillPlatModule.GrillPlatform(platform_config) except: + control['critical_error'] = True + write_control(control, direct_write=True, origin='control') controlLogger.exception(f'Error occurred configuring grillplatform module ({settings["modules"]["grillplat"]}). Trace dump: ') from grillplat.prototype import GrillPlatform # Simulated Library for controlling the grill platform - grill_platform = GrillPlatform(out_pins, in_pins, trigger_level) + grill_platform = GrillPlatform(platform_config) error_event = f'An error occurred configuring the [{settings["modules"]["grillplat"]}] platform object. The ' \ f'prototype module has been loaded instead. This sometimes means that the hardware is not ' \ f'connected properly, or the module is not configured. Please run the configuration wizard ' \ @@ -104,6 +102,7 @@ errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) if settings['globals']['debug_mode']: raise @@ -111,23 +110,35 @@ Set up Probes Input Module ''' try: - from probes.main import ProbesMain # Probe device libary: loads probe devices and maps them to ports + from probes.main import ProbesMain # Probe device library: loads probe devices and maps them to ports probe_complex = ProbesMain(settings["probe_settings"]["probe_map"], settings['globals']['units']) except: controlLogger.exception(f'Error occurred loading probes modules. Trace dump: ') - settings['probe_settings']['probe_map'] = default_probe_map(settings["probe_settings"]['probe_profiles']) - probe_complex = ProbesMain(settings["probe_settings"]["probe_map"], settings['globals']['units']) - error_event = f'An error occurred loading the probes module(s). The prototype ' \ - f'module has been loaded instead. This sometimes means that the hardware is not connected ' \ - f'properly, or the module is not configured. Please run the configuration wizard again from ' \ - f'the admin panel to fix this issue.' + #settings['probe_settings']['probe_map'] = default_probe_map(settings["probe_settings"]['probe_profiles']) + probe_complex = ProbesMain(settings["probe_settings"]["probe_map"], settings['globals']['units'], disable=True) + error_event = f'An error occurred loading the probes module(s). All probes & probe devices have been disabled. ' \ + f'This sometimes means that the hardware is not connected properly, or the module is not configured correctly. ' \ + f'Please run the configuration wizard again from the admin panel to fix this issue. ' errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) if settings['globals']['debug_mode']: raise +# Get probe initialization errors and pass along to the frontend +probe_errors = probe_complex.get_errors() +if len(probe_errors) > 0: + for error in probe_errors: + eventLogger.error(error) + errors.append(error) + write_errors(errors) + +# Get probe device info for frontend +probe_device_info = probe_complex.get_device_info() +write_generic_key('probe_device_info', probe_device_info) + ''' Set up Display Module ''' @@ -136,6 +147,7 @@ DisplayModule = importlib.import_module(f'display.{display_name}') display_config = settings['display']['config'][display_name] display_config['probe_info'] = get_probe_info(settings['probe_settings']['probe_map']['probe_info']) + disp_rotation = display_config.get('rotation', 0) except: controlLogger.exception(f'Error occurred loading the display module ({display_name}). Trace dump: ') @@ -147,16 +159,17 @@ errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) if settings['globals']['debug_mode']: raise try: - display_device = DisplayModule.Display(dev_pins=dev_pins, buttonslevel=buttons_level, + display_device = DisplayModule.Display(dev_pins=settings['platform']['devices'], buttonslevel=settings['platform']['buttonslevel'], rotation=disp_rotation, units=units, config=display_config) except: controlLogger.exception(f'Error occurred configuring the display module ({settings["modules"]["display"]}). Trace dump: ') from display.none import Display # Simulated Library for controlling the grill platform - display_device = Display(dev_pins=dev_pins, buttonslevel=buttons_level, rotation=disp_rotation, units=units, config={}) + display_device = Display(dev_pins=settings['platform']['devices'], buttonslevel=settings['platform']['buttonslevel'], rotation=disp_rotation, units=units, config={}) error_event = f'An error occurred configuring the [{settings["modules"]["display"]}] display object. The ' \ f'"display_none" module has been loaded instead. This sometimes means that the hardware is ' \ f'not connected properly, or the module is not configured. Please run the configuration wizard ' \ @@ -164,6 +177,7 @@ errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) if settings['globals']['debug_mode']: raise @@ -184,22 +198,23 @@ errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) try: if settings['modules']['grillplat'] == 'prototype' and settings['modules']['dist'] == 'prototype': # If in prototype mode, enable test reading (i.e. random values from proto distance sensor) dist_device = DistanceModule.HopperLevel( - dev_pins=dev_pins, empty=settings['pelletlevel']['empty'], full=settings['pelletlevel']['full'], + dev_pins=settings['platform']['devices'], empty=settings['pelletlevel']['empty'], full=settings['pelletlevel']['full'], debug=settings['globals']['debug_mode'], random=True) else: dist_device = DistanceModule.HopperLevel( - dev_pins=dev_pins, empty=settings['pelletlevel']['empty'], full=settings['pelletlevel']['full'], + dev_pins=settings['platform']['devices'], empty=settings['pelletlevel']['empty'], full=settings['pelletlevel']['full'], debug=settings['globals']['debug_mode']) except: controlLogger.exception(f'Error occurred configuring the distance module ({dist_name}). Trace dump: ') from distance.none import HopperLevel # Simulated Library for controlling the grill platform dist_device = HopperLevel( - dev_pins=dev_pins, empty=settings['pelletlevel']['empty'], full=settings['pelletlevel']['full'], + dev_pins=settings['platform']['devices'], empty=settings['pelletlevel']['empty'], full=settings['pelletlevel']['full'], debug=settings['globals']['debug_mode']) error_event = f'An error occurred configuring the [{settings["modules"]["dist"]}] distance object. The ' \ f'none module has been loaded instead. This sometimes means that the hardware is not ' \ @@ -208,10 +223,11 @@ errors.append(error_event) write_errors(errors) eventLogger.error(error_event) + controlLogger.error(error_event) # Get current hopper level and save it to the current pellet information pelletdb = read_pellet_db() -pelletdb['current']['hopper_level'] = dist_device.get_level() +pelletdb['current']['hopper_level'] = dist_device.get_level(override=True) write_pellet_db(pelletdb) eventLogger.info(f'Hopper Level Checked @ {pelletdb["current"]["hopper_level"]}%') @@ -227,7 +243,7 @@ def _start_fan(settings, duty_cycle=None): :param settings: Settings :param duty_cycle: Duty Cycle to set. If not provided will be set to max_duty_cycle (dc_fan only) """ - if dc_fan: + if settings['platform']['dc_fan']: if duty_cycle is not None: adjusted_dc = max(duty_cycle, settings['pwm']['min_duty_cycle']) adjusted_dc = min(adjusted_dc, settings['pwm']['max_duty_cycle']) @@ -285,6 +301,8 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device settings = read_settings() control = read_control() pelletdb = read_pellet_db() + control['hopper_check'] = True + write_control(control, direct_write=True, origin='control') eventLogger.info(f'{mode} Mode started.') @@ -327,7 +345,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device last = grill_platform.get_input_status() # Set DC fan frequency if it has changed since init - if dc_fan: + if settings['platform']['dc_fan']: pwm_frequency = settings['pwm']['frequency'] frequency_status = grill_platform.get_output_status() if not pwm_frequency == frequency_status['frequency']: @@ -554,7 +572,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device write_control(control, direct_write=True, origin='control') # Check hopper level when requested or every 300 seconds - if control['hopper_check'] or (now - hopper_toggle_time) > 300: + if control['hopper_check'] or (now - hopper_toggle_time) > 60: pelletdb = read_pellet_db() override = False if control['hopper_check']: @@ -568,7 +586,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device eventLogger.info("Hopper Level Checked @ " + str(pelletdb['current']['hopper_level']) + "%") # Check for update in ON/OFF Switch - if not standalone and last != grill_platform.get_input_status(): + if not settings['platform']['standalone'] and last != grill_platform.get_input_status(): last = grill_platform.get_input_status() if not last: eventLogger.info('Switch set to off, going to monitor mode.') @@ -606,7 +624,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device elif not control['manual']['power'] and current_output_status['power']: grill_platform.power_off() eventLogger.debug('Power OFF') - if dc_fan and control['manual']['fan'] and current_output_status['fan'] and \ + if settings['platform']['dc_fan'] and control['manual']['fan'] and current_output_status['fan'] and \ not control['manual']['pwm'] == current_output_status['pwm']: speed = control['manual']['pwm'] eventLogger.debug('PWM Speed: ' + str(speed) + '%') @@ -716,7 +734,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device status_data['recipe_paused'] = False status_data['outpins'] = {} current = grill_platform.get_output_status() # Get current pin settings - for item in settings['outpins']: + for item in settings['platform']['outputs']: try: status_data['outpins'][item] = current[item] except KeyError: @@ -770,7 +788,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device LidOpenDetect = False # If PWM Fan Control enabled set duty_cycle based on temperature - if (dc_fan and mode == 'Hold' and control['pwm_control'] and + if (settings['platform']['dc_fan'] and mode == 'Hold' and control['pwm_control'] and (now - fan_update_time) > settings['pwm']['update_time']): fan_update_time = now if ptemp > control['primary_setpoint']: @@ -807,7 +825,7 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device elif ((now - sp_cycle_toggle_time) > settings['smoke_plus']['off_time'] and not current_output_status['fan']): sp_cycle_toggle_time = now - if (dc_fan and (mode == 'Smoke' or (mode == 'Hold' and not control['pwm_control'])) and + if (settings['platform']['dc_fan'] and (mode == 'Smoke' or (mode == 'Hold' and not control['pwm_control'])) and settings['smoke_plus']['fan_ramp']): on_time = settings['smoke_plus']['on_time'] max_duty_cycle = settings['pwm']['max_duty_cycle'] @@ -826,19 +844,19 @@ def _work_cycle(mode, grill_platform, probe_complex, display_device, dist_device eventLogger.debug('Smoke Plus: Fan Returned to On') # If Smoke Plus was disabled while fan was ramping return it to the correct duty cycle - elif (dc_fan and current_output_status['pwm'] != control['duty_cycle'] and not + elif (settings['platform']['dc_fan'] and current_output_status['pwm'] != control['duty_cycle'] and not control['s_plus'] and pwm_fan_ramping): pwm_fan_ramping = False grill_platform.set_duty_cycle(control['duty_cycle']) eventLogger.debug('Smoke Plus: Fan Returned to ' + str(control['duty_cycle']) + '% duty cycle') # Set Fan Duty Cycle based on Average Grill Temp Using Profile - elif dc_fan and control['pwm_control'] and current_output_status['pwm'] != control['duty_cycle']: + elif settings['platform']['dc_fan'] and control['pwm_control'] and current_output_status['pwm'] != control['duty_cycle']: grill_platform.set_duty_cycle(control['duty_cycle']) eventLogger.debug('Temp Fan Control: Fan Set to ' + str(control['duty_cycle']) + '% duty cycle') # If PWM Fan Control is turned off check current Duty Cycle and set back to max_duty_cycle if required - elif (dc_fan and not control['pwm_control'] and current_output_status['pwm'] != + elif (settings['platform']['dc_fan'] and not control['pwm_control'] and current_output_status['pwm'] != settings['pwm']['max_duty_cycle']): control['duty_cycle'] = settings['pwm']['max_duty_cycle'] write_control(control, direct_write=True, origin='control') @@ -1058,11 +1076,32 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta return() +def exit_handler(): + """ + Exit handler function that logs a message and performs cleanup operations before exiting the control script. + + This function is called when the control script is about to exit. It logs a message indicating that the script is exiting using the `eventLogger.info()` function. It also logs a formatted message using the `controlLogger.info()` function to provide additional information about the exit. + + After logging the messages, the function calls the `grill_platform.cleanup()` function to perform any necessary cleanup operations related to the grill platform. + + This function does not take any parameters and does not return any values. + + Example usage: + ```python + exit_handler() + ``` + """ + eventLogger.info('Control Script Exiting.') + controlLogger.info('Control Script Exiting.') + grill_platform.cleanup() + return + +# Register the exit handler +atexit.register(exit_handler) + # ***************************************** # Main Program Start / Init and Loop # ***************************************** -eventLogger.info('Control Script Starting Up.') -controlLogger.info(f'Control Script Starting Up.') last = grill_platform.get_input_status() @@ -1077,9 +1116,8 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta status = read_status(init=True) while True: - # Check the On/Off switch for changes - if not standalone and last != grill_platform.get_input_status(): + if not settings['platform']['standalone'] and last != grill_platform.get_input_status(): last = grill_platform.get_input_status() if not last: eventLogger.info('Switch set to off, going to stop mode.') @@ -1090,17 +1128,18 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta status = read_status() current = grill_platform.get_output_status() # Get current pin settings - for item in settings['outpins']: + for item in settings['platform']['outputs']: try: status['outpins'][item] = current[item] except KeyError: continue write_status(status) - # 1. Check control for changes + # Check control for changes execute_control_writes() control = read_control() - # 2. Check for system commands + + # Check for system commands _process_system_commands(grill_platform) # Check if there were updates to any of the settings that were flagged @@ -1151,7 +1190,7 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta probe_complex.update_probe_profiles(settings['probe_settings']['probe_map']['probe_info']) eventLogger.info('Active probe profiles updated in control script.') - if control['updated']: + if control['updated'] and not control['critical_error']: eventLogger.debug(f'Control Settings Updated. Mode: {control["mode"]}, Units Change: {control["units_change"]} ') # Clear control flag control['updated'] = False # Reset Control Updated to False @@ -1231,7 +1270,7 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta # Prime (dump preset amount of pellets into the firepot) elif control['mode'] == 'Prime': - if not standalone and not grill_platform.get_input_status(): + if not settings['platform']['standalone'] and not grill_platform.get_input_status(): eventLogger.warning('PiFire is set to OFF. This doesn\'t prevent startup, but this means the switch won\'t behave as normal.') # Call Work Cycle for Startup Mode _work_cycle('Prime', grill_platform, probe_complex, display_device, dist_device) @@ -1241,7 +1280,7 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta # Startup (startup sequence) elif control['mode'] == 'Startup': - if not standalone and not grill_platform.get_input_status(): + if not settings['platform']['standalone'] and not grill_platform.get_input_status(): eventLogger.warning('PiFire is set to OFF. This doesn\'t prevent startup, but this means the switch won\'t behave as normal.') settings = read_settings() # Clear History (in the case it wasn't already cleared fromt he last run) @@ -1305,7 +1344,7 @@ def _recipe_mode(grill_platform, probe_complex, display_device, dist_device, sta # Reignite (reignite sequence) elif control['mode'] == 'Reignite': - if (not standalone) and (not grill_platform.get_input_status()): + if (not settings['platform']['standalone']) and (not grill_platform.get_input_status()): eventLogger.warning("PiFire is set to OFF. This doesn't prevent reignite, " "but this means the switch won't behave as normal.") control['next_mode'] = control['safety']['reignitelaststate'] diff --git a/dashboard/basic.json b/dashboard/basic.json index f9fa5f5e..a8fdcced 100644 --- a/dashboard/basic.json +++ b/dashboard/basic.json @@ -12,5 +12,5 @@ "custom" : { "hidden_cards" : [] }, - "config" : {} + "config" : [] } \ No newline at end of file diff --git a/display/base_240x320.py b/display/base_240x320.py index 4ec15177..f31fe816 100644 --- a/display/base_240x320.py +++ b/display/base_240x320.py @@ -279,9 +279,14 @@ def _display_loop(self): self.display_timeout = time.time() + 10 if self.display_command == 'network': - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - network_ip = s.getsockname()[0] + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(4) + s.connect(("8.8.8.8", 80)) + network_ip = s.getsockname()[0] + except: + network_ip = '' + if network_ip != '': self._display_network(network_ip) self.display_timeout = time.time() + 30 @@ -758,7 +763,9 @@ def _display_current(self, in_data, status_data): percents = [0,0,0] temps[0] = in_data['probe_history']['primary'][label] - if temps[0] <= 0: + if temps[0] == None: + temps[0] = 0 + if temps[0] == None or temps[0] <= 0: percents[0] = 0 elif self.units == 'F': percents[0] = round((temps[0] / 600) * 100) # F Temp Range [0 - 600F] for Grill @@ -799,7 +806,9 @@ def _display_current(self, in_data, status_data): percents = [0,0,0] temps[0] = in_data['probe_history']['food'][label] - if temps[0] <= 0: + if temps[0] == None: + temps[0] = 0 + if temps[0] == None or temps[0] <= 0: percents[0] = 0 elif self.units == 'F': percents[0] = round((temps[0] / 300) * 100) # F Temp Range [0 - 300F] for probe @@ -836,7 +845,9 @@ def _display_current(self, in_data, status_data): percents = [0,0,0] temps[0] = in_data['probe_history']['food'][label] - if temps[0] <= 0: + if temps[0] == None: + temps[0] = 0 + if temps[0] == None or temps[0] <= 0: percents[0] = 0 elif self.units == 'F': percents[0] = round((temps[0] / 300) * 100) # F Temp Range [0 - 300F] for probe diff --git a/display/base_flex.py b/display/base_flex.py index 00904b97..3ef5c32e 100644 --- a/display/base_flex.py +++ b/display/base_flex.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 ''' ***************************************** PiFire Flexible Display Interface Library @@ -20,9 +19,9 @@ import logging import socket import os +from display.flexobject import * from PIL import Image from common import read_control, write_control, is_real_hardware, read_generic_json, read_settings, write_settings, read_status, read_current -from display.flexobject_pil import FlexObject as FlexObjPIL ''' ================================================================================== @@ -44,13 +43,16 @@ def __init__(self, dev_pins, buttonslevel='HIGH', rotation=0, units='F', config= self.last_status_data = {} self.input_enabled = False - + self.input_origin = None + self.input_button = True if 'button' in self.config.get('input_types_supported', []) else False + self.input_encoder = True if 'encoder' in self.config.get('input_types_supported', []) else False + self.input_touch = True if 'touch' in self.config.get('input_types_supported', []) else False + self.display_active = None self.display_timeout = None self.TIMEOUT = 10 self.command = 'splash' self.command_data = None - self.input_origin = None self.real_hardware = True if is_real_hardware() else False # Attempt to set the log level of PIL so that it does not pollute the logs @@ -61,24 +63,20 @@ def __init__(self, dev_pins, buttonslevel='HIGH', rotation=0, units='F', config= # Init Display Device, Input Device, Assets self._init_globals() self._init_framework() + self._init_display_canvas() self._init_input() self._init_display_device() def _init_globals(self): # Init constants and variables - ''' - 0 = Zero Degrees Rotation - 90, 1 = 90 Degrees Rotation (Pimoroni Libraries, Luma.LCD Libraries) - 180, 2 = 180 Degrees Rotation (Pimoroni Libraries, Luma.LCD Libraries) - 270, 3 = 270 Degrees Rotation (Pimoroni Libraries, Luma.LCD Libraries) - ''' - self.rotation = self.config.get('rotation', 0) self.buttonslevel = self.config.get('buttonslevel', 'HIGH') - + if self.display_profile == None: + self.display_profile = self.config.get('default_profile', 'profile_1') + ''' Get Local IP Address ''' - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.settimeout(0) try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(1) # doesn't even have to be reachable s.connect(('10.254.254.254', 1)) self.ip_address = s.getsockname()[0] @@ -93,47 +91,53 @@ def _init_framework(self): Initialize the dash/home/menu framework ''' self.display_data = read_generic_json(self.config['display_data_filename']) - self.WIDTH = self.display_data['metadata'].get('screen_width', 800) - self.HEIGHT = self.display_data['metadata'].get('screen_height', 480) + if self.config.get('rotation', 0) in [0, 180]: + self.WIDTH = self.display_data['metadata'].get('screen_width', 800) + self.HEIGHT = self.display_data['metadata'].get('screen_height', 480) + else: + self.WIDTH = self.display_data['metadata'].get('screen_height', 480) + self.HEIGHT = self.display_data['metadata'].get('screen_width', 800) self.SPLASH_DELAY = self.display_data['metadata'].get('splash_delay', 1000) self.FRAMERATE = self.display_data['metadata'].get('framerate', 30) - if self.display_data.get('home', []) == []: + if self.display_data[self.display_profile].get('home', []) == []: self.HOME_ENABLED = False else: self.HOME_ENABLED = True - - self.display_data['menus']['qrcode']['ip_address'] = self.ip_address + self.display_data[self.display_profile]['menus']['qrcode']['ip_address'] = self.ip_address self._fixup_display_data() self._init_assets() def _fixup_display_data(self): - for index, object in enumerate(self.display_data['home']): + for index, object in enumerate(self.display_data[self.display_profile]['home']): for key in list(object.keys()): - if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color']: - self.display_data['home'][index][key] = tuple(object[key]) - for index, object in enumerate(self.display_data['dash']): + if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color', 'sp_color', 'np_color']: + self.display_data[self.display_profile]['home'][index][key] = tuple(object[key]) + for index, object in enumerate(self.display_data[self.display_profile]['dash']): #print(f'Object Name: {object["name"]}') for key in list(object.keys()): - if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color']: + if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color', 'sp_color', 'np_color']: #print(f'[{key}] = {object[key]}') - self.display_data['dash'][index][key] = tuple(object[key]) + self.display_data[self.display_profile]['dash'][index][key] = tuple(object[key]) #print(f'converted = {tuple(object[key])}') if key in ['color_levels']: color_level_list = [] - for item in self.display_data['dash'][index][key]: + for item in self.display_data[self.display_profile]['dash'][index][key]: color_level_list.append(tuple(item)) - self.display_data['dash'][index][key] = color_level_list - for menu, object in self.display_data['menus'].items(): + self.display_data[self.display_profile]['dash'][index][key] = color_level_list + if key in ['units']: + ''' Ensure we start with the right units displayed ''' + self.display_data[self.display_profile]['dash'][index][key] = self.units + for menu, object in self.display_data[self.display_profile]['menus'].items(): for key in list(object.keys()): - if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color']: - self.display_data['menus'][menu][key] = tuple(object[key]) - for input, object in self.display_data['input'].items(): + if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color', 'sp_color', 'np_color']: + self.display_data[self.display_profile]['menus'][menu][key] = tuple(object[key]) + for input, object in self.display_data[self.display_profile]['input'].items(): for key in list(object.keys()): - if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color']: - self.display_data['input'][input][key] = tuple(object[key]) - #print(f'Fixed Up: \n{self.display_data["menus"]}') + if key in ['position', 'size', 'fg_color', 'bg_color', 'color', 'active_color', 'inactive_color', 'sp_color', 'np_color']: + self.display_data[self.display_profile]['input'][input][key] = tuple(object[key]) + #print(f'Fixed Up: \n{self.display_data[self.display_profile]["menus"]}') def _init_display_device(self): @@ -142,6 +146,12 @@ def _init_display_device(self): ''' pass + def _init_display_canvas(self): + ''' + Setup display canvas to be used by PIL to display objects + ''' + self.display_canvas = Image.new('RGBA', (self.WIDTH, self.HEIGHT)) + def _init_input(self): ''' Inheriting classes will override this function to setup the inputs. @@ -222,7 +232,18 @@ def _init_background(self): def _init_splash(self): splash_image_path = self.display_data['metadata']['splash_image'] self.splash = Image.open(splash_image_path) - self.splash = self.splash.resize((self.WIDTH, self.HEIGHT)) + width, height = self.splash.size + if width > self.WIDTH or height > self.HEIGHT: + # Scale the splash image to fit the display + # Calculate the scaling factor based on aspect ratio + ratio_w = self.WIDTH / float(width) + ratio_h = self.HEIGHT / float(height) + ratio = min(ratio_w, ratio_h) + + # Apply the scaling factor + new_width = int(width * ratio) + new_height = int(height * ratio) + self.splash = self.splash.resize((new_width, new_height)) def _wake_display(self): ''' @@ -237,10 +258,7 @@ def _sleep_display(self): pass def _display_clear(self): - ''' - Inheriting classes will override this function to clear the display device. - ''' - pass + self.display_canvas.paste((0, 0, 0, 255), (0,0, self.WIDTH, self.HEIGHT)) def _display_canvas(self, canvas): ''' @@ -249,10 +267,9 @@ def _display_canvas(self, canvas): pass def _display_splash(self): - ''' - Inheriting classes will override this function to display the splash screen. - ''' - pass + width, height = self.splash.size + self.display_canvas.paste(self.splash, ((self.WIDTH // 2) - (width // 2), (self.HEIGHT // 2) - (height // 2))) + self._display_canvas() def _display_background(self): ''' @@ -266,24 +283,29 @@ def _display_menu_background(self): ''' pass - def _build_objects(self, background): + def _build_objects(self, background=None): ''' Inheriting classes may override this function to ensure the right object type is loaded ''' self.display_object_list = [] if self.display_active in ['home', 'dash']: - section_data = self.display_data[self.display_active] + section_data = self.display_data[self.display_profile][self.display_active] elif 'menu_' in self.display_active: - section_data = [self.display_data['menus'][self.display_active.replace('menu_', '')]] + section_data = [self.display_data[self.display_profile]['menus'][self.display_active.replace('menu_', '')]] elif 'input_' in self.display_active: - section_data = [self.display_data['input'][self.display_active.replace('input_', '')]] + section_data = [self.display_data[self.display_profile]['input'][self.display_active.replace('input_', '')]] section_data[0]['data']['origin'] = self.input_origin else: return for object_data in section_data: - self.display_object_list.append(FlexObjPIL(object_data['type'], object_data, background)) + ''' Add the object to the display object list ''' + if self.status_data.get('hopper_level', None) != None and object_data["type"] == 'hopper_status': + object_data['data']['level'] = self.status_data['hopper_level'] # Add the current hopper level to the object + FlexObject_ClassName = FlexObject_TypeMap[object_data["type"]] + FlexObject_Constructor = globals()[FlexObject_ClassName] + self.display_object_list.append(FlexObject_Constructor(object_data['type'], object_data, background)) def _configure_dash(self): ''' Build Food Probe Map ''' @@ -296,7 +318,7 @@ def _configure_dash(self): ''' Remove Unused Food Probes & Rename Used Food Probes''' display_data_dash_list = [] - for object in self.display_data['dash']: + for object in self.display_data[self.display_profile]['dash']: if 'food_probe_gauge_' in object['name'] and object['name'] not in list(self.food_probe_label_map.keys()): pass else: @@ -311,7 +333,7 @@ def _configure_dash(self): display_data_dash_list.append(object) - self.display_data['dash'] = display_data_dash_list + self.display_data[self.display_profile]['dash'] = display_data_dash_list def _build_dash_map(self): ''' Setup dash object mapping ''' @@ -389,7 +411,7 @@ def _update_dash_objects(self): ''' Update the Primary Gauge ''' object_data = self.display_object_list[self.dash_map['primary_gauge']].get_object_data() - object_data['temps'][0] = self.in_data['P'][primary_key] + object_data['temps'][0] = self.in_data['P'][primary_key] if self.in_data['P'][primary_key] is not None else 0 object_data['temps'][1] = self.in_data['NT'][primary_key] object_data['temps'][2] = self.in_data['PSP'] object_data['units'] = self.units @@ -407,7 +429,7 @@ def _update_dash_objects(self): ''' Update this food gauge ''' object_data = self.display_object_list[self.dash_map[gauge]].get_object_data() - object_data['temps'][0] = self.in_data['F'][key] + object_data['temps'][0] = self.in_data['F'][key] if self.in_data['F'][key] is not None else 0 object_data['temps'][1] = self.in_data['NT'][key] object_data['temps'][2] = 0 # There is no set temp for food probes object_data['units'] = self.units @@ -519,16 +541,7 @@ def _update_dash_objects(self): self.last_in_data = self.in_data.copy() self.last_status_data = self.status_data.copy() - def _update_input_objects(self): - ''' - for index, object in enumerate(self.display_object_list): - objectData = object.get_object_data() - if objectData['data']['input'] != '': - self.display_object_list[index].update_object_data(objectData) - ''' - pass - - def _animate_objects(self): + def _draw_objects(self): for object in self.display_object_list: objectData = object.get_object_data() objectState = object.get_object_state() @@ -536,11 +549,14 @@ def _animate_objects(self): if objectData['animation_enabled'] and objectState['animation_active']: object_image = object.update_object_data() self.display_updated = True - self.display_surface.blit(object_image, objectData['position']) else: - object_image = object.get_object_surface() - self.display_surface.blit(object_image, objectData['position']) + object_image = object.get_object_canvas() + if objectData['glow']: + object_image_glow = object_image.copy() + object_image_glow = object_image_glow.filter(ImageFilter.GaussianBlur(radius=3)) + self.display_canvas.paste(object_image, objectData['position'], object_image_glow) + self.display_canvas.paste(object_image, objectData['position'], object_image) ''' ====================== Input/Event Handling ======================== @@ -585,6 +601,8 @@ def _command_handler(self): } write_control(data, origin='display') #print('Sent Monitor Mode Command!') + self.display_active = 'dash' + self.display_init = True if 'startup' in self.command: data = { @@ -602,6 +620,8 @@ def _command_handler(self): 'mode' : 'Smoke' } write_control(data, origin='display') + self.display_active = 'dash' + self.display_init = True if 'hold' in self.command: ''' Set hold target for primary probe ''' @@ -619,8 +639,8 @@ def _command_handler(self): 'primary_setpoint' : primary_setpoint } write_control(data, origin='display') - self.display_active = 'dash' - self.display_init = True + self.display_active = 'dash' + self.display_init = True if 'notify' in self.command: ''' Set notification targets for probes/grill ''' @@ -653,6 +673,8 @@ def _command_handler(self): 'mode' : 'Shutdown', } write_control(data, origin='display') + self.display_active = 'dash' + self.display_init = True if 'stop' in self.command: data = { @@ -668,11 +690,13 @@ def _command_handler(self): self.display_timeout = time.time() + self.TIMEOUT if 'splus' in self.command: - enable = True if self.command_data == "on" else False + toggle = False if self.last_status_data.get('s_plus', False) else True data = { - 's_plus' : enable, + 's_plus' : toggle, } write_control(data, origin='display') + self.display_active = 'dash' + self.display_init = True if 'primestartup' in self.command: data = { @@ -725,6 +749,8 @@ def _command_handler(self): # User is forcing next step data['updated'] = True write_control(data, origin='display') + self.display_active = 'dash' + self.display_init = True if 'reboot' in self.command: data = { @@ -773,6 +799,8 @@ def _command_handler(self): 'hopper_check' : True } write_control(data, origin='display') + self.display_active = 'dash' + self.display_init = True if 'none' in self.command: pass diff --git a/display/dsi_800x480t.json b/display/dsi_800x480t.json index 8e1a3096..140eca0d 100644 --- a/display/dsi_800x480t.json +++ b/display/dsi_800x480t.json @@ -7,512 +7,1182 @@ "dash_background" : "./static/img/display/background.png", "splash_image" : "./static/img/display/splash_800x480.png", "splash_delay" : 500, - "max_food_probes" : 5 + "max_food_probes" : 5, + "default_profile" : "profile_1" }, - "home" : [], - "dash" : [ - { - "name" : "primary_gauge", - "type" : "gauge", - "position" : [225, 50], - "animation_enabled" : true, - "size" : [350, 350], - "fg_color" : [255, 255, 255, 255], - "bg_color" : [25, 25, 25, 255], - "glow" : true, - "font" : "trebuc.ttf", - "temps" : [0, 0, 0], - "label" : "Primary", - "units" : "F", - "max_temp" : 600, - "data" : {}, - "button_list" : ["input_notify"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "control_panel", - "type" : "control_panel", - "position" : [200, 380], - "animation_enabled" : false, - "glow" : true, - "size" : [400, 100], - "label" : "control_panel", - "data" : {}, - "button_list" : ["menu_prime", "menu_startup", "cmd_monitor", "cmd_stop"], - "button_type" : ["Prime", "Startup", "Monitor", "Stop"], - "button_active" : "Stop", - "touch_areas" : [] - }, - { - "name" : "food_probe_gauge_0", - "type" : "gauge_compact", - "position" : [0, 55], - "animation_enabled" : false, - "size" : [220, 110], - "fg_color" : [255, 255, 255, 255], - "bg_color" : [25, 25, 25, 255], - "glow" : true, - "font" : "trebuc.ttf", - "temps" : [0, 0, 0], - "label" : "Food Probe", - "units" : "F", - "max_temp" : 300, - "data" : {}, - "button_list" : ["input_notify"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "food_probe_gauge_1", - "type" : "gauge_compact", - "position" : [580, 55], - "animation_enabled" : false, - "size" : [220, 110], - "fg_color" : [255, 255, 255, 255], - "bg_color" : [25, 25, 25, 255], - "glow" : true, - "font" : "trebuc.ttf", - "temps" : [0, 0, 0], - "label" : "Food Probe", - "units" : "F", - "max_temp" : 300, - "data" : {}, - "button_list" : ["input_notify"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "food_probe_gauge_2", - "type" : "gauge_compact", - "position" : [0, 160], - "animation_enabled" : false, - "size" : [220, 110], - "fg_color" : [255, 255, 255, 255], - "bg_color" : [25, 25, 25, 255], - "glow" : true, - "font" : "trebuc.ttf", - "temps" : [0, 0, 0], - "label" : "Food Probe", - "units" : "F", - "max_temp" : 300, - "data" : {}, - "button_list" : ["input_notify"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "food_probe_gauge_3", - "type" : "gauge_compact", - "position" : [580, 160], - "animation_enabled" : false, - "size" : [220, 110], - "fg_color" : [255, 255, 255, 255], - "bg_color" : [25, 25, 25, 255], - "glow" : true, - "font" : "trebuc.ttf", - "temps" : [0, 0, 0], - "label" : "Food Probe", - "units" : "F", - "max_temp" : 300, - "data" : {}, - "button_list" : ["input_notify"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "food_probe_gauge_4", - "type" : "gauge_compact", - "position" : [0, 275], - "animation_enabled" : false, - "size" : [220, 110], - "fg_color" : [255, 255, 255, 255], - "bg_color" : [25, 25, 25, 255], - "glow" : true, - "font" : "trebuc.ttf", - "temps" : [0, 0, 0], - "label" : "Food Probe", - "units" : "F", - "max_temp" : 300, - "data" : {}, - "button_list" : ["input_notify"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "mode_bar", - "type" : "mode_bar", - "position" : [200, 0], - "animation_enabled" : false, - "size" : [400, 60], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "mode_bar", - "text" : "Stop", - "data" : {}, - "touch_areas" : [] - }, - { - "name" : "fan_status", - "type" : "status_icon", - "position" : [10, 5], - "animation_enabled" : false, - "size" : [50, 50], - "glow" : true, - "icon" : "Fan", - "label" : "fan_status", - "active" : false, - "inactive_color" : [128,128,128,128], - "active_color" : [255,255,255,255], - "rotation" : 0, - "data" : {}, - "touch_areas" : [] - }, - { - "name" : "auger_status", - "type" : "status_icon", - "position" : [85, 5], - "animation_enabled" : false, - "size" : [50, 50], - "glow" : true, - "icon" : "Auger", - "label" : "auger_status", - "active" : false, - "inactive_color" : [128,128,128,128], - "active_color" : [255,255,255,255], - "rotation" : 0, - "data" : {}, - "touch_areas" : [] - }, - { - "name" : "igniter_status", - "type" : "status_icon", - "position" : [150, 5], - "animation_enabled" : false, - "size" : [50, 50], - "glow" : true, - "icon" : "Igniter", - "label" : "igniter_status", - "active" : false, - "inactive_color" : [128,128,128,128], - "active_color" : [255,255,255,255], - "rotation" : 0, - "data" : {}, - "touch_areas" : [] - }, - { - "name" : "menu_icon", - "type" : "menu_icon", - "position" : [740, 5], - "animation_enabled" : false, - "size" : [50, 50], - "glow" : true, - "icon" : "Hamburger", - "label" : "menu_icon", - "color" : [255,255,255,255], - "button_list" : ["menu_main"], - "button_value" : [], - "data" : {}, - "touch_areas" : [] - }, - { - "name" : "timer", - "type" : "timer", - "position" : [0, 380], - "animation_enabled" : false, - "size" : [200, 100], - "glow" : true, - "label" : "Timer", - "color" : [255,255,255,255], - "button_list" : [], - "button_value" : [], - "data" : { - "seconds" : 0 - }, - "touch_areas" : [] - }, - { - "name" : "lid_indicator", - "type" : "alert", - "position" : [600, 380], - "animation_enabled" : false, - "size" : [200, 100], - "glow" : true, - "label" : "Lid Open Detected", - "active" : false, - "color" : [0,200,0,255], - "data" : { - "text" : [ - "Lid Open", - "Detected" - ] - }, - "button_list" : [], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "p_mode", - "type" : "p_mode_control", - "position" : [600, 380], - "animation_enabled" : false, - "size" : [200, 100], - "glow" : true, - "label" : "P-Mode", - "active" : false, - "color" : [255,255,255,255], - "data" : { - "pmode" : 0 - }, - "button_list" : ["menu_pmode"], - "button_value" : [], - "touch_areas" : [] - }, - { - "name" : "smoke_plus", - "type" : "splus_control", - "position" : [650, 0], - "animation_enabled" : false, - "size" : [60, 60], - "glow" : false, - "label" : "P-Mode", - "active" : false, - "color" : [200,0,200,225], - "data" : {}, - "button_list" : ["cmd_splus"], - "touch_areas" : [], - "button_value" : ["on"] - }, - { - "name" : "hopper", - "type" : "hopper_status", - "position" : [580, 275], - "animation_enabled" : false, - "size" : [220, 110], - "glow" : true, - "label" : "Hopper Level", - "active" : false, - "color" : [255,255,255,255], - "color_levels" : [ - [225, 50, 50, 255], - [225, 150, 50, 255], - [225, 225, 50, 255], - [50, 225, 50, 255], - [255, 255, 255, 255] - ], - "data" : { - "level" : 100 - }, - "button_list" : ["cmd_hopper_level"], - "button_value" : [], - "touch_areas" : [] - } - ], - "menus" : { - "main" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "main_menu", - "title_text" : "Main Menu", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off"], - "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System"], - "button_value" : [], - "touch_areas" : [] - }, - "qrcode" : { - "type" : "qrcode", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "qr_code", - "ip_address" : "0.0.0.0", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close"], - "button_value" : [], - "touch_areas" : [] - }, - "main_reboot" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "main_menu_reboot", - "title_text" : "Reboot the System?", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "cmd_reboot", "menu_close"], - "button_text" : ["Close Menu", "Yes", "No"], - "button_value" : [], - "touch_areas" : [] - }, - "main_power_off" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "main_menu_power_off", - "title_text" : "Power Off the System?", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], - "button_text" : ["Close Menu", "Yes", "No"], - "button_value" : [], - "touch_areas" : [] - }, - "prime" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "menu_prime_start", - "title_text" : "Startup after priming?", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], - "button_text" : ["Close Menu", "Yes", "No"], - "button_value" : [], - "touch_areas" : [] - }, - "prime_startup" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "menu_prime", - "title_text" : "Select Amount to Prime:", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup"], - "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], - "button_value" : [0, 10, 25, 50], - "touch_areas" : [] - }, - "prime_only" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "menu_prime", - "title_text" : "Select Amount to Prime:", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly"], - "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], - "button_value" : [0, 10, 25, 50], - "touch_areas" : [] - }, - "startup" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "menu_startup", - "title_text" : "Do you want to start up?", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "cmd_startup", "menu_close"], - "button_text" : ["Close Menu", "Yes", "No"], - "button_value" : [], - "touch_areas" : [] - }, - "pmode" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "menu_startup", - "title_text" : "Select a PMode:", - "color" : [255,255,255,255], - "data" : {}, - "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], - "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], - "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - "touch_areas" : [] + "profile_1" : { + "home" : [], + "dash" : [ + { + "name" : "primary_gauge", + "type" : "gauge", + "position" : [225, 50], + "animation_enabled" : true, + "size" : [350, 350], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : true, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Primary", + "units" : "F", + "max_temp" : 600, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "control_panel", + "type" : "control_panel", + "position" : [200, 380], + "animation_enabled" : false, + "glow" : false, + "size" : [400, 100], + "label" : "control_panel", + "data" : {}, + "button_list" : ["menu_prime", "menu_startup", "cmd_monitor", "cmd_stop"], + "button_type" : ["Prime", "Startup", "Monitor", "Stop"], + "button_active" : "Stop", + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_0", + "type" : "gauge_compact", + "position" : [0, 55], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_1", + "type" : "gauge_compact", + "position" : [580, 55], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_2", + "type" : "gauge_compact", + "position" : [0, 160], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_3", + "type" : "gauge_compact", + "position" : [580, 160], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_4", + "type" : "gauge_compact", + "position" : [0, 275], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "mode_bar", + "type" : "mode_bar", + "position" : [200, 0], + "animation_enabled" : false, + "size" : [400, 60], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [0,0,0,100], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "mode_bar", + "text" : "Stop", + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "fan_status", + "type" : "status_icon", + "position" : [10, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Fan", + "label" : "fan_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "auger_status", + "type" : "status_icon", + "position" : [85, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Auger", + "label" : "auger_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "igniter_status", + "type" : "status_icon", + "position" : [150, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Igniter", + "label" : "igniter_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "menu_icon", + "type" : "menu_icon", + "position" : [740, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : false, + "icon" : "Hamburger", + "label" : "menu_icon", + "color" : [255,255,255,255], + "button_list" : ["menu_main"], + "button_value" : [], + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "timer", + "type" : "timer", + "position" : [0, 380], + "animation_enabled" : false, + "size" : [200, 100], + "glow" : false, + "label" : "Timer", + "fg_color" : [255,255,255,255], + "bg_color" : [0,0,0,100], + "button_list" : [], + "button_value" : [], + "data" : { + "seconds" : 0 + }, + "touch_areas" : [] + }, + { + "name" : "lid_indicator", + "type" : "alert", + "position" : [600, 380], + "animation_enabled" : false, + "size" : [200, 100], + "glow" : false, + "label" : "Lid Open Detected", + "active" : false, + "fg_color" : [0,200,0,255], + "bg_color" : [0,0,0,0], + "data" : { + "text" : [ + "Lid Open", + "Detected" + ] + }, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "p_mode", + "type" : "p_mode_control", + "position" : [600, 380], + "animation_enabled" : false, + "size" : [200, 100], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "fg_color" : [255,255,255,255], + "bg_color" : [0,0,0,0], + "data" : { + "pmode" : 0 + }, + "button_list" : ["menu_pmode"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "smoke_plus", + "type" : "splus_control", + "position" : [650, 0], + "animation_enabled" : false, + "size" : [60, 60], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "active_color" : [200,0,200,225], + "inactive_color" : [255,255,255,100], + "data" : {}, + "button_list" : ["cmd_splus"], + "touch_areas" : [], + "button_value" : ["on"] + }, + { + "name" : "hopper", + "type" : "hopper_status", + "position" : [580, 275], + "animation_enabled" : false, + "size" : [220, 110], + "glow" : false, + "label" : "Hopper Level", + "active" : false, + "color" : [255,255,255,255], + "color_levels" : [ + [225, 50, 50, 255], + [225, 150, 50, 255], + [225, 225, 50, 255], + [50, 225, 50, 255], + [255, 255, 255, 255] + ], + "data" : { + "level" : 100 + }, + "button_list" : ["cmd_hopper_level"], + "button_value" : [], + "touch_areas" : [] + } + ], + "menus" : { + "main" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_normal" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_monitor" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_recipe" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "system" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off", "menu_close"], + "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System", "Close Menu"], + "button_value" : [], + "touch_areas" : [] + }, + "qrcode" : { + "type" : "qrcode", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "qr_code", + "ip_address" : "0.0.0.0", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_reboot" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_reboot", + "title_text" : "Reboot the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_reboot", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "main_power_off" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_power_off", + "title_text" : "Power Off the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime_start", + "title_text" : "Startup after priming?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime_startup" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "prime_only" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "startup" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Do you want to start up?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_startup", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "pmode" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Select a PMode:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], + "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], + "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "touch_areas" : [] + }, + "message" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "message", + "title_text" : "Message", + "color" : [255,255,255,255], + "data" : { + "text" : "" + }, + "button_list" : ["menu_close", "menu_close"], + "button_text" : ["Close Menu", "OK"], + "button_value" : [], + "touch_areas" : [] + } }, - "message" : { - "type" : "menu", - "position" : [100, 40], - "animation_enabled" : false, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "message", - "title_text" : "Message", - "color" : [255,255,255,255], - "data" : { - "text" : "" - }, - "button_list" : ["menu_close", "menu_close"], - "button_text" : ["Close Menu", "OK"], - "button_value" : [], - "touch_areas" : [] + "input" : { + "hold" : { + "type" : "input_number", + "position" : [100, 40], + "animation_enabled" : true, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_hold", + "title_text" : "Enter Hold Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_hold", + "data" : { + "input": "", + "value" : 200, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + "notify" : { + "type" : "input_number", + "position" : [100, 40], + "animation_enabled" : true, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_notify", + "title_text" : "Enter Notify Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_notify", + "data" : { + "input": "", + "value" : 165, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + } } }, - "input" : { - "hold" : { - "type" : "input_number", - "position" : [100, 40], - "animation_enabled" : true, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "input_hold", - "title_text" : "Enter Hold Temperature:", - "color" : [255,255,255,255], - "command" : "cmd_hold", - "data" : { - "input": "", - "value" : 200, - "origin" : "" - }, - "step" : 5, - "button_list" : [], - "button_value" : [], - "touch_areas" : [] + "profile_2" : { + "home" : [], + "dash" : [ + { + "name" : "primary_gauge", + "type" : "gauge", + "position" : [65, 50], + "animation_enabled" : true, + "size" : [350, 350], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : true, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Primary", + "units" : "F", + "max_temp" : 600, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "control_panel", + "type" : "control_panel", + "position" : [40, 700], + "animation_enabled" : false, + "glow" : false, + "size" : [400, 100], + "label" : "control_panel", + "data" : {}, + "button_list" : ["menu_prime", "menu_startup", "cmd_monitor", "cmd_stop"], + "button_type" : ["Prime", "Startup", "Monitor", "Stop"], + "button_active" : "Stop", + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_0", + "type" : "gauge_compact", + "position" : [0, 380], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_1", + "type" : "gauge_compact", + "position" : [260, 380], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_2", + "type" : "gauge_compact", + "position" : [0, 490], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_3", + "type" : "gauge_compact", + "position" : [260, 490], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_4", + "type" : "gauge_compact", + "position" : [0, 600], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "mode_bar", + "type" : "mode_bar", + "position" : [65, 0], + "animation_enabled" : false, + "size" : [350, 60], + "fg_color" : [255, 255, 255, 255], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "mode_bar", + "text" : "Stop", + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "fan_status", + "type" : "status_icon", + "position" : [0, 0], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Fan", + "label" : "fan_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "auger_status", + "type" : "status_icon", + "position" : [0, 60], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Auger", + "label" : "auger_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "igniter_status", + "type" : "status_icon", + "position" : [0, 120], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Igniter", + "label" : "igniter_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "smoke_plus", + "type" : "splus_control", + "position" : [0, 180], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "color" : [200,0,200,225], + "data" : {}, + "button_list" : ["cmd_splus"], + "touch_areas" : [], + "button_value" : ["on"] + }, + { + "name" : "menu_icon", + "type" : "menu_icon", + "position" : [430, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : false, + "icon" : "Hamburger", + "label" : "menu_icon", + "color" : [255,255,255,255], + "button_list" : ["menu_main"], + "button_value" : [], + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "timer", + "type" : "timer", + "position" : [260, 600], + "animation_enabled" : false, + "size" : [220, 110], + "glow" : false, + "label" : "Timer", + "color" : [255,255,255,255], + "button_list" : [], + "button_value" : [], + "data" : { + "seconds" : 0 + }, + "touch_areas" : [] + }, + { + "name" : "lid_indicator", + "type" : "alert", + "position" : [260, 600], + "animation_enabled" : false, + "size" : [220, 110], + "glow" : false, + "label" : "Lid Open Detected", + "active" : false, + "color" : [0,200,0,255], + "data" : { + "text" : [ + "Lid Open", + "Detected" + ] + }, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "p_mode", + "type" : "p_mode_control", + "position" : [360, 320], + "animation_enabled" : false, + "size" : [120, 65], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "color" : [255,255,255,255], + "data" : { + "pmode" : 0 + }, + "button_list" : ["menu_pmode"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "hopper", + "type" : "hopper_status", + "position" : [0, 320], + "animation_enabled" : false, + "size" : [120, 65], + "glow" : false, + "label" : "Hopper Level", + "active" : false, + "color" : [255,255,255,255], + "color_levels" : [ + [225, 50, 50, 255], + [225, 150, 50, 255], + [225, 225, 50, 255], + [50, 225, 50, 255], + [255, 255, 255, 255] + ], + "data" : { + "level" : 100 + }, + "button_list" : ["cmd_hopper_level"], + "button_value" : [], + "touch_areas" : [] + } + ], + "menus" : { + "main" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_normal" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_monitor" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_recipe" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "system" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off", "menu_close"], + "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System", "Close Menu"], + "button_value" : [], + "touch_areas" : [] + }, + "qrcode" : { + "type" : "qrcode", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "qr_code", + "ip_address" : "0.0.0.0", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_reboot" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_reboot", + "title_text" : "Reboot the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_reboot", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "main_power_off" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_power_off", + "title_text" : "Power Off the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime_start", + "title_text" : "Startup after priming?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime_startup" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "prime_only" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "startup" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Do you want to start up?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_startup", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "pmode" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Select a PMode:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], + "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], + "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "touch_areas" : [] + }, + "message" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "message", + "title_text" : "Message", + "color" : [255,255,255,255], + "data" : { + "text" : "" + }, + "button_list" : ["menu_close", "menu_close"], + "button_text" : ["Close Menu", "OK"], + "button_value" : [], + "touch_areas" : [] + } }, - "notify" : { - "type" : "input_number", - "position" : [100, 40], - "animation_enabled" : true, - "size" : [600, 400], - "glow" : true, - "font" : "trebuc.ttf", - "label" : "input_notify", - "title_text" : "Enter Notify Temperature:", - "color" : [255,255,255,255], - "command" : "cmd_notify", - "data" : { - "input": "", - "value" : 165, - "origin" : "" - }, - "step" : 5, - "button_list" : [], - "button_value" : [], - "touch_areas" : [] + "input" : { + "hold" : { + "type" : "input_number", + "position" : [20, 40], + "animation_enabled" : true, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_hold", + "title_text" : "Enter Hold Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_hold", + "data" : { + "input": "", + "value" : 200, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + "notify" : { + "type" : "input_number", + "position" : [20, 40], + "animation_enabled" : true, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_notify", + "title_text" : "Enter Notify Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_notify", + "data" : { + "input": "", + "value" : 165, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + } } } } diff --git a/display/dsi_800x480t.py b/display/dsi_800x480t.py index 2129d781..ae3c95ee 100644 --- a/display/dsi_800x480t.py +++ b/display/dsi_800x480t.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python3 ''' ***************************************** PiFire Display Interface Library ***************************************** Description: This library supports using pygame - with a DSI attached touch display like the official - Raspberry Pi 7 inch DSI attached display. + with a DSI attached touch display on the Raspberry Pi + like the official Raspberry Pi 7 inch DSI attached display. This version supports mouse for development. @@ -19,9 +18,11 @@ import time import multiprocessing import pygame +from pygame import image as PyImage + from PIL import Image, ImageFilter from display.base_flex import DisplayBase -from display.flexobject_pygame import FlexObjectPygame as FlexObjPygame +from display.flexobject import FlexObject ''' Dummy backlight class for prototyping @@ -38,7 +39,12 @@ def __init__(self): class Display(DisplayBase): def __init__(self, dev_pins, buttonslevel='HIGH', rotation=0, units='F', config={}): - config['display_data_filename'] = "./display/dsi_800x480t.json" + # Set display profile based on rotation + self.rotation = config.get('rotation', 0) + if self.rotation in [0, 180]: + self.display_profile = 'profile_1' + else: + self.display_profile = 'profile_2' super().__init__(dev_pins, buttonslevel, rotation, units, config) def _init_display_device(self): @@ -61,6 +67,7 @@ def _init_input(self): self.input_enabled = True self.input_event = None self.touch_pos = (0,0) + self.DEBOUNCE = 100 # ms def _display_loop(self): """ @@ -158,7 +165,6 @@ def _display_loop(self): if self.display_active == 'home': if self.display_init: ''' Initialize Home Screen ''' - self._display_background() self._build_objects(self.background) self.display_init = False self.display_updated = True @@ -166,7 +172,6 @@ def _display_loop(self): elif self.display_active == 'dash': if self.display_init: ''' Initialize Dash Screen ''' - self._display_background() if self.dash_object_list == []: self._init_dash() self._restore_dash_objects() @@ -175,6 +180,7 @@ def _display_loop(self): self.display_updated = True else: self._update_dash_objects() + self._display_background() elif self.display_active is not None: if (('menu_' in self.display_active) or ('input_' in self.display_active)) and self.display_init: @@ -184,8 +190,8 @@ def _display_loop(self): self.display_init = False self.display_updated = True - ''' Perform any animations that need to be displayed. ''' - self._animate_objects() + ''' Draw all objects. Perform any animations that need to be displayed. ''' + self._draw_objects() if self.display_updated: self._display_canvas() @@ -208,22 +214,6 @@ def _display_loop(self): ============== Graphics / Display / Draw Methods ============= ''' - def _init_background(self): - super()._init_background() - ''' Convert image to PyGame surface ''' - strFormat = self.background.mode - size = self.background.size - raw_str = self.background.tobytes("raw", strFormat) - self.background_surface = pygame.image.fromstring(raw_str, size, strFormat) - - def _init_splash(self): - super()._init_splash() - ''' Convert image to PyGame surface ''' - strFormat = self.splash.mode - size = self.splash.size - raw_str = self.splash.tobytes("raw", strFormat) - self.splash = pygame.image.fromstring(raw_str, size, strFormat) - def _wake_display(self): self.backlight.power = True self.backlight.brightness = 100 @@ -236,61 +226,45 @@ def _sleep_display(self): self.backlight.power = False def _display_clear(self): - self.eventLogger.info('Screen Cleared.') self._sleep_display() self.display_surface.fill((0,0,0,255)) pygame.display.update() + self.display_canvas.paste((0,0,0,255), (0,0, self.WIDTH, self.HEIGHT)) + self.eventLogger.info('Screen Cleared.') def _display_canvas(self): + self._canvas_to_surface() pygame.display.update() - def _display_splash(self): - self.display_surface.blit(self.splash, (0,0)) - self._display_canvas() + def _canvas_to_surface(self): + # Convert temporary canvas to PyGame surface + strFormat = self.display_canvas.mode + size = self.display_canvas.size + raw_str = self.display_canvas.tobytes("raw", strFormat) + self.display_surface.blit(PyImage.fromstring(raw_str, size, strFormat), (0,0)) def _display_background(self): - self.display_surface.blit(self.background_surface, (0,0)) - self._display_canvas() + self.display_canvas.paste(self.background, (0,0)) def _capture_background(self): - pil_string_image = pygame.image.tostring(self.display_surface, 'RGBA', False) - pil_image = Image.frombytes('RGBA', (self.WIDTH, self.HEIGHT), pil_string_image) - self.menu_background = pil_image.filter(ImageFilter.GaussianBlur(radius = 5)) + self.menu_background = self.display_canvas.filter(ImageFilter.GaussianBlur(radius = 5)) def _display_menu_background(self): - strFormat = self.menu_background.mode - size = self.menu_background.size - raw_str = self.menu_background.tobytes("raw", strFormat) - background_surface = pygame.image.fromstring(raw_str, size, strFormat) - self.display_surface.blit(background_surface, (0,0)) - self._display_canvas() - - def _build_objects(self, background): - self.display_object_list = [] - - if self.display_active in ['home', 'dash']: - section_data = self.display_data[self.display_active] - elif 'menu_' in self.display_active: - section_data = [self.display_data['menus'][self.display_active.replace('menu_', '')]] - elif 'input_' in self.display_active: - section_data = [self.display_data['input'][self.display_active.replace('input_', '')]] - section_data[0]['data']['origin'] = self.input_origin - else: - return - - for object_data in section_data: - self.display_object_list.append(FlexObjPygame(object_data['type'], object_data, background)) - + self.display_canvas.paste(self.menu_background, (0,0)) + def _init_dash(self): self._init_framework() self._configure_dash() - self._build_objects(self.background) + self._build_objects(None) self._build_dash_map() self._store_dash_objects() ''' ====================== Input & Menu Code ======================== ''' + def _debounce(self): + pygame.time.delay(self.DEBOUNCE) + def _event_detect(self): """ Called to detect input events from buttons, encoder, touch, etc. @@ -304,16 +278,106 @@ def _event_detect(self): self.input_event = None self.touch_pos = (0,0) return - elif user_input == 'TOUCH': + elif user_input == 'TOUCH' and self.input_touch: self._process_touch() - elif user_input in ['UP', 'DOWN', 'ENTER']: - ''' TODO ''' - pass + elif user_input in ['UP', 'DOWN', 'ENTER'] and (self.input_button or self.input_encoder): + self._process_button() # Clear the input event and touch_pos self.input_event = None self.touch_pos = (0,0) + def _process_button(self): + self._debounce() + if self.display_active: + if 'dash' in self.display_active: + ''' + Process dash button events + ''' + self._capture_background() + self._store_dash_objects() + if self.status_data['mode'] == 'Stop': + self.display_active = 'menu_main' + elif self.status_data['mode'] in ['Startup', 'Reignite', 'Smoke', 'Hold', 'Shutdown']: + self.display_active = 'menu_main_active_normal' + elif self.status_data['mode'] == 'Monitor': + self.display_active = 'menu_main_active_monitor' + elif self.status_data['mode'] == 'Recipe': + self.display_active = 'menu_main_active_recipe' + else: + self.display_active = 'menu_main' + self.display_init = True + elif 'menu_' in self.display_active: + ''' + Process menu button events + ''' + objectData = self.display_object_list[0].get_object_data() + button_selected = objectData['data'].get('button_selected', None) + button_list = objectData.get('button_list', []) + + if button_selected is not None and button_list != []: + if self.input_event == 'UP': + if button_selected <= 1: + objectData['data']['button_selected'] = len(button_list) - 1 # Note: button_list has extra close_menu entry at index 0 + else: + objectData['data']['button_selected'] -= 1 + self.display_object_list[0].update_object_data(updated_objectData = objectData) + elif self.input_event == 'DOWN': + if len(button_list) - 1 > button_selected: + objectData['data']['button_selected'] += 1 + else: + objectData['data']['button_selected'] = 1 + self.display_object_list[0].update_object_data(updated_objectData = objectData) + elif self.input_event == 'ENTER': + if 'cmd_' in objectData['button_list'][button_selected]: + self.command = objectData['button_list'][button_selected] + if objectData.get('button_value', False): + self.command_data = objectData['button_value'][button_selected] + else: + self.command_data = None + self._command_handler() + elif objectData['button_list'][button_selected] == 'menu_close': + self.display_active = 'dash' + self.display_init = True + elif ('menu_' in objectData['button_list'][button_selected]) or ('input_' in objectData['button_list'][button_selected]): + if self.display_active == 'dash': + self._capture_background() + self._store_dash_objects() + if ('input_' in self.display_active) and ('input_' in objectData['button_list'][button_selected]) and ('button_value' in list(objectData.keys())): + self.input_origin = objectData['button_value'][button_selected] + self.display_active = objectData['button_list'][button_selected] + self.display_init = True + elif 'button_' in button_selected: + objectData['data']['input'] = objectData['button_list'][button_selected].replace('button_', '') + self.display_object_list[0].update_object_data(updated_objectData=objectData) + elif self.input_event == 'ENTER' and button_selected == None: + self.display_active = 'dash' + self.display_init = True + elif 'input_' in self.display_active: + ''' + Process input button events + ''' + objectData = self.display_object_list[0].get_object_data() + if self.input_event == 'UP': + objectData['data']['input'] = 'up' + self.display_object_list[0].update_object_data(updated_objectData=objectData) + if self.input_event == 'DOWN': + objectData['data']['input'] = 'down' + self.display_object_list[0].update_object_data(updated_objectData=objectData) + if self.input_event == 'ENTER': + self.command = objectData['command'] + self._command_handler() + self.display_active = 'dash' + self.display_init = True + else: + ''' + Wake the display & go to home/dash + ''' + self._wake_display() + self.display_active = 'home' if self.HOME_ENABLED else 'dash' + self.display_init = True + self.display_timeout = time.time() + self.TIMEOUT + def _process_touch(self): if self.display_active: ''' diff --git a/display/flexobject_pil.py b/display/flexobject.py similarity index 69% rename from display/flexobject_pil.py rename to display/flexobject.py index 65f5b829..8cfb35dc 100644 --- a/display/flexobject_pil.py +++ b/display/flexobject.py @@ -5,6 +5,28 @@ from PIL import Image, ImageDraw, ImageFont, ImageFilter from pygame import Rect # Needed for touch support +''' +The following is a map of the FlexObject types to their respective classes. +''' + +FlexObject_TypeMap = { + 'gauge' : 'GaugeCircle', + 'gauge_compact' : 'GaugeCompact', + 'mode_bar' : 'ModeBar', + 'control_panel' : 'ControlPanel', + 'status_icon' : 'StatusIcon', + 'menu_icon' : 'MenuIcon', + 'menu' : 'MenuGeneric', + 'qrcode' : 'MenuQRCode', + 'input_number' : 'InputNumber', + 'input_number_simple' : 'InputNumberSimple', + 'timer' : 'TimerStatus', + 'alert' : 'AlertMessage', + 'splus_control' : 'SPlusStatus', + 'p_mode_control' : 'PModeStatus', + 'hopper_status' : 'HopperStatus' +} + ''' Display Flex Object Class Definition ''' @@ -17,16 +39,19 @@ def __init__(self, objectType, objectData, background): 'animation_start' : False } self.background = background - self._init_surface() self._init_background() self.update_object_data(objectData) def _init_background(self): - ''' saves the slice of background image in PIL to be used for background ''' - crop_region = (self.objectData['position'][0], self.objectData['position'][1], self.objectData['position'][0] + self.objectData['size'][0], self.objectData['position'][1] + self.objectData['size'][0]) - self.objectBG = self.background.crop(crop_region) + if self.background is not None: + ''' saves the slice of background image in PIL to be used for background ''' + crop_region = (self.objectData['position'][0], self.objectData['position'][1], self.objectData['position'][0] + self.objectData['size'][0], self.objectData['position'][1] + self.objectData['size'][0]) + self.objectBG = self.background.crop(crop_region) + else: + self.objectBG = Image.new("RGBA", self.objectData['size']) def update_object_data(self, updated_objectData=None): + ''' If object was changed, update the objectData with the new values ''' if updated_objectData is not None: if updated_objectData['animation_enabled']: self.objectState['animation_active'] = True @@ -37,68 +62,17 @@ def update_object_data(self, updated_objectData=None): for key, value in updated_objectData.items(): self.objectData[key] = value - if self.objectType == 'gauge': - if self.objectState['animation_active']: - self.objectCanvas = self._animate_gauge() - else: - self.objectCanvas = self._draw_gauge(self.objectData['size'], self.objectData['fg_color'], self.objectData['bg_color'], - self.objectData['temps'], self.objectData['label'], units=self.objectData['units'], - max_temp=self.objectData['max_temp']) - self._define_generic_touch_area() - - if self.objectType == 'gauge_compact': - self.objectCanvas = self._draw_gauge_compact() - self._define_generic_touch_area() - - if self.objectType == 'mode_bar': - self.objectCanvas = self._draw_mode_bar(self.objectData['size'], self.objectData['text']) - - if self.objectType == 'control_panel': - self.objectCanvas = self._draw_control_panel(self.objectData['size'], self.objectData['button_list'], active=self.objectData['button_active']) - self._define_control_panel_touch_areas() - - if self.objectType == 'status_icon': - if self.objectState['animation_active']: - self.objectCanvas = self._animate_status_icon() - else: - self.objectCanvas = self._draw_status_icon() - - if self.objectType == 'menu_icon': - self.objectCanvas = self._draw_menu_icon(self.objectData['size']) - self._define_generic_touch_area() - - if self.objectType == 'menu': - self.objectCanvas = self._draw_menu() - - if self.objectType == 'qrcode': - self.objectCanvas = self._draw_qrcode() - self._define_generic_touch_area() + ''' If the object has input, process the input ''' + self._process_input() - if self.objectType == 'input_number': - self._process_number_input() - - if self.objectState['animation_active']: - self.objectCanvas = self._animate_input_number() - else: - self.objectCanvas = self._draw_input_number() - - if self.objectType == 'timer': - self.objectCanvas = self._draw_timer() - - if self.objectType == 'alert': - self.objectCanvas = self._draw_alert() - - if self.objectType == 'p_mode_control': - self.objectCanvas = self._draw_pmode_status() - self._define_generic_touch_area() - - if self.objectType == 'splus_control': - self.objectCanvas = self._draw_splus_status() - self._define_generic_touch_area() + ''' If the object has animation, process the animation ''' + if self.objectState['animation_active']: + self.objectCanvas = self._animate_object() + else: + self.objectCanvas = self._draw_object() - if self.objectType == 'hopper_status': - self.objectCanvas = self._draw_hopper_status() - self._define_generic_touch_area() + ''' Define the touch area - if applicable ''' + self._define_touch_areas() return self.objectCanvas @@ -113,71 +87,115 @@ def get_object_state(self): current_objectState = self.objectState.copy() return current_objectState - def _animate_gauge(self): - if self.objectState['animation_start']: - self.objectState['animation_start'] = False # Run animation start only once - self.objectState['animation_temps'] = self.objectData['temps'].copy() - self.objectState['animation_temps'][0] = self.objectState['animation_lastData']['temps'][0] - target_temp = self.objectData['temps'][0] - last_temp = self.objectState['animation_lastData']['temps'][0] - self.delta = target_temp - last_temp + def _draw_text(self, text, font_name, font_point_size, color, rect=False, bg_fill=None): + font = ImageFont.truetype(font_name, font_point_size) + font_bbox = font.getbbox(str(text)) # Grab the width of the text + font_canvas_size = (font_bbox[2], font_bbox[3]) + font_canvas = Image.new('RGBA', font_canvas_size) + font_draw = ImageDraw.Draw(font_canvas) + font_draw.text((0,0), str(text), font=font, fill=color) + font_canvas = font_canvas.crop(font_canvas.getbbox()) + if rect: + font_canvas_size = font_canvas.size + rect_canvas_size = (font_canvas_size[0] + 16, font_canvas_size[1] + 16) + rect_canvas = Image.new('RGBA', rect_canvas_size) + if bg_fill is not None: + rect_canvas.paste(bg_fill, (0,0) + rect_canvas.size) + rect_draw = ImageDraw.Draw(rect_canvas) + rect_draw.rounded_rectangle((0, 0, rect_canvas_size[0], rect_canvas_size[1]), radius=8, outline=color, width=3) + rect_canvas.paste(font_canvas, (8,8), font_canvas) + return rect_canvas + elif bg_fill is not None: + output_canvas = Image.new('RGBA', font_canvas.size) + output_canvas.paste(bg_fill, (0,0) + font_canvas.size) + output_canvas.paste(font_canvas, (0,0), font_canvas) + return output_canvas + else: + return font_canvas - if self.delta == 0: - self.objectState['animation_active'] = False - self.step_value = 0 - elif self.delta > 0: - self.step_value = int(self.delta / 3) if int(self.delta / 3) != 0 else 1 - else: - self.step_value = int(self.delta / 3) if int(self.delta / 3) != 0 else -1 + def _create_icon(self, charid, font_size, color, bg_fill=None): + # Get font and character size + font = ImageFont.truetype("./static/font/FA-Free-Solid.otf", font_size) + # Create canvas + font_bbox = font.getbbox(charid) # Grab the width of the text + font_width = font_bbox[2] + font_height = font_bbox[3] - if self.objectState['animation_temps'][0] != self.objectData['temps'][0]: - self.objectState['animation_temps'][0] += self.step_value + icon_canvas = Image.new('RGBA', (font_width, font_height)) + if bg_fill is not None: + icon_canvas.paste(bg_fill, (0,0) + icon_canvas.size) - if self.objectState['animation_temps'][0] <= 0: - self.objectState['animation_temps'][0] = self.objectData['temps'][0] - self.objectState['animation_active'] = False #if len(self.objectData['label']) <= 5 else True + # Create drawing object + draw = ImageDraw.Draw(icon_canvas) + draw.text((0, 0), charid, font=font, fill=color) + icon_canvas = icon_canvas.crop(icon_canvas.getbbox()) + return icon_canvas - elif (self.delta >= 0) and (abs(self.objectState['animation_temps'][0]) >= abs(self.objectData['temps'][0])): - self.objectState['animation_temps'][0] = self.objectData['temps'][0] - self.objectState['animation_active'] = False #if len(self.objectData['label']) <= 5 else True + def _define_touch_areas(self): + """ + Defines the touch area for the object. This object may be + overridden by subclasses. + """ + touch_area = Rect((self.objectData['position'], self.objectData['size'])) + # Create button rectangle / touch area and append to list + self.objectData['touch_areas'] = [touch_area] - elif (self.delta <= 0) and (abs(self.objectState['animation_temps'][0]) <= abs(self.objectData['temps'][0])): - self.objectState['animation_temps'][0] = self.objectData['temps'][0] - self.objectState['animation_active'] = False #if len(self.objectData['label']) <= 5 else True - ''' - if len(self.objectData['label']) > 5: - self.objectState['animation_label_position'] += 1 - if self.objectState['animation_label_position'] >= 100: - self.objectState['animation_label_position'] = 0 - ''' - - return self._draw_gauge(self.objectData['size'], self.objectData['fg_color'], self.objectData['bg_color'], - self.objectState['animation_temps'], self.objectData['label'], units=self.objectData['units'], - max_temp=self.objectData['max_temp']) - - def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0, 200, 255, 255), notify_point_color=(255, 255, 0, 255), units='F', max_temp=600): - ''' - Draw a gauge and return an Image canvas with that gauge. - - :param size: The size of the gauge to produce in (width, height) format. - :type size: tuple(int, int) - :param fg_color: The foreground color of the gauge in (r,g,b,a) format. - :type fg_color: tuple(int, int, int, int) - :param bg_color: The background color of the gauge in (r,g,b,a) format. - :type bg_color: tuple(int, int, int, int) - :param temps: The list of temperatures to display. [current, notify_point, set_point] - :type temps: list[float, float, float] - :param set_point_color: The set point color of the gauge in (r,g,b,a) format. This is used mainly for the primary gauge. - :type set_point_color: tuple(int, int, int, int) - :param notify_point_color: The notify point color of the gauge in (r,g,b,a) format. - :type notify_point_color: tuple(int, int, int, int) - :param str units: Either 'F' for fahrenheit or 'C' for celcius. - :param int max_temp: Maximum temperature to display on the gauge arc. - :return: Image object. - ''' - - output_size = size + def _scale_touch_area(self, rectangle, screen_size_old, screen_size_new): + """Scales a rectangle size and position according to the screen size change. + Args: + rectangle: A tuple of (x, y, width, height). + screen_size_old: The old screen size. + screen_size_new: The new screen size. + + Returns: + A tuple of (x, y, width, height) of the scaled rectangle. + """ + x, y, width, height = rectangle + scaled_width = int(width * (screen_size_new[0] / screen_size_old[0])) + scaled_height = int(height * (screen_size_new[1] / screen_size_old[1])) + xlated_x = int(x * (screen_size_new[0] / screen_size_old[0])) + xlated_y = int(y * (screen_size_new[1] / screen_size_old[1])) + return (xlated_x, xlated_y, scaled_width, scaled_height) + + def _transform_touch_area(self, touch_area, origin): + """ Transforms the touch area to the correct place on the screen. """ + return (touch_area[0] + origin[0], touch_area[1] + origin[1], touch_area[2], touch_area[3]) + + def _draw_object(self): + """ + This function will draw the object and return the object canvas. + The inheriting function will override this function. + """ + return self.objectCanvas + + def _animate_object(self): + """ + This function will animate the object and return the object canvas. + The inheriting function will override this function. + """ + return self._draw_object() + + def _process_input(self): + """ + This function will process the input and return the object canvas. + The inheriting function will override this function. + """ + pass + +class GaugeCircle(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): + """ + Draws a gauge on an image canvas based on the object's data. + + Returns: + Image: The image canvas with the gauge drawn on it. + """ + output_size = self.objectData['size'] + size = (400,400) # Create drawing object @@ -191,22 +209,22 @@ def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0 start_rad = 135 # Determine the radian (0-270) for the current temperature - temp_rad = 270 * min(temps[0]/max_temp, 1) + temp_rad = 270 * min(self.objectData['temps'][0]/self.objectData['max_temp'], 1) end_rad = start_rad + temp_rad - + # Draw Temperature Arc - draw.arc(coords, start=start_rad, end=end_rad, fill=fg_color, width=30) - + draw.arc(coords, start=start_rad, end=end_rad, fill=self.objectData['fg_color'], width=30) + # Draw Background Arc - draw.arc(coords, start=end_rad, end=45, fill=bg_color, width=30) - + draw.arc(coords, start=end_rad, end=45, fill=self.objectData['bg_color'], width=30) + # Current Temperature (Large Centered) - cur_temp = str(temps[0])[:5] + cur_temp = str(self.objectData['temps'][0])[:5] if len(cur_temp) < 5: font_point_size = round(size[1] * 0.3) # Font size as a ratio of the object size else: font_point_size = round(size[1] * 0.25) # Font size as a ratio of the object size - font = ImageFont.truetype("trebuc.ttf", font_point_size) + font = ImageFont.truetype(self.objectData['font'], font_point_size) font_bbox = font.getbbox(cur_temp) # Grab the width of the text font_width = font_bbox[2] - font_bbox[0] font_height = font_bbox[3] - font_bbox[1] @@ -214,30 +232,30 @@ def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0 label_y = (size[1] // 2) - (font_height // 1.1) label_origin = (label_x, label_y) - draw.text(label_origin, cur_temp, font=font, fill=fg_color) + draw.text(label_origin, cur_temp, font=font, fill=self.objectData['fg_color']) # Units Label (Small Centered) - unit_label = f'{units}°' + unit_label = f'{self.objectData["units"]}°' font_point_size = font_point_size = round((size[1] * 0.35) / 4) # Font size as a ratio of the object size - font = ImageFont.truetype("trebuc.ttf", font_point_size) - font_bbox = font.getbbox(units) # Grab the width of the text + font = ImageFont.truetype(self.objectData['font'], font_point_size) + font_bbox = font.getbbox(self.objectData['units']) # Grab the width of the text font_width = font_bbox[2] - font_bbox[0] font_height = font_bbox[3] - font_bbox[1] label_x = (size[0] // 2) - (font_width // 2) label_y = round((size[1] * 0.60)) label_origin = (label_x, label_y) - draw.text(label_origin, unit_label, font=font, fill=(255, 255, 255)) + draw.text(label_origin, unit_label, font=font, fill=self.objectData['fg_color']) # Gauge Label # Gauge Label Text - if len(label) > 7: - label_displayed = label[0:7] + if len(self.objectData['label']) > 7: + label_displayed = self.objectData['label'][0:7] else: - label_displayed = label + label_displayed = self.objectData['label'] font_point_size = round((size[1] * 0.55) / 4) # Font size as a ratio of the object size - font = ImageFont.truetype("trebuc.ttf", font_point_size) + font = ImageFont.truetype(self.objectData['font'], font_point_size) font_bbox = font.getbbox(label_displayed) # Grab the width of the text font_width = font_bbox[2] - font_bbox[0] font_height = font_bbox[3] - font_bbox[1] @@ -246,24 +264,23 @@ def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0 label_x = (size[0] // 2) - (font_width // 2) label_y = round((size[1] * 0.75)) label_origin = (label_x, label_y) - draw.text(label_origin, label_displayed, font=font, fill=(255, 255, 255)) + draw.text(label_origin, label_displayed, font=font, fill=self.objectData['fg_color']) # Gauge Label Rectangle # rounded_rectangle = (label_x-6, label_y+4, label_x + font_width + 8, label_y + font_height + 16) rounded_rectangle = (label_x-8, label_y+(font_bbox[1] - 8), label_x + font_width + 8, label_y + font_bbox[1] + font_height + 8) - draw.rounded_rectangle(rounded_rectangle, radius=8, outline=(255,255,255), width=3) - + draw.rounded_rectangle(rounded_rectangle, radius=8, outline=self.objectData['fg_color'], width=3) # Set Points Labels - if temps[1] > 0 and temps[2] > 0: + if self.objectData['temps'][1] > 0 and self.objectData['temps'][2] > 0: dual_label = 1 else: dual_label = 0 # Notify Point Label - if temps[1] > 0: - notify_point_label = f'{temps[1]}' + if self.objectData['temps'][1] > 0: + notify_point_label = f'{self.objectData["temps"][1]}' font_point_size = round((size[1] * (0.5 - (dual_label * 0.15))) / 4) # Font size as a ratio of the object size - font = ImageFont.truetype("trebuc.ttf", font_point_size) + font = ImageFont.truetype(self.objectData['font'], font_point_size) font_bbox = font.getbbox(notify_point_label) # Grab the width of the text font_width = font_bbox[2] - font_bbox[0] font_height = font_bbox[3] - font_bbox[1] @@ -271,22 +288,22 @@ def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0 label_x = (size[0] // 2) - (font_width // 2) - (dual_label * ((font_width // 2) + 10)) label_y = round((size[1] * (0.20 + (dual_label * 0.05)))) label_origin = (label_x, label_y) - draw.text(label_origin, notify_point_label, font=font, fill=notify_point_color) + draw.text(label_origin, notify_point_label, font=font, fill=self.objectData['np_color']) # Notify Point Label Rectangle rounded_rectangle = (label_x-8, label_y+(font_bbox[1] - 8), label_x + font_width + 8, label_y + font_bbox[1] + font_height + 8) # (label_x-6, label_y+2, label_x + font_width + 6, label_y + font_height + 4) - draw.rounded_rectangle(rounded_rectangle, radius=8, outline=notify_point_color, width=3) + draw.rounded_rectangle(rounded_rectangle, radius=8, outline=self.objectData['np_color'], width=3) # Draw Tic for notify point - setpoint = 270 * min(temps[1]/max_temp, 1) + setpoint = 270 * min(self.objectData['temps'][1]/self.objectData['max_temp'], 1) setpoint += start_rad - draw.arc(coords, start=setpoint - 1, end=setpoint + 1, fill=notify_point_color, width=30) + draw.arc(coords, start=setpoint - 1, end=setpoint + 1, fill=self.objectData['np_color'], width=30) # Set Point Label - if temps[2] > 0: - set_point_label = f'{temps[2]}' + if self.objectData['temps'][2] > 0: + set_point_label = f'{self.objectData["temps"][2]}' font_point_size = round((size[1] * (0.5 - (dual_label * 0.15))) / 4) # Font size as a ratio of the object size - font = ImageFont.truetype("trebuc.ttf", font_point_size) + font = ImageFont.truetype(self.objectData['font'], font_point_size) font_bbox = font.getbbox(set_point_label) # Grab the width of the text font_width = font_bbox[2] - font_bbox[0] font_height = font_bbox[3] - font_bbox[1] @@ -294,15 +311,15 @@ def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0 label_x = (size[0] // 2) - (font_width // 2) + (dual_label * ((font_width // 2) + 10)) label_y = round((size[1] * (0.20 + (dual_label * 0.05)))) label_origin = (label_x, label_y) - draw.text(label_origin, set_point_label, font=font, fill=set_point_color) + draw.text(label_origin, set_point_label, font=font, fill=self.objectData['sp_color']) # Set Point Label Rectangle rounded_rectangle = (label_x-8, label_y+(font_bbox[1] - 8), label_x + font_width + 8, label_y + font_bbox[1] + font_height + 8) - draw.rounded_rectangle(rounded_rectangle, radius=8, outline=set_point_color, width=3) + draw.rounded_rectangle(rounded_rectangle, radius=8, outline=self.objectData['sp_color'], width=3) # Draw Tic for set point - setpoint = 270 * min(temps[2]/max_temp, 1) + setpoint = 270 * min(self.objectData['temps'][2]/self.objectData['max_temp'], 1) setpoint += start_rad - draw.arc(coords, start=setpoint - 1, end=setpoint + 1, fill=set_point_color, width=30) + draw.arc(coords, start=setpoint - 1, end=setpoint + 1, fill=self.objectData['sp_color'], width=30) # Create drawing object canvas = Image.new("RGBA", (output_size[0], output_size[1])) @@ -311,7 +328,45 @@ def _draw_gauge(self, size, fg_color, bg_color, temps, label, set_point_color=(0 return canvas - def _draw_gauge_compact(self): + def _animate_object(self): + if self.objectState['animation_start']: + self.objectState['animation_start'] = False # Run animation start only once + self.objectState['animation_temps'] = self.objectData['temps'].copy() + self.objectState['animation_temps'][0] = self.objectState['animation_lastData']['temps'][0] + target_temp = self.objectData['temps'][0] + last_temp = self.objectState['animation_lastData']['temps'][0] + self.delta = target_temp - last_temp + + if self.delta == 0: + self.objectState['animation_active'] = False + self.step_value = 0 + elif self.delta > 0: + self.step_value = int(self.delta / 3) if int(self.delta / 3) != 0 else 1 + else: + self.step_value = int(self.delta / 3) if int(self.delta / 3) != 0 else -1 + + if self.objectState['animation_temps'][0] != self.objectData['temps'][0]: + self.objectState['animation_temps'][0] += self.step_value + + if self.objectState['animation_temps'][0] <= 0: + self.objectState['animation_temps'][0] = self.objectData['temps'][0] + self.objectState['animation_active'] = False #if len(self.objectData['label']) <= 5 else True + + elif (self.delta >= 0) and (abs(self.objectState['animation_temps'][0]) >= abs(self.objectData['temps'][0])): + self.objectState['animation_temps'][0] = self.objectData['temps'][0] + self.objectState['animation_active'] = False #if len(self.objectData['label']) <= 5 else True + + elif (self.delta <= 0) and (abs(self.objectState['animation_temps'][0]) <= abs(self.objectData['temps'][0])): + self.objectState['animation_temps'][0] = self.objectData['temps'][0] + self.objectState['animation_active'] = False #if len(self.objectData['label']) <= 5 else True + + return self._draw_object() + +class GaugeCompact(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): output_size = self.objectData['size'] size = (400,200) # Working Canvas Size @@ -328,11 +383,11 @@ def _draw_gauge_compact(self): else: label_displayed = self.objectData['label'] - gauge_label = self._draw_text(label_displayed, 'trebuc.ttf', 50, (255,255,255)) + gauge_label = self._draw_text(label_displayed, self.objectData['font'], 50, self.objectData['fg_color']) gauge.paste(gauge_label, (40,30), gauge_label) # Draw Temperature Value - current_temp = self._draw_text(self.objectData['temps'][0], 'trebuc.ttf', 100, (255,255,255)) + current_temp = self._draw_text(self.objectData['temps'][0], self.objectData['font'], 100, self.objectData['fg_color']) gauge.paste(current_temp, (40, 75), current_temp) # Determine if Displaying Notify Point AND Set Point @@ -345,22 +400,29 @@ def _draw_gauge_compact(self): font_size = 50 y_position_offset = 15 + if self.objectData["units"] == 'F': + x_position = 215 + y_position = 75 + else: + ''' Since Celcius can be a larger number, we need to adjust the positioning of the text ''' + x_position = 250 + y_position = 5 # Draw Notify Point Value if self.objectData['temps'][1]: - notify_point_temp = self._draw_text(self.objectData['temps'][1], 'trebuc.ttf', font_size, (255, 255, 0), rect=True) - gauge.paste(notify_point_temp, (215, 75 + y_position_offset), notify_point_temp) + notify_point_temp = self._draw_text(self.objectData['temps'][1], self.objectData['font'], font_size, self.objectData['np_color'], rect=True) + gauge.paste(notify_point_temp, (x_position, y_position + y_position_offset), notify_point_temp) # Draw Set Point Value if self.objectData['temps'][2]: - set_point_temp = self._draw_text(self.objectData['temps'][2], 'trebuc.ttf', font_size, (0, 255, 255), rect=True) + set_point_temp = self._draw_text(self.objectData['temps'][2], self.objectData['font'], font_size, self.objectData['sp_color'], rect=True) if dual_temp: y_position_offset = notify_point_temp.size[1] + 2 - gauge.paste(set_point_temp, (215, 75 + y_position_offset), set_point_temp) + gauge.paste(set_point_temp, (x_position, y_position + y_position_offset), set_point_temp) # Draw Units text = f'{self.objectData["units"]}°' - units_label = self._draw_text(text, 'Trebuchet_MS_Bold.ttf', 50, (255,255,255)) + units_label = self._draw_text(text, 'Trebuchet_MS_Bold.ttf', 50, self.objectData['fg_color']) #units_label_size = units_label.size() units_label_position = (330, (size[1] // 2)) gauge.paste(units_label, units_label_position, units_label) @@ -373,7 +435,7 @@ def _draw_gauge_compact(self): current_temp_adjusted = 360 current_temp_bar = (40, 160, current_temp_adjusted, 170) draw.rounded_rectangle(temp_bar, radius=10, fill=(0,0,0,200)) - draw.rounded_rectangle(current_temp_bar, radius=10, fill=(255,255,255,255)) + draw.rounded_rectangle(current_temp_bar, radius=10, fill=self.objectData['fg_color']) # Draw Notify Point Polygon if self.objectData['temps'][1]: @@ -381,7 +443,7 @@ def _draw_gauge_compact(self): if notify_temp_adjusted > 360: notify_temp_adjusted = 360 triangle_coords = [(notify_temp_adjusted, 168), (notify_temp_adjusted + 10, 150), (notify_temp_adjusted - 10, 150)] - draw.polygon(triangle_coords, fill=(255,255,0,255)) + draw.polygon(triangle_coords, fill=self.objectData['np_color']) # Draw Set Point Polygon if self.objectData['temps'][2]: @@ -389,43 +451,21 @@ def _draw_gauge_compact(self): if set_temp_adjusted > 360: set_temp_adjusted = 360 triangle_coords = [(set_temp_adjusted, 168), (set_temp_adjusted + 10, 150), (set_temp_adjusted - 10, 150)] - draw.polygon(triangle_coords, fill=(0,255,255,255)) + draw.polygon(triangle_coords, fill=self.objectData['sp_color']) # Create drawing object canvas = Image.new("RGBA", (output_size[0], output_size[1])) gauge = gauge.resize(output_size) canvas.paste(gauge, (0, 0), gauge) - return canvas + return canvas - def _draw_text(self, text, font_name, font_point_size, color, rect=False, bg_fill=None): - font = ImageFont.truetype(font_name, font_point_size) - font_bbox = font.getbbox(str(text)) # Grab the width of the text - font_canvas_size = (font_bbox[2], font_bbox[3]) - font_canvas = Image.new('RGBA', font_canvas_size) - font_draw = ImageDraw.Draw(font_canvas) - font_draw.text((0,0), str(text), font=font, fill=color) - font_canvas = font_canvas.crop(font_canvas.getbbox()) - if rect: - font_canvas_size = font_canvas.size - rect_canvas_size = (font_canvas_size[0] + 16, font_canvas_size[1] + 16) - rect_canvas = Image.new('RGBA', rect_canvas_size) - if bg_fill is not None: - rect_canvas.paste(bg_fill, (0,0) + rect_canvas.size) - rect_draw = ImageDraw.Draw(rect_canvas) - rect_draw.rounded_rectangle((0, 0, rect_canvas_size[0], rect_canvas_size[1]), radius=8, outline=color, width=3) - rect_canvas.paste(font_canvas, (8,8), font_canvas) - return rect_canvas - elif bg_fill is not None: - output_canvas = Image.new('RGBA', font_canvas.size) - output_canvas.paste(bg_fill, (0,0) + font_canvas.size) - output_canvas.paste(font_canvas, (0,0), font_canvas) - return output_canvas - else: - return font_canvas +class ModeBar(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) - def _draw_mode_bar(self, size, text): - output_size = size + def _draw_object(self): + output_size = self.objectData['size'] size = (400,60) @@ -434,15 +474,15 @@ def _draw_mode_bar(self, size, text): draw = ImageDraw.Draw(mode_bar) # Text Rectangle from top - draw.rounded_rectangle((10, -20, size[0]-10, size[1]-10), radius=8, outline=(255,255,255,255), width=2, fill=(0, 0, 0, 100)) + draw.rounded_rectangle((10, -20, size[0]-10, size[1]-10), radius=8, outline=self.objectData['fg_color'], width=2, fill=self.objectData['bg_color']) # Mode Text - if len(text) > 16: - label_displayed = text[0:16] + if len(self.objectData['text']) > 16: + label_displayed = self.objectData['text'][0:16] else: - label_displayed = text + label_displayed = self.objectData['text'] font_point_size = round(size[1] * 0.80) # Font size as a ratio of the object size - font = ImageFont.truetype("trebuc.ttf", font_point_size) + font = ImageFont.truetype(self.objectData['font'], font_point_size) font_bbox = font.getbbox(label_displayed) # Grab the width of the text font_width = font_bbox[2] - font_bbox[0] font_height = font_bbox[3] - font_bbox[1] @@ -450,34 +490,23 @@ def _draw_mode_bar(self, size, text): label_x = (size[0] // 2) - (font_width // 2) label_y = (size[1] // 2) - (font_height // 2) - 18 label_origin = (label_x, label_y) - draw.text(label_origin, label_displayed, font=font, fill=(255, 255, 255)) + draw.text(label_origin, label_displayed, font=font, fill=self.objectData['fg_color']) # Create drawing object canvas = Image.new("RGBA", (output_size[0], output_size[1])) mode_bar = mode_bar.resize(output_size) canvas.paste(mode_bar, (0, 0), mode_bar) - return canvas - - def _create_icon(self, charid, font_size, color, bg_fill=None): - # Get font and character size - font = ImageFont.truetype("./static/font/FA-Free-Solid.otf", font_size) - # Create canvas - font_bbox = font.getbbox(charid) # Grab the width of the text - font_width = font_bbox[2] - font_height = font_bbox[3] + return canvas - icon_canvas = Image.new('RGBA', (font_width, font_height)) - if bg_fill is not None: - icon_canvas.paste(bg_fill, (0,0) + icon_canvas.size) + def _define_touch_areas(self): + pass - # Create drawing object - draw = ImageDraw.Draw(icon_canvas) - draw.text((0, 0), charid, font=font, fill=color) - icon_canvas = icon_canvas.crop(icon_canvas.getbbox()) - return icon_canvas +class ControlPanel(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) - def _draw_control_panel(self, size, button_type, active='Stop'): + def _draw_object(self): output_size = self.objectData['size'] button_type = self.objectData['button_type'] active = self.objectData['button_active'] @@ -541,7 +570,7 @@ def _draw_control_panel(self, size, button_type, active='Stop'): return canvas - def _define_control_panel_touch_areas(self): + def _define_touch_areas(self): spacing = int((self.objectData['size'][0]) / (len(self.objectData['button_list']))) # Draw Dividing Lines self.objectData['touch_areas'] = [] @@ -555,7 +584,11 @@ def _define_control_panel_touch_areas(self): self.objectData['touch_areas'].append(touch_area) #print(f'Index: {index} Button: {self.objectData["button_list"][index]} Touch Area: {touch_area}') - def _draw_status_icon(self, rotation=0, breath_step=0): +class StatusIcon(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self, rotation=0, breath_step=0): # Save output size output_size = self.objectData['size'] type = self.objectData['icon'] @@ -614,9 +647,9 @@ def _draw_status_icon(self, rotation=0, breath_step=0): canvas = canvas.resize(output_size) - return canvas + return canvas - def _animate_status_icon(self): + def _animate_object(self): if self.objectState['animation_start']: self.objectState['animation_start'] = False # Run animation start only once self.objectState['animation_rotation'] = 0 # Set initial rotation @@ -632,12 +665,18 @@ def _animate_status_icon(self): if self.objectData['icon'] in ['Auger', 'Igniter', 'Recipe']: self.objectState['animation_breathe'] += 1 - return self._draw_status_icon(rotation=self.objectState['animation_rotation'], breath_step=self.objectState['animation_breathe']) + return self._draw_object(rotation=self.objectState['animation_rotation'], breath_step=self.objectState['animation_breathe']) + + def _define_touch_areas(self): + pass +class MenuIcon(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) - def _draw_menu_icon(self, size): + def _draw_object(self): # Save output size - output_size = size + output_size = self.objectData['size'] # Working Size size = (40,40) @@ -663,42 +702,25 @@ def _draw_menu_icon(self, size): return canvas - def _define_generic_touch_area(self): - touch_area = Rect((self.objectData['position'], self.objectData['size'])) - # Create button rectangle / touch area and append to list - self.objectData['touch_areas'] = [touch_area] - - def _scale_touch_area(self, rectangle, screen_size_old, screen_size_new): - """Scales a rectangle size and position according to the screen size change. - - Args: - rectangle: A tuple of (x, y, width, height). - screen_size_old: The old screen size. - screen_size_new: The new screen size. - - Returns: - A tuple of (x, y, width, height) of the scaled rectangle. - """ - x, y, width, height = rectangle - scaled_width = int(width * (screen_size_new[0] / screen_size_old[0])) - scaled_height = int(height * (screen_size_new[1] / screen_size_old[1])) - xlated_x = int(x * (screen_size_new[0] / screen_size_old[0])) - xlated_y = int(y * (screen_size_new[1] / screen_size_old[1])) - return (xlated_x, xlated_y, scaled_width, scaled_height) +class MenuGeneric(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) - def _transform_touch_area(self, touch_area, origin): - """ Transforms the touch area to the correct place on the screen. """ - return (touch_area[0] + origin[0], touch_area[1] + origin[1], touch_area[2], touch_area[3]) - - def _draw_menu(self): + def _draw_object(self): size = (600, 400) # Define working size canvas = Image.new("RGBA", size) # Create canvas output object draw = ImageDraw.Draw(canvas) # Create drawing object fg_color = self.objectData['color'] + bg_color = (0,0,0,255) # Clear any touch areas that might have been defined before self.objectData['touch_areas'] = [] + selected = self.objectData['data'].get('button_selected', None) + if selected == None and len(self.objectData['button_list']) > 1: + self.objectData['data']['button_selected'] = 1 + selected = 1 + # Rounded rectangle that fills the canvas size menu_padding = 10 # Define padding around outside of the menu rectangle draw.rounded_rectangle((menu_padding, menu_padding, size[0]-menu_padding, size[1]-menu_padding), radius=8, outline=(0,0,0,225), fill=(0, 0, 0, 250)) @@ -712,7 +734,7 @@ def _draw_menu(self): number_of_buttons = len(self.objectData['button_list']) - two_column_mode = True if number_of_buttons > 5 else False + two_column_mode = True if number_of_buttons > 6 else False if two_column_mode: button_height = 50 @@ -739,10 +761,10 @@ def _draw_menu(self): close_icon = self._create_icon('\uf00d', 34, (255,255,255)) close_position = (size[0] - (menu_padding * 4), (menu_padding * 2)) canvas.paste(close_icon, close_position, close_icon) - close_touch_area = (close_position[0]+self.objectData['position'][0], close_position[1]+self.objectData['position'][1], close_icon.width, close_icon.height) - close_touch_area = self._scale_touch_area(close_touch_area, size, self.objectData['size']) + close_touch_area = (close_position[0], close_position[1], close_icon.width, close_icon.height) scaled_touch_area = self._scale_touch_area(close_touch_area, size, self.objectData['size']) - self.objectData['touch_areas'].append(Rect(scaled_touch_area)) + transformed_touch_area = self._transform_touch_area(scaled_touch_area, self.objectData['position']) + self.objectData['touch_areas'].append(Rect(transformed_touch_area)) else: if button_count > 10: break # Stop if at 11 items @@ -758,16 +780,22 @@ def _draw_menu(self): rect_size = (button_width, button_height) rect_coords = (rect_position[0], rect_position[1], rect_position[0] + rect_size[0], rect_position[1] + rect_size[1]) - - draw.rounded_rectangle(rect_coords, radius=8, outline=(255,255,255,255), fill=(0, 0, 0, 250)) + if selected == index: + # Reverse colors if selected + draw.rounded_rectangle(rect_coords, radius=8, outline=(255,255,255,255), fill=fg_color) + else: + draw.rounded_rectangle(rect_coords, radius=8, outline=(255,255,255,255), fill=bg_color) # Put button text inside rectangle if len(self.objectData['button_text'][index]) > 25: label_displayed = self.objectData['button_text'][index][0:25] else: label_displayed = self.objectData['button_text'][index] - - label = self._draw_text(label_displayed, self.objectData['font'], 35, fg_color) + if selected == index: + # Reverse colors if selected + label = self._draw_text(label_displayed, self.objectData['font'], 35, bg_color) + else: + label = self._draw_text(label_displayed, self.objectData['font'], 35, fg_color) label_x = rect_position[0] + (rect_size[0] // 2) - (label.width // 2) label_y = rect_position[1] + (rect_size[1] // 2) - (label.height // 2) label_position = (label_x, label_y) @@ -789,7 +817,14 @@ def _draw_menu(self): return canvas - def _draw_qrcode(self): + def _define_touch_areas(self): + pass + +class MenuQRCode(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): size = (600, 400) # Define working size canvas = Image.new("RGBA", size) # Create canvas output object draw = ImageDraw.Draw(canvas) # Create drawing object @@ -814,10 +849,15 @@ def _draw_qrcode(self): img_qr = img_qr.resize((300,300)) position = (150, 60) canvas.paste(img_qr, position) + canvas = canvas.resize(self.objectData['size']) return canvas - def _draw_input_number(self): +class InputNumber(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): size = (600, 400) # Define working size canvas = Image.new("RGBA", size) # Create canvas output object draw = ImageDraw.Draw(canvas) # Create drawing object @@ -833,10 +873,10 @@ def _draw_input_number(self): close_icon = self._create_icon('\uf00d', 34, (255,255,255)) close_position = (size[0] - (menu_padding * 4), (menu_padding * 2)) canvas.paste(close_icon, close_position, close_icon) - close_touch_area = (close_position[0]+self.objectData['position'][0], close_position[1]+self.objectData['position'][1], close_icon.width, close_icon.height) - close_touch_area = self._scale_touch_area(close_touch_area, size, self.objectData['size']) + close_touch_area = (close_position[0], close_position[1], close_icon.width, close_icon.height) scaled_touch_area = self._scale_touch_area(close_touch_area, size, self.objectData['size']) - self.objectData['touch_areas'].append(Rect(scaled_touch_area)) + transformed_touch_area = self._transform_touch_area(scaled_touch_area, self.objectData['position']) + self.objectData['touch_areas'].append(Rect(transformed_touch_area)) self.objectData['button_list'].append('menu_close') # Menu Title @@ -944,9 +984,11 @@ def _draw_input_number(self): self.objectData['touch_areas'].append(Rect(scaled_touch_area)) self.objectData['button_list'].append(f'button_{col}') + # Resize for output + canvas = canvas.resize(self.objectData['size']) return canvas - def _animate_input_number(self): + def _animate_object(self): if self.objectState['animation_start']: self.objectState['animation_start'] = False # Run animation start only once self.objectState['animation_counter'] = 0 # Setup a counter for number of frames to produce @@ -957,14 +999,181 @@ def _animate_input_number(self): self.objectState['animation_active'] = False # Disable animation after one frame self.objectState['animation_input'] = '' - canvas = self._draw_input_number() - self.objectState['animation_counter'] += 1 # Increment the frame counter - return canvas + return self._draw_object() + + def _process_input(self): + if self.objectData['data']['input'] != '': + ''' Check first for up / down input ''' + if self.objectData['data']['input'] == 'up': + self.objectData['data']['value'] += self.objectData['step'] - def _process_number_input(self): + if self.objectData['data']['input'] == 'down': + self.objectData['data']['value'] -= self.objectData['step'] + if self.objectData['data']['value'] < 0: + self.objectData['data']['value'] = 0 + + ''' Convert value to list of characters ''' + temp_string = str(self.objectData['data']['value']) + self.objectState['value'] = [char for char in temp_string] + + if self.objectData['data']['input'] == 'DEL': + + if len(self.objectState['value']) > 1: + ''' If a float, delete back to the decimal value ''' + if '.' in self.objectState['value'] and self.objectState['value'][-2] == '.': + self.objectState['value'].pop() + self.objectState['value'].pop() + else: + self.objectState['value'].pop() + else: + self.objectState['value'] = ['0'] + + if '.' in self.objectState['value'] and self.objectData['data']['input'] == '.': + pass + elif self.objectData['data']['input'] in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.']: + + if len(self.objectState['value']) > 5: + pass + if '.' in self.objectState['value']: + self.objectState['value'].pop() + self.objectState['value'].append(self.objectData['data']['input']) + elif len(self.objectState['value']) == 3 and self.objectData['data']['input'] == '.': + self.objectState['value'].append(self.objectData['data']['input']) + elif len(self.objectState['value']) < 3: + self.objectState['value'].append(self.objectData['data']['input']) + + ''' Combine list of characters back to string and then back to a float or int ''' + temp_string = "".join([str(i) for i in self.objectState['value']]) + + if '.' in temp_string: + self.objectData['data']['value'] = float(temp_string) + else: + self.objectData['data']['value'] = int(temp_string) + + def _define_touch_areas(self): + pass + +class InputNumberSimple(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): + size = (600, 400) # Define working size + canvas = Image.new("RGBA", size) # Create canvas output object + draw = ImageDraw.Draw(canvas) # Create drawing object + button_pushed = self.objectState.get('animation_input', '') + self.objectData['touch_areas'] = [] + self.objectData['button_list'] = [] + # Rounded rectangle that fills the canvas size + menu_padding = 10 # Define padding around outside of the menu rectangle + draw.rounded_rectangle((menu_padding, menu_padding, size[0]-menu_padding, size[1]-menu_padding), radius=8, outline=(0,0,0,225), fill=(0, 0, 0, 250)) + + # Close Icon Upper Right + close_icon = self._create_icon('\uf00d', 34, (255,255,255)) + close_position = (size[0] - (menu_padding * 4), (menu_padding * 2)) + canvas.paste(close_icon, close_position, close_icon) + close_touch_area = (close_position[0], close_position[1], close_icon.width, close_icon.height) + scaled_touch_area = self._scale_touch_area(close_touch_area, size, self.objectData['size']) + transformed_touch_area = self._transform_touch_area(scaled_touch_area, self.objectData['position']) + self.objectData['touch_areas'].append(Rect(transformed_touch_area)) + self.objectData['button_list'].append('menu_close') + + # Menu Title + title = self._draw_text(self.objectData['title_text'], self.objectData['font'], 35, self.objectData['color'], rect=False, bg_fill=(0,0,0,250)) + title_x = (size[0] // 2) - (title.width // 2) + title_y = 15 + canvas.paste(title, (title_x, title_y)) + + # Number Display + number_entry_position = (40, 80) + number_entry_size = (340, 200) + number_entry_coords = number_entry_position + (number_entry_position[0] + number_entry_size[0], number_entry_position[1] + number_entry_size[1]) + number_entry_bg_color = (50,50,50) + draw.rounded_rectangle(number_entry_coords, radius=8, fill=number_entry_bg_color) + number_digits = self._draw_text(self.objectData['data']['value'], self.objectData['font'], 160, self.objectData['color'], bg_fill=number_entry_bg_color) + number_digits_position = (number_entry_position[0] + ((number_entry_size[0] // 2) - (number_digits.width // 2)), number_entry_position[1] + (number_entry_size[1] // 2) - (number_digits.height // 2)) + canvas.paste(number_digits, number_digits_position) + + # Up Arrow + if button_pushed == 'up': + bg_fill = (255,255,255,255) + fg_fill = (0,0,0,255) + else: + bg_fill = (0,0,0,255) + fg_fill = self.objectData['color'] + button_position = (420, 80) + button_size = (140, 80) + button_coords = button_position + (button_position[0] + button_size[0], button_position[1] + button_size[1]) + draw.rounded_rectangle(button_coords, radius=8, outline=fg_fill, fill=bg_fill, width=3) + button_icon = self._create_icon('\uf077', 35, fg_fill, bg_fill=bg_fill) + button_icon_position = (button_position[0] + ((button_size[0] // 2) - (button_icon.width // 2)), button_position[1] + (button_size[1] // 2) - (button_icon.height // 2)) + canvas.paste(button_icon, button_icon_position) + # Scale and Store Touch Area + button_touch_area = button_position + button_size + scaled_touch_area = self._scale_touch_area(button_touch_area, size, self.objectData['size']) + transform_touch_area = self._transform_touch_area(scaled_touch_area, self.objectData['position']) + self.objectData['touch_areas'].append(Rect(transform_touch_area)) + self.objectData['button_list'].append('button_up') + + # Down Arrow + if button_pushed == 'down': + bg_fill = (255,255,255,255) + fg_fill = (0,0,0,255) + else: + bg_fill = (0,0,0,255) + fg_fill = self.objectData['color'] + button_position = (420, 200) + button_size = (140, 80) + button_coords = button_position + (button_position[0] + button_size[0], button_position[1] + button_size[1]) + draw.rounded_rectangle(button_coords, radius=8, outline=fg_fill, fill=bg_fill, width=3) + button_icon = self._create_icon('\uf078', 35, fg_fill, bg_fill=bg_fill) + button_icon_position = (button_position[0] + ((button_size[0] // 2) - (button_icon.width // 2)), button_position[1] + (button_size[1] // 2) - (button_icon.height // 2)) + canvas.paste(button_icon, button_icon_position) + # Scale and Store Touch Area + button_touch_area = button_position + button_size + scaled_touch_area = self._scale_touch_area(button_touch_area, size, self.objectData['size']) + transform_touch_area = self._transform_touch_area(scaled_touch_area, self.objectData['position']) + self.objectData['touch_areas'].append(Rect(transform_touch_area)) + self.objectData['button_list'].append('button_down') + + # Enter Button + button_position = (180, 300) + button_size = (240, 80) + button_coords = button_position + (button_position[0] + button_size[0], button_position[1] + button_size[1]) + draw.rounded_rectangle(button_coords, radius=8, outline=self.objectData['color'], width=3) + button_text = self._draw_text('ENTER', self.objectData['font'], 60, self.objectData['color'], bg_fill=(0,0,0)) + button_text_position = (button_position[0] + ((button_size[0] // 2) - (button_text.width // 2)), button_position[1] + (button_size[1] // 2) - (button_text.height // 2)) + canvas.paste(button_text, button_text_position) + # Scale and Store Touch Area + button_touch_area = button_position + button_size + scaled_touch_area = self._scale_touch_area(button_touch_area, size, self.objectData['size']) + transform_touch_area = self._transform_touch_area(scaled_touch_area, self.objectData['position']) + self.objectData['touch_areas'].append(Rect(transform_touch_area)) + self.objectData['button_list'].append(self.objectData['command']) + + # Resize for output + canvas = canvas.resize(self.objectData['size']) + return canvas + + def _animate_object(self): + if self.objectState['animation_start']: + self.objectState['animation_start'] = False # Run animation start only once + self.objectState['animation_counter'] = 0 # Setup a counter for number of frames to produce + self.objectState['animation_input'] = self.objectData['data']['input'] # Save input from user + self.objectData['data']['input'] = '' # Clear user input + + if self.objectState['animation_counter'] > 1: + self.objectState['animation_active'] = False # Disable animation after one frame + self.objectState['animation_input'] = '' + + self.objectState['animation_counter'] += 1 # Increment the frame counter + + return self._draw_object() + + def _process_input(self): if self.objectData['data']['input'] != '': ''' Check first for up / down input ''' if self.objectData['data']['input'] == 'up': @@ -1013,10 +1222,18 @@ def _process_number_input(self): else: self.objectData['data']['value'] = int(temp_string) - def _draw_timer(self): + def _define_touch_areas(self): + pass + +class TimerStatus(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): output_size = self.objectData['size'] size = (400,200) # Working Canvas Size - fg_color = self.objectData['color'] + fg_color = self.objectData['fg_color'] + bg_color = self.objectData['bg_color'] # Create drawing object canvas = Image.new("RGBA", size) @@ -1025,7 +1242,7 @@ def _draw_timer(self): # If not in use, display empty box if self.objectData['data']['seconds'] > 0: # Timer Background - draw.rounded_rectangle((15, 15, size[0]-15, size[1]-15), radius=20, fill=(255,255,255,100)) + draw.rounded_rectangle((15, 15, size[0]-15, size[1]-15), radius=20, fill=bg_color) # Draw Stopwatch Icon timer_icon = self._create_icon('\uf2f2', 35, fg_color) @@ -1054,10 +1271,18 @@ def _draw_timer(self): return canvas - def _draw_alert(self): + def _define_touch_areas(self): + pass + +class AlertMessage(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): output_size = self.objectData['size'] size = (400,200) # Working Canvas Size - fg_color = self.objectData['color'] + fg_color = self.objectData['fg_color'] + bg_color = self.objectData['bg_color'] # Create canvas & drawing object canvas = Image.new("RGBA", size) @@ -1065,7 +1290,7 @@ def _draw_alert(self): if self.objectData['active']: # Draw Rectangle - draw.rounded_rectangle((15, 15, size[0]-15, size[1]-15), radius=20, outline=fg_color, width=6) + draw.rounded_rectangle((15, 15, size[0]-15, size[1]-15), radius=20, outline=fg_color, width=6, fill=bg_color) # Draw Alert Icon fg_color_alpha = list(fg_color) @@ -1098,10 +1323,18 @@ def _draw_alert(self): return canvas - def _draw_pmode_status(self): + def _define_touch_areas(self): + pass + +class PModeStatus(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): output_size = self.objectData['size'] size = (400,200) # Working Canvas Size - fg_color = self.objectData['color'] + fg_color = self.objectData['fg_color'] + bg_color = self.objectData['bg_color'] # Create canvas & drawing object canvas = Image.new("RGBA", size) @@ -1109,7 +1342,7 @@ def _draw_pmode_status(self): if self.objectData['active']: draw = ImageDraw.Draw(canvas) # Draw Rectangle - draw.rounded_rectangle((15, 15, size[0]-15, size[1]-15), radius=20, outline=fg_color, width=6) + draw.rounded_rectangle((15, 15, size[0]-15, size[1]-15), radius=20, outline=fg_color, width=6, fill=bg_color) # Draw PMode Icon fg_color_alpha = list(fg_color) @@ -1140,8 +1373,12 @@ def _draw_pmode_status(self): canvas.paste(resized, (0, 0), resized) return canvas + +class SPlusStatus(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) - def _draw_splus_status(self): + def _draw_object(self): output_size = self.objectData['size'] size = (200,200) # Working Canvas Size @@ -1150,21 +1387,18 @@ def _draw_splus_status(self): canvas = Image.new("RGBA", size) draw = ImageDraw.Draw(canvas) - fg_color = self.objectData['color'] - - if not self.objectData['active']: - fg_color = (255,255,255,125) + color = self.objectData['active_color'] if self.objectData['active'] else self.objectData['inactive_color'] # Draw Rectangle padding = 25 - draw.rounded_rectangle((padding, padding, size[0]-padding, size[1]-padding), radius=20, outline=fg_color, width=6) + draw.rounded_rectangle((padding, padding, size[0]-padding, size[1]-padding), radius=20, outline=color, width=6) # Draw Smoke Plus Icon(s) - cloud_icon = self._create_icon('\uf0c2', 85, fg_color) + cloud_icon = self._create_icon('\uf0c2', 85, color) cloud_icon_pos = ((size[0] // 2) - (cloud_icon.width // 2) - 8, (size[1] // 2) - (cloud_icon.height // 2)) canvas.paste(cloud_icon, cloud_icon_pos, cloud_icon) - plus_icon = self._create_icon('\uf067', 50, fg_color) + plus_icon = self._create_icon('\uf067', 50, color) plus_icon_pos = (120, 50) #plus_icon_pos = ((size[0] // 2) - (plus_icon.width // 2), (size[1] // 2) - (plus_icon.height // 2)) canvas.paste(plus_icon, plus_icon_pos, plus_icon) @@ -1176,7 +1410,11 @@ def _draw_splus_status(self): return canvas - def _draw_hopper_status(self): +class HopperStatus(FlexObject): + def __init__(self, objectType, objectData, background): + super().__init__(objectType, objectData, background) + + def _draw_object(self): output_size = self.objectData['size'] size = (400,200) # Working Canvas Size #fg_color = self.objectData['color'] @@ -1224,4 +1462,5 @@ def _draw_hopper_status(self): canvas = Image.new("RGBA", (output_size[0], output_size[1])) canvas.paste(resized, (0, 0), resized) - return canvas + return canvas + \ No newline at end of file diff --git a/display/flexobject_pygame.py b/display/flexobject_pygame.py deleted file mode 100644 index 337f33e3..00000000 --- a/display/flexobject_pygame.py +++ /dev/null @@ -1,48 +0,0 @@ -''' - Imported Libraries -''' - -from pygame import Surface, SRCALPHA -from pygame import image as PyImage -from PIL import Image, ImageFilter -from display.flexobject_pil import FlexObject - -''' -Display Flex Object Class Definition -''' -class FlexObjectPygame(FlexObject): - def __init__(self, objectType, objectData, background): - super().__init__(objectType, objectData, background) - - def _init_surface(self): - ''' pygame surface for output ''' - self.objectSurface = Surface(self.objectData['size'], SRCALPHA, 32) # Foreground Surface - - def _canvas_to_surface(self): - # Create Temporary Canvas - canvas = Image.new("RGBA", self.objectData['size']) - # Paste Background Chunk onto canvas - canvas.paste(self.objectBG, (0,0)) - # Paste Working Canvas onto canvas - if self.objectData['glow']: - glow = self.objectCanvas.filter(ImageFilter.GaussianBlur(radius = 5)) - canvas.paste(glow, (0,0), glow) - canvas.paste(self.objectCanvas, (0,0), self.objectCanvas) - - # Convert temporary canvas to PyGame surface - strFormat = canvas.mode - size = canvas.size - raw_str = canvas.tobytes("raw", strFormat) - - self.objectSurface = PyImage.fromstring(raw_str, size, strFormat) - - def update_object_data(self, updated_objectData=None): - super().update_object_data(updated_objectData) - - ''' Convert the PIL Canvas to Pygame Surface ''' - self._canvas_to_surface() - - return self.objectSurface - - def get_object_surface(self): - return self.objectSurface diff --git a/display/protoflex.py b/display/protoflex.py new file mode 100644 index 00000000..f23016a3 --- /dev/null +++ b/display/protoflex.py @@ -0,0 +1,423 @@ +''' +***************************************** +PiFire Display Interface Library +***************************************** + + Description: This is a prototype library for the display device using the flex + configuration. This library will take in a flex configuration file and configure + the display accordingly. + + This version supports mouse for development, touch, and keyboard input. + + All display output is done through the Pygame library. + +***************************************** +''' + +''' + Imported Libraries +''' +import time +import multiprocessing +import pygame +from pygame import image as PyImage + +from PIL import Image, ImageFilter +from display.base_flex import DisplayBase +from display.flexobject import FlexObject + +''' +Dummy backlight class for prototyping +''' +class DummyBacklight(): + def __init__(self): + self.brightness = 100 + self.power = True + self.fade_duration = 1 + +''' +Display class definition +''' +class Display(DisplayBase): + + def __init__(self, dev_pins, buttonslevel='HIGH', rotation=0, units='F', config={}): + # Set display profile based on rotation + self.rotation = config.get('rotation', 0) + if self.rotation in [0, 180]: + self.display_profile = 'profile_1' + else: + self.display_profile = 'profile_2' + super().__init__(dev_pins, buttonslevel, rotation, units, config) + + def _init_display_device(self): + ''' Init backlight ''' + if self.real_hardware: + # Use the rpi-backlight module if running on the RasPi + from rpi_backlight import Backlight + self.backlight = Backlight() + else: + # Else use a fake module class for backlight + self.backlight = DummyBacklight() + + self._wake_display() + + # Setup & Start Display Loop Worker + display_worker = multiprocessing.Process(target=self._display_loop) + display_worker.start() + + def _init_input(self): + self.input_enabled = True + self.input_event = None + self.touch_pos = (0,0) + self.DEBOUNCE = 100 # ms + + def _display_loop(self): + """ + Main display loop worker + """ + # Init Device + pygame.init() + # Set the pygame window name (for debug) + pygame.display.set_caption('PiFire Device Display') + # Create Display Surface + + if self.real_hardware: + flags = pygame.FULLSCREEN | pygame.DOUBLEBUF + self.display_surface = pygame.display.set_mode((0, 0), flags) + pygame.mouse.set_visible(False) # make mouse pointer invisible + else: + self.display_surface = pygame.display.set_mode(size=(self.WIDTH, self.HEIGHT), flags=pygame.SHOWN) + + self.clock = pygame.time.Clock() + + self.display_loop_active = True + + ''' Display the Splash Screen on Startup ''' + self._display_splash() + pygame.time.delay(self.SPLASH_DELAY) # Hold splash screen for designated time + self._display_clear() + + self.command = None + self.display_active = None + self.display_timeout = None + self.display_init = True + self.display_updated = False + + self.dash_object_list = [] + + refresh_data = 0 + + ''' Display Loop ''' + while self.display_loop_active: + ''' Fetch display data every 200ms ''' + now = time.time() + if now - refresh_data > 0.2: + self._fetch_data() + refresh_data = now + + ''' Poll for PyGame Events ''' + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.display_loop_active = False + break + # Check for mouse inputs + elif event.type == pygame.MOUSEBUTTONDOWN: + mouse_x, mouse_y = pygame.mouse.get_pos() + #print(f'{mouse_x}, {mouse_y}') + self.touch_pos = (mouse_x, mouse_y) + self.touch_held = True + elif event.type == pygame.MOUSEBUTTONUP: + self.touch_held = False + + # Check for touch inputs + elif event.type == pygame.FINGERDOWN: + touch_x = int(event.x * self.display_surface.get_width()) + touch_y = int(event.y * self.display_surface.get_height()) + #print(f'{touch_x}, {touch_y}') + self.touch_pos = (touch_x, touch_y) + self.touch_held = True + elif event.type == pygame.FINGERUP: + self.touch_held = False + + ''' Check for pressed keys ''' + keys = pygame.key.get_pressed() + if keys[pygame.K_UP]: + self.input_event = 'UP' + elif keys[pygame.K_DOWN]: + self.input_event = 'DOWN' + elif keys[pygame.K_RETURN]: + self.input_event = 'ENTER' + elif keys[pygame.K_x] or keys[pygame.K_q]: + self.display_loop_active = False + break + elif self.touch_pos != (0,0): + self.input_event = 'TOUCH' + + ''' Normal display loop''' + self._event_detect() + + if self.display_active != None: + + if self.display_timeout: + if time.time() > self.display_timeout: + self.display_timeout = None + self.display_active = None + self.display_init = True + + if self.display_active == 'home': + if self.display_init: + ''' Initialize Home Screen ''' + self._build_objects() + self.display_init = False + self.display_updated = True + + elif self.display_active == 'dash': + if self.display_init: + ''' Initialize Dash Screen ''' + if self.dash_object_list == []: + self._init_dash() + self._restore_dash_objects() + self._update_dash_objects() + self.display_init = False + self.display_updated = True + else: + self._update_dash_objects() + self._display_background() + + elif self.display_active is not None: + if (('menu_' in self.display_active) or ('input_' in self.display_active)) and self.display_init: + ''' Initialize Menu / Input Dialog ''' + self._display_menu_background() + self._build_objects(self.menu_background) + self.display_init = False + self.display_updated = True + + ''' Draw all objects. Perform any animations that need to be displayed. ''' + self._draw_objects() + + if self.display_updated: + self._display_canvas() + self.display_update = False + + else: + if self.display_init: + self._display_clear() + self.display_init = False + if not self.HOME_ENABLED: + self.display_active = 'dash' + self._init_dash() + self.display_active = None + + self.clock.tick(self.FRAMERATE) + + pygame.quit() + + ''' + ============== Graphics / Display / Draw Methods ============= + ''' + + def _wake_display(self): + self.backlight.power = True + self.backlight.brightness = 100 + self.backlight.fade_duration = 1 + + def _sleep_display(self): + self.backlight.fade_duration = 1 + self.backlight.brightness = 0 + pygame.time.delay(1000) # give time for the screen to fade + self.backlight.power = False + + def _display_clear(self): + self._sleep_display() + self.display_surface.fill((0,0,0,255)) + pygame.display.update() + self.display_canvas.paste((0,0,0,255), (0,0, self.WIDTH, self.HEIGHT)) + self.eventLogger.info('Screen Cleared.') + + def _display_canvas(self): + self._canvas_to_surface() + pygame.display.update() + + def _canvas_to_surface(self): + # Convert temporary canvas to PyGame surface + strFormat = self.display_canvas.mode + size = self.display_canvas.size + raw_str = self.display_canvas.tobytes("raw", strFormat) + self.display_surface.blit(PyImage.fromstring(raw_str, size, strFormat), (0,0)) + + def _display_background(self): + self.display_canvas.paste(self.background, (0,0)) + + def _capture_background(self): + self.menu_background = self.display_canvas.filter(ImageFilter.GaussianBlur(radius = 5)) + + def _display_menu_background(self): + self.display_canvas.paste(self.menu_background, (0,0)) + + def _init_dash(self): + self._init_framework() + self._configure_dash() + self._build_objects() + self._build_dash_map() + self._store_dash_objects() + + ''' + ====================== Input & Menu Code ======================== + ''' + def _debounce(self): + pygame.time.delay(self.DEBOUNCE) + + def _event_detect(self): + """ + Called to detect input events from buttons, encoder, touch, etc. + """ + user_input = self.input_event # Save to variable to prevent spurious changes + self.command = None + if user_input: + if self.display_timeout is not None: + self.display_timeout = time.time() + self.TIMEOUT + if user_input not in ['UP', 'DOWN', 'ENTER', 'TOUCH']: + self.input_event = None + self.touch_pos = (0,0) + return + elif user_input == 'TOUCH' and self.input_touch: + self._process_touch() + elif user_input in ['UP', 'DOWN', 'ENTER'] and (self.input_button or self.input_encoder): + self._process_button() + + # Clear the input event and touch_pos + self.input_event = None + self.touch_pos = (0,0) + + def _process_button(self): + self._debounce() + if self.display_active: + if 'dash' in self.display_active: + ''' + Process dash button events + ''' + self._capture_background() + self._store_dash_objects() + if self.status_data['mode'] == 'Stop': + self.display_active = 'menu_main' + elif self.status_data['mode'] in ['Startup', 'Reignite', 'Smoke', 'Hold', 'Shutdown']: + self.display_active = 'menu_main_active_normal' + elif self.status_data['mode'] == 'Monitor': + self.display_active = 'menu_main_active_monitor' + elif self.status_data['mode'] == 'Recipe': + self.display_active = 'menu_main_active_recipe' + else: + self.display_active = 'menu_main' + self.display_init = True + elif 'menu_' in self.display_active: + ''' + Process menu button events + ''' + objectData = self.display_object_list[0].get_object_data() + button_selected = objectData['data'].get('button_selected', None) + button_list = objectData.get('button_list', []) + + if button_selected is not None and button_list != []: + if self.input_event == 'UP': + if button_selected <= 1: + objectData['data']['button_selected'] = len(button_list) - 1 # Note: button_list has extra close_menu entry at index 0 + else: + objectData['data']['button_selected'] -= 1 + self.display_object_list[0].update_object_data(updated_objectData = objectData) + elif self.input_event == 'DOWN': + if len(button_list) - 1 > button_selected: + objectData['data']['button_selected'] += 1 + else: + objectData['data']['button_selected'] = 1 + self.display_object_list[0].update_object_data(updated_objectData = objectData) + elif self.input_event == 'ENTER': + if 'cmd_' in objectData['button_list'][button_selected]: + self.command = objectData['button_list'][button_selected] + if objectData.get('button_value', False): + self.command_data = objectData['button_value'][button_selected] + else: + self.command_data = None + self._command_handler() + elif objectData['button_list'][button_selected] == 'menu_close': + self.display_active = 'dash' + self.display_init = True + elif ('menu_' in objectData['button_list'][button_selected]) or ('input_' in objectData['button_list'][button_selected]): + if self.display_active == 'dash': + self._capture_background() + self._store_dash_objects() + if ('input_' in self.display_active) and ('input_' in objectData['button_list'][button_selected]) and ('button_value' in list(objectData.keys())): + self.input_origin = objectData['button_value'][button_selected] + self.display_active = objectData['button_list'][button_selected] + self.display_init = True + elif 'button_' in button_selected: + objectData['data']['input'] = objectData['button_list'][button_selected].replace('button_', '') + self.display_object_list[0].update_object_data(updated_objectData=objectData) + elif self.input_event == 'ENTER' and button_selected == None: + self.display_active = 'dash' + self.display_init = True + elif 'input_' in self.display_active: + ''' + Process input button events + ''' + objectData = self.display_object_list[0].get_object_data() + if self.input_event == 'UP': + objectData['data']['input'] = 'up' + self.display_object_list[0].update_object_data(updated_objectData=objectData) + if self.input_event == 'DOWN': + objectData['data']['input'] = 'down' + self.display_object_list[0].update_object_data(updated_objectData=objectData) + if self.input_event == 'ENTER': + self.command = objectData['command'] + self._command_handler() + self.display_active = 'dash' + self.display_init = True + else: + ''' + Wake the display & go to home/dash + ''' + self._wake_display() + self.display_active = 'home' if self.HOME_ENABLED else 'dash' + self.display_init = True + self.display_timeout = time.time() + self.TIMEOUT + + def _process_touch(self): + if self.display_active: + ''' + Loop through current displayed objects and check for touch collisions + ''' + for pointer, object in enumerate(self.display_object_list): + objectData = object.get_object_data() + for index, touch_area in enumerate(objectData['touch_areas']): + if touch_area.collidepoint(self.touch_pos): + #print(f'You touched {objectData["button_list"][index]}.') + if 'cmd_' in objectData['button_list'][index]: + self.command = objectData['button_list'][index] + if objectData.get('button_value', False): + self.command_data = objectData['button_value'][index] + else: + self.command_data = None + self._command_handler() + elif objectData['button_list'][index] == 'menu_close': + self.display_active = 'dash' + self.display_init = True + elif ('menu_' in objectData['button_list'][index]) or ('input_' in objectData['button_list'][index]): + if self.display_active == 'dash': + self._capture_background() + self._store_dash_objects() + if ('input_' in objectData['button_list'][index]) and ('button_value' in list(objectData.keys())): + self.input_origin = objectData['button_value'][index] + self.display_active = objectData['button_list'][index] + self.display_init = True + elif 'button_' in objectData['button_list'][index]: + objectData['data']['input'] = objectData['button_list'][index].replace('button_', '') + self.display_object_list[pointer].update_object_data(updated_objectData=objectData) + + else: + ''' + Wake the display & go to home/dash + ''' + self._wake_display() + self.display_active = 'home' if self.HOME_ENABLED else 'dash' + self.display_init = True + self.display_timeout = time.time() + self.TIMEOUT + \ No newline at end of file diff --git a/display/protoflex_320x240.json b/display/protoflex_320x240.json new file mode 100644 index 00000000..cc19ed8c --- /dev/null +++ b/display/protoflex_320x240.json @@ -0,0 +1,1012 @@ +{ + "metadata" : { + "name" : "protoflex_320x240", + "screen_width" : 320, + "screen_height" : 240, + "framerate" : 20, + "dash_background" : "./static/img/display/background.png", + "splash_image" : "./static/img/display/splash_800x480.png", + "splash_delay" : 500, + "max_food_probes" : 2, + "default_profile" : "profile_1" + }, + "profile_1" : { + "home" : [], + "dash" : [ + { + "name" : "primary_gauge", + "type" : "gauge", + "position" : [70, 30], + "animation_enabled" : true, + "size" : [180, 180], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : true, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Primary", + "units" : "F", + "max_temp" : 600, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_0", + "type" : "gauge_compact", + "position" : [0, 180], + "animation_enabled" : false, + "size" : [100, 60], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_1", + "type" : "gauge_compact", + "position" : [220, 180], + "animation_enabled" : false, + "size" : [100, 60], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "mode_bar", + "type" : "mode_bar", + "position" : [60, 0], + "animation_enabled" : false, + "size" : [200, 30], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [0,0,0,100], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "mode_bar", + "text" : "Stop", + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "fan_status", + "type" : "status_icon", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "icon" : "Fan", + "label" : "fan_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "auger_status", + "type" : "status_icon", + "position" : [10, 60], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "icon" : "Auger", + "label" : "auger_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "igniter_status", + "type" : "status_icon", + "position" : [270, 60], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "icon" : "Igniter", + "label" : "igniter_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "smoke_plus", + "type" : "splus_control", + "position" : [270, 10], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "label" : "SPlus", + "active" : false, + "active_color" : [200,0,200,225], + "inactive_color" : [255,255,255,100], + "data" : {}, + "button_list" : ["cmd_splus"], + "touch_areas" : [], + "button_value" : ["on"] + }, + { + "name" : "timer", + "type" : "timer", + "position" : [110, 190], + "animation_enabled" : false, + "size" : [100, 50], + "glow" : false, + "label" : "Timer", + "fg_color" : [255,255,255,255], + "bg_color" : [0,0,0,100], + "button_list" : [], + "button_value" : [], + "data" : { + "seconds" : 0 + }, + "touch_areas" : [] + }, + { + "name" : "lid_indicator", + "type" : "alert", + "position" : [110, 190], + "animation_enabled" : false, + "size" : [100, 50], + "glow" : false, + "label" : "Lid Open Detected", + "active" : false, + "fg_color" : [0,200,0,255], + "bg_color" : [0,0,0,0], + "data" : { + "text" : [ + "Lid Open", + "Detected" + ] + }, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "p_mode", + "type" : "p_mode_control", + "position" : [240, 140], + "animation_enabled" : false, + "size" : [80, 40], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "fg_color" : [255,255,255,255], + "bg_color" : [0,0,0,0], + "data" : { + "pmode" : 0 + }, + "button_list" : ["menu_pmode"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "hopper", + "type" : "hopper_status", + "position" : [0, 140], + "animation_enabled" : false, + "size" : [80, 40], + "glow" : false, + "label" : "Hopper Level", + "active" : false, + "color" : [255,255,255,255], + "color_levels" : [ + [225, 50, 50, 255], + [225, 150, 50, 255], + [225, 225, 50, 255], + [50, 225, 50, 255], + [255, 255, 255, 255] + ], + "data" : { + "level" : 100 + }, + "button_list" : ["cmd_hopper_level"], + "button_value" : [], + "touch_areas" : [] + } + ], + "menus" : { + "main" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_normal" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_value" : [null, null, null, null, null, "on", null, null, null], + "touch_areas" : [] + }, + "main_active_monitor" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_recipe" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_value" : [null, null, null, null, "on", null, null], + "touch_areas" : [] + }, + "system" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off", "menu_close"], + "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System", "Close Menu"], + "button_value" : [], + "touch_areas" : [] + }, + "qrcode" : { + "type" : "qrcode", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "qr_code", + "ip_address" : "0.0.0.0", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_value" : ["Close Menu"], + "touch_areas" : [] + }, + "main_reboot" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_reboot", + "title_text" : "Reboot the System?", + "color" : [255,255,255,255], + "data" : { + "button_selected" : 2 + }, + "button_list" : ["menu_close", "cmd_reboot", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "main_power_off" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_power_off", + "title_text" : "Power Off the System?", + "color" : [255,255,255,255], + "data" : { + "button_selected" : 2 + }, + "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime_start", + "title_text" : "Startup after priming?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime_startup" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup", "menu_close"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams", "Cancel"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "prime_only" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly", "menu_close"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams", "Cancel"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "startup" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Do you want to start up?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_startup", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "pmode" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Select a PMode:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], + "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], + "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "touch_areas" : [] + }, + "message" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "message", + "title_text" : "Message", + "color" : [255,255,255,255], + "data" : { + "text" : "" + }, + "button_list" : ["menu_close", "menu_close"], + "button_text" : ["Close Menu", "OK"], + "button_value" : [], + "touch_areas" : [] + } + }, + "input" : { + "hold" : { + "type" : "input_number_simple", + "position" : [10, 10], + "animation_enabled" : true, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_hold", + "title_text" : "Enter Hold Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_hold", + "data" : { + "input": "", + "value" : 200, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + "notify" : { + "type" : "input_number_simple", + "position" : [10, 10], + "animation_enabled" : true, + "size" : [300, 220], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_notify", + "title_text" : "Enter Notify Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_notify", + "data" : { + "input": "", + "value" : 165, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + } + } + }, + "profile_2" : { + "home" : [], + "dash" : [ + { + "name" : "primary_gauge", + "type" : "gauge", + "position" : [30, 65], + "animation_enabled" : true, + "size" : [180, 180], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : true, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Primary", + "units" : "F", + "max_temp" : 600, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_0", + "type" : "gauge_compact", + "position" : [0, 250], + "animation_enabled" : false, + "size" : [110, 70], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_1", + "type" : "gauge_compact", + "position" : [130, 250], + "animation_enabled" : false, + "size" : [110, 70], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "mode_bar", + "type" : "mode_bar", + "position" : [20, 0], + "animation_enabled" : false, + "size" : [200, 30], + "fg_color" : [255, 255, 255, 255], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "mode_bar", + "text" : "Stop", + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "fan_status", + "type" : "status_icon", + "position" : [4, 40], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "icon" : "Fan", + "label" : "fan_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "auger_status", + "type" : "status_icon", + "position" : [4, 80], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "icon" : "Auger", + "label" : "auger_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "igniter_status", + "type" : "status_icon", + "position" : [200, 40], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "icon" : "Igniter", + "label" : "igniter_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "smoke_plus", + "type" : "splus_control", + "position" : [200, 80], + "animation_enabled" : false, + "size" : [40, 40], + "glow" : false, + "label" : "SPlus", + "active" : false, + "color" : [200,0,200,225], + "data" : {}, + "button_list" : ["cmd_splus"], + "touch_areas" : [], + "button_value" : ["on"] + }, + { + "name" : "timer", + "type" : "timer", + "position" : [70, 25], + "animation_enabled" : false, + "size" : [100, 50], + "glow" : false, + "label" : "Timer", + "color" : [255,255,255,255], + "button_list" : [], + "button_value" : [], + "data" : { + "seconds" : 0 + }, + "touch_areas" : [] + }, + { + "name" : "lid_indicator", + "type" : "alert", + "position" : [70, 25], + "animation_enabled" : false, + "size" : [100, 50], + "glow" : false, + "label" : "Lid Open Detected", + "active" : false, + "color" : [0,200,0,255], + "data" : { + "text" : [ + "Lid Open", + "Detected" + ] + }, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "p_mode", + "type" : "p_mode_control", + "position" : [160, 215], + "animation_enabled" : false, + "size" : [80, 40], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "color" : [255,255,255,255], + "data" : { + "pmode" : 0 + }, + "button_list" : ["menu_pmode"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "hopper", + "type" : "hopper_status", + "position" : [0, 215], + "animation_enabled" : false, + "size" : [80, 40], + "glow" : false, + "label" : "Hopper Level", + "active" : false, + "color" : [255,255,255,255], + "color_levels" : [ + [225, 50, 50, 255], + [225, 150, 50, 255], + [225, 225, 50, 255], + [50, 225, 50, 255], + [255, 255, 255, 255] + ], + "data" : { + "level" : 100 + }, + "button_list" : ["cmd_hopper_level"], + "button_value" : [], + "touch_areas" : [] + } + ], + "menus" : { + "main" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_normal" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_value" : [null, null, null, null, null, "on", null, null, null], + "touch_areas" : [] + }, + "main_active_monitor" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_recipe" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_value" : [null, null, null, null, "on", null, null], + "touch_areas" : [] + }, + "system" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off", "menu_close"], + "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System", "Close Menu"], + "button_value" : [], + "touch_areas" : [] + }, + "qrcode" : { + "type" : "qrcode", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "qr_code", + "ip_address" : "0.0.0.0", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_value" : ["Close Menu"], + "touch_areas" : [] + }, + "main_reboot" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_reboot", + "title_text" : "Reboot the System?", + "color" : [255,255,255,255], + "data" : { + "button_selected" : 2 + }, + "button_list" : ["menu_close", "cmd_reboot", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "main_power_off" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_power_off", + "title_text" : "Power Off the System?", + "color" : [255,255,255,255], + "data" : { + "button_selected" : 2 + }, + "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime_start", + "title_text" : "Startup after priming?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime_startup" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup", "menu_close"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams", "Cancel"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "prime_only" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly", "menu_close"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams", "Cancel"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "startup" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Do you want to start up?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_startup", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "pmode" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Select a PMode:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], + "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], + "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "touch_areas" : [] + }, + "message" : { + "type" : "menu", + "position" : [10, 10], + "animation_enabled" : false, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "message", + "title_text" : "Message", + "color" : [255,255,255,255], + "data" : { + "text" : "" + }, + "button_list" : ["menu_close", "menu_close"], + "button_text" : ["Close Menu", "OK"], + "button_value" : [], + "touch_areas" : [] + } + }, + "input" : { + "hold" : { + "type" : "input_number_simple", + "position" : [10, 10], + "animation_enabled" : true, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_hold", + "title_text" : "Enter Hold Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_hold", + "data" : { + "input": "", + "value" : 200, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + "notify" : { + "type" : "input_number_simple", + "position" : [10, 10], + "animation_enabled" : true, + "size" : [220, 300], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_notify", + "title_text" : "Enter Notify Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_notify", + "data" : { + "input": "", + "value" : 165, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + } + } + } +} diff --git a/display/protoflex_800x480.json b/display/protoflex_800x480.json new file mode 100644 index 00000000..705e6a73 --- /dev/null +++ b/display/protoflex_800x480.json @@ -0,0 +1,1188 @@ +{ + "metadata" : { + "name" : "protoflex_800x480", + "screen_width" : 800, + "screen_height" : 480, + "framerate" : 20, + "dash_background" : "./static/img/display/background.png", + "splash_image" : "./static/img/display/splash_800x480.png", + "splash_delay" : 500, + "max_food_probes" : 5, + "default_profile" : "profile_1" + }, + "profile_1" : { + "home" : [], + "dash" : [ + { + "name" : "primary_gauge", + "type" : "gauge", + "position" : [225, 50], + "animation_enabled" : true, + "size" : [350, 350], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : true, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Primary", + "units" : "F", + "max_temp" : 600, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "control_panel", + "type" : "control_panel", + "position" : [200, 380], + "animation_enabled" : false, + "glow" : false, + "size" : [400, 100], + "label" : "control_panel", + "data" : {}, + "button_list" : ["menu_prime", "menu_startup", "cmd_monitor", "cmd_stop"], + "button_type" : ["Prime", "Startup", "Monitor", "Stop"], + "button_active" : "Stop", + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_0", + "type" : "gauge_compact", + "position" : [0, 55], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_1", + "type" : "gauge_compact", + "position" : [580, 55], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_2", + "type" : "gauge_compact", + "position" : [0, 160], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_3", + "type" : "gauge_compact", + "position" : [580, 160], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_4", + "type" : "gauge_compact", + "position" : [0, 275], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "mode_bar", + "type" : "mode_bar", + "position" : [200, 0], + "animation_enabled" : false, + "size" : [400, 60], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [0,0,0,100], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "mode_bar", + "text" : "Stop", + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "fan_status", + "type" : "status_icon", + "position" : [10, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Fan", + "label" : "fan_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "auger_status", + "type" : "status_icon", + "position" : [85, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Auger", + "label" : "auger_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "igniter_status", + "type" : "status_icon", + "position" : [150, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Igniter", + "label" : "igniter_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "menu_icon", + "type" : "menu_icon", + "position" : [740, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : false, + "icon" : "Hamburger", + "label" : "menu_icon", + "color" : [255,255,255,255], + "button_list" : ["menu_main"], + "button_value" : [], + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "timer", + "type" : "timer", + "position" : [0, 380], + "animation_enabled" : false, + "size" : [200, 100], + "glow" : false, + "label" : "Timer", + "fg_color" : [255,255,255,255], + "bg_color" : [0,0,0,100], + "button_list" : [], + "button_value" : [], + "data" : { + "seconds" : 0 + }, + "touch_areas" : [] + }, + { + "name" : "lid_indicator", + "type" : "alert", + "position" : [600, 380], + "animation_enabled" : false, + "size" : [200, 100], + "glow" : false, + "label" : "Lid Open Detected", + "active" : false, + "fg_color" : [0,200,0,255], + "bg_color" : [0,0,0,0], + "data" : { + "text" : [ + "Lid Open", + "Detected" + ] + }, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "p_mode", + "type" : "p_mode_control", + "position" : [600, 380], + "animation_enabled" : false, + "size" : [200, 100], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "fg_color" : [255,255,255,255], + "bg_color" : [0,0,0,0], + "data" : { + "pmode" : 0 + }, + "button_list" : ["menu_pmode"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "smoke_plus", + "type" : "splus_control", + "position" : [650, 0], + "animation_enabled" : false, + "size" : [60, 60], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "active_color" : [200,0,200,225], + "inactive_color" : [255,255,255,100], + "data" : {}, + "button_list" : ["cmd_splus"], + "touch_areas" : [], + "button_value" : ["on"] + }, + { + "name" : "hopper", + "type" : "hopper_status", + "position" : [580, 275], + "animation_enabled" : false, + "size" : [220, 110], + "glow" : false, + "label" : "Hopper Level", + "active" : false, + "color" : [255,255,255,255], + "color_levels" : [ + [225, 50, 50, 255], + [225, 150, 50, 255], + [225, 225, 50, 255], + [50, 225, 50, 255], + [255, 255, 255, 255] + ], + "data" : { + "level" : 100 + }, + "button_list" : ["cmd_hopper_level"], + "button_value" : [], + "touch_areas" : [] + } + ], + "menus" : { + "main" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_normal" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_monitor" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_recipe" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "system" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off", "menu_close"], + "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System", "Close Menu"], + "button_value" : [], + "touch_areas" : [] + }, + "qrcode" : { + "type" : "qrcode", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "qr_code", + "ip_address" : "0.0.0.0", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_reboot" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_reboot", + "title_text" : "Reboot the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_reboot", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "main_power_off" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_power_off", + "title_text" : "Power Off the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime_start", + "title_text" : "Startup after priming?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime_startup" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "prime_only" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "startup" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Do you want to start up?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_startup", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "pmode" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Select a PMode:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], + "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], + "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "touch_areas" : [] + }, + "message" : { + "type" : "menu", + "position" : [100, 40], + "animation_enabled" : false, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "message", + "title_text" : "Message", + "color" : [255,255,255,255], + "data" : { + "text" : "" + }, + "button_list" : ["menu_close", "menu_close"], + "button_text" : ["Close Menu", "OK"], + "button_value" : [], + "touch_areas" : [] + } + }, + "input" : { + "hold" : { + "type" : "input_number", + "position" : [100, 40], + "animation_enabled" : true, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_hold", + "title_text" : "Enter Hold Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_hold", + "data" : { + "input": "", + "value" : 200, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + "notify" : { + "type" : "input_number", + "position" : [100, 40], + "animation_enabled" : true, + "size" : [600, 400], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_notify", + "title_text" : "Enter Notify Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_notify", + "data" : { + "input": "", + "value" : 165, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + } + } + }, + "profile_2" : { + "home" : [], + "dash" : [ + { + "name" : "primary_gauge", + "type" : "gauge", + "position" : [65, 50], + "animation_enabled" : true, + "size" : [350, 350], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : true, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Primary", + "units" : "F", + "max_temp" : 600, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "control_panel", + "type" : "control_panel", + "position" : [40, 700], + "animation_enabled" : false, + "glow" : false, + "size" : [400, 100], + "label" : "control_panel", + "data" : {}, + "button_list" : ["menu_prime", "menu_startup", "cmd_monitor", "cmd_stop"], + "button_type" : ["Prime", "Startup", "Monitor", "Stop"], + "button_active" : "Stop", + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_0", + "type" : "gauge_compact", + "position" : [0, 380], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_1", + "type" : "gauge_compact", + "position" : [260, 380], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_2", + "type" : "gauge_compact", + "position" : [0, 490], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_3", + "type" : "gauge_compact", + "position" : [260, 490], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "food_probe_gauge_4", + "type" : "gauge_compact", + "position" : [0, 600], + "animation_enabled" : false, + "size" : [220, 110], + "fg_color" : [255, 255, 255, 255], + "bg_color" : [25, 25, 25, 255], + "sp_color" : [0, 200, 255, 255], + "np_color" : [255, 255, 0, 255], + "glow" : false, + "font" : "trebuc.ttf", + "temps" : [0, 0, 0], + "label" : "Food Probe", + "units" : "F", + "max_temp" : 300, + "data" : {}, + "button_list" : ["input_notify"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "mode_bar", + "type" : "mode_bar", + "position" : [65, 0], + "animation_enabled" : false, + "size" : [350, 60], + "fg_color" : [255, 255, 255, 255], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "mode_bar", + "text" : "Stop", + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "fan_status", + "type" : "status_icon", + "position" : [0, 0], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Fan", + "label" : "fan_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "auger_status", + "type" : "status_icon", + "position" : [0, 60], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Auger", + "label" : "auger_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "igniter_status", + "type" : "status_icon", + "position" : [0, 120], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : true, + "icon" : "Igniter", + "label" : "igniter_status", + "active" : false, + "inactive_color" : [128,128,128,128], + "active_color" : [255,255,255,255], + "rotation" : 0, + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "smoke_plus", + "type" : "splus_control", + "position" : [0, 180], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "color" : [200,0,200,225], + "data" : {}, + "button_list" : ["cmd_splus"], + "touch_areas" : [], + "button_value" : ["on"] + }, + { + "name" : "menu_icon", + "type" : "menu_icon", + "position" : [430, 5], + "animation_enabled" : false, + "size" : [50, 50], + "glow" : false, + "icon" : "Hamburger", + "label" : "menu_icon", + "color" : [255,255,255,255], + "button_list" : ["menu_main"], + "button_value" : [], + "data" : {}, + "touch_areas" : [] + }, + { + "name" : "timer", + "type" : "timer", + "position" : [260, 600], + "animation_enabled" : false, + "size" : [220, 110], + "glow" : false, + "label" : "Timer", + "color" : [255,255,255,255], + "button_list" : [], + "button_value" : [], + "data" : { + "seconds" : 0 + }, + "touch_areas" : [] + }, + { + "name" : "lid_indicator", + "type" : "alert", + "position" : [260, 600], + "animation_enabled" : false, + "size" : [220, 110], + "glow" : false, + "label" : "Lid Open Detected", + "active" : false, + "color" : [0,200,0,255], + "data" : { + "text" : [ + "Lid Open", + "Detected" + ] + }, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "p_mode", + "type" : "p_mode_control", + "position" : [360, 320], + "animation_enabled" : false, + "size" : [120, 65], + "glow" : false, + "label" : "P-Mode", + "active" : false, + "color" : [255,255,255,255], + "data" : { + "pmode" : 0 + }, + "button_list" : ["menu_pmode"], + "button_value" : [], + "touch_areas" : [] + }, + { + "name" : "hopper", + "type" : "hopper_status", + "position" : [0, 320], + "animation_enabled" : false, + "size" : [120, 65], + "glow" : false, + "label" : "Hopper Level", + "active" : false, + "color" : [255,255,255,255], + "color_levels" : [ + [225, 50, 50, 255], + [225, 150, 50, 255], + [225, 225, 50, 255], + [50, 225, 50, 255], + [255, 255, 255, 255] + ], + "data" : { + "level" : 100 + }, + "button_list" : ["cmd_hopper_level"], + "button_value" : [], + "touch_areas" : [] + } + ], + "menus" : { + "main" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime", "menu_startup", "cmd_monitor", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Prime", "Startup", "Monitor", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_normal" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "input_hold", "cmd_shutdown", "cmd_stop", "cmd_smoke", "cmd_splus", "menu_pmode", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Hold", "Shutdown", "Stop", "Smoke", "Smoke+", "PMode", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_monitor" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_stop", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Stop", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_active_recipe" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_next_step", "cmd_shutdown", "cmd_stop", "cmd_splus", "menu_system", "menu_close"], + "button_text" : ["Close Menu", "Next Step", "Shutdown", "Stop", "Smoke+", "System", "Close"], + "button_value" : [], + "touch_areas" : [] + }, + "system" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu", + "title_text" : "Main Menu", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_qrcode", "menu_main_reboot", "menu_main_power_off", "menu_close"], + "button_text" : ["Close Menu", "Show QR Code", "Reboot System", "Power Off System", "Close Menu"], + "button_value" : [], + "touch_areas" : [] + }, + "qrcode" : { + "type" : "qrcode", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "qr_code", + "ip_address" : "0.0.0.0", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close"], + "button_value" : [], + "touch_areas" : [] + }, + "main_reboot" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_reboot", + "title_text" : "Reboot the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_reboot", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "main_power_off" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "main_menu_power_off", + "title_text" : "Power Off the System?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_poweroff", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime_start", + "title_text" : "Startup after priming?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "menu_prime_startup", "menu_prime_only"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "prime_startup" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primestartup", "cmd_primestartup", "cmd_primestartup"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "prime_only" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_prime", + "title_text" : "Select Amount to Prime:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_primeonly", "cmd_primeonly", "cmd_primeonly"], + "button_text" : ["Close Menu", "10 grams", "25 grams", "50 grams"], + "button_value" : [0, 10, 25, 50], + "touch_areas" : [] + }, + "startup" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Do you want to start up?", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_startup", "menu_close"], + "button_text" : ["Close Menu", "Yes", "No"], + "button_value" : [], + "touch_areas" : [] + }, + "pmode" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "menu_startup", + "title_text" : "Select a PMode:", + "color" : [255,255,255,255], + "data" : {}, + "button_list" : ["menu_close", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode", "cmd_pmode"], + "button_text" : ["Close Menu", "PMode 0", "PMode 1", "PMode 2", "PMode 3", "PMode 4", "PMode 5", "PMode 6", "PMode 7", "PMode 8", "PMode 9"], + "button_value" : [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "touch_areas" : [] + }, + "message" : { + "type" : "menu", + "position" : [20, 40], + "animation_enabled" : false, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "message", + "title_text" : "Message", + "color" : [255,255,255,255], + "data" : { + "text" : "" + }, + "button_list" : ["menu_close", "menu_close"], + "button_text" : ["Close Menu", "OK"], + "button_value" : [], + "touch_areas" : [] + } + }, + "input" : { + "hold" : { + "type" : "input_number", + "position" : [20, 40], + "animation_enabled" : true, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_hold", + "title_text" : "Enter Hold Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_hold", + "data" : { + "input": "", + "value" : 200, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + }, + "notify" : { + "type" : "input_number", + "position" : [20, 40], + "animation_enabled" : true, + "size" : [440, 320], + "glow" : false, + "font" : "trebuc.ttf", + "label" : "input_notify", + "title_text" : "Enter Notify Temperature:", + "color" : [255,255,255,255], + "command" : "cmd_notify", + "data" : { + "input": "", + "value" : 165, + "origin" : "" + }, + "step" : 5, + "button_list" : [], + "button_value" : [], + "touch_areas" : [] + } + } + } +} diff --git a/display/ssd1306b.py b/display/ssd1306b.py index 69416828..b27a97c1 100644 --- a/display/ssd1306b.py +++ b/display/ssd1306b.py @@ -181,9 +181,13 @@ def _display_loop(self): if self.display_command == 'network': self.display_active = True - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - network_ip = s.getsockname()[0] + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(4) + s.connect(("8.8.8.8", 80)) + network_ip = s.getsockname()[0] + except: + network_ip = '' if network_ip != '': self._display_network(network_ip) self.display_timeout = time.time() + 30 diff --git a/display/st7789v_240x320.py b/display/st7789v_240x320.py new file mode 100644 index 00000000..988036d6 --- /dev/null +++ b/display/st7789v_240x320.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +''' +***************************************** +PiFire Display Interface Library +***************************************** + + Description: + This library supports using + the ST7789V display with resolution. + This module utilizes the a forked/experimental + library to interface this display. + (https://github.com/mander1000/st7789-python) + As such, your mileage may vary. + +***************************************** +''' + +''' + Imported Libraries +''' +import threading +import ST7789 as ST7789 +from display.base_240x320 import DisplayBase +from PIL import Image + +''' +Display class definition +''' +class Display(DisplayBase): + + def __init__(self, dev_pins, buttonslevel='HIGH', rotation=0, units='F', config={}): + super().__init__(dev_pins, buttonslevel, rotation, units, config) + + def _init_display_device(self): + # Init Device + dc_pin = self.dev_pins['display']['dc'] + bl_pin = self.dev_pins['display']['led'] + rst_pin = self.dev_pins['display']['rst'] + + self.device = ST7789.ST7789( + 0, # PORT + 0, # SPI Device Number (0 or 1) + dc_pin, # DC Pin + spi_cs=0, + backlight=bl_pin, + rst=rst_pin, + width=320, + height=240, + rotation=self.rotation, + spi_speed_hz=60 * 1000 * 1000, + variant=2 # VARIANT_ST7789V + ) + self.WIDTH = self.device.width + self.HEIGHT = self.device.height + + # Setup & Start Display Loop Thread + display_thread = threading.Thread(target=self._display_loop) + display_thread.start() + + ''' + ============== Graphics / Display / Draw Methods ============= + ''' + + def _display_clear(self): + # Create blank canvas + img = Image.new('RGB', (self.WIDTH, self.HEIGHT), color=(0, 0, 0)) + self.device.display(img) + # Kill the backlight to the display + self.device.set_backlight(0) + + def _display_canvas(self, canvas): + # Display canvas to screen for ST7789 + # Turn on Backlight (just in case it was off) + self.device.set_backlight(1) + self.device.display(canvas.convert(mode="RGB")) diff --git a/distance/vl53l0x.py b/distance/vl53l0x.py index 44494817..6d4fc576 100644 --- a/distance/vl53l0x.py +++ b/distance/vl53l0x.py @@ -32,6 +32,8 @@ def __init__(self, dev_pins, empty=22, full=4, debug=False): self.debug = debug self.distance_read = 100 + self.event = threading.Event() + if self.empty <= self.full: event = 'ERROR: Invalid Hopper Level Configuration Empty Level <= Full Level (forcing defaults)' self.logger.error(event) @@ -107,8 +109,9 @@ def _sensing_loop(self): self.__start_sensor() # Attempt re-init of sensor event = 'Warning: The TOF sensor took longer than normal to get a reading. Re-initializing the sensor.' self.logger.info(event) - - self.sensor_thread_override = False + if self.sensor_thread_override: + self.event.set() + self.sensor_thread_override = False sample_time = time.time() time.sleep(1) @@ -130,5 +133,6 @@ def get_level(self, override=False): ''' If override selected, force the sensor thread to update ''' if override: self.sensor_thread_override = True - time.sleep(3) + self.event.wait(3) # Wait 3 seconds for sensor to update + self.event.clear() # Clear event flag return self.distance_read diff --git a/grillplat/pifire.py b/grillplat/pifire.py deleted file mode 100644 index a1d79e6d..00000000 --- a/grillplat/pifire.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 - -# ***************************************** -# PiFire OEM Interface Library -# ***************************************** -# -# Description: This library supports -# controlling the PiFire Outputs, alongside -# the OEM controller outputs via -# Raspberry Pi GPIOs, to a 4-channel relay -# -# ***************************************** - -# ***************************************** -# Imported Libraries -# ***************************************** - -import subprocess -from common import is_float, create_logger -from gpiozero import OutputDevice -from gpiozero import Button - -class GrillPlatform: - - def __init__(self, outpins, inpins, triggerlevel='LOW'): - self.logger = create_logger('control') - self.outpins = outpins # { 'power' : 4, 'auger' : 14, 'fan' : 15, 'igniter' : 18 } - self.inpins = inpins # { 'selector' : 17 } - self.current = {} - - self.selector = Button(self.inpins['selector']) - - active_high = triggerlevel == 'HIGH' - - self.fan = OutputDevice(self.outpins['fan'], active_high=active_high, initial_value=False) - self.auger = OutputDevice(self.outpins['auger'], active_high=active_high, initial_value=False) - self.igniter = OutputDevice(self.outpins['igniter'], active_high=active_high, initial_value=False) - self.power = OutputDevice(self.outpins['power'], active_high=active_high, initial_value=False) - - def auger_on(self): - self.auger.on() - - def auger_off(self): - self.auger.off() - - def fan_on(self): - self.fan.on() - - def fan_off(self): - self.fan.off() - - def fan_toggle(self): - self.fan.toggle() - - def igniter_on(self): - self.igniter.on() - - def igniter_off(self): - self.igniter.off() - - def power_on(self): - self.power.on() - - def power_off(self): - self.power.off() - - def get_input_status(self): - return self.selector.value - - def get_output_status(self): - self.current = {} - self.current['auger'] = self.auger.is_active - self.current['igniter'] = self.igniter.is_active - self.current['power'] = self.power.is_active - self.current['fan'] = self.fan.is_active - return self.current - - """ - ============================== - System / Platform Commands - ============================== - - Commands callable by outside processes to get status or information, for the platform. - - """ - - def supported_commands(self, arglist): - supported_commands = [ - 'check_throttled', - 'check_wifi_quality', - 'check_cpu_temp', - 'supported_commands', - 'check_alive' - ] - - data = { - 'result' : 'OK', - 'message' : 'Supported commands listed in "data".', - 'data' : { - 'supported_cmds' : supported_commands - } - } - return data - - def check_throttled(self, arglist): - """Checks for under-voltage and throttling using vcgencmd. - - Returns: - (bool, bool): A tuple of (under_voltage, throttled) indicating their status. - """ - - output = subprocess.check_output(["vcgencmd", "get_throttled"]) - status_str = output.decode("utf-8").strip()[10:] # Extract the numerical value - status_int = int(status_str, 16) # Convert from hex to decimal - - under_voltage = bool(status_int & 0x10000) # Check bit 16 for under-voltage - throttled = bool(status_int & 0x5) # Check bits 0 and 2 for active throttling - - if under_voltage or throttled: - message = 'WARNING: Under-voltage or throttled situation detected' - else: - message = 'No under-voltage or throttling detected.' - - data = { - 'result' : 'OK', - 'message' : message, - 'data' : { - 'cpu_under_voltage' : under_voltage, - 'cpu_throttled' : throttled - } - } - self.logger.debug(f'Check Throttled Called. [data = {data}]') - return data - - - def check_wifi_quality(self, arglist): - """Checks the Wi-Fi signal quality on a Raspberry Pi and returns the percentage value (or None if not connected).""" - data = { - 'result' : 'ERROR', - 'message' : 'Unable to obtain wifi quality data.', - 'data' : {} - } - - try: - # Use iwconfig to get the signal quality - output = subprocess.check_output(["iwconfig", "wlan0"]) - lines = output.decode("utf-8").splitlines() - - # Find the line containing "Link Quality" and extract the relevant part - for line in lines: - if "Link Quality=" in line: - quality_str = line.split("=")[1].strip() # Isolate the part after "=" - quality_parts = quality_str.split(" ")[0] # Extract only the first part before spaces - - try: - quality_value, quality_max = quality_parts.split("/") # Split for numerical values - percentage = (int(quality_value) / int(quality_max)) * 100 - data['result'] = 'OK' - data['message'] = 'Successfully obtained wifi quality data.' - data['data']['wifi_quality_value'] = int(quality_value) - data['data']['wifi_quality_max'] = int(quality_max) - data['data']['wifi_quality_percentage'] = round(percentage, 2) # Round to two decimal places - - except ValueError: - # Handle cases where the value might not be directly convertible to an integer - pass - - except subprocess.CalledProcessError: - # Handle errors, such as iwconfig not being found or wlan0 not existing - self.logger.debug(f'Check Throttled had a subprocess error') - pass - - self.logger.debug(f'Check Throttled Called. [data = {data}]') - return data - - def check_cpu_temp(self, arglist): - output = subprocess.check_output(["vcgencmd", "measure_temp"]) - temp = output.decode("utf-8").replace("temp=","").replace("'C", "").replace("\n", "") - - if is_float(temp): - temp = float(temp) - else: - temp = 0.0 - - data = { - 'result' : 'OK', - 'message' : 'Success.', - 'data' : { - 'cpu_temp' : float(temp) - } - } - self.logger.debug(f'Check CPU Temp Called. [data = {data}]') - return data - - def check_alive(self, arglist): - ''' - Simple check to see if the platform is up and running. - ''' - - data = { - 'result' : 'OK', - 'message' : 'The control script is running.', - 'data' : {} - } - return data \ No newline at end of file diff --git a/grillplat/prototype.py b/grillplat/prototype.py index e5ffed9b..7c6de5c4 100644 --- a/grillplat/prototype.py +++ b/grillplat/prototype.py @@ -6,23 +6,40 @@ # # Description: This library simulates # controlling the Grill outputs via -# GPIOs, to a 4-channel relay +# GPIOs, to a 4-channel relay and/or DC Fan # # ***************************************** +""" + ============================== + Imported Libraries + ============================== +""" + from gpiozero.threads import GPIOThread -from common import is_float +from common import is_float, create_logger + +""" + ============================== + Class Definition + ============================== +""" class GrillPlatform: - def __init__(self, out_pins, in_pins, trigger_level='LOW', dc_fan=False, frequency=100): - self.out_pins = out_pins # { 'power' : 4, 'auger' : 14, 'fan' : 15, 'dc_fan' : 26, 'igniter' : 18, 'pwm' : 13 } - self.in_pins = in_pins # { 'selector' : 17 } - self.dc_fan = dc_fan - self.frequency = frequency - self.current = {} + def __init__(self, config): + self.logger = create_logger('control') + try: + self.out_pins = config.get('outputs', None) # Pins to control the PiFire outputs + self.in_pins = config.get('inputs', None) # Pins for input + self.dc_fan = config.get('dc_fan', False) # Save state for DC Fan + self.frequency = config.get('frequency', 100) # Save configured fan frequency + self.standalone = config.get('standalone', True) # Save configured state for Standalone + self.current = {} + except: + self.logger.error('Error parsing platform configuration. Check your settings.json file.') - if dc_fan: + if self.dc_fan: self._ramp_thread = None self.out_pins['pwm'] = 100 @@ -125,6 +142,13 @@ def _ramp_device(self, on_time, min_duty_cycle, max_duty_cycle, fps=25): if self._ramp_thread.stopping.wait(delay): break + def cleanup(self): + self.power_off() + self.igniter_off() + self.auger_off() + self.fan_off() + + # MARK: System Platform Commands """ ============================== System / Platform Commands diff --git a/grillplat/pifire_pwm.py b/grillplat/raspberry_pi_all.py similarity index 74% rename from grillplat/pifire_pwm.py rename to grillplat/raspberry_pi_all.py index 1c46babc..348581c4 100644 --- a/grillplat/pifire_pwm.py +++ b/grillplat/raspberry_pi_all.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # ***************************************** -# PiFire PWM PCB Platform Interface Library +# PiFire Raspberry Pi Platform Interface Library # ***************************************** # -# Description: This library supports controlling the PiFire PWM PCB, which -# integrates solid state relays and PWM amplifier circuitry to drive a 12V DC PWM fan. +# Description: This library utilizes the Raspberry Pi to control outputs and +# monitor inputs for PiFire. # -# Relays (augur, igniter) are controlled via GPIO pins. +# Relays (power, auger, igniter, and fan) are controlled via GPIO pins. # -# 12V power to the fan is controlled via GPIO, which controls a power transistor. +# If a DC Fan is installed, 12V power to the fan is controlled via GPIO, which controls a power transistor. # # A 3.3v PWM fan signal is generated by the RPi Hardware PWM module (PWM1 / GPIO 13). # This 3.3v PWM signal controls an amplifying transistor that supplies 5v, and the design @@ -18,8 +18,6 @@ # # ***************************************** # -# TODO - Fix debug logging to only toggle when debug is enabled -# # TODO - Rewrite all functions/variables to use the following nomenclature: # PWM duty cycle - the actual "percent high" waveform parameter sent to the PWM generator # Fan percent (or "fan duty cycle") - the requested fan speed percentage @@ -27,9 +25,12 @@ # PWM duty cycle = (100 - fan percent speed / fan duty cycle) # Fan percent speed = (100 - PWM duty cycle) # -# ***************************************** -# Imported Libraries -# ***************************************** + +""" + ============================== + Imported Libraries + ============================== +""" import subprocess from common import is_float, create_logger @@ -38,30 +39,44 @@ from gpiozero.threads import GPIOThread from rpi_hardware_pwm import HardwarePWM +""" + ============================== + Class Definition + ============================== +""" + class GrillPlatform: - def __init__(self, out_pins, in_pins, trigger_level='LOW', dc_fan=False, frequency=100): + def __init__(self, config): self.logger = create_logger('control') - self.logger.info('grillplat pifire_pwm __init__: ************************************************') - self.logger.info('grillplat pifire_pwm __init__: **** Starting Grill Platform Initialization ****') - self.logger.info('grillplat pifire_pwm __init__: ************************************************') - self.out_pins = out_pins # { 'power' : 4, 'auger' : 14, 'fan' : 15, 'dc_fan' : 26, 'igniter' : 18, 'pwm' : 13 } - self.in_pins = in_pins # { 'selector' : 17 } - self.dc_fan = dc_fan - self.frequency = frequency - self.current = {} - - self.selector = Button(self.in_pins['selector']) + try: + self.out_pins = config.get('outputs', None) # Pins to control the PiFire outputs + self.in_pins = config.get('inputs', None) # Pins for input + self.dc_fan = config.get('dc_fan', False) # Save state for DC Fan + self.frequency = config.get('frequency', 100) # Save configured fan frequency + self.standalone = config.get('standalone', True) # Save configured state for Standalone + self.current = {} + except: + self.logger.error('Error parsing platform configuration. Check your settings.json file.') + raise + + if not self.standalone: + self.selector = Button(self.in_pins['selector']) + else: + self.selector = None - active_high = trigger_level == 'HIGH' + active_high = True if config.get('trigger_level', 'HIGH') == 'HIGH' else False - if dc_fan: + if self.dc_fan: self.current_fan_speed_percent = 100 # Hardware PWM library does not have a mechanism to retrieve the current duty cycle - initialize a variable to track this self._ramp_thread = None self.fan = OutputDevice(self.out_pins['dc_fan'], active_high=active_high, initial_value=False) - self.hardware_pwm_channel = 1 # PiFire PWM PCB uses GPIO 13 for PWM signal generation, which maps to Hardware PWM channel 1 + if self.out_pins['pwm'] in [13, 19]: + self.hardware_pwm_channel = 1 # Raspberry Pi maps GPIO13 & GPIO19 to Hardware PWM channel 1 + else: + self.hardware_pwm_channel = 0 # Raspberry Pi maps GPIO12 & GPIO18 to Hardware PWM channel 0 self.pwm = HardwarePWM(pwm_channel=self.hardware_pwm_channel, hz=self.frequency) - self.logger.debug('grillplat pifire_pwm __init__: Hardware PWM setup: Using PWM channel ' + str(self.hardware_pwm_channel) + ' and PWM frequency ' + str(self.frequency)) + self.logger.debug('Hardware PWM setup: Using PWM channel ' + str(self.hardware_pwm_channel) + ' and PWM frequency ' + str(self.frequency)) else: self.fan = OutputDevice(self.out_pins['fan'], active_high=active_high, initial_value=False) @@ -71,18 +86,18 @@ def __init__(self, out_pins, in_pins, trigger_level='LOW', dc_fan=False, frequen self.power = OutputDevice(self.out_pins['power'], active_high=active_high, initial_value=False) def auger_on(self): - self.logger.debug('grillplat pifire_pwm auger_on: Turning on augur') + self.logger.debug('auger_on: Turning on auger') self.auger.on() def auger_off(self): - self.logger.debug('grillplat pifire_pwm auger_off: Turning off augur') + self.logger.debug('auger_off: Turning off auger') self.auger.off() def fan_on(self, fan_speed_percent=100): self.fan.on() # Turn on fan output pin to enable fan power if self.dc_fan: self._stop_ramp() - self.logger.debug('grillplat pifire_pwm fan_on: Turning on PWM fan with fan speed percent ' + str(fan_speed_percent)) + self.logger.debug('fan_on: Turning on PWM fan with fan speed percent ' + str(fan_speed_percent)) start_duty_cycle = float(100 - fan_speed_percent) # PWM duty cycle = (100 - fan percent speed) self.pwm.start(start_duty_cycle) # Hardware PWM needs to have a start() before we can change_duty_cycle() later self.current_fan_speed_percent = fan_speed_percent # Keep track of our current fan percent speed @@ -90,7 +105,7 @@ def fan_on(self, fan_speed_percent=100): def fan_off(self): self.fan.off() if self.dc_fan: - self.logger.debug('grillplat pifire_pwm fan_off: Turning off PWM fan') + self.logger.debug('fan_off: Turning off PWM fan') self.pwm.stop() self.current_fan_speed_percent = 0 # Fan is off, so our current fan speed is now 0 @@ -103,38 +118,40 @@ def set_duty_cycle(self, fan_speed_percent, override_ramping=True): # If the ramp thread is doing the calling, then we set override_ramping to False when calling, so we don't end up stopping the thread we are in. if override_ramping: self._stop_ramp() - self.logger.debug('grillplat pifire_pwm set_duty_cycle: Changing fan speed percent to ' + str(fan_speed_percent)) + self.logger.debug('set_duty_cycle: Changing fan speed percent to ' + str(fan_speed_percent)) pwm_duty_cycle = float(100 - fan_speed_percent) # Duty cycle is inverted due to PWM board amplifier circuitry self.pwm.change_duty_cycle(pwm_duty_cycle) # Hardware PWM library simply takes duty cycle in percent self.current_fan_speed_percent = fan_speed_percent # Keep track of our current fan percent speed def pwm_fan_ramp(self, on_time=5, min_duty_cycle=20, max_duty_cycle=100): self.fan.on() - self.logger.debug('grillplat pifire_pwm pwm_fan_ramp: Starting fan ramp: on_time: ' + str(on_time) + ' min_duty_cycle: ' + str(min_duty_cycle) + ' max_duty_cycle: ' + str(max_duty_cycle)) + self.logger.debug('pwm_fan_ramp: Starting fan ramp: on_time: ' + str(on_time) + ' min_duty_cycle: ' + str(min_duty_cycle) + ' max_duty_cycle: ' + str(max_duty_cycle)) self._start_ramp(on_time=on_time, min_duty_cycle=min_duty_cycle, max_duty_cycle=max_duty_cycle) def set_pwm_frequency(self, frequency=30): - self.logger.debug('grillplat pifire_pwm set_pwm_frequency: Setting PWM signal frequency to ' + str(frequency)) + self.logger.debug('set_pwm_frequency: Setting PWM signal frequency to ' + str(frequency)) self.pwm.change_frequency(frequency) def igniter_on(self): - self.logger.debug('grillplat pifire_pwm igniter_on: Turning on igniter') + self.logger.debug('igniter_on: Turning on igniter') self.igniter.on() def igniter_off(self): - self.logger.debug('grillplat pifire_pwm igniter_off: Turning off igniter') + self.logger.debug('igniter_off: Turning off igniter') self.igniter.off() def power_on(self): - self.logger.debug('grillplat pifire_pwm power_on: Powering on grill platform') + self.logger.debug('power_on: Powering on grill platform') self.power.on() def power_off(self): - self.logger.debug('grillplat pifire_pwm power_off: Powering off grill platform') + self.logger.debug('power_off: Powering off grill platform') self.power.off() def get_input_status(self): - return self.selector.is_active + if self.in_pins['selector'] is not None and self.standalone == False: + return self.selector.is_active + return False def get_output_status(self): self.current = {} @@ -143,15 +160,15 @@ def get_output_status(self): self.current['power'] = self.power.is_active self.current['fan'] = self.fan.is_active if self.dc_fan: -# self.logger.debug('grillplat pifire_pwm get_output_status: self.current_fan_speed_percent = ' + str(self.current_fan_speed_percent)) # This is a little verbose, even for debug logging +# self.logger.debug('get_output_status: self.current_fan_speed_percent = ' + str(self.current_fan_speed_percent)) # This is a little verbose, even for debug logging self.current['pwm'] = self.current_fan_speed_percent self.current['frequency'] = self.frequency return self.current def _start_ramp(self, on_time, min_duty_cycle, max_duty_cycle, background=True): self._stop_ramp() - self.logger.debug('grillplat pifire_pwm _start_ramp: Setting starting fan percentage for ramp: min_duty_cycle: ' + str(min_duty_cycle)) - self.logger.debug('grillplat pifire_pwm _start_ramp: Starting fan ramp thread: on_time: ' + str(on_time) + ' min_duty_cycle: ' + str(min_duty_cycle) + ' max_duty_cycle: ' + str(max_duty_cycle)) + self.logger.debug('_start_ramp: Setting starting fan percentage for ramp: min_duty_cycle: ' + str(min_duty_cycle)) + self.logger.debug('_start_ramp: Starting fan ramp thread: on_time: ' + str(on_time) + ' min_duty_cycle: ' + str(min_duty_cycle) + ' max_duty_cycle: ' + str(max_duty_cycle)) min_fan_percent = min_duty_cycle # Keeping things sane, the setting passed in is actually percent, not the eventual PWM duty cycle self.fan_on(min_fan_percent) # Need to turn on PWM with starting percentage self._ramp_thread = GPIOThread(self._ramp_device, (on_time, min_duty_cycle, max_duty_cycle)) @@ -161,14 +178,14 @@ def _start_ramp(self, on_time, min_duty_cycle, max_duty_cycle, background=True): self._ramp_thread = None def _stop_ramp(self): - self.logger.debug('grillplat pifire_pwm _stop_ramp: Stopping fan ramp') + self.logger.debug('_stop_ramp: Stopping fan ramp') if self._ramp_thread: self._ramp_thread.stop() self._ramp_thread = None def _ramp_device(self, on_time, min_duty_cycle, max_duty_cycle, fps=25): duty_cycle = max_duty_cycle / 100 - self.logger.debug('grillplat pifire_pwm _ramp_device: Fan ramp thread calculating / executing') + self.logger.debug('_ramp_device: Fan ramp thread calculating / executing') sequence = [] sequence += [ (1 - (i * (duty_cycle / fps) / on_time), 1 / fps) @@ -186,6 +203,16 @@ def _ramp_device(self, on_time, min_duty_cycle, max_duty_cycle, fps=25): if self._ramp_thread.stopping.wait(delay): break + def cleanup(self): + self.power.close() + self.igniter.close() + self.auger.close() + self.fan.close() + self.pwm.stop() + if self.selector is not None: + self.selector.close() + + # MARK: System / Platform Commands """ ============================== System / Platform Commands diff --git a/notify/notifications.py b/notify/notifications.py index f46f40c1..315e5b1d 100644 --- a/notify/notifications.py +++ b/notify/notifications.py @@ -24,7 +24,7 @@ import logging import math from common import write_settings, write_control, create_logger, read_history -from scipy.interpolate import interp1d +from sklearn.linear_model import LinearRegression ''' ============================================================================== @@ -463,59 +463,40 @@ def _estimate_eta(temperatures, target_temperature, interval_seconds=3, max_hist #print('DEBUG: ETA: History data interval not between 1 and 60 seconds.') eventLogger.debug(f'ETA: History data interval not between 1 and 60 seconds.') return None - - # If there is more data than needed, shorten the list - readings_per_minute = (60 // interval_seconds) - minutes_of_data = len(temperatures) // readings_per_minute - if minutes_of_data > max_history_minutes: - while len(temperatures) // readings_per_minute > max_history_minutes: - temperatures.pop(0) - # If there is less data than needed, return None - elif minutes_of_data < min_history_minutes: - #print('DEBUG: Not enough history data to make estimate.') + + # Convert minutes to seconds + max_data_points = int(max_history_minutes * 60 / interval_seconds) + min_data_points = int(min_history_minutes * 60 / interval_seconds) + + # Check if enough data points provided + if len(temperatures) < min_data_points: eventLogger.debug(f'ETA: Not enough history data to make estimate.') return None - # Build times list - times = [] - for index in range(0, len(temperatures) * interval_seconds, interval_seconds): - times.append(index) + # Truncate data to fit within limits + temperatures = temperatures[-max_data_points:] - #print(f'===========================================') - #print(f'DEBUG: ETA: times = {times}') - #print(f'DEBUG: ETA: temps = {temperatures}') - #print(f'===========================================') + # Prepare data for linear regression + X = [[i] for i in range(len(temperatures))] # Time steps as features + y = temperatures # Temperature values as target try: - # Create an interpolation function from the temperature data - interpolator = interp1d(times, temperatures, axis=0, bounds_error=False, kind="linear", fill_value="extrapolate") - - # Estimate the time to reach the target temperature - estimated_time = interpolator(target_temperature) - # If estimated time is over 24 hours or less than 0, it's likely to be a bad guess - if estimated_time > 86400 or estimated_time <= 0: - #print(f'DEBUG: ETA: Estimated time outside of bounds. [{estimated_time}]') - eventLogger.debug(f'ETA: Estimated time outside of bounds. [{estimated_time}]') - return None - eta = math.ceil(int(estimated_time) - times[-1]) - if eta <= 0: - # Additional bounds testing - #print(f'DEBUG: ETA: Estimated time outside of bounds. [{eta}]') - eventLogger.debug(f'ETA: Estimated time outside of bounds. [{eta}]') + # Fit the linear regression model + model = LinearRegression() + model.fit(X, y) + + # Predict time to reach target temperature (assuming linear trend) + predicted_time = (target_temperature - y[-1]) / model.coef_[0] * interval_seconds + + # Ensure positive prediction + if predicted_time < 0: + eventLogger.debug(f'ETA: Estimated time is negative. [{predicted_time}]') return None - #print(f'===========================================') - #print(f'DEBUG: ETA: {eta}s') - #print(f'===========================================') - eventLogger.debug(f'Calculated ETA: {eta}s') - except: - # Something failed, return None - #print('DEBUG: ETA: An exception occurred.') - eventLogger.debug(f'ETA: An exception occurred.') - #raise - return None - - return eta + eventLogger.debug(f'ETA: Error calculating ETA.') + return None + + return int(predicted_time) mqtt = None def _send_mqtt_notification(control, settings, diff --git a/probes/base.py b/probes/base.py index 3e0be000..d223895d 100644 --- a/probes/base.py +++ b/probes/base.py @@ -32,6 +32,10 @@ class ProbeInterface: def __init__(self, probe_info, device_info, units): self.units = units self.device_info = device_info + if self.device_info['config'].get('transient', 'False') == 'True': + self.transient = True + else: + self.transient = False self.set_profiles(probe_info) self._build_port_map(probe_info) self._build_output_data(probe_info) @@ -123,6 +127,10 @@ def _temp_to_resistance(self, temp, probe_profile): return Tr def _voltage_to_temp(self, voltage, probe_profile): + if voltage == None: + ''' Transient probe detected. ''' + return None, 0 + ''' Check to make sure voltage is between 0V and Vs defined in profile, plus some guard band ''' if(voltage > 0) and (voltage <= ((probe_profile['Vs'] * 1000) * 1.01)): ''' @@ -192,15 +200,20 @@ def read_all_ports(self, output_data): port_values[port], self.output_data['tr'][self.port_map[port]] = self._voltage_to_temp(port_values[port], self.probe_profiles[port]) ''' Enqueue the Temperature Readings to Port Queues ''' - self.port_queues[port].enqueue(port_values[port]) + if port_values[port] == None: + ''' If the read value is None, pass that to the output instead of adding to the queue ''' + output_value = None + else: + self.port_queues[port].enqueue(port_values[port]) + output_value = self.port_queues[port].average() ''' Get average temperature from the queue and store it in the output data structure''' if port == self.primary_port: - self.output_data['primary'][self.port_map[port]] = self.port_queues[port].average() + self.output_data['primary'][self.port_map[port]] = output_value elif port in self.food_ports: - self.output_data['food'][self.port_map[port]] = self.port_queues[port].average() + self.output_data['food'][self.port_map[port]] = output_value elif port in self.aux_ports: - self.output_data['aux'][self.port_map[port]] = self.port_queues[port].average() + self.output_data['aux'][self.port_map[port]] = output_value if self.time_delay: time.sleep(self.time_delay) # Time delay, if needed for single-shot mode on some ADC's @@ -224,6 +237,9 @@ def set_profiles(self, probe_info): def get_port_map(self): return self.port_map + def get_device_info(self): + return self.device_info + class FakeDevice: def __init__(self, port_map, primary_port, units): diff --git a/probes/disabled.py b/probes/disabled.py new file mode 100644 index 00000000..6587483c --- /dev/null +++ b/probes/disabled.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +''' +***************************************** +PiFire Probes Disabled Module +***************************************** + +Description: + This module is loaded in the case where the probes complex is disabled. + + Ex Device Definition: + + device_info = { + 'device' : 'your_device_name', # Unique name for the device + 'module' : 'disabled', # Must be populated for this module to load properly + 'ports' : ['ADC0', 'RTD0', etc... ], # This should be defined by the user with the number of ports desired + 'config' : {} + } +''' + +''' +***************************************** + Imported Libraries +***************************************** +''' + +from probes.base import ProbeInterface + +''' +***************************************** + Class Definitions +***************************************** +''' + +class ReadProbes(ProbeInterface): + + def __init__(self, probe_info, device_info, units): + super().__init__(probe_info, device_info, units) + + def _init_device(self): + pass + + def read_all_ports(self, output_data): + for port in self.port_map: + self.output_data['tr'][self.port_map[port]] = 0 + + ''' Get average temperature from the queue and store it in the output data structure''' + if port == self.primary_port: + self.output_data['primary'][self.port_map[port]] = 0 + elif port in self.food_ports: + self.output_data['food'][self.port_map[port]] = 0 + elif port in self.aux_ports: + self.output_data['aux'][self.port_map[port]] = 0 + + return self.output_data diff --git a/probes/ds18b20.py b/probes/ds18b20.py index 6f9432b3..934b0c9c 100644 --- a/probes/ds18b20.py +++ b/probes/ds18b20.py @@ -8,18 +8,33 @@ Description: This module utilizes the DS18B20 hardware and returns temperature data. Depends on: pip install w1thermsensor + Details on this module can be found here: https://github.com/timofurrer/w1thermsensor + + Note: This device is not recommended (currently) for anything other than a reference probe. Thus, the recommendation + is to utilize this probe for calibrating / tuning probe profiles only. PiFire will operate best if it is added to + the configuration only when needed to do calibration, then removed from the configuration during normal usage. + This prevents the probe from being queried every time the temperature is read from other devices. However, if it is + left enabled, the probe can be hot-plugged and should read temperatures when attached and initialized by the kernel. + Set this probe to AUX in the configuration wizard so that it does not appear in the user interface. + + Note: Expects this device to be connected to a specific GPIO Pin defined in config.txt, + should be pulled up through a physical 4.7-10k pull-up to 3.3V. Removed the passive pull-up + code from this version going forward. + + Edit /boot/config.txt (or /boot/firmware/config.txt) to add: + dtoverlay=w1-gpio,gpiopin=[pin_number] + + (This is automatically added by the configuration wizard / board configuration tool) - Note: Still experimental. Expects this device to be connected to GPIO 6 (Pin 31) - Edit /boot/config.txt to add: - dtoverlay=w1-gpio,gpiopin=6,pullup="y" - Ex Device Definition: - device = { + device_info = { 'device' : 'your_device_name', # Unique name for the device 'module' : 'ds18b20', # Must be populated for this module to load properly 'ports' : ['DS0'], # This is defined in the module, so this does not need to be defined. - 'config' : {} + 'config' : { + 'transient' = 'True' + } } ''' @@ -31,9 +46,9 @@ ''' import logging import time -from w1thermsensor import W1ThermSensor, Unit +import threading +from w1thermsensor import W1ThermSensor, Unit, Sensor from probes.base import ProbeInterface -import RPi.GPIO as GPIO ''' ***************************************** @@ -45,16 +60,60 @@ class DS18B20_Device(): ''' DS18B20 Device Utilizing the w1thermsensor module ''' def __init__(self): self.logger = logging.getLogger("control") - # Setup software pull-up which will wake the device connected - GPIO.setmode(GPIO.BCM) - GPIO.setup(6, GPIO.IN, pull_up_down=GPIO.PUD_UP) + self.available = False + self.initialized = False + self.temperature_C = None + + try: + self.init_device() + except: + ''' Device is unavailable for some reason ''' + self.logger.info('DS18B20 device wasn\'t initialized because it was not found.') + + # Setup & Start Sensor Loop Thread + self.sensor_thread_active = True + self.sensor_thread_update = True # Get initial temperature from sensor + self.sensor_thread = threading.Thread(target=self._sensing_loop) + self.sensor_thread.start() - time.sleep(4) # Give time for the kernel to connect to the 1-wire bus and get the device IDs - self.sensor = W1ThermSensor() + def init_device(self): + try: + self.sensor = W1ThermSensor() + self.available = True + self.initialized = True + except: + self.available = False + self.initialized = False + + def check_availability(self): + try: + if not self.initialized: + self.init_device() + for sensor in self.sensor.get_available_sensors([Sensor.DS18B20]): + self.available = True + except: + self.available = False + return self.available + + def _sensing_loop(self): + while self.sensor_thread_active: + if self.sensor_thread_update: + try: + if not self.initialized: + self.init_device() + else: + self.temperature_C = self.sensor.get_temperature() + except: + self.temperature_C = None + self.available = False + + self.sensor_thread_update = False + time.sleep(0.1) @property def temperature(self): - return self.sensor.get_temperature() + self.sensor_thread_update = True + return self.temperature_C class ReadProbes(ProbeInterface): @@ -65,16 +124,17 @@ def __init__(self, probe_info, device_info, units): def _init_device(self): self.time_delay = 0 self.device_info['ports'] = ['DS0'] - try: - self.device = DS18B20_Device() - except: - self.logger.error('Something went wrong when trying to initialize the DS18B20 device.') - raise + self.device = DS18B20_Device() def read_all_ports(self, output_data): ''' Read temperature from device ''' - tempC = round(self.device.temperature, 1) - tempF = int(tempC * (9/5) + 32) # Celsius to Fahrenheit + tempC = self.device.temperature + if tempC is not None: + tempC = round(tempC, 1) + tempF = int(tempC * (9/5) + 32) # Celsius to Fahrenheit + else: + tempC = None + tempF = None port = self.device_info['ports'][0] ''' Read resistance from device ''' diff --git a/probes/main.py b/probes/main.py index f37406a0..11e42240 100644 --- a/probes/main.py +++ b/probes/main.py @@ -21,11 +21,14 @@ class ProbesMain: - def __init__(self, probe_map, units): + def __init__(self, probe_map, units, disable=False): + self.errors = [] self.logger = logging.getLogger("control") - self.units = units + self.units = units + self.disable = disable self.probe_devices = probe_map['probe_devices'] self.probe_info = probe_map['probe_info'] + self.device_info_list = [] self._setup_probe_devices(self.probe_devices) def _setup_probe_devices(self, probe_devices): @@ -33,29 +36,33 @@ def _setup_probe_devices(self, probe_devices): self.probe_device_list = [] for device in probe_devices: try: - modulename = device['module'] + if not self.disable: + modulename = device['module'] + else: + modulename = 'disabled' + devicename = device['device'] newmodule = importlib.import_module(f'probes.{modulename}') except: - newmodule = importlib.import_module('probes.prototype') - error_event = f'An error occurred loading the [{modulename}] probe module. The ' \ - f'prototype module has been loaded instead. This sometimes means that the hardware is not connected ' \ - f'properly, or the module is not configured. Please run the configuration wizard again from the admin ' \ - f'panel to fix this issue.' - self.logger.exception(error_event) - break + newmodule = importlib.import_module('probes.disabled') + device['module'] = 'disabled' + error_event = f'An error occurred loading the [{modulename}] probe module for [{devicename}]. '\ + f'PiFire will not display probe data for this device ({devicename}). ' \ + f'This sometimes means that the hardware is not connected properly, or the module is not configured. ' \ + f'Please run the configuration wizard again from the admin panel to fix this issue. ' + self.errors.append(error_event) + self.logger.error(error_event) ''' Send the probe information and the device information to the device module ''' instance = newmodule.ReadProbes(self.probe_info, device, self.units) + self.device_info_list.append(device) # Build list of device information ''' Append the probe device to the devices list ''' self.probe_device_list.append(instance) - return error_event - def read_probes(self): ''' Loop through all probe devices and get all data @@ -94,4 +101,10 @@ def update_units(self, units): :return: None """ for device in self.probe_device_list: - device.update_units(units) \ No newline at end of file + device.update_units(units) + + def get_errors(self): + return self.errors + + def get_device_info(self): + return self.device_info_list diff --git a/probes/prototype.py b/probes/prototype.py index 1e6fd2c1..e7755882 100644 --- a/probes/prototype.py +++ b/probes/prototype.py @@ -10,7 +10,7 @@ Ex Device Definition: - device = { + device_info = { 'device' : 'your_device_name', # Unique name for the device 'module' : 'prototype', # Must be populated for this module to load properly 'ports' : ['ADC0', 'ADC1', 'ADC2', 'ADC3'], # This should be defined by the user with the number of ports desired @@ -20,7 +20,8 @@ 'ADC2_rd': '10000', 'ADC3_rd': '10000', 'i2c_bus_addr': '0x48', - 'voltage_ref': '3.28' + 'voltage_ref': '3.28', + 'transient' : False } } @@ -47,7 +48,8 @@ class ProtoDevice(): ''' Create a test devices that returns values for testing ''' - def __init__(self, port_map, primary_port, units): + def __init__(self, port_map, primary_port, units, transient=False): + self.transient = transient self.port_value = {} self.primary_port = primary_port self.units = units @@ -76,6 +78,11 @@ def read_voltage(self, port): elif seed < 1 and self.port_value[port] > self.minPrimaryVoltage: self.port_value[port] -= self.primaryChangeFactor else: + if self.transient: + ''' If transient, then return None-type approximately 80% of the time. ''' + if random.randint(0,100) < 80: + return None + if seed > 7 and self.port_value[port] > self.maxFoodVoltage: self.port_value[port] -= self.otherChangeFactor elif seed < 1 and self.port_value[port] < self.minFoodVoltage: @@ -90,4 +97,4 @@ def __init__(self, probe_info, device_info, units): def _init_device(self): self.time_delay = 0 - self.device = ProtoDevice(self.port_map, self.primary_port, self.units) + self.device = ProtoDevice(self.port_map, self.primary_port, self.units, transient=self.transient) diff --git a/probes/temp_queue.py b/probes/temp_queue.py index 255e139b..ad6e0e34 100644 --- a/probes/temp_queue.py +++ b/probes/temp_queue.py @@ -35,7 +35,7 @@ def average(self): self.last_average = 0 return(0) elif self.last_average == 0: - # Handle case if lastaverage isn't initialized + # Handle case if last_average isn't initialized average = (sum(self.queue) / self.qlength) self.last_average = average if self.units == 'F': diff --git a/static/img/wizard/custom.png b/static/img/wizard/custom.png new file mode 100644 index 00000000..d79533f4 Binary files /dev/null and b/static/img/wizard/custom.png differ diff --git a/static/img/wizard/custom.xcf b/static/img/wizard/custom.xcf new file mode 100644 index 00000000..26f8f5e8 Binary files /dev/null and b/static/img/wizard/custom.xcf differ diff --git a/static/img/wizard/dsi_touch.png b/static/img/wizard/dsi_touch.png index 86007aa9..49792426 100644 Binary files a/static/img/wizard/dsi_touch.png and b/static/img/wizard/dsi_touch.png differ diff --git a/static/img/wizard/dsi_touch.xcf b/static/img/wizard/dsi_touch.xcf index 6f91e0d3..9808b107 100644 Binary files a/static/img/wizard/dsi_touch.xcf and b/static/img/wizard/dsi_touch.xcf differ diff --git a/static/img/wizard/pcb_2.00a.png b/static/img/wizard/pcb_2.00a.png new file mode 100644 index 00000000..7d9ce00f Binary files /dev/null and b/static/img/wizard/pcb_2.00a.png differ diff --git a/static/img/wizard/pcb_2.00a.xcf b/static/img/wizard/pcb_2.00a.xcf new file mode 100644 index 00000000..b69ceb75 Binary files /dev/null and b/static/img/wizard/pcb_2.00a.xcf differ diff --git a/static/img/wizard/pcb_3.01a.png b/static/img/wizard/pcb_3.01a.png new file mode 100644 index 00000000..4f0d80b2 Binary files /dev/null and b/static/img/wizard/pcb_3.01a.png differ diff --git a/static/img/wizard/pcb_3.01a.xcf b/static/img/wizard/pcb_3.01a.xcf new file mode 100644 index 00000000..c02c6325 Binary files /dev/null and b/static/img/wizard/pcb_3.01a.xcf differ diff --git a/static/img/wizard/pcb_4.x.x.png b/static/img/wizard/pcb_4.x.x.png new file mode 100644 index 00000000..1ee3ea47 Binary files /dev/null and b/static/img/wizard/pcb_4.x.x.png differ diff --git a/static/img/wizard/pcb_4.x.x.xcf b/static/img/wizard/pcb_4.x.x.xcf new file mode 100644 index 00000000..f90533d1 Binary files /dev/null and b/static/img/wizard/pcb_4.x.x.xcf differ diff --git a/static/img/wizard/pcb_pwm.png b/static/img/wizard/pcb_pwm.png new file mode 100644 index 00000000..fa2853f5 Binary files /dev/null and b/static/img/wizard/pcb_pwm.png differ diff --git a/static/img/wizard/pcb_pwm.xcf b/static/img/wizard/pcb_pwm.xcf new file mode 100644 index 00000000..3a2458c3 Binary files /dev/null and b/static/img/wizard/pcb_pwm.xcf differ diff --git a/static/img/wizard/protoflex.png b/static/img/wizard/protoflex.png new file mode 100644 index 00000000..c0cf75d1 Binary files /dev/null and b/static/img/wizard/protoflex.png differ diff --git a/static/js/dash_basic.js b/static/js/dash_basic.js index c73b0cc0..9906d359 100644 --- a/static/js/dash_basic.js +++ b/static/js/dash_basic.js @@ -1,6 +1,8 @@ // Dashboard JS // Global Variables +var errorCounter = 0; +var maxErrorCount = 30; var hopper_level = 100; var hopper_pellets = ''; var ui_hash = ''; @@ -23,6 +25,12 @@ function updateProbeCards() { url : '/api/current', type : 'GET', success : function(current){ + // Clear error counter + if (errorCounter > 0) { + errorCounter = 0; + $("#serverOfflineModal").modal('hide'); + }; + // Local Variables: var probes = []; @@ -218,6 +226,13 @@ function updateProbeCards() { // document.getElementById('smokeplus_status').innerHTML = ''; //}; + }, + error: function() { + console.log('Error: Failed to get current status from server. Try: ' + errorCounter); + errorCounter += 1; + if (errorCounter > maxErrorCount) { + $("#serverOfflineModal").modal('show'); + }; } }); }; @@ -460,8 +475,12 @@ function setPmode(pmode) { // Show the Dashboard Settings Modal/Dialog when clicked function dashSettings() { + dashLoadConfig(); $("#dashSettingsModal").modal('show'); - //dashData(); +}; + +function dashLoadConfig() { + $("#dash_config_card").load("/dashconfig"); }; // Get dashboard data structure @@ -527,6 +546,10 @@ function dashToggleVisible(cardID) { }; } +function dashClearErrorCounter() { + errorCounter = 0; +}; + // Main $(document).ready(function(){ // Setup Listeners @@ -545,5 +568,5 @@ $(document).ready(function(){ updateHopperStatus(); // Current hopper information loop - setInterval(updateHopperStatus, 150000); // Update every 150000ms + setInterval(updateHopperStatus, 30000); // Update every 30000ms (30 seconds) }); diff --git a/static/js/dash_default.js b/static/js/dash_default.js index 000a3236..d6a89ca6 100644 --- a/static/js/dash_default.js +++ b/static/js/dash_default.js @@ -1,6 +1,8 @@ // Dashboard Default JS // Global Variables +var errorCounter = 0; +var maxErrorCount = 30; var hopper_level = 100; var hopper_pellets = ''; var ui_hash = ''; @@ -26,17 +28,21 @@ if (typeof dashDataStruct == 'undefined') { if (units == 'F') { var maxTempPrimary = 600; var maxTempFood = 300; + var minTemp = 0; } else { var maxTempPrimary = 300; var maxTempFood = 150; + var minTemp = -20; }; } else { if (units == 'F') { var maxTempPrimary = dashDataStruct.config.max_primary_temp_F; var maxTempFood = dashDataStruct.config.max_food_temp_F; + var minTemp = 0; } else { var maxTempPrimary = dashDataStruct.config.max_primary_temp_C; var maxTempFood = dashDataStruct.config.max_food_temp_C; + var minTemp = -20; }; }; @@ -74,9 +80,10 @@ function initProbeGauge(key) { }; var probeGauge = Gauge(document.getElementById(key+"_gauge"), { max: maxTemp, + min: minTemp, // custom label renderer label: function(value) { - return Math.round(value); + return Math.round(value); }, value: 0, // Custom dial colors (Optional) @@ -98,6 +105,12 @@ function updateProbeCards() { url : '/api/current', type : 'GET', success : function(current){ + // Clear error counter + if (errorCounter > 0) { + errorCounter = 0; + $("#serverOfflineModal").modal('hide'); + }; + // Local Variables: // Check for server side changes and reload if needed @@ -305,12 +318,28 @@ function updateProbeCards() { // document.getElementById('smokeplus_status').innerHTML = ''; //}; + }, + error: function() { + console.log('Error: Failed to get current status from server. Try: ' + errorCounter); + errorCounter += 1; + if (errorCounter > maxErrorCount) { + $("#serverOfflineModal").modal('show'); + }; } }); }; // Update the temperature for a specific probe/card function updateTempCard(key, temp) { + //console.log('Update Temp Card: ' + key + ' temp: ' + temp); + var index = dashDataStruct.custom.hidden_cards.indexOf(key); // Index of cardID + if (index == -1) { + if ((temp != null) && $('#card_'+key).is(":hidden")) { + $('#card_'+key).show(); + } else if (temp == null) { + $('#card_'+key).hide(); + }; + }; probeGauges[key].setValueAnimated(temp, 0.25); // (value, animation duration in seconds) }; @@ -600,7 +629,7 @@ function dashToggleVisible(cardID) { if (index !== -1) { dashDataStruct.custom.hidden_cards.splice(index, 1); // If found, remove }; - console.log('dashData Hidden='+dashDataStruct.custom.hidden_cards); + //console.log('dashData Hidden='+dashDataStruct.custom.hidden_cards); dashSetData(); } else { // change card to hidden @@ -612,11 +641,15 @@ function dashToggleVisible(cardID) { if (index == -1) { dashDataStruct.custom.hidden_cards.push(cardID); // If not found, add }; - console.log('dashData Hidden='+dashDataStruct.custom.hidden_cards); + //console.log('dashData Hidden='+dashDataStruct.custom.hidden_cards); dashSetData(); }; } +function dashClearErrorCounter() { + errorCounter = 0; +}; + // Main $(document).ready(function(){ // Setup Listeners @@ -638,5 +671,5 @@ $(document).ready(function(){ updateHopperStatus(); // Current hopper information loop - setInterval(updateHopperStatus, 150000); // Update every 150000ms + setInterval(updateHopperStatus, 30000); // Update every 30000ms (30 seconds) }); diff --git a/templates/_macro_control_panel.html b/templates/_macro_control_panel.html index 90e8a4a2..e6c36b6a 100644 --- a/templates/_macro_control_panel.html +++ b/templates/_macro_control_panel.html @@ -88,7 +88,7 @@ {% endif %} id="splus_btn" name="setmodesmokeplus" value="true"> -
{% endmacro %} -{% macro render_config_card(dash_metadata, dash_data) %} -{% from '_macro_generic_config.html' import config_input_float_int, config_input_list, config_input_string%} - -{% endmacro %} diff --git a/templates/_macro_generic_config.html b/templates/_macro_generic_config.html index 7937e291..e302136f 100644 --- a/templates/_macro_generic_config.html +++ b/templates/_macro_generic_config.html @@ -25,3 +25,53 @@ name="{{ id_prefix }}_{{ label }}"/> {% endmacro %} +{% macro render_dash_config_card(dash_metadata, dash_data) %} + +{% endmacro %} \ No newline at end of file diff --git a/templates/_macro_settings.html b/templates/_macro_settings.html index cc8c7a9c..5b26bcec 100644 --- a/templates/_macro_settings.html +++ b/templates/_macro_settings.html @@ -38,7 +38,7 @@Setting | -Value | -Recommended | -Description | +Setting | +Value | +Default | +Description | Cycle Time (s) | - + | - + | - + |
---|