Zapic's Blog
DIY一个给电脑显示器用的重力感应屏幕旋转装置
2022-04-29

买了个能转的显示器当副屏, 然后因为买的太便宜, 没有带旋转感应的(不知道别的有没有).
然后觉得好像不大方便, 就想着写一个脚本, 能够一键切换屏幕旋转状态.
最后配合 MultiMonitorTool 的加载配置文件功能实现了一键切换布局.
我找到了内心的平静.

直到我发现, 这实在是太蠢了.
我每次都要找到脚本在哪, 再双击他切换布局.

于是我找了一个右键菜单编辑器, 把这个脚本放进了桌面的右键菜单里.
然后在桌面右键点击两次就能切换旋转状态.
我找到了内心的平静.

直到我发现, 这实在是太蠢了.
为什么不能直接根据实际状态旋转呢?

(x

DIY 这样一个东西, 大约需要 30 来块钱.
我买了一块已经焊好 USB 的 CH341A 和一块 MMA8452Q 模块, 某宝大约二十拿下.
然后再买了一些很长的杜邦线(因为要把 CH341 藏到桌子下面), 这样一共就花了三十多.

把 CH341 调成 3.3v 输出和 I2C 模式, 接上 MMA8452 的 4 根 I2C 针脚.
然后用装上 CH341 的 I2C 模式驱动:驱动
从里面找到CH341DLL.DLL, 之后要用.
使用测试工具验证连接是否正确, 驱动是否安装成功: CH341 测试工具
找到压缩包内的CH341PAR\VC\CH341PAR.EXE, 然后再把之前从驱动压缩包里翻出来的CH341DLL.DLL放在一起.
运行CH341PAR.EXE, 切换到 EEPROM 配置.
按照文档说明, 只接四根线的话, MMA8452 的设备地址应该是0x1c: MMA8452 文档
然后在0x0d上能够读取的 MMA8452 的 WHO\_AM\_I 信息: 0x2a
到这里硬件部分算是连接成功了.

然后编写代码轮询传感器, 获取屏幕旋转状态并通过 MultiMonitorTool 自动切换布局.
MultiMonitorTool
从刚刚的 CH341 测试工具包里可以找到CH341DLL.hCH341DLL.lib
代码仅供参考, 我一个写 JS 的, 写出来的玩意能跑就行.

#include <windows.h>
#include <stdio.h>
#include <signal.h>

#include "CH341DLL.H"

#define DEVICE_ADDR 0x1c // MMA8452 设备地址
#define DEVICE_ID 0 // CH341 设备编号

#define getbit(x, y)   ((x) >> (y)&1)

// 检查设备状态并自动初始化重连
int CheckDevice(int DeviceId) {
    UCHAR read = 0;
    // 尝试读取 MMA8452 的 WHO_AM_I 信息
    if (!CH341ReadI2C(DeviceId, DEVICE_ADDR, 0x0d, &read)) {
        // 如果读不到, 尝试重新打开 CH341, 并再次尝试读取 WHO_AM_I 信息
        CH341CloseDevice(DeviceId);
        if (CH341OpenDevice(DeviceId) == INVALID_HANDLE_VALUE || !CH341ReadI2C(DeviceId, DEVICE_ADDR, 0x0d, &read)) {
            printf("Failed to open device #%d.\n", DeviceId);
            return 0;
        } else {
            printf("Opened device #%d.\n", DeviceId);
        }
    }
    // 根据读到的 WHO_AM_I 信息检查模块是否为 MMA8452, 如果没连接模块应该读到 0xFF
    if (read != 0x2a) {
        puts("Invalid device WHO_AM_I response.");
        return 0;
    }
    // 读取模块状态
    if (!CH341ReadI2C(DeviceId, DEVICE_ADDR, 0x2a, &read)) {
        printf("Device #%d closed unexpected.\n", DeviceId);
        return 0;
    }
    // 如果模块没有被启用
    if (read != 0x01) {
        if(
            !CH341WriteI2C(DeviceId, DEVICE_ADDR, 0x2a, 0b00000000) // 禁用模块(必须禁用掉模块才能调整模块设置)
            || !CH341WriteI2C(DeviceId, DEVICE_ADDR, 0x11, 0b11000000) // 启用旋转方向检测
            || !CH341WriteI2C(DeviceId, DEVICE_ADDR, 0x2a, 0b00000001) // 启用模块
            || (!CH341ReadI2C(DeviceId, DEVICE_ADDR, 0x2a, &read) || read != 0x01) // 检查模块是否被正确启用
        ) {
            printf("Failed to initialize device #%d.\n", DeviceId);
            return 0;
        } else {
            printf("Device #%d initialized successfully.\n", DeviceId);
        }
    }
    return 1;
}

int main() {
    UCHAR mBuffer = 0;
    UCHAR Last = 255;
    UCHAR Curr = 255;
    if (LoadLibrary("CH341DLL.DLL") == NULL) {
        puts("Cannot load required Dynamic Library CH341DLL.DLL.");
        return -1;
    }
    while (1) {
        Sleep(1000);
        if (!CheckDevice(DEVICE_ID)) {
            continue;
        }
        // 读取模块旋转状态
        if (!CH341ReadI2C(0, DEVICE_ADDR, 0x10, &mBuffer) || mBuffer == 0xff) {
            puts("Device disconnected unexpected.");
            continue;
        }
        // 取出数据
        // 我的显示器只能旋转 90 度, 于是偷懒只取出了表示横屏还是竖屏的数据位
        // 如果有需要, 可以根据文档取得更多数据
        // 此数据第 2 位表示横屏竖屏
        // 第 1 位表示旋转方向
        // 这两位结合可以判断屏幕的四向旋转
        // 具体请参考 MMA8452 文档对于 0x10 数据的解释
        Curr = getbit(mBuffer, 2);
        if (Curr != Last) {
            if (Curr == 0) {
                system("MultiMonitorTool.exe /LoadConfig landscape.cfg"); // 用 MMT 加载预先写好的显示器布局配置文件
            } else {
                system("MultiMonitorTool.exe /LoadConfig portrait.cfg");
            }
        }
        Last = Curr;
    }
}

代码必须使用 VC 去编译, 因为 CH341DLL.dll 就是用的 VC, mingw 弄半天发现跑不起来就是这个操蛋原因.
把编译出来的东西跟 MultiMonitorTool 还有 CH341DLL.dll 放在一起, 就能使用了.
编译的成品有需要可以分享出来, 但是写了这么久还是有点懒了, 晚点再传吧.

把模块粘在显示器背后, 再把这个轮询程序放到后台, 非常完美.
纯 C 编写的工具占用也非常小, 挂后台 0.3M 的内存占用, 基本不吃米.