楼灯光矩阵显示系统:从理论到实践的完整技术方案

前言

当夜幕降临,城市中的高楼大厦变身为巨大的显示屏,通过窗户的灯光展示动态图案、文字甚至动画,这种震撼的视觉效果不仅是城市夜景的亮点,更是物联网技术、分布式控制系统的完美展现。本文将深入探讨如何实现这样一个大楼灯光矩阵显示系统,从技术可行性分析到完整的代码实现。

一、技术可行性分析

1.1 核心概念

将大楼的每个窗户视为一个"像素点",通过控制每个房间的灯光开关和颜色,整栋大楼就变成了一个巨型的LED显示屏。这个概念的实现需要解决以下核心问题:

  • 硬件层:智能灯光设备的选型与部署
  • 网络层:可靠的通信协议与网络架构
  • 控制层:分布式控制系统的设计
  • 应用层:图形渲染与显示算法

1.2 技术优势

  1. 视觉冲击力强:建筑物本身的大尺寸提供了无与伦比的展示效果
  2. 成本相对可控:利用现有建筑和智能照明系统
  3. 可扩展性好:模块化设计易于维护和升级
  4. 环保节能:使用LED智能灯具,可编程控制

1.3 技术挑战

  1. 同步性要求:数百个灯光节点需要精确同步
  2. 网络稳定性:需要可靠的网络通信
  3. 分辨率限制:受建筑窗户布局限制
  4. 功耗管理:大规模灯光系统的电力管理

二、系统架构设计

2.1 整体架构

复制代码
┌─────────────────────────────────────────┐
│          Web控制台/移动端应用              │
└───────────────┬─────────────────────────┘
                │ HTTP/WebSocket
┌───────────────┴─────────────────────────┐
│          中央控制服务器                    │
│  - 图形渲染引擎                           │
│  - 设备管理模块                           │
│  - 指令分发系统                           │
└───────────────┬─────────────────────────┘
                │ MQTT/CoAP
┌───────────────┴─────────────────────────┐
│          楼层控制器集群                    │
│  (每层一个或多个控制器)                    │
└───────────────┬─────────────────────────┘
                │ Zigbee/BLE/WiFi
┌───────────────┴─────────────────────────┐
│          智能灯光节点                      │
│  (每个房间/窗户一个智能灯泡)               │
└─────────────────────────────────────────┘

2.2 技术选型

  • 智能灯具:支持RGB调色的WiFi/Zigbee智能灯泡
  • 通信协议:MQTT(消息队列)+ WebSocket(实时通信)
  • 后端语言:Python(Flask/FastAPI)+ Node.js可选
  • 前端框架:React/Vue.js + Canvas API
  • 数据库:Redis(缓存)+ MongoDB(配置存储)

三、核心功能实现

3.1 图形渲染引擎

首先实现一个将图像转换为灯光矩阵数据的渲染引擎:

python 复制代码
# graphics_renderer.py
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from typing import List, Tuple

class BuildingLightRenderer:
    """大楼灯光图形渲染引擎"""
    
    def __init__(self, width: int, height: int):
        """
        初始化渲染器
        :param width: 大楼宽度方向的窗户数量
        :param height: 大楼高度方向的窗户数量
        """
        self.width = width
        self.height = height
        self.canvas = Image.new('RGB', (width, height), color='black')
        
    def load_image(self, image_path: str) -> np.ndarray:
        """
        加载并缩放图像到建筑物分辨率
        :param image_path: 图像文件路径
        :return: RGB数组
        """
        img = Image.open(image_path)
        # 缩放到建筑物的分辨率
        img = img.resize((self.width, self.height), Image.Resampling.LANCZOS)
        return np.array(img)
    
    def render_text(self, text: str, font_size: int = 10, 
                   color: Tuple[int, int, int] = (255, 255, 255)) -> np.ndarray:
        """
        渲染文字
        :param text: 要显示的文字
        :param font_size: 字体大小
        :param color: 文字颜色(R,G,B)
        :return: RGB数组
        """
        img = Image.new('RGB', (self.width, self.height), color='black')
        draw = ImageDraw.Draw(img)
        
        try:
            # 尝试使用系统字体
            font = ImageFont.truetype("arial.ttf", font_size)
        except:
            font = ImageFont.load_default()
        
        # 计算文字位置使其居中
        bbox = draw.textbbox((0, 0), text, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]
        
        x = (self.width - text_width) // 2
        y = (self.height - text_height) // 2
        
        draw.text((x, y), text, fill=color, font=font)
        return np.array(img)
    
    def render_pattern(self, pattern_type: str) -> np.ndarray:
        """
        渲染预定义图案
        :param pattern_type: 图案类型 (wave, gradient, checkerboard等)
        :return: RGB数组
        """
        img = np.zeros((self.height, self.width, 3), dtype=np.uint8)
        
        if pattern_type == 'wave':
            # 波浪图案
            for y in range(self.height):
                for x in range(self.width):
                    wave = int(127 * (1 + np.sin(x / self.width * 4 * np.pi)))
                    img[y, x] = [wave, wave, 255]
                    
        elif pattern_type == 'gradient':
            # 渐变图案
            for y in range(self.height):
                for x in range(self.width):
                    r = int(255 * x / self.width)
                    b = int(255 * y / self.height)
                    img[y, x] = [r, 0, b]
                    
        elif pattern_type == 'checkerboard':
            # 棋盘图案
            block_size = max(1, min(self.width, self.height) // 8)
            for y in range(self.height):
                for x in range(self.width):
                    if ((x // block_size) + (y // block_size)) % 2 == 0:
                        img[y, x] = [255, 255, 255]
                        
        return img
    
    def create_animation_frames(self, text: str, frame_count: int = 30) -> List[np.ndarray]:
        """
        创建滚动文字动画帧
        :param text: 要显示的文字
        :param frame_count: 动画帧数
        :return: 帧数组列表
        """
        frames = []
        font_size = min(self.height // 2, 15)
        
        for i in range(frame_count):
            img = Image.new('RGB', (self.width * 2, self.height), color='black')
            draw = ImageDraw.Draw(img)
            
            try:
                font = ImageFont.truetype("arial.ttf", font_size)
            except:
                font = ImageFont.load_default()
            
            # 文字从右向左滚动
            x_offset = self.width - int((self.width * 2) * i / frame_count)
            y_offset = self.height // 3
            
            draw.text((x_offset, y_offset), text, fill=(0, 255, 255), font=font)
            
            # 裁剪到建筑物尺寸
            cropped = img.crop((0, 0, self.width, self.height))
            frames.append(np.array(cropped))
            
        return frames
    
    def matrix_to_light_commands(self, matrix: np.ndarray) -> List[dict]:
        """
        将图像矩阵转换为灯光控制指令
        :param matrix: RGB图像矩阵
        :return: 灯光控制指令列表
        """
        commands = []
        for y in range(self.height):
            for x in range(self.width):
                r, g, b = matrix[y, x]
                # 判断是否点亮(亮度阈值)
                brightness = (int(r) + int(g) + int(b)) / 3
                state = 'on' if brightness > 30 else 'off'
                
                command = {
                    'node_id': f'light_{x}_{y}',
                    'x': x,
                    'y': y,
                    'state': state,
                    'color': {
                        'r': int(r),
                        'g': int(g),
                        'b': int(b)
                    },
                    'brightness': min(100, int(brightness / 2.55))
                }
                commands.append(command)
                
        return commands

3.2 MQTT控制服务

实现基于MQTT的灯光控制服务:

python 复制代码
# mqtt_controller.py
import paho.mqtt.client as mqtt
import json
import time
from typing import List, Callable
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class BuildingLightController:
    """大楼灯光MQTT控制器"""
    
    def __init__(self, broker_host: str = 'localhost', 
                 broker_port: int = 1883,
                 username: str = None,
                 password: str = None):
        """
        初始化MQTT控制器
        :param broker_host: MQTT代理服务器地址
        :param broker_port: MQTT代理服务器端口
        :param username: 用户名
        :param password: 密码
        """
        self.broker_host = broker_host
        self.broker_port = broker_port
        self.client = mqtt.Client(client_id="building_light_controller")
        
        if username and password:
            self.client.username_pw_set(username, password)
        
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect
        
        self.connected = False
        self.message_callbacks = []
        
    def _on_connect(self, client, userdata, flags, rc):
        """连接回调"""
        if rc == 0:
            self.connected = True
            logger.info(f"成功连接到MQTT代理: {self.broker_host}:{self.broker_port}")
            # 订阅状态反馈主题
            self.client.subscribe("building/lights/+/status")
        else:
            logger.error(f"连接失败,错误码: {rc}")
    
    def _on_message(self, client, userdata, msg):
        """消息接收回调"""
        try:
            payload = json.loads(msg.payload.decode())
            logger.debug(f"收到消息 - 主题: {msg.topic}, 内容: {payload}")
            
            # 调用注册的回调函数
            for callback in self.message_callbacks:
                callback(msg.topic, payload)
        except Exception as e:
            logger.error(f"处理消息时出错: {e}")
    
    def _on_disconnect(self, client, userdata, rc):
        """断开连接回调"""
        self.connected = False
        logger.warning(f"与MQTT代理断开连接,错误码: {rc}")
    
    def connect(self):
        """连接到MQTT代理"""
        try:
            self.client.connect(self.broker_host, self.broker_port, 60)
            self.client.loop_start()
            
            # 等待连接建立
            timeout = 10
            start_time = time.time()
            while not self.connected and (time.time() - start_time) < timeout:
                time.sleep(0.1)
                
            if not self.connected:
                raise ConnectionError("连接超时")
                
        except Exception as e:
            logger.error(f"连接MQTT代理失败: {e}")
            raise
    
    def disconnect(self):
        """断开MQTT连接"""
        self.client.loop_stop()
        self.client.disconnect()
        logger.info("已断开MQTT连接")
    
    def send_light_command(self, node_id: str, command: dict):
        """
        发送单个灯光控制指令
        :param node_id: 节点ID
        :param command: 控制指令
        """
        topic = f"building/lights/{node_id}/command"
        payload = json.dumps(command)
        
        result = self.client.publish(topic, payload, qos=1)
        if result.rc != mqtt.MQTT_ERR_SUCCESS:
            logger.error(f"发送指令失败: {node_id}")
    
    def send_batch_commands(self, commands: List[dict], batch_size: int = 50):
        """
        批量发送灯光控制指令
        :param commands: 控制指令列表
        :param batch_size: 每批发送的数量
        """
        logger.info(f"开始发送 {len(commands)} 条灯光指令")
        
        for i in range(0, len(commands), batch_size):
            batch = commands[i:i + batch_size]
            
            for cmd in batch:
                node_id = cmd['node_id']
                self.send_light_command(node_id, cmd)
            
            # 小延迟避免网络拥塞
            time.sleep(0.05)
            
            if (i + batch_size) % 200 == 0:
                logger.info(f"已发送 {i + batch_size}/{len(commands)} 条指令")
        
        logger.info("所有指令发送完成")
    
    def send_sync_command(self, commands: List[dict], sync_time: float = None):
        """
        发送同步控制指令(所有灯光在指定时间同步变化)
        :param commands: 控制指令列表
        :param sync_time: 同步执行的时间戳(秒)
        """
        if sync_time is None:
            sync_time = time.time() + 2  # 默认2秒后执行
        
        sync_payload = {
            'sync_time': sync_time,
            'commands': commands
        }
        
        topic = "building/lights/sync"
        payload = json.dumps(sync_payload)
        self.client.publish(topic, payload, qos=2)
        logger.info(f"已发送同步指令,将在 {sync_time} 执行")
    
    def set_all_lights(self, state: str, color: dict = None):
        """
        设置所有灯光
        :param state: 'on' 或 'off'
        :param color: RGB颜色字典
        """
        command = {
            'state': state,
            'color': color or {'r': 255, 'g': 255, 'b': 255},
            'brightness': 100 if state == 'on' else 0
        }
        
        topic = "building/lights/all/command"
        payload = json.dumps(command)
        self.client.publish(topic, payload, qos=1)
        logger.info(f"已设置所有灯光为: {state}")
    
    def register_callback(self, callback: Callable):
        """
        注册消息回调函数
        :param callback: 回调函数,接收 (topic, payload) 参数
        """
        self.message_callbacks.append(callback)

3.3 Web API服务

创建RESTful API和WebSocket服务:

python 复制代码
# api_server.py
from flask import Flask, request, jsonify
from flask_socketio import SocketIO, emit
from flask_cors import CORS
import threading
import time
from graphics_renderer import BuildingLightRenderer
from mqtt_controller import BuildingLightController

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*")

# 全局对象
renderer = BuildingLightRenderer(width=20, height=30)  # 假设20x30的灯光矩阵
mqtt_controller = BuildingLightController(
    broker_host='localhost',
    broker_port=1883
)

# 动画播放状态
animation_state = {
    'playing': False,
    'current_frame': 0,
    'frames': []
}

@app.route('/api/health', methods=['GET'])
def health_check():
    """健康检查"""
    return jsonify({
        'status': 'ok',
        'mqtt_connected': mqtt_controller.connected,
        'renderer_size': f'{renderer.width}x{renderer.height}'
    })

@app.route('/api/display/text', methods=['POST'])
def display_text():
    """显示文字"""
    try:
        data = request.json
        text = data.get('text', 'Hello')
        color = data.get('color', [255, 255, 255])
        font_size = data.get('font_size', 10)
        
        # 渲染文字
        matrix = renderer.render_text(text, font_size, tuple(color))
        
        # 转换为灯光指令
        commands = renderer.matrix_to_light_commands(matrix)
        
        # 发送到MQTT
        mqtt_controller.send_batch_commands(commands)
        
        return jsonify({
            'success': True,
            'message': f'已显示文字: {text}',
            'commands_sent': len(commands)
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/display/pattern', methods=['POST'])
def display_pattern():
    """显示图案"""
    try:
        data = request.json
        pattern_type = data.get('pattern', 'wave')
        
        # 渲染图案
        matrix = renderer.render_pattern(pattern_type)
        
        # 转换为灯光指令
        commands = renderer.matrix_to_light_commands(matrix)
        
        # 发送到MQTT
        mqtt_controller.send_batch_commands(commands)
        
        return jsonify({
            'success': True,
            'message': f'已显示图案: {pattern_type}',
            'commands_sent': len(commands)
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/display/image', methods=['POST'])
def display_image():
    """显示图片"""
    try:
        if 'file' not in request.files:
            return jsonify({'success': False, 'error': '未找到图片文件'}), 400
        
        file = request.files['file']
        
        # 保存临时文件
        temp_path = '/tmp/uploaded_image.png'
        file.save(temp_path)
        
        # 渲染图片
        matrix = renderer.load_image(temp_path)
        
        # 转换为灯光指令
        commands = renderer.matrix_to_light_commands(matrix)
        
        # 发送到MQTT
        mqtt_controller.send_batch_commands(commands)
        
        return jsonify({
            'success': True,
            'message': '已显示图片',
            'commands_sent': len(commands)
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/animation/start', methods=['POST'])
def start_animation():
    """启动动画"""
    try:
        data = request.json
        text = data.get('text', 'HELLO WORLD')
        frame_count = data.get('frame_count', 30)
        
        # 生成动画帧
        frames = renderer.create_animation_frames(text, frame_count)
        
        animation_state['frames'] = frames
        animation_state['playing'] = True
        animation_state['current_frame'] = 0
        
        # 启动动画播放线程
        thread = threading.Thread(target=play_animation)
        thread.daemon = True
        thread.start()
        
        return jsonify({
            'success': True,
            'message': '动画已启动',
            'frame_count': len(frames)
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/animation/stop', methods=['POST'])
def stop_animation():
    """停止动画"""
    animation_state['playing'] = False
    return jsonify({'success': True, 'message': '动画已停止'})

@app.route('/api/lights/all', methods=['POST'])
def control_all_lights():
    """控制所有灯光"""
    try:
        data = request.json
        state = data.get('state', 'off')
        color = data.get('color', {'r': 255, 'g': 255, 'b': 255})
        
        mqtt_controller.set_all_lights(state, color)
        
        return jsonify({
            'success': True,
            'message': f'所有灯光已设置为: {state}'
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

def play_animation():
    """动画播放线程"""
    fps = 10  # 每秒10帧
    frame_delay = 1.0 / fps
    
    while animation_state['playing'] and animation_state['frames']:
        frame_idx = animation_state['current_frame']
        frames = animation_state['frames']
        
        if frame_idx >= len(frames):
            frame_idx = 0
            animation_state['current_frame'] = 0
        
        # 渲染当前帧
        matrix = frames[frame_idx]
        commands = renderer.matrix_to_light_commands(matrix)
        mqtt_controller.send_batch_commands(commands, batch_size=100)
        
        # 通过WebSocket发送进度
        socketio.emit('animation_progress', {
            'current_frame': frame_idx,
            'total_frames': len(frames)
        })
        
        animation_state['current_frame'] += 1
        time.sleep(frame_delay)

@socketio.on('connect')
def handle_connect():
    """WebSocket连接"""
    print('客户端已连接')
    emit('connection_status', {'status': 'connected'})

@socketio.on('disconnect')
def handle_disconnect():
    """WebSocket断开"""
    print('客户端已断开')

def initialize_system():
    """初始化系统"""
    try:
        mqtt_controller.connect()
        print("MQTT控制器已连接")
    except Exception as e:
        print(f"初始化失败: {e}")

if __name__ == '__main__':
    initialize_system()
    socketio.run(app, host='0.0.0.0', port=5000, debug=True)

3.4 前端控制界面

创建简单的Web控制界面:

html 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大楼灯光控制系统</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            overflow: hidden;
        }
        
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }
        
        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
        }
        
        .content {
            padding: 30px;
        }
        
        .control-panel {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        
        .control-card {
            background: #f8f9fa;
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        
        .control-card h3 {
            color: #667eea;
            margin-bottom: 20px;
            font-size: 1.3em;
        }
        
        .input-group {
            margin-bottom: 15px;
        }
        
        .input-group label {
            display: block;
            margin-bottom: 8px;
            color: #666;
            font-weight: 500;
        }
        
        .input-group input,
        .input-group select {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        
        .input-group input:focus,
        .input-group select:focus {
            outline: none;
            border-color: #667eea;
        }
        
        button {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 12px 30px;
            border-radius: 8px;
            font-size: 16px;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
            width: 100%;
            font-weight: 600;
        }
        
        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
        }
        
        button:active {
            transform: translateY(0);
        }
        
        .canvas-container {
            background: #000;
            border-radius: 15px;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 400px;
        }
        
        #preview-canvas {
            border: 2px solid #667eea;
            border-radius: 8px;
            image-rendering: pixelated;
        }
        
        .status-bar {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 10px;
            margin-top: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .status-indicator {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .status-dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #4CAF50;
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }
        
        .color-picker-group {
            display: flex;
            gap: 10px;
            align-items: center;
        }
        
        .color-picker-group input[type="color"] {
            width: 50px;
            height: 40px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🏢 大楼灯光控制系统</h1>
            <p>Building Light Matrix Display Controller</p>
        </div>
        
        <div class="content">
            <div class="control-panel">
                <!-- 文字显示控制 -->
                <div class="control-card">
                    <h3>📝 文字显示</h3>
                    <div class="input-group">
                        <label>输入文字</label>
                        <input type="text" id="text-input" placeholder="输入要显示的文字" value="HELLO">
                    </div>
                    <div class="input-group color-picker-group">
                        <label>文字颜色</label>
                        <input type="color" id="text-color" value="#00ffff">
                    </div>
                    <button onclick="displayText()">显示文字</button>
                </div>
                
                <!-- 图案显示控制 -->
                <div class="control-card">
                    <h3>🎨 图案显示</h3>
                    <div class="input-group">
                        <label>选择图案</label>
                        <select id="pattern-select">
                            <option value="wave">波浪</option>
                            <option value="gradient">渐变</option>
                            <option value="checkerboard">棋盘</option>
                        </select>
                    </div>
                    <button onclick="displayPattern()">显示图案</button>
                </div>
                
                <!-- 动画控制 -->
                <div class="control-card">
                    <h3>🎬 动画控制</h3>
                    <div class="input-group">
                        <label>动画文字</label>
                        <input type="text" id="animation-text" placeholder="滚动文字" value="WELCOME">
                    </div>
                    <div class="input-group">
                        <label>帧数</label>
                        <input type="number" id="frame-count" value="30" min="10" max="100">
                    </div>
                    <button onclick="startAnimation()">开始动画</button>
                    <button onclick="stopAnimation()" style="margin-top: 10px; background: #f44336;">停止动画</button>
                </div>
                
                <!-- 全局控制 -->
                <div class="control-card">
                    <h3>🎛️ 全局控制</h3>
                    <button onclick="allLightsOn()">全部打开</button>
                    <button onclick="allLightsOff()" style="margin-top: 10px; background: #f44336;">全部关闭</button>
                </div>
            </div>
            
            <!-- 预览画布 -->
            <div class="canvas-container">
                <canvas id="preview-canvas" width="200" height="300"></canvas>
            </div>
            
            <!-- 状态栏 -->
            <div class="status-bar">
                <div class="status-indicator">
                    <div class="status-dot"></div>
                    <span>系统运行中</span>
                </div>
                <div id="status-message">准备就绪</div>
            </div>
        </div>
    </div>
    
    <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
    <script>
        const API_BASE = 'http://localhost:5000/api';
        const socket = io('http://localhost:5000');
        
        // 初始化画布
        const canvas = document.getElementById('preview-canvas');
        const ctx = canvas.getContext('2d');
        
        // WebSocket事件
        socket.on('connect', () => {
            console.log('WebSocket已连接');
            updateStatus('已连接到服务器');
        });
        
        socket.on('animation_progress', (data) => {
            updateStatus(`动画播放中: ${data.current_frame}/${data.total_frames}`);
        });
        
        // 更新状态消息
        function updateStatus(message) {
            document.getElementById('status-message').textContent = message;
        }
        
        // 显示文字
        async function displayText() {
            const text = document.getElementById('text-input').value;
            const colorHex = document.getElementById('text-color').value;
            const color = hexToRgb(colorHex);
            
            updateStatus('正在显示文字...');
            
            try {
                const response = await fetch(`${API_BASE}/display/text`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ text, color })
                });
                
                const result = await response.json();
                if (result.success) {
                    updateStatus(`文字"${text}"已显示`);
                    drawTextPreview(text, color);
                }
            } catch (error) {
                updateStatus('错误: ' + error.message);
            }
        }
        
        // 显示图案
        async function displayPattern() {
            const pattern = document.getElementById('pattern-select').value;
            updateStatus('正在显示图案...');
            
            try {
                const response = await fetch(`${API_BASE}/display/pattern`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ pattern })
                });
                
                const result = await response.json();
                if (result.success) {
                    updateStatus(`图案"${pattern}"已显示`);
                    drawPatternPreview(pattern);
                }
            } catch (error) {
                updateStatus('错误: ' + error.message);
            }
        }
        
        // 开始动画
        async function startAnimation() {
            const text = document.getElementById('animation-text').value;
            const frameCount = parseInt(document.getElementById('frame-count').value);
            
            updateStatus('正在启动动画...');
            
            try {
                const response = await fetch(`${API_BASE}/animation/start`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ text, frame_count: frameCount })
                });
                
                const result = await response.json();
                if (result.success) {
                    updateStatus('动画已启动');
                }
            } catch (error) {
                updateStatus('错误: ' + error.message);
            }
        }
        
        // 停止动画
        async function stopAnimation() {
            try {
                const response = await fetch(`${API_BASE}/animation/stop`, {
                    method: 'POST'
                });
                
                const result = await response.json();
                if (result.success) {
                    updateStatus('动画已停止');
                }
            } catch (error) {
                updateStatus('错误: ' + error.message);
            }
        }
        
        // 全部打开
        async function allLightsOn() {
            updateStatus('打开所有灯光...');
            
            try {
                const response = await fetch(`${API_BASE}/lights/all`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ state: 'on' })
                });
                
                const result = await response.json();
                if (result.success) {
                    updateStatus('所有灯光已打开');
                    ctx.fillStyle = '#ffffff';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                }
            } catch (error) {
                updateStatus('错误: ' + error.message);
            }
        }
        
        // 全部关闭
        async function allLightsOff() {
            updateStatus('关闭所有灯光...');
            
            try {
                const response = await fetch(`${API_BASE}/lights/all`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ state: 'off' })
                });
                
                const result = await response.json();
                if (result.success) {
                    updateStatus('所有灯光已关闭');
                    ctx.fillStyle = '#000000';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                }
            } catch (error) {
                updateStatus('错误: ' + error.message);
            }
        }
        
        // 预览画布绘制
        function drawTextPreview(text, color) {
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
            ctx.font = '40px Arial';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(text, canvas.width / 2, canvas.height / 2);
        }
        
        function drawPatternPreview(pattern) {
            const imageData = ctx.createImageData(canvas.width, canvas.height);
            
            for (let y = 0; y < canvas.height; y++) {
                for (let x = 0; x < canvas.width; x++) {
                    const i = (y * canvas.width + x) * 4;
                    
                    if (pattern === 'wave') {
                        const wave = Math.floor(127 * (1 + Math.sin(x / canvas.width * 4 * Math.PI)));
                        imageData.data[i] = wave;
                        imageData.data[i + 1] = wave;
                        imageData.data[i + 2] = 255;
                    } else if (pattern === 'gradient') {
                        imageData.data[i] = Math.floor(255 * x / canvas.width);
                        imageData.data[i + 1] = 0;
                        imageData.data[i + 2] = Math.floor(255 * y / canvas.height);
                    } else if (pattern === 'checkerboard') {
                        const blockSize = 20;
                        const value = ((Math.floor(x / blockSize) + Math.floor(y / blockSize)) % 2) * 255;
                        imageData.data[i] = value;
                        imageData.data[i + 1] = value;
                        imageData.data[i + 2] = value;
                    }
                    
                    imageData.data[i + 3] = 255;
                }
            }
            
            ctx.putImageData(imageData, 0, 0);
        }
        
        // 辅助函数:十六进制转RGB
        function hexToRgb(hex) {
            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
            return result ? [
                parseInt(result[1], 16),
                parseInt(result[2], 16),
                parseInt(result[3], 16)
            ] : [255, 255, 255];
        }
        
        // 初始化
        ctx.fillStyle = '#000000';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
    </script>
</body>
</html>

四、部署与实施步骤

4.1 硬件准备

  1. 智能灯具选型

    • 推荐:飞利浦Hue、Yeelight、小米智能灯泡
    • 要求:支持WiFi/Zigbee,RGB调色,可编程控制
  2. 网络基础设施

    • 千兆以太网主干网络
    • WiFi 6 AP覆盖(每层至少2个)
    • Zigbee网关(可选)
  3. 控制服务器

    • CPU: 4核心以上
    • RAM: 8GB以上
    • 存储: 256GB SSD
    • 操作系统: Ubuntu 20.04 LTS

4.2 软件部署

bash 复制代码
# 1. 安装系统依赖
sudo apt update
sudo apt install -y python3-pip python3-venv mosquitto redis-server

# 2. 创建Python虚拟环境
python3 -m venv venv
source venv/bin/activate

# 3. 安装Python依赖
pip install flask flask-socketio flask-cors paho-mqtt pillow numpy

# 4. 启动MQTT代理
sudo systemctl start mosquitto
sudo systemctl enable mosquitto

# 5. 启动Redis
sudo systemctl start redis-server
sudo systemctl enable redis-server

# 6. 运行应用
python api_server.py

4.3 配置文件示例

yaml 复制代码
# config.yaml
building:
  name: "Demo Building"
  dimensions:
    width: 20  # 横向窗户数
    height: 30 # 纵向窗户数
  
mqtt:
  broker: "localhost"
  port: 1883
  username: "admin"
  password: "password"
  
network:
  api_host: "0.0.0.0"
  api_port: 5000
  websocket_enabled: true
  
performance:
  batch_size: 50
  frame_rate: 10
  sync_tolerance_ms: 100

五、性能优化建议

5.1 网络优化

  1. MQTT QoS策略

    • 状态更新使用QoS 0(快速但不保证)
    • 关键指令使用QoS 1(保证送达)
    • 同步指令使用QoS 2(仅一次送达)
  2. 批量处理

    • 将多个指令合并为批次发送
    • 使用消息压缩减少带宽占用

5.2 同步机制

python 复制代码
# 时间同步示例
import ntplib
from datetime import datetime

def sync_system_time():
    """与NTP服务器同步时间"""
    client = ntplib.NTPClient()
    try:
        response = client.request('pool.ntp.org', version=3)
        return response.tx_time
    except:
        return time.time()

5.3 故障恢复

  • 实现断线重连机制
  • 设备状态缓存与恢复
  • 日志记录与监控告警

六、实际案例与效果

6.1 应用场景

  1. 节日庆典:显示节日主题图案和祝福语
  2. 品牌宣传:展示企业Logo和宣传标语
  3. 艺术装置:创意灯光艺术表演
  4. 互动游戏:结合手机App的互动游戏

6.2 成本估算

以30层、20个窗户宽的建筑为例(600个控制点):

项目 数量 单价 小计
智能灯泡 600 ¥80 ¥48,000
网关/控制器 30 ¥300 ¥9,000
服务器 1 ¥8,000 ¥8,000
网络设备 1套 ¥15,000 ¥15,000
软件开发 - - ¥50,000
总计 ¥130,000

七、总结与展望

大楼灯光矩阵显示系统是物联网技术在建筑领域的创新应用,通过本文的技术方案,我们实现了:

完整的系统架构 :从硬件到软件的全栈方案

可扩展的设计 :支持不同规模建筑的灵活部署

丰富的功能 :文字、图案、动画的多样化展示

友好的接口:Web控制台和API接口方便集成

未来发展方向

  1. AI驱动:使用机器学习优化显示效果
  2. 实时互动:结合摄像头实现观众互动
  3. 声光同步:配合音乐实现声光表演
  4. 能耗优化:智能调度降低电力消耗

本文所有代码均为示例性质,实际部署时需根据具体硬件和需求进行调整。

相关推荐
然后,是第八天5 小时前
【机械臂运动学基础】变换矩阵
线性代数·矩阵
野蛮人6号8 小时前
力扣热题100道之73矩阵置零
算法·leetcode·矩阵
虚行9 小时前
WPF入门
开发语言·c#·wpf
周杰伦fans10 小时前
MahApps.Metro WPF 开发使用过程中遇到的问题 - 未能加载文件或程序集“Microsoft.Xaml.Behaviors,
microsoft·wpf
通信小呆呆21 小时前
以矩阵视角统一理解:外积、Kronecker 积与 Khatri–Rao 积(含MATLAB可视化)
线性代数·算法·matlab·矩阵·信号处理
△曉風殘月〆21 小时前
WPF中的坐标转换
wpf
一个天蝎座 白勺 程序猿1 天前
深度解析:通过ADO.NET驱动Kdbndp高效连接与操作Kingbase数据库
数据库·.net·wpf·kingbase·金仓数据库
时光追逐者1 天前
一个使用 WPF 开发的 Diagram 画板工具(包含流程图FlowChart,思维导图MindEditor)
c#·.net·wpf·流程图
Vae_Mars1 天前
WPF中的DataTemplate
wpf