Switch模拟按键技术-考察与实现(一)
背景
个别switch游戏实在是不把玩家当人,肝到手酸甚至闭着眼睛一边睡一遍刷的情况也经常在我身上发生,而且一直按也会对我那昂贵的JoyCon造成不可逆的损伤。那么,一个能够模拟switch按键并且自动化执行的工具就显得尤为重要了。
本文是这个系列的第一篇文章,到文末能够实现的功能是:可以通过Arduino把既定的按键序,循环地发送到Switch上执行。
通过蓝牙直接模拟JoyCon手柄
比较熟悉虚拟机的话这种方式是成本最低的,使用VMware开个虚拟机,连接实体机的蓝牙功能即可。但我还是选择使用树莓派,一是能够脱离计算机运行,二是不用在拥挤的硬盘上再扣出一个Ubuntu系统的空间。
在Github上能够找到的技术实现,比较热门的就两个,一个是mart1nro/joycontrol,截至今天12月30日,它已经至少13个月没有更新,虽然Issue里还有比较新的留言,但显然作者已经没有再专注这件事。而在今年上半年我刚买Switch的时候尝试过这个程序,虽然连接不稳定,但还是可以用,不过在游戏机系统更新至12.0.0以上后就失效了。从Issue来看,有人分析出是Switch更新系统后在保持与手柄的蓝牙连接的同时会去动态地修改发送频率。。。感觉是完全的被针对了。
另一个是Brikwerk/nxbt,截止今日两个月没更新,还是比较活跃的,但是同样没有在我的Switch上运行成功,Issue里也有人提到版本的问题,但也有人折腾成功的。。鉴于作者没有对这块儿进行可靠的说明,遂放弃。
USB模拟手柄
用蓝牙模拟JoyCon手柄这条路虽然肯定有办法做到,但我选择放弃。转而考虑使用USB模拟手柄,正好手头也有Arduino。
需求分析
- 能够通过Arduino连接Switch,能够发送按键信号
- 能够通过读取配置文件(如JSON),自动化按序执行按键
扩展需求:
- 能够与上位机通信,动态修改按键策略
- 使用上位机(树莓派),通过视觉获取Switch上的信息,并改变策略(比如牧场物语重聚矿石镇,自动按Y键积累农具熟练度,然后用视觉来获取左上角的体力值状态,在体力值低下时执行按键序吃东西来补充)
硬件需求
Switch游戏机本体,Arduino开发板(德国原装128元,国产25-30元,用着没区别),USB—Type-C转换头
额外:树莓派(作为上位机),摄像头,按钮模块
技术难点
- 需要用Arduino伪装成手柄,本文基于开源项目Switch-Fightstick,用Flip软件进行烧写
- 要求让程序能够读取配置文件,但Arduino上无法直接上传文件,需要考虑使用自带的EEPROM来进行数据的持久化存储
- 视觉方面考虑使用YOLO进行fine-tune。先找到游戏画面并抠出来,然后找到左上角的表情和体力值。中间可以加入图片畸变调整。
开始折腾
国内关于这方面,做的人似乎特别少,翻来覆去只找到两个看着有点帮助的,一是https://www.dazhuanlan.com/lol86277496/topics/1271709,这篇文章顺着他思路做完是可以成功运行的,文中对于Arduino固件写入的部分说明得也很详细,但他是直接提供了可执行文件,没有告诉读者修改哪部分代码,也没有关于编译的过程。实际上对于硬件折腾地较少的人来说,编译确实有很大的难度。这篇文章的程序拿来运行后能达到的效果也只是循环地自动按A键。
二是https://blog.csdn.net/gurncdsn/article/details/104620200,这篇就比较庞大了,用Arduino实现自动玩音游太鼓达人,不过其中不小的篇幅都在讲关于游戏中歌曲的问题,对于我感兴趣的编译则又是一笔带过了。
程序改写
在初步的需求分析之后,我预想这个步骤会是比较简单的,然而事实并非如此。。
对于硬件经验较少的伙伴们来说, Switch-Fightstick 的代码还是比较难懂的,而且他的README中也没有对程序结构进行一个大体的说明,用到的诸多16进制常量也让我头昏脑涨;此时我想到的是先编译运行看一下结果,再从结果分析它的功能,然而当我把烧录好的板子插到Switch上之后,竟然是,没有任何反应。
好在GitHub上还很多人对 Switch-Fightstick 进行了不同的改进,创建了许多分支,但我尝试了许多,大部分都是无法直接使用的, shinyquagsire23/Switch-Fightstick 是基于 Switch-Fightstick 实现了在《喷射战士》中进行自动的涂鸦绘制;ebith/Switch-Fightstick 实现了通过计算机的串口向开发板发送指令来实时操作Switch,这可以说是我最想实现的功能,因为这么做之后开发板上的工作就结束了,不会再有代码改写和编译的工作,接下来随便写点Python来循环发送指令就能达成目的,修改指令序列或者创建新的序列只要改python就行,十分方便。不过上述的代码都没有实践成功。
最后成功的是 bertrandom/snowball-thrower,这位作者的代码十分友好,对 Switch-Fightstick 进行了封装,把按键动作和延时动作都放在了一个数组里然后循环执行:
static const command step[] = {
// Setup controller
{ NOTHING, 250 },
{ TRIGGERS, 5 },
{ NOTHING, 150 },
{ TRIGGERS, 5 },
{ NOTHING, 150 },
{ A, 5 },
{ NOTHING, 250 },
// Talk to Pondo
{ A, 5 }, // Start
{ NOTHING, 30 },
{ B, 5 }, // Quick output of text
{ NOTHING, 20 }, // Halloo, kiddums!
………………………..
这就十分的快乐了,我们再也不需要去理解代码的实现逻辑,只要修改这个数组即可。
不过这个项目的一点点不足之处是它在 GetNextReport()函数中进行的按键映射居然是不完整的。。缺少了好几个键位,导致我在修改上文数组时加入的Y键一直没起作用。代码如下,大家把代码pull下来之后记得自己添加一下缺少的键位映射。

程序编译
编写完代码之后我们需要对程序进行编译,Switch-Fightstick的MakeFile使用的是avr编译环境,我首先尝试了windows系统,遂按照某博客的思路下载WinAVR-20100110。简单安装完以后配置一下系统环境变量即可。
接下来就可以进行make了,我第一个遇到的问题是:
PS I:\Projects\ArduinoProjs\JoyConSimulate\Switch-Fightstick> make
makefile:29: LUFA/Build/lufa_core.mk: No such file or directory
makefile:30: LUFA/Build/lufa_sources.mk: No such file or directory
makefile:31: LUFA/Build/lufa_build.mk: No such file or directory
makefile:32: LUFA/Build/lufa_cppcheck.mk: No such file or directory
makefile:33: LUFA/Build/lufa_doxygen.mk: No such file or directory
makefile:34: LUFA/Build/lufa_dfu.mk: No such file or directory
makefile:35: LUFA/Build/lufa_hid.mk: No such file or directory
makefile:36: LUFA/Build/lufa_avrdude.mk: No such file or directory
makefile:37: LUFA/Build/lufa_atprogram.mk: No such file or directory
make: *** No rule to make target `LUFA/Build/lufa_atprogram.mk’. Stop.
显然是缺了什么东西,一看我连./LUFA这整个文件夹都没有。。原来 Switch-Fightstick 这个项目是基于LUFA这个开源框架的,它用于对AVR芯片进行USB终端设备的开发,在本文中,我们就是用它来把Arduino伪装成一个USB设备的。那么,我们还需要去Github下载一个LUFA。下载后将其目录下的LUFA子文件夹复制到 Switch-Fightstick 的目录下即可。
天不遂人愿,再次make又出了如下错误:
avr-objcopy -O ihex -j .eeprom –set-section-flags=.eeprom=”alloc,load” –change-section-lma .eeprom=0 –no-change-warnings Joystick.elf Joystick.eep || exit 0
0 [main] sh 52856 sync_with_child: child 48540(0x1D8) died before initialization with status code 0xC0000142
336 [main] sh 52856 sync_with_child: *** child state waiting for longjmp
/usr/bin/sh: fork: Resource temporarily unavailable
make: *** [Joystick.eep] Error 128
嗯。。从描述来看是某个必要的子进程在初始化之前就死亡了。大家都知道扒一份开源代码是一件非常痛苦的事,因此我想试试换条路走,如果还是不通,再来把代码解决这个error也不迟。。
于是我改用Linux,由于只需要完成编译工作,不做烧写,所以不管用虚拟机还是云服务器都是可以的。在手头没有Linux实体机的情况下当然是虚拟机的性价比最高,不过我本来就一直都在续费一台每月十元的云服务器,而且在需要进行多次实验的情况下,相比于维持一个ssh连接,一直开着一台虚拟机的性能代价还是比较大的。
把包含了LUFA的 Switch-Fightstick 整个项目拷贝到虚拟机中,然后执行以下命令下载一些软件:
sudo apt-get install gdb-avr simulavr avrdude avr-libc
我安装时少了avr-libc,这会导致make时找不到库文件。
安装结束后进入项目目录,执行make命令,不出意外的话会出现以下文为末尾的提示:
[INFO] : Finished building project “Joystick”.
此时编译就算是成功了,当前目录下生成的Joystick.hex就是我们需要的可执行文件。
固件烧写
安装JRE-Flip-Installer.exe,记住安装位置。安装好之后,把 Arduino 开发板拔下来重插,然后找一个金属物品,剪刀或者钥匙什么的,用金属物体短接(就是短暂接通)下图所示板子上离 USB 接口最近的两个插针。

如果接通成功,板子灯会闪,电脑也会有设备拔出和重新插入的提示音,这时候Arduino即进入 DFU 模式,可以写固件了。
这时候,在我的电脑右击,点击管理->设备管理器,应该会发现一个打着感叹号的未知设备,在这个设备上右击,点击更新驱动程序,选择浏览计算机以查找驱动程序软件,然后选择刚才安装Flip的文件夹Flip 3.4.7usb并勾选上包括子文件夹,点击下一步,如果没问题的话就 OK 了。
接下来再把板子拔下来重插,然后再次短接那两个插针,这时候电脑有提示音设备插入,设备管理器也没有未知设备了。打开安装的 Flip 软件,按照图示选择ATmega16U2

然后,点击数据线按钮并选择USB,出现提示框,点击Open后板子会闪,如果没有提示错误,那么就连接成功。
接下来点击File,Load Hex File,在弹出的文件浏览器中选择固件Joystick.hex文件,注意这个 hex 文件应该放置在纯英文目录下。
然后勾选这 4 项,点击Run,然后会弹出进度条,速度很快几秒钟,那四个选项前的灰点变绿,说明一切 OK 至此开发板的准备已经完成。

到这一步,Arduino已经伪装成了一个Switch手柄,使用USB转换器将它插到Switch的TypeC口就会自动执行 Joystick.hex 中的程序了。
固件还原
与上述烧录步骤相同,只是使用的hex文件替换成Arduino-usbserial-atmega16u2-Uno-Rev3.hex。
总结
到此,整个流程就结束了,当然目前仅仅是能够自定义按键序然后循环执行而已,而且每次修改也需要编译,过程繁琐。我会考虑在后续工作中实现用串口通信来实时向Arduino发送指令的功能,这样不管是调试还是编写都会方便很多。
另外通过视觉来获取Switch信息并执行相应操作的功能,相关工作由于比较庞大,还是得慢慢来,而且我今天想到使用视频采集卡也是一条路,比起用摄像头显然具有更高的稳定性和低时延,淘宝上最便宜的只要80元,值得尝试。