* 本文信息仅供参考,以设备最新产品说明为准!
网关 | 型号 | 角色 | 蓝牙 | 蓝牙 Mesh | Zigbee | Wi-Fi | 端口 | 其它 |
小米中枢网关 | ZSWG01CM | 中枢网关 | 支持 100 个 | 支持 200 个 | 不支持 | 2.4G / 5G | 网口 | 四核,4G存储 |
小米路由器 BE6500 Pro | DR08 | 中枢网关 | 支持 | 支持 | 不支持 | 2.4G / 5G | 4个 | Wi-Fi 7 路由器,四核1G |
小米多模网关 | ZNDMWG03LM | 从网关 | 支持 100 个 | 支持 100 个 | Zigbee 3.0 支持 32 个(中继 128 个) | 2.4G | 无 | 支持 HomeKit |
小米多模网关2 | DMWG03LM | 从网关 | 支持 100 个 | 支持 100 个 | Zigbee 3.0 支持 32 个(中继 128 个) | 2.4G / 5G | 网口 | 双核128M |
绿米 Aqara 网关M1S | ZHWG15LM | 从网关 | 不支持 | 不支持 | Zigbee 3.0 支持 32 个(中继 128 个) | 2.4G | 无 | 支持 HomeKit 支持夜灯、报警 |
绿米 Aqara 网关M1S 2022款(第二代) | ZHWG20LM | 从网关 | 不支持 | 不支持 | Zigbee 3.0 支持 32 个(中继 127 个) | 2.4G | 无 | 双核64M 还支持 matter、Apple Home 支持夜灯、报警 |
绿米 Aqara 网关 M2 2022款 | ZHWG19LM | - | 不支持 | 不支持 | Zigbee 3.0 | 2.4G | 网口 | 不支持米家 App 支持红外 |
绿米 Aqara 方舟智慧中枢 M3 | ZHWG24LM | 中枢网关 | 支持Aqara设备 | 支持 | 2.4G / 5G | 不支持米家 App,支持 Aqara Home app、Apple Home、matter,支持红外,8G存储 | ||
易来 Yeelight 网关(mesh 版) | YLWG01YL白色 | 盲网关 | 支持 | 支持 | 不支持 | 2.4G | 网口 | 支持 HomeKit |
易来 Yeelight Pro S20 蓝牙 Mesh 网关 | YLWG01YL灰色 | 从网关 | 支持 | 支持 | 不支持 | 2.4G | 支持 Yeelight Pro 和 HomeKit 3 未原生接入米家,但可通过“其它平台”接入米家 | |
青萍蓝牙网关 | CGSPR1 | 盲网关 | 支持 | 不支持 | 不支持 | 2.4G | 无 | |
小米智能家庭面板 | XMZHP01LM | 从网关 | 支持 100 个 | 支持 100 个 | 不支持 | 2.4G / 5G | 无 | 86型墙壁开关 |
-> 查看目前可以做从网关的产品(2023-12-07)
接入中枢体系内的蓝牙设备,会由主中枢来统一调度,决定该从哪一个网关来上报消息
主中枢信号强度范围内,主中枢会尽可能自己接管设备。信号太弱的设备,才由信号更好的备中枢或从网关接管
蓝牙 Mesh 设备的数量统一显示在中枢下,备中枢及从网关均不显示蓝牙 Mesh 设备
备中枢的所有设备都显示在主中枢下,备中枢设备数为 0。从网关会显示普通蓝牙设备,但不显示蓝牙 Mesh 设备
普通蓝牙设备可同时显示在多个蓝牙网关下,只要中枢下也同时有显示,设备就接受中枢的统一管理
目前中枢下显示的设备信号强度是主中枢可控范围内的设备信号,可能会显示偏低,实际上不影响正常使用
Zigbee 设备只能通过多模网关或其它支持 Zigbee 的网关接入中枢体系
可能成为盲网关的原因:与主中枢不在同一局域网、设备太旧固件不支持等
备中枢的子设备会统一展示到主中枢的子设备页面。
从网关的子设备会统一展示到主中枢的子设备页面。
对于有中枢设备的家庭,建议关闭盲网关的网关功能。
参考:
[1] 南境云姐.中枢本地化教学-设备管理及协作规则
[2] 米家小编.中枢与网关使用指南
方法:
一、点开“工具”,点击“获取工具和功能”
二、在“单个组件”中找到“C++ 核心功能”并安装
三、成功
友情提示:
微软自己说的,Spy 已经淘汰了,Inspect 也淘汰了,建议用 Accessibility Insights。
[DS218+] DS218plus 上每秒接收的日志数超出 10 的容差值
DS218plus 每秒接收 55 个日志,这超出了每秒 10 个日志的容差值。请前往日志中心查看详细信息。
来自 DS218plus
打开群晖日志中心,左侧切换到“日志”选项卡,列表上方有“常规”、“连接”、“文件传输”、“硬盘”四个日志分类。
我的情况是“文件传输”中有大量日志,是因为我的小米摄像头设置的是把录制的视频保存到NAS中,而且是始终录制,而不是录制移动画面。这样就会产生多个视频文件,当自动清理一年前的视频时会出现同一秒处理了超过指定数量的文件,群晖就会发送这个通知。
解决的方法很简单:
方法一:米家 - 摄像头 - ...(右上角的设置) - 存储设置 - 存储卡状态 - 录制模式 改为录制移动画面。
方法二:群晖 - 日志中心 - 通知 - 修改“每秒日志数超出”。
CorelDRAW 版本 | “另存为”支持的最低版本 |
CorelDRAW 2023(版本 24.3) | 15.0 (X5) |
CorelDRAW 2022(版本 24.0) | 15.0 (X5) |
CorelDRAW 2021(版本 23.0) | 15.0 (X5) |
CorelDRAW 2020(版本 22.0) | 11.0 |
CorelDRAW 2019(版本 21.0) | 11.0 |
CorelDRAW 2018(版本 20.0) | 11.0 |
CorelDRAW 2017(版本 19.0) | 11.0 |
CorelDRAW X8(版本 18.0) | 11.0 |
CorelDRAW X7(版本 17.0) | 11.0 |
CorelDRAW X6(版本 16.0) | 7.0 |
CorelDRAW X5(版本 15.0) | 7.0 |
CorelDRAW X4(版本 14.0) | 7.0 |
CorelDRAW X3(版本 13.0) | 7.0 |
本文基于 manifest v3
插件使用 Web 技术开发
浏览器提供额外的插件 API
插件与网页分离,运行在一个独立的环境中
插件 API 功能
管理 Tabs、窗口、历史记录、书签、Cookies、下载、浏览数据、通知、网站权限
管理浏览器的外观和感觉 - 背景、主题、右键菜单、新标签页、启动页面
定制 DevTools
向网页注入脚本,与网页通信,与系统中的 Native 应用程序通信
修改/监听发送的 HTTP 请求/接收的回复
插件构成
结构 | 功能 | 调用 API | 操作 Dom | 进程 |
manifest | 配置文件 | - | ||
popup | 弹出对话框 | 所有 | Extension process | |
option page | 用户使用的设置页面 | 所有 | ||
background script | 后台脚本 | 所有 | 禁止? | Extension process |
content script | 内容脚本(注入到网页中) | 有限 | 允许 | Renderer process |
* 即便 content script 是注入到网页中的,而且是运行在 Renderer 进程(与主网页相同的进程),但是它们仍运行在不同的世界(world)。主网页运行在 main world,插件的 content script 运行在 isolated world。比如说 main world 中有一个变量,它在 isolated world 中是访问不到的,但是如果修改了 dom,对其它世界是有影响的。
将 Chrome 插件迁移到 Edge
移除 Chrome 独有的 API(如调用 Google 账户)
移动 update_URL 字段(如果是从 Chrome 商店直接下载的包会有这个字段)
改名 Chrome 相关的文字(插件名称、描述文字)
参考文献
SignalR 是一个开源的实时通信库,用于构建实时 Web 应用程序。它提供了一个简单的 API,可以在客户端和服务器之间建立持久连接,以便实时地推送数据。
与传统的 WebSocket 相比,SignalR 提供了更高级的功能和更简单的开发体验。下面是一些主要区别:
支持多种传输方式:SignalR 可以使用多种传输方式,包括 WebSocket、Server-Sent Events(SSE)、长轮询和 Forever Frame。这使得 SignalR 在不同的环境中都能提供实时通信的能力,即使某些浏览器不支持 WebSocket,也可以使用其他传输方式。
自动处理连接管理:SignalR 管理连接的生命周期,包括连接的建立、断开和重新连接。它会自动处理连接的失败和重新连接的逻辑,简化了开发人员的工作。
服务器端推送:SignalR 允许服务器端主动推送消息给客户端,而不需要客户端发起请求。这使得实时通信变得更加高效和实时,适用于聊天应用、实时监控等场景。
跨平台支持:SignalR 可以在多个平台上使用,包括 .NET、Java、JavaScript 等。这使得开发人员可以使用自己熟悉的语言和框架来构建实时应用程序。
微软官方提供了针对 ASP.NET Core Web 应用(Razor 页面)的详细教程,这里给出 MVC 版本入门教程。
最终将创建一个正常运行的聊天应用:
创建 Web 应用项目
添加 SignalR 客户端库
在“解决方案资源管理器”>中,右键单击项目,然后选择“添加”“客户端库”。
在“添加客户端库”对话框中:
“提供程序”选择“unpkg”
“库”,请输入 @microsoft/signalr@latest。
选择“选择特定文件”,展开“dist/browser”文件夹,然后选择 signalr.js 和 signalr.min.js。
点击“安装” 。
创建 SignalR Hubs 类
using Microsoft.AspNetCore.SignalR; /// <summary> /// Hub 类管理连接、组和消息 /// </summary> public class ChatHub : Hub { /// <summary> /// 可通过已连接客户端调用 SendMessage,以向所有客户端发送消息 /// </summary> public async Task SendMessage(string user, string message) { // Clients.All 向所有的客户端发送消息 // ReceiveMessage 是客户端监听的方法 await Clients.All.SendAsync("ReceiveMessage", user, message); } }
其父类 Hub 可管理连接、组和消息。这里演示的是向所有客户端发送消息。
配置 SignalR
打开 Program.cs,添加注入:
builder.Services.AddSignalR();
添加路由:
app.MapHub<ChatHub>("/chatHub");
添加 SignalR 客户端代码
视图页面:
<div class="container"> <div class="row p-1"> <div class="col-1">用户</div> <div class="col-5"><input type="text" id="userInput" /></div> </div> <div class="row p-1"> <div class="col-1">消息</div> <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div> </div> <div class="row p-1"> <div class="col-6 text-end"> <input type="button" id="sendButton" value="发送消息" /> </div> </div> <div class="row p-1"> <div class="col-6"> <hr /> </div> </div> <div class="row p-1"> <div class="col-6"> <ul id="messagesList"></ul> </div> </div> </div> <script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script> <script src="~/js/chat.js"></script>
chat.js 文件:
"use strict"; var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build(); // 在连接建立之前禁用发送按钮 document.getElementById("sendButton").disabled = true; connection.on("ReceiveMessage", function (user, message) { var li = document.createElement("li"); document.getElementById("messagesList").appendChild(li); // 修改此处时应注意脚本注入问题 li.textContent = `${user} says ${message}`; }); connection.start().then(function () { document.getElementById("sendButton").disabled = false; }).catch(function (err) { return console.error(err.toString()); }); document.getElementById("sendButton").addEventListener("click", function (event) { var user = document.getElementById("userInput").value; var message = document.getElementById("messageInput").value; connection.invoke("SendMessage", user, message).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); });
完成。在线示例:https://xoyozo.net/Demo/SignalRDemo
推荐使用 SignalR 来平替 WebSocket,参考教程。
【服务端(.NET)】
一、创建 WebSocketHandler 类,用于处理客户端发送过来的消息,并分发到其它客户端
public class WebSocketHandler { private static List<WebSocket> connectedClients = []; public static async Task Handle(WebSocket webSocket) { connectedClients.Add(webSocket); byte[] buffer = new byte[1024]; WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); while (!result.CloseStatus.HasValue) { string message = Encoding.UTF8.GetString(buffer, 0, result.Count); Console.WriteLine($"Received message: {message}"); // 处理接收到的消息,例如广播给其他客户端 await BroadcastMessage(message, webSocket); result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); } connectedClients.Remove(webSocket); await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); } private static async Task BroadcastMessage(string message, WebSocket sender) { foreach (var client in connectedClients) { if (client != sender && client.State == WebSocketState.Open) { await client.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, CancellationToken.None); } } } }
二、在 Program.cs 或 Startup.cs 中插入:
// 添加 WebSocket 路由 app.UseWebSockets(); app.Use(async (context, next) => { if (context.Request.Path == "/chat") { if (context.WebSockets.IsWebSocketRequest) { WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); await WebSocketHandler.Handle(webSocket); } else { context.Response.StatusCode = 400; } } else { await next(); } });
这里的路由路径可以更改,此处将会创建一个 WebSocket 连接,并将其传递给 WebSocketHandler.Handle 方法进行处理。
也可以用控制器来接收 WebSocket 消息。
【客户端(JS)】
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" /> <title>WebSocket 示例(JS + ASP.NET)</title> <style> #list { width: 100%; max-width: 500px; } .tip { color: red; } </style> </head> <body> <h2>WebSocket 示例(JS + ASP.NET)</h2> <textarea id="list" readonly rows="20"></textarea> <div> <input type="text" id="msg" /> <button onclick="fn_send()">发送</button> <button onclick="fn_disconnect()">断开</button> </div> <p id="tip_required">请输入要发送的内容</p> <p id="tip_closed">WebSocket 连接未建立</p> <script> var wsUrl = "wss://" + window.location.hostname + ":" + window.location.port + "/chat"; var webSocket; var domList = document.getElementById('list'); // 初始化 WebSocket 并连接 function fn_ConnectWebSocket() { webSocket = new WebSocket(wsUrl); // 向服务端发送连接请求 webSocket.onopen = function (event) { var content = domList.value; content += "[ WebSocket 连接已建立 ]" + '\r\n'; domList.innerHTML = content; document.getElementById('tip_closed').style.display = 'none'; webSocket.send(JSON.stringify({ msg: 'Hello' })); }; // 接收服务端发送的消息 webSocket.onmessage = function (event) { if (event.data) { var content = domList.value; content += event.data + '\r\n'; domList.innerHTML = content; domList.scrollTop = domList.scrollHeight; } }; // 各种情况导致的连接关闭或失败 webSocket.onclose = function (event) { var content = domList.value; content += "[ WebSocket 连接已关闭,3 秒后自动重连 ]" + '\r\n'; domList.innerHTML = content; document.getElementById('tip_closed').style.display = 'block'; setTimeout(function () { var content = domList.value; content += "[ 正在重连... ]" + '\r\n'; domList.innerHTML = content; fn_ConnectWebSocket(); }, 3000); // 3 秒后重连 }; } // 检查连接状态 function fn_IsWebSocketConnected() { if (webSocket.readyState === WebSocket.OPEN) { document.getElementById('tip_closed').style.display = 'none'; return true; } else { document.getElementById('tip_closed').style.display = 'block'; return false; } } // 发送内容 function fn_send() { if (fn_IsWebSocketConnected()) { var message = document.getElementById('msg').value; document.getElementById('tip_required').style.display = message ? 'none' : 'block'; if (message) { webSocket.send(JSON.stringify({ msg: message })); } } } // 断开连接 function fn_disconnect() { if (fn_IsWebSocketConnected()) { // 部分浏览器调用 close() 方法关闭 WebSocket 时不支持传参 // webSocket.close(001, "Reason"); webSocket.close(); } } // 执行连接 fn_ConnectWebSocket(); </script> </body> </html>
【在线示例】
https://xoyozo.net/Demo/JsNetWebSocket
【其它】
-
服务端以 IIS 作为 Web 服务器,那么需要安装 WebSocket 协议
-
一个连接对应一个客户端(即 JS 中的 WebSocket 对象),注意与会话 Session 的区别
-
在实际项目中使用应考虑兼容性问题
-
程序设计应避免 XSS、CSRF 等安全隐患
-
参考文档:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets
如果手机开启了自动横屏,那么网页就会跟着旋转,导致网页宽度变大,高度变小,页面布局不友好,我们可以通过判断屏幕方向 api,用 css 设置旋转。
HTML:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" /> <title>保持竖屏示例</title> </head> <body> <div id="app"> <div class="page"> <div>把手机横过来试试</div> <div>页面永远保持竖屏方向</div> </div> </div> </body> </html>
CSS:
body { margin: 0; } .page { text-align: center; padding-top: 100px; } @media screen and (orientation: landscape) { body { width: 100vh; height: 100vw; overflow: hidden; } .page.landscape-primary { transform: rotate(-90deg); transform-origin: top left; width: 100vh; height: 100vw; position: absolute; top: 100%; left: 0; } .page.landscape-secondary { transform: rotate(90deg); transform-origin: top left; width: 100vh; height: 100vw; position: absolute; top: 0; left: 100%; } }
JS:
function fn_set_orientation() { var orientation = window.screen.orientation || window.screen.mozOrientation || window.screen.msOrientation; const div = document.querySelector('.page'); console.log(orientation.type) switch (orientation.type) { case 'landscape-primary': { div.classList.remove('landscape-secondary'); div.classList.add('landscape-primary'); break; } case 'landscape-secondary': { div.classList.remove('landscape-primary'); div.classList.add('landscape-secondary'); break; } default: { div.classList.remove('landscape-primary'); div.classList.remove('landscape-secondary'); break; } } } window.addEventListener("orientationchange", function () { fn_set_orientation(); }); fn_set_orientation();
示例:
https://xoyozo.net/Demo/Orientation