如何从零开始:用 CH592F制作CS2生命值胸章并实现和游戏联动效果

healthpin_in_game

背景

在CS2中(CS:GO里面也有)中有一个叫做生命值胸章的饰品
healthpin_in_cs2
第一次看到时候就觉得这个很适合使用LED做出来.
而且还可以通过CSGO GSI同步游戏中生命值状态.

补充说明

有不少人指出胸章出自半条命:Alyx,以前只在朋友家玩过开头, 对这个装置在半条命中的印象不是很深了.
观看了一些Alyx的实况视频后了解到这个装置的更为具体的一些信息, 或许将来可以制作三个完整的爱心版本,然后像Alyx中的一样, 每颗心都有大小的变化, 极大的提高了丰富表现力.

元件选型

MCU选型

由于胸章的尺寸较小(60mmx20mm), 而且我也不希望成品太厚无法佩戴,还不想拖着一根线,所以主控得是一个支持无线(乐鑫:有人叫我?)的芯片.
虽然我很喜欢乐鑫家的芯片(我之前写过很多关于 ESP32 串流ESP32 显示公网图片 的教程)),但是抱歉, 这次真的对不起,你的功耗实在是太高了.

之前在制作Friday Ink时候,用的芯片是CH582F, 使我对WCH王翠花家的芯片略有好感,不仅便宜够用,功耗还低.

结合成本和开发量评估后, 最终选择WCH王翠花家的CH592F作为主控, 和之前的CH582F对比, 只是RAM略有些减少, 但是对于当前的HealthPin是绝对足够的.

ch592

LDO选型

之前爱用的LDO是AP2112K-3.3TRG, 源自多年前抄Arduino Pro micro时候形成的习惯.

为了进一步压缩成本, LDO换成了更容易购买到而且仅需0.18元的ME6211.

锂电池充电芯片选型

在之前的罗盘设计中, 锂电池充电管理芯片用的是MCP73831, 这款IC我首次购买时候4元,后来居然涨价到了8元(不过最近有所回落,但还是让我有所忌惮).
因此充电管理芯片换成了仅需0.45元的TP4057, 二者外围器件差异不大,仅需注意引脚定义.

灯珠和MOS选择

你知道的, RGB可以提升300%性能,所以灯珠毫不犹豫的选择了内置WS2812B的1mm x 1mm RGB灯珠.

考虑到WS2812B的静态功耗有点大, 对于我们本来就不大的电池来说雪上加霜, 所以三颗心分为三组, 除了最小的那颗心无法主动关闭供电外, 其余两颗心都会使用MOS管控制电源开关,达到不用时候彻底关闭省电的目的.
对于灯泡、电机这类无源负载,通常使用驱动简单的NMOS作为下管来控制其地线回路。而对于WS2812B这类需要与MCU进行精确数字通信的有源器件,为了确保通信信号的完整性以及两者之间的稳定共地,优选使用PMOS作为上管来控制其电源,而不是控制其地线。
为了节省空间, 选择一个SOT23-6封装的BRCS4953提供双PMOS.

PCB设计

尝试过把灯珠和主控分别放在两张板子上, 再焊接方式粘在一起, 但是这样会增加一个板子的厚度,成本也会因此上升.
遂放弃这个方案, 继而使用传统的双面元件方式实现.
PCBA最终渲染如下:
healthpin3dmodel
其中电池外围则印有BOMB HAS BEEN PLANTED HANDLE WITH CARE字样彩蛋.

实物焊接效果展示

由于没有外壳, 而且阻焊是白色,所以看上去和游戏原设计有所出入.
real_health_pin

程序

StatTrak项目回顾

在CS:GO还没升级成为CS2的时候,我就在现实中制作过一个可以和游戏联动的StatTrak.
当时方案是直接编写一个python脚本和CS:GO GSI通信, 再借助pyserial向装置发送击杀数据.

Auraro

我记得罗技的GHUB是能够控制自家设备和游戏的联动, 于是就在想是否有类似的开源方案来做到类似的统一管理.

一方搜索后得知了Project Aurora这个开源项目.

最最令人激动的是, Aurora不仅仅适配了很多知名厂商的设备, 他甚至还有一套Device Script来供第三方设备实现对接.

Device Script编写

在Aurora的仓库下有一个example_script.cs提供了简单的示例.
HealthPin的通信方式是使用蓝牙HID, 而蓝牙HID装置在连接电脑后会被视作一个普通的HID装置, 只需要使用标准HID通信API向装置发送数据即可.

协议拟定和实现

我们的装置有LED和蜂鸣器, 所以协议中在将装置这些能力暴露出来.

初步定义如下类型的协议结构和Command, 并且着重介绍SetHeartColor命令

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
// 命令 ID 枚举
enum class HidCommand : uint8_t {
Ping = 0x01,
Reboot = 0x0F,
SetAllLEDs = 0x11,
ShowLEDs = 0x12,
ClearAllLEDs = 0x13,
SetSingleLED = 0x14,
SetHeartColor = 0x15,
SetLEDRange = 0x16,
PlayBeep = 0x21,
BuzzerOn = 0x22,
BuzzerOff = 0x23
};

// CMD 0x15 (Set Heart)
struct Payload_SetHeart {
uint8_t heart_id;
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t reserved[3];
};

// Payload 的 Union
union HidPayloadUnion {
uint8_t raw[7]; // 原始 7 字节
Payload_SetAll setAll;
Payload_SetSingle setSingle;
Payload_SetHeart setHeart;
Payload_SetRange setRange;
Payload_PlayBeep playBeep;
Payload_BuzzerOn buzzerOn;
Payload_NoArgs noArgs;
};

/**
* @brief HID 命令包结构体
* HID 堆栈已经消费了 Report ID,所以不包含 Report ID,
*/
struct HidCommandPacket {
uint8_t cmd; // Byte 0 (命令 ID)
HidPayloadUnion payload; // Bytes 1-7 (负载)
};

Device Script完善

Aurora下发颜色配置时候, 会触发Script Device的UpdateDevice(object keyColors, bool forced)函数, 这个函数里面我们可以查询到所有键位颜色信息.

而HealthPin三颗心分别被映射到了键盘的7,8,9三个键,所以只关注这三个键的数据即可. 当然如果你想关注F1,F2,F3的配置也没问题, 只是需要注意在调整Profiles时候要设置对应的按键.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Color targetHeart1 = Color.Black;
Color targetHeart2 = Color.Black;
Color targetHeart3 = Color.Black;
var dict = keyColors as IDictionary;
foreach (DictionaryEntry entry in dict)
{
string key = entry.Key?.ToString();
if (string.IsNullOrEmpty(key)) continue;
if (key.Equals("NUM_SEVEN", StringComparison.OrdinalIgnoreCase))
{
targetHeart1 = ParseColorObject(entry.Value);
hasUpdate = true;
}
else if (key.Equals("NUM_EIGHT", StringComparison.OrdinalIgnoreCase))
{
targetHeart2 = ParseColorObject(entry.Value);
hasUpdate = true;
}
else if (key.Equals("NUM_NINE", StringComparison.OrdinalIgnoreCase))
{
targetHeart3 = ParseColorObject(entry.Value);
hasUpdate = true;
}
}

获取到颜色数据,只需要使用HidD_SetFeature函数发送数据即可.

1
2
3
4
private static extern bool HidD_SetFeature(
SafeFileHandle HidDeviceObject,
byte[] ReportBuffer,
uint ReportBufferLength);

下位机程序

当初选择CH592F有大原因就是有很多人用它做键盘, 而且官方的例子中就有一个基于蓝牙的HID_Keyboard example.

在构思本项目时候, 就像好了要伪装成为一个HID设备, 再和电脑走HID进行通信. 这个方案不用考虑串口驱动或者其他USB驱动的麻烦事情了.

驱动WS2812B

具体可以参考WCH官方实现, 巧妙地使用了SPI来模拟WS2812时序,这里不做赘述.

HID命令解析

这部分和上位机反着处理即可, 在hidEmuRptCB函数中, 处理HID_DEV_OPER_WRITE的情况,然后区分是否是我们需要关注的事件.

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
// 解析自定义的HID灯光协议
HidCommandPacket *packet = reinterpret_cast<HidCommandPacket *> (pData);
// USB_Printf(" -> Received Command ID: %d\n", packet->cmd);
switch (static_cast<HidCommand> (packet->cmd)) {

case HidCommand::ShowLEDs: {
extern NeoPixelController *g_pLeds;
if (g_pLeds) {
g_pLeds->show();
}
break;
}
case HidCommand::SetHeartColor: {
uint8_t heart_id = packet->payload.setHeart.heart_id;
uint8_t r = packet->payload.setHeart.r;
uint8_t g = packet->payload.setHeart.g;
uint8_t b = packet->payload.setHeart.b;
extern NeoPixelController *g_pLeds;
if (g_pLeds) {
switch (heart_id) {
case 0:
g_pLeds->heart1 (r << 16 | g << 8 | b);
break;
case 1:
g_pLeds->heart2 (r << 16 | g << 8 | b);
break;
case 2:
g_pLeds->heart3 (r << 16 | g << 8 | b);
break;
default:
// 无效的 heart_id,忽略
break;
}
}
break;
}

case HidCommand::PlayBeep:
case HidCommand::BuzzerOff:
case HidCommand::SetSingleLED:
case HidCommand::SetAllLEDs: {
// 省略其余具体实现
break;
}
default:
// 收到未知命令
break;
}

然后享受Aurora对大量游戏的支持,以及丰富的灯光配置.
aurora_add_script_device

aurora_cs2_profile

视频

如何从零开始:用 CH592F制作CS2生命值胸章并实现和游戏联动效果

https://chaosgoo.com/2025/10/29/Real-life-HealthPin/

作者

Chaos Goo

发布于

2025-10-29

更新于

2025-11-17

许可协议