mirror of https://github.com/nirenjan/libx52.git
Compare commits
51 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
75f0125f54 | |
|
|
f52e328a8b | |
|
|
806a88c93d | |
|
|
c0c8787331 | |
|
|
de465fbf6a | |
|
|
583d4fd646 | |
|
|
836206d93f | |
|
|
9c1eaaa4b2 | |
|
|
9c47e78fc5 | |
|
|
7bdaea442e | |
|
|
8b139a05c4 | |
|
|
c49689c1ee | |
|
|
0fdcb725af | |
|
|
03d58c62e8 | |
|
|
991a307191 | |
|
|
6ec133488b | |
|
|
667e8e2a7b | |
|
|
357ea96676 | |
|
|
6a36fc7764 | |
|
|
283b476c5e | |
|
|
75e6f253c9 | |
|
|
230951a232 | |
|
|
c99f775b70 | |
|
|
8914184613 | |
|
|
fdafda1d34 | |
|
|
9501813c36 | |
|
|
aa555f5e66 | |
|
|
842e7e53ed | |
|
|
dfbe3e6d21 | |
|
|
732bc21b65 | |
|
|
3c1abd57d5 | |
|
|
b0b457d14e | |
|
|
ae077dbed8 | |
|
|
d29be6213f | |
|
|
273ed22f8e | |
|
|
cdb00739ca | |
|
|
08a6b0a736 | |
|
|
45d561e0d8 | |
|
|
b626a9367f | |
|
|
a1098bc134 | |
|
|
569902be76 | |
|
|
0cb137bbe0 | |
|
|
899ea57bf7 | |
|
|
e1e020a4f5 | |
|
|
7cbf091dc7 | |
|
|
9d180531b9 | |
|
|
74229b391d | |
|
|
5f8177f16b | |
|
|
c5ec15231f | |
|
|
33bbafe970 | |
|
|
e9a806a6a2 |
|
|
@ -2,10 +2,20 @@
|
|||
# Run the build and tests
|
||||
set -e
|
||||
|
||||
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled build
|
||||
cd build
|
||||
ninja
|
||||
ninja test
|
||||
BUILDDIR="${1:-build}"
|
||||
|
||||
rm -rf "$BUILDDIR"
|
||||
|
||||
# Handle the meson dist failure in CI
|
||||
if [[ "$GITHUB_ACTIONS" == "true" ]]
|
||||
then
|
||||
git config --global --add safe.directory '*'
|
||||
fi
|
||||
|
||||
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled "$BUILDDIR"
|
||||
cd "$BUILDDIR"
|
||||
meson compile
|
||||
meson test
|
||||
meson dist
|
||||
|
||||
# Print bugreport output
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate the list of distros as a build matrix"""
|
||||
|
||||
import pathlib
|
||||
import json
|
||||
import sys
|
||||
|
||||
class BuildMatrix:
|
||||
"""Generate a build matrix for Github actions"""
|
||||
|
||||
COMPILER = 'gcc'
|
||||
OS = 'ubuntu-latest'
|
||||
EXPERIMENTAL = False
|
||||
|
||||
def __init__(self, image_prefix):
|
||||
self.matrix = []
|
||||
self.image_prefix = image_prefix
|
||||
self.get_distros()
|
||||
self.add_extra_builds()
|
||||
self.generate_output()
|
||||
|
||||
def build_matrix_obj(self, distro, experimental, os=None, compiler=None, image=None):
|
||||
"""Build the matrix object for the given distro"""
|
||||
matrix_obj = {
|
||||
'distro': distro,
|
||||
'experimental': experimental,
|
||||
'os': os or self.OS,
|
||||
'compiler': compiler or self.COMPILER,
|
||||
}
|
||||
|
||||
if image is None:
|
||||
image = f"{self.image_prefix}/ci-build-{distro}:latest"
|
||||
|
||||
matrix_obj['image'] = image
|
||||
|
||||
return matrix_obj
|
||||
|
||||
def get_distros(self):
|
||||
"""Get the list of distros from the Dockerfiles"""
|
||||
for dockerfile in pathlib.Path('.').glob('docker/Dockerfile.*'):
|
||||
distro = dockerfile.suffix[1:]
|
||||
|
||||
with open(dockerfile, encoding='utf-8') as dfd:
|
||||
experimental = 'experimental="true"' in dfd.read()
|
||||
|
||||
self.matrix.append(self.build_matrix_obj(distro, experimental))
|
||||
|
||||
def add_extra_builds(self):
|
||||
"""Add manual canary builds that don't have a corresponding dockerfile"""
|
||||
canary_build = self.build_matrix_obj('ubuntu22', False, compiler='clang')
|
||||
self.matrix.append(canary_build)
|
||||
|
||||
macos_build = self.build_matrix_obj('macos', True, os='macos-latest',
|
||||
compiler='clang', image='')
|
||||
self.matrix.append(macos_build)
|
||||
|
||||
def generate_output(self):
|
||||
"""Generate the output for github actions"""
|
||||
matrix_data = json.dumps(self.matrix)
|
||||
print(f"matrix={matrix_data}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
BuildMatrix(sys.argv[1])
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
# Get the list of changed Dockerfiles
|
||||
# Usage: get-changed-dockerfiles.sh <before-SHA> <head-SHA>
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
mapfile -t ALL_DOCKERFILES < <(git ls-files 'docker/Dockerfile.*')
|
||||
if [[ -n "${BUILD_DOCKER_MANUAL_ENV:-}" ]]
|
||||
then
|
||||
|
||||
if [[ "${BUILD_DOCKER_MANUAL_ENV}" == "ALL" ]]
|
||||
then
|
||||
DOCKERFILES=("${ALL_DOCKERFILES[@]}")
|
||||
else
|
||||
BUILD_LIST=($BUILD_DOCKER_MANUAL_ENV)
|
||||
DOCKERFILES=()
|
||||
for item in "${BUILD_LIST[@]}"
|
||||
do
|
||||
if [[ -f "docker/Dockerfile.${item}" ]]
|
||||
then
|
||||
DOCKERFILES+=("$item")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR HEAD^..HEAD)
|
||||
|
||||
mapfile -t DOCKERFILES < <(echo "$CHANGED_FILES" | grep 'docker/Dockerfile')
|
||||
mapfile -t SCRIPT_CHANGES < <(echo "$CHANGED_FILES" | grep 'docker/' | grep '\.sh$')
|
||||
|
||||
for file in "${SCRIPT_CHANGES[@]}"
|
||||
do
|
||||
for dockerfile in "${ALL_DOCKERFILES[@]}"
|
||||
do
|
||||
if grep -q "$(basename "$file")" "$dockerfile"
|
||||
then
|
||||
DOCKERFILES+=("$dockerfile")
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
echo -n "matrix="
|
||||
echo "${DOCKERFILES[@]}" | \
|
||||
tr ' ' '\n' | sort -u | sed 's,docker/Dockerfile\.,,' | \
|
||||
jq -Rsc 'split("\n") | map(select(length > 0)) | unique'
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
#!/bin/bash -x
|
||||
# Install dependencies to build and test on Ubuntu runners
|
||||
brew install \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
pkg-config \
|
||||
python3 \
|
||||
gettext \
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
# Install dependencies to build and test on Ubuntu runners
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
pkg-config \
|
||||
python3 \
|
||||
gettext \
|
||||
|
|
@ -17,7 +14,6 @@ sudo apt-get install -y \
|
|||
libcmocka-dev \
|
||||
faketime \
|
||||
meson \
|
||||
ninja-build \
|
||||
libinih-dev
|
||||
ninja-build
|
||||
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -10,34 +10,46 @@ on:
|
|||
- '!gh-pages'
|
||||
paths-ignore:
|
||||
- 'kernel_module/**'
|
||||
- 'docker/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "!(contains(github.event.head_commit.message, '[ci skip]') || contains(github.event.head_commit.message, '[skip ci]'))"
|
||||
name: ${{ join(matrix.*, '/') }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ startsWith(matrix.os, 'macos-') }}
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
list-distros:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: set-matrix
|
||||
run: .github/scripts/generate_build_matrix.py ghcr.io/${{ github.repository }} >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: list-distros
|
||||
if: "!(contains(github.event.head_commit.message, '[ci skip]') || contains(github.event.head_commit.message, '[skip ci]'))"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['ubuntu-22.04', 'ubuntu-24.04', 'macos-latest']
|
||||
cc: ['gcc', 'clang']
|
||||
include: ${{ fromJson(needs.list-distros.outputs.matrix) }}
|
||||
|
||||
name: ${{ format('{0}/{1}{2}', matrix.distro, matrix.compiler, matrix.experimental == true && ' (experimental)' || '') }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies (Ubuntu)
|
||||
run: ./.github/scripts/install-dependencies-ubuntu.sh
|
||||
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
|
||||
|
||||
- name: Install dependencies (MacOS)
|
||||
run: ./.github/scripts/install-dependencies-macos.sh
|
||||
if: ${{ startsWith(matrix.os, 'macos-') }}
|
||||
|
||||
- name: Build and Test
|
||||
env:
|
||||
CC: ${{ matrix.compiler }}
|
||||
run: ./.github/scripts/build-and-test.sh
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
name: Code Coverage
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '!gh-pages'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
./.github/scripts/install-dependencies-ubuntu.sh
|
||||
sudo apt-get install -y gcovr
|
||||
|
||||
- name: Configure and Build
|
||||
run: |
|
||||
meson setup build -Db_coverage=true --buildtype=debug
|
||||
meson compile -C build
|
||||
|
||||
- name: Run Tests
|
||||
run: meson test -C build
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: |
|
||||
# This generates the XML report for the upload step
|
||||
ninja -C build coverage-xml
|
||||
|
||||
- name: Upload Report to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
files: buildd/meson-logs/coverage.xml
|
||||
fail_ci_if_error: true
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
name: Build Docker CI Images
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
distros:
|
||||
description: "List distros to build (space separated)"
|
||||
type: string
|
||||
default: "ALL"
|
||||
push:
|
||||
paths:
|
||||
- "docker/Dockerfile.*"
|
||||
- "docker/*.sh"
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_DOCKER_MANUAL_ENV: ${{ inputs.distros || '' }}
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Needed for detecting changes
|
||||
|
||||
- name: Set build matrix
|
||||
id: set-matrix
|
||||
run: .github/scripts/get-changed-dockerfiles.sh >> $GITHUB_OUTPUT
|
||||
|
||||
build-and-push:
|
||||
needs: detect-changes
|
||||
if: ${{ needs.detect-changes.outputs.matrix != '[]' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false # Don't cancel other distros if this one fails
|
||||
matrix:
|
||||
distro: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker
|
||||
file: ./docker/Dockerfile.${{ matrix.distro }}
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}/ci-build-${{ matrix.distro }}:latest
|
||||
|
||||
- name: Cleanup old builds
|
||||
uses: actions/delete-package-versions@v5
|
||||
with:
|
||||
package-name: libx52/ci-build-${{ matrix.distro }}
|
||||
package-type: container
|
||||
delete-only-untagged-versions: true
|
||||
|
|
@ -38,4 +38,4 @@ jobs:
|
|||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ x52test*
|
|||
x52evtest*
|
||||
libx52/test_*
|
||||
libx52test*
|
||||
libx52util/util_char_map.c
|
||||
libx52util/char_map.c
|
||||
udev/*.rules
|
||||
# Built artifacts / local installs named x52d*; daemon sources use short names.
|
||||
# Keep tracked daemon files that still use the x52d prefix (headers, configs).
|
||||
x52d*
|
||||
!daemon/x52d*.*
|
||||
!libx52/x52dcomm.h
|
||||
test-*
|
||||
libx52-*.tar.gz
|
||||
|
||||
|
|
@ -26,6 +29,10 @@ Module.symvers
|
|||
# Vim swap files
|
||||
.*.swp
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Autotools objects
|
||||
.deps
|
||||
.dirstamp
|
||||
|
|
|
|||
16
ChangeLog.md
16
ChangeLog.md
|
|
@ -6,8 +6,24 @@ The format is based upon [Keep a Changelog].
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated CI builds to run in multiple distro containers.
|
||||
- Improved virtual mouse motion to use a smoother approach, as well as allow an isometric speed calculation. This change deprecates the old `Mouse.Speed` configuration option and replaces it with a Sensitivity percentage option.
|
||||
|
||||
### Fixed
|
||||
- Addressed meson build bugs found in v0.3.3
|
||||
|
||||
## [0.3.3] - 2026-03-12
|
||||
|
||||
**Note:** While this release does introduce Meson support and deprecate the
|
||||
Autotools framework, there are several bugs in the Meson build files, causing a
|
||||
number of missing builds, notably the systemd service file, the documentation
|
||||
and man pages. In addition, the translation files were not handled at all, and
|
||||
translation services were disabled by default, compared to the Autotools
|
||||
framework where it was enabled by default. These bugs will be addressed in the
|
||||
next release.
|
||||
|
||||
### Added
|
||||
- Updated build infrastructure to use Meson instead of Autotools.
|
||||
- Added a [privacy policy](PRIVACY.md) to comply with GDPR/CCPA regulations.
|
||||
|
|
|
|||
849
Doxyfile.in
849
Doxyfile.in
File diff suppressed because it is too large
Load Diff
56
INSTALL.md
56
INSTALL.md
|
|
@ -5,22 +5,22 @@ Build has been tested on the following operating systems (x86-64 only):
|
|||
|
||||
* Ubuntu 22.04 LTS
|
||||
* Ubuntu 24.04 LTS
|
||||
* macOS Big Sur 11
|
||||
* macOS Monterey 12
|
||||
* Fedora latest (Fedora 42, as of this commit)
|
||||
* Archlinux
|
||||
* Alpine Linux (Experimental)
|
||||
* Ubuntu 26.04 LTS (Experimental as of this commit)
|
||||
* macOS (latest tag on Github, ARM)
|
||||
|
||||
# Prerequisites
|
||||
|
||||
## Required Packages
|
||||
|
||||
* automake
|
||||
* autoconf
|
||||
* autopoint
|
||||
* meson
|
||||
* ninja
|
||||
* gettext
|
||||
* hidapi + headers
|
||||
* inih
|
||||
* libtool
|
||||
* libusb-1.0 + headers
|
||||
* libevdev + headers (on Linux)
|
||||
* pkg-config
|
||||
* python3 (3.6 or greater)
|
||||
* git (not required for builds, but necessary to clone the repository)
|
||||
|
|
@ -29,15 +29,16 @@ Build has been tested on the following operating systems (x86-64 only):
|
|||
|
||||
| Platform | Install instructions |
|
||||
| -------- | -------------------- |
|
||||
| Ubuntu | `sudo apt-get install automake autoconf gettext autopoint libhidapi-dev libevdev-dev libtool libusb-1.0-0-dev libinih-dev pkg-config python3 git` |
|
||||
| MacOS + Homebrew | `brew install automake autoconf gettext hidapi libtool libusb pkg-config python3 git` |
|
||||
| Arch Linux | `pacman -S base-devel libusb hidapi libevdev libinih python git` |
|
||||
| Fedora | `sudo dnf install autoconf automake gettext-devel findutils libtool hidapi-devel libusb-devel libevdev-devel inih-devel pkg-config python3 git` |
|
||||
| Ubuntu | `sudo apt-get install meson gettext libhidapi-dev libevdev-dev libusb-1.0-0-dev libinih-dev pkg-config python3 git` |
|
||||
| MacOS + Homebrew | `brew install meson gettext hidapi libusb pkg-config python3 git` |
|
||||
| Arch Linux | `pacman -S base-devel meson libusb hidapi libevdev libinih python git` |
|
||||
| Fedora | `sudo dnf install meson gettext-devel findutils hidapi-devel libusb-devel libevdev-devel inih-devel pkg-config python3 git` |
|
||||
|
||||
## Optional Packages
|
||||
|
||||
* doxygen - to generate HTML documentation and man pages
|
||||
* libcmocka (1.1 or greater) + headers - to run unit tests
|
||||
* libevdev + headers (on Linux) - to add virtual keyboard/mouse support
|
||||
|
||||
# Installation Instructions
|
||||
|
||||
|
|
@ -46,40 +47,39 @@ Build has been tested on the following operating systems (x86-64 only):
|
|||
git clone https://github.com/nirenjan/libx52.git
|
||||
```
|
||||
|
||||
2. Run autogen.sh
|
||||
2. Configure the build (from the repository root)
|
||||
```
|
||||
cd ./libx52
|
||||
./autogen.sh
|
||||
cd libx52
|
||||
meson setup build -Dprefix=/usr
|
||||
```
|
||||
|
||||
3. Run the following commands:
|
||||
3. Compile and install:
|
||||
```
|
||||
./configure --prefix=/usr --localstatedir=/var --sysconfdir=/etc
|
||||
make && sudo make install
|
||||
meson compile -C build && meson install -C build
|
||||
```
|
||||
|
||||
You may want to remove or edit the `--prefix=/usr` option, most users prefer
|
||||
non-distro binaries in `/usr/local` (default without `--prefix`) or `/opt`.
|
||||
You may want to remove or edit the `-Dprefix=/usr` option, most users prefer
|
||||
non-distro binaries in `/usr/local` (default without `-Dprefix`) or `/opt`.
|
||||
|
||||
## Configuration options
|
||||
|
||||
### udev
|
||||
|
||||
The configuration system should automatically detect the udev rules directory,
|
||||
but you can override it by using the following argument to `configure`:
|
||||
but you can override it by using the following argument to `meson setup`:
|
||||
|
||||
```
|
||||
--with-udevrulesdir=/path/to/udev/rules.d
|
||||
-Dudev-rules-dir=/path/to/udev/rules.d
|
||||
```
|
||||
|
||||
### Input group
|
||||
|
||||
The udev rules that are installed provide read/write access to members of the
|
||||
input devices group. This defaults to `plugdev`, but can be modified using
|
||||
the following argument to `configure`:
|
||||
the following argument to `meson setup`:
|
||||
|
||||
```
|
||||
--with-input-group=group
|
||||
-Dinput-group=group
|
||||
```
|
||||
|
||||
### Systemd support
|
||||
|
|
@ -89,13 +89,13 @@ itself to run in the background. Typical deployments with systemd will have it
|
|||
run in the foreground, and disable timestamps in the logs, since those are
|
||||
inserted automatically by journald.
|
||||
|
||||
Systemd support is enabled by default, but can be disabled with the
|
||||
`--disable-systemd` argument to `configure`
|
||||
Systemd support is enabled by default, which disables timestamps in the program
|
||||
logs, but you can re-enable timestamps by passing `-Dsystemd-logs=disabled`
|
||||
argument to `meson setup`
|
||||
|
||||
It is also possible to configure the directory in which the service file is
|
||||
installed with the following option. This is ignored if you have specified
|
||||
`--disable-systemd`.
|
||||
installed with the following option. This is ignored if systemd is not found.
|
||||
|
||||
```
|
||||
--with-systemdsystemunitdir=/path/to/systemd/system
|
||||
-Dsystemd-unit-dir=/path/to/systemd/system
|
||||
```
|
||||
|
|
|
|||
110
Makefile.am
110
Makefile.am
|
|
@ -1,110 +0,0 @@
|
|||
# Top level Automake for libx52
|
||||
#
|
||||
# Copyright (C) 2012-2018 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
# Build any support libraries first
|
||||
SUBDIRS = subprojects
|
||||
|
||||
if USE_NLS
|
||||
SUBDIRS += po
|
||||
endif
|
||||
|
||||
#######################################################################
|
||||
# Defaults
|
||||
#######################################################################
|
||||
bin_PROGRAMS =
|
||||
check_PROGRAMS =
|
||||
lib_LTLIBRARIES =
|
||||
check_LTLIBRARIES =
|
||||
pkgconfig_DATA =
|
||||
TESTS =
|
||||
EXTRA_DIST =
|
||||
CLEANFILES =
|
||||
BUILT_SOURCES =
|
||||
|
||||
x52includedir = $(includedir)/libx52
|
||||
x52include_HEADERS =
|
||||
|
||||
LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
|
||||
|
||||
########################################################################
|
||||
# Get build version
|
||||
########################################################################
|
||||
BUILT_SOURCES += version-info.h
|
||||
CLEANFILES += version-info.h
|
||||
|
||||
version-info.h: ${top_srcdir}/version-info
|
||||
CC=${CC} ${top_srcdir}/version-info ${top_srcdir} >$@
|
||||
|
||||
########################################################################
|
||||
# Include automake stubs
|
||||
########################################################################
|
||||
include libx52/Makefile.am
|
||||
include libx52util/Makefile.am
|
||||
include libx52io/Makefile.am
|
||||
include libusbx52/Makefile.am
|
||||
|
||||
include cli/Makefile.am
|
||||
include joytest/Makefile.am
|
||||
include evtest/Makefile.am
|
||||
include daemon/Makefile.am
|
||||
include udev/Makefile.am
|
||||
|
||||
include bugreport/Makefile.am
|
||||
include docs/Makefile.am
|
||||
|
||||
#######################################################################
|
||||
# Doxygen support
|
||||
#######################################################################
|
||||
if HAVE_DOXYGEN
|
||||
DXGEN = $(DXGEN_@AM_V@)
|
||||
DXGEN_ = $(DXGEN_@AM_DEFAULT_V@)
|
||||
DXGEN_0 = @printf " DXGEN $<\n";
|
||||
|
||||
SYSCONFDIR=@sysconfdir@
|
||||
LOCALSTATEDIR=@localstatedir@
|
||||
export SYSCONFDIR
|
||||
export LOCALSTATEDIR
|
||||
docs/.stamp: Doxyfile
|
||||
$(DXGEN)$(DOXYGEN) $<
|
||||
$(AM_V_at)touch $@
|
||||
|
||||
all-local: docs/.stamp
|
||||
clean-local:
|
||||
rm -rf $(top_builddir)/docs
|
||||
|
||||
man1_MANS = docs/man/man1/x52cli.1 docs/man/man1/x52bugreport.1
|
||||
$(man1_MANS): docs/.stamp
|
||||
|
||||
# Install Doxygen generated HTML documentation and manpages
|
||||
install-data-local:
|
||||
$(INSTALL) -d $(DESTDIR)$(docdir)
|
||||
cp -R -P $(top_builddir)/docs/html $(DESTDIR)$(docdir)
|
||||
|
||||
uninstall-local:
|
||||
rm -rf $(DESTDIR)$(docdir)
|
||||
|
||||
endif
|
||||
|
||||
# Extra files that need to be in the distribution
|
||||
EXTRA_DIST += \
|
||||
ABOUT-NLS \
|
||||
AUTHORS \
|
||||
ChangeLog.md \
|
||||
CONTRIBUTING.md \
|
||||
Doxyfile.in \
|
||||
DoxygenLayout.xml \
|
||||
INSTALL.md \
|
||||
LICENSE \
|
||||
README.md \
|
||||
config.rpath \
|
||||
version-info \
|
||||
Version \
|
||||
gettext.h \
|
||||
usb-ids.h \
|
||||
po/README.md
|
||||
|
||||
|
|
@ -4,6 +4,8 @@ Saitek X52Pro joystick driver for Linux
|
|||

|
||||

|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=nirenjan_libx52)
|
||||
[](https://codecov.io/gh/nirenjan/libx52)
|
||||
|
||||
This project adds a new driver for the Saitek/MadCatz X52 Pro flight
|
||||
control system. The X52 pro is a HOTAS (hand on throttle and stick)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh -x
|
||||
|
||||
autoreconf --install
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Automake for libx52-bugreport
|
||||
#
|
||||
# Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
bin_PROGRAMS += x52bugreport
|
||||
|
||||
# Bug report program that reports the build and linked library versions
|
||||
x52bugreport_SOURCES = bugreport/bugreport.c
|
||||
x52bugreport_CFLAGS = \
|
||||
-I$(top_srcdir)/libx52io \
|
||||
@LIBUSB_CFLAGS@ \
|
||||
@HIDAPI_CFLAGS@ \
|
||||
$(WARN_CFLAGS)
|
||||
|
||||
x52bugreport_LDFLAGS = \
|
||||
@LIBUSB_LIBS@ \
|
||||
@HIDAPI_LIBS@ \
|
||||
$(WARN_LDFLAGS)
|
||||
|
||||
x52bugreport_LDADD = libx52io.la
|
||||
|
||||
$(x52bugreport_OBJECTS): version-info.h
|
||||
|
||||
EXTRA_DIST += bugreport/bugreport.dox
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
#include "libusb.h"
|
||||
#include "hidapi.h"
|
||||
#include "libx52io.h"
|
||||
#include <libx52/libx52io.h>
|
||||
#include "version-info.h"
|
||||
|
||||
static void print_sysinfo(void)
|
||||
|
|
@ -65,7 +65,7 @@ devinfo_cleanup:
|
|||
libx52io_exit(ctx);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
int main(void)
|
||||
{
|
||||
const struct libusb_version *libusb;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# Automake for x52cli
|
||||
#
|
||||
# Copyright (C) 2012-2018 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
bin_PROGRAMS += x52cli
|
||||
|
||||
# Command line utility that front ends the core library
|
||||
x52cli_SOURCES = cli/x52_cli.c
|
||||
x52cli_CFLAGS = -I $(top_srcdir)/libx52 $(WARN_CFLAGS)
|
||||
x52cli_LDFLAGS = $(WARN_LDFLAGS)
|
||||
x52cli_LDADD = libx52.la
|
||||
|
||||
if HAVE_CMOCKA
|
||||
TESTS += test-cli
|
||||
check_PROGRAMS += test-cli
|
||||
|
||||
test_cli_SOURCES = cli/x52_cli.c cli/test_x52_cli.c
|
||||
test_cli_CFLAGS = @CMOCKA_CFLAGS@ -DX52_CLI_TESTING -I $(top_srcdir)/libx52
|
||||
test_cli_LDFLAGS = @CMOCKA_LIBS@ $(WARN_LDFLAGS)
|
||||
|
||||
# Add a dependency on test_x52_cli_tests.c
|
||||
cli/test_x52_cli.c: cli/test_x52_cli_tests.c
|
||||
endif
|
||||
|
||||
EXTRA_DIST += cli/test_x52_cli_tests.c
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
extern int run_main(int argc, char **argv);
|
||||
|
||||
|
|
@ -26,22 +26,26 @@ int libx52_init(libx52_device **dev)
|
|||
|
||||
int libx52_connect(libx52_device *dev)
|
||||
{
|
||||
(void)dev;
|
||||
function_called();
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_update(libx52_device *dev)
|
||||
{
|
||||
(void)dev;
|
||||
return LIBX52_SUCCESS;
|
||||
}
|
||||
|
||||
void libx52_exit(libx52_device *dev)
|
||||
{
|
||||
(void)dev;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *libx52_strerror(libx52_error_code rc)
|
||||
{
|
||||
(void)rc;
|
||||
function_called();
|
||||
return "";
|
||||
}
|
||||
|
|
@ -172,7 +176,7 @@ const struct CMUnitTest tests[] = {
|
|||
#include "test_x52_cli_tests.c"
|
||||
};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
int main(void)
|
||||
{
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
cmocka_run_group_tests(tests, NULL, NULL);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
#ifndef TEST_LIST
|
||||
// Setup the test case function
|
||||
#define TEST_CASE(tc) static void tc(void **state)
|
||||
#define TEST_CASE(tc) static void tc(void **state __attribute__((unused)))
|
||||
#define TEST_DEF(x) x
|
||||
// Function header, this calls the corresponding libx52 function, and expects
|
||||
// a certain number of calls to that function
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ possibly through \b sudo(8)
|
|||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -158,7 +158,7 @@ possibly through \b sudo(8)
|
|||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
struct string_map {
|
||||
const char *key;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@
|
|||
/* Define to the location of the local state directory */
|
||||
#mesondefine LOCALSTATEDIR
|
||||
|
||||
/* Define to the installation data directory (e.g. $prefix/share) */
|
||||
#mesondefine DATADIR
|
||||
|
||||
/* Define to the location of the log directory */
|
||||
#define LOGDIR LOCALSTATEDIR "/log"
|
||||
|
||||
|
|
|
|||
148
configure.ac
148
configure.ac
|
|
@ -1,148 +0,0 @@
|
|||
# Autoconf settings for libx52
|
||||
#
|
||||
# Copyright (C) 2012-2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
AC_INIT([libx52], [m4_esyscmd_s([cat ./Version])], [nirenjan@gmail.com])
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
AM_INIT_AUTOMAKE([-Wall foreign subdir-objects])
|
||||
AC_REQUIRE_AUX_FILE([tap-driver.sh])
|
||||
AC_PROG_CC
|
||||
AC_PROG_CC_STDC
|
||||
AC_PROG_AWK
|
||||
AC_PROG_SED
|
||||
AC_PROG_MKDIR_P
|
||||
AM_PROG_AR
|
||||
AM_PATH_PYTHON([3.5])
|
||||
LT_INIT
|
||||
PKG_PROG_PKG_CONFIG
|
||||
PKG_INSTALLDIR
|
||||
AX_COMPILER_FLAGS
|
||||
AC_CANONICAL_HOST
|
||||
AX_GCC_FUNC_ATTRIBUTE([constructor])
|
||||
AX_GCC_FUNC_ATTRIBUTE([destructor])
|
||||
AX_GCC_FUNC_ATTRIBUTE([format])
|
||||
AX_GCC_FUNC_ATTRIBUTE([noreturn])
|
||||
AC_C_TYPEOF
|
||||
|
||||
AC_MSG_NOTICE([Detected host OS is ${host_os}])
|
||||
build_linux=no
|
||||
# Detect target system
|
||||
case "${host_os}" in
|
||||
linux*)
|
||||
build_linux=yes
|
||||
;;
|
||||
esac
|
||||
AM_CONDITIONAL([LINUX], [test "x${build_linux}" = "xyes"])
|
||||
|
||||
# Internationalization
|
||||
AM_GNU_GETTEXT([external])
|
||||
AM_GNU_GETTEXT_VERSION(0.19)
|
||||
AM_CONDITIONAL([USE_NLS], [test "x${USE_NLS}" == "xyes"])
|
||||
|
||||
# Check for libusb-1.0
|
||||
PKG_CHECK_MODULES([LIBUSB], [libusb-1.0])
|
||||
AC_SUBST([LIBUSB_PC], [libusb-1.0])
|
||||
|
||||
# systemd support
|
||||
PKG_CHECK_MODULES([SYSTEMD], [systemd], [have_systemd=yes], [have_systemd=no])
|
||||
AC_ARG_ENABLE([systemd],
|
||||
[AS_HELP_STRING([--disable-systemd], [Disable systemd support])]
|
||||
)
|
||||
AC_ARG_WITH([systemdsystemunitdir],
|
||||
[AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd unit files])],
|
||||
[systemdsystemunitdir=$withval],
|
||||
[systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)]
|
||||
)
|
||||
AC_SUBST([systemdsystemunitdir], [$systemdsystemunitdir])
|
||||
AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$have_systemd" = "xyes" -a "x$enable_systemd" != "xno"])
|
||||
AM_COND_IF([HAVE_SYSTEMD],,
|
||||
[
|
||||
AC_MSG_NOTICE([systemd not found or disabled. Enabling timestamps in logs])
|
||||
AX_APPEND_FLAG([-DPINELOG_SHOW_DATE=1], [PINELOG_CFLAGS])
|
||||
]
|
||||
)
|
||||
|
||||
# evdev support
|
||||
# This is only on Linux machines, so we need to set an automake conditional
|
||||
PKG_CHECK_MODULES([EVDEV], [libevdev], [have_evdev=yes], [have_evdev=no])
|
||||
AM_CONDITIONAL([HAVE_EVDEV], [test "x$have_evdev" = "xyes"])
|
||||
|
||||
# Pinelog configuration
|
||||
AX_APPEND_FLAG([-DPINELOG_SHOW_LEVEL=1], [PINELOG_CFLAGS])
|
||||
AX_APPEND_FLAG([-DPINELOG_SHOW_BACKTRACE=1], [PINELOG_CFLAGS])
|
||||
AX_APPEND_FLAG([-DPINELOG_BUFFER_SZ=1024], [PINELOG_CFLAGS])
|
||||
AC_SUBST([PINELOG_CFLAGS])
|
||||
|
||||
# Check for hidapi. This uses a different pkg-config file on Linux vs other
|
||||
# hosts, so check accordingly
|
||||
AM_COND_IF([LINUX], [hidapi_backend=hidapi-hidraw], [hidapi_backend=hidapi])
|
||||
PKG_CHECK_MODULES([HIDAPI], [${hidapi_backend}])
|
||||
AC_SUBST([HIDAPI_PC], [${hidapi_backend}])
|
||||
|
||||
# Check for inih library, this is now packaged with recent distros
|
||||
PKG_CHECK_MODULES([INIH], [inih])
|
||||
|
||||
# Check for pthreads
|
||||
ACX_PTHREAD
|
||||
|
||||
# make distcheck doesn't work if some files are installed outside $prefix.
|
||||
# Check for a prefix ending in /_inst, if this is found, we can assume this
|
||||
# to be a make distcheck, and disable some of the installcheck stuff.
|
||||
AS_CASE([$prefix], [*/_inst],
|
||||
[AC_MSG_NOTICE([[Prefix ends in /_inst; this looks like a 'make distcheck']])
|
||||
is_make_distcheck=yes])
|
||||
AM_CONDITIONAL([IS_MAKE_DISTCHECK], [test "x$is_make_distcheck" = xyes])
|
||||
AC_MSG_CHECKING([final decision IS_MAKE_DISTCHECK (running "make distcheck"?)])
|
||||
AM_COND_IF([IS_MAKE_DISTCHECK], [AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no])])
|
||||
|
||||
# udev support
|
||||
PKG_CHECK_MODULES([UDEV], [udev], [have_udev=yes], [have_udev=no])
|
||||
AM_CONDITIONAL([HAVE_UDEV], [test "x$have_udev" = xyes])
|
||||
AC_ARG_WITH([udevrulesdir],
|
||||
AS_HELP_STRING([--with-udevrulesdir=DIR], [Directory for udev rules]),
|
||||
[udevrulesdir=$withval],
|
||||
[udevrulesdir=$($PKG_CONFIG --variable=udevdir udev)"/rules.d"])
|
||||
AC_SUBST([udevrulesdir], [$udevrulesdir])
|
||||
|
||||
AC_ARG_WITH([input-group],
|
||||
AS_HELP_STRING([--with-input-group=GROUP], [Group allowed to access input devices]),
|
||||
[input_group=$withval],
|
||||
[input_group=plugdev])
|
||||
AC_SUBST([input_group], [$input_group])
|
||||
|
||||
# Doxygen Support
|
||||
AC_CHECK_PROGS([DOXYGEN], [doxygen])
|
||||
AM_CONDITIONAL([HAVE_DOXYGEN], [test -n "$DOXYGEN"])
|
||||
AM_COND_IF([HAVE_DOXYGEN],
|
||||
[AC_CONFIG_FILES([Doxyfile])],
|
||||
[AC_MSG_WARN(["Doxygen not found; continuing without doxygen support"])])
|
||||
|
||||
# cmocka unit tests
|
||||
PKG_CHECK_MODULES([CMOCKA], [cmocka >= 1.1], [have_cmocka=yes], [have_cmocka=no])
|
||||
AM_CONDITIONAL([HAVE_CMOCKA], [test "x$have_cmocka" = xyes])
|
||||
AM_COND_IF([HAVE_CMOCKA], [],
|
||||
[AC_MSG_WARN(["cmocka not found; disabling unit test build"])])
|
||||
|
||||
# Check for the presence of tm_gmtoff in struct tm. If we have this, then we
|
||||
# can use it to determine the true GMT offset
|
||||
AC_CHECK_MEMBERS([struct tm.tm_gmtoff],,
|
||||
[AC_MSG_WARN(["Cannot find tm_gmtoff in struct tm, using slower method"])],
|
||||
[#define _GNU_SOURCE
|
||||
#include <time.h>
|
||||
])
|
||||
|
||||
# Configuration headers
|
||||
AC_CONFIG_HEADERS([config.h])
|
||||
|
||||
AC_CONFIG_FILES([ po/Makefile.in
|
||||
Makefile
|
||||
subprojects/Makefile
|
||||
libx52/libx52.pc
|
||||
libx52io/libx52io.pc
|
||||
libx52util/libx52util.pc
|
||||
subprojects/pinelog/Makefile
|
||||
udev/60-saitek-x52-x52pro.rules
|
||||
])
|
||||
AC_OUTPUT
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
# Automake for x52d
|
||||
#
|
||||
# Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
bin_PROGRAMS += x52d x52ctl
|
||||
|
||||
# Service daemon that manages the X52 device
|
||||
x52d_SOURCES = \
|
||||
daemon/x52d_main.c \
|
||||
daemon/x52d_config_parser.c \
|
||||
daemon/x52d_config_dump.c \
|
||||
daemon/x52d_config.c \
|
||||
daemon/x52d_device.c \
|
||||
daemon/x52d_client.c \
|
||||
daemon/x52d_clock.c \
|
||||
daemon/x52d_mouse.c \
|
||||
daemon/x52d_notify.c \
|
||||
daemon/x52d_led.c \
|
||||
daemon/x52d_command.c \
|
||||
daemon/x52d_comm_internal.c \
|
||||
daemon/x52d_comm_client.c
|
||||
|
||||
x52d_CFLAGS = \
|
||||
-I $(top_srcdir) \
|
||||
-I $(top_srcdir)/libx52io \
|
||||
-I $(top_srcdir)/libx52 \
|
||||
-I $(top_srcdir)/libx52util \
|
||||
-I $(top_srcdir)/subprojects/pinelog \
|
||||
-DSYSCONFDIR=\"$(sysconfdir)\" \
|
||||
-DLOCALEDIR=\"$(localedir)\" \
|
||||
-DLOGDIR=\"$(localstatedir)/log\" \
|
||||
-DRUNDIR=\"$(localstatedir)/run\" \
|
||||
@INIH_CFLAGS@ @PTHREAD_CFLAGS@ $(WARN_CFLAGS)
|
||||
|
||||
x52d_LDFLAGS = @INIH_LIBS@ @PTHREAD_LIBS@ $(WARN_LDFLAGS)
|
||||
x52d_LDADD = \
|
||||
subprojects/pinelog/libpinelog.la \
|
||||
libx52.la \
|
||||
@LTLIBINTL@
|
||||
|
||||
if HAVE_EVDEV
|
||||
x52d_SOURCES += \
|
||||
daemon/x52d_io.c \
|
||||
daemon/x52d_mouse_evdev.c
|
||||
|
||||
x52d_CFLAGS += -DHAVE_EVDEV @EVDEV_CFLAGS@
|
||||
x52d_LDFLAGS += @EVDEV_LIBS@
|
||||
x52d_LDADD += libx52io.la
|
||||
endif
|
||||
|
||||
lib_LTLIBRARIES += libx52dcomm.la
|
||||
|
||||
# Client library to communicate with X52 daemon
|
||||
libx52dcomm_la_SOURCES = \
|
||||
daemon/x52d_comm_client.c \
|
||||
daemon/x52d_comm_internal.c
|
||||
libx52dcomm_la_CFLAGS = \
|
||||
-I $(top_srcdir) \
|
||||
-DSYSCONFDIR=\"$(sysconfdir)\" \
|
||||
-DLOCALEDIR=\"$(localedir)\" \
|
||||
-DLOGDIR=\"$(localstatedir)/log\" \
|
||||
-DRUNDIR=\"$(localstatedir)/run\" \
|
||||
$(WARN_CFLAGS)
|
||||
libx52dcomm_la_LDFLAGS = $(WARN_LDFLAGS)
|
||||
|
||||
x52include_HEADERS += daemon/x52dcomm.h
|
||||
|
||||
x52ctl_SOURCES = daemon/x52ctl.c
|
||||
x52ctl_CFLAGS = \
|
||||
-I $(top_srcdir) \
|
||||
$(WARN_CFLAGS)
|
||||
x52ctl_LDFLAGS = $(WARN_LDFLAGS)
|
||||
x52ctl_LDADD = libx52dcomm.la @LTLIBINTL@
|
||||
|
||||
x52dconfdir = @sysconfdir@/x52d
|
||||
x52dconf_DATA = daemon/x52d.conf
|
||||
|
||||
install-exec-hook:
|
||||
$(MKDIR_P) $(DESTDIR)$(localstatedir)/log
|
||||
$(MKDIR_P) $(DESTDIR)$(localstatedir)/run
|
||||
|
||||
EXTRA_DIST += \
|
||||
daemon/daemon.dox \
|
||||
daemon/protocol.dox \
|
||||
daemon/x52d.service.in \
|
||||
daemon/x52d_client.h \
|
||||
daemon/x52d_clock.h \
|
||||
daemon/x52d_config.def \
|
||||
daemon/x52d_config.h \
|
||||
daemon/x52d_const.h \
|
||||
daemon/x52d_device.h \
|
||||
daemon/x52d_io.h \
|
||||
daemon/x52d_mouse.h \
|
||||
daemon/x52d_notify.h \
|
||||
daemon/x52d_command.h \
|
||||
daemon/x52dcomm.h \
|
||||
daemon/x52dcomm-internal.h \
|
||||
daemon/x52d.conf
|
||||
|
||||
# Test cases
|
||||
EXTRA_DIST += \
|
||||
daemon/test_daemon_comm.py \
|
||||
daemon/tests/config/args.tc \
|
||||
daemon/tests/config/clock.tc \
|
||||
daemon/tests/config/led.tc \
|
||||
daemon/tests/config/mouse.tc \
|
||||
daemon/tests/logging/error.tc \
|
||||
daemon/tests/logging/global.tc \
|
||||
daemon/tests/logging/module.tc \
|
||||
daemon/tests/cli.tc
|
||||
|
||||
TESTS += daemon/test_daemon_comm.py
|
||||
|
||||
if HAVE_CMOCKA
|
||||
check_PROGRAMS += x52d-mouse-test
|
||||
|
||||
x52d_mouse_test_SOURCES = \
|
||||
daemon/x52d_mouse_test.c \
|
||||
daemon/x52d_mouse.c
|
||||
x52d_mouse_test_CFLAGS = \
|
||||
-DLOCALEDIR='"$(localedir)"' \
|
||||
-I $(top_srcdir) \
|
||||
-I $(top_srcdir)/libx52 \
|
||||
-I $(top_srcdir)/libx52io \
|
||||
-I $(top_srcdir)/subprojects/pinelog \
|
||||
$(WARN_CFLAGS) @CMOCKA_CFLAGS@
|
||||
x52d_mouse_test_LDFLAGS = @CMOCKA_LIBS@ $(WARN_LDFLAGS)
|
||||
x52d_mouse_test_LDADD = \
|
||||
subprojects/pinelog/libpinelog.la \
|
||||
@LTLIBINTL@
|
||||
|
||||
TESTS += x52d-mouse-test
|
||||
endif
|
||||
|
||||
if HAVE_SYSTEMD
|
||||
if !IS_MAKE_DISTCHECK
|
||||
SED_ARGS = s,%bindir%,$(bindir),g
|
||||
x52d.service: daemon/x52d.service.in
|
||||
$(AM_V_GEN) $(SED) -e '$(SED_ARGS)' $< > $@
|
||||
systemdsystemunit_DATA = x52d.service
|
||||
endif
|
||||
endif
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
#include <unistd.h>
|
||||
|
||||
#include "pinelog.h"
|
||||
#include "x52d_client.h"
|
||||
#include "x52dcomm-internal.h"
|
||||
#include <daemon/client.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
void x52d_client_init(int client_fd[X52D_MAX_CLIENTS])
|
||||
{
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
#include <stdbool.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include "x52d_const.h"
|
||||
#include <daemon/constants.h>
|
||||
|
||||
#define MAX_CONN (X52D_MAX_CLIENTS + 1)
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
|
@ -15,10 +15,10 @@
|
|||
|
||||
#define PINELOG_MODULE X52D_MOD_CLOCK
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_clock.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_device.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/clock.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/device.h>
|
||||
|
||||
static bool clock_enabled = false;
|
||||
static int clock_primary_is_local = false;
|
||||
|
|
@ -169,6 +169,7 @@ static pthread_t clock_thr;
|
|||
static void * x52_clock_thr(void *param)
|
||||
{
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
PINELOG_INFO(_("Starting X52 clock manager thread"));
|
||||
for (;;) {
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
|
@ -15,8 +15,8 @@
|
|||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "x52dcomm.h"
|
||||
#include "x52dcomm-internal.h"
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static int _setup_socket(struct sockaddr_un *remote, int len)
|
||||
{
|
||||
|
|
@ -6,15 +6,15 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "x52dcomm-internal.h"
|
||||
#include "x52d_const.h"
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
const char * x52d_command_sock_path(const char *sock_path)
|
||||
{
|
||||
|
|
@ -54,7 +54,7 @@ static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path)
|
|||
remote->sun_family = AF_UNIX;
|
||||
/* We've already verified that sock_path will fit, so we don't need strncpy */
|
||||
strcpy(remote->sun_path, sock_path);
|
||||
len += sizeof(remote->sun_family);
|
||||
len += sizeof(*remote) - sizeof(remote->sun_path);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
|
@ -21,11 +21,11 @@
|
|||
|
||||
#define PINELOG_MODULE X52D_MOD_COMMAND
|
||||
#include "pinelog.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_command.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_client.h"
|
||||
#include "x52dcomm-internal.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/command.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/client.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static int client_fd[X52D_MAX_CLIENTS];
|
||||
|
||||
|
|
@ -277,6 +277,7 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
|
|||
[X52D_MOD_COMMAND] = "command",
|
||||
[X52D_MOD_CLIENT] = "client",
|
||||
[X52D_MOD_NOTIFY] = "notify",
|
||||
[X52D_MOD_KEYBOARD_LAYOUT] = "keyboard_layout",
|
||||
};
|
||||
|
||||
// This corresponds to the levels in pinelog
|
||||
|
|
@ -409,6 +410,7 @@ int x52d_command_loop(int sock_fd)
|
|||
|
||||
static void * x52d_command_thread(void *param)
|
||||
{
|
||||
(void)param;
|
||||
for (;;) {
|
||||
if (x52d_command_loop(command_sock_fd) < 0) {
|
||||
PINELOG_FATAL(_("Error %d during command loop: %s"),
|
||||
|
|
@ -6,13 +6,13 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <errno.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
static struct x52d_config x52d_config;
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ void x52d_config_apply_immediate(const char *section, const char *key)
|
|||
x52d_cfg_set_ ## c_sec ## _ ## c_key(x52d_config . name); \
|
||||
} else
|
||||
|
||||
#include "x52d_config.def"
|
||||
#include <daemon/config.def>
|
||||
// Dummy to capture the trailing else
|
||||
// Wrap it in braces in case tracing has been disabled
|
||||
{ PINELOG_TRACE("Ignoring apply_immediate(%s.%s)", section, key); }
|
||||
|
|
@ -108,5 +108,5 @@ void x52d_config_apply(void)
|
|||
#define CFG(section, key, name, parser, def) \
|
||||
PINELOG_TRACE("Calling configuration callback for " #section "." #key); \
|
||||
x52d_cfg_set_ ## section ## _ ## key(x52d_config . name);
|
||||
#include "x52d_config.def"
|
||||
#include <daemon/config.def>
|
||||
}
|
||||
|
|
@ -71,13 +71,26 @@ CFG(Brightness, LED, brightness[1], int, 128)
|
|||
// Enabled controls whether the virtual mouse is enabled or not.
|
||||
CFG(Mouse, Enabled, mouse_enabled, bool, true)
|
||||
|
||||
// Speed is a value that is proportional to the speed of updates to the
|
||||
// virtual mouse
|
||||
// DEPRECATED: Speed is a value that is proportional to the speed of updates to
|
||||
// the virtual mouse
|
||||
CFG(Mouse, Speed, mouse_speed, int, 0)
|
||||
|
||||
// Sensitivity is a percentage that is used to scale the speed of the virtual
|
||||
// mouse. This replaces the old speed value.
|
||||
CFG(Mouse, Sensitivity, mouse_sensitivity, int, 0)
|
||||
|
||||
// ReverseScroll controls the scrolling direction
|
||||
CFG(Mouse, ReverseScroll, mouse_reverse_scroll, bool, false)
|
||||
|
||||
// IsometricMode controls whether to use linear or isometric speed calculations
|
||||
CFG(Mouse, IsometricMode, mouse_isometric_mode, bool, false)
|
||||
|
||||
// CurveFactor controls the speed curve
|
||||
CFG(Mouse, CurveFactor, mouse_curve_factor, int, 3)
|
||||
|
||||
// Deadzone controls the deadzone range for the thumbstick
|
||||
CFG(Mouse, Deadzone, mouse_deadzone_factor, int, 0)
|
||||
|
||||
/**********************************************************************
|
||||
* Profiles - only valid on Linux
|
||||
*********************************************************************/
|
||||
|
|
@ -92,4 +105,8 @@ CFG(Profiles, ClutchEnabled, clutch_enabled, bool, false)
|
|||
// be held down to remain in clutch mode.
|
||||
CFG(Profiles, ClutchLatched, clutch_latched, bool, false)
|
||||
|
||||
// KeyboardLayout is the default keyboard layout used when mapping
|
||||
// profile keys to keyboard events.
|
||||
CFG(Profiles, KeyboardLayout, profile_keyboard_layout, string, us)
|
||||
|
||||
#undef CFG
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
/**
|
||||
* @brief Configuration structure
|
||||
|
|
@ -39,12 +39,18 @@ struct x52d_config {
|
|||
|
||||
bool mouse_enabled;
|
||||
int mouse_speed;
|
||||
int mouse_sensitivity;
|
||||
bool mouse_reverse_scroll;
|
||||
bool mouse_isometric_mode;
|
||||
int mouse_curve_factor;
|
||||
int mouse_deadzone_factor;
|
||||
|
||||
bool clutch_enabled;
|
||||
bool clutch_latched;
|
||||
|
||||
char profiles_dir[NAME_MAX];
|
||||
|
||||
char profile_keyboard_layout[NAME_MAX];
|
||||
};
|
||||
|
||||
/* Callback functions for configuration */
|
||||
|
|
@ -72,10 +78,15 @@ void x52d_cfg_set_Brightness_MFD(uint16_t param);
|
|||
void x52d_cfg_set_Brightness_LED(uint16_t param);
|
||||
void x52d_cfg_set_Mouse_Enabled(bool param);
|
||||
void x52d_cfg_set_Mouse_Speed(int param);
|
||||
void x52d_cfg_set_Mouse_Sensitivity(int param);
|
||||
void x52d_cfg_set_Mouse_ReverseScroll(bool param);
|
||||
void x52d_cfg_set_Mouse_IsometricMode(bool param);
|
||||
void x52d_cfg_set_Mouse_CurveFactor(int param);
|
||||
void x52d_cfg_set_Mouse_Deadzone(int param);
|
||||
void x52d_cfg_set_Profiles_Directory(char* param);
|
||||
void x52d_cfg_set_Profiles_ClutchEnabled(bool param);
|
||||
void x52d_cfg_set_Profiles_ClutchLatched(bool param);
|
||||
void x52d_cfg_set_Profiles_KeyboardLayout(char *param);
|
||||
|
||||
int x52d_config_process_kv(void *user, const char *section, const char *key, const char *value);
|
||||
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key);
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||
#include "pinelog.h"
|
||||
#include "libx52.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
// Create a pointer "name" of type "type", which stores the value of the
|
||||
// corresponding element within the config struct.
|
||||
|
|
@ -130,7 +130,7 @@ int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
|
|||
fprintf(cfg_fp, "%s = %s\n", #key, value); \
|
||||
} \
|
||||
} while (0);
|
||||
#include "x52d_config.def"
|
||||
#include <daemon/config.def>
|
||||
|
||||
exit_dump:
|
||||
free(current_section);
|
||||
|
|
@ -145,7 +145,7 @@ const char *x52d_config_get_param(struct x52d_config *cfg, const char *section,
|
|||
return type ## _dumper(section, key, cfg, offsetof(struct x52d_config, name)); \
|
||||
} \
|
||||
} while (0);
|
||||
#include "x52d_config.def"
|
||||
#include <daemon/config.def>
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||
#include "ini.h"
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
/* Parser function typedef */
|
||||
typedef int (*parser_fn)(struct x52d_config *, size_t, const char *);
|
||||
|
|
@ -142,7 +142,7 @@ static const struct config_map {
|
|||
parser_fn parser;
|
||||
size_t offset;
|
||||
} config_map[] = {
|
||||
#include "x52d_config.def"
|
||||
#include <daemon/config.def>
|
||||
|
||||
// Terminating entry
|
||||
{NULL, NULL, NULL, 0}
|
||||
|
|
@ -195,7 +195,7 @@ int x52d_config_set_defaults(struct x52d_config *cfg) {
|
|||
if (rc != 0) { \
|
||||
return rc; \
|
||||
}
|
||||
#include "x52d_config.def"
|
||||
#include <daemon/config.def>
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ enum {
|
|||
X52D_MOD_COMMAND,
|
||||
X52D_MOD_CLIENT,
|
||||
X52D_MOD_NOTIFY,
|
||||
X52D_MOD_KEYBOARD_LAYOUT,
|
||||
|
||||
X52D_MOD_MAX
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 (zlib / Python zlib.crc32 compatible)
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Table matches zlib's crc_table (reflect, poly 0xEDB88320). */
|
||||
static const uint32_t x52_crc32_table[256] = {
|
||||
0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u,
|
||||
0x9e6495a3u, 0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu,
|
||||
0xe7b82d07u, 0x90bf1d91u, 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du,
|
||||
0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, 0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu,
|
||||
0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u,
|
||||
0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, 0x35b5a8fau, 0x42b2986cu,
|
||||
0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, 0x26d930acu,
|
||||
0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu,
|
||||
0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu,
|
||||
0xb6662d3du, 0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu,
|
||||
0x9fbfe4a5u, 0xe8b8d433u, 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu,
|
||||
0x086d3d2du, 0x91646c97u, 0xe6635c01u, 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu,
|
||||
0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, 0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau,
|
||||
0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, 0x4db26158u, 0x3ab551ceu,
|
||||
0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, 0x4369e96au,
|
||||
0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u,
|
||||
0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u,
|
||||
0xce61e49fu, 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u,
|
||||
0xb7bd5c3bu, 0xc0ba6cadu, 0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u,
|
||||
0x9dd277afu, 0x04db2615u, 0x73dc1683u, 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u,
|
||||
0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, 0xf00f9344u, 0x8708a3d2u, 0x1e01f268u,
|
||||
0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, 0xfed41b76u, 0x89d32be0u,
|
||||
0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, 0xd6d6a3e8u,
|
||||
0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu,
|
||||
0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu,
|
||||
0x4669be79u, 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u,
|
||||
0x220216b9u, 0x5505262fu, 0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u,
|
||||
0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au,
|
||||
0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu,
|
||||
0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, 0x86d3d2d4u, 0xf1d4e242u,
|
||||
0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, 0x88085ae6u,
|
||||
0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u,
|
||||
0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du,
|
||||
0x3e6e77dbu, 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u,
|
||||
0x47b2cf7fu, 0x30b5ffe9u, 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u,
|
||||
0xcdd70693u, 0x54de5729u, 0x23d967bfu, 0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u,
|
||||
0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du,
|
||||
};
|
||||
|
||||
uint32_t x52_crc32_init(void)
|
||||
{
|
||||
return X52_CRC32_INIT;
|
||||
}
|
||||
|
||||
uint32_t x52_crc32_update(uint32_t crc, const void *data, size_t len)
|
||||
{
|
||||
const unsigned char *p = data;
|
||||
|
||||
if (len == 0) {
|
||||
return crc;
|
||||
}
|
||||
|
||||
crc = ~crc;
|
||||
|
||||
while (len != 0) {
|
||||
crc = x52_crc32_table[(crc ^ *p++) & 0xffu] ^ (crc >> 8);
|
||||
len--;
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 (zlib / Python zlib.crc32 compatible)
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file crc32.h
|
||||
* @brief IEEE/ZIP CRC-32 matching @c zlib.crc32 / Python @c zlib.crc32 (polynomial 0xEDB88320).
|
||||
*
|
||||
* Incremental use: @code
|
||||
* uint32_t crc = x52_crc32_init();
|
||||
* crc = x52_crc32_update(crc, chunk0, len0);
|
||||
* crc = x52_crc32_update(crc, chunk1, len1);
|
||||
* // crc is final value (unsigned 32-bit, same encoding as Python after & 0xFFFFFFFF)
|
||||
* @endcode
|
||||
*/
|
||||
#ifndef X52D_CRC32_H
|
||||
#define X52D_CRC32_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Initial accumulator value (same as first argument to zlib @c crc32 for a new stream). */
|
||||
#define X52_CRC32_INIT 0u
|
||||
|
||||
/** Start a new CRC computation. */
|
||||
uint32_t x52_crc32_init(void);
|
||||
|
||||
/**
|
||||
* Feed zero or more bytes into a running CRC.
|
||||
*
|
||||
* @param crc Value from @ref x52_crc32_init or a prior @ref x52_crc32_update
|
||||
* @param data Input bytes; must be non-NULL if @p len is non-zero
|
||||
* @param len Number of bytes
|
||||
* @return Updated CRC-32 (same as Python <tt>zlib.crc32(data, crc) & 0xFFFFFFFF</tt>
|
||||
* when @p data is the new chunk only)
|
||||
*/
|
||||
uint32_t x52_crc32_update(uint32_t crc, const void *data, size_t len);
|
||||
|
||||
/** Alias for @ref x52_crc32_update (zlib-style name). */
|
||||
static inline uint32_t x52_crc32(uint32_t crc, const void *data, size_t len)
|
||||
{
|
||||
return x52_crc32_update(crc, data, len);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_CRC32_H */
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 unit tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
|
||||
/* Golden values: Python zlib.crc32(data) & 0xFFFFFFFF */
|
||||
|
||||
static void test_init_zero(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal((int)x52_crc32_init(), 0);
|
||||
assert_int_equal((int)X52_CRC32_INIT, 0);
|
||||
}
|
||||
|
||||
static void test_empty(void **state)
|
||||
{
|
||||
(void)state;
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, NULL, 0);
|
||||
assert_true(crc == 0u);
|
||||
crc = x52_crc32_update(crc, "", 0);
|
||||
assert_true(crc == 0u);
|
||||
}
|
||||
|
||||
static void test_len0_preserves(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const char *s = "abc";
|
||||
uint32_t crc = x52_crc32_update(0, s, 3);
|
||||
uint32_t again = x52_crc32_update(crc, s, 0);
|
||||
assert_true(again == crc);
|
||||
}
|
||||
|
||||
static void test_string_123456789(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t crc = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
assert_true(crc == 0xcbf43926u);
|
||||
}
|
||||
|
||||
static void test_bytes_0_to_255(void **state)
|
||||
{
|
||||
(void)state;
|
||||
unsigned char buf[256];
|
||||
for (unsigned i = 0; i < 256; i++) {
|
||||
buf[i] = (unsigned char)i;
|
||||
}
|
||||
uint32_t crc = x52_crc32_update(0, buf, sizeof(buf));
|
||||
assert_true(crc == 0x29058c73u);
|
||||
}
|
||||
|
||||
static void test_incremental_matches_one_shot(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t a = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
|
||||
uint32_t b = x52_crc32_init();
|
||||
b = x52_crc32_update(b, s, 3);
|
||||
b = x52_crc32_update(b, s + 3, 3);
|
||||
b = x52_crc32_update(b, s + 6, 3);
|
||||
|
||||
assert_true(b == a);
|
||||
}
|
||||
|
||||
static void test_chaining_second_segment(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char h[] = "hello";
|
||||
static const char w[] = "world";
|
||||
static const char hw[] = "helloworld";
|
||||
|
||||
uint32_t c = x52_crc32_update(0, h, sizeof(h) - 1u);
|
||||
c = x52_crc32_update(c, w, sizeof(w) - 1u);
|
||||
|
||||
uint32_t whole = x52_crc32_update(0, hw, sizeof(hw) - 1u);
|
||||
assert_true(c == whole);
|
||||
assert_true(c == 0xf9eb20adu);
|
||||
}
|
||||
|
||||
static void test_one_byte_at_a_time(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t expect = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
uint32_t crc = x52_crc32_init();
|
||||
for (size_t i = 0; i < sizeof(s) - 1u; i++) {
|
||||
crc = x52_crc32_update(crc, s + i, 1);
|
||||
}
|
||||
assert_true(crc == expect);
|
||||
}
|
||||
|
||||
static void test_x52_crc32_alias(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t u = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
uint32_t v = x52_crc32(0, s, sizeof(s) - 1u);
|
||||
assert_true(u == v);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_init_zero),
|
||||
cmocka_unit_test(test_empty),
|
||||
cmocka_unit_test(test_len0_preserves),
|
||||
cmocka_unit_test(test_string_123456789),
|
||||
cmocka_unit_test(test_bytes_0_to_255),
|
||||
cmocka_unit_test(test_incremental_matches_one_shot),
|
||||
cmocka_unit_test(test_chaining_second_segment),
|
||||
cmocka_unit_test(test_one_byte_at_a_time),
|
||||
cmocka_unit_test(test_x52_crc32_alias),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ string "quit", or terminates input by using Ctrl+D.
|
|||
default socket.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
|
@ -48,8 +48,8 @@ string "quit", or terminates input by using Ctrl+D.
|
|||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "x52d_const.h"
|
||||
#include "x52dcomm.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
|
||||
#define APP_NAME "x52ctl"
|
||||
#if HAVE_FUNC_ATTRIBUTE_NORETURN
|
||||
|
|
@ -89,15 +89,53 @@ static int send_command(int sock_fd, int argc, char **argv)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static void interactive_mode(int sock_fd)
|
||||
{
|
||||
bool keep_running = true;
|
||||
char buffer[1024];
|
||||
|
||||
fputs("> ", stdout);
|
||||
while (keep_running && fgets(buffer, sizeof(buffer), stdin) != NULL) {
|
||||
int sargc;
|
||||
char *sargv[512] = { 0 };
|
||||
int pos;
|
||||
|
||||
if (strcasecmp(buffer, "quit\n") == 0) {
|
||||
keep_running = false;
|
||||
} else {
|
||||
/* Break the buffer into argc/argv */
|
||||
sargc = 0;
|
||||
pos = 0;
|
||||
while (buffer[pos]) {
|
||||
if (isspace(buffer[pos])) {
|
||||
buffer[pos] = '\0';
|
||||
pos++;
|
||||
} else {
|
||||
sargv[sargc] = &buffer[pos];
|
||||
sargc++;
|
||||
for (; buffer[pos] && !isspace(buffer[pos]); pos++);
|
||||
}
|
||||
}
|
||||
|
||||
if (send_command(sock_fd, sargc, sargv)) {
|
||||
keep_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (keep_running) {
|
||||
fputs("\n> ", stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
bool interactive = false;
|
||||
char *socket_path = NULL;
|
||||
const char *socket_path = NULL;
|
||||
int opt;
|
||||
int sock_fd;
|
||||
int rc = EXIT_SUCCESS;
|
||||
|
||||
char buffer[1024];
|
||||
|
||||
/*
|
||||
* Parse command line arguments
|
||||
|
|
@ -142,47 +180,13 @@ int main(int argc, char **argv)
|
|||
_("Running in interactive mode, ignoring extra arguments\n"));
|
||||
}
|
||||
|
||||
fputs("> ", stdout);
|
||||
while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
|
||||
int sargc;
|
||||
char *sargv[512] = { 0 };
|
||||
int pos;
|
||||
|
||||
if (strcasecmp(buffer, "quit\n") == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Break the buffer into argc/argv */
|
||||
sargc = 0;
|
||||
pos = 0;
|
||||
while (buffer[pos]) {
|
||||
if (isspace(buffer[pos])) {
|
||||
buffer[pos] = '\0';
|
||||
pos++;
|
||||
} else {
|
||||
sargv[sargc] = &buffer[pos];
|
||||
sargc++;
|
||||
for (; buffer[pos] && !isspace(buffer[pos]); pos++);
|
||||
}
|
||||
}
|
||||
|
||||
if (send_command(sock_fd, sargc, sargv)) {
|
||||
rc = EXIT_FAILURE;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
fputs("\n> ", stdout);
|
||||
}
|
||||
|
||||
interactive_mode(sock_fd);
|
||||
} else {
|
||||
if (send_command(sock_fd, argc - optind, &argv[optind])) {
|
||||
rc = EXIT_FAILURE;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cleanup:
|
||||
close(sock_fd);
|
||||
return rc;
|
||||
}
|
||||
|
|
@ -6,17 +6,17 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_DEVICE
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_device.h"
|
||||
#include "x52d_notify.h"
|
||||
#include "libx52.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/device.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <libx52/libx52.h>
|
||||
#include "pinelog.h"
|
||||
|
||||
static libx52_device *x52_dev;
|
||||
|
|
@ -29,6 +29,7 @@ static volatile bool device_update_needed;
|
|||
static void *x52_dev_thr(void *param)
|
||||
{
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
#define DEV_ACQ_DELAY 5 // seconds
|
||||
#define DEV_UPD_DELAY 50000 // microseconds
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
#ifndef X52D_DEVICE_H
|
||||
#define X52D_DEVICE_H
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
void x52d_dev_init(void);
|
||||
void x52d_dev_exit(void);
|
||||
|
|
@ -6,16 +6,16 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_io.h"
|
||||
#include "x52d_mouse.h"
|
||||
#include "libx52io.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/io.h>
|
||||
#include <daemon/mouse.h>
|
||||
#include <libx52/libx52io.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_IO
|
||||
#include "pinelog.h"
|
||||
|
|
@ -36,6 +36,7 @@ static void *x52_io_thr(void *param)
|
|||
int rc;
|
||||
libx52io_report report;
|
||||
libx52io_report prev_report;
|
||||
(void)param;
|
||||
|
||||
#define IO_READ_TIMEOUT 50 /* milliseconds */
|
||||
#define IO_ACQ_TIMEOUT 5 /* seconds */
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - keyboard layout from config
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_KEYBOARD_LAYOUT
|
||||
#include "pinelog.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <daemon/layout_format.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static x52_layout *active_layout;
|
||||
|
||||
/**
|
||||
* Normal install: @c $DATADIR/x52d/<basename>.x52l
|
||||
*
|
||||
* If @c X52D_LAYOUT_DIR is set (non-empty), load @c $X52D_LAYOUT_DIR/<basename>.x52l instead so
|
||||
* uninstalled tests can point at the Meson build directory.
|
||||
*/
|
||||
static int load_layout_for_basename(const char *basename, x52_layout **out)
|
||||
{
|
||||
const char *layout_dir = getenv("X52D_LAYOUT_DIR");
|
||||
|
||||
if (layout_dir != NULL && layout_dir[0] != '\0') {
|
||||
char path[PATH_MAX];
|
||||
int n = snprintf(path, sizeof path, "%s/%s.x52l", layout_dir, basename);
|
||||
if (n < 0) {
|
||||
return EIO;
|
||||
}
|
||||
if ((size_t)n >= sizeof path) {
|
||||
return ENAMETOOLONG;
|
||||
}
|
||||
return x52_layout_load_path(path, out);
|
||||
}
|
||||
|
||||
return x52_layout_load_datadir(DATADIR, basename, out);
|
||||
}
|
||||
|
||||
const x52_layout *x52d_keyboard_layout_get(void)
|
||||
{
|
||||
return active_layout;
|
||||
}
|
||||
|
||||
void x52d_keyboard_layout_fini(void)
|
||||
{
|
||||
x52_layout_free(active_layout);
|
||||
active_layout = NULL;
|
||||
}
|
||||
|
||||
void x52d_keyboard_layout_reload(char *profile_keyboard_layout_value)
|
||||
{
|
||||
char basename[256];
|
||||
bool rejected = false;
|
||||
|
||||
x52_layout_normalize_keyboard_basename(profile_keyboard_layout_value, basename, sizeof basename, &rejected);
|
||||
if (rejected) {
|
||||
PINELOG_WARN(_("Invalid Profiles.KeyboardLayout value; using default layout basename 'us'"));
|
||||
}
|
||||
|
||||
x52_layout *new_layout = NULL;
|
||||
int err = load_layout_for_basename(basename, &new_layout);
|
||||
|
||||
if (err != 0 && strcmp(basename, "us") != 0) {
|
||||
PINELOG_WARN(
|
||||
_("Keyboard layout '%s' could not be loaded (%s); loading default 'us'"),
|
||||
basename, strerror(err > 0 ? err : EIO));
|
||||
err = load_layout_for_basename("us", &new_layout);
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
PINELOG_FATAL(_("Could not load keyboard layout from %s/x52d (%s)"), DATADIR,
|
||||
strerror(err > 0 ? err : EIO));
|
||||
}
|
||||
|
||||
x52_layout *old_layout = active_layout;
|
||||
active_layout = new_layout;
|
||||
x52_layout_free(old_layout);
|
||||
|
||||
const char *desc = x52_layout_description(active_layout);
|
||||
PINELOG_INFO(_("Keyboard layout ready: %s (%s)"), x52_layout_name(active_layout),
|
||||
desc[0] != '\0' ? desc : _("no description"));
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Profiles_KeyboardLayout(char *param)
|
||||
{
|
||||
x52d_keyboard_layout_reload(param);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - keyboard layout from config
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_KEYBOARD_LAYOUT_H
|
||||
#define X52D_KEYBOARD_LAYOUT_H
|
||||
|
||||
#include <daemon/layout_format.h>
|
||||
|
||||
/**
|
||||
* @brief Load or reload layout from @c profile_keyboard_layout (@c Profiles.KeyboardLayout).
|
||||
*
|
||||
* Resolves @c $datadir/x52d/<basename>.x52l with basename hardening; falls back to @c us once if the
|
||||
* requested file is missing. Exits the process if no layout can be loaded.
|
||||
*/
|
||||
void x52d_keyboard_layout_reload(char *profile_keyboard_layout_value);
|
||||
|
||||
/** Active layout after @ref x52d_keyboard_layout_reload, or @c NULL before the first apply. */
|
||||
const x52_layout *x52d_keyboard_layout_get(void);
|
||||
|
||||
void x52d_keyboard_layout_fini(void);
|
||||
|
||||
#endif /* X52D_KEYBOARD_LAYOUT_H */
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout v1 binary format
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file layout_format.h
|
||||
* @brief On-disk keyboard layout (@c .x52l) v1: constants, documentation, load and lookup API.
|
||||
*
|
||||
* The file is a dense index: entry @c entries[c] maps Unicode scalar @c c when
|
||||
* @c 0 <= c < codepoint_limit. For @c c >= codepoint_limit there is no mapping.
|
||||
*
|
||||
* **Header (128 bytes, all multi-byte integers big-endian / network order):**
|
||||
* - **0..3:** magic @c 'X' @c '5' @c '2' @c 'L'
|
||||
* - **4..5:** @c version (must be @ref X52_LAYOUT_FORMAT_VERSION)
|
||||
* - **6..7:** @c flags (v1: only @ref X52_LAYOUT_FLAG_NAME_TRUNCATED and/or
|
||||
* @ref X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED; other bits are reserved and must be zero)
|
||||
* - **8..11:** @c codepoint_limit — exclusive end of range; number of two-byte rows in @c entries
|
||||
* - **12..15:** @c checksum — CRC-32 (ZIP/IEEE, same as Python @c zlib.crc32) over the full
|
||||
* file with bytes 12..15 taken as zero when computing the digest
|
||||
* - **16..47:** @c layout_name (required: at least one character before @c NUL; remainder zero)
|
||||
* - **48..111:** @c description (optional, NUL-terminated, remainder zero)
|
||||
* - **112..127:** reserved (ignored on read in v1)
|
||||
*
|
||||
* **128+:** @c entries[] — pairs @c (modifiers, usage_key) for HID page 0x07; @c (0, 0) is empty.
|
||||
*
|
||||
* **File size:** exactly @c 128 + 2 * @c codepoint_limit bytes.
|
||||
*/
|
||||
#ifndef X52D_LAYOUT_FORMAT_H
|
||||
#define X52D_LAYOUT_FORMAT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Four-byte magic at offset 0 (not NUL-terminated in the file). */
|
||||
#define X52_LAYOUT_MAGIC_0 'X'
|
||||
#define X52_LAYOUT_MAGIC_1 '5'
|
||||
#define X52_LAYOUT_MAGIC_2 '2'
|
||||
#define X52_LAYOUT_MAGIC_3 'L'
|
||||
|
||||
/** Total header size and byte offset of the @c entries table. */
|
||||
#define X52_LAYOUT_HEADER_BYTES 128u
|
||||
|
||||
#define X52_LAYOUT_FORMAT_VERSION 1u
|
||||
|
||||
/** Inclusive maximum for @c codepoint_limit - 1 (Unicode scalar space + sentinel ceiling). */
|
||||
#define X52_LAYOUT_CODEPOINT_LIMIT_MAX 0x110000u
|
||||
|
||||
/** @c layout_name field size in the header (offset 16). */
|
||||
#define X52_LAYOUT_NAME_FIELD_BYTES 32u
|
||||
|
||||
/** @c description field size in the header (offset 48). */
|
||||
#define X52_LAYOUT_DESCRIPTION_FIELD_BYTES 64u
|
||||
|
||||
/**
|
||||
* v1 header @c flags (big-endian on disk). @ref x52_layout_flags returns the host-endian value.
|
||||
*/
|
||||
#define X52_LAYOUT_FLAG_NAME_TRUNCATED 1u
|
||||
#define X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED 2u
|
||||
|
||||
/** Bitmask of flags defined for v1; other bits must be zero. */
|
||||
#define X52_LAYOUT_FLAGS_KNOWN (X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED)
|
||||
|
||||
/** Loaded layout snapshot (opaque): full file copy, validated at load time. */
|
||||
typedef struct x52_layout x52_layout;
|
||||
|
||||
/**
|
||||
* @brief Read a layout file into a malloc'd snapshot and validate it (no @c mmap).
|
||||
*
|
||||
* @param path Path to the @c .x52l file; must not be @c NULL (otherwise @c EINVAL)
|
||||
* @param out On success, receives a new @ref x52_layout; must not be @c NULL; caller must @ref x52_layout_free
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno value (@c EINVAL, @c ENOMEM, @c EIO, @c ENOENT, …)
|
||||
*/
|
||||
int x52_layout_load_path(const char *path, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Turn @c Profiles.KeyboardLayout INI value into a safe layout basename.
|
||||
*
|
||||
* Empty or @c NULL yields @c "us" with @p rejected_out @c false. Values containing @c '/' ,
|
||||
* @c '\\' , @c ".." , disallowed characters, or oversize strings are rejected: @p out becomes
|
||||
* @c "us" and @p rejected_out is @c true (caller should log once). Requires @p out_sz @c >= 3.
|
||||
*
|
||||
* Allowed characters: ASCII alphanumeric, @c '_' , @c '-'.
|
||||
*/
|
||||
void x52_layout_normalize_keyboard_basename(const char *cfg_value, char *out, size_t out_sz, bool *rejected_out);
|
||||
|
||||
/**
|
||||
* @brief Build @c <datadir>/x52d/<basename>.x52l into @p path.
|
||||
*
|
||||
* @returns 0 on success, or @c ENAMETOOLONG if the path does not fit
|
||||
*/
|
||||
int x52_layout_join_file_path(char *path, size_t path_sz, const char *datadir, const char *basename);
|
||||
|
||||
/**
|
||||
* @brief Load @c join(datadir, "x52d", basename + ".x52l") after the same validation as @ref x52_layout_load_path.
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno (e.g. @c ENOENT, @c EINVAL, @c ENAMETOOLONG)
|
||||
*/
|
||||
int x52_layout_load_datadir(const char *datadir, const char *basename, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Copy @p data into an owned buffer and validate it.
|
||||
*
|
||||
* Same validation rules as @ref x52_layout_load_path (magic, version, flags, size, CRC-32, entries,
|
||||
* non-empty @c layout_name, etc.).
|
||||
*
|
||||
* @param data Layout file bytes; may be @c NULL only if @p len is zero (otherwise @c EINVAL)
|
||||
* @param len Number of bytes in @p data
|
||||
* @param out On success, receives a new @ref x52_layout; must not be @c NULL; caller must @ref x52_layout_free
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno value (@c EINVAL, @c ENOMEM, …)
|
||||
*/
|
||||
int x52_layout_load_memory(const void *data, size_t len, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Release a layout loaded by @ref x52_layout_load_path or @ref x52_layout_load_memory.
|
||||
*
|
||||
* @param layout Layout to free; @c NULL is a no-op
|
||||
*/
|
||||
void x52_layout_free(x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Exclusive end of the Unicode scalar range covered by @c entries (same as on-disk @c codepoint_limit).
|
||||
*
|
||||
* Lookups for @c code_point >= this value are not in the table.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns The limit value, or @c 0 if @p layout is @c NULL
|
||||
*/
|
||||
uint32_t x52_layout_codepoint_limit(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Host-endian copy of the on-disk @c flags field at header offset 6.
|
||||
*
|
||||
* Only bits in @ref X52_LAYOUT_FLAGS_KNOWN may be set in valid files; the loader rejects unknown bits.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Flag word, or @c 0 if @p layout is @c NULL
|
||||
*/
|
||||
uint16_t x52_layout_flags(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Layout name from the header @c layout_name field.
|
||||
*
|
||||
* If @ref X52_LAYOUT_FLAG_NAME_TRUNCATED is set, the returned string is the on-disk name plus
|
||||
* @c "<truncated>". The pointer remains valid until @ref x52_layout_free; do not modify the string.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Read-only NUL-terminated UTF-8 string; empty string if @p layout is @c NULL
|
||||
*/
|
||||
const char *x52_layout_name(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Optional description from the header @c description field.
|
||||
*
|
||||
* If @ref X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED is set, the returned string is the on-disk text plus
|
||||
* @c "<truncated>". The pointer remains valid until @ref x52_layout_free; do not modify the string.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Read-only NUL-terminated UTF-8 string, or empty string if @p layout is @c NULL or the field is empty
|
||||
*/
|
||||
const char *x52_layout_description(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief O(1) lookup of the HID chord for Unicode scalar @p code_point.
|
||||
*
|
||||
* Returns @c false when @p code_point is out of range, when the slot is empty @c (modifiers, usage_key) = (0, 0),
|
||||
* or when any argument is invalid. On @c false, @p modifiers_out and @p usage_key_out are left unchanged.
|
||||
*
|
||||
* @param layout Loaded layout; if @c NULL, returns @c false
|
||||
* @param code_point Unicode scalar value
|
||||
* @param[out] modifiers_out HID keyboard modifier byte (@ref vkm_key_modifiers bits); if @c NULL, returns @c false
|
||||
* @param[out] usage_key_out HID usage (page 0x07); if @c NULL, returns @c false
|
||||
*
|
||||
* @returns @c true if a non-empty mapping exists and was written to @p modifiers_out and @p usage_key_out
|
||||
*/
|
||||
bool x52_layout_lookup(const x52_layout *layout, uint32_t code_point, uint8_t *modifiers_out,
|
||||
uint8_t *usage_key_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_LAYOUT_FORMAT_H */
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout v1 loader
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
#include <daemon/layout_format.h>
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct x52_layout {
|
||||
uint8_t *data;
|
||||
size_t size;
|
||||
uint32_t codepoint_limit;
|
||||
uint16_t flags;
|
||||
char *name;
|
||||
char *description;
|
||||
};
|
||||
|
||||
static size_t meta_field_len(const uint8_t *field, size_t nbytes)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < nbytes; i++) {
|
||||
if (field[i] == '\0') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static char *copy_meta_string(const uint8_t *field, size_t nbytes, bool add_trunc_suffix)
|
||||
{
|
||||
static const char suf[] = "<truncated>";
|
||||
const size_t suf_len = sizeof(suf) - 1u;
|
||||
size_t base_len = meta_field_len(field, nbytes);
|
||||
size_t total = base_len + (add_trunc_suffix ? suf_len : 0);
|
||||
char *s = (char *)malloc(total + 1u);
|
||||
|
||||
if (s == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (base_len != 0) {
|
||||
memcpy(s, field, base_len);
|
||||
}
|
||||
if (add_trunc_suffix) {
|
||||
memcpy(s + base_len, suf, suf_len);
|
||||
}
|
||||
s[total] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
static uint16_t read_be16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] << 8 | (uint16_t)p[1]);
|
||||
}
|
||||
|
||||
static uint32_t read_be32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static uint32_t x52_layout_file_crc(const uint8_t *buf, size_t len)
|
||||
{
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, buf, 12u);
|
||||
static const uint8_t zero_chk[4] = {0, 0, 0, 0};
|
||||
crc = x52_crc32_update(crc, zero_chk, 4u);
|
||||
if (len > 16u) {
|
||||
crc = x52_crc32_update(crc, buf + 16u, len - 16u);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static int validate_entries(const uint8_t *buf, uint32_t codepoint_limit)
|
||||
{
|
||||
const uint8_t *base = buf + X52_LAYOUT_HEADER_BYTES;
|
||||
|
||||
for (uint32_t i = 0; i < codepoint_limit; i++) {
|
||||
uint8_t usage = base[2u * (size_t)i + 1u];
|
||||
if (usage == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!x52_layout_usage_key_allowed(usage)) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int layout_validate_and_adopt(uint8_t *buf, size_t len, x52_layout **out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
*out = NULL;
|
||||
|
||||
if (len < X52_LAYOUT_HEADER_BYTES) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (memcmp(buf, "X52L", 4) != 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint16_t version = read_be16(buf + 4);
|
||||
uint16_t flags = read_be16(buf + 6);
|
||||
uint32_t codepoint_limit = read_be32(buf + 8);
|
||||
uint32_t stored_crc = read_be32(buf + 12);
|
||||
|
||||
if (version != X52_LAYOUT_FORMAT_VERSION) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
if ((flags & (uint16_t)~X52_LAYOUT_FLAGS_KNOWN) != 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
if (codepoint_limit > X52_LAYOUT_CODEPOINT_LIMIT_MAX) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
size_t body = (size_t)codepoint_limit * 2u;
|
||||
size_t expected = X52_LAYOUT_HEADER_BYTES + body;
|
||||
if (expected < X52_LAYOUT_HEADER_BYTES || len != expected) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (meta_field_len(buf + 16, X52_LAYOUT_NAME_FIELD_BYTES) == 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint32_t calc = x52_layout_file_crc(buf, len);
|
||||
if (calc != stored_crc) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
int en = validate_entries(buf, codepoint_limit);
|
||||
if (en != 0) {
|
||||
free(buf);
|
||||
return en;
|
||||
}
|
||||
|
||||
x52_layout *L = (x52_layout *)malloc(sizeof *L);
|
||||
if (L == NULL) {
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
L->data = buf;
|
||||
L->size = len;
|
||||
L->codepoint_limit = codepoint_limit;
|
||||
L->flags = flags;
|
||||
L->name = copy_meta_string(buf + 16, X52_LAYOUT_NAME_FIELD_BYTES,
|
||||
(flags & X52_LAYOUT_FLAG_NAME_TRUNCATED) != 0);
|
||||
if (L->name == NULL) {
|
||||
free(L);
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
L->description = copy_meta_string(buf + 48, X52_LAYOUT_DESCRIPTION_FIELD_BYTES,
|
||||
(flags & X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED) != 0);
|
||||
if (L->description == NULL) {
|
||||
free(L->name);
|
||||
free(L);
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
*out = L;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void x52_layout_normalize_keyboard_basename(const char *cfg_value, char *out, size_t out_sz, bool *rejected_out)
|
||||
{
|
||||
*rejected_out = false;
|
||||
if (out_sz < 3) {
|
||||
if (out_sz > 0) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfg_value == NULL || cfg_value[0] == '\0') {
|
||||
out[0] = 'u';
|
||||
out[1] = 's';
|
||||
out[2] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (strchr(cfg_value, '/') != NULL || strchr(cfg_value, '\\') != NULL ||
|
||||
strstr(cfg_value, "..") != NULL) {
|
||||
goto failed_normalization;
|
||||
}
|
||||
|
||||
size_t len = strlen(cfg_value);
|
||||
if (len >= out_sz) {
|
||||
goto failed_normalization;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)cfg_value[i];
|
||||
if (!isalnum((int)c) && c != '_' && c != '-') {
|
||||
goto failed_normalization;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(out, cfg_value, len + 1u);
|
||||
return;
|
||||
|
||||
failed_normalization:
|
||||
*rejected_out = true;
|
||||
out[0] = 'u';
|
||||
out[1] = 's';
|
||||
out[2] = '\0';
|
||||
}
|
||||
|
||||
int x52_layout_join_file_path(char *path, size_t path_sz, const char *datadir, const char *basename)
|
||||
{
|
||||
if (path == NULL || datadir == NULL || basename == NULL || path_sz == 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
int n = snprintf(path, path_sz, "%s/x52d/%s.x52l", datadir, basename);
|
||||
if (n < 0) {
|
||||
return EIO;
|
||||
}
|
||||
if ((size_t)n >= path_sz) {
|
||||
return ENAMETOOLONG;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int x52_layout_load_datadir(const char *datadir, const char *basename, x52_layout **out)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
|
||||
int rc = x52_layout_join_file_path(path, sizeof path, datadir, basename);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
return x52_layout_load_path(path, out);
|
||||
}
|
||||
|
||||
int x52_layout_load_path(const char *path, x52_layout **out)
|
||||
{
|
||||
if (path == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return errno != 0 ? errno : EIO;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
|
||||
long pos = ftell(fp);
|
||||
if (pos < 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
|
||||
size_t len = (size_t)pos;
|
||||
if ((long)len != pos || pos < (long)X52_LAYOUT_HEADER_BYTES) {
|
||||
fclose(fp);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(len);
|
||||
if (buf == NULL) {
|
||||
fclose(fp);
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, len, fp);
|
||||
fclose(fp);
|
||||
if (n != len) {
|
||||
free(buf);
|
||||
return EIO;
|
||||
}
|
||||
|
||||
return layout_validate_and_adopt(buf, len, out);
|
||||
}
|
||||
|
||||
int x52_layout_load_memory(const void *data, size_t len, x52_layout **out)
|
||||
{
|
||||
if (data == NULL && len != 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(len);
|
||||
if (buf == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
if (len != 0) {
|
||||
memcpy(buf, data, len);
|
||||
}
|
||||
return layout_validate_and_adopt(buf, len, out);
|
||||
}
|
||||
|
||||
void x52_layout_free(x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return;
|
||||
}
|
||||
free(layout->name);
|
||||
free(layout->description);
|
||||
free(layout->data);
|
||||
free(layout);
|
||||
}
|
||||
|
||||
uint32_t x52_layout_codepoint_limit(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return layout->codepoint_limit;
|
||||
}
|
||||
|
||||
uint16_t x52_layout_flags(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return layout->flags;
|
||||
}
|
||||
|
||||
const char *x52_layout_name(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->name == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->name;
|
||||
}
|
||||
|
||||
const char *x52_layout_description(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->description == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->description;
|
||||
}
|
||||
|
||||
bool x52_layout_lookup(const x52_layout *layout, uint32_t code_point, uint8_t *modifiers_out,
|
||||
uint8_t *usage_key_out)
|
||||
{
|
||||
if (layout == NULL || modifiers_out == NULL || usage_key_out == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (code_point >= layout->codepoint_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t off = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)code_point;
|
||||
uint8_t mod = layout->data[off];
|
||||
uint8_t usage = layout->data[off + 1u];
|
||||
if (usage == 0) {
|
||||
return false;
|
||||
}
|
||||
*modifiers_out = mod;
|
||||
*usage_key_out = usage;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout loader tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
#include <daemon/layout_format.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
static void write_be16(uint8_t *p, uint16_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 8);
|
||||
p[1] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static void write_be32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 24);
|
||||
p[1] = (uint8_t)(v >> 16);
|
||||
p[2] = (uint8_t)(v >> 8);
|
||||
p[3] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static uint32_t read_be32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
/** Patch checksum (offsets 12..15) after building the rest; @p len is full file size. */
|
||||
static void finalize_crc(uint8_t *buf, size_t len)
|
||||
{
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, buf, 12u);
|
||||
static const uint8_t z[4] = {0, 0, 0, 0};
|
||||
crc = x52_crc32_update(crc, z, 4u);
|
||||
if (len > 16u) {
|
||||
crc = x52_crc32_update(crc, buf + 16u, len - 16u);
|
||||
}
|
||||
write_be32(buf + 12, crc);
|
||||
}
|
||||
|
||||
/** Python @c zlib.crc32 over @c minimal v1 layout (@c limit=1, name @c "x", empty entry). */
|
||||
#define X52_LAYOUT_TEST_MINIMAL_ZLIB_CRC32 0xc951bfaau
|
||||
|
||||
static void test_load_minimal_lookup(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 98u; /* enough for 'a' at 97 */
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "minimal", 8);
|
||||
/* chord for 'a': VKM_KEY_A */
|
||||
size_t off = X52_LAYOUT_HEADER_BYTES + 2u * 97u;
|
||||
buf[off] = 0x02; /* LSHIFT */
|
||||
buf[off + 1] = (uint8_t)VKM_KEY_A;
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_codepoint_limit(L), (int)limit);
|
||||
|
||||
uint8_t m = 0xff;
|
||||
uint8_t u = 0xff;
|
||||
assert_true(x52_layout_lookup(L, 97u, &m, &u));
|
||||
assert_int_equal((int)m, 0x02);
|
||||
assert_int_equal((int)u, (int)VKM_KEY_A);
|
||||
|
||||
m = 0;
|
||||
u = 0;
|
||||
assert_false(x52_layout_lookup(L, 0u, &m, &u));
|
||||
assert_false(x52_layout_lookup(L, limit, &m, &u));
|
||||
|
||||
assert_int_equal((int)x52_layout_flags(L), 0);
|
||||
assert_string_equal(x52_layout_name(L), "minimal");
|
||||
assert_string_equal(x52_layout_description(L), "");
|
||||
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_bad_checksum(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
write_be32(buf + 12, 0xdeadbeefu);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_layout_crc_matches_python_zlib_minimal(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
assert_int_equal((int)read_be32(buf + 12), (int)X52_LAYOUT_TEST_MINIMAL_ZLIB_CRC32);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_string_equal(x52_layout_name(L), "x");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_tampered_checksum_byte(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
buf[12] ^= 0x01u;
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_codepoint_limit_not_big_endian(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* Little-endian uint32_t 1 in the codepoint_limit field: read_be32 → 0x01000000. */
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
buf[8] = 0x01u;
|
||||
buf[9] = 0x00u;
|
||||
buf[10] = 0x00u;
|
||||
buf[11] = 0x00u;
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_version_word_not_big_endian_one(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
/* Native little-endian 0x0001 would appear as 01 00 — not BE version 1 (00 01). */
|
||||
buf[4] = 0x01u;
|
||||
buf[5] = 0x00u;
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_size_mismatch(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 4u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len - 1u, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_disallowed_usage(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
buf[X52_LAYOUT_HEADER_BYTES + 1u] = 0x3A; /* VKM_KEY_F1 — not in allowlist */
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_plain(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "us", 3);
|
||||
memcpy(buf + 48, "US QWERTY", 10);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_string_equal(x52_layout_name(L), "us");
|
||||
assert_string_equal(x52_layout_description(L), "US QWERTY");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_truncated_suffix(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6,
|
||||
(uint16_t)(X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED));
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "longish", 8);
|
||||
memcpy(buf + 48, "desc", 5);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_flags(L),
|
||||
(int)(X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED));
|
||||
assert_string_equal(x52_layout_name(L), "longish<truncated>");
|
||||
assert_string_equal(x52_layout_description(L), "desc<truncated>");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_name_truncated_flag_only(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, X52_LAYOUT_FLAG_NAME_TRUNCATED);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "nm", 3);
|
||||
memcpy(buf + 48, "plain", 6);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_flags(L), (int)X52_LAYOUT_FLAG_NAME_TRUNCATED);
|
||||
assert_string_equal(x52_layout_name(L), "nm<truncated>");
|
||||
assert_string_equal(x52_layout_description(L), "plain");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_unknown_flags(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0x8000);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_empty_name(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_basename_normalize_and_join(void **state)
|
||||
{
|
||||
(void)state;
|
||||
char out[256];
|
||||
bool rej;
|
||||
|
||||
x52_layout_normalize_keyboard_basename(NULL, out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("de", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "de");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("ab_cd-9", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "ab_cd-9");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("../x", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("a/b", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("bad name", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("a\\b", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("x..y", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
memset(out, 0, sizeof out);
|
||||
x52_layout_normalize_keyboard_basename("almost..", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
char path[PATH_MAX];
|
||||
assert_int_equal(x52_layout_join_file_path(path, sizeof path, "/usr/share", "us"), 0);
|
||||
assert_string_equal(path, "/usr/share/x52d/us.x52l");
|
||||
assert_int_equal(x52_layout_join_file_path(path, 20, "/usr/share", "us"), ENAMETOOLONG);
|
||||
}
|
||||
|
||||
static void test_reject_version(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, 99);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "v", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_load_minimal_lookup),
|
||||
cmocka_unit_test(test_metadata_plain),
|
||||
cmocka_unit_test(test_metadata_truncated_suffix),
|
||||
cmocka_unit_test(test_metadata_name_truncated_flag_only),
|
||||
cmocka_unit_test(test_reject_unknown_flags),
|
||||
cmocka_unit_test(test_reject_empty_name),
|
||||
cmocka_unit_test(test_reject_bad_checksum),
|
||||
cmocka_unit_test(test_layout_crc_matches_python_zlib_minimal),
|
||||
cmocka_unit_test(test_reject_tampered_checksum_byte),
|
||||
cmocka_unit_test(test_reject_codepoint_limit_not_big_endian),
|
||||
cmocka_unit_test(test_reject_version_word_not_big_endian_one),
|
||||
cmocka_unit_test(test_reject_size_mismatch),
|
||||
cmocka_unit_test(test_reject_disallowed_usage),
|
||||
cmocka_unit_test(test_reject_version),
|
||||
cmocka_unit_test(test_basename_normalize_and_join),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Keyboard layout HID usage allowlist
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
/* layout binary and compiler must stay aligned with vkm_key numeric values. */
|
||||
_Static_assert((unsigned)VKM_KEY_A == 0x04u, "vkm_key main block start");
|
||||
_Static_assert((unsigned)VKM_KEY_CAPS_LOCK == 0x39u, "vkm_key main block end");
|
||||
_Static_assert((unsigned)VKM_KEY_INTL_BACKSLASH == 0x64u, "vkm_key ISO backslash");
|
||||
|
||||
bool x52_layout_usage_key_allowed(uint8_t usage)
|
||||
{
|
||||
if (usage == 0) {
|
||||
return false;
|
||||
}
|
||||
if (usage >= 0x04 && usage <= 0x39) {
|
||||
return true;
|
||||
}
|
||||
if (usage == 0x64) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Keyboard layout HID usage allowlist
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file layout_usage_allowlist.h
|
||||
* @brief HID keyboard/page (0x07) usages permitted as layout chord \c usage_key bytes.
|
||||
*
|
||||
* The set is the USB HID "main block" (usages @c 0x04-@c 0x39, i.e. @c VKM_KEY_A
|
||||
* through @c VKM_KEY_CAPS_LOCK) plus @c VKM_KEY_INTL_BACKSLASH (@c 0x64). It excludes
|
||||
* @c VKM_KEY_NONE, modifiers, function row, navigation cluster, keypad, and all other
|
||||
* @ref vkm_key values; same rule as @c tools/x52compile_layout.py.
|
||||
*/
|
||||
#ifndef X52D_LAYOUT_USAGE_ALLOWLIST_H
|
||||
#define X52D_LAYOUT_USAGE_ALLOWLIST_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Return whether @p usage may appear as the non-modifier byte in a layout entry.
|
||||
*
|
||||
* @param usage HID usage ID (page 0x07), same encoding as @ref vkm_key.
|
||||
*
|
||||
* @returns true if @p usage is in the main-block allowlist; false for @c VKM_KEY_NONE
|
||||
* and for any disallowed usage.
|
||||
*/
|
||||
bool x52_layout_usage_key_allowed(uint8_t usage);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_LAYOUT_USAGE_ALLOWLIST_H */
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - layout HID usage allowlist tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
static void test_allows_main_block(void **state)
|
||||
{
|
||||
(void)state;
|
||||
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_A));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_Z));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_1));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_0));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_SPACE));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_CAPS_LOCK));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_INTL_BACKSLASH));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_NONUS_HASH));
|
||||
}
|
||||
|
||||
static void test_rejects_disallowed(void **state)
|
||||
{
|
||||
(void)state;
|
||||
|
||||
assert_false(x52_layout_usage_key_allowed(0));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_F1));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_LEFT_CTRL));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_KEYPAD_1));
|
||||
assert_false(x52_layout_usage_key_allowed(0x03));
|
||||
assert_false(x52_layout_usage_key_allowed(0x3A));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_allows_main_block),
|
||||
cmocka_unit_test(test_rejects_disallowed),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
|
@ -15,9 +15,9 @@
|
|||
|
||||
#define PINELOG_MODULE X52D_MOD_LED
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_device.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/device.h>
|
||||
|
||||
#define SET_LED_STATE(led, state) \
|
||||
PINELOG_TRACE("Setting LED %s state to %s (%d)", \
|
||||
|
|
@ -6,26 +6,28 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <signal.h>
|
||||
#include <inttypes.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "x52d_clock.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_device.h"
|
||||
#include "x52d_io.h"
|
||||
#include "x52d_mouse.h"
|
||||
#include "x52d_command.h"
|
||||
#include "x52d_notify.h"
|
||||
#include "x52dcomm-internal.h"
|
||||
#include "x52dcomm.h"
|
||||
#include <daemon/clock.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/device.h>
|
||||
#include <daemon/io.h>
|
||||
#include <daemon/mouse.h>
|
||||
#include <daemon/command.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include "pinelog.h"
|
||||
|
||||
static volatile int flag_quit;
|
||||
|
|
@ -38,12 +40,14 @@ static void termination_handler(int signum)
|
|||
static volatile bool flag_reload;
|
||||
static void reload_handler(int signum)
|
||||
{
|
||||
(void)signum;
|
||||
flag_reload = true;
|
||||
}
|
||||
|
||||
static volatile bool flag_save_cfg;
|
||||
static void save_config_handler(int signum)
|
||||
{
|
||||
(void)signum;
|
||||
flag_save_cfg = true;
|
||||
}
|
||||
|
||||
|
|
@ -111,14 +115,16 @@ static void start_daemon(bool foreground, const char *pid_file)
|
|||
pid_fd = fopen(pid_file, "r");
|
||||
if (pid_fd != NULL) {
|
||||
int rc;
|
||||
intmax_t tmp_pid;
|
||||
|
||||
/* File exists, read the PID and check if it exists */
|
||||
rc = fscanf(pid_fd, "%u", &pid);
|
||||
rc = fscanf(pid_fd, "%" SCNdMAX, &tmp_pid);
|
||||
fclose(pid_fd);
|
||||
|
||||
if (rc != 1) {
|
||||
perror("fscanf");
|
||||
} else {
|
||||
pid = (pid_t)tmp_pid;
|
||||
rc = kill(pid, 0);
|
||||
if (rc == 0 || (rc < 0 && errno == EPERM)) {
|
||||
PINELOG_FATAL(_("Daemon is already running as PID %u"), pid);
|
||||
|
|
@ -324,10 +330,8 @@ int main(int argc, char **argv)
|
|||
goto cleanup;
|
||||
}
|
||||
x52d_notify_init(notify_sock);
|
||||
#if defined(HAVE_EVDEV)
|
||||
x52d_io_init();
|
||||
x52d_mouse_evdev_init();
|
||||
#endif
|
||||
x52d_mouse_handler_init();
|
||||
|
||||
// Re-enable signals
|
||||
rc = pthread_sigmask(SIG_UNBLOCK, &sigblockset, NULL);
|
||||
|
|
@ -361,15 +365,14 @@ int main(int argc, char **argv)
|
|||
PINELOG_INFO(_("Received termination signal %s"), strsignal(flag_quit));
|
||||
|
||||
cleanup:
|
||||
x52d_keyboard_layout_fini();
|
||||
// Stop device threads
|
||||
x52d_clock_exit();
|
||||
x52d_dev_exit();
|
||||
x52d_command_exit();
|
||||
x52d_notify_exit();
|
||||
#if defined(HAVE_EVDEV)
|
||||
x52d_mouse_evdev_exit();
|
||||
x52d_mouse_handler_exit();
|
||||
x52d_io_exit();
|
||||
#endif
|
||||
|
||||
// Remove the PID file
|
||||
PINELOG_TRACE("Removing PID file %s", pid_file);
|
||||
|
|
@ -1,64 +1,147 @@
|
|||
# x52d
|
||||
# x52d (dep_config_h: Meson build-config.h; private API is daemon/config.h)
|
||||
libx52dcomm_version = '1.0.0'
|
||||
|
||||
libx52dcomm_sources = [
|
||||
'x52d_comm_client.c',
|
||||
'x52d_comm_internal.c'
|
||||
'comm_client.c',
|
||||
'comm_internal.c'
|
||||
]
|
||||
|
||||
install_headers('x52dcomm.h', subdir: meson.project_name())
|
||||
|
||||
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
|
||||
dependencies: [dep_intl],
|
||||
dependencies: [dep_intl, dep_config_h],
|
||||
version: libx52dcomm_version,
|
||||
c_args: sym_hidden_cargs,
|
||||
install: true,
|
||||
include_directories: includes)
|
||||
|
||||
pkgconfig.generate(lib_libx52dcomm,
|
||||
name: 'x52dcomm',
|
||||
description: 'Client library for communicating with the x52d X52 daemon.',
|
||||
version: libx52dcomm_version,
|
||||
)
|
||||
|
||||
x52d_sources = [
|
||||
'x52d_main.c',
|
||||
'x52d_config_parser.c',
|
||||
'x52d_config_dump.c',
|
||||
'x52d_config.c',
|
||||
'x52d_device.c',
|
||||
'x52d_client.c',
|
||||
'x52d_clock.c',
|
||||
'x52d_mouse.c',
|
||||
'x52d_notify.c',
|
||||
'x52d_led.c',
|
||||
'x52d_command.c',
|
||||
'main.c',
|
||||
'config_parser.c',
|
||||
'config_dump.c',
|
||||
'config.c',
|
||||
'device.c',
|
||||
'client.c',
|
||||
'clock.c',
|
||||
'mouse.c',
|
||||
'notify.c',
|
||||
'led.c',
|
||||
'command.c',
|
||||
'io.c',
|
||||
'mouse_handler.c',
|
||||
'layout_usage_allowlist.c',
|
||||
'layout_load.c',
|
||||
'keyboard_layout.c',
|
||||
'crc32.c',
|
||||
]
|
||||
|
||||
dep_threads = dependency('threads')
|
||||
x52d_linkwith = [lib_libx52, lib_libx52dcomm]
|
||||
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_intl]
|
||||
x52d_cflags = []
|
||||
if dep_evdev.found()
|
||||
x52d_sources += 'x52d_io.c'
|
||||
x52d_sources += 'x52d_mouse_evdev.c'
|
||||
x52d_cflags += '-DHAVE_EVDEV'
|
||||
x52d_linkwith += lib_libx52io
|
||||
x52d_deps += dep_evdev
|
||||
endif
|
||||
|
||||
exe_x52d = executable('x52d', x52d_sources,
|
||||
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
|
||||
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io]
|
||||
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h]
|
||||
x52d_cflags = []
|
||||
|
||||
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
||||
install: true,
|
||||
include_directories: includes,
|
||||
c_args: x52d_cflags,
|
||||
c_args: sym_hidden_cargs + x52d_cflags,
|
||||
dependencies: x52d_deps,
|
||||
link_with: x52d_linkwith)
|
||||
|
||||
executable('x52ctl', 'x52ctl.c',
|
||||
exe_x52ctl = executable('x52ctl', 'daemon_control.c',
|
||||
install: true,
|
||||
dependencies: [dep_intl],
|
||||
dependencies: [dep_intl, dep_config_h],
|
||||
include_directories: includes,
|
||||
link_with: lib_libx52dcomm)
|
||||
|
||||
install_data('x52d.conf',
|
||||
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
|
||||
|
||||
test('daemon-communication', files('test_daemon_comm.py')[0],
|
||||
depends: exe_x52d, protocol: 'tap')
|
||||
us_x52l = custom_target(
|
||||
'us-x52l',
|
||||
input: files('../data/layouts/us.layout'),
|
||||
output: 'us.x52l',
|
||||
command: [
|
||||
python,
|
||||
join_paths(meson.project_source_root(), 'tools', 'x52compile_layout.py'),
|
||||
'@INPUT@',
|
||||
'@OUTPUT@',
|
||||
],
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('datadir'), 'x52d'))
|
||||
|
||||
x52d_mouse_test_sources = ['x52d_mouse_test.c', 'x52d_mouse.c']
|
||||
test('daemon-communication', files('test_daemon_comm.py')[0],
|
||||
depends: [exe_x52d, exe_x52ctl], protocol: 'tap')
|
||||
|
||||
x52d_mouse_test_sources = ['mouse_test.c', 'mouse.c']
|
||||
x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_pinelog, dep_cmocka, dep_intl])
|
||||
dependencies: [dep_pinelog, dep_cmocka, dep_intl, dep_math, dep_config_h])
|
||||
|
||||
test('x52d-mouse-test', x52d_mouse_test, protocol: 'tap')
|
||||
|
||||
layout_usage_allowlist_test = executable('layout-usage-allowlist-test',
|
||||
'layout_usage_allowlist_test.c',
|
||||
'layout_usage_allowlist.c',
|
||||
build_by_default: false,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('layout-usage-allowlist', layout_usage_allowlist_test, protocol: 'tap')
|
||||
|
||||
crc32_test = executable('crc32-test', 'crc32_test.c', 'crc32.c',
|
||||
build_by_default: false,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('crc32', crc32_test, protocol: 'tap')
|
||||
|
||||
layout_load_test = executable('layout-load-test',
|
||||
'layout_load_test.c',
|
||||
'layout_load.c',
|
||||
'layout_usage_allowlist.c',
|
||||
'crc32.c',
|
||||
build_by_default: false,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('layout-load', layout_load_test, protocol: 'tap')
|
||||
|
||||
pymod_daemon = import('python')
|
||||
python_layout_test = pymod_daemon.find_installation('python3')
|
||||
test('layout-usage-allowlist-sync', python_layout_test,
|
||||
args: [join_paths(meson.project_source_root(), 'tools', 'test_layout_allowlist_sync.py')],
|
||||
protocol: 'tap')
|
||||
|
||||
test('layout-compile-py', python_layout_test,
|
||||
args: [join_paths(meson.project_source_root(), 'tools', 'test_x52compile_layout.py')],
|
||||
protocol: 'tap')
|
||||
|
||||
# Install service file
|
||||
if dep_systemd.found()
|
||||
systemd_system_unit_dir = get_option('systemd-unit-dir')
|
||||
if systemd_system_unit_dir == ''
|
||||
systemd_system_unit_dir = dep_systemd.get_variable(
|
||||
pkgconfig: 'systemd_system_unit_dir',
|
||||
default_value: '/lib/systemd/system')
|
||||
endif
|
||||
sed = find_program('sed')
|
||||
bindir_path = get_option('prefix') / get_option('bindir')
|
||||
sed_script = 's|%bindir%|' + bindir_path + '|g'
|
||||
|
||||
systemd_service_file = configure_file(
|
||||
input: 'x52d.service.in',
|
||||
output: 'x52d.service',
|
||||
command: [sed, sed_script, '@INPUT@'],
|
||||
capture: true,
|
||||
install: true,
|
||||
install_dir: systemd_system_unit_dir
|
||||
)
|
||||
endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <math.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/mouse.h>
|
||||
|
||||
// Mouse speed is the delay in microseconds between subsequent mouse reports
|
||||
#define DEFAULT_MOUSE_DELAY 70000
|
||||
#define MOUSE_DELAY_DELTA 5000
|
||||
#define MOUSE_DELAY_MIN 10000
|
||||
#define MOUSE_MULT_FACTOR 4
|
||||
#define MAX_MOUSE_MULT 5
|
||||
#define MIN_SENSITIVITY 10
|
||||
#define MAX_SENSITIVITY 500
|
||||
|
||||
volatile int mouse_scroll_dir = 1;
|
||||
volatile bool mouse_isometric_mode = false;
|
||||
volatile int mouse_curve_factor = 3;
|
||||
volatile int mouse_deadzone_factor = 0;
|
||||
volatile int mouse_sensitivity = 0;
|
||||
|
||||
static int clamp_int(const char *description, int value, int min, int max)
|
||||
{
|
||||
if (value < min) {
|
||||
PINELOG_DEBUG(_("Clamping %s value %d to range [%d..%d]"),
|
||||
description, value, min, max);
|
||||
return min;
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
PINELOG_DEBUG(_("Clamping %s value %d to range [%d..%d]"),
|
||||
description, value, min, max);
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Enabled(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse enable to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
x52d_mouse_thread_control(enabled);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Speed(int speed)
|
||||
{
|
||||
// DEPRECATED, calculate the sensitivity instead
|
||||
int new_delay;
|
||||
int new_mult;
|
||||
|
||||
int max_base_speed = (DEFAULT_MOUSE_DELAY - MOUSE_DELAY_MIN) / MOUSE_DELAY_DELTA;
|
||||
int max_speed = max_base_speed + MAX_MOUSE_MULT * MOUSE_MULT_FACTOR;
|
||||
|
||||
double sensitivity;
|
||||
|
||||
if (mouse_sensitivity == 0) {
|
||||
PINELOG_WARN(_("Config option 'mouse.speed' is DEPRECATED. Please use 'mouse.sensitivity' instead"));
|
||||
}
|
||||
|
||||
speed = clamp_int("mouse speed", speed, 0, max_speed);
|
||||
if (speed <= max_base_speed) {
|
||||
new_delay = DEFAULT_MOUSE_DELAY - speed * MOUSE_DELAY_DELTA;
|
||||
new_mult = MOUSE_MULT_FACTOR;
|
||||
} else {
|
||||
// speed between max_base_speed & max_speed
|
||||
new_delay = MOUSE_DELAY_MIN;
|
||||
new_mult = MOUSE_MULT_FACTOR + (speed - max_base_speed);
|
||||
}
|
||||
|
||||
sensitivity = round(1e6 / new_delay * new_mult / (double)MOUSE_MULT_FACTOR);
|
||||
|
||||
PINELOG_INFO(_("Migrating legacy mouse speed '%d' to sensitivity '%d' (percentage)"),
|
||||
speed, (int)sensitivity);
|
||||
mouse_sensitivity = clamp_int(_("speed -> sensitivity"), (int)sensitivity,
|
||||
MIN_SENSITIVITY, MAX_SENSITIVITY);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_ReverseScroll(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse reverse scroll to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
|
||||
if (enabled) {
|
||||
mouse_scroll_dir = -1;
|
||||
} else {
|
||||
mouse_scroll_dir = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_IsometricMode(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse isometric mode to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
mouse_isometric_mode = enabled;
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Sensitivity(int factor)
|
||||
{
|
||||
mouse_sensitivity = clamp_int(_("sensitivity"), factor,
|
||||
MIN_SENSITIVITY, MAX_SENSITIVITY);
|
||||
|
||||
PINELOG_DEBUG(_("Setting mouse sensitivity to %d%%"), mouse_sensitivity);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_CurveFactor(int factor)
|
||||
{
|
||||
// Factor ranges from 1-5, clamp it in this range
|
||||
// Shift by 1 so it uses the correct index
|
||||
mouse_curve_factor = clamp_int(_("curve factor"), factor, 1, 5) - 1;
|
||||
PINELOG_DEBUG(_("Setting mouse curve factor to %d"), mouse_curve_factor);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Deadzone(int factor)
|
||||
{
|
||||
// Factor ranges from 0-12, clamp it in this range
|
||||
mouse_deadzone_factor = clamp_int(_("deadzone factor"), factor, 0, 11);
|
||||
PINELOG_DEBUG(_("Setting mouse deadzone to %d"), mouse_deadzone_factor);
|
||||
}
|
||||
|
|
@ -10,17 +10,17 @@
|
|||
#define X52D_MOUSE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "libx52io.h"
|
||||
#include <libx52/libx52io.h>
|
||||
|
||||
extern volatile int mouse_delay;
|
||||
extern volatile int mouse_mult;
|
||||
extern volatile bool mouse_isometric_mode;
|
||||
extern volatile int mouse_scroll_dir;
|
||||
extern volatile int mouse_curve_factor;
|
||||
extern volatile int mouse_deadzone_factor;
|
||||
extern volatile int mouse_sensitivity;
|
||||
|
||||
#define MOUSE_MULT_FACTOR 4
|
||||
|
||||
void x52d_mouse_evdev_thread_control(bool enabled);
|
||||
void x52d_mouse_evdev_init(void);
|
||||
void x52d_mouse_evdev_exit(void);
|
||||
void x52d_mouse_thread_control(bool enabled);
|
||||
void x52d_mouse_handler_init(void);
|
||||
void x52d_mouse_handler_exit(void);
|
||||
void x52d_mouse_report_event(libx52io_report *report);
|
||||
|
||||
#endif // !defined X52D_MOUSE_H
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021-2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#include <math.h>
|
||||
|
||||
#include <libx52/libx52io.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/mouse.h>
|
||||
|
||||
static pthread_t mouse_thr;
|
||||
static bool mouse_thr_enabled = false;
|
||||
|
||||
static vkm_context *mouse_context;
|
||||
|
||||
static volatile libx52io_report old_report;
|
||||
static volatile libx52io_report new_report;
|
||||
|
||||
static int report_button_change(vkm_mouse_button button, int index)
|
||||
{
|
||||
vkm_result rc = VKM_ERROR_NO_CHANGE;
|
||||
bool old_button = old_report.button[index];
|
||||
bool new_button = new_report.button[index];
|
||||
vkm_button_state state;
|
||||
|
||||
if (old_button != new_button) {
|
||||
state = new_button ? VKM_BUTTON_PRESSED : VKM_BUTTON_RELEASED;
|
||||
rc = vkm_mouse_click(mouse_context, button, state);
|
||||
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
|
||||
PINELOG_ERROR(_("Error %d writing mouse button event (button %d, state %d)"),
|
||||
rc, button, (int)new_button);
|
||||
}
|
||||
}
|
||||
|
||||
return (rc == VKM_SUCCESS);
|
||||
}
|
||||
|
||||
static int report_wheel(void)
|
||||
{
|
||||
vkm_result rc = VKM_ERROR_NO_CHANGE;
|
||||
int wheel = 0;
|
||||
bool scroll_up = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
|
||||
bool scroll_dn = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
|
||||
bool old_scroll_up = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
|
||||
bool old_scroll_dn = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
|
||||
vkm_mouse_scroll_direction dir;
|
||||
|
||||
/*
|
||||
* Handle multiple scroll button presses in sequence. This happens if a
|
||||
* hardware axis is very noisy and the firmware sends a sequence of reports
|
||||
* with button down, even though this is technically a momentary button.
|
||||
*/
|
||||
scroll_up = scroll_up && !old_scroll_up;
|
||||
scroll_dn = scroll_dn && !old_scroll_dn;
|
||||
|
||||
if (scroll_up) {
|
||||
// Scroll up event
|
||||
wheel = 1 * mouse_scroll_dir;
|
||||
} else if (scroll_dn) {
|
||||
// Scroll down event
|
||||
wheel = -1 * mouse_scroll_dir;
|
||||
}
|
||||
|
||||
if (wheel != 0) {
|
||||
dir = (wheel == 1) ? VKM_MOUSE_SCROLL_UP : VKM_MOUSE_SCROLL_DOWN;
|
||||
rc = vkm_mouse_scroll(mouse_context, dir);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error writing mouse wheel event %d"), dir);
|
||||
}
|
||||
}
|
||||
|
||||
return (rc == VKM_SUCCESS);
|
||||
}
|
||||
|
||||
static inline int fsgn(double f)
|
||||
{
|
||||
return (f >= 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
static const double MOUSE_CURVE_FACTORS[5] = {
|
||||
1.0, 1.2, 1.5, 1.8, 2.0
|
||||
};
|
||||
|
||||
static const double MOUSE_DEADZONES[12] = {
|
||||
0.0, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5
|
||||
};
|
||||
|
||||
static int report_axis(void)
|
||||
{
|
||||
#define MAX_TICK_SPEED 250.0
|
||||
|
||||
static double accum_x = 0.0;
|
||||
static double accum_y = 0.0;
|
||||
|
||||
/* Center raw HID values (0,15) => (-8, 7) */
|
||||
int dx = new_report.axis[LIBX52IO_AXIS_THUMBX] - 8;
|
||||
int dy = new_report.axis[LIBX52IO_AXIS_THUMBY] - 8;
|
||||
|
||||
/* Calculate radial magnitude */
|
||||
double mag = sqrt((double)(dx * dx + dy * dy));
|
||||
double cfg_deadzone = MOUSE_DEADZONES[mouse_deadzone_factor];
|
||||
|
||||
/* Radial deadzone check */
|
||||
if (mag <= cfg_deadzone) {
|
||||
accum_x = 0.0;
|
||||
accum_y = 0.0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Calculate gain */
|
||||
double gain = (double)mouse_sensitivity / 100.0;
|
||||
double exponent = MOUSE_CURVE_FACTORS[mouse_curve_factor];
|
||||
|
||||
/* Normalize magnitude */
|
||||
double adj_mag = mag - cfg_deadzone;
|
||||
double out_x = 0.0;
|
||||
double out_y = 0.0;
|
||||
|
||||
if (mouse_isometric_mode) {
|
||||
/* Isometric mode: speed is a function of total distance */
|
||||
double speed = gain * pow(adj_mag, exponent);
|
||||
|
||||
/* Clamp total speed before breaking into components */
|
||||
if (speed > MAX_TICK_SPEED) {
|
||||
speed = MAX_TICK_SPEED;
|
||||
}
|
||||
|
||||
/* Unit vector * speed */
|
||||
out_x = (dx / mag) * speed;
|
||||
out_y = (dy / mag) * speed;
|
||||
} else {
|
||||
/* Linear mode: speed is independently calculated for X & Y axes */
|
||||
double ratio = adj_mag / mag;
|
||||
double cur_x = dx * ratio;
|
||||
double cur_y = dy * ratio;
|
||||
|
||||
out_x = fsgn(cur_x) * gain * pow(fabs(cur_x), exponent);
|
||||
out_y = fsgn(cur_y) * gain * pow(fabs(cur_y), exponent);
|
||||
|
||||
/* Clamp individual axis speeds */
|
||||
if (fabs(out_x) > MAX_TICK_SPEED) {
|
||||
out_x = fsgn(out_x) * MAX_TICK_SPEED;
|
||||
}
|
||||
|
||||
if (fabs(out_y) > MAX_TICK_SPEED) {
|
||||
out_y = fsgn(out_y) * MAX_TICK_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accumulate movement and independent resets */
|
||||
accum_x += out_x;
|
||||
accum_y += out_y;
|
||||
|
||||
if (dx == 0) {
|
||||
accum_x = 0.0;
|
||||
}
|
||||
if (dy == 0) {
|
||||
accum_y = 0.0;
|
||||
}
|
||||
|
||||
/* Extract integer values for VKM injection */
|
||||
int move_x = (int)accum_x;
|
||||
int move_y = (int)accum_y;
|
||||
|
||||
accum_x -= move_x;
|
||||
accum_y -= move_y;
|
||||
|
||||
vkm_result rc;
|
||||
rc = vkm_mouse_move(mouse_context, move_x, move_y);
|
||||
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
|
||||
PINELOG_ERROR(_("Error %d writing mouse axis event (dx %d, dy %d)"),
|
||||
rc, move_x, move_y);
|
||||
}
|
||||
|
||||
return (rc == VKM_SUCCESS);
|
||||
}
|
||||
|
||||
static void report_sync(void)
|
||||
{
|
||||
vkm_result rc;
|
||||
rc = vkm_sync(mouse_context);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error writing mouse sync event"));
|
||||
} else {
|
||||
memcpy((void *)&old_report, (void *)&new_report, sizeof(old_report));
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_reports(void)
|
||||
{
|
||||
memset((void *)&old_report, 0, sizeof(old_report));
|
||||
/* Set the default thumbstick values to the mid-point */
|
||||
old_report.axis[LIBX52IO_AXIS_THUMBX] = 8;
|
||||
old_report.axis[LIBX52IO_AXIS_THUMBY] = 8;
|
||||
memcpy((void *)&new_report, (void *)&old_report, sizeof(new_report));
|
||||
}
|
||||
|
||||
static void * x52_mouse_thr(void *param)
|
||||
{
|
||||
(void)param;
|
||||
|
||||
PINELOG_INFO(_("Starting X52 virtual mouse driver thread"));
|
||||
for (;;) {
|
||||
if (report_axis()) {
|
||||
report_sync();
|
||||
}
|
||||
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void x52d_mouse_thr_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Initializing virtual mouse driver");
|
||||
rc = pthread_create(&mouse_thr, NULL, x52_mouse_thr, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing mouse thread: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
static void x52d_mouse_thr_exit(void)
|
||||
{
|
||||
PINELOG_INFO(_("Shutting down X52 virtual mouse driver thread"));
|
||||
pthread_cancel(mouse_thr);
|
||||
}
|
||||
|
||||
void x52d_mouse_thread_control(bool enabled)
|
||||
{
|
||||
if (!vkm_is_ready(mouse_context)) {
|
||||
PINELOG_INFO(_("Virtual mouse not created. Ignoring thread state change"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (mouse_thr_enabled) {
|
||||
PINELOG_TRACE("Ignoring re-enable mouse thread");
|
||||
return;
|
||||
} else {
|
||||
reset_reports();
|
||||
x52d_mouse_thr_init();
|
||||
}
|
||||
} else {
|
||||
if (!mouse_thr_enabled) {
|
||||
PINELOG_TRACE("Ignoring re-disable mouse thread");
|
||||
return;
|
||||
} else {
|
||||
x52d_mouse_thr_exit();
|
||||
}
|
||||
}
|
||||
mouse_thr_enabled = enabled;
|
||||
}
|
||||
|
||||
void x52d_mouse_report_event(libx52io_report *report)
|
||||
{
|
||||
bool state_changed;
|
||||
if (report) {
|
||||
memcpy((void *)&new_report, report, sizeof(new_report));
|
||||
|
||||
if (!vkm_is_ready(mouse_context) || !mouse_thr_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state_changed = false;
|
||||
state_changed = (0 == report_button_change(VKM_MOUSE_BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY)) || state_changed;
|
||||
state_changed = (0 == report_button_change(VKM_MOUSE_BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY)) || state_changed;
|
||||
state_changed = (0 == report_wheel()) || state_changed;
|
||||
|
||||
if (state_changed) {
|
||||
report_sync();
|
||||
}
|
||||
} else {
|
||||
reset_reports();
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_mouse_handler_init(void)
|
||||
{
|
||||
vkm_result rc;
|
||||
|
||||
rc = vkm_init(&mouse_context);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
|
||||
return;
|
||||
}
|
||||
|
||||
vkm_set_option(mouse_context, VKM_OPT_DEVICE_NAME, "X52 virtual mouse");
|
||||
|
||||
rc = vkm_start(mouse_context);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_mouse_handler_exit(void)
|
||||
{
|
||||
x52d_mouse_thread_control(false);
|
||||
vkm_exit(mouse_context);
|
||||
mouse_context = NULL;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
|
@ -15,110 +15,102 @@
|
|||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_mouse.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/mouse.h>
|
||||
|
||||
#if defined HAVE_EVDEV
|
||||
/* Stub for evdev */
|
||||
void x52d_mouse_evdev_thread_control(bool enabled)
|
||||
/* Stub for handler */
|
||||
void x52d_mouse_thread_control(bool enabled)
|
||||
{
|
||||
function_called();
|
||||
check_expected(enabled);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void test_mouse_thread_enabled(void **state)
|
||||
{
|
||||
#if defined HAVE_EVDEV
|
||||
expect_function_calls(x52d_mouse_evdev_thread_control, 1);
|
||||
expect_value(x52d_mouse_evdev_thread_control, enabled, true);
|
||||
#endif
|
||||
(void)state;
|
||||
expect_function_calls(x52d_mouse_thread_control, 1);
|
||||
expect_value(x52d_mouse_thread_control, enabled, true);
|
||||
|
||||
x52d_cfg_set_Mouse_Enabled(true);
|
||||
}
|
||||
|
||||
static void test_mouse_thread_disabled(void **state)
|
||||
{
|
||||
#if defined HAVE_EVDEV
|
||||
expect_function_calls(x52d_mouse_evdev_thread_control, 1);
|
||||
expect_value(x52d_mouse_evdev_thread_control, enabled, false);
|
||||
#endif
|
||||
(void)state;
|
||||
expect_function_calls(x52d_mouse_thread_control, 1);
|
||||
expect_value(x52d_mouse_thread_control, enabled, false);
|
||||
|
||||
x52d_cfg_set_Mouse_Enabled(false);
|
||||
}
|
||||
|
||||
/* The following tests are dependent on the values in mouse.c */
|
||||
static void test_mouse_speed_negative(void **state)
|
||||
{
|
||||
int orig_mouse_delay = mouse_delay;
|
||||
int orig_mouse_mult = mouse_mult;
|
||||
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(-1);
|
||||
assert_int_equal(mouse_delay, orig_mouse_delay);
|
||||
assert_int_equal(mouse_mult, orig_mouse_mult);
|
||||
assert_int_equal(mouse_sensitivity, 14);
|
||||
}
|
||||
|
||||
/* The following tests are dependent on the values in x52d_mouse.c */
|
||||
static void test_mouse_speed_0(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(0);
|
||||
assert_int_equal(mouse_delay, 70000);
|
||||
assert_int_equal(mouse_mult, 4);
|
||||
assert_int_equal(mouse_sensitivity, 14);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_mid_base(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(6);
|
||||
assert_int_equal(mouse_delay, 40000);
|
||||
assert_int_equal(mouse_mult, 4);
|
||||
assert_int_equal(mouse_sensitivity, 25);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_max_base(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(12);
|
||||
assert_int_equal(mouse_delay, 10000);
|
||||
assert_int_equal(mouse_mult, 4);
|
||||
assert_int_equal(mouse_sensitivity, 100);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_min_hyper(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(13);
|
||||
assert_int_equal(mouse_delay, 10000);
|
||||
assert_int_equal(mouse_mult, 5);
|
||||
assert_int_equal(mouse_sensitivity, 125);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_mid_hyper(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(22);
|
||||
assert_int_equal(mouse_delay, 10000);
|
||||
assert_int_equal(mouse_mult, 14);
|
||||
assert_int_equal(mouse_sensitivity, 350);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_max_hyper(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(32);
|
||||
assert_int_equal(mouse_delay, 10000);
|
||||
assert_int_equal(mouse_mult, 24);
|
||||
assert_int_equal(mouse_sensitivity, 500);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_above_max(void **state)
|
||||
{
|
||||
int orig_mouse_delay = mouse_delay;
|
||||
int orig_mouse_mult = mouse_mult;
|
||||
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(33);
|
||||
assert_int_equal(mouse_delay, orig_mouse_delay);
|
||||
assert_int_equal(mouse_mult, orig_mouse_mult);
|
||||
assert_int_equal(mouse_sensitivity, 500);
|
||||
}
|
||||
|
||||
static void test_mouse_reverse_scroll_enabled(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_ReverseScroll(true);
|
||||
assert_int_equal(mouse_scroll_dir, -1);
|
||||
}
|
||||
|
||||
static void test_mouse_reverse_scroll_disabled(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_ReverseScroll(false);
|
||||
assert_int_equal(mouse_scroll_dir, 1);
|
||||
}
|
||||
|
|
@ -13,11 +13,11 @@
|
|||
|
||||
#define PINELOG_MODULE X52D_MOD_NOTIFY
|
||||
#include "pinelog.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_notify.h"
|
||||
#include "x52d_client.h"
|
||||
#include "x52dcomm.h"
|
||||
#include "x52dcomm-internal.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/client.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static pthread_t notify_thr;
|
||||
static pthread_t notify_listen;
|
||||
|
|
@ -71,6 +71,7 @@ static void * x52_notify_thr(void * param)
|
|||
char buffer[X52D_BUFSZ];
|
||||
uint16_t bufsiz;
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
for (;;) {
|
||||
do {
|
||||
|
|
@ -146,6 +147,7 @@ static void * x52_notify_loop(void * param)
|
|||
{
|
||||
struct pollfd pfd[MAX_CONN];
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
for (;;) {
|
||||
rc = x52d_client_poll(client_fd, pfd, notify_sock);
|
||||
|
|
@ -134,7 +134,11 @@ class Test:
|
|||
with open(daemon_cmdline[4], 'w', encoding='utf-8'):
|
||||
pass
|
||||
|
||||
self.daemon = subprocess.Popen(daemon_cmdline) # pylint: disable=consider-using-with
|
||||
env = os.environ.copy()
|
||||
# Uninstalled build: us.x52l lives next to the x52d binary (see daemon/meson.build).
|
||||
env['X52D_LAYOUT_DIR'] = os.path.dirname(self.program)
|
||||
|
||||
self.daemon = subprocess.Popen(daemon_cmdline, env=env) # pylint: disable=consider-using-with
|
||||
|
||||
print("# Sleeping 2 seconds for daemon to start")
|
||||
time.sleep(2)
|
||||
|
|
|
|||
|
|
@ -82,12 +82,44 @@ LED=128
|
|||
# Enabled controls whether the virtual mouse is enabled or not.
|
||||
Enabled=yes
|
||||
|
||||
# Speed is proportional to the speed of updates to the virtual mouse
|
||||
# Sensitivity is the sensitivity percentage of the virtual mouse. This
|
||||
# replaces the old Speed option, and is a percentage value by which to
|
||||
# scale the input. The sensitivity can vary from 10% to 500%.
|
||||
Sensitivity=100
|
||||
|
||||
# DEPRECATED: Speed is proportional to the speed of updates to the virtual mouse
|
||||
# This used a calculation with delays and multiplication factors to simulate
|
||||
# the mouse moves, but it felt choppy at lower speeds.
|
||||
Speed=0
|
||||
|
||||
# ReverseScroll reverses the direction of the virtual scroll wheel
|
||||
ReverseScroll=no
|
||||
|
||||
# Isometric mode controls if the mouse movement is computed based on
|
||||
# both X and Y movements. If enabled, the behavior is similar to the
|
||||
# mouse nubs found on some laptops. Otherwise, the X and Y movements
|
||||
# are independent of each other.
|
||||
IsometricMode=no
|
||||
|
||||
# Curve factor controls the speed curve in an exponential manner, so
|
||||
# that the user can get finer control at the lower end of motion, while
|
||||
# increasing speeds at the upper end. Values range from 1-5, with the
|
||||
# following descriptions. Values are clamped in this range.
|
||||
# 1: Linear motion - no curve
|
||||
# 2: Soft curve: slight dampening in the lower ranges
|
||||
# 3: Standard: Feels like a Thinkpad
|
||||
# 4: Precision: heavy dampening in lower ranges, high speed elsewhere
|
||||
# 5: Aggressive: "sniper" mode in the lower rnages, "flick" elsewhere
|
||||
CurveFactor=3
|
||||
|
||||
# Deadzone is a configurable value from 0-11, with 0 being no deadzone
|
||||
# and the deadzone size increasing with increasing values. This is useful
|
||||
# when there is a loose thumbstick and you want to restrict the motion
|
||||
# when there's no user input. A deadzone of 0 is perfectly fine for a
|
||||
# new joystick, but keep in mind that the higher values will require
|
||||
# you to push more to get any motion out of the virtual mouse.
|
||||
Deadzone=0
|
||||
|
||||
######################################################################
|
||||
# Profiles - only valid on Linux
|
||||
######################################################################
|
||||
|
|
@ -107,8 +139,12 @@ ClutchEnabled=no
|
|||
# be held down to remain in clutch mode.
|
||||
ClutchLatched=no
|
||||
|
||||
# KeyboardLayout is a basename only (alphanumeric, underscore, hyphen), not a path.
|
||||
# Resolves to $datadir/x52d/<basename>.x52l; default us uses the installed us.x52l pack.
|
||||
KeyboardLayout=us
|
||||
|
||||
##################
|
||||
#X52 Input Servic#
|
||||
#Version 0.2.2 #
|
||||
#Version 0.3.3 #
|
||||
#OS: Linux #
|
||||
##################
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_mouse.h"
|
||||
|
||||
// Mouse speed is the delay in microseconds between subsequent mouse reports
|
||||
#define DEFAULT_MOUSE_DELAY 70000
|
||||
#define MOUSE_DELAY_DELTA 5000
|
||||
#define MOUSE_DELAY_MIN 10000
|
||||
#define MAX_MOUSE_MULT 5
|
||||
|
||||
volatile int mouse_delay = DEFAULT_MOUSE_DELAY;
|
||||
volatile int mouse_mult = MOUSE_MULT_FACTOR;
|
||||
volatile int mouse_scroll_dir = 1;
|
||||
|
||||
void x52d_cfg_set_Mouse_Enabled(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse enable to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
#if defined HAVE_EVDEV
|
||||
x52d_mouse_evdev_thread_control(enabled);
|
||||
#endif
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Speed(int speed)
|
||||
{
|
||||
int new_delay;
|
||||
int new_mult;
|
||||
|
||||
int max_base_speed = (DEFAULT_MOUSE_DELAY - MOUSE_DELAY_MIN) / MOUSE_DELAY_DELTA;
|
||||
int max_speed = max_base_speed + MAX_MOUSE_MULT * MOUSE_MULT_FACTOR;
|
||||
|
||||
if (speed < 0 || speed > max_speed) {
|
||||
PINELOG_INFO(_("Ignoring mouse speed %d outside supported range (0-%d)"),
|
||||
speed, max_speed);
|
||||
return;
|
||||
} else if (speed <= max_base_speed) {
|
||||
new_delay = DEFAULT_MOUSE_DELAY - speed * MOUSE_DELAY_DELTA;
|
||||
new_mult = MOUSE_MULT_FACTOR;
|
||||
} else {
|
||||
// speed between max_base_speed & max_speed
|
||||
new_delay = MOUSE_DELAY_MIN;
|
||||
new_mult = MOUSE_MULT_FACTOR + (speed - max_base_speed);
|
||||
}
|
||||
|
||||
PINELOG_DEBUG(_("Setting mouse speed to %d (delay %d ms, multiplier %f)"),
|
||||
speed, new_delay / 1000, new_mult / (double)MOUSE_MULT_FACTOR);
|
||||
mouse_delay = new_delay;
|
||||
mouse_mult = new_mult;
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_ReverseScroll(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse reverse scroll to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
|
||||
if (enabled) {
|
||||
mouse_scroll_dir = -1;
|
||||
} else {
|
||||
mouse_scroll_dir = 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "libevdev/libevdev.h"
|
||||
#include "libevdev/libevdev-uinput.h"
|
||||
#include "libx52io.h"
|
||||
|
||||
#include "pinelog.h"
|
||||
#include "x52d_config.h"
|
||||
#include "x52d_const.h"
|
||||
#include "x52d_mouse.h"
|
||||
|
||||
static pthread_t mouse_thr;
|
||||
static bool mouse_thr_enabled = false;
|
||||
|
||||
static struct libevdev_uinput *mouse_uidev;
|
||||
static bool mouse_uidev_created = false;
|
||||
|
||||
static volatile libx52io_report old_report;
|
||||
static volatile libx52io_report new_report;
|
||||
|
||||
static int report_button_change(int button, int index)
|
||||
{
|
||||
int rc = 1;
|
||||
bool old_button = old_report.button[index];
|
||||
bool new_button = new_report.button[index];
|
||||
|
||||
if (old_button != new_button) {
|
||||
rc = libevdev_uinput_write_event(mouse_uidev, EV_KEY, button,
|
||||
(int)new_button);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error writing mouse button event (button %d, state %d)"),
|
||||
button, (int)new_button);
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int report_wheel(void)
|
||||
{
|
||||
int rc = 1;
|
||||
int wheel = 0;
|
||||
bool scroll_up = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
|
||||
bool scroll_dn = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
|
||||
|
||||
if (scroll_up) {
|
||||
// Scroll up event
|
||||
wheel = 1 * mouse_scroll_dir;
|
||||
} else if (scroll_dn) {
|
||||
// Scroll down event
|
||||
wheel = -1 * mouse_scroll_dir;
|
||||
}
|
||||
|
||||
if (wheel != 0) {
|
||||
rc = libevdev_uinput_write_event(mouse_uidev, EV_REL, REL_WHEEL, wheel);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error writing mouse wheel event %d"), wheel);
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int report_axis(int axis, int index)
|
||||
{
|
||||
int rc = 1;
|
||||
|
||||
int axis_val = new_report.axis[index];
|
||||
|
||||
/*
|
||||
* Axis value ranges from 0 to 15, with the default midpoint at 8.
|
||||
* We need to translate this to a range of -7 to +7. Since the midpoint
|
||||
* is slightly off-center, we will shift the values left, and subtract
|
||||
* 15, effectively, giving us a range of -15 to +15. Shifting right again
|
||||
* will reduce the range to -7 to +7, and effectively ignore the reported
|
||||
* values of 7 and 8.
|
||||
*/
|
||||
axis_val = ((axis_val << 1) - 15) >> 1;
|
||||
|
||||
/*
|
||||
* Factor in the multiplicative factor for the axis. This deliberately
|
||||
* uses integer division, since the uinput event only accepts integers.
|
||||
* For the speed purposes, this should be good enough.
|
||||
*/
|
||||
axis_val = (axis_val * mouse_mult) / MOUSE_MULT_FACTOR;
|
||||
|
||||
if (axis_val) {
|
||||
rc = libevdev_uinput_write_event(mouse_uidev, EV_REL, axis, axis_val);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error writing mouse axis event (axis %d, value %d)"),
|
||||
axis, axis_val);
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void report_sync(void)
|
||||
{
|
||||
int rc;
|
||||
rc = libevdev_uinput_write_event(mouse_uidev, EV_SYN, SYN_REPORT, 0);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error writing mouse sync event"));
|
||||
} else {
|
||||
memcpy((void *)&old_report, (void *)&new_report, sizeof(old_report));
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_reports(void)
|
||||
{
|
||||
memset((void *)&old_report, 0, sizeof(old_report));
|
||||
/* Set the default thumbstick values to the mid-point */
|
||||
old_report.axis[LIBX52IO_AXIS_THUMBX] = 8;
|
||||
old_report.axis[LIBX52IO_AXIS_THUMBY] = 8;
|
||||
memcpy((void *)&new_report, (void *)&old_report, sizeof(new_report));
|
||||
}
|
||||
|
||||
static void * x52_mouse_thr(void *param)
|
||||
{
|
||||
bool state_changed;
|
||||
|
||||
PINELOG_INFO(_("Starting X52 virtual mouse driver thread"));
|
||||
for (;;) {
|
||||
state_changed = false;
|
||||
state_changed |= (0 == report_axis(REL_X, LIBX52IO_AXIS_THUMBX));
|
||||
state_changed |= (0 == report_axis(REL_Y, LIBX52IO_AXIS_THUMBY));
|
||||
|
||||
if (state_changed) {
|
||||
report_sync();
|
||||
}
|
||||
|
||||
usleep(mouse_delay);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void x52d_mouse_thr_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Initializing virtual mouse driver");
|
||||
rc = pthread_create(&mouse_thr, NULL, x52_mouse_thr, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing mouse thread: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
static void x52d_mouse_thr_exit(void)
|
||||
{
|
||||
PINELOG_INFO(_("Shutting down X52 virtual mouse driver thread"));
|
||||
pthread_cancel(mouse_thr);
|
||||
}
|
||||
|
||||
void x52d_mouse_evdev_thread_control(bool enabled)
|
||||
{
|
||||
if (!mouse_uidev_created) {
|
||||
PINELOG_INFO(_("Virtual mouse not created. Ignoring thread state change"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (mouse_thr_enabled) {
|
||||
PINELOG_TRACE("Ignoring re-enable mouse thread");
|
||||
return;
|
||||
} else {
|
||||
reset_reports();
|
||||
x52d_mouse_thr_init();
|
||||
}
|
||||
} else {
|
||||
if (!mouse_thr_enabled) {
|
||||
PINELOG_TRACE("Ignoring re-disable mouse thread");
|
||||
return;
|
||||
} else {
|
||||
x52d_mouse_thr_exit();
|
||||
}
|
||||
}
|
||||
mouse_thr_enabled = enabled;
|
||||
}
|
||||
|
||||
void x52d_mouse_report_event(libx52io_report *report)
|
||||
{
|
||||
bool state_changed;
|
||||
if (report) {
|
||||
memcpy((void *)&new_report, report, sizeof(new_report));
|
||||
|
||||
if (!mouse_uidev_created || !mouse_thr_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state_changed = false;
|
||||
state_changed |= (0 == report_button_change(BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY));
|
||||
state_changed |= (0 == report_button_change(BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY));
|
||||
state_changed |= (0 == report_wheel());
|
||||
|
||||
if (state_changed) {
|
||||
report_sync();
|
||||
}
|
||||
} else {
|
||||
reset_reports();
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_mouse_evdev_init(void)
|
||||
{
|
||||
int rc;
|
||||
struct libevdev *dev;
|
||||
|
||||
/* Create a new mouse device */
|
||||
dev = libevdev_new();
|
||||
libevdev_set_name(dev, "X52 virtual mouse");
|
||||
libevdev_enable_event_type(dev, EV_REL);
|
||||
libevdev_enable_event_code(dev, EV_REL, REL_X, NULL);
|
||||
libevdev_enable_event_code(dev, EV_REL, REL_Y, NULL);
|
||||
libevdev_enable_event_code(dev, EV_REL, REL_WHEEL, NULL);
|
||||
libevdev_enable_event_type(dev, EV_KEY);
|
||||
libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, NULL);
|
||||
libevdev_enable_event_code(dev, EV_KEY, BTN_RIGHT, NULL);
|
||||
|
||||
rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED,
|
||||
&mouse_uidev);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error %d creating X52 virtual mouse: %s"),
|
||||
-rc, strerror(-rc));
|
||||
} else {
|
||||
mouse_uidev_created = true;
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_mouse_evdev_exit(void)
|
||||
{
|
||||
x52d_mouse_evdev_thread_control(false);
|
||||
mouse_uidev_created = false;
|
||||
libevdev_uinput_destroy(mouse_uidev);
|
||||
}
|
||||
|
|
@ -22,6 +22,16 @@
|
|||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifndef X52DCOMM_API
|
||||
# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
|
||||
# define X52DCOMM_API __attribute__((visibility("default")))
|
||||
# elif defined(_WIN32)
|
||||
# define X52DCOMM_API __declspec(dllexport)
|
||||
# else
|
||||
# define X52DCOMM_API
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
|
@ -52,7 +62,7 @@ extern "C" {
|
|||
*
|
||||
* @exception E2BIG returned if the passed socket path is too big
|
||||
*/
|
||||
int x52d_dial_command(const char *sock_path);
|
||||
X52DCOMM_API int x52d_dial_command(const char *sock_path);
|
||||
|
||||
/**
|
||||
* @brief Open a connection to the daemon notify socket.
|
||||
|
|
@ -73,7 +83,7 @@ int x52d_dial_command(const char *sock_path);
|
|||
*
|
||||
* @exception E2BIG returned if the passed socket path is too big
|
||||
*/
|
||||
int x52d_dial_notify(const char *sock_path);
|
||||
X52DCOMM_API int x52d_dial_notify(const char *sock_path);
|
||||
|
||||
/**
|
||||
* @brief Format a series of command strings into a buffer
|
||||
|
|
@ -92,7 +102,7 @@ int x52d_dial_notify(const char *sock_path);
|
|||
* @returns number of bytes in the formatted command
|
||||
* @returns -1 on an error condition, and \c errno is set accordingly.
|
||||
*/
|
||||
int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
|
||||
X52DCOMM_API int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen);
|
||||
|
||||
/**
|
||||
* @brief Send a command to the daemon and retrieve the response.
|
||||
|
|
@ -121,7 +131,7 @@ int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen
|
|||
* @returns number of bytes returned from the server
|
||||
* @returns -1 on an error condition, and \c errno is set accordingly.
|
||||
*/
|
||||
int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
|
||||
X52DCOMM_API int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout);
|
||||
|
||||
/**
|
||||
* @brief Notification callback function type
|
||||
|
|
@ -147,7 +157,7 @@ typedef int (* x52d_notify_callback_fn)(int argc, char **argv);
|
|||
* @returns return code of the callback function on success
|
||||
* @returns -1 on an error condition, and \c errno is set accordingly.
|
||||
*/
|
||||
int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
|
||||
X52DCOMM_API int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback);
|
||||
|
||||
/** @} */
|
||||
#ifdef __cplusplus
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
# US QWERTY — main block (letters, digits, punctuation, space, controls)
|
||||
name: us
|
||||
description: US QWERTY main block
|
||||
|
||||
U+0008 BACKSPACE
|
||||
U+0009 TAB
|
||||
U+000A ENTER
|
||||
U+000D ENTER
|
||||
U+001B ESCAPE
|
||||
U+0020 SPACE
|
||||
|
||||
a a
|
||||
b b
|
||||
c c
|
||||
d d
|
||||
e e
|
||||
f f
|
||||
g g
|
||||
h h
|
||||
i i
|
||||
j j
|
||||
k k
|
||||
l l
|
||||
m m
|
||||
n n
|
||||
o o
|
||||
p p
|
||||
q q
|
||||
r r
|
||||
s s
|
||||
t t
|
||||
u u
|
||||
v v
|
||||
w w
|
||||
x x
|
||||
y y
|
||||
z z
|
||||
|
||||
A SHIFT+a
|
||||
B SHIFT+b
|
||||
C SHIFT+c
|
||||
D SHIFT+d
|
||||
E SHIFT+e
|
||||
F SHIFT+f
|
||||
G SHIFT+g
|
||||
H SHIFT+h
|
||||
I SHIFT+i
|
||||
J SHIFT+j
|
||||
K SHIFT+k
|
||||
L SHIFT+l
|
||||
M SHIFT+m
|
||||
N SHIFT+n
|
||||
O SHIFT+o
|
||||
P SHIFT+p
|
||||
Q SHIFT+q
|
||||
R SHIFT+r
|
||||
S SHIFT+s
|
||||
T SHIFT+t
|
||||
U SHIFT+u
|
||||
V SHIFT+v
|
||||
W SHIFT+w
|
||||
X SHIFT+x
|
||||
Y SHIFT+y
|
||||
Z SHIFT+z
|
||||
|
||||
1 1
|
||||
2 2
|
||||
3 3
|
||||
4 4
|
||||
5 5
|
||||
6 6
|
||||
7 7
|
||||
8 8
|
||||
9 9
|
||||
0 0
|
||||
! SHIFT+1
|
||||
@ SHIFT+2
|
||||
# SHIFT+3
|
||||
$ SHIFT+4
|
||||
% SHIFT+5
|
||||
^ SHIFT+6
|
||||
& SHIFT+7
|
||||
* SHIFT+8
|
||||
( SHIFT+9
|
||||
) SHIFT+0
|
||||
_ SHIFT+MINUS
|
||||
+ SHIFT+EQUAL
|
||||
|
||||
[ LEFT_BRACKET
|
||||
{ SHIFT+LEFT_BRACKET
|
||||
] RIGHT_BRACKET
|
||||
} SHIFT+RIGHT_BRACKET
|
||||
\ BACKSLASH
|
||||
| SHIFT+BACKSLASH
|
||||
|
||||
; SEMICOLON
|
||||
: SHIFT+SEMICOLON
|
||||
' APOSTROPHE
|
||||
" SHIFT+APOSTROPHE
|
||||
` GRAVE
|
||||
~ SHIFT+GRAVE
|
||||
|
||||
, COMMA
|
||||
< SHIFT+COMMA
|
||||
. PERIOD
|
||||
> SHIFT+PERIOD
|
||||
/ SLASH
|
||||
? SHIFT+SLASH
|
||||
|
||||
- MINUS
|
||||
= EQUAL
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
FROM archlinux:base-devel
|
||||
|
||||
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
|
||||
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
|
||||
|
||||
COPY ./install-dependencies-archlinux.sh ./ci-setup.sh /tmp/
|
||||
|
||||
RUN /tmp/install-dependencies-archlinux.sh && /tmp/ci-setup.sh
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
FROM fedora:latest
|
||||
|
||||
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
|
||||
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
|
||||
|
||||
COPY ./install-dependencies-fedora.sh ./ci-setup.sh /tmp/
|
||||
|
||||
RUN /tmp/install-dependencies-fedora.sh && dnf clean all && /tmp/ci-setup.sh
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
FROM ubuntu:22.04
|
||||
|
||||
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
|
||||
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY ./install-dependencies-ubuntu.sh ./ci-setup.sh /tmp/
|
||||
|
||||
RUN /tmp/install-dependencies-ubuntu.sh && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
/tmp/ci-setup.sh
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
|
||||
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY ./install-dependencies-ubuntu.sh ./ci-setup.sh /tmp/
|
||||
|
||||
RUN /tmp/install-dependencies-ubuntu.sh && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
/tmp/ci-setup.sh
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
FROM ubuntu:26.04
|
||||
|
||||
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
|
||||
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
|
||||
LABEL com.project.ci.experimental="true"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY ./install-dependencies-ubuntu.sh ./ci-setup.sh /tmp/
|
||||
|
||||
RUN /tmp/install-dependencies-ubuntu.sh && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
/tmp/ci-setup.sh
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
Dockerfiles for CI builds
|
||||
=========================
|
||||
|
||||
This repo contains a number of Dockerfiles for building libx52 against multiple
|
||||
distros. Github only supports Ubuntu LTS for it's Linux runners, but libx52
|
||||
users run multiple distros, including Arch Linux, Gentoo and Fedora.
|
||||
|
||||
For this reason, it's better to get a prebuilt Docker image that builds the CI
|
||||
environment, pulling in all the relevant dependencies, and staging the CI build
|
||||
environment as a container within Github. The regular build pipeline can then
|
||||
run on these containers, and this will help ensure that the breakages are
|
||||
reduced to a minimum.
|
||||
|
||||
Build Instructions
|
||||
==================
|
||||
|
||||
In order to run the CI build on all the supported platforms, first run the
|
||||
`build-containers.sh` script, it will find the Dockerfiles for all the
|
||||
suppported platforms in the `docker` directory, and build the container images
|
||||
for them with the necessary dependencies included.
|
||||
|
||||
Once the container images are built, you can build against one or all of the
|
||||
container images by running `build-repo.sh`. If you don't specify the container
|
||||
distro, which is basically the same as the extension after `Dockerfile.`, it
|
||||
will run everything. If you specify a distro that doesn't exist, or has not been
|
||||
built, the script will silently exit.
|
||||
|
||||
Extending to a new distro
|
||||
=========================
|
||||
|
||||
To extend the builds to a new distro, create a `Dockerfile.<distro>` with the
|
||||
necessary instructions to build a container image for that distro. Make sure you
|
||||
install the necessary dependencies for the distro. It is strongly recommended to
|
||||
add a `install-dependencies-<distro>.sh` so that you can build against multiple
|
||||
versions of that distro, eg. Ubuntu 22.04 and Ubuntu 24.04. Make sure you copy
|
||||
`ci-setup.sh` to allow setting up the environment, since it is highly likely
|
||||
that the `meson dist` command will fail if you do not run that script.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
# Build containers for all the target environments
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
if command -v realpath &>/dev/null
|
||||
then
|
||||
SCRIPT_DIR=$(realpath "$SCRIPT_DIR")
|
||||
fi
|
||||
|
||||
GITHUB_SCRIPTS_DIR="$SCRIPT_DIR/../.github/scripts"
|
||||
if command -v realpath &>/dev/null
|
||||
then
|
||||
GITHUB_SCRIPTS_DIR=$(realpath "$GITHUB_SCRIPTS_DIR")
|
||||
fi
|
||||
|
||||
for dockerfile in "$SCRIPT_DIR"/[Dd]ockerfile.*
|
||||
do
|
||||
if [[ -e "$dockerfile" ]]
|
||||
then
|
||||
SUFFIX="$(basename "$dockerfile" | cut -d. -f2)"
|
||||
TAG="ghcr.io/nirenjan/libx52/ci-build-${SUFFIX}:latest"
|
||||
docker build --tag "$TAG" -f "$dockerfile" "${SCRIPT_DIR}"
|
||||
fi
|
||||
done
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
# Build the repository for all found directories
|
||||
# Build containers for all the target environments
|
||||
set -euo pipefail
|
||||
|
||||
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||||
CC=${CC:-gcc}
|
||||
|
||||
IMAGE="${1:-*}"
|
||||
|
||||
for image in $(docker images --filter "reference=ghcr.io/nirenjan/libx52/ci-build-${IMAGE}" --format '{{ .Repository}}')
|
||||
do
|
||||
distro=${image##*/ci-build-}
|
||||
container_name="libx52-runner-${distro}"
|
||||
|
||||
if [[ "$(docker ps -aq -f name=$container_name)" ]]
|
||||
then
|
||||
echo "Cleaning up old container for '$distro'"
|
||||
|
||||
docker rm -f $container_name >/dev/null
|
||||
fi
|
||||
|
||||
experimental=$(docker inspect --format='{{.Config.Labels}}' $image | \
|
||||
grep -q 'experimental:true' && echo " (experimental)" || true)
|
||||
|
||||
if docker run --rm --name $container_name \
|
||||
--device /dev/bus/usb:/dev/bus/usb \
|
||||
-v "$GIT_ROOT":/code \
|
||||
-w /code \
|
||||
-e CC="${CC}" \
|
||||
$image \
|
||||
/bin/bash -c ".github/scripts/build-and-test.sh builddir/${distro}" \
|
||||
&> "$GIT_ROOT/build-${distro}.log"
|
||||
then
|
||||
echo "=== ${distro}${experimental} OK ==="
|
||||
else
|
||||
echo "=== ${distro}${experimental} !!! FAIL !!! ==="
|
||||
tail -20 "$GIT_ROOT/build-${distro}.log"
|
||||
fi
|
||||
done
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
# Common CI setup
|
||||
|
||||
git config --global --add safe.directory /code
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
cat >> /root/.bashrc <<"EOF"
|
||||
echo 'WARNING: This is an internal CI container'
|
||||
echo 'Do not use for production'
|
||||
EOF
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# This file is deliberately named with a lowercase d, in order to avoid the
|
||||
# Github action logic from picking this up as a supported distro. The Alpine
|
||||
# image fails in the Github actions, because it needs the /dev/bus/usb device
|
||||
# mounted inside the container, however, attempting to do so causes every
|
||||
# build to fail. Therefore, we disable the Alpine image in CI, but keep it
|
||||
# local, so that we can test the build against Alpine locally if necessary.
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL org.opencontainers.image.description="INTERNAL CI USE ONLY - Not intended for production"
|
||||
LABEL org.opencontainers.image.authors="Nirenjan Krishnan"
|
||||
LABEL com.project.ci.experimental="true"
|
||||
|
||||
COPY ./install-dependencies-alpine.sh ./ci-setup.sh /tmp/
|
||||
RUN /tmp/install-dependencies-alpine.sh
|
||||
RUN /tmp/ci-setup.sh
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/sh
|
||||
# Setup for alpine
|
||||
set -eux
|
||||
|
||||
apk update
|
||||
apk add --no-cache \
|
||||
build-base \
|
||||
meson \
|
||||
bash \
|
||||
git \
|
||||
gettext \
|
||||
libusb-dev \
|
||||
hidapi-dev \
|
||||
libevdev-dev \
|
||||
inih-dev \
|
||||
cmocka-dev \
|
||||
tzdata \
|
||||
musl-libintl \
|
||||
doxygen
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
# Install dependencies on Archlinux container
|
||||
# Assumes that it's running off a base-devel tag
|
||||
set -euo pipefail
|
||||
|
||||
pacman -Syu --noconfirm \
|
||||
git \
|
||||
meson \
|
||||
libusb \
|
||||
hidapi \
|
||||
libinih \
|
||||
libevdev \
|
||||
python \
|
||||
gettext
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
# Install dependencies on Fedora container
|
||||
set -euo pipefail
|
||||
|
||||
dnf update -y
|
||||
dnf install -y \
|
||||
gcc \
|
||||
git \
|
||||
meson \
|
||||
libusb1-devel \
|
||||
hidapi-devel \
|
||||
inih-devel \
|
||||
libevdev-devel \
|
||||
pkg-config \
|
||||
python3 \
|
||||
gettext-devel
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
# Install dependencies to build and test on Ubuntu runners
|
||||
apt-get update && apt-get upgrade -y
|
||||
apt-get install -y \
|
||||
git \
|
||||
gcc clang \
|
||||
meson \
|
||||
gettext \
|
||||
pkg-config \
|
||||
python3 \
|
||||
libusb-1.0-0-dev \
|
||||
libhidapi-dev \
|
||||
libevdev-dev \
|
||||
libinih-dev \
|
||||
doxygen \
|
||||
libcmocka-dev \
|
||||
tzdata
|
||||
|
||||
exit 0
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Automake for documentation
|
||||
#
|
||||
# Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
EXTRA_DIST += \
|
||||
docs/main.dox \
|
||||
docs/caveats.dox \
|
||||
docs/integration.dox
|
||||
|
|
@ -13,10 +13,9 @@ up the list of devices manually and simulate various scenarios.
|
|||
|
||||
# Design Overview
|
||||
|
||||
Unfortunately, the automake infrastructure does not support the use of
|
||||
LD_PRELOAD because it is deemed "non-portable" in the automake sense. As a
|
||||
result, this is now up to a test runner application to implement a method to
|
||||
control the data passed between two processes.
|
||||
The build system does not wire up `LD_PRELOAD` for this library: using it is
|
||||
left to the test runner, which must arrange for the stub to be preloaded and
|
||||
control the data passed between processes.
|
||||
|
||||
# Data Structures
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ to a supported joystick. This function must be called after \ref libx52_init
|
|||
<b>Example Initialization/Deinitialization Code</b>
|
||||
|
||||
@code{.c}
|
||||
#include <libx52.h>
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
libx52_device* init_libx52(void)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ with your application.
|
|||
See the documentation for the following files for a complete list of all
|
||||
functions.
|
||||
|
||||
- libx52.h
|
||||
- libx52io.h
|
||||
- libx52util.h
|
||||
- libx52/libx52.h
|
||||
- libx52/libx52io.h
|
||||
- libx52/libx52util.h
|
||||
- vkm/vkm.h
|
||||
- libx52/x52dcomm.h
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# Automake for x52evtest
|
||||
#
|
||||
# Copyright (C) 2012-2020 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
bin_PROGRAMS += x52evtest
|
||||
|
||||
# Event test utility that works similarly to the Linux evtest
|
||||
x52evtest_SOURCES = evtest/ev_test.c
|
||||
x52evtest_CFLAGS = -I $(top_srcdir)/libx52io -I $(top_srcdir) -DLOCALEDIR=\"$(localedir)\" $(WARN_CFLAGS)
|
||||
x52evtest_LDFLAGS = $(WARN_LDFLAGS)
|
||||
x52evtest_LDADD = libx52io.la
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include "libx52io.h"
|
||||
#include <libx52/libx52io.h>
|
||||
#include "gettext.h"
|
||||
|
||||
/*
|
||||
|
|
@ -44,6 +44,7 @@ static bool exit_loop = false;
|
|||
|
||||
static void signal_handler(int sig)
|
||||
{
|
||||
(void)sig;
|
||||
exit_loop = true;
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ static bool denoise = true;
|
|||
|
||||
/* For i18n */
|
||||
#define _(x) gettext(x)
|
||||
int main(int argc, char **argv)
|
||||
int main(void)
|
||||
{
|
||||
libx52io_context *ctx;
|
||||
libx52io_report last, curr;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/sh
|
||||
# Install Doxygen HTML and man trees from the build directory.
|
||||
# Arguments are paths relative to the install prefix (MESON_INSTALL_DESTDIR_PREFIX).
|
||||
set -e
|
||||
|
||||
doc_html="$1"
|
||||
mandir="$2"
|
||||
|
||||
WANTED_PAGES="man1/x52cli.1 man1/x52bugreport.1"
|
||||
|
||||
if [ -d "$MESON_BUILD_ROOT/docs/html" ]; then
|
||||
mkdir -p "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html"
|
||||
cp -R "$MESON_BUILD_ROOT/docs/html"/. "$MESON_INSTALL_DESTDIR_PREFIX/$doc_html/"
|
||||
fi
|
||||
|
||||
if [ -d "$MESON_BUILD_ROOT/docs/man" ]; then
|
||||
MANDIR="$MESON_INSTALL_DESTDIR_PREFIX/$mandir"
|
||||
mkdir -p "$MANDIR"
|
||||
for manpage in $WANTED_PAGES
|
||||
do
|
||||
section=$(dirname "$manpage")
|
||||
mkdir -p "$MANDIR/$section"
|
||||
cp "$MESON_BUILD_ROOT/docs/man/$manpage" "$MANDIR/$section"
|
||||
done
|
||||
fi
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Automake for x52test
|
||||
#
|
||||
# Copyright (C) 2012-2018 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
bin_PROGRAMS += x52test
|
||||
|
||||
# Test utility that exercises all the library functions
|
||||
x52test_SOURCES = \
|
||||
joytest/x52_test.c \
|
||||
joytest/x52_test_mfd.c \
|
||||
joytest/x52_test_led.c \
|
||||
joytest/x52_test_clock.c
|
||||
x52test_CFLAGS = -I $(top_srcdir)/libx52 -I $(top_srcdir) -DLOCALEDIR=\"$(localedir)\" $(WARN_CFLAGS)
|
||||
x52test_LDFLAGS = $(WARN_LDFLAGS)
|
||||
x52test_LDADD = libx52.la
|
||||
|
||||
# Extra files that need to be in the distribution
|
||||
EXTRA_DIST += joytest/x52_test_common.h
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
#include <locale.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include "x52_test_common.h"
|
||||
libx52_device *dev;
|
||||
int test_exit;
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include "x52_test_common.h"
|
||||
|
||||
int test_clock(void)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
#ifndef X52_TEST_COMMON_H
|
||||
#define X52_TEST_COMMON_H
|
||||
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include "gettext.h"
|
||||
|
||||
extern libx52_device *dev;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include "x52_test_common.h"
|
||||
|
||||
#define TEST_LED(name, state) do { \
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "libx52.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include "x52_test_common.h"
|
||||
|
||||
#define TEST_BRIGHTNESS(mfd, value) TEST(brightness, mfd, value)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ and therefore, do not require this driver.
|
|||
|
||||
# Building
|
||||
|
||||
This directory is deliberately not integrated with the top level Autotools
|
||||
based build framework.
|
||||
This directory is deliberately not integrated with the top-level Meson build.
|
||||
|
||||
Install the Linux headers for the currently running kernel. On Ubuntu, this
|
||||
can be done by running `sudo apt-get install -y linux-headers-$(uname -r)`.
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
# Automake for libusbx52 and associated utilities
|
||||
#
|
||||
# Copyright (C) 2012-2018 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
# libusb stub library for use by test programs
|
||||
check_LTLIBRARIES += libusbx52.la
|
||||
|
||||
libusbx52_la_SOURCES = libusbx52/usb_x52_stub.c libusbx52/fopen_env.c
|
||||
libusbx52_la_CFLAGS = -I $(top_srcdir)/libusbx52 @LIBUSB_CFLAGS@ $(WARN_CFLAGS)
|
||||
libusbx52_la_LDFLAGS = -rpath /nowhere -module $(WARN_LDFLAGS)
|
||||
|
||||
# Utility programs for use by tests
|
||||
check_PROGRAMS += x52test_create_device_list x52test_log_actions
|
||||
|
||||
x52test_create_device_list_SOURCES = libusbx52/util/create_device_list.c $(libusbx52_la_SOURCES)
|
||||
x52test_create_device_list_CFLAGS = $(libusbx52_la_CFLAGS)
|
||||
x52test_create_device_list_LDFLAGS = $(WARN_LDFLAGS)
|
||||
|
||||
x52test_log_actions_SOURCES = libusbx52/util/log_actions.c $(libusbx52_la_SOURCES)
|
||||
x52test_log_actions_CFLAGS = -I $(top_srcdir)/libx52 $(libusbx52_la_CFLAGS)
|
||||
x52test_log_actions_LDFLAGS = $(WARN_LDFLAGS)
|
||||
|
||||
EXTRA_DIST += libusbx52/README.md libusbx52/libusbx52.h
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue