这篇文章上次修改于 695 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

使用加速度计传感器,通过编写服务程序不断监听重力方向,当重力方向改变时自动修改系统设置,旋转界面于重力相匹配。

背景

手中一台Dell显示器具有旋转功能,正常情况下以横屏方式使用,当阅读代码或某些项目要求可以旋转以竖屏方式使用。但当旋转后屏幕显示并不能自动旋转,还需要手动修改系统显示设置,但由于界面旋转后操控与大脑认知不同,调整很不方便,故考虑可以增加重力感应传感器配合运行程序方式实现自动旋转,类似于手机的重力感应功能。

加速度传感器

经过筛选,选用了GY-25T加速度+陀螺仪模块。以下内容来自淘宝站点介绍。

GY-25T是一款低成本数字角度传感器模块。工作电压 3-5v,功耗小,体积小,安装方便。

其工作原理是通过陀螺仪与加速度传感器经过数据融合算法最后得到直接的角度数据。

此模块有两种方式读取数据:即串口 UART(TTL电平)和IIC(2线)模式。串口的波特率支持2400bps 至230400bps配置,支持连续和询问两种输出方式,支持掉电保存设置。

GY-25T可以设置模块倾斜度报警临界值,并以开关量输出。当模块倾斜角度超过临界值时,OUT引脚输出高电平。IIC模式下,可以设置不同IIC地址,以便多个传感器共用同一总线通信。
GY-25T支持航向角滤波设置,可通过参数抑制漂移。

模块:
gy-25t_usb.png
GY-25T与USB-TTL接线(也可根据USBTTL上的标识直接将两个焊接在一起):
connect.png

本次改造仅使用了GY-25T的加速度计功能,未使用到陀螺仪及处理后的角度数据,所以改造后仅支持检测显示器竖直状态下的旋转,水平状态无法实现(可以利用陀螺仪等实现对水平旋转动作的检测及处理,但由于本人无此使用场景,故未做实现)。这也导致了模块功能的浪费,仅一个加速度传感器的成本相比于陀螺仪+加速度传感器还是低很多的。

选用GY-25T的另一个原因是它支持TTL电平输出,对于没有GPIO接口的PC来说直接使用一个 USB转TTL模块即可实现通信,且本人购买的网店直接可以买到规格匹配的两种元件。到手后仅需使用杜邦线或直接将两个模块焊接在一起即可。

系统服务开发

服务使用了Qt框架,使用Qt可以方便实现界面功能和接操作GY-25T传感器。

传感器设置

通过QSerialPort对GY-25T的数据收发。在连接传感器后,设置传感器初始参数并保存,这一步主要是为例防止传感器被其他软件修改了配置,使服务无法获取到正确的数据,(服务通讯文档可参见GY-25T文档)。主要设置参数如下:

// set mode
QByteArray mode = QByteArray::fromHex("00 06 07 53 60"); //水平模式、加速度量程等
ret = writeSync(mode);  // WriteSync主要实现了同步写功能,写入后会等待反馈
qDebug() << "set mode ret:" << ret;

QByteArray rate = QByteArray::fromHex("00 06 02 01 09");   //自动发送数据频率设置为每秒50次
ret = writeSync(rate);
qDebug() << "set 10hz ret:" << ret;

// read register setting
QByteArray rreg = QByteArray::fromHex("00 03 08 06 11 ");   //设置读取及自动发送的传感器信息:这里为发送3轴的加速度数据
ret = writeSync(rreg);
qDebug() << "set reading register ret:" << ret;

QByteArray refresh = QByteArray::fromHex("00 06 03 00 09"); //设置自动发送模式,无需手动读取
ret = writeSync(refresh);
qDebug() << "set reading auto" << ret;

// save settings
QByteArray save = QByteArray::fromHex("00 06 05 55 60");  //保存上述设置
ret = writeSync(save);
qDebug() << "save settings ret:" << ret;

计算重力方向

参数设置成功后,模块会自动将加速度数据发给服务,服务对角度进行简单的处理。为防止漂移发生,这里通过计算最近20组数据平均值作为当前重力方向。并且在检测到重力方向改变稳定后(连续20次数据方向相同)发送方向改变的Qt信号。

bool GY_25T_TTL::handleBuffer_Acc(QByteArray& buf)
{
    /* A4 03 08 06 FE E0 05 EF 4C 73 46
    //   A4: frame header ID
    //   03: func code - read
    //   08: start register
    //   06: registers count
    //   FE E0: acc_x(h l)
    //   05 EF: acc_y(h l)
    //   4C 73: acc_z(h l)
    //   46: checksum(l)
    //*/

    if ((quint8)buf[2] != 0x08) {
        //qDebug() << __FUNCTION__ << "start register invalid";
        return false;
    }
    if ((quint8)buf[3] != 0x06) {
        //qDebug() << __FUNCTION__ << "registers count invalid";
        return false;
    }

    emit ready(true);

    quint16 ux = (quint8)buf[4] << 8 | (quint8)buf[5];
    quint16 uy = (quint8)buf[6] << 8 | (quint8)buf[7];
    quint16 uz = (quint8)buf[8] << 8 | (quint8)buf[9];

    qint16 ix = ((qint16)ux / 100);
    qint16 iy = ((qint16)uy / 100);
    qint16 iz = ((qint16)uz / 100);

    //qDebug() << __FUNCTION__ << "acc_x:" << ix;
    //qDebug() << __FUNCTION__ << "acc_x:" << iy;
    //qDebug() << __FUNCTION__ << "acc_x:" << iz;

    if (m_iAccCount < ACC_WINDOW) {
        m_iAccCount++;
    }

    m_iAccIndex = (m_iAccIndex + 1) % ACC_WINDOW;
    m_aAcc[0][m_iAccIndex] = ix;
    m_aAcc[1][m_iAccIndex] = iy;
    m_aAcc[2][m_iAccIndex] = iz;

    average_acc(); //计算平均值

    //qDebug() << __FUNCTION__ << "avg_acc_x:"
    //         << m_aAcc[0][ACC_WINDOW] << m_aAcc[0][ACC_WINDOW + 1];
    //qDebug() << __FUNCTION__ << "avg_acc_x:"
    //         << m_aAcc[1][ACC_WINDOW] << m_aAcc[1][ACC_WINDOW + 1];
    //qDebug() << __FUNCTION__ << "avg_acc_x:"
    //         << m_aAcc[2][ACC_WINDOW] << m_aAcc[2][ACC_WINDOW + 1];

    quint32 key = m_aAcc[0][ACC_WINDOW + 1] << 16 | (m_aAcc[1][ACC_WINDOW + 1]) << 8 | m_aAcc[2][ACC_WINDOW + 1];
    auto it = m_mpCount.find(key);
    if (it == m_mpCount.end()) {
        m_mpCount.clear();
        m_mpCount[key] = 1;
    }
    else {
        if (it.value() == 20) { //稳定后(20次方向未变化)发送已发生旋转信号
            Rotate r = getRotate(); //获取当前重力方向(见下文)
            if (r != ACC_UNKNOWN) {
                emit rotated(r);
            }
        }
        if (it.value() < 100) {
            m_mpCount[key] = it.value() + 1;
        }
    }

    return true;
}

在测试时发现,X轴的数据存在方向不准确情况,主要表现为稍快速转动和慢速转动在某一姿态输出不同现象。所以在判断当前方向时,先对Y轴数据进行判断,Y轴数据无法区分时再使用X轴数据。

GY_25T_TTL::Rotate GY_25T_TTL::getRotate()
{
    if (!isOpen())
        return ACC_UNKNOWN;

    int x = m_aAcc[0][ACC_WINDOW + 1];//m_aAcc[*][ACC_WINDOW + 1]: 对3轴加速度归一化线性变化后的结果
    int y = m_aAcc[1][ACC_WINDOW + 1];
    int z = m_aAcc[2][ACC_WINDOW + 1];

    QString xyz = QString("%1 %2 %3")
        .arg(x).arg(y).arg(z);
    qDebug().noquote() << "changed direction." << xyz;

    if (y == 1) {
        return ACC_RIGHT;
    }
    if (y == 0) {
        //return ACC_DOWN;
    }
    if (y == -1) {
        return ACC_LEFT;
    }
    if (x == 0) {
        return ACC_UP;
    }
    if (x == 2) {
        return ACC_DOWN;
    }
    return ACC_UNKNOWN;
}

屏幕旋转

在得到当前重力方向后,即可据此修改系统设置。主要使用到的系统接口 EnumDisplaySettingsExChangeDisplaySettingsEx等,其中EnumDisplaySettingsEx 用于获取当前系统配置,ChangeDisplaySettingsEx 用于实现修改设置。
获取当前配置:

DEVMODE devMode;
ZeroMemory(&devMode, sizeof(DEVMODE));
devMode.dmSize = sizeof(devMode);
int result = EnumDisplaySettingsEx(NULL, ENUM_CURRENT_SETTINGS, &devMode, NULL);
if (result == 0) {
    m_sErrinf = "EnumDisplaySettingsEx failed";
return false;
}

DEVMODE 结构包含了显示参数情况,主要用到的成员有:
dmDisplayOrientation:旋转方向
dmPelsWidth: 宽度
dmPelsHeight: 高度

实现显示旋转:

switch (r) // r: 需要设置的方向
{
case ROTATE_DEFAULT:
    devMode.dmDisplayOrientation = DMDO_DEFAULT;
    devMode.dmPelsWidth = m_defSzie.width();
    devMode.dmPelsHeight = m_defSzie.height();
    break;
case ROTATE_90:
    /* Rotate Orientation - 90 */
    devMode.dmDisplayOrientation = DMDO_90;
    swap(devMode.dmPelsHeight, devMode.dmPelsWidth);
    devMode.dmPelsWidth = m_defSzie.height();
    devMode.dmPelsHeight = m_defSzie.width();
    break;
case ROTATE_180:
    /* Rotate Orientation - 180 */
    devMode.dmDisplayOrientation = DMDO_180;
    devMode.dmPelsWidth = m_defSzie.width();
    devMode.dmPelsHeight = m_defSzie.height();
    break;
case ROTATE_270:
    /* Rotate Orientation - 270 */
    devMode.dmDisplayOrientation = DMDO_270;
    swap(devMode.dmPelsHeight, devMode.dmPelsWidth);
    devMode.dmPelsWidth = m_defSzie.height();
    devMode.dmPelsHeight = m_defSzie.width();
    break;
default:
    m_sErrinf = QString(__FUNCTION__) + ": parameter invalid.";
    return false;
}
if (oldOri == devMode.dmDisplayOrientation) {// 当前方向即为设置的方向,直接返回(可防止界面闪烁)
    return true;
}

devMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYORIENTATION;
ret = ChangeDisplaySettingsEx(NULL, &devMode, NULL, CDS_RESET, NULL);

多屏幕支持

多显示器时实现对指定显示器的操作仅需要修改EnumDisplaySettingsExChangeDisplaySettingsEx的第1个参数为设置名即可。此设备名可先通过 EnumDisplaySettingsEx 对设备进行遍历获得。

QString RotateDisp::getMonitorName(int index)
{
    QString ret = "";

    DISPLAY_DEVICE displayDevice;
    displayDevice.cb = sizeof(DISPLAY_DEVICE);
    int result = EnumDisplayDevices(NULL, index, &displayDevice, 0);
    if (result != 0 && (displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE)) {
        PDISPLAY_DEVICE monitor = new DISPLAY_DEVICE();
        monitor->cb = sizeof(DISPLAY_DEVICE);
        EnumDisplayDevices(displayDevice.DeviceName, 0, monitor, 0);
        delete monitor;

        ret = QString::fromWCharArray(displayDevice.DeviceName);
    }

    return ret;
}

效果

系统页面

RotateDisp.png

功能演示

源码下载

https://github.com/doufu3344/RotateDisp