Mastodon Politics, Power, and Science: PicoW IoT Framework Extension: Semantic Display Organization

Friday, November 28, 2025

PicoW IoT Framework Extension: Semantic Display Organization

J. Rogers, SE Ohio

This is a planned upgrade to this project:

https://github.com/BuckRogers1965/Pico-IoT-Replacement

Overview

This extension introduces semantic organization capabilities to the registry system, enabling more intuitive and professional web UI layouts through conceptual vectors, modal groups, and visual hierarchy.

Motivation

The current framework provides basic sensor and control registration but lacks semantic organization. This extension adds:

  1. Spatial Organization: Precise positioning through grid-based layout

  2. Logical Grouping: Related controls are visually and functionally grouped

  3. Modal Behavior: Controls can interact with each other (radio buttons, dependencies)

  4. Visual Hierarchy: Important controls are emphasized

  5. Responsive 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

The frontend JavaScript would be updated to handle the new semantic organization:

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

  1. Intuitive Layout: Controls are positioned logically rather than randomly

  2. Visual Hierarchy: Important controls stand out through priority styling

  3. Functional Grouping: Related controls are visually and functionally connected

  4. Automatic Behavior: Modal groups handle complex interactions automatically

  5. Responsive Design: Grid layout adapts to different screen sizes

  6. Reduced Code: Common patterns are handled by the framework

  7. Error Prevention: Modal groups prevent invalid states

  8. Contextual Controls: Dependencies show/hide relevant controls based on context

Migration Path

Existing projects can be gradually migrated:

  1. Phase 1: Add grid positioning to existing controls

  2. Phase 2: Group related controls and add icons

  3. Phase 3: Implement modal behavior for interactive controls

  4. Phase 4: Add dependencies for contextual controls

The framework maintains backward compatibility - all new fields have sensible defaults, so existing code continues to work without modification.

Conclusion

This extension transforms the framework from a simple data registry to a semantic organization system that produces professional, intuitive interfaces with minimal code. The conceptual vectors provide both spatial organization and behavioral relationships between controls, dramatically improving the user experience while simplifying development.

No comments:

Post a Comment

Progress on the campaign manager

You can see that you can build tactical maps automatically from the world map data.  You can place roads, streams, buildings. The framework ...