Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5e954f2
feat(aux): add auxiliary relay settings defaults and get_aux_list helper
dberlin Aug 11, 2026
687a89a
feat(aux): add auxiliary relay support to the prototype platform
dberlin Aug 11, 2026
d8ea1ce
feat(aux): add auxiliary relay support to the raspberry pi platform
dberlin Aug 11, 2026
bd8ebbe
feat(aux): process auxiliary relay requests in the control script
dberlin Aug 11, 2026
ccad2ac
feat(aux): add /api/set/aux endpoint for auxiliary relay control
dberlin Aug 11, 2026
6370648
feat(aux): add auxiliary relay toggles to the default dashboard
dberlin Aug 11, 2026
c56fa32
feat(aux): add auxiliary relay toggles to the basic dashboard
dberlin Aug 11, 2026
ea5a2f3
Ignore agent scratch directories
dberlin Aug 11, 2026
9a22e52
feat(aux): add auxiliary relay menu to the 800x480 touch displays
dberlin Aug 11, 2026
7a381dc
fix(aux): prefix aux relay toggle command with cmd_ so touch/ENTER di…
dberlin Aug 11, 2026
88ad761
feat(aux): publish auxiliary relay state over MQTT and the mobile API
dberlin Aug 11, 2026
22c83a9
fix(aux): restore CRLF line endings in mqtt_handler.py
dberlin Aug 11, 2026
be925d2
feat(aux): add auxiliary relay GPIO pin selection to the wizard
dberlin Aug 11, 2026
418c9cc
feat(aux): add auxiliary relay label editing to the settings page
dberlin Aug 11, 2026
9126c47
Fix stored-XSS in auxiliary relay label rendering
dberlin Aug 11, 2026
dbc45b7
Fix findings from final whole-branch review of auxiliary relay feature
dberlin Aug 11, 2026
07c5354
Color the aux relay menu buttons by live on/off state
dberlin Aug 11, 2026
f2cd9d9
Add auxiliary relay GPIO dropdowns to PCB presets in wizard manifest
dberlin Aug 11, 2026
460397a
Guard wizard against assigning one GPIO pin to two platform functions
dberlin Aug 11, 2026
43c8170
Also claim 1-Wire and SPI0 CE0/CE1 pins in the wizard pin-collision g…
dberlin Aug 11, 2026
1d8feef
Also claim the GPIO shutdown pin in the wizard pin-collision guard
dberlin Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@

generated/

# Agent scratch / tooling state - never commit
.superpowers/
.serena/
.worktrees/

*.pyc

control.err.log
Expand Down
66 changes: 64 additions & 2 deletions blueprints/dash/static/basic/js/dash_basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var notify_status = {}; // store all notify statuses
var last_fan_status = null;
var last_auger_status = null;
var last_igniter_status = null;
var last_aux_status = {};
var aux_labels = {};
var last_pmode_status = null;
var last_lid_open_status = false;
var display_mode = null;
Expand Down Expand Up @@ -58,6 +60,29 @@ function initNotifyIndicators(probes) {
};

// Update temperatures on probe status cards
// Auxiliary relay icons render as either <i> (before Font Awesome's SVG
// conversion runs) or <svg> (after conversion, which replaces the <i>
// element and moves the tooltip text into an inner <title> node). These
// helpers update whichever form is currently in the DOM using only
// property/textContent assignment (never innerHTML), so a user-supplied
// label can never be parsed as markup.
function getAuxIcon(container) {
return container.querySelector('svg, i');
}

function setAuxIconTooltip(icon, text) {
if (icon.tagName.toLowerCase() === 'svg') {
var titleEl = icon.querySelector('title');
if (titleEl === null) {
titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title');
icon.insertBefore(titleEl, icon.firstChild);
}
titleEl.textContent = text;
} else {
icon.title = text;
}
}

function updateProbeCards() {
req = $.ajax({
url : '/api/current',
Expand Down Expand Up @@ -231,6 +256,31 @@ function updateProbeCards() {
};
};

['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) {
if (!(aux_name in current.status.outpins)) {
return;
}
if (current.status.outpins[aux_name] != last_aux_status[aux_name]) {
last_aux_status[aux_name] = current.status.outpins[aux_name];
var element = document.getElementById(aux_name + '_status');
if (element === null) {
return;
}
var icon = getAuxIcon(element);
if (icon === null) {
return;
}
var label = aux_labels[aux_name] || aux_name;
if (last_aux_status[aux_name]) {
setAuxIconTooltip(icon, label + ' ON');
icon.style.color = 'rgb(0, 190, 0)';
} else {
setAuxIconTooltip(icon, label + ' OFF');
icon.style.color = 'rgb(150, 150, 150)';
}
}
});

if (current.status.p_mode != last_pmode_status) {
last_pmode_status = current.status.p_mode;
if (last_pmode_status == 0) {
Expand Down Expand Up @@ -785,8 +835,20 @@ function dashClearErrorCounter() {
$(document).ready(function(){
// Setup Listeners
$('#reloadPage').click(function() {
// Reload page when server side changes detected.
location.reload();
// Reload page when server side changes detected.
location.reload();
});

['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) {
var element = document.getElementById(aux_name + '_status');
if (element === null) {
return;
}
var icon = getAuxIcon(element);
aux_labels[aux_name] = (icon !== null && icon.dataset.label) ? icon.dataset.label : aux_name;
$('#' + aux_name + '_status').click(function() {
cp_api_set('aux/' + aux_name + '/toggle');
});
});

// Initialize Dashboard Data
Expand Down
62 changes: 62 additions & 0 deletions blueprints/dash/static/default/js/dash_default.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var probesReady = false; // Pre-initialized state
var last_fan_status = null;
var last_auger_status = null;
var last_igniter_status = null;
var last_aux_status = {};
var aux_labels = {};
var last_pmode_status = null;
var last_lid_open_status = false;
var last_probe_status = {};
Expand Down Expand Up @@ -146,6 +148,29 @@ function initNotifyIndicators() {
};

// Update temperatures on probe status cards
// Auxiliary relay icons render as either <i> (before Font Awesome's SVG
// conversion runs) or <svg> (after conversion, which replaces the <i>
// element and moves the tooltip text into an inner <title> node). These
// helpers update whichever form is currently in the DOM using only
// property/textContent assignment (never innerHTML), so a user-supplied
// label can never be parsed as markup.
function getAuxIcon(container) {
return container.querySelector('svg, i');
}

function setAuxIconTooltip(icon, text) {
if (icon.tagName.toLowerCase() === 'svg') {
var titleEl = icon.querySelector('title');
if (titleEl === null) {
titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title');
icon.insertBefore(titleEl, icon.firstChild);
}
titleEl.textContent = text;
} else {
icon.title = text;
}
}

function updateProbeCards() {
req = $.ajax({
url : '/api/current',
Expand Down Expand Up @@ -337,6 +362,31 @@ function updateProbeCards() {
};
};

['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) {
if (!(aux_name in current.status.outpins)) {
return;
}
if (current.status.outpins[aux_name] != last_aux_status[aux_name]) {
last_aux_status[aux_name] = current.status.outpins[aux_name];
var element = document.getElementById(aux_name + '_status');
if (element === null) {
return;
}
var icon = getAuxIcon(element);
if (icon === null) {
return;
}
var label = aux_labels[aux_name] || aux_name;
if (last_aux_status[aux_name]) {
setAuxIconTooltip(icon, label + ' ON');
icon.style.color = 'rgb(0, 190, 0)';
} else {
setAuxIconTooltip(icon, label + ' OFF');
icon.style.color = 'rgb(150, 150, 150)';
}
}
});

if (current.status.p_mode != last_pmode_status) {
last_pmode_status = current.status.p_mode;
if (last_pmode_status == 0) {
Expand Down Expand Up @@ -1116,6 +1166,18 @@ $(document).ready(function(){
dash_api_set('lid_open/toggle');
});

['aux1', 'aux2', 'aux3', 'aux4'].forEach(function(aux_name) {
var element = document.getElementById(aux_name + '_status');
if (element === null) {
return;
}
var icon = getAuxIcon(element);
aux_labels[aux_name] = (icon !== null && icon.dataset.label) ? icon.dataset.label : aux_name;
$('#' + aux_name + '_status').click(function() {
dash_api_set('aux/' + aux_name + '/toggle');
});
});

// Initialize Dashboard Data
dashGetData();

Expand Down
14 changes: 13 additions & 1 deletion blueprints/dash/templates/basic/_macro_dash_basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ <h5>Current: <span id="hopperPellets"></span></h5>
</div>
{% endmacro %}

{% macro render_status_card() %}
{% macro render_status_card(settings) %}
<div class="card shadow">
<div class="card-header bg-primary text-white text-center">
<i class="fas fa-info-circle"></i>&nbsp; Status: <span id="mode_status" style="color:yellow"><b>Stop</b></span>
Expand All @@ -332,6 +332,13 @@ <h5>Current: <span id="hopperPellets"></span></h5>
<td>
<span id="igniter_status"><i class="fas fa-fire fa-2x" data-toggle="tooltip" data-placement="top" title="Igniter ON" style="color:rgb(150, 150, 150)"></i></span>&nbsp;
</td>
{% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %}
{% if settings['platform']['outputs'].get(aux_name) is not none %}
<td>
<span id="{{ aux_name }}_status"><i class="fas fa-plug fa-2x" data-toggle="tooltip" data-placement="top" title="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }} OFF" data-label="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}" style="color:rgb(150, 150, 150)"></i></span>&nbsp;
</td>
{% endif %}
{% endfor %}
<td>
<span id="pmode_status" class="fa-stack" style="vertical-align: top;">
<i class="far fa-square fa-stack-2x" style="color:rgb(150, 150, 150)" data-toggle="tooltip" data-placement="top" title="P-Mode"></i>
Expand All @@ -343,6 +350,11 @@ <h5>Current: <span id="hopperPellets"></span></h5>
<td><span class="badge badge-secondary">Fan</span></td>
<td><span class="badge badge-secondary">Auger</span></td>
<td><span class="badge badge-secondary">Igniter</span></td>
{% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %}
{% if settings['platform']['outputs'].get(aux_name) is not none %}
<td><span class="badge badge-secondary">{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}</span></td>
{% endif %}
{% endfor %}
<td><span class="badge badge-secondary">P-Mode</span></td>
</tr>
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion blueprints/dash/templates/basic/dash_basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
style="display:none"
{% endif %}>
<br>
{{ render_status_card() }}
{{ render_status_card(settings) }}
<br>
</div>
</div> <!-- End of Row -->
Expand Down
12 changes: 12 additions & 0 deletions blueprints/dash/templates/default/_macro_dash_default.html
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,13 @@ <h4 class="modal-title"><i class="fas fa-temperature-high"></i>&nbsp; {{ probe_d
<td>
<span id="igniter_status"><i class="fas fa-fire fa-2x" data-toggle="tooltip" data-placement="top" title="Igniter ON" style="color:rgb(150, 150, 150)"></i></span>&nbsp;
</td>
{% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %}
{% if settings['platform']['outputs'].get(aux_name) is not none %}
<td>
<span id="{{ aux_name }}_status"><i class="fas fa-plug fa-2x" data-toggle="tooltip" data-placement="top" title="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }} OFF" data-label="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}" style="color:rgb(150, 150, 150)"></i></span>&nbsp;
</td>
{% endif %}
{% endfor %}
<td id="pmode_icon">
<span id="pmode_status" class="fa-stack" style="vertical-align: top;">
<i class="far fa-square fa-stack-2x" style="color:rgb(150, 150, 150)" data-toggle="tooltip" data-placement="top" title="P-Mode"></i>
Expand All @@ -405,6 +412,11 @@ <h4 class="modal-title"><i class="fas fa-temperature-high"></i>&nbsp; {{ probe_d
<td><span class="badge badge-secondary">Fan</span></td>
<td><span class="badge badge-secondary">Auger</span></td>
<td><span class="badge badge-secondary">Igniter</span></td>
{% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %}
{% if settings['platform']['outputs'].get(aux_name) is not none %}
<td><span class="badge badge-secondary">{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}</span></td>
{% endif %}
{% endfor %}
<td id="pmode_badge">
<span class="badge badge-secondary">P-Mode</span>
</td>
Expand Down
3 changes: 2 additions & 1 deletion blueprints/mobile/socket_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ def _get_dash_data(settings, pelletdb):
'outputs': {
'fan': status['outpins']['fan'],
'auger': status['outpins']['auger'],
'igniter': status['outpins']['igniter']
'igniter': status['outpins']['igniter'],
'aux': {aux['name']: status['outpins'].get(aux['name'], False) for aux in get_aux_list(settings)}
},
'recipeStatus': {
'recipeMode': status['recipe'],
Expand Down
20 changes: 19 additions & 1 deletion blueprints/settings/routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

from flask import render_template, request, render_template_string, jsonify
from common.common import read_settings, read_control, write_settings, write_control, read_generic_json, generate_uuid, convert_settings_units
from common.common import read_settings, read_control, write_settings, write_control, read_generic_json, generate_uuid, convert_settings_units, get_aux_list
from common.app import is_not_blank, is_checked

from . import settings_bp
Expand Down Expand Up @@ -563,6 +563,24 @@ def settings_page(action=None):

write_settings(settings)

if request.method == 'POST' and action == 'aux':
response = request.form

for aux in get_aux_list(settings):
field = f'aux_label_{aux["name"]}'
if field in response:
label = response[field].strip()[:25]
if label != '':
settings['platform']['aux_labels'][aux['name']] = label

event['type'] = 'updated'
event['text'] = 'Successfully updated auxiliary relay settings.'

control['settings_update'] = True

write_settings(settings)
write_control(control, origin='app')

if request.method == 'POST' and action == 'pellets':
response = request.form

Expand Down
37 changes: 37 additions & 0 deletions blueprints/settings/templates/settings/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ <h6 class="nav-category-header text-muted font-weight-bold mt-2 mb-1 px-3">Front
<!-- Operations -->
<h6 class="nav-category-header text-muted font-weight-bold mt-3 mb-1 px-3">Operations</h6>
<a class="nav-link" id="v-pills-safety-tab" data-toggle="pill" href="#v-pills-safety" role="tab" aria-controls="v-pills-safety" aria-selected="false">Safety</a>
<a class="nav-link" id="v-pills-aux-tab" data-toggle="pill" href="#v-pills-aux" role="tab" aria-controls="v-pills-aux" aria-selected="false">Auxiliary Relays</a>
<a class="nav-link" id="v-pills-startup-tab" data-toggle="pill" href="#v-pills-startup" role="tab" aria-controls="v-pills-startup" aria-selected="false">Startup & Shutdown</a>
<a class="nav-link" id="v-pills-work-mode-tab" data-toggle="pill" href="#v-pills-work-mode" role="tab" aria-controls="v-pills-work-mode" aria-selected="false">Controller Settings</a>
{% if settings['platform']['dc_fan'] %}
Expand Down Expand Up @@ -1313,6 +1314,42 @@ <h5>
</div><!-- End of Card -->
<br><br><br>
</div><!-- End of Tab -->
<!-- ============================ Auxiliary Relay Settings ========================== -->
<div class="tab-pane fade" id="v-pills-aux" role="tabpanel" aria-labelledby="v-pills-aux-tab">
<form name="auxsettings" action="/settings/aux#v-pills-aux" method="POST">
<div class="card shadow">
<div class="card-header">
<h5><i class="fas fa-plug"></i>&nbsp; Auxiliary Relays</h5>
</div>
<div class="card-body">
{% set aux_list = [] %}
{% for aux_name in ['aux1', 'aux2', 'aux3', 'aux4'] %}
{% if settings['platform']['outputs'].get(aux_name) is not none %}
{% set _ = aux_list.append(aux_name) %}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" data-toggle="tooltip" title="Name shown on the dashboard and display for this relay.">
<i class="fas fa-plug"></i>&nbsp; {{ aux_name|upper }} (GPIO{{ settings['platform']['outputs'][aux_name] }})
</span>
</div>
<input id="aux_label_{{ aux_name }}" type="text" maxlength="25" class="form-control" value="{{ settings['platform']['aux_labels'].get(aux_name, aux_name) }}" name="aux_label_{{ aux_name }}">
</div>
{% endif %}
{% endfor %}
{% if aux_list|length == 0 %}
<span class="badge badge-warning">NOTE:</span>
<i class="small"> No auxiliary relays are configured. Assign a GPIO pin to one or more auxiliary relays in the configuration wizard to enable them.</i>
{% else %}
<span class="badge badge-warning">NOTE:</span>
<i class="small"> Auxiliary relays are never controlled by PiFire during a cook. They switch off whenever the main power relay switches off. Names are limited to 25 characters so they fit on the touch display.</i>
{% endif %}
</div>
<div class="card-footer bg-light">
<button type="submit" class="btn btn-outline-primary" {% if aux_list|length == 0 %}disabled{% endif %}>Save</button>
</div>
</div>
</form>
</div>
<!-- ============================ Pellet Settings ========================== -->
<div class="tab-pane fade" id="v-pills-pellets" role="tabpanel" aria-labelledby="v-pills-pellets-tab">
<div class="card shadow">
Expand Down
24 changes: 15 additions & 9 deletions blueprints/wizard/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,21 @@ def wizard_page(action=None):
if action=='finish':
if control['mode'] == 'Stop':
wizardInstallInfo = prepare_wizard_data(r)
store_wizard_install_info(wizardInstallInfo)
set_wizard_install_status(0, 'Starting Install...', '')
os.system(f'{python_exec} wizard.py &') # Kickoff Installation
return render_template(
'wizard/wizard-finish.html',
page_theme=settings['globals'].get('page_theme', 'light'),
grill_name=settings['globals'].get('grill_name', ''),
wizardData=wizardData
)
profile_selected = wizardInstallInfo['modules']['grillplatform']['profile_selected'][0]
settings_dependencies = wizardData['modules']['grillplatform'][profile_selected]['settings_dependencies']
errors += find_platform_pin_collisions(settings_dependencies, wizardInstallInfo['modules']['grillplatform']['settings'])
if not errors:
store_wizard_install_info(wizardInstallInfo)
set_wizard_install_status(0, 'Starting Install...', '')
os.system(f'{python_exec} wizard.py &') # Kickoff Installation
return render_template(
'wizard/wizard-finish.html',
page_theme=settings['globals'].get('page_theme', 'light'),
grill_name=settings['globals'].get('grill_name', ''),
wizardData=wizardData
)
# else: fall through to the shared render below so the user sees the errors.
# Nothing is stored and no install is launched.

if action=='modulecard':
module = r['module']
Expand Down
Loading