Compare commits

..

51 Commits

Author SHA1 Message Date
nirenjan 75f0125f54 feat: Add layout file parsing to x52d
This change is an initial step to adding support for profiles in x52d.
This will allow the user to create a keyboard layout in an easy to
read/write text based format, and have it compiled into a flat layout
that's easy for the daemon to parse and load into memory. This layout
can then be used to map a user's action key to the actual input usage
needed. This is necessary, because keyboards don't actually send the
character that is typed, but just the position on the keyboard. For
example, on a French AZERTY keyboard, the A key would actually send the
usage for `Keyboard_q_and_Q`. The OS would translate that into the
letter 'a' (or 'A' if Shift key is held) and pass that to the active
window.

This commit adds the full logic necessary for the layout loading,
validation and compiling, as well as tests for the compiler and loader.
2026-04-04 23:16:51 -07:00
nirenjan f52e328a8b
Merge pull request #66 from nirenjan/restructure-layout
Restructure layout
2026-04-04 00:02:11 -07:00
nirenjan 806a88c93d refactor: Remove vkm_ prefix from VKM source files 2026-04-03 23:54:58 -07:00
nirenjan c0c8787331 fix: Handle test case with PINELOG_STRIP_FILE_PATH 2026-04-03 23:50:18 -07:00
nirenjan de465fbf6a refactor: Rename daemon sources to remove x52d_ prefix 2026-04-03 23:50:18 -07:00
nirenjan 583d4fd646 feat: Make prefix stripping optional in pinelog
Prior to this change, pinelog would always strip the directory prefix
from __FILE__, displaying only the basename.

This change adds the ability to check the full path passed to the
compiler, which may still be only a basename, but that's still fine.
2026-04-03 23:12:01 -07:00
nirenjan 836206d93f build: Rename source files to remove prefixes
This change makes it simpler to add new sources in the future, they are
already grouped by functionality, so the prefix does nothing.
2026-04-03 23:02:43 -07:00
nirenjan 9c1eaaa4b2 build: Migrate includes to use libx52/ prefix
As part of the refactor, have the sources include the public headers via
the libx52 prefix, instead of relying on the include directories
approach in meson.build
2026-04-03 22:54:48 -07:00
nirenjan 9c47e78fc5 fix: Make pc files not include -Ilibx52/ in CFLAGS
Modern libraries do not handhold the user into just calling the header,
instead they namespace it, e.g. libevdev/libevdev.h. Because we already
install our headers under the libx52/ directory, having the pkg-config
files specify `-Ilibx52/` is a legacy from the old days, and should no
longer be used.

This commit also adds pkg-config files for x52dcomm and vkm, installing
them in their proper locations.
2026-04-03 22:42:37 -07:00
nirenjan 7bdaea442e cleanup: Remove autotools build infrastructure
The autotools build infrastructure was deprecated back in 0.3.3, and any
bugs in the Meson build infrastructure have been fixed now. This
eliminates the legacy build scripts which were hard to maintain.
2026-04-03 22:36:10 -07:00
nirenjan 8b139a05c4 fix: Add missing test_io_strings.c 2026-04-03 18:09:57 -07:00
nirenjan c49689c1ee feat(libx52io): Add _from_str and from_str_nocase APIs
This change add APIs to convert the string forms of the axis and button
names back to their corresponding enum identifiers. This is effectively
built such that a roundtrip of _to_str and _from_str will return the
same input. The _nocase variants handle case insensitive matching of the
names by folding of the ASCII alphabets A-Z and a-z only, so it doesn't
depend on localization.
2026-04-03 18:06:30 -07:00
nirenjan 0fdcb725af feat: Reorder VKM key identifiers to sync with HID usage tables
This change allows us to create manual key maps in the future, allowing
users with different keyboard layoutss to share a profile without having
to rewrite it for their keyboard layout.
2026-04-02 22:11:05 -07:00
nirenjan 03d58c62e8 feat: Improve virtual mouse speed calculations
Prior to this change, the mouse speed was controlled by an opaque
numeric value, that controlled both the speed and the delay between
updates. This caused a lot of choppy behavior with lower speeds, and the
really low speeds had as little as 1% speed difference between them in
the practical pixels/second speed, while there was effectively a 50%
jump in the speed between speed settings 11 and 12, due to the
hyperbolic relationship between steps. Post that, it was an even 25%
increase in sensitivity for every step.

This change modifies it so that the old Speed option is deprecated, it
is now replaced by the Sensitivity option, which is a direct percentage
scale from 10% to 500%. In addition, there is a CurveFactor option to
let it have fine control when there is little deflection, and move
faster when further away from the center. This also adds an
IsometricMode option which computes the speed as a function of the
cartesian distance from the center (`sqrt(dx^2 + dy^2)`). The default
behavior uses the existing linear speed which controls the speed of the
X and Y axes independently, but now uses the sensitivity and curve
factors to get better behavior. Also, the mouse events are consistently
reported every 10ms. This should make it a lot smoother.

Finally, this change also adds a Deadzone factor, which allows the user
to ignore small changes near the center of the joystick that can cause
mouse drift. This deadzone uses the total distance, so if just the X or
Y axis has moved, it will still allow suppressing any play in the thumb
stick.

Issue: #44
2026-04-01 22:49:04 -07:00
nirenjan 991a307191 fix: Handle shortcut execution in x52d_mouse_report_event
Prior to this change, there is a potential bug where if the left and
right mouse buttons and/or wheel are reported in the same HID report,
the first one to have a change would block any subsequent items. This is
due to the shortcut execution of the `||` operator. By switching the
order in which we evaluate the operands, we can ensure that the report_*
functions are always called.
2026-04-01 20:41:38 -07:00
nirenjan 6ec133488b doc: Add new headers 2026-03-31 21:53:57 -07:00
nirenjan 667e8e2a7b feat: Update symbol visibility 2026-03-30 22:35:52 -07:00
nirenjan 357ea96676 fix: Fix syntax issue with logical operators 2026-03-30 11:52:41 -07:00
nirenjan 6a36fc7764 fix: Use logical operators with boolean arguments
SonarQube flagged the use of bitwise operators with boolean variables.
This is changed to use the correct logical operators instead of their
bitwise equivalents.
2026-03-30 11:47:12 -07:00
nirenjan 283b476c5e fix: Handle issues found by SonarQube 2026-03-30 10:52:47 -07:00
nirenjan 75e6f253c9 feat: Add virtual keyboard/mouse library support
Previously, x52d had conditional compilation flags linking to libevdev
on Linux. However, I wanted to change this so that we use an abstraction
layer that will link to the appropriate backend (evdev on Linux only for
now). The idea is that we get rid of all conditional compilation blocks
and deal with the backend through the vkm library.

This new library handles the mouse scrolling, clicking, as well as
adding the ability to include keyboard events so that we can support the
profiles feature in a future commit.
2026-03-30 10:01:50 -07:00
nirenjan 230951a232
Merge pull request #65 from nirenjan/dependabot/github_actions/codecov/codecov-action-6
build(deps): bump codecov/codecov-action from 5 to 6
2026-03-27 01:59:47 -07:00
dependabot[bot] c99f775b70
build(deps): bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 08:17:04 +00:00
nirenjan 8914184613 build: Migrate CI to use distro containers
Previously, the build workflow was restricted to running only on Ubuntu
22.04, Ubuntu 24.04 and macOS, which are the only available native
runners on Github Actions. However, the Ubuntu runner does allow us to
run the build inside a container. Therefore, this commit adds the
ability to pull a prebuilt container with all the necessary dependencies
and build libx52 inside of that container. This commit also adds support
scripts to build the containers and run the CI build against those
prebuilt containers locally for testing, without having to rely
exclusively on Github Actions.

This change also adds support for testing libx52 against Alpine Linux,
in order to verify the portability, given that Alpine uses musl instead
of glibc. The limitation is that we need to mount the `/dev/bus/usb`
device tree inside the container, otherwise libusb inside the Alpine
image fails with LIBUSB_ERROR_OTHER. This is not a concern on the other
distributions, but due to limitations in the Github actions environment,
there is no `/dev/bus/usb` tree to export. For this reason, Alpine is
not a part of the CI build, but is available for testing locally.

Also, because a default bare container would need several minutes of
package installation just to get to a point where we could run
build-and-test.sh, this includes a prebuild workflow which generates the
container images and pushes them to ghcr.io, and the build workflow
pulls from there. There is also logic to ensure that we only keep the
latest image, since there is no value in retaining older images.
2026-03-26 12:23:38 -07:00
nirenjan fdafda1d34
Merge pull request #64 from nirenjan/dependabot/github_actions/actions/deploy-pages-5
build(deps): bump actions/deploy-pages from 4 to 5
2026-03-26 05:56:34 -07:00
dependabot[bot] 9501813c36
build(deps): bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-26 08:18:04 +00:00
nirenjan aa555f5e66 doc: Update INSTALL.md with Meson instructions
This change removes the old autotools based documentation and switches
to using Meson exclusively. In addition, the build action will build
using meson compile and meson test instead of ninja/ninja test.
2026-03-22 19:13:00 -07:00
nirenjan 842e7e53ed fix: Scope the list of man pages installed
The previous commit to address #63 added a number of extra unwanted man
pages. This commit addresses that and scopes down the actual installed
man pages to only x52cli and x52bugreport.

GH-Issue: #63
GH-Issue-URL: https://github.com/nirenjan/libx52/issues/63
2026-03-22 15:59:35 -07:00
nirenjan dfbe3e6d21 fix: Enable install for libx52dcomm
The change to build using Meson broke the install target, causing x52ctl
and x52d to fail with a missing libx52dcomm library. This was fixed by
setting `install: true` in the library call.

In addition, several features were used that were leftover from my
earlier attempts to migrate to Meson, but targeted older Meson versions.
Some of these features were deprecated in newer Meson versions, and
therefore, cause warnings to show up during meson setup.

GH-Issue: #63
GH-Issue-URL: https://github.com/nirenjan/libx52/issues/63
2026-03-22 15:47:13 -07:00
nirenjan 732bc21b65 fix: Address compiler warnings
When enabling --warnlevel=3 during Meson setup, the build threw up
several warnings, mostly related to either unused parameters, or
sometimes an integer type mismatch. This commit addresses all of those
changes and ensures that the build does not contain any unnecessary
warnings.
2026-03-19 00:10:18 -07:00
nirenjan 3c1abd57d5 fix(pinelog): Cleanup unused parameter warnings
With a recent enough version, the compiler reports a number of unused
parameter warnings when Meson is configured with `--warnlevel=3`. This
commit addresses those warnings.
2026-03-18 22:25:26 -07:00
nirenjan b0b457d14e doc: Update Doxyfile.in to 1.9.8 syntax
Doxygen in Ubuntu 24.04 has removed several features, and introduced a
few new ones. This commit updates the Doxyfile.in template to use the
current syntax.

CI: [skip ci]
2026-03-18 00:54:55 -07:00
nirenjan ae077dbed8 fix: Add library version for libx52dcomm
This change adds the library version for libx52dcomm, so that it can be
versioned if necessary.
2026-03-17 16:43:54 -07:00
nirenjan d29be6213f doc: Add badges for SonarQube and CodeCov
CI: [skip ci] [skip doxy]
2026-03-16 23:22:23 -07:00
nirenjan 273ed22f8e refactor(x52ctl): Break out interactive mode
SonarQube keeps complaining about issues with cognitive complexity and
bad practices, this commit addresses those commits.
2026-03-16 23:07:39 -07:00
nirenjan cdb00739ca fix(x52ctl): Remove unnecessary goto statements
The `goto cleanup` statements are not really needed, and can be safely
replaced with `break` statements in x52ctl.
2026-03-16 22:50:11 -07:00
nirenjan 08a6b0a736 build: Add coverage support with codecov.io
This change adds code coverage to the Meson builds, and reports them to
codecov.io, to help keep the code coverage high.
2026-03-16 22:39:12 -07:00
nirenjan 45d561e0d8 test: Fix SMP test in libx52util
The SMP test was incorrectly calculating the codepoints, resulting in a
test of the codepoints inside the BMP, instead of the individual SMPs.
2026-03-16 22:23:00 -07:00
nirenjan b626a9367f fix(pinelog): Mark benchmark tests as such in Meson
Prior to this change, the pinelog benchmark suite was running as a
regular test, however, this is not ideal since it can result in timing
issues and giving false data to the runners.

This change explicitly marks them as benchmarks, so they can run using
`meson test --benchmark`
2026-03-16 15:56:38 -07:00
nirenjan a1098bc134 feat: Add benchmarking for libx52util-bmp-test 2026-03-16 11:16:41 -07:00
nirenjan 569902be76 fix: Add support for Automake tests
In order to continue to support Automake builds, we need to update the
source to comply with C90 standards, as well as ignoring the warnings in
the test binary.
2026-03-16 10:45:02 -07:00
nirenjan 0cb137bbe0 feat: Handle the entire BMP in libx52util
Prior to this change, the libx52util_convert_utf8_string function had a
limited set of characters that it would convert to the MFD character
map, these characters were derived from the x52_char_map.cfg file.
However, this is a tiny subset of the actual supported characters in the
Basic Multilingual Plane (BMP), since many characters in the BMP can be
normalized to a different character (or character sequence) that has a
corresponding glyph on the X52 MFD.

One example of this is the half-width Katakana characters which are
mapped in the display, however the corresponding full-width characters
were not explicitly mapped. With this commit, the generator script now
automatically detects that the half-width characters can be normalized
to the corresponding full width forms, and maps the full width forms
back to the correct characters on the MFD.

A second benefit of this change is that the MFD can now show characters
that would otherwise never be seen, for example, the 3/4 symbol or 5/8
symbol have no corresponding glyph in the MFD, but they can be
translated to the sequence `3` `/` `4`, giving us much more flexibility
on the characters that can actually be displayed.

Finally, with this change, the function also maps missing or unsupported
characters to the box character (0xDB in the display), making it clearer
that there was something there that could not be displayed. Earlier, it
would have simply skipped that character.
2026-03-16 10:18:16 -07:00
nirenjan 899ea57bf7 doc: Update ChangeLog.md to list Meson bugs
Because several bugs were found in the Meson build infrastructure since
the release of v0.3.3, I've had to keep the existing Autotools
infrastructure running for a bit longer while I address the bugs. As of
this commit, Autotools is still the preferred way to build libx52,
though it is technically deprecated and will be replaced with Meson as
the source of truth.

CI: [skip ci] [skip doxy]
2026-03-13 09:54:23 -07:00
nirenjan e1e020a4f5 fix: Handle test dependency on x52ctl
Prior to this change, running meson test without running meson compile
first would cause the daemon communication tests to fail since it
wouldn't find the x52ctl binary. While I could rewrite the test runner
to directly talk to the daemon, it's faster to just ensure the
dependencies are setup correctly.
2026-03-13 09:27:51 -07:00
nirenjan 7cbf091dc7 fix: Ensure man pages and docs are installed via Meson 2026-03-12 16:19:49 -07:00
nirenjan 9d180531b9 fix: Treat NLS=auto as enabled
Prior to this change, the check was for an explicit -Dnls=enabled,
however, if the option was never set, it defaulted to disabled. With
this change, unless explicitly disabled, the Meson build system will
automatically build with NLS support.
2026-03-12 15:53:30 -07:00
nirenjan 74229b391d fix: Update pkgconfig to use correct details 2026-03-12 15:35:11 -07:00
nirenjan 5f8177f16b fix: Use correct paths for local,sysconf,localstate dirs 2026-03-12 15:17:00 -07:00
nirenjan c5ec15231f feat: Add meson support for configuring udev rules dir 2026-03-12 14:45:08 -07:00
nirenjan 33bbafe970 fix: Fix systemd service installation in Meson
The Meson build change broke the systemd service file installation. This
commit fixes that, while retaining Autotools support.
2026-03-12 13:21:59 -07:00
nirenjan e9a806a6a2 fix: Update meson.build to handle localization
The previous version of the Meson build files did not handle the po
directory correctly, and lost a lot of information. As part of the
migration away from Autotools, this is one more item that needs to be
checked off.

CI: [ci skip] [doxy skip]
2026-03-12 12:48:23 -07:00
188 changed files with 9888 additions and 4283 deletions

View File

@ -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

View File

@ -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])

View File

@ -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'

View File

@ -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 \

View File

@ -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

View File

@ -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

44
.github/workflows/coverage.yml vendored 100644
View File

@ -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

66
.github/workflows/docker.yml vendored 100644
View File

@ -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

View File

@ -38,4 +38,4 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5

9
.gitignore vendored
View File

@ -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

View File

@ -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.

File diff suppressed because it is too large Load Diff

View File

@ -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
```

View File

@ -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

View File

@ -4,6 +4,8 @@ Saitek X52Pro joystick driver for Linux
![Build/Test](https://github.com/nirenjan/libx52/workflows/Build/Test/badge.svg)
![Kernel Module](https://github.com/nirenjan/libx52/workflows/Kernel%20Module/badge.svg)
![CodeQL](https://github.com/nirenjan/libx52/workflows/CodeQL/badge.svg)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nirenjan_libx52&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=nirenjan_libx52)
[![codecov](https://codecov.io/gh/nirenjan/libx52/graph/badge.svg?token=BH1DQQ5N3Y)](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)

View File

@ -1,3 +0,0 @@
#!/bin/sh -x
autoreconf --install

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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])
{

View File

@ -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)

View File

@ -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 (;;) {

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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"),

View File

@ -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>
}

View File

@ -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

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -36,6 +36,7 @@ enum {
X52D_MOD_COMMAND,
X52D_MOD_CLIENT,
X52D_MOD_NOTIFY,
X52D_MOD_KEYBOARD_LAYOUT,
X52D_MOD_MAX
};

75
daemon/crc32.c 100644
View File

@ -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;
}

57
daemon/crc32.h 100644
View File

@ -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 */

131
daemon/crc32_test.c 100644
View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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 */

View File

@ -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);
}

View File

@ -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 */

View File

@ -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 */

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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 */

View File

@ -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);
}

View File

@ -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)", \

View File

@ -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);

View 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

132
daemon/mouse.c 100644
View File

@ -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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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)

View File

@ -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 #
##################

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

37
docker/README.md 100644
View File

@ -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.

View File

@ -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

View File

@ -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

10
docker/ci-setup.sh 100755
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
{

View File

@ -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
*/

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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 { \

View File

@ -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)

View File

@ -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)`.

View File

@ -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

View File

@ -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