Compare commits

...

124 Commits

Author SHA1 Message Date
nirenjan 57676cf540 Revert "feat: Add localipc library for IPC over AF_UNIX"
This reverts commit b8f059a881.

The localipc library was accidentally merged on the master branch,
whereas it should have been only on a side branch. This was generated
with the help of Cursor, and was in the process of being tested, but I
don't think it follows the style of the rest of the project. In
addition, this seems to be needlessly complex, and needs to be greatly
simplified.
2026-04-27 18:29:12 -07:00
nirenjan b8f059a881 feat: Add localipc library for IPC over AF_UNIX
In order to make daemon communication more secure, we are introducing a
new library to handle the interprocess communication over the Unix
socket. The localipc library abstracts away a lot of the boilerplate
around the socket API and ensures that the packets are cleanly
transferred between the clients and server.

A future change will modify the daemon logic to leverage localipc
instead of x52dcomm, and eliminate the manual poll loop. The daemon can
then handle the business logic of commands/notifications.
2026-04-26 00:04:12 -07:00
nirenjan 03d7336234 doc: Fix exclude patterns after restructuring 2026-04-25 15:08:58 -07:00
nirenjan a710ab5591 fix: Remove redundant condition in scroll.c 2026-04-25 13:04:45 -07:00
nirenjan 130a1f67de feat: Add scroll functionality to libx52util
This change adds the ability to scroll a text blob to fit within the 16
character MFD limit. The daemon can use this in the future to implement
scrolling similar to how the Windows SST software did it.
2026-04-25 12:54:41 -07:00
nirenjan 4422ee89c0 feat: Add script to validate layout file
This change adds a Python script to validate the compiled layout file
and optionally dump the contents.
2026-04-22 15:18:54 -07:00
nirenjan 9aaec8b2f0 feat: Add Python paths module
This will be used in future commits to enable Python support
2026-04-22 12:12:21 -07:00
nirenjan 3f4990de4d ci: Handle distro specific meson dist 2026-04-22 09:45:22 -07:00
nirenjan fb913a06a2 ci: Pass allow-dirty to meson dist 2026-04-22 09:37:00 -07:00
nirenjan c373ca9647 build: Fix meson dist issue inside containers 2026-04-22 09:34:12 -07:00
nirenjan 9cddfe9cef fix: Address dependency issue with generated module-map.h 2026-04-22 09:16:14 -07:00
nirenjan 5fecdd3929 feat: Generate identifiers for communication
In order to improve security around the command processing, I plan to
upgrade the communication protocol to use a fixed structure with an
optional payload. As a first pass on this, I added a script to generate
the IDs automatically from the configuration, as well as a script to map
the module names and log levels to integer IDs.

This also replaces the manual lmap_* and array_get_index lookup tables,
so I don't have to always deal with creating an entry in constants.h and
command.c and making sure they are in sync.
2026-04-22 09:02:17 -07:00
nirenjan 772017661d test: Add tests for x52evtest and x52joytest
This change adds additional code coverage test cases for the evtest and
joytest modules.
2026-04-20 22:32:31 -07:00
nirenjan fdf884cb1c build: Fix install of vkm.h 2026-04-20 10:38:47 -07:00
nirenjan e0ab5bbab7
Merge pull request #69 from nirenjan/rework-to-address-sonarqube-duplicate-code-issue
refactor: Rearrange files to avoid duplicate code issue
2026-04-20 10:34:43 -07:00
nirenjan 3c02fe5ec2 refactor: Rearrange files to avoid duplicate code issue
Prior to this change, SonarQube complains about duplicated code in the
symlinked header files. This change reworks the layout so that the
installed headers all reside under the 'includes' directory, and the
meson.build files are updated to reflect the new paths.
2026-04-20 10:28:24 -07:00
nirenjan dbf891f951
Merge pull request #68 from nirenjan/dependabot/github_actions/actions/upload-pages-artifact-5
build(deps): bump actions/upload-pages-artifact from 4 to 5
2026-04-16 18:06:13 -07:00
nirenjan 79676250fc
Merge pull request #67 from nirenjan/dependabot/github_actions/softprops/action-gh-release-3
build(deps): bump softprops/action-gh-release from 2 to 3
2026-04-16 18:05:56 -07:00
dependabot[bot] a2ce2e2218
build(deps): bump actions/upload-pages-artifact from 4 to 5
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 08:21:56 +00:00
dependabot[bot] c487508b7b
build(deps): bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 08:38:12 +00:00
nirenjan 84e151389a feat: Enhance x52 character map bin file
Prior to this change, the char_map.bin file was only used for testing.
However, with the upcoming support for profiles, we need to enhance it
and have it installed in the data directory for use by the (future)
compiler.
2026-04-07 23:50:28 -07:00
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
nirenjan ad30bfff7b fix: Typo in release changelog generation 2026-03-12 12:10:26 -07:00
nirenjan cccb561020 feat: Add Changelog generation script for releases
This change automates the release workflow and reduces manual touch.
2026-03-12 12:06:24 -07:00
nirenjan 6743c60dfd doc: Update for version 0.3.3 2026-03-12 11:34:54 -07:00
nirenjan b4ec8d4629 build: Migrate to meson build
Meson is a far more robust build framework, compared to autotools. This
greatly simplifies adding new features, since it's far easier to
maintain a set of meson.build files vs the autotools mishmash.

DEPRECATION NOTICE: Autotools based build is deprecated and will be
removed in the future.
2026-03-12 10:20:01 -07:00
nirenjan 3fb0d72124
Merge pull request #62 from nirenjan/remove-inih-dependency
build!: Update build to use system inih
2026-03-12 09:57:44 -07:00
nirenjan 74fe559f4a build!: Update build to use system inih
When the x52d daemon was originally implemented, the inih library was
not bundled with any major distribution, and had to be compiled from
source everytime. However, with recent distributions (starting with
Ubuntu 22.04 LTS), this is no longer an issue, and inih is available in
the distro package manager. As a result, there is no longer a need to
vendor the inih sources with thiis repository.

However, as a result of this change, third party packaging scripts such
as those on the AUR or other similar registries that directly query the
git repository will fail unless they update the dependencies.

BREAKING CHANGE: Packaging scripts (AUR, etc.) need dependency update
2026-03-12 09:50:58 -07:00
nirenjan b6e61fc54e fix: Avoid backward jumps using goto
SonarQube cloud identified a maintainability issue based on MISRA C
guidelines that prohibit backward jumps. While not a mandatory fix, it
helps to clean up the codebase and improves readability.

Ref. MISRA C:2012, 15.2 - The goto statement shall jump to a label
declared later in the same function.
2026-03-12 08:34:58 -07:00
nirenjan e479e338a2
Merge pull request #61 from nirenjan/dependabot/github_actions/actions/upload-pages-artifact-4
build(deps): bump actions/upload-pages-artifact from 3 to 4
2026-03-10 08:47:54 -07:00
dependabot[bot] 69ae9626c7
build(deps): bump actions/upload-pages-artifact from 3 to 4
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 08:18:25 +00:00
nirenjan e9f4e1b3a8 test: Add test cases for libx52util
This change adds a test suite for libx52util, testing all the positive
cases where a character is added to the lookup table. For now, this test
suite only verifies single character mappings, not double character
mappings. A future commit will add test cases for characters not in the
map.
2026-03-09 14:43:33 -07:00
nirenjan f43ba6b902
Merge pull request #60 from nirenjan/add-action-permissions
ci: Add permissions blocks to action definitions
2026-03-09 10:39:41 -07:00
nirenjan 378cbbd931 ci: Add permissions blocks to action definitions
CodeQL identified a medium severity security issue with the action
definitions not including a permissions block as required by modern
security practices. This change ensures that the majority of the actions
force the token to be read-only and not accidentally write content back
into the repository.
2026-03-09 10:34:40 -07:00
nirenjan 47da6e22d1 ci: Fix doxygen.yml to include enviroment 2026-03-09 10:30:00 -07:00
nirenjan e98b8b4bc3 ci: Update doxygen build flow to use modern pages deployment
This change adds the new permissions structure to the action definition,
and migrates away from the 3rd party action to an official action. This
was identified as a possible security vulnerability by CodeQL
2026-03-09 10:24:25 -07:00
nirenjan 7b7065f8f0 po: Update translation files to fix build 2026-03-09 00:47:29 -07:00
nirenjan 2fa9f52ddb build: Fix the GCC pragma to only apply for GCC > 13
The -Wanalyzer-fd-leak flag was introduced in GCC 13, so this pragma
fails to build on older GCC versions.
2026-03-09 00:26:06 -07:00
nirenjan a17312dcbc fix: Fix potential error scenarios
Configuring the build with CFLAGS="-O2 -g -fanalyzer", we ran into some
build errors, which we address in this commit.

First off, GCC identified a false positive file descriptor leak in
x52d_client.c, this instance is suppressed to avoid breaking the build.

There was a bug in x52d_clock.c, where if the original timezone could
not have a copy due to malloc failure, it would fail when resetting the
TZ environment. This is fixed by ensuring the copy is valid.

Finally, there was a potential NULL pointer dereference if the pinelog
module messes up the log levels, and the lmap_get_string method ends up
returning a NULL which was passed to the DATA macro. This is fixed by
checking for NULL and handling it in case of failure.
2026-03-09 00:13:19 -07:00
nirenjan 2be7792024 build: Fix libx52util SOLIB version
Because of the fix in 2378ba7dc4, the
library revision should have been updated.
2026-03-08 23:16:40 -07:00
nirenjan f51b777ca0 fix: Handle NULL pointer dereferencing in libx52
libx52_exit dereferences the device pointer to deinitialize libusb.
However, a user could pass NULL to this function, resulting in a null
pointer dereference.
2026-03-08 23:13:16 -07:00
nirenjan b3dff7182b fix: Handle possible double-free in pinelog
The pinelog_init function frees the module_level and module_name
pointers at the start of the function, but doesn't reset them back to
NULL. If a subsequent malloc fails, then it would attempt to free the
pointer again, resulting in a double-free situation.

However, this is only hit if the pinelog_init function is called more
than once. While this is not likely (given that I'm the only known user
of pinelog at this time), it's still good coding practice.
2026-03-08 23:08:05 -07:00
nirenjan 0356a2d610
Merge pull request #59 from nirenjan/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-03-08 20:57:04 -07:00
dependabot[bot] c1e3c85738
Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:53:36 +00:00
nirenjan accd2a1f4e ci: Add dependabot to keep Github Actions up to date 2026-03-08 20:52:55 -07:00
nirenjan e8208e97cb
Merge pull request #58 from nirenjan/fix/update-codeql-action-version
build: Update CodeQL action to v4
2026-03-08 20:49:41 -07:00
nirenjan 421e2964b3 build: Update CodeQL action to v4
Signed-off-by: nirenjan <nirenjan@gmail.com>
2026-03-08 20:45:11 -07:00
nirenjan 2378ba7dc4 fix: Fix boundary check error in libx52util
Prior to this change, if the input string terminates exactly when `len`
characters have been output, the libx52util_convert_utf8_string function
returns an error of `-E2BIG`, even though the buffer is sufficiently
large. Because the output buffer is not expected to be NUL terminated,
this additional character can be safely placed in the output buffer
without overrun.

This change checks for a non-NUL character when the index matches or
exceeds the output buffer length, and only then will it return -E2BIG.
2026-03-08 20:30:03 -07:00
nirenjan 762a3468b2 fix: Handle negative index in libx52-string-test
The test case uses a negative value to force a test of the ID to string
functions in the abnormal case. However, this ends up with accessing the
expected array with a negative index. This is technically undefined
behavior, and may cause failures in some systems. This change ensures
that the negative values will be mapped directly to the unknown
string, without having to perform a negative index.
2026-03-08 20:25:29 -07:00
nirenjan ef4cbee127 fix: Handle malformed UTF-8 input in libx52util
The libx52util_convert_utf8_string function manually converts the UTF-8
string into the character map supported by the X52/X52Pro MFD. However,
there was a bug when handling malformed UTF-8 input. If the state
machine thinks it is at the start of a word and receives malformed UTF-8
input (between 0x80 and 0xC0), it will ignore the characters, but it
will not reset the entry to the map_root location, thereby causing
subsequent characters to be dropped.

This change ensures that the entry is reset to map_root[*input] after
skipping over an invalid UTF-8 sequence.
2026-03-08 20:25:29 -07:00
nirenjan c63b924705
doc: Add security policy and reporting guidelines 2026-03-06 15:29:31 -08:00
nirenjan 1b00bf4a69 build: Force build timestamp to be in UTC time
As part of improving the privacy around the software, even though time
zone is not considered PII, it's still coarse location data that we
don't need at all. By using UTC, we can eliminate even this last bit of
identifying information from the bug reports.
2026-03-06 13:56:08 -08:00
nirenjan a40546bda3 doc: Fix bullets in PRIVACY.md 2026-03-03 18:00:03 -08:00
nirenjan 108293abdf feat: Minimize identifying information in bugreport
In order to comply with recent privacy laws such as GDPR and CCPA, the
bug report utility has been updated to remove personally identifiable
information such as device serial number and system hostname from the
output.

In addition, this change also adds a PRIVACY.md file which describes how
this project handles data, in compliance with GDPR/CCPA. Documentation
is updated to link to the privacy document as well.
2026-03-03 17:55:26 -08:00
nirenjan 9361c7af5c build: Update Github Actions to use only supported distros 2026-03-03 16:40:45 -08:00
nirenjan 004eca2418 build: Update build workflow to use current runners 2026-02-26 22:40:45 -08:00
nirenjan 1902ca0d27 build: update PO files to reflect new version 2024-06-09 20:24:45 -07:00
nirenjan 6330d28c4d fix: Update Version metadata
Version metadata was not updated to reflect the new version. This fixes
the version metadata and updates the changelog file to reflect the
reason why the older version was deprecated.
2024-06-09 20:21:54 -07:00
nirenjan 5c37c4a9db doc: Update changelog for v0.3.1 2024-06-08 22:24:46 -07:00
nirenjan 863e43e4ad ci: Update stable OS versions for LKM build 2024-06-08 21:51:48 -07:00
nirenjan 49c57f4a6a Update workflows to use actions/checkout@v4
Since Node 16 has been deprecated, Github is requiring all Actions users
to migrate to Node 20, and therefore use actions/checkout@v4. This also
applies to other jobs that use Node 16 as their runtime.

See: https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/
2024-06-08 21:38:34 -07:00
nirenjan b0b9123a2e fix: Add CMOCKA_CFLAGS to test program CFLAGS
In macos-14, programs that rely on cmocka need to specify CMOCKA_CFLAGS
as part of their CFLAGS. This has not been an issue so far on older
versions of macOS, or on any release of Ubuntu, but it should be done to
ensure that the library headers can be found.
2024-06-08 21:25:42 -07:00
nirenjan 50a911160f fix: Disable macos-12 builds
macos-12 builds fail with the following error message:

    ld: warning: -undefined dynamic_lookup may not work with chained fixups

This causes the overall build status to be marked as fail, even though
macOS builds are not treated as failing the build. Also, macOS 12 is
going to effectively be end-of-lifed in November 2024, so it's not worth
spending the time to look into this.
2024-06-08 20:57:53 -07:00
nirenjan 7a56af032b fix: Disable mouse tests if cmocka is not present
Cmocka is optional for the builds, but the absence of cmocka causes the
Daemon build to fail when running `make check`. This commit addresses
that issue, while keeping tests that don't need cmocka.
2024-06-05 09:18:01 -07:00
nirenjan c46cec3138 ci: Disable macos-11 and add newer versions
Github has deprecated macos-11 runners and will stop them towards the
end of June 2024.
2024-06-05 09:16:22 -07:00
nirenjan 21050e40a8 Fix syntax of calloc calls in pinelog.c
`calloc` requires the count to be the first argument, and the size
parameter to be the second argument. However, this has not really caused
issues in the past, and older compilers were not so strict about it.

However, newer compilers (at least GCC 14) triggers a warning on this
and causes the build to fail.

Fixes #52
2024-06-04 15:02:27 -07:00
nirenjan 9e2e8cb8ff Add compiler details to bugreport 2023-06-02 00:02:05 -07:00
nirenjan 5f4dfe4c01 Add host details to version-info 2023-06-01 23:33:54 -07:00
nirenjan 0870518598 Disable builds on macOS 12
macOS 12 builds are currently failing with the following error:

    ld: warning: -undefined dynamic_lookup may not work with chained fixups

This is causing the overall CI to fail, therefore, I am disabling it
until such time that this can be fixed.
2023-01-21 02:49:45 -08:00
nirenjan d7b4a694fa Update Github action workflows to use actions/checkout@v3
Due to the Node 12 runtime being deprecated, jobs are required to move
to actions/checkout@v3 which uses Node 16 runtime.

See: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2023-01-21 02:39:59 -08:00
nirenjan 326ac992ac Fix hyperlink for ChangeLog.md
[skip ci] [skip doxy]
2023-01-21 02:34:17 -08:00
nirenjan d3973a0abf Update daemon protocol documentation
This change ensures that the documentation is in sync with the code.

[skip ci]
2023-01-04 10:07:20 -08:00
nirenjan ebca9566d7 Move common socket code into x52d_comm_internal.c 2023-01-04 08:42:47 -08:00
nirenjan 03c0376e7c Initialize variables to avoid maybe-uninitialized warnings
When building the package for PPA, gcc throws errors indicating that
some variables may be used uninitialized. This is not a real problem
that shows up during the CI build, but only when building using
dpkg-buildpackage.

This change adds some dummy initialization so that it avoids triggering
those warnings during debuild/dpkg-buildpackage.
2023-01-03 12:26:51 -08:00
232 changed files with 12846 additions and 4978 deletions

4
.gitattributes vendored
View File

@ -1 +1,5 @@
/version-info ident
*/meson.build ident
/.github/ export-ignore
.gitignore export-ignore
.gitattributes export-ignore

8
.github/dependabot.yml vendored 100644
View File

@ -0,0 +1,8 @@
version: 2
updates:
# Maintain dependencies for Github Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 5

View File

@ -2,16 +2,32 @@
# Run the build and tests
set -e
./autogen.sh
mkdir build
cd build
../configure
make -j V=0
make -j check V=0
make -j distcheck
BUILDDIR="${1:-build}"
rm -rf "$BUILDDIR"
# Handle the meson dist failure in CI
if [[ "$GITHUB_ACTIONS" == "true" ]]
then
# If in a container, then use the system directory
git config --system --add safe.directory '*' || \
git config --global --add safe.directory '*'
fi
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled "$BUILDDIR"
cd "$BUILDDIR"
meson compile
meson test
if [[ $(printf "%s\n" "0.62.0" "$(meson --version)" | sort -V | head -1) == "0.62.0" ]]
then
meson dist --allow-dirty # Required to fix CI build
else
meson dist
fi
# Print bugreport output
./x52bugreport
./bugreport/x52bugreport
# Make sure that there are no changes to the source code
# This may happen if the source have changed with differences to the

View File

@ -2,8 +2,6 @@
# Generate Doxygen documentation
set -e
./autogen.sh
mkdir build
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled build
cd build
../configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
make docs/.stamp
ninja docs

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,38 @@
#!/usr/bin/env python3
"""Generate a changelog for the latest release and dump it to stdout"""
from pathlib import Path
def get_git_root():
script_path = Path(__file__).resolve()
# This is always going to reside at <git-root>/.github/scripts/*.py
scripts_dir = Path(script_path.parent)
gh_dir = Path(scripts_dir.parent)
return Path(gh_dir.parent)
def main():
git_root = get_git_root()
changelog_file = git_root / 'ChangeLog.md'
latest = False
with open(changelog_file) as cfd:
for line in cfd:
if line.startswith('## '):
if 'Unreleased' in line:
continue
if latest:
break
latest = True
continue
if latest:
print(line, end='')
if __name__ == '__main__':
main()

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,15 +1,16 @@
#!/bin/bash -x
# Install dependencies to build and test on Ubuntu runners
brew install \
autoconf \
automake \
libtool \
pkg-config \
python3 \
gettext \
libusb \
hidapi \
inih \
doxygen \
cmocka
cmocka \
meson \
ninja \
inih
exit 0

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 \
@ -12,8 +9,11 @@ sudo apt-get install -y \
libusb-1.0-0-dev \
libhidapi-dev \
libevdev-dev \
libinih-dev \
doxygen \
libcmocka-dev \
faketime
faketime \
meson \
ninja-build
exit 0

View File

@ -1,5 +1,8 @@
name: Build/Test
permissions:
contents: read
on:
push:
branches:
@ -7,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-') || (matrix.os == 'ubuntu-22.04') }}
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-20.04', 'ubuntu-22.04', 'macos-11', 'macos-12']
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@v2
- name: Install dependencies (Ubuntu)
run: ./.github/scripts/install-dependencies-ubuntu.sh
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
uses: actions/checkout@v6
- 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

View File

@ -1,5 +1,9 @@
name: "CodeQL"
permissions:
contents: read
security-events: write
on:
pull_request:
# The branches below must be a subset of the branches above
@ -14,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@ -30,26 +34,13 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v4
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Build
run: ./.github/scripts/build-and-test.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v4

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

@ -5,14 +5,22 @@ on:
branches:
- 'master'
permissions:
contents: read
pages: write # Required to push to the Pages server
id-token: write # Required to verify the deployment is legitimate
jobs:
doxygen:
if: "!(contains(github.event.head_commit.message, '[doxy skip]') || contains(github.event.head_commit.message, '[skip doxy]'))"
runs-on: 'ubuntu-latest'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: Install dependencies
run: ./.github/scripts/install-dependencies-ubuntu.sh
@ -23,8 +31,11 @@ jobs:
- name: Dump generated files
run: find ./build -type f -print
- name: Deploy generated documentation to Github pages
uses: peaceiris/actions-gh-pages@v3
- name: Upload built pages
uses: actions/upload-pages-artifact@v5
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/docs/html
path: './build/docs/html'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5

View File

@ -1,5 +1,8 @@
name: Kernel Module
permissions:
contents: read
on:
push:
branches: [ '*' ]
@ -16,11 +19,11 @@ jobs:
strategy:
matrix:
os: ['ubuntu-18.04', 'ubuntu-20.04']
os: ['ubuntu-22.04', 'ubuntu-24.04']
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: Install kernel dependencies
run: ./.github/scripts/install-kernel-dependencies.sh

View File

@ -1,5 +1,8 @@
name: Create Release
permissions:
contents: write
on:
push:
tags:
@ -11,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: Install dependencies
run: ./.github/scripts/install-dependencies-ubuntu.sh
@ -19,30 +22,40 @@ jobs:
- name: Build project
run: ./.github/scripts/build-and-test.sh
- name: Find release tarball
id: find_release
- name: Prepare release assets
run: |
echo "::set-output name=path::$(find $PWD -name 'libx52*.tar*')"
echo "::set-output name=asset::$(find . -name 'libx52*.tar*' -exec basename {} \; | sed 's/libx52-\(.*\)\.tar/libx52_\1.orig.tar/')"
# Find the Meson generated tarball
# meson dist usuall creates a tar.xz, but be prepared to handle
# additional compression formats
DIST_FILE=$(find build/meson-dist -name 'libx52-*.tar.*' -a ! -name '*.tar.*sum')
# Extract the version from the filename
VERSION=$(echo "$DIST_FILE" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
EXTENSION="${DIST_FILE#*${VERSION}}"
ASSET_NAME="libx52_${VERSION}.orig${EXTENSION}"
ASSET_PATH="build/meson-dist/${ASSET_NAME}"
# Rename the file
mv -v "$DIST_FILE" "$ASSET_PATH"
cd build/meson-dist
rm *.sha256sum
sha256sum "$ASSET_NAME" > "${ASSET_NAME}.sha256sum"
cd ../..
- name: Generate changelog
run: ./.github/scripts/generate_changelog.py > ${{ github.workspace }}/CHANGELOG.txt
- name: Create Release
id: create_release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
name: Release ${{ github.ref_name }}
body_path: ${{ github.workspace }}/CHANGELOG.txt
draft: false
prerelease: false
- name: Upload Release Tarball
id: upload-release-tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.find_release.outputs.path }}
asset_name: ${{ steps.find_release.outputs.asset }}
asset_content_type: application/gzip
files: |
build/meson-dist/*

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,6 +6,73 @@ 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.
- Added a [security policy](SECURITY.md) to help securely report vulnerabilities.
- Added Dependabot configuration to keep Action files up to date.
- Added a changelog script to automatically generate the latest release changelog.
### Changed
- **BREAKING**: Removed vendored inih package and switched build framework to use inih from the system package manager.
- `x52bugreport` tool now strips out potentially identifying information.
- Removed the use of a 3rd party action to deploy generated Doxygen pages to the gh-pages branch. This now uses the modern gh-pages deployment action.
- Updated release action to use softprops/action-gh-release@v2, since the original actions are no longer maintained.
### Deprecated
- Autotools build framework is now deprecated, and will be removed in the next release.
### Fixed
- Github Actions updated to use current set of runners
- Fixed handling malformed UTF-8 input in libx52util
- Fixed boundary check issue in libx52util that incorrectly returned `-E2BIG` if the output buffer was the exact size to capture the translated string and the null terminator.
- Fixed potential UB in libx52-string-test
- Fixed NULL pointer dereference in `libx52_exit`
- Fixed errors identified by the GCC `-fanalyzer` flag
### Security
- Updated action files to include permission blocks
## [0.3.2] - 2024-06-09
### Added
- Updated bug report utility to add details about build host details and
compiler information.
### Fixed
- Updated syntax check for calloc calls. See
[#52](https://github.com/nirenjan/libx52/issues/52)
- Fixed a tooling bug where running make check on a system without cmocka
library installed would fail during daemon testing.
- Cleaned up daemon protocol documentation
### Changed
- Moved socket code around to make it easier to reuse the communication logic
out in both client(s) and server.
## 0.3.1 - 2024-06-08
**Important:** Tag 0.3.1 has a bad Version file and should not be used. This has
been superseded by 0.3.2 with corrected metadata. The changes from the previous
release are the same.
## [0.3.0] - 2022-12-25
### Added
- Bug report utility to make it easier to gather system and build information
@ -158,8 +225,10 @@ The format is based upon [Keep a Changelog].
[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/
[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
[TAP]: https://testanything.org
[Unreleased]: https://github.com/nirenjan/libx52/compare/v0.3.0...HEAD
[0.2.3]: https://github.com/nirenjan/libx52/compare/v0.2.3...v0.3.0
[Unreleased]: https://github.com/nirenjan/libx52/compare/v0.3.3...HEAD
[0.3.3]: https://github.com/nirenjan/libx52/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/nirenjan/libx52/compare/v0.3.0...v0.3.2
[0.3.0]: https://github.com/nirenjan/libx52/compare/v0.2.3...v0.3.0
[0.2.3]: https://github.com/nirenjan/libx52/compare/v0.2.2...v0.2.3
[0.2.2]: https://github.com/nirenjan/libx52/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/nirenjan/libx52/compare/v0.2.0...v0.2.1

File diff suppressed because it is too large Load Diff

View File

@ -3,23 +3,24 @@ Installation instructions for libx52
Build has been tested on the following operating systems (x86-64 only):
* Ubuntu 20.04 LTS
* Ubuntu 22.04 LTS
* macOS Big Sur 11
* macOS Monterey 12
* Ubuntu 24.04 LTS
* 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
* libtool
* inih
* 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)
@ -28,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 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 python git` |
| Fedora | `sudo dnf install autoconf automake gettext-devel findutils libtool hidapi-devel libusb-devel libevdev-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
@ -45,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
@ -88,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 = lib
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
${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

58
PRIVACY.md 100644
View File

@ -0,0 +1,58 @@
# Privacy
This document describes how the libx52 project handles data, for compliance with
privacy laws such as the GDPR and CCPA.
## Summary
- **No automatic collection**: The software does not transmit any data to the
project or third parties. No telemetry, analytics, or crash reporting is
implemented.
- **Local operation**: All processing is on your machine. Configuration and logs
stay local unless you choose to share them (e.g. when opening a bug report).
## Data that may be displayed or stored locally
| Data | Where | Purpose |
|------|--------|---------|
| **Device serial number** | `evtest` only | Shown when you run evtest against a connected device (for local identification). **Not** included in **x52bugreport** output. |
| **Device type info** | `x52bugreport`, `evtest` | Vendor ID, product ID, device version, manufacturer and product name (e.g. "Saitek" / "X52 Pro"). No serial number or hostname in bugreport. |
| **System information** | `x52bugreport` only | Kernel name/release, machine architecture, kernel version string. **Hostname is not included.** |
| **Build environment** | `x52bugreport` only | Compiler, build date, kernel/arch/OS version at build time. No hostname or other machine identifier. |
| **Paths** | Daemon logs (if enabled) | Log file path, config path, socket path. Default paths use system directories (e.g. `/var`, `/run`, `/etc`), not your home directory. |
| **Configuration** | Config files (e.g. under `/etc/x52d/`) | MFD/LED and daemon settings. Stored only on your system. |
None of this data is sent anywhere by the software. The only way it leaves your
system is if you voluntarily paste it (e.g. into a GitHub issue).
## Bug reports
**x52bugreport** output is designed to avoid personal and device identifiers:
- **Device serial number** and **system hostname** are **not** included in the output.
- Included: package/build version, compiler, build date, kernel and machine type, library versions, device vendor/product ID and device name (manufacturer/product strings only).
You may still redact any line before posting if you prefer. For most bugs, the information above is sufficient.
## Your rights (GDPR / CCPA style)
- **No account or sign-up** is required to use the software, so we do not hold
an account-based profile on you.
- **No selling of data**: We do not collect or sell personal data.
- **Transparency**: This document describes what the software can display or
store locally.
- **Control**: You decide whether to run `x52bugreport` and what to include when
opening an issue.
## Third-party services
- **Source and issues**: If you clone the repo or open an issue on GitHub,
GitHubs privacy policy applies to that interaction ([GitHub Privacy
Statement](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement)).
- **Packages**: Installation via Ubuntu PPA or Arch AUR is subject to
Canonicals or the AURs respective terms and privacy practices.
## Changes
We may update this document to reflect changes in the software or in legal
requirements. The current version is in the project repository.

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)
@ -18,6 +20,8 @@ capable of reading the joystick, but it cannot control the MFD or LEDs.
Most of the extra functionality can be handled from userspace. See
the individual folders for README information.
For data handling and privacy (e.g. GDPR/CCPA), see [PRIVACY.md](PRIVACY.md).
**Note:** This repository currently only provides commandline interfaces to
control the MFD and LEDs. If you are not comfortable working in the commandline,
then the [gx52](https://gitlab.com/leinardi/gx52) project might be a better fit

9
SECURITY.md 100644
View File

@ -0,0 +1,9 @@
# Security Policy
## Reporting Security Vulnerabiltiies
You may report a security vulnerability by [creating a new security advisory](https://github.com/nirenjan/libx52/security/advisories/new).
## Supported Versions
All security fixes will be made to the `master` branch. Older versions are not supported.

View File

@ -1 +1 @@
0.3.0
0.3.3

View File

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

View File

@ -1,24 +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
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)
@ -59,15 +59,13 @@ static void print_devinfo(void)
printf("Device name: '%s' '%s'\n",
libx52io_get_manufacturer_string(ctx),
libx52io_get_product_string(ctx));
printf("Serial number: '%s'\n",
libx52io_get_serial_number_string(ctx));
libx52io_close(ctx);
devinfo_cleanup:
libx52io_exit(ctx);
}
int main(int argc, char **argv)
int main(void)
{
const struct libusb_version *libusb;
@ -75,7 +73,12 @@ int main(int argc, char **argv)
puts("================");
printf("Package version: %s\n", VERSION);
printf("Build version: %s\n", BUILD_VERSION);
printf("Built on: %s\n", BUILD_DATE);
printf("Build host kernel: %s\n", BUILD_KERNEL);
printf("Build host architecture: %s\n", BUILD_ARCH);
printf("Build host version: %s\n", BUILD_OS_VERSION);
printf("Build target: %s\n", BUILD_TARGET);
printf("Compiler: %s\n", BUILD_COMPILER);
printf("Build date: %s\n", BUILD_DATE);
printf("version-info %s\n", BUILD_VERSION_INFO_IDENT);
puts("");

View File

@ -15,6 +15,10 @@
current system and build environment. The reported information can be provided
when raising a bug report on https://github.com/nirenjan/libx52/issues.
The output does not include device serial number or system hostname. You may
redact any line before posting if you prefer. See the project PRIVACY.md for
details.
# USAGE
\b x52bugreport

View File

@ -0,0 +1,66 @@
#######################################################################
# Version information
#######################################################################
compiler_version = run_command(compiler.cmd_array(), '--version',
capture: true,
check: true).stdout().split('\n')[0]
build_date = run_command('date', '+%Y-%m-%dT%H:%M:%S%z',
capture: true,
check: true).stdout().strip()
build_kernel = run_command('uname', '-sr',
capture: true,
check: true).stdout().strip()
build_arch = run_command('uname', '-mp',
capture: true,
check: true).stdout().strip()
build_os_version = run_command('uname', '-v',
capture: true,
check: true).stdout().strip()
built_for = '@0@ @1@ @2@-endian'.format(
host_machine.system(),
host_machine.cpu(),
host_machine.endian(),
)
git = find_program('git', required: false)
if git.found()
vcs_describe = run_command(git, 'describe', '--dirty',
capture: true,
check: false).stdout().strip()
if vcs_describe == ''
vcs_describe = meson.project_version()
endif
else
vcs_describe = meson.project_version()
endif
version_data = configuration_data()
version_data.set_quoted('BUILD_VERSION', vcs_describe)
version_data.set_quoted('BUILD_DATE', build_date)
version_data.set_quoted('BUILD_KERNEL', build_kernel)
version_data.set_quoted('BUILD_ARCH', build_arch)
version_data.set_quoted('BUILD_OS_VERSION', build_os_version)
version_data.set_quoted('BUILD_COMPILER', compiler_version)
version_data.set_quoted('BUILD_TARGET', built_for)
version_data.set_quoted('BUILD_VERSION_INFO_IDENT', '$Id$')
version_info_h = configure_file(
input: 'version-info.h.meson',
output: 'version-info.h',
configuration: version_data
)
# x52bugreport
exe_bugreport = executable('x52bugreport', 'bugreport.c',
install: true,
include_directories: [includes],
dependencies: [dep_libusb, dep_hidapi],
link_with: [lib_libx52io])
# Test only to get code coverage
test('x52bugreport', exe_bugreport, protocol:'exitcode')

View File

@ -0,0 +1,8 @@
#mesondefine BUILD_VERSION
#mesondefine BUILD_DATE
#mesondefine BUILD_KERNEL
#mesondefine BUILD_ARCH
#mesondefine BUILD_OS_VERSION
#mesondefine BUILD_COMPILER
#mesondefine BUILD_TARGET
#mesondefine BUILD_VERSION_INFO_IDENT

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

16
cli/meson.build 100644
View File

@ -0,0 +1,16 @@
# x52cli
executable('x52cli', 'x52_cli.c',
install: true,
include_directories: [includes],
link_with: lib_libx52)
test_cli = executable('test-cli', 'x52_cli.c', 'test_x52_cli.c',
build_by_default: false,
c_args: ['-DX52_CLI_TESTING'],
include_directories: [includes],
dependencies: [dep_cmocka],
)
test('test-cli', test_cli, protocol: 'tap')

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;
@ -462,12 +462,12 @@ static void do_help(const struct command_handler *cmd)
if (cmd) {
fprintf(stderr, "Command usage: %s\n", cmd->help);
} else {
printf("\nCommands:\n");
fprintf(stderr, "\nCommands:\n");
for (i = 0; i < X52_CTL_CMD_MAX; i++) {
printf("\t%s\n", handlers[i].help);
fprintf(stderr, "\t%s\n", handlers[i].help);
}
printf("\nWARNING: raw command may damage your device\n\n");
fprintf(stderr, "\nWARNING: raw command may damage your device\n\n");
}
}

View File

@ -1,146 +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 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
lib/Makefile
libx52/libx52.pc
libx52io/libx52io.pc
libx52util/libx52util.pc
lib/pinelog/Makefile
lib/inih/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)/lib/pinelog \
-I $(top_srcdir)/lib/inih \
-DSYSCONFDIR=\"$(sysconfdir)\" \
-DLOCALEDIR=\"$(localedir)\" \
-DLOGDIR=\"$(localstatedir)/log\" \
-DRUNDIR=\"$(localstatedir)/run\" \
@PTHREAD_CFLAGS@ $(WARN_CFLAGS)
x52d_LDFLAGS = @PTHREAD_LIBS@ $(WARN_LDFLAGS)
x52d_LDADD = \
lib/pinelog/libpinelog.la \
lib/inih/libinih.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
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)/lib/pinelog \
$(WARN_CFLAGS)
x52d_mouse_test_LDFLAGS = @CMOCKA_LIBS@ $(WARN_LDFLAGS)
x52d_mouse_test_LDADD = \
lib/pinelog/libpinelog.la \
@LTLIBINTL@
TESTS += \
daemon/test_daemon_comm.py \
x52d-mouse-test
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])
{
@ -27,6 +27,11 @@ bool x52d_client_register(int client_fd[X52D_MAX_CLIENTS], int sock_fd)
int fd;
int i;
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 13
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wanalyzer-fd-leak"
#endif
fd = accept(sock_fd, NULL, NULL);
if (fd < 0) {
PINELOG_ERROR(_("Error accepting client connection on socket fd %d: %s"),
@ -40,6 +45,11 @@ bool x52d_client_register(int client_fd[X52D_MAX_CLIENTS], int sock_fd)
goto error;
}
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 13
#pragma GCC diagnostic pop
#endif
for (i = 0; i < X52D_MAX_CLIENTS; i++) {
if (client_fd[i] == INVALID_CLIENT) {
PINELOG_TRACE("Accepted client %d on socket %d, slot %d", fd, sock_fd, i);
@ -109,12 +119,11 @@ int x52d_client_poll(int client_fd[X52D_MAX_CLIENTS], struct pollfd pfd[MAX_CONN
PINELOG_TRACE("Polling %d file descriptors", pfd_count);
retry_poll:
rc = poll(pfd, pfd_count, -1);
do {
rc = poll(pfd, pfd_count, -1);
} while (rc < 0 && errno == EINTR);
if (rc < 0) {
if (errno == EINTR) {
goto retry_poll;
}
PINELOG_ERROR(_("Error %d when polling %d descriptors: %s"),
errno, pfd_count, strerror(errno));
} else if (rc == 0) {

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;
@ -97,8 +97,11 @@ cleanup:
if (orig_tz == NULL) {
unsetenv("TZ");
} else {
setenv("TZ", orig_tz_copy, true);
free(orig_tz_copy);
// If the copy is NULL, then we didn't change TZ, so don't bother
if (orig_tz_copy != NULL) {
setenv("TZ", orig_tz_copy, true);
free(orig_tz_copy);
}
}
if (new_tz != NULL) {
@ -166,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;
}
@ -89,6 +89,22 @@ sock_failure:
return -1;
}
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd)
{
/* Cleanup any existing socket */
unlink(local->sun_path);
if (bind(sock_fd, (struct sockaddr *)local, (socklen_t)len) < 0) {
/* Failure binding socket */
return -1;
}
if (listen(sock_fd, X52D_MAX_CLIENTS) < 0) {
return -1;
}
return 0;
}
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen)
{
int i = 0;

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];
@ -208,77 +208,21 @@ static void cmd_config(char *buffer, int *buflen, int argc, char **argv)
ERR_fmt("Unknown subcommand '%s' for 'config' command", argv[1]);
}
struct level_map {
int level;
const char *string;
};
#define DATA_LMAP(level, resp) do {\
int input_level_ ## __LINE__ = level; \
const char *lmap_level_ ## __LINE__ = lookup_level_by_id(input_level_ ## __LINE__); \
char lmap_unknown_level ## __LINE__[32] = {0}; \
if (lmap_level_ ## __LINE__ == NULL) { \
snprintf(lmap_unknown_level ## __LINE__, sizeof(lmap_unknown_level ## __LINE__), \
"unknown (%d)", input_level_ ## __LINE__); \
lmap_level_ ## __LINE__ = lmap_unknown_level ## __LINE__; \
} \
DATA(resp, lmap_level_ ## __LINE__); \
} while(0)
static int lmap_get_level(const struct level_map *map, const char *string, int notfound)
{
int i;
for (i = 0; map[i].string != NULL; i++) {
if (strcasecmp(map[i].string, string) == 0) {
return map[i].level;
}
}
return notfound;
}
static const char *lmap_get_string(const struct level_map *map, int level)
{
int i;
for (i = 0; map[i].string != NULL; i++) {
if (map[i].level == level) {
return map[i].string;
}
}
return NULL;
}
static int array_find_index(const char **array, int nmemb, const char *string)
{
int i;
for (i = 0; i < nmemb; i++) {
if (strcasecmp(array[i], string) == 0) {
return i;
}
}
return nmemb;
}
static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
{
static const char *modules[X52D_MOD_MAX] = {
[X52D_MOD_CONFIG] = "config",
[X52D_MOD_CLOCK] = "clock",
[X52D_MOD_DEVICE] = "device",
[X52D_MOD_IO] = "io",
[X52D_MOD_LED] = "led",
[X52D_MOD_MOUSE] = "mouse",
[X52D_MOD_COMMAND] = "command",
[X52D_MOD_CLIENT] = "client",
[X52D_MOD_NOTIFY] = "notify",
};
// This corresponds to the levels in pinelog
static const struct level_map loglevels[] = {
{PINELOG_LVL_NOTSET, "default"},
{PINELOG_LVL_NONE, "none"},
{PINELOG_LVL_FATAL, "fatal"},
{PINELOG_LVL_ERROR, "error"},
{PINELOG_LVL_WARNING, "warning"},
{PINELOG_LVL_INFO, "info"},
{PINELOG_LVL_DEBUG, "debug"},
{PINELOG_LVL_TRACE, "trace"},
{0, NULL},
};
if (argc < 2) {
ERR("Insufficient arguments for 'logging' command");
return;
@ -287,14 +231,13 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
// logging show [module]
MATCH(1, "show") {
if (argc == 2) {
// Show default logging level
DATA("global", lmap_get_string(loglevels, pinelog_get_level()));
DATA_LMAP(pinelog_get_level(), "global");
} else if (argc == 3) {
int module = array_find_index(modules, X52D_MOD_MAX, argv[2]);
if (module == X52D_MOD_MAX) {
int module = lookup_module_by_name(argv[2]);
if (module == INT_MAX) {
ERR_fmt("Invalid module '%s'", argv[2]);
} else {
DATA(argv[2], lmap_get_string(loglevels, pinelog_get_module_level(module)));
DATA_LMAP(pinelog_get_module_level(module), argv[2]);
}
} else {
ERR_fmt("Unexpected arguments for 'logging show' command; got %d, expected 2 or 3", argc);
@ -306,7 +249,7 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
// logging set [module] <level>
MATCH(1, "set") {
if (argc == 3) {
int level = lmap_get_level(loglevels, argv[2], INT_MAX);
int level = lookup_level_by_name(argv[2]);
if (level == INT_MAX) {
ERR_fmt("Unknown level '%s' for 'logging set' command", argv[2]);
} else if (level == PINELOG_LVL_NOTSET) {
@ -316,10 +259,10 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
OK("logging", "set", argv[2]);
}
} else if (argc == 4) {
int level = lmap_get_level(loglevels, argv[3], INT_MAX);
int module = array_find_index(modules, X52D_MOD_MAX, argv[2]);
int level = lookup_level_by_name(argv[3]);
int module = lookup_module_by_name(argv[2]);
if (module == X52D_MOD_MAX) {
if (module == INT_MAX) {
ERR_fmt("Invalid module '%s'", argv[2]);
return;
}
@ -397,6 +340,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"),
@ -412,7 +356,6 @@ int x52d_command_init(const char *sock_path)
int sock_fd;
int len;
struct sockaddr_un local;
int flags;
x52d_client_init(client_fd);
@ -431,33 +374,17 @@ int x52d_command_init(const char *sock_path)
return -1;
}
/* Mark the socket as non-blocking */
flags = fcntl(sock_fd, F_GETFL);
if (flags < 0) {
PINELOG_ERROR(_("Error getting command socket flags: %s"), strerror(errno));
goto sock_failure;
}
if (fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
PINELOG_ERROR(_("Error setting command socket flags: %s"), strerror(errno));
goto sock_failure;
}
/* Cleanup any existing socket */
unlink(local.sun_path);
if (bind(sock_fd, (struct sockaddr *)&local, (socklen_t)len) < 0) {
/* Failure binding socket */
PINELOG_ERROR(_("Error binding to command socket: %s"), strerror(errno));
goto listen_failure;
}
if (listen(sock_fd, X52D_MAX_CLIENTS) < 0) {
PINELOG_ERROR(_("Error listening on command socket: %s"), strerror(errno));
goto listen_failure;
}
command_sock_fd = sock_fd;
if (command_sock_fd < 0) {
command_sock_fd = -1;
/* Mark the socket as non-blocking */
if (x52d_set_socket_nonblocking(sock_fd) < 0) {
PINELOG_ERROR(_("Error marking command socket as nonblocking: %s"),
strerror(errno));
goto sock_failure;
}
if (x52d_listen_socket(&local, len, sock_fd) < 0) {
PINELOG_ERROR(_("Error listening on command socket: %s"), strerror(errno));
goto listen_failure;
}

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

@ -0,0 +1,54 @@
{
"_comment": "The configuration registry is a historic record of\nall configuration identifiers. Do NOT edit this file manually, or else, the\ncommunication protocol may break.",
"sections": {
"CLOCK": 1,
"LED": 2,
"BRIGHTNESS": 3,
"MOUSE": 4,
"PROFILES": 5
},
"options": {
"CLOCK": {
"ENABLED": 1,
"PRIMARYISLOCAL": 2,
"SECONDARY": 3,
"TERTIARY": 4,
"FORMATPRIMARY": 5,
"FORMATSECONDARY": 6,
"FORMATTERTIARY": 7,
"DATEFORMAT": 8
},
"LED": {
"FIRE": 1,
"THROTTLE": 2,
"A": 3,
"B": 4,
"D": 5,
"E": 6,
"T1": 7,
"T2": 8,
"T3": 9,
"POV": 10,
"CLUTCH": 11
},
"BRIGHTNESS": {
"MFD": 1,
"LED": 2
},
"MOUSE": {
"ENABLED": 1,
"SENSITIVITY": 2,
"SPEED": 3,
"REVERSESCROLL": 4,
"ISOMETRICMODE": 5,
"CURVEFACTOR": 6,
"DEADZONE": 7
},
"PROFILES": {
"DIRECTORY": 1,
"CLUTCHENABLED": 2,
"CLUTCHLATCHED": 3,
"KEYBOARDLAYOUT": 4
}
}
}

View File

@ -26,18 +26,6 @@
#define X52D_MAX_CLIENTS 63
enum {
X52D_MOD_CONFIG,
X52D_MOD_CLOCK,
X52D_MOD_DEVICE,
X52D_MOD_IO,
X52D_MOD_LED,
X52D_MOD_MOUSE,
X52D_MOD_COMMAND,
X52D_MOD_CLIENT,
X52D_MOD_NOTIFY,
X52D_MOD_MAX
};
#include "module-map.h" // For module IDs
#endif // !defined X52D_CONST_H

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

182
daemon/meson.build 100644
View File

@ -0,0 +1,182 @@
# x52d (dep_config_h: Meson build-config.h; private API is daemon/config.h)
config_defs = custom_target('config-defs',
depend_files: ['x52d_map_config.py', 'x52d.conf'],
input: [
'x52d.conf',
'config_registry.json'
],
output: [
'config-defs.h',
'config-defs.c',
'config_defs.py'
],
command: [
python, meson.current_source_dir() / 'x52d_map_config.py',
'@INPUT0@', '@INPUT1@',
'@OUTPUT0@', '@OUTPUT1@', '@OUTPUT2@'
])
module_defs = custom_target('module-defs',
depend_files: ['x52d_gen_module.py', 'module_defs.py'],
output: ['module-map.h', 'module-map.c'],
command: [python, meson.current_source_dir() / 'x52d_gen_module.py',
'@OUTPUT0@', '@OUTPUT1@'])
# Header only: ordering on module-map.h without compiling module-map.c per target.
dep_module_map_gen = declare_dependency(sources: module_defs[0])
# Full module_defs: name-id-map.c includes module-map.h (generated with the .c).
slib_comm_defs = static_library('x52dcommdefs',
config_defs[1],
module_defs,
'name-id-map.c',
)
libx52dcomm_version = '1.0.0'
libx52dcomm_sources = [
'comm_client.c',
'comm_internal.c'
]
root_includes = include_directories('..')
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
version: libx52dcomm_version,
c_args: sym_hidden_cargs,
install: true,
include_directories: [includes, root_includes])
pkgconfig.generate(lib_libx52dcomm,
name: 'x52dcomm',
description: 'Client library for communicating with the x52d X52 daemon.',
version: libx52dcomm_version,
)
x52d_sources = [
'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')
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs]
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h,
dep_module_map_gen]
x52d_cflags = []
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
install: true,
include_directories: [includes, root_includes],
c_args: sym_hidden_cargs + x52d_cflags,
dependencies: x52d_deps,
link_with: x52d_linkwith)
exe_x52ctl = executable('x52ctl', 'daemon_control.c',
install: true,
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
include_directories: [includes, root_includes],
link_with: lib_libx52dcomm)
install_data('x52d.conf',
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
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'))
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, root_includes],
dependencies: [dep_pinelog, dep_cmocka, dep_intl, dep_math, dep_config_h,
dep_module_map_gen])
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, root_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, root_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, root_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

View File

@ -0,0 +1,28 @@
"""Module name to identifier mapping"""
from enum import Enum
class Module(Enum):
"""Module name to identifier"""
CONFIG = 0
CLOCK = 1
DEVICE = 2
IO = 3
LED = 4
MOUSE = 5
COMMAND = 6
CLIENT = 7
NOTIFY = 8
KEYBOARD_LAYOUT = 9
class LogLevel(Enum):
"""Map log level names to pinelog levels"""
# This is hard coded to the pinelog levels
NOTSET = -2
NONE = -1
FATAL = 0
ERROR = 1
WARNING = 2
INFO = 3
DEBUG = 4
TRACE = 5

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

@ -0,0 +1,59 @@
/*
* Name ID map - needed to map module/loglevel names to numeric v alues
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stddef.h>
#include <string.h>
#include "name-id-map.h"
#include "module-map.h"
static int map_get_id(const struct name_id_map *map, const char *string)
{
int i;
for (i = 0; map[i].name != NULL; i++) {
if (strcasecmp(map[i].name, string) == 0) {
return map[i].id;
}
}
// We've broken out of the loop, return the current ID
return map[i].id;
}
static const char *map_get_name(const struct name_id_map *map, int id)
{
int i;
for (i = 0; map[i].name != NULL; i++) {
if (map[i].id == id) {
return map[i].name;
}
}
return NULL;
}
int lookup_module_by_name(const char *name)
{
return map_get_id(module_map, name);
}
const char * lookup_module_by_id(int id)
{
return map_get_name(module_map, id);
}
int lookup_level_by_name(const char *name)
{
return map_get_id(loglevel_map, name);
}
const char * lookup_level_by_id(int id)
{
return map_get_name(loglevel_map, id);
}

View File

@ -0,0 +1,19 @@
/*
* Name ID map - needed to map module/loglevel names to numeric v alues
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#ifndef NAME_ID_MAP_H
#define NAME_ID_MAP_H
struct name_id_map {
char *name;
int id;
};
extern const struct name_id_map module_map[];
extern const struct name_id_map loglevel_map[];
#endif // !defined NAME_ID_MAP_H

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;
@ -52,15 +52,7 @@ static int listen_notify(const char *notify_sock_path)
return -1;
}
/* Cleanup any existing socket */
unlink(local.sun_path);
if (bind(sock_fd, (struct sockaddr *)&local, (socklen_t)len) < 0) {
/* Failure binding socket */
PINELOG_ERROR(_("Error binding to notification socket: %s"), strerror(errno));
goto listen_failure;
}
if (listen(sock_fd, X52D_MAX_CLIENTS) < 0) {
if (x52d_listen_socket(&local, len, sock_fd) < 0) {
PINELOG_ERROR(_("Error listening on notification socket: %s"), strerror(errno));
goto listen_failure;
}
@ -79,47 +71,35 @@ static void * x52_notify_thr(void * param)
char buffer[X52D_BUFSZ];
uint16_t bufsiz;
int rc;
(void)param;
for (;;) {
read_pipe_size:
rc = read(notify_pipe[0], &bufsiz, sizeof(bufsiz));
if (rc < 0) {
if (errno == EINTR) {
goto read_pipe_size;
} else {
PINELOG_ERROR(_("Error %d reading from pipe: %s"),
errno, strerror(errno));
}
}
do {
rc = read(notify_pipe[0], &bufsiz, sizeof(bufsiz));
} while (rc < 0 && errno == EINTR);
if (rc < 0) {
PINELOG_ERROR(_("Error %d reading from pipe: %s"),
errno, strerror(errno));
// Error condition, try again
continue;
}
read_pipe_data:
rc = read(notify_pipe[0], buffer, bufsiz);
if (rc < 0) {
if (errno == EINTR) {
goto read_pipe_data;
} else {
PINELOG_ERROR(_("Error %d reading from pipe: %s"),
errno, strerror(errno));
}
}
do {
rc = read(notify_pipe[0], buffer, bufsiz);
} while (rc < 0 && errno == EINTR);
if (rc < 0) {
PINELOG_ERROR(_("Error %d reading from pipe: %s"),
errno, strerror(errno));
// Error condition, try again
continue;
}
for (int i = 0; i < X52D_MAX_CLIENTS; i++) {
// Broadcast to every connected client
if (client_fd[i] != INVALID_CLIENT) {
write_client_notification:
rc = write(client_fd[i], buffer, bufsiz);
if (rc < 0 && errno == EINTR) {
goto write_client_notification;
}
do {
rc = write(client_fd[i], buffer, bufsiz);
} while (rc < 0 && errno == EINTR);
}
}
}
@ -167,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

@ -193,10 +193,9 @@ A side effect of this is that the client could request a set for any arbitrary
section and key pair, and if that pair was not recognized, it would be ignored,
but the daemon would still send an `OK` response.
Finally, this will only set the value within the configuration memory
structures, and will not invoke any callback to update the rest of the threads
or device state. The client will need to call the `apply` subcommand to actually
invoke the necessary callbacks.
This will set the value within the configuration memory structures, and will
immediately invoke the relevant callback to update the rest of the threads or
device state.
\b Arguments
@ -221,22 +220,6 @@ invoke the necessary callbacks.
ERR\0Error 22 setting 'led.fire'='none': Invalid argument\0
```
# Apply configuration
The `config apply` command will invoke all the callbacks and ensure that the
configuration is applied to the running state.
\b Arguments
- `config`
- `apply`
\b Returns
- `OK`
- `config`
- `apply`
*/
/**
@ -259,12 +242,14 @@ user to fine tune the logging while the daemon is running.
of modules is below:
- \c Config
- \c Cllient
- \c Clock
- \c Command
- \c Device
- \c IO
- \c LED
- \c Mouse
- \c Notify
# Logging levels

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

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Generate the module name to map for use by the daemon"""
import os.path
import sys
import module_defs
def main():
if len(sys.argv) != 3:
print("Usage: {sys.argv[0]} <output-header> <output-source>", file=sys.stderr)
sys.exit(1)
with open(sys.argv[1], 'w', encoding='utf-8') as out_fd:
# Generate the header
print("// Autogenerated module/loglevel header - DO NOT EDIT\n",
file=out_fd)
include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper()
print(f"#ifndef {include_guard}", file=out_fd)
print(f"#define {include_guard}\n", file=out_fd)
for mod in module_defs.Module:
print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd)
print(f"#define X52D_MOD_GLOBAL 0xFF", file=out_fd)
print(f"#define X52D_MOD_MAX {len(module_defs.Module)}\n", file=out_fd)
print(f"int lookup_module_by_name(const char *name);", file=out_fd)
print(f"const char * lookup_module_by_id(int id);", file=out_fd)
print(f"int lookup_level_by_name(const char *name);", file=out_fd)
print(f"const char * lookup_level_by_id(int id);", file=out_fd)
print(f"\n#endif // !defined {include_guard}", file=out_fd)
with open(sys.argv[2], 'w', encoding='utf-8') as out_fd:
print("// Autogenerated module/loglevel tables - DO NOT EDIT\n",
file=out_fd)
print('#include <stddef.h>', file=out_fd)
print('#include <limits.h>\n', file=out_fd)
print(f'#include "{os.path.basename(sys.argv[1])}"', file=out_fd)
print('#include "name-id-map.h"\n', file=out_fd)
print('const struct name_id_map module_map[] = {', file=out_fd)
for mod in module_defs.Module:
print(f' {{ "{mod.name.lower()}", {mod.value} }},', file=out_fd)
print(' { NULL, INT_MAX }', file=out_fd)
print('};\n', file=out_fd)
print('const struct name_id_map loglevel_map[] = {', file=out_fd)
for level in module_defs.LogLevel:
if level == module_defs.LogLevel.NOTSET:
level_name = 'default'
else:
level_name = level.name.lower()
print(f' {{ "{level_name}", {level.value} }},', file=out_fd)
print(' { NULL, INT_MAX }', file=out_fd)
print('};\n', file=out_fd)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Read the default configuration file, and create ID enums for sections
and options.
"""
import argparse
import configparser
import json
import os.path
from collections import defaultdict
from itertools import count
from pprint import pprint
class ConfigToEnum:
"""ConfigToEnum scans a configuration file, and dumps the secions and
options within a section as Python Enums"""
REGISTRY_COMMENT = """The configuration registry is a historic record of
all configuration identifiers. Do NOT edit this file manually, or else, the
communication protocol may break."""
def __init__(self, cfg_file, config_ids):
"""Initialize the object"""
self.config = configparser.ConfigParser(default_section=None, interpolation=None)
self.config.optionxform = str
self.config.read(cfg_file)
self.registry_file = config_ids
try:
with open(self.registry_file, encoding='utf-8') as regfd:
self.config_ids = json.load(regfd)
except Exception:
# On any error, ignore it and start with a clean slate
self.config_ids = {}
self.sections = {}
self.options = {}
def parse(self):
"""Parse the config object and assign IDs"""
self._parse_sections()
for section in self.config.sections():
self._parse_options(section)
def _parse_sections(self):
"""Assign IDs to each section"""
sections = {}
unassigned = []
for section in self.config.sections():
section = section.upper()
section_id = self.config_ids.get('sections', {}).get(section)
if section_id is None:
unassigned.append(section)
else:
sections[section] = section_id
if not sections:
counter = count(1)
else:
counter = count(max(sections.values()) + 1)
sections.update({k:v for k, v in zip(unassigned, counter)})
orig_sections = self.config_ids.get('sections', {})
sections.update({k:v for k, v in orig_sections.items() if k not in sections})
self.sections = sections
def _parse_options(self, section):
options = {}
unassigned = []
for option in self.config.options(section):
option = option.upper()
section = section.upper()
option_id = self.config_ids.get('options', {}).get(section, {}).get(option)
if option_id is None:
unassigned.append(option)
else:
options[option] = option_id
if not options:
counter = count(1)
else:
counter = count(max(options.values()) + 1)
options.update({k:v for k, v in zip(unassigned, counter)})
orig_options = self.config_ids.get('options', {}).get(section, {})
# Make sure that we have all the entries already
options.update({k:v for k, v in orig_options.items() if k not in options})
self.options[section] = options
def save_registry(self):
"""Save the generated registry"""
registry = {
"_comment": self.REGISTRY_COMMENT,
"sections": self.sections,
"options": self.options,
}
with open(self.registry_file, 'w', encoding='utf-8') as regfd:
json.dump(registry, regfd, indent=4)
def generate_c_definitions(self, output_header, output_source):
"""Generate the C definitions"""
with open(output_header, 'w', encoding='utf-8') as out_fd:
include_guard = os.path.basename(output_header).replace('-', '_').replace('.', '_').upper()
print("// Autogenerated config identifiers - DO NOT EDIT\n", file=out_fd)
print(f"#ifndef {include_guard}", file=out_fd)
print(f"#define {include_guard}", file=out_fd)
print(file=out_fd)
max_sec_val = max(self.sections.values()) + 1
max_opt_val_global = 0
for section, value in self.sections.items():
print(f"#define CFG_SECTION_{section} {value}", file=out_fd)
max_opt_val = max(self.options[section].values()) + 1
max_opt_val_global = max(max_opt_val, max_opt_val_global)
for option, value in self.options[section].items():
print(f"#define CFG_OPTION_{section}_{option} {value}", file=out_fd)
print(f"#define CFG_OPTION_{section}_MAX_OPTIONS {max_opt_val}\n", file=out_fd)
print(f"#define CFG_SECTION_MAX {max_sec_val}\n", file=out_fd)
print(f"#define CFG_SECTION_MAX_OPT_VAL {max_opt_val_global}\n", file=out_fd)
print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd)
print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd)
print(f"#endif // !defined {include_guard}", file=out_fd)
with open(output_source, 'w', encoding='utf-8') as out_fd:
print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd)
print(f'#include "{os.path.basename(output_header)}"', file=out_fd)
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
for section, value in self.sections.items():
print(f' [{value}] = "{section.lower()}",', file=out_fd)
print("};\n", file=out_fd)
print("const char * options_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd)
for section, value in self.sections.items():
print(f' [{value}] =', '{', file=out_fd)
for option, value in self.options[section].items():
print(f' [{value}] = "{option.lower()}",', file=out_fd)
print(' },', file=out_fd)
print("};\n", file=out_fd)
def generate_py_definitions(self, output_file):
"""Generate the Python definitions"""
try:
out_fd = open(output_file, 'w', encoding='utf-8')
print("'''Autogenerated config identifiers from x52d.conf'''", file=out_fd)
print("# DO NOT EDIT\n", file=out_fd)
print("from enum import Enum", file=out_fd)
print("\nclass Section(Enum):", file=out_fd)
print(" '''Section identifiers'''", file=out_fd)
for section, value in self.sections.items():
print(f" {section} = {value}", file=out_fd)
for section in self.sections.keys():
print(f"\nclass {section}(Enum):", file=out_fd)
print(f" '''Section {section} identifiers'''", file=out_fd)
for option, value in self.options[section].items():
print(f" {option} = {value}", file=out_fd)
finally:
out_fd.close()
def main():
parser = argparse.ArgumentParser(description="Generate C enum and python enum for config")
parser.add_argument('input_file')
parser.add_argument('registry')
parser.add_argument('output_c_header')
parser.add_argument('output_c_strings')
parser.add_argument('output_py_defs')
args = parser.parse_args()
c2e = ConfigToEnum(args.input_file, args.registry)
c2e.parse()
c2e.save_registry()
c2e.generate_c_definitions(args.output_c_header, args.output_c_strings)
c2e.generate_py_definitions(args.output_py_defs)
if __name__ == '__main__':
main()

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

@ -20,6 +20,7 @@ int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote);
const char *x52d_notify_sock_path(const char *sock_path);
int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote);
int x52d_set_socket_nonblocking(int sock_fd);
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd);
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen);
#endif // !defined X52DCOMM_INTERNAL_H

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

Some files were not shown because too many files have changed in this diff Show More