Overview
Motivation
Spatial Organization : Precise positioning through grid-based layoutLogical Grouping : Related controls are visually and functionally groupedModal Behavior : Controls can interact with each other (radio buttons, dependencies)Visual Hierarchy : Important controls are emphasizedResponsive Design : Layouts adapt to different screen sizes
Implementation Details
1. Extended Registry Structure
enum ModalType {
MODAL_NONE, // Independent controls (default)
MODAL_RADIO, // Only one can be active
MODAL_CHECKBOX, // Multiple can be active
MODAL_EXCLUSIVE, // At most one, but can be all off
MODAL_AT_LEAST_ONE // At least one must be on
};
struct RegistryItem {
// Existing fields...
char id[20], name[32];
ItemType type;
volatile float value;
float min_val, max_val, step;
char unit[8];
uint32_t update_interval_ms;
void (*read_callback)(int idx);
unsigned long last_update_time;
// NEW: Layout vectors
int grid_col, grid_row; // Position in CSS Grid
int grid_col_span, grid_row_span; // Size in grid
char group[16]; // Logical grouping
int priority; // Visual importance (1-10)
char icon[32]; // Icon name/emoji
// NEW: Modal behavior
ModalType modal_type;
char modal_group[16]; // Group name for modal behavior
char modal_dependency[32]; // ID that controls visibility
};
2. Enhanced Registration Macros
// Sensor with layout vectors
#define SENSOR_DISPLAY_V(ID, NAME, UNIT, COL, ROW, GROUP, PRIORITY, ICON) \
strcpy((char*)registry[i].id, ID); \
strcpy((char*)registry[i].name, NAME); \
registry[i].type = TYPE_SENSOR_GENERIC; \
registry[i].value = 0; \
registry[i].min_val = 0; \
registry[i].max_val = 0; \
registry[i].step = 0; \
strcpy((char*)registry[i].unit, UNIT); \
registry[i].grid_col = COL; \
registry[i].grid_row = ROW; \
registry[i].grid_col_span = 1; \
registry[i].grid_row_span = 1; \
strcpy((char*)registry[i].group, GROUP); \
registry[i].priority = PRIORITY; \
strcpy((char*)registry[i].icon, ICON); \
registry[i].update_interval_ms = 0; \
registry[i].read_callback = NULL; \
i++;
// Slider with layout vectors
#define CONTROL_SLIDER_V(ID, NAME, DEFAULT, MIN, MAX, STEP, UNIT, COL, ROW, GROUP, PRIORITY, ICON) \
strcpy((char*)registry[i].id, ID); \
strcpy((char*)registry[i].name, NAME); \
registry[i].type = TYPE_CONTROL_SLIDER; \
registry[i].value = DEFAULT; \
registry[i].min_val = MIN; \
registry[i].max_val = MAX; \
registry[i].step = STEP; \
strcpy((char*)registry[i].unit, UNIT); \
registry[i].grid_col = COL; \
registry[i].grid_row = ROW; \
registry[i].grid_col_span = 1; \
registry[i].grid_row_span = 1; \
strcpy((char*)registry[i].group, GROUP); \
registry[i].priority = PRIORITY; \
strcpy((char*)registry[i].icon, ICON); \
registry[i].modal_type = MODAL_NONE; \
registry[i].update_interval_ms = 0; \
registry[i].read_callback = NULL; \
i++;
// Slider with spanning capability
#define CONTROL_SLIDER_SPAN(ID, NAME, DEFAULT, MIN, MAX, STEP, UNIT, COL, ROW, COL_SPAN, GROUP, PRIORITY, ICON) \
CONTROL_SLIDER_V(ID, NAME, DEFAULT, MIN, MAX, STEP, UNIT, COL, ROW, GROUP, PRIORITY, ICON) \
registry[i-1].grid_col_span = COL_SPAN;
// Modal radio button (only one in group can be active)
#define CONTROL_MODAL_RADIO(ID, NAME, GROUP, COL, ROW, PRIORITY, ICON) \
strcpy((char*)registry[i].id, ID); \
strcpy((char*)registry[i].name, NAME); \
registry[i].type = TYPE_CONTROL_TOGGLE; \
registry[i].value = 0; \
registry[i].min_val = 0; \
registry[i].max_val = 1; \
registry[i].step = 1; \
strcpy((char*)registry[i].unit, ""); \
registry[i].grid_col = COL; \
registry[i].grid_row = ROW; \
registry[i].grid_col_span = 1; \
registry[i].grid_row_span = 1; \
strcpy((char*)registry[i].group, GROUP); \
strcpy((char*)registry[i].modal_group, GROUP); \
registry[i].priority = PRIORITY; \
strcpy((char*)registry[i].icon, ICON); \
registry[i].modal_type = MODAL_RADIO; \
registry[i].update_interval_ms = 0; \
registry[i].read_callback = NULL; \
i++;
// Modal checkbox (multiple can be active)
#define CONTROL_MODAL_CHECKBOX(ID, NAME, GROUP, COL, ROW, PRIORITY, ICON) \
CONTROL_MODAL_RADIO(ID, NAME, GROUP, COL, ROW, PRIORITY, ICON) \
registry[i-1].modal_type = MODAL_CHECKBOX;
// Dependent control (only shows when dependency is active)
#define CONTROL_DEPENDENT(ID, NAME, DEPENDS_ON, COL, ROW, PRIORITY, ICON) \
CONTROL_SLIDER_V(ID, NAME, 0, 0, 1, 1, "", COL, ROW, "dependent", PRIORITY, ICON) \
strcpy((char*)registry[i-1].modal_dependency, DEPENDS_ON);
3. Frontend Implementation
class SemanticLayoutManager {
constructor() {
this.modalGroups = new Map();
this.dependencyMap = new Map();
this.gridContainer = null;
}
initialize() {
this.gridContainer = document.getElementById('controls-grid');
this.gridContainer.style.display = 'grid';
this.gridContainer.style.gridTemplateColumns = 'repeat(12, 1fr)';
this.gridContainer.style.gap = '15px';
}
registerControl(item) {
// Register modal groups
if (item.modal_type !== 'MODAL_NONE') {
if (!this.modalGroups.has(item.modal_group)) {
this.modalGroups.set(item.modal_group, {
type: item.modal_type,
items: []
});
}
this.modalGroups.get(item.modal_group).items.push(item);
}
// Register dependencies
if (item.modal_dependency) {
if (!this.dependencyMap.has(item.modal_dependency)) {
this.dependencyMap.set(item.modal_dependency, []);
}
this.dependencyMap.get(item.modal_dependency).push(item.id);
}
}
renderControl(item) {
this.registerControl(item);
const card = document.createElement('div');
card.className = 'control-card';
card.style.gridColumn = `${item.grid_col} / span ${item.grid_col_span}`;
card.style.gridRow = `${item.grid_row} / span ${item.grid_row_span}`;
card.dataset.group = item.group;
card.dataset.priority = item.priority;
card.dataset.modalGroup = item.modal_group || '';
card.dataset.modalType = item.modal_type || 'MODAL_NONE';
// Add icon if specified
let iconHtml = item.icon ? `<span class="icon">${item.icon}</span>` : '';
// Render based on type
switch(item.type) {
case TYPE_CONTROL_SLIDER:
card.innerHTML = `
${iconHtml}<h3>${item.name}</h3>
<div class="slider-container">
<input type="range" min="${item.min_val}" max="${item.max_val}"
step="${item.step}" value="${item.value}"
id="${item.id}">
<span class="value">${item.value}</span>
</div>
`;
break;
case TYPE_CONTROL_TOGGLE:
card.innerHTML = `
<div class="toggle-group">
${iconHtml}
<label class="toggle-label">${item.name}</label>
<label class="toggle-switch">
<input type="checkbox" id="${item.id}"
${item.value > 0 ? 'checked' : ''}
onchange="layoutManager.handleToggle('${item.id}', this.checked ? 1 : 0)">
<span class="slider"></span>
</label>
</div>
`;
break;
case TYPE_SENSOR_GENERIC:
card.innerHTML = `
${iconHtml}<h3>${item.name}</h3>
<div class="sensor-value" id="${item.id}">${item.value}</div>
<div class="sensor-unit">${item.unit}</div>
`;
break;
}
this.gridContainer.appendChild(card);
return card;
}
handleToggle(itemId, newValue) {
const item = this.findItem(itemId);
if (!item) return true;
// Handle modal group behavior
if (item.modal_type !== 'MODAL_NONE') {
const group = this.modalGroups.get(item.modal_group);
switch (item.modal_type) {
case 'MODAL_RADIO':
if (newValue === 1) {
// Turn off all others in the group
group.items.forEach(otherItem => {
if (otherItem.id !== itemId) {
this.setControlValue(otherItem.id, 0);
document.getElementById(otherItem.id).checked = false;
}
});
}
break;
case 'MODAL_EXCLUSIVE':
if (newValue === 1) {
// Turn off others, but allow all-off
group.items.forEach(otherItem => {
if (otherItem.id !== itemId && otherItem.value === 1) {
this.setControlValue(otherItem.id, 0);
document.getElementById(otherItem.id).checked = false;
}
});
}
break;
case 'MODAL_AT_LEAST_ONE':
if (newValue === 0) {
// Check if this is the last one
const activeCount = group.items.filter(i => i.value === 1).length;
if (activeCount === 1) {
// Prevent turning off the last one
document.getElementById(itemId).checked = true;
return false;
}
}
break;
}
}
// Handle dependencies
this.updateDependencies();
return true;
}
updateDependencies() {
this.dependencyMap.forEach((dependents, masterId) => {
const masterValue = this.getItemValue(masterId);
const shouldShow = masterValue > 0;
dependents.forEach(depId => {
const elem = document.getElementById(depId);
if (elem) {
const card = elem.closest('.control-card');
if (shouldShow) {
card.style.display = 'block';
card.classList.add('dependent');
} else {
card.style.display = 'none';
}
}
});
});
}
setControlValue(id, value) {
fetch("/api/update", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({id: id, value: value})
});
}
getItemValue(id) {
const elem = document.getElementById(id);
return elem ? (elem.checked ? 1 : 0) : 0;
}
findItem(id) {
// Search through manifest to find item by id
return window.manifest.find(item => item.id === id);
}
}
// Initialize layout manager
const layoutManager = new SemanticLayoutManager();
// Update the handleManifest function
function handleManifest() {
String json = "[";
mutex_enter_blocking(&data_mutex);
for (int i = 0; i < registry_count; i++) {
json += "{\"id\":\"" + String((char*)registry[i].id) + "\",\"name\":\"" + String((char*)registry[i].name) + "\",\"type\":" + String(registry[i].type) + ",\"value\":" + String(registry[i].value) + ",\"min_val\":" + String(registry[i].min_val) + ",\"max_val\":" + String(registry[i].max_val) + ",\"step\":" + String(registry[i].step) + ",\"unit\":\"" + String((char*)registry[i].unit) + "\"";
// Add new fields
json += ",\"grid_col\":" + String(registry[i].grid_col) + ",\"grid_row\":" + String(registry[i].grid_row);
json += ",\"grid_col_span\":" + String(registry[i].grid_col_span) + ",\"grid_row_span\":" + String(registry[i].grid_row_span);
json += ",\"group\":\"" + String((char*)registry[i].group) + "\",\"priority\":" + String(registry[i].priority);
json += ",\"icon\":\"" + String((char*)registry[i].icon) + "\",\"modal_type\":" + String(registry[i].modal_type);
json += ",\"modal_group\":\"" + String((char*)registry[i].modal_group) + "\",\"modal_dependency\":\"" + String((char*)registry[i].modal_dependency) + "\"";
if (i < registry_count - 1) json += ",";
}
mutex_exit(&data_mutex);
json += "]";
server.send(200, "application/json", json);
}
4. CSS Enhancements
.control-card {
background: #1e1e1e;
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
border: 1px solid transparent;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* Priority-based styling */
.control-card[data-priority="10"] {
border-color: #03dac6;
box-shadow: 0 0 10px rgba(3, 218, 198, 0.3);
}
.control-card[data-priority="9"] {
border-color: #03dac6;
}
.control-card[data-priority="7"], .control-card[data-priority="8"] {
border-color: #bb86fc;
}
/* Modal group styling */
.control-card[data-modal-type="MODAL_RADIO"] {
border-left: 4px solid #03dac6;
}
.control-card[data-modal-type="MODAL_EXCLUSIVE"] {
border-left: 4px solid #bb86fc;
}
.control-card[data-modal-type="MODAL_CHECKBOX"] {
border-left: 4px solid #f2a900;
}
.control-card[data-modal-type="MODAL_AT_LEAST_ONE"] {
border-left: 4px solid #cf6679;
}
/* Dependent controls */
.control-card.dependent {
margin-left: 20px;
opacity: 0.8;
border-left-color: #666;
}
/* Group highlighting */
.control-card[data-modal-group]:hover {
transform: translateX(5px);
}
/* Icon styling */
.icon {
font-size: 1.5em;
margin-right: 10px;
vertical-align: middle;
}
/* Responsive adjustments */
@media (max-width: 768px) {
#controls-grid {
grid-template-columns: repeat(6, 1fr);
}
}
@media (max-width: 480px) {
#controls-grid {
grid-template-columns: repeat(4, 1fr);
}
}
Example Usage
void app_register_items() {
mutex_enter_blocking(&data_mutex);
int i = 0;
// Main controls - high priority, top row
CONTROL_SLIDER_V("motor_speed", "Motor Speed", 0, 0, 255, 1, "RPM",
1, 1, "primary", 10, "⚡");
CONTROL_SLIDER_V("target_pressure", "Target Pressure", 50, 0, 100, 1, "PSI",
4, 1, "primary", 9, "🎯");
// Mode selection - radio group (only one can be active)
CONTROL_MODAL_RADIO("mode_manual", "Manual", "mode_group", 1, 2, 8, "🎮");
CONTROL_MODAL_RADIO("mode_auto", "Auto", "mode_group", 2, 2, 8, "🤖");
CONTROL_MODAL_RADIO("mode_eco", "Eco", "mode_group", 3, 2, 8, "🍃");
// Status displays - medium priority
SENSOR_DISPLAY_V("current_pressure", "Current Pressure", "PSI",
7, 1, "sensors", 7, "📊");
SENSOR_DISPLAY_V("temperature", "Temperature", "°C",
10, 1, "sensors", 6, "🌡️");
// Dependent controls - only show when auto mode is active
CONTROL_DEPENDENT("auto_temp", "Target Temp", "mode_auto", 4, 3, 5, "🌡️");
CONTROL_DEPENDENT("auto_schedule", "Schedule", "mode_auto", 7, 3, 5, "📅");
// Wide spanning control
CONTROL_SLIDER_SPAN("profile_curve", "Response Curve", 50, 0, 100, 1, "%",
1, 4, 6, "advanced", 3, "📈");
// Zone selection - exclusive group (can be all off)
CONTROL_MODAL_EXCLUSIVE("zone1", "Zone 1", "zone_group", 7, 2, 4, "📍");
CONTROL_MODAL_EXCLUSIVE("zone2", "Zone 2", "zone_group", 8, 2, 4, "📍");
CONTROL_MODAL_EXCLUSIVE("zone3", "Zone 3", "zone_group", 9, 2, 4, "📍");
// Features - checkbox group (multiple can be active)
CONTROL_MODAL_CHECKBOX("feature_heating", "Heating", "feature_group", 7, 3, 3, "🔥");
CONTROL_MODAL_CHECKBOX("feature_cooling", "Cooling", "feature_group", 8, 3, 3, "❄️");
CONTROL_MODAL_CHECKBOX("feature_fan", "Fan", "feature_group", 9, 3, 3, "💨");
// At least one must be active group
CONTROL_MODAL_AT_LEAST_ONE("primary_pump", "Primary Pump", "pump_group", 7, 4, 6, "⚡");
CONTROL_MODAL_AT_LEAST_ONE("backup_pump", "Backup Pump", "pump_group", 8, 4, 6, "⚡");
registry_count = i;
mutex_exit(&data_mutex);
}
Benefits
Intuitive Layout : Controls are positioned logically rather than randomlyVisual Hierarchy : Important controls stand out through priority stylingFunctional Grouping : Related controls are visually and functionally connectedAutomatic Behavior : Modal groups handle complex interactions automaticallyResponsive Design : Grid layout adapts to different screen sizesReduced Code : Common patterns are handled by the frameworkError Prevention : Modal groups prevent invalid statesContextual Controls : Dependencies show/hide relevant controls based on context
Migration Path
Phase 1 : Add grid positioning to existing controlsPhase 2 : Group related controls and add iconsPhase 3 : Implement modal behavior for interactive controlsPhase 4 : Add dependencies for contextual controls
No comments:
Post a Comment