米米的博客

做了一点微小的工作

家里开通了电信 IPTV,但套餐只包含了一台机顶盒,家中有多台电视时使用起来并不方便。很早以前我就知道可以通过 igmpproxy 转发组播,再配合 udpxy 将组播转换为普通 HTTP 单播,但此前一直没有配置成功。

最近重新梳理了一遍光猫、机顶盒和组播之间的关系,终于实现了以下目标:

  • 原装 IPTV 机顶盒继续正常使用,直播和点播均不受影响;
  • 局域网中的其他设备(电视、电脑、手机等)可以收看直播。

本文记录的是我在湖北武汉电信网络中的实际配置。不同地区的 VLAN 和认证方式都可能不同,请不要直接照抄参数。修改光猫前,建议先备份配置,并记录每一处原始设置。

原始网络结构

光猫中原本有两项主要业务:

  1. 上网业务,采用路由模式,本文不讨论;
  2. iTV 业务,采用桥接模式,仅绑定到光猫的 LAN2 口。

iTV 桥接连接的关键参数如下:

项目原始设置
业务类型OTHER
连接模式桥接
IP 模式IPv4
业务 VLAN ID3542
端口绑定LAN2(iTV 口)

在这一结构下,电信机顶盒连接 LAN2,并由机顶盒自己进行 PPPoE 拨号。机顶盒的网络设置中可以看到 IPTV 账号,但密码通常不会直接显示。

这里有一个容易踩的坑:IPTV 密码可能由机房或装维系统下发。即使通过 10000 客服重置或查询到了一个密码,也不一定能在现网环境中成功拨号。如果确认账号无误但始终认证失败,最好直接联系当地装维人员,获取当前线路实际使用的 IPTV 密码。

失败的尝试:让两个设备分别拨号

我最初的思路很直接:在光猫的 iTV 桥接连接中,除 LAN2 外再绑定一个 LAN3,让原装机顶盒连接 LAN2,另一个路由器或小主机连接 LAN3。两台设备分别使用同一组 IPTV 账号、密码拨号,小主机负责运行 igmpproxy,就可以实现局域网组播。

实际测试发现,这种方式行不通:只要其中一台设备已经拨号成功,第二台设备的拨号就会被拒绝。至少在我的线路上,同一个 IPTV 账号无法同时建立两个 PPPoE 会话。这意味着如果光猫桥接,让下游设备拨号,原装机顶盒和独立组播代理只能二选一。

关键调整:改由光猫拨号

进一步查看机顶盒设置时,我发现它除了 PPPoE,还支持 DHCP 联网。由此可以换一种思路:

不再让机顶盒直接拨号,而是让光猫建立 IPTV PPPoE 连接,再通过 DHCP 给 LAN2、LAN3 下游设备分配地址。

我将光猫中的 iTV 连接从桥接模式改为路由模式,填写 IPTV PPPoE 账号和密码,并同时绑定 LAN2、LAN3。

修改后,光猫成功获得了运营商 IPTV 网络中的 10.x.x.x 地址,机顶盒改为 DHCP 后也能取得光猫下发的局域网地址。

此时测试结果如下:

  • IPTV 平台登录正常;
  • 菜单和业务页面正常;
  • 点播节目可以播放;
  • 直播频道仍然黑屏

这个现象很有价值。它说明 PPPoE 认证、DHCP 和普通 IP 通信已经正常,机顶盒也没有被限制为必须自行拨号;但直播所需的组播链路尚未打通。点播通常走普通单播,而直播依赖 IGMP 和运营商的组播 VLAN,因此两者完全可能出现一个正常、另一个黑屏的情况。

阅读全文 »

过去几年里,大模型把语音助手的想象空间重新打开了。我们已经习惯了在手机、网页和电脑上和 AI 对话,但家里的智能音箱却常常还停留在比较固定的指令式体验里:问天气、放音乐、控制设备可以,但想要更自然的连续对话、更灵活的工具调用,或者接入自己选择的大模型服务,就没那么顺手了。

xiaoai-agent 想解决的正是这个问题:让小爱音箱不只是一个云端语音入口,而是变成一台可以常驻运行的端侧语音 Agent 设备。

项目地址:github.com/stevenjoezhang/xiaoai-agent

背景:小爱音箱还有很多潜力

小爱音箱本身是一类很适合承载语音 Agent 的设备。它有麦克风阵列,有扬声器,有唤醒能力,也有成熟的音频播放和 TTS 链路。换句话说,它的硬件形态已经非常接近「家里的 AI 入口」。

但现实使用中会遇到几个限制:

  • 原生小爱同学的对话能力和可扩展性有限,很难按自己的偏好接入新的大模型、ASR 或工具。
  • 一些改造方案需要额外部署服务器或桥接服务,链路变长,维护成本也会变高。
  • 原生小爱同学仍然可能监听麦克风,出现抢麦、抢答,甚至触发小米云端控制的情况。
  • 音频体验很难做好,比如唤醒、录音、回声消除、连续对话、中途打断等细节,任何一个环节不稳都会影响日常可用性。

xiaoai-agent 的方向,是尽量把这些能力收回到音箱端侧,让音箱自己完成唤醒、录音、识别、思考、工具调用和回复。

现有方案与不足

xiaoai-agent 之前,已经有不少项目尝试把小爱音箱和大模型连接起来。这些探索证明了小爱音箱的可玩性,也把很多底层路径跑通了。

比如原生小爱同学提供了稳定的唤醒、TTS、设备控制和生态接入能力,适合普通用户日常使用。但它的对话能力、模型选择和工具扩展都比较固定,用户很难把自己的 ASR、大模型或智能家居工作流接进去。

Open-XiaoAI 这类项目则打开了更底层的设备改造路径,让音箱具备 SSH、启动脚本和自定义部署的可能。它解决的是「能不能进入设备、能不能运行自己的东西」的问题,为后续折腾提供了基础。

MiGPT 等桥接类方案则更进一步,把大模型对话接入到小爱音箱体验里,让用户可以用自然语言和 AI 聊天。这类方案的优势是思路清晰、上手相对友好,但通常需要额外服务端或桥接层来承载 Agent 逻辑,音箱本身更像前端入口。

这些方案各有价值,但也留下了一些共同痛点:语音链路复杂、部署组件偏多、原生小爱可能抢麦或抢答、云端控制可能被误触发,音频细节也容易受多层转发影响。xiaoai-agent 正是在这些探索之上,尝试把 Agent 主流程进一步收回到音箱端侧。

它解决了什么痛点

xiaoai-agent 最核心的价值,是把小爱音箱改造成一个更开放、更可控的语音 Agent 载体。

首先,它避免了「两个助手同时工作」的混乱。项目会让原生小爱同学的麦克风输入静音,由 xiaoai-agent 接管真实麦克风音频。这样可以减少抢麦、抢答,以及误触发小米云端控制的问题。

其次,它降低了长期运行的复杂度。传统方案常常需要在局域网里额外跑一个服务,音箱和服务之间再做消息转发。xiaoai-agent 选择直接在音箱端侧运行 Agent,部署链路更短,也更像一台独立设备。

第三,它复用了音箱原本已经做好的音频能力。唤醒、VPM 音频回调、TTS 播报、播放控制等能力都来自设备原生系统,日常交互会更接近「真正的音箱体验」,而不是把音箱当作一个简单的外设。

最后,它给用户留下了足够多的选择空间。ASR 可以使用兼容 OpenAI POST /v1/audio/transcriptions 的服务,大模型也可以接入 OpenAI-compatible API。你可以按自己的预算、延迟和隐私偏好选择服务,而不是被固定在单一路径里。

主要特性

xiaoai-agent 是一个非官方技术研究项目,核心 Agent 使用 Rust 编写,运行在小爱音箱端侧。目前项目已经在 Xiaomi 智能音箱 Pro(OH2P)固件 1.62.2 上测试成功。

它目前已经具备一套完整的语音 Agent 闭环:

  • 端侧常驻运行:Agent 直接运行在音箱上,不需要额外部署专门服务端。
  • 完整语音流程:支持唤醒、录音、ASR、大模型对话、工具调用和 TTS 回复。
  • 更自然的交互:支持连续对话、VAD、中途打断、回声消除和播放时录音。
  • 开放模型接入:ASR 和 LLM 均可配置为兼容 OpenAI API 的服务。
  • 工具与设备控制:内置时间、天气、音乐播放等工具,并可通过 Home Assistant MCP 控制智能家居。
  • 保留部分系统能力:语音对话由 Agent 接管,但蓝牙网关等非语音服务不应受到影响。
  • 可自定义体验:可以调整唤醒响应、LED 状态、音乐服务和运行参数。

项目仓库中也包含补丁固件制作、刷机辅助、示例配置和部署说明,方便用户从设备准备一路走到 Agent 常驻运行。需要特别说明的是,它不是官方项目,也不是面向所有型号的一键通用方案;现阶段更适合愿意折腾、理解刷机风险,并且手上有对应设备的用户尝试。

这套能力组合起来,小爱音箱就不再只是一个固定功能的智能音箱,而更像是家里可以听、可以说、可以调用工具的 AI 节点。

阅读全文 »

背景

笔者很久以前在 Mac 上通过 Boot Camp 安装了 Windows 系统,最近计划将这个系统迁移到 ESXi 虚拟机中使用。笔者首先使用同一机器上 macOS 系统安装的 VMware Fusion 来将 Boot Camp 的 Windows 转化为虚拟机,这一步转化是成功的,虚拟机可以启动。但是进一步导出 OVF 模板并在 ESXi 上导入时,结果发现虚拟机无法启动。经过分析,发现问题的根源在于磁盘扇区大小不匹配。

Mac 上通过 Boot Camp 安装的 Windows,磁盘默认使用 4Kn(4K Native) 扇区格式 —— 物理扇区和逻辑扇区均为 4096 字节。这种格式的磁盘在正常使用时是无感的,但是在需要迁移系统到其他磁盘(比如转成 ESXi 虚拟机)时,就会碰到麻烦:大多数虚拟化平台的虚拟磁盘只支持 512 字节扇区(512n 或 512e)。

这个问题其实并不好解决。扇区大小不匹配,那么直接克隆必然失败,很多常见的硬盘分区、数据恢复工具,包括声称支持不同扇区大小之间的克隆转换的 Clonezilla,都无法解决这个问题。

真正可行的方案:DISM

经过测试,微软自带的 DISM(Deployment Image Servicing and Management) 工具可以完美解决这个问题。

原因很简单 ——DISM 的工作方式是文件级别的:

  • 备份时:把系统分区的所有文件打包成一个 .wim 镜像
  • 还原时:把 .wim 镜像中的文件逐个释放到目标分区

整个过程完全不涉及扇区结构。它不关心源盘是 4Kn 还是 512,也不关心目标盘是什么格式。扇区大小的差异在文件级操作面前根本不是问题。

大致流程:

  1. 在 WinPE 环境下,用 DISM 将 Boot Camp 的 Windows 系统捕获为 .wim 文件
  2. 在虚拟机中创建一个 512 扇区的虚拟磁盘,手动建好 ESP 分区和系统分区
  3. 用 DISM 将 .wim 释放到虚拟磁盘的系统分区
  4. 写入引导,完成

Dism++ 同样可以

如果你不喜欢敲命令行,Dism++ 是 DISM 的图形化前端,底层调用的是同样的 API,同样是文件级操作。用它来备份和还原效果完全一样,只是操作更直观。

唯一需要注意的是:无论用 DISM 还是 Dism++,都需要你事先在目标磁盘上手动建好分区(包括 ESP 分区),它们只负责释放文件和写入引导,不会自动帮你分区。

小结

在 4Kn 到 512 的系统迁移场景下,凡是涉及扇区级操作的工具都可能翻车。而 DISM / Dism++ 因为是纯文件级的备份还原,天然绕过了扇区大小的问题,是目前最简单可靠的方案。

本文记录了我在忘记 VMware Workstation 虚拟机加密密码后,通过 PowerShell 调用 Windows 凭据管理器 API 成功恢复密码的完整过程。如果你也遇到了同样的问题,希望这篇文章能帮到你。

起因

前几天我需要迁移一台之前创建的 VMware 加密虚拟机,选择导出 OVF,结果弹出了密码输入框 —— 我完全不记得当时设置了什么密码。

说实话,这个密码根本不是我主动设置的。VMware Workstation 在某些操作(比如启用加密、克隆加密虚拟机)时会引导你设置加密密码,同时提供一个「Remember the password」(记住密码)选项。当时我随手勾选了这个选项,之后每次打开虚拟机都是自动解锁的。

直到需要迁移虚拟机时,它需要用户主动输入密码了。于是我开始研究恢复方案。

原理

经过一番搜索,我了解到以下关键信息:

  1. VMware Workstation 在你勾选「Remember the password」时,会将加密密码存储在 Windows 凭据管理器(Credential Manager) 中。
  2. 每台加密虚拟机都有一个唯一的 GUID,VMware 用这个 GUID 作为凭据的名称(Target Name)来存储密码。
  3. 虽然你可以在 Windows 的「凭据管理器」界面中看到这些条目,但界面上不会显示密码明文
  4. 好消息是,我们可以通过 Windows 的 Win32 CredReadW API 直接读取凭据中存储的密码。

换句话说,密码一直都在你的电脑里,只是 VMware 没有提供一个「显示密码」的按钮而已。

恢复步骤

找到虚拟机的加密 GUID

用文本编辑器打开虚拟机的 .vmx 配置文件。这个文件通常位于虚拟机的存储目录下,例如:

1
C:\Users\<用户名>\Documents\Virtual Machines\<虚拟机名>\<虚拟机名>.vmx

在文件中搜索 encryptedVM.guid,你会找到类似这样的一行:

1
encryptedVM.guid = "{833AB4F5-587E-4B11-9260-4DB13742FA7F}"

记下这个 GUID(包括大括号),后面会用到。

在 PowerShell 中加载 Win32 API

打开 PowerShell,将以下代码完整复制粘贴进去并回车执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Add-Type @"
using System;
using System.Runtime.InteropServices;

public class Win32Cred
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CREDENTIAL
{
public int Flags;
public int Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public int CredentialBlobSize;
public IntPtr CredentialBlob;
public int Persist;
public int AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}

[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredReadW(
string target, int type, int reservedFlag, out IntPtr credentialPtr);

[DllImport("advapi32.dll", SetLastError = true)]
public static extern void CredFree(IntPtr buffer);

public static string CredRead(string targetName, int type = 1)
{
IntPtr credPtr;
if (CredReadW(targetName, type, 0, out credPtr))
{
CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(
credPtr, typeof(CREDENTIAL));
string pass = Marshal.PtrToStringAnsi(
cred.CredentialBlob, cred.CredentialBlobSize);
CredFree(credPtr);
return pass;
}
throw new System.ComponentModel.Win32Exception(
Marshal.GetLastWin32Error());
}
}
"@

这段代码通过 C# 的 P / Invoke 机制定义了对 advapi32.dllCredReadWCredFree 函数的调用接口,并封装了一个简洁的 CredRead 方法。

读取密码

继续在 PowerShell 中执行以下命令,将 GUID 替换为你在第一步中找到的值:

1
[Win32Cred]::CredRead("{833AB4F5-587E-4B11-9260-4DB13742FA7F}")

如果一切顺利,密码会直接输出到屏幕上:

1
2
PS C:\Users\Andrew> [Win32Cred]::CredRead("{833AB4F5-587E-4B11-9260-4DB13742FA7F}")
MySecretPassword

就是这么简单。 拿到密码后,回到 VMware 输入即可解锁虚拟机。

总结

整个恢复过程可以概括为三步:

步骤操作关键点
1打开 .vmx 文件找到 encryptedVM.guid
2在 PowerShell 中加载 Win32 API复制粘贴 Add-Type 代码块
3调用 CredRead 方法传入 GUID,密码直接输出

VMware 的加密密码其实一直安静地躺在 Windows 凭据管理器里,只是没有提供直观的查看方式。通过一小段 PowerShell + C# 互操作代码,就能把它读出来。

希望这篇文章能帮助到同样被 VMware 加密密码困扰的你。


参考文章:andshrew 的 Gist

在五年前的文章将 Yeelight 烛光氛围灯接入 Apple HomeKit 中,笔者介绍了通过 Homebridge 控制 Yeelight 烛光氛围灯的方法。然而,笔者在将 Homebridge 相关代码迁移(使用 AI 转写)到 ESPHome 平台,并尝试在 Home Assistant 中使用时,发现原版代码的蓝牙交互实际上存在问题。yee.js 使用了0x43,0x67,0xde,0xad,0xbe,0xbf作为蓝牙连接后认证的 payload,但这实际上无效;即使 ESP32 和灯连接上了蓝牙,过一段时间后就会断开,下次控制时又需要重新连接。

经过搜索,笔者找到了两个重要的参考:I need Help to use esp32 to Control Yeelight Candla BLE Lamp with MQTT / HomeAssisant 以及“失联” 7 年的 Yeelight 烛光氛围灯,上线 HomeKit 平台 | 新人报到 - 少数派。根据这些资料,如果不完成配对,蓝牙每过一段时间就会被强制断开,和笔者遇到的情况一致。而测试成功的交互流程是,连接蓝牙后发送0x43,0x40,0x02进行认证,此时灯会进入配对模式,亮度会闪烁,之后旋转氛围灯,就可以配对成功。

基于此,笔者重新编写了 ESPHome 配置文件,供参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
substitutions:
YEELIGHT_PAIR_BUTTON_PIN: GPIO0
YEELIGHT_STATUS_LED_PIN: GPIO2

YEELIGHT_SERVICE_UUID: "0000FE87-0000-1000-8000-00805f9b34fb"
YEELIGHT_NOTIFY_UUID: "8F65073D-9F57-4AAA-AFEA-397D19D5BBEB"
YEELIGHT_CONTROL_UUID: "AA7D3F34-2D4F-41E0-807F-52FBF8CF7443"
YEELIGHT_MAC: "F8:24:41:XX:XX:XX"

esphome:
name: yeelight
platformio_options:
board_build.flash_mode: dio

esp32:
board: esp32dev
framework:
type: esp-idf

logger:

api:

ota:
platform: esphome
password: ""

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password

binary_sensor:
- platform: gpio
name: yeelight_pair_button
pin:
number: $YEELIGHT_PAIR_BUTTON_PIN
inverted: true
on_click:
- script.execute: script_yeelight_pair
internal: true

ble_client:
- mac_address: $YEELIGHT_MAC
id: ble_yeelight

sensor:
- platform: ble_client
id: sensor_yeelight_notify
type: characteristic
ble_client_id: ble_yeelight
internal: true
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_NOTIFY_UUID
notify: true
lambda: |-
if (x.size() > 4 && x[0] == 0x43 && x[1] == 0x45) {
bool power = (x[2] == 0x01);
float brightness = (float)x[3] / 100.0;
auto target = id(yeelight_candle);
target->remote_values.set_state(power);
target->remote_values.set_brightness(brightness);
target->publish_state();
ESP_LOGD("yeelight", "Sync - Power: %s, Brightness: %.0f%%", power ? "ON" : "OFF", brightness * 100);
}
return NAN;

light:
- platform: monochromatic
name: "Yeelight Candela"
id: yeelight_candle
output: output_yeelight_brightness
on_turn_on:
- script.execute: script_yeelight_on
on_turn_off:
- script.execute: script_yeelight_off
- platform: binary
id: yeelight_status_led
output: output_yeelight_status
internal: true

output:
- platform: template
id: output_yeelight_brightness
type: float
write_action:
- script.execute:
id: script_yeelight_brightness
value: !lambda return state;
- platform: gpio
id: output_yeelight_status
pin: $YEELIGHT_STATUS_LED_PIN

script:
- id: script_yeelight_pair
then:
- script.execute: script_yeelight_off
- script.execute: script_yeelight_on
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: [0x43, 0x67, 0x02] # enable pulse mode
- light.turn_on: yeelight_status_led
- delay: 10s
- light.turn_off: yeelight_status_led
- id: script_yeelight_on
then:
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: [0x43, 0x40, 0x01]
- id: script_yeelight_off
then:
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: [0x43, 0x40, 0x02]
- id: script_yeelight_brightness
parameters:
value: float # 0% - 100%
then:
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: !lambda return { 0x43, 0x42, (uint8_t)(value * 100) };

笔者做的主要更改是支持了sensor_yeelight_notify,它监听灯的状态变化,并将状态同步到 ESPHome 的 light 实体上。这样,无论是通过 Home Assistant 控制灯,还是直接旋转灯进行亮度调节,ESPHome 都能正确地获取灯的状态并更新。

使用方式是:

  1. 将上述代码保存为yeelight.yaml,并替换其中的YEELIGHT_MAC为实际的灯的 MAC 地址(可以先扫描附近的 BLE 设备,然后根据名称yeelight_ms或者yl_candela找到灯),配对按键使用的GPIO0也可以根据情况修改。
  2. 通过esphome run yeelight.yaml将代码烧录到 ESP32 上。
  3. 确保灯处于蓝牙连接范围内。
  4. 通过esphome logs yeelight.yaml查看日志,在配对成功前,可以看到 ESP32 和灯的蓝牙连接间歇性断开,虽然 ESP32 会自动重连,但是此时操作的延迟会很大。
  5. 趁着连接成功的间隙,按下配对按钮(这里使用的是GPIO0,通常是 ESP32 开发板的BOOT按钮),等待 ESP32 开发板上的 LED 亮起,氛围灯也开始闪烁时,表示开始配对,此时旋转灯进行配对。
  6. 配对成功后,ESP32 和灯的连接会保持稳定,此时就可以添加进 Home Assistant,来控制灯的开关和亮度了。

此外,如果 ESP32 的物理按键不方便使用,也可以将binary_sensor去除,改为通过编写button组件在 Home Assistant 中生成模拟按键,点击即可触发配对脚本。例如:

1
2
3
4
5
button:
- platform: template
name: "Yeelight Pair Button"
on_press:
- script.execute: script_yeelight_pair
0%