From c8361948c83418b5d6caadeb8fb14fb6bedbf7a2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 26 Sep 2026 20:26:42 -0400 Subject: [PATCH] Add Surface Pro 11 ambient light sensor support The Surface Pro 11's light sensor runs on the ADSP's Qualcomm sensor core, which reports nothing until the host serves it its configuration. Build hexagonrpcd, with registry write support, and libssc, reading Microsoft's "color" sensor, from pinned upstream source into a private directory, and point iio-sensor-proxy at that libssc through a drop-in. The libssc pin includes the fix for iio-sensor-proxy spinning when the sensor server never appears. The configuration is Microsoft and Qualcomm's and is not shipped. surface-pro-11-sensors-extract copies it from the machine's own Windows driver store, with the SoC ID and revision from the running system; it runs only on the Surface Pro 11 OLED and reuses an already mounted Windows partition. The service starts when the ADSP appears, so a missing DSP never delays boot or iio-sensor-proxy, and a sleep hook stops iio-sensor-proxy across suspend so the light stream does not wake the SoC. The package is tagged for the Qualcomm platform guard and published to edge. Validated with test.sh, hexagonrpc's tests, and on a Surface Pro 11: the extracted root matches the previously qualified one, the service starts from the ADSP device, and iio-sensor-proxy reports live illuminance. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../.omarchy/package.json | 4 + ...std-support-writable-sensor-registry.patch | 966 ++++++++++++++++++ .../0001-light-use-surface-color-sensor.patch | 23 + .../10-surface-pro-11-sensors.conf | 11 + pkgbuilds/surface-pro-11-sensors/PKGBUILD | 84 ++ .../surface-pro-11-sensors/README.package.md | 29 + .../surface-pro-11-sensors-extract | 168 +++ .../surface-pro-11-sensors-sleep | 28 + .../surface-pro-11-sensors.service | 36 + pkgbuilds/surface-pro-11-sensors/test.sh | 88 ++ 10 files changed, 1437 insertions(+) create mode 100644 pkgbuilds/surface-pro-11-sensors/.omarchy/package.json create mode 100644 pkgbuilds/surface-pro-11-sensors/0001-apps-std-support-writable-sensor-registry.patch create mode 100644 pkgbuilds/surface-pro-11-sensors/0001-light-use-surface-color-sensor.patch create mode 100644 pkgbuilds/surface-pro-11-sensors/10-surface-pro-11-sensors.conf create mode 100644 pkgbuilds/surface-pro-11-sensors/PKGBUILD create mode 100644 pkgbuilds/surface-pro-11-sensors/README.package.md create mode 100755 pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-extract create mode 100755 pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-sleep create mode 100644 pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors.service create mode 100755 pkgbuilds/surface-pro-11-sensors/test.sh diff --git a/pkgbuilds/surface-pro-11-sensors/.omarchy/package.json b/pkgbuilds/surface-pro-11-sensors/.omarchy/package.json new file mode 100644 index 000000000..2537ec08a --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/.omarchy/package.json @@ -0,0 +1,4 @@ +{ + "source": "local", + "channels": ["edge"] +} diff --git a/pkgbuilds/surface-pro-11-sensors/0001-apps-std-support-writable-sensor-registry.patch b/pkgbuilds/surface-pro-11-sensors/0001-apps-std-support-writable-sensor-registry.patch new file mode 100644 index 000000000..54e27b260 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/0001-apps-std-support-writable-sensor-registry.patch @@ -0,0 +1,966 @@ +From: Brandon Williams +Subject: [PATCH] apps_std: support persistent Qualcomm sensor registries + +Allow the sensor DSP to create and update its persistent registry inside the +existing virtual filesystem sandbox. Implement the reverse-RPC file methods +used by registry generation, extended QAIC method dispatch, and larger input +messages needed while parsing sensor configuration. + +Writes, removal, and rename remain restricted to the sensor registry and its +three generator metadata files. + +Signed-off-by: Brandon Williams +--- +diff --git a/hexagonrpcd/apps_std.c b/hexagonrpcd/apps_std.c +index ff4d5a7..5d6f318 100644 +--- a/hexagonrpcd/apps_std.c ++++ b/hexagonrpcd/apps_std.c +@@ -20,6 +20,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -45,6 +46,39 @@ static const int apps_std_whence_table[] = { + SEEK_END, + }; + ++static bool apps_std_is_registry_path(const char *path) ++{ ++ static const char *const prefixes[] = { ++ "/persist/sensors/registry/", ++ "/mnt/vendor/persist/sensors/registry/", ++ }; ++ size_t i; ++ ++ for (i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]); i++) { ++ const char *relative; ++ const char *name; ++ ++ if (strncmp(path, prefixes[i], strlen(prefixes[i]))) ++ continue; ++ ++ relative = path + strlen(prefixes[i]); ++ if (!strcmp(relative, "sns_reg_version") || ++ !strcmp(relative, "parsed_file_list.csv") || ++ !strcmp(relative, "fstempfile")) ++ return true; ++ ++ if (strncmp(relative, "registry/", strlen("registry/"))) ++ continue; ++ ++ name = relative + strlen("registry/"); ++ if (name[0] != '\0' && strcmp(name, ".") && ++ strcmp(name, "..") && strchr(name, '/') == NULL) ++ return true; ++ } ++ ++ return false; ++} ++ + /* + * This is a placeholder function used to complete any I/O operations. + * File descriptors do not have a flush operation because their reads and +@@ -120,6 +154,40 @@ static uint32_t apps_std_fread(void *data, + return 0; + } + ++static uint32_t apps_std_fwrite(void *data, ++ const struct fastrpc_io_buffer *inbufs, ++ struct fastrpc_io_buffer *outbufs) ++{ ++ struct apps_std_ctx *ctx = data; ++ const struct { ++ uint32_t fd; ++ uint32_t buf_size; ++ } *first_in = inbufs[0].p; ++ struct { ++ uint32_t written; ++ uint32_t is_eof; ++ } *first_out = outbufs[0].p; ++ ssize_t ret; ++ ++ ret = hexagonfs_write(ctx->fds, first_in->fd, ++ first_in->buf_size, inbufs[1].p); ++ if (ret < 0) { ++ fprintf(stderr, "Could not write file: %s\n", strerror(-ret)); ++ return AEE_EFAILED; ++ } ++ ++#ifdef HEXAGONRPC_VERBOSE ++ printf("write(%u, %u) -> %ld\n", first_in->fd, ++ first_in->buf_size, ++ ret); ++#endif ++ ++ first_out->written = ret; ++ first_out->is_eof = first_out->written < first_in->buf_size; ++ ++ return 0; ++} ++ + static uint32_t apps_std_fseek(void *data, + const struct fastrpc_io_buffer *inbufs, + struct fastrpc_io_buffer *outbufs) +@@ -150,6 +218,23 @@ static uint32_t apps_std_fseek(void *data, + return 0; + } + ++static uint32_t apps_std_fsync(void *data, ++ const struct fastrpc_io_buffer *inbufs, ++ struct fastrpc_io_buffer *outbufs) ++{ ++ struct apps_std_ctx *ctx = data; ++ const uint32_t *fd = inbufs[0].p; ++ int ret; ++ ++ ret = hexagonfs_fsync(ctx->fds, *fd); ++ if (ret) { ++ fprintf(stderr, "Could not sync file: %s\n", strerror(-ret)); ++ return AEE_EFAILED; ++ } ++ ++ return 0; ++} ++ + static uint32_t apps_std_fopen_with_env(void *data, + const struct fastrpc_io_buffer *inbufs, + struct fastrpc_io_buffer *outbufs) +@@ -157,19 +242,27 @@ static uint32_t apps_std_fopen_with_env(void *data, + struct apps_std_ctx *ctx = data; + uint32_t *out = outbufs[0].p; + char rw_mode; +- int dirfd, fd; ++ int dirfd, fd, flags; + + // The name and environment variable must also be NULL-terminated +- if (((const char *) inbufs[1].p)[inbufs[1].s - 1] != 0 +- || ((const char *) inbufs[3].p)[inbufs[3].s - 1] != 0 +- || ((const char *) inbufs[4].p)[inbufs[4].s - 1] != 0) ++ if (inbufs[1].s == 0 || inbufs[3].s == 0 || inbufs[4].s == 0 || ++ ((const char *) inbufs[1].p)[inbufs[1].s - 1] != 0 || ++ ((const char *) inbufs[3].p)[inbufs[3].s - 1] != 0 || ++ ((const char *) inbufs[4].p)[inbufs[4].s - 1] != 0) + return AEE_EBADPARM; + + rw_mode = ((const char *) inbufs[4].p)[0]; + if (rw_mode == 'w' || rw_mode == 'a') { +- fprintf(stderr, "Tried to open %s for writing\n", +- (const char *) inbufs[3].p); +- return AEE_EUNSUPPORTED; ++ if (!apps_std_is_registry_path(inbufs[3].p)) { ++ fprintf(stderr, "Tried to open %s for writing\n", ++ (const char *) inbufs[3].p); ++ return AEE_EUNSUPPORTED; ++ } ++ ++ flags = O_WRONLY | O_CREAT; ++ flags |= rw_mode == 'w' ? O_TRUNC : O_APPEND; ++ } else { ++ flags = O_RDONLY; + } + + if (!strcmp(inbufs[1].p, "ADSP_LIBRARY_PATH")) { +@@ -188,11 +281,12 @@ static uint32_t apps_std_fopen_with_env(void *data, + return AEE_EFAILED; + } + +- fd = hexagonfs_openat(ctx->fds, ctx->rootfd, dirfd, inbufs[3].p); ++ fd = hexagonfs_openat_flags(ctx->fds, ctx->rootfd, dirfd, ++ inbufs[3].p, flags, 0644); + if (fd < 0) { + fprintf(stderr, "Could not open %s: %s\n", + (const char *) inbufs[3].p, +- strerror(errno)); ++ strerror(-fd)); + return AEE_EFAILED; + } + +@@ -208,6 +302,69 @@ static uint32_t apps_std_fopen_with_env(void *data, + return 0; + } + ++static uint32_t apps_std_fremove(void *data, ++ const struct fastrpc_io_buffer *inbufs, ++ struct fastrpc_io_buffer *outbufs) ++{ ++ struct apps_std_ctx *ctx = data; ++ const char *path = inbufs[1].p; ++ int ret; ++ ++ if (inbufs[1].s == 0 || ++ ((const char *) inbufs[1].p)[inbufs[1].s - 1] != 0) ++ return AEE_EBADPARM; ++ ++ if (!apps_std_is_registry_path(path)) { ++ fprintf(stderr, "Tried to remove %s\n", path); ++ return AEE_EUNSUPPORTED; ++ } ++ ++ ret = hexagonfs_unlinkat(ctx->fds, ctx->rootfd, ctx->rootfd, path, 0); ++ if (ret && ret != -ENOENT) { ++ fprintf(stderr, "Could not remove %s: %s\n", ++ path, strerror(-ret)); ++ return AEE_EFAILED; ++ } ++ ++#ifdef HEXAGONRPC_VERBOSE ++ printf("remove(%s)\n", path); ++#endif ++ ++ return 0; ++} ++ ++static uint32_t apps_std_frename(void *data, ++ const struct fastrpc_io_buffer *inbufs, ++ struct fastrpc_io_buffer *outbufs) ++{ ++ struct apps_std_ctx *ctx = data; ++ const char *old_path = inbufs[1].p; ++ const char *new_path = inbufs[2].p; ++ int ret; ++ ++ if (inbufs[1].s == 0 || inbufs[2].s == 0 || ++ old_path[inbufs[1].s - 1] != '\0' || ++ new_path[inbufs[2].s - 1] != '\0') ++ return AEE_EBADPARM; ++ ++ if (!apps_std_is_registry_path(old_path) || ++ !apps_std_is_registry_path(new_path)) { ++ fprintf(stderr, "Tried to rename %s to %s\n", ++ old_path, new_path); ++ return AEE_EUNSUPPORTED; ++ } ++ ++ ret = hexagonfs_renameat(ctx->fds, ctx->rootfd, ctx->rootfd, ++ old_path, ctx->rootfd, new_path); ++ if (ret) { ++ fprintf(stderr, "Could not rename %s to %s: %s\n", ++ old_path, new_path, strerror(-ret)); ++ return AEE_EFAILED; ++ } ++ ++ return 0; ++} ++ + static uint32_t apps_std_opendir(void *data, + const struct fastrpc_io_buffer *inbufs, + struct fastrpc_io_buffer *outbufs) +@@ -405,70 +562,63 @@ void fastrpc_apps_std_deinit(struct fastrpc_interface *iface) + free(iface); + } + +-static const struct fastrpc_function_impl apps_std_procs[] = { +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { ++static const struct fastrpc_function_impl apps_std_procs[34] = { ++ [2] = { + .def = &apps_std_fflush_def, + .impl = apps_std_fflush, + }, +- { ++ [3] = { + .def = &apps_std_fclose_def, + .impl = apps_std_fclose, + }, +- { ++ [4] = { + .def = &apps_std_fread_def, + .impl = apps_std_fread, + }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { ++ [5] = { ++ .def = &apps_std_fwrite_def, ++ .impl = apps_std_fwrite, ++ }, ++ [9] = { + .def = &apps_std_fseek_def, + .impl = apps_std_fseek, + }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { ++ [19] = { + .def = &apps_std_fopen_with_env_def, + .impl = apps_std_fopen_with_env, + }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { ++ [23] = { ++ .def = &apps_std_fsync_def, ++ .impl = apps_std_fsync, ++ }, ++ [24] = { ++ .def = &apps_std_fremove_def, ++ .impl = apps_std_fremove, ++ }, ++ [26] = { + .def = &apps_std_opendir_def, + .impl = apps_std_opendir, + }, +- { ++ [27] = { + .def = &apps_std_closedir_def, + .impl = apps_std_closedir, + }, +- { ++ [28] = { + .def = &apps_std_readdir_def, + .impl = apps_std_readdir, + }, +- { .def = NULL, .impl = NULL, }, +- { .def = NULL, .impl = NULL, }, +- { ++ [31] = { + .def = &apps_std_stat_def, + .impl = apps_std_stat, + }, ++ [33] = { ++ .def = &apps_std_frename_def, ++ .impl = apps_std_frename, ++ }, + }; + + const struct fastrpc_interface apps_std_interface = { + .name = "apps_std", +- .n_procs = 32, ++ .n_procs = 34, + .procs = apps_std_procs, + }; +diff --git a/hexagonrpcd/hexagonfs.c b/hexagonrpcd/hexagonfs.c +index bd9f353..439070c 100644 +--- a/hexagonrpcd/hexagonfs.c ++++ b/hexagonrpcd/hexagonfs.c +@@ -20,6 +20,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -139,7 +140,8 @@ err: + return ret; + } + +-int hexagonfs_openat(struct hexagonfs_fd **fds, int rootfd, int dirfd, const char *name) ++int hexagonfs_openat_flags(struct hexagonfs_fd **fds, int rootfd, int dirfd, ++ const char *name, int flags, mode_t mode) + { + struct hexagonfs_fd *fd; + const char *curr = name; +@@ -169,7 +171,10 @@ int hexagonfs_openat(struct hexagonfs_fd **fds, int rootfd, int dirfd, const cha + } else if (!strcmp(segment, "..")) { + fd = pop_dir(fd, fds[rootfd]); + } else { +- ret = fd->ops->openat(fd, segment, expect_dir, &fd); ++ int segment_flags = *curr == '\0' ? flags : O_RDONLY; ++ ++ ret = fd->ops->openat(fd, segment, expect_dir, ++ segment_flags, mode, &fd); + } + + next: +@@ -191,6 +196,12 @@ err: + return ret; + } + ++int hexagonfs_openat(struct hexagonfs_fd **fds, int rootfd, int dirfd, ++ const char *name) ++{ ++ return hexagonfs_openat_flags(fds, rootfd, dirfd, name, O_RDONLY, 0); ++} ++ + int hexagonfs_close(struct hexagonfs_fd **fds, int fileno) + { + struct hexagonfs_fd *fd; +@@ -244,6 +255,177 @@ ssize_t hexagonfs_read(struct hexagonfs_fd **fds, int fileno, size_t size, void + return fd->ops->read(fd, size, ptr); + } + ++ssize_t hexagonfs_write(struct hexagonfs_fd **fds, int fileno, size_t size, ++ const void *ptr) ++{ ++ struct hexagonfs_fd *fd; ++ ++ if (fileno < 0 || fileno >= HEXAGONFS_MAX_FD) ++ return -EBADF; ++ ++ fd = fds[fileno]; ++ if (fd == NULL) ++ return -EBADF; ++ ++ if (fd->ops->write == NULL) ++ return -ENOSYS; ++ ++ return fd->ops->write(fd, size, ptr); ++} ++ ++int hexagonfs_fsync(struct hexagonfs_fd **fds, int fileno) ++{ ++ struct hexagonfs_fd *fd; ++ ++ if (fileno < 0 || fileno >= HEXAGONFS_MAX_FD) ++ return -EBADF; ++ ++ fd = fds[fileno]; ++ if (fd == NULL) ++ return -EBADF; ++ ++ if (fd->ops->fsync == NULL) ++ return -ENOSYS; ++ ++ return fd->ops->fsync(fd); ++} ++ ++int hexagonfs_unlinkat(struct hexagonfs_fd **fds, int rootfd, int dirfd, ++ const char *name, int flags) ++{ ++ struct hexagonfs_fd *dir; ++ char *path; ++ char *base; ++ int parentfd; ++ bool close_parent = false; ++ int ret; ++ ++ path = strdup(name); ++ if (path == NULL) ++ return -ENOMEM; ++ ++ base = strrchr(path, '/'); ++ if (base != NULL) { ++ *base++ = '\0'; ++ if (path[0] == '\0') { ++ parentfd = rootfd; ++ } else { ++ parentfd = hexagonfs_openat(fds, rootfd, dirfd, path); ++ close_parent = parentfd >= 0; ++ } ++ } else { ++ base = path; ++ parentfd = dirfd; ++ } ++ ++ if (parentfd < 0) { ++ ret = parentfd; ++ goto out; ++ } ++ ++ dir = fds[parentfd]; ++ if (base[0] == '\0' || !strcmp(base, ".") || !strcmp(base, "..")) ++ ret = -EINVAL; ++ else if (dir->ops->unlinkat == NULL) ++ ret = -ENOSYS; ++ else ++ ret = dir->ops->unlinkat(dir, base, flags); ++ ++ if (close_parent) ++ hexagonfs_close(fds, parentfd); ++out: ++ free(path); ++ return ret; ++} ++ ++struct parent_path { ++ char *path; ++ char *base; ++ int fd; ++ bool close_fd; ++}; ++ ++static int hexagonfs_open_parent(struct hexagonfs_fd **fds, int rootfd, ++ int dirfd, const char *name, ++ struct parent_path *parent) ++{ ++ char *slash; ++ ++ parent->path = strdup(name); ++ if (parent->path == NULL) ++ return -ENOMEM; ++ ++ slash = strrchr(parent->path, '/'); ++ if (slash != NULL) { ++ *slash++ = '\0'; ++ parent->base = slash; ++ if (parent->path[0] == '\0') { ++ parent->fd = rootfd; ++ parent->close_fd = false; ++ } else { ++ parent->fd = hexagonfs_openat(fds, rootfd, dirfd, ++ parent->path); ++ parent->close_fd = parent->fd >= 0; ++ } ++ } else { ++ parent->base = parent->path; ++ parent->fd = dirfd; ++ parent->close_fd = false; ++ } ++ ++ if (parent->fd < 0) ++ return parent->fd; ++ ++ if (parent->base[0] == '\0' || !strcmp(parent->base, ".") || ++ !strcmp(parent->base, "..")) ++ return -EINVAL; ++ ++ return 0; ++} ++ ++static void hexagonfs_close_parent(struct hexagonfs_fd **fds, ++ struct parent_path *parent) ++{ ++ if (parent->close_fd) ++ hexagonfs_close(fds, parent->fd); ++ free(parent->path); ++} ++ ++int hexagonfs_renameat(struct hexagonfs_fd **fds, int rootfd, int olddirfd, ++ const char *oldname, int newdirfd, const char *newname) ++{ ++ struct parent_path old_parent = { 0 }; ++ struct parent_path new_parent = { 0 }; ++ struct hexagonfs_fd *old_dir; ++ struct hexagonfs_fd *new_dir; ++ int ret; ++ ++ ret = hexagonfs_open_parent(fds, rootfd, olddirfd, oldname, ++ &old_parent); ++ if (ret) ++ goto out_old; ++ ++ ret = hexagonfs_open_parent(fds, rootfd, newdirfd, newname, ++ &new_parent); ++ if (ret) ++ goto out_new; ++ ++ old_dir = fds[old_parent.fd]; ++ new_dir = fds[new_parent.fd]; ++ if (old_dir->ops->renameat == NULL || ++ old_dir->ops->renameat != new_dir->ops->renameat) ++ ret = -ENOSYS; ++ else ++ ret = old_dir->ops->renameat(old_dir, old_parent.base, ++ new_dir, new_parent.base); ++ ++out_new: ++ hexagonfs_close_parent(fds, &new_parent); ++out_old: ++ hexagonfs_close_parent(fds, &old_parent); ++ return ret; ++} ++ + int hexagonfs_readdir(struct hexagonfs_fd **fds, int fileno, size_t ent_size, char *ent) + { + struct hexagonfs_fd *fd; +diff --git a/hexagonrpcd/hexagonfs.h b/hexagonrpcd/hexagonfs.h +index 7682604..157b049 100644 +--- a/hexagonrpcd/hexagonfs.h ++++ b/hexagonrpcd/hexagonfs.h +@@ -37,9 +37,16 @@ struct hexagonfs_file_ops { + int (*openat)(struct hexagonfs_fd *dir, + const char *segment, + bool expect_dir, ++ int flags, ++ mode_t mode, + struct hexagonfs_fd **out); + int (*readdir)(struct hexagonfs_fd *fd, size_t size, char *out); + ssize_t (*read)(struct hexagonfs_fd *fd, size_t size, void *ptr); ++ ssize_t (*write)(struct hexagonfs_fd *fd, size_t size, const void *ptr); ++ int (*fsync)(struct hexagonfs_fd *fd); ++ int (*unlinkat)(struct hexagonfs_fd *dir, const char *name, int flags); ++ int (*renameat)(struct hexagonfs_fd *old_dir, const char *old_name, ++ struct hexagonfs_fd *new_dir, const char *new_name); + int (*stat)(struct hexagonfs_fd *fd, struct stat *stats); + int (*seek)(struct hexagonfs_fd *fd, off_t off, int whence); + }; +@@ -71,11 +78,20 @@ extern struct hexagonfs_file_ops hexagonfs_virt_dir_ops; + + int hexagonfs_open_root(struct hexagonfs_fd **fds, struct hexagonfs_dirent *root); + int hexagonfs_openat(struct hexagonfs_fd **fds, int rootfd, int dirfd, const char *name); ++int hexagonfs_openat_flags(struct hexagonfs_fd **fds, int rootfd, int dirfd, ++ const char *name, int flags, mode_t mode); + int hexagonfs_close(struct hexagonfs_fd **fds, int fileno); + + int hexagonfs_fstat(struct hexagonfs_fd **fds, int fileno, struct stat *stats); + int hexagonfs_lseek(struct hexagonfs_fd **fds, int fileno, off_t pos, int whence); + int hexagonfs_readdir(struct hexagonfs_fd **fds, int fileno, size_t size, char *name); + ssize_t hexagonfs_read(struct hexagonfs_fd **fds, int fileno, size_t size, void *ptr); ++ssize_t hexagonfs_write(struct hexagonfs_fd **fds, int fileno, size_t size, ++ const void *ptr); ++int hexagonfs_fsync(struct hexagonfs_fd **fds, int fileno); ++int hexagonfs_unlinkat(struct hexagonfs_fd **fds, int rootfd, int dirfd, ++ const char *name, int flags); ++int hexagonfs_renameat(struct hexagonfs_fd **fds, int rootfd, int olddirfd, ++ const char *oldname, int newdirfd, const char *newname); + + #endif +diff --git a/hexagonrpcd/hexagonfs_mapped.c b/hexagonrpcd/hexagonfs_mapped.c +index fd28c8c..02e9be4 100644 +--- a/hexagonrpcd/hexagonfs_mapped.c ++++ b/hexagonrpcd/hexagonfs_mapped.c +@@ -24,6 +24,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -81,12 +82,13 @@ err: + static int mapped_openat(struct hexagonfs_fd *dir, + const char *segment, + bool expect_dir, ++ int flags, ++ mode_t mode, + struct hexagonfs_fd **out) + { + struct mapped_ctx *dir_ctx = dir->data; + struct hexagonfs_fd *fd; + struct mapped_ctx *ctx; +- int flags = O_RDONLY; + int ret; + + ctx = malloc(sizeof(struct mapped_ctx)); +@@ -102,7 +104,7 @@ static int mapped_openat(struct hexagonfs_fd *dir, + if (expect_dir) + flags |= O_DIRECTORY; + +- ctx->fd = openat(dir_ctx->fd, segment, flags); ++ ctx->fd = openat(dir_ctx->fd, segment, flags, mode); + if (ctx->fd == -1) { + ret = -errno; + goto err_free_fd; +@@ -138,6 +140,52 @@ static ssize_t mapped_read(struct hexagonfs_fd *fd, size_t size, void *out) + return ret; + } + ++static ssize_t mapped_write(struct hexagonfs_fd *fd, size_t size, ++ const void *in) ++{ ++ struct mapped_ctx *ctx = fd->data; ++ ssize_t ret; ++ ++ ret = write(ctx->fd, in, size); ++ if (ret < 0) ++ return -errno; ++ ++ return ret; ++} ++ ++static int mapped_fsync(struct hexagonfs_fd *fd) ++{ ++ struct mapped_ctx *ctx = fd->data; ++ ++ if (fsync(ctx->fd) < 0) ++ return -errno; ++ ++ return 0; ++} ++ ++static int mapped_unlinkat(struct hexagonfs_fd *dir, const char *name, ++ int flags) ++{ ++ struct mapped_ctx *ctx = dir->data; ++ ++ if (unlinkat(ctx->fd, name, flags) < 0) ++ return -errno; ++ ++ return 0; ++} ++ ++static int mapped_renameat(struct hexagonfs_fd *old_dir, const char *old_name, ++ struct hexagonfs_fd *new_dir, const char *new_name) ++{ ++ struct mapped_ctx *old_ctx = old_dir->data; ++ struct mapped_ctx *new_ctx = new_dir->data; ++ ++ if (renameat(old_ctx->fd, old_name, new_ctx->fd, new_name) < 0) ++ return -errno; ++ ++ return 0; ++} ++ + static int mapped_readdir(struct hexagonfs_fd *fd, size_t size, char *out) + { + struct mapped_ctx *ctx = fd->data; +@@ -236,10 +284,12 @@ static int mapped_or_empty_from_dirent(const void *dirent_data, bool dir, void * + static int mapped_or_empty_openat(struct hexagonfs_fd *dir, + const char *segment, + bool expect_dir, ++ int flags, ++ mode_t mode, + struct hexagonfs_fd **out) + { + if (dir->data) +- return mapped_openat(dir, segment, expect_dir, out); ++ return mapped_openat(dir, segment, expect_dir, flags, mode, out); + else + return -ENOENT; + } +@@ -327,6 +377,10 @@ struct hexagonfs_file_ops hexagonfs_mapped_ops = { + .from_dirent = mapped_from_dirent, + .openat = mapped_openat, + .read = mapped_read, ++ .write = mapped_write, ++ .fsync = mapped_fsync, ++ .unlinkat = mapped_unlinkat, ++ .renameat = mapped_renameat, + .readdir = mapped_readdir, + .seek = mapped_seek, + .stat = mapped_stat, +diff --git a/hexagonrpcd/hexagonfs_plat_subtype_name.c b/hexagonrpcd/hexagonfs_plat_subtype_name.c +index d20916f..a5707b1 100644 +--- a/hexagonrpcd/hexagonfs_plat_subtype_name.c ++++ b/hexagonrpcd/hexagonfs_plat_subtype_name.c +@@ -68,6 +68,8 @@ static int plat_subtype_name_from_dirent(const void *dirent_data, + static int plat_subtype_name_openat(struct hexagonfs_fd *dir, + const char *segment, + bool expect_dir, ++ int flags, ++ mode_t mode, + struct hexagonfs_fd **out) + { + return -ENOTDIR; +diff --git a/hexagonrpcd/hexagonfs_virt_dir.c b/hexagonrpcd/hexagonfs_virt_dir.c +index eaba520..956bffe 100644 +--- a/hexagonrpcd/hexagonfs_virt_dir.c ++++ b/hexagonrpcd/hexagonfs_virt_dir.c +@@ -63,6 +63,8 @@ static int virt_dir_from_dirent(const void *dirent_data, bool dir, void **fd_dat + static int virt_dir_openat(struct hexagonfs_fd *dir, + const char *segment, + bool expect_dir, ++ int flags, ++ mode_t mode, + struct hexagonfs_fd **out) + { + const struct hexagonfs_dirent *const **dirlist = dir->data; +diff --git a/hexagonrpcd/interfaces/apps_std.def b/hexagonrpcd/interfaces/apps_std.def +index 2fbbdb1..fde0bf7 100644 +--- a/hexagonrpcd/interfaces/apps_std.def ++++ b/hexagonrpcd/interfaces/apps_std.def +@@ -28,11 +28,15 @@ HEXAGONRPC_DEFINE_REMOTE_METHOD(1, apps_std_freopen, 1, 0, 1, 1) + HEXAGONRPC_DEFINE_REMOTE_METHOD(2, apps_std_fflush, 8, 0, 0, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(3, apps_std_fclose, 1, 0, 0, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(4, apps_std_fread, 1, 0, 2, 1) ++HEXAGONRPC_DEFINE_REMOTE_METHOD(5, apps_std_fwrite, 1, 1, 2, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(9, apps_std_fseek, 3, 0, 0, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(19, apps_std_fopen_with_env, 0, 4, 1, 0) ++HEXAGONRPC_DEFINE_REMOTE_METHOD(23, apps_std_fsync, 1, 0, 0, 0) ++HEXAGONRPC_DEFINE_REMOTE_METHOD(24, apps_std_fremove, 0, 1, 0, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(26, apps_std_opendir, 0, 1, 2, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(27, apps_std_closedir, 2, 0, 0, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(28, apps_std_readdir, 2, 0, 66, 0) + HEXAGONRPC_DEFINE_REMOTE_METHOD(31, apps_std_stat, 1, 1, 24, 0) ++HEXAGONRPC_DEFINE_REMOTE_METHOD(33, apps_std_frename, 1, 2, 0, 0) + + #endif /* INTERFACE_APPS_STD_DEF */ +diff --git a/hexagonrpcd/listener.c b/hexagonrpcd/listener.c +index e298688..6d7d846 100644 +--- a/hexagonrpcd/listener.c ++++ b/hexagonrpcd/listener.c +@@ -20,6 +20,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -142,7 +143,7 @@ static int return_for_next_invoke(int fd, + struct fastrpc_io_buffer **decoded) + { + struct fastrpc_decoder_context *ctx; +- char inbufs[256]; ++ char inbufs[65536]; + char *outbufs = NULL; + uint32_t inbufs_len; + uint32_t outbufs_len; +@@ -164,7 +165,7 @@ static int return_for_next_invoke(int fd, + *rctx, result, + outbufs_len, outbufs, + rctx, handle, sc, +- &inbufs_len, 256, inbufs); ++ &inbufs_len, sizeof(inbufs), inbufs); + if (ret) { + if (ret == -1) + perror("Could not fetch next FastRPC message"); +@@ -174,8 +175,9 @@ static int return_for_next_invoke(int fd, + goto err_free_outbufs; + } + +- if (inbufs_len > 256) { +- fprintf(stderr, "Large (>256B) input buffers aren't implemented\n"); ++ if (inbufs_len > sizeof(inbufs)) { ++ fprintf(stderr, "Large (>%zuB) input buffers aren't implemented\n", ++ sizeof(inbufs)); + ret = -1; + goto err_free_outbufs; + } +@@ -233,6 +235,18 @@ static int invoke_requested_procedure(size_t n_ifaces, + return 1; + } + ++ /* ++ * QAIC encodes method IDs above 31 as method 31 plus the full method ++ * ID in the first primitive input word. ++ */ ++ if (method == 31 && decoded != NULL && decoded[0].s >= sizeof(uint32_t)) { ++ uint32_t extended_method = ++ le32toh(*(const uint32_t *)decoded[0].p); ++ ++ if (extended_method >= 31) ++ method = extended_method; ++ } ++ + if (method >= ifaces[handle]->n_procs) { + fprintf(stderr, "Unsupported method: %u (%08x)\n", method, sc); + *result = AEE_EUNSUPPORTED; +diff --git a/hexagonrpcd/rpcd_builder.c b/hexagonrpcd/rpcd_builder.c +index d374958..c0cd4bb 100644 +--- a/hexagonrpcd/rpcd_builder.c ++++ b/hexagonrpcd/rpcd_builder.c +@@ -29,7 +29,7 @@ + #define ACDBDATA "/acdb/" + #define DSP_LIBS "/dsp/" + #define SENSORS_CONFIG "/sensors/config/" +-#define SENSORS_REGISTRY "/sensors/registry/" ++#define SENSORS_REGISTRY "/sensors/persist/" + #define SNS_REG_CONFIG "/sensors/sns_reg.conf" + #define SYSFS_SOCINFO "/socinfo/" + +@@ -162,9 +162,7 @@ struct hexagonfs_dirent *construct_root_dir(const char *prefix, const char *dsp) + */ + persist_dir = hfs_mkdir("persist", 1, + hfs_mkdir("sensors", 1, +- hfs_mkdir("registry", 1, +- hfs_map("registry", sns_reg) +- ) ++ hfs_map("registry", sns_reg) + ) + ); + +diff --git a/tests/test_hexagonfs.c b/tests/test_hexagonfs.c +index ae9ee7a..9f2bd20 100644 +--- a/tests/test_hexagonfs.c ++++ b/tests/test_hexagonfs.c +@@ -22,7 +22,10 @@ + #include + #include + #include ++#include ++#include + #include ++#include + #include + + #include "../hexagonrpcd/hexagonfs.h" +@@ -83,6 +86,80 @@ static int test_mapped_seq_read(const char *path) + return 0; + } + ++static int test_mapped_write_remove(void) ++{ ++ struct hexagonfs_fd *fds[HEXAGONFS_MAX_FD] = { 0 }; ++ char dir_template[] = "/tmp/hexagonrpc-test-XXXXXX"; ++ char file_path[sizeof(dir_template) + sizeof("/registry")]; ++ char renamed_path[sizeof(dir_template) + sizeof("/registry-renamed")]; ++ struct hexagonfs_dirent root = { ++ .name = "/", ++ .ops = &hexagonfs_mapped_ops, ++ }; ++ const char contents[] = "persistent registry"; ++ char readback[sizeof(contents)] = { 0 }; ++ char *dir; ++ int rootfd; ++ int fd; ++ int ret = 1; ++ ++ dir = mkdtemp(dir_template); ++ if (dir == NULL) ++ return 1; ++ snprintf(file_path, sizeof(file_path), "%s/registry", dir); ++ snprintf(renamed_path, sizeof(renamed_path), "%s/registry-renamed", dir); ++ ++ root.u.phys = dir; ++ rootfd = hexagonfs_open_root(fds, &root); ++ if (rootfd < 0) ++ goto out_dir; ++ ++ fd = hexagonfs_openat_flags(fds, rootfd, rootfd, "registry", ++ O_WRONLY | O_CREAT | O_TRUNC, 0600); ++ if (fd < 0) ++ goto out_root; ++ ++ if (hexagonfs_write(fds, fd, sizeof(contents), contents) != sizeof(contents)) ++ goto out_file; ++ if (hexagonfs_fsync(fds, fd)) ++ goto out_file; ++ if (hexagonfs_close(fds, fd)) ++ goto out_root; ++ ++ fd = hexagonfs_openat(fds, rootfd, rootfd, "registry"); ++ if (fd < 0) ++ goto out_root; ++ if (hexagonfs_read(fds, fd, sizeof(readback), readback) != sizeof(readback)) ++ goto out_file; ++ if (memcmp(contents, readback, sizeof(contents))) ++ goto out_file; ++ if (hexagonfs_close(fds, fd)) ++ goto out_root; ++ ++ if (hexagonfs_renameat(fds, rootfd, rootfd, "/registry", ++ rootfd, "/registry-renamed")) ++ goto out_root; ++ if (access(file_path, F_OK) == 0 || access(renamed_path, F_OK) != 0) ++ goto out_root; ++ if (hexagonfs_unlinkat(fds, rootfd, rootfd, "/registry-renamed", 0)) ++ goto out_root; ++ if (access(renamed_path, F_OK) == 0) ++ goto out_root; ++ ++ ret = 0; ++ goto out_root; ++ ++out_file: ++ hexagonfs_close(fds, fd); ++out_root: ++ hexagonfs_close(fds, rootfd); ++out_dir: ++ unlink(file_path); ++ unlink(renamed_path); ++ rmdir(dir_template); ++ return ret; ++} ++ + int main(int argc, const char **argv) + { + int ret; +@@ -94,5 +171,9 @@ int main(int argc, const char **argv) + if (ret) + return ret; + ++ ret = test_mapped_write_remove(); ++ if (ret) ++ return ret; ++ + return 0; + } diff --git a/pkgbuilds/surface-pro-11-sensors/0001-light-use-surface-color-sensor.patch b/pkgbuilds/surface-pro-11-sensors/0001-light-use-surface-color-sensor.patch new file mode 100644 index 000000000..066d507d3 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/0001-light-use-surface-color-sensor.patch @@ -0,0 +1,23 @@ +From: Brandon Williams +Subject: [PATCH] light: use the Surface color sensor data type + +Surface Pro 11 firmware exposes its usable ambient-light measurement through +Microsoft's composite "color" sensor. The standard "ambient_light" endpoint +is present but reports zero. Both reports use the standard repeated-float +payload, with illuminance in element zero. + +Signed-off-by: Brandon Williams +--- +diff --git a/src/libssc-sensor-light.c b/src/libssc-sensor-light.c +index e0bd99a..f399ff7 100644 +--- a/src/libssc-sensor-light.c ++++ b/src/libssc-sensor-light.c +@@ -274,7 +274,7 @@ ssc_sensor_light_new (GCancellable *cancellable, GAsyncReadyCallback callback, g + cancellable, + callback, + user_data, +- SSC_SENSOR_DATA_TYPE, "ambient_light", ++ SSC_SENSOR_DATA_TYPE, "color", + NULL); + } + diff --git a/pkgbuilds/surface-pro-11-sensors/10-surface-pro-11-sensors.conf b/pkgbuilds/surface-pro-11-sensors/10-surface-pro-11-sensors.conf new file mode 100644 index 000000000..2738f264a --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/10-surface-pro-11-sensors.conf @@ -0,0 +1,11 @@ +# Read the Surface Pro 11 light sensor through the patched libssc, after the +# sensor service when both start together. Ordering only: iio-sensor-proxy +# never waits for a sensor service that cannot start. +[Unit] +After=surface-pro-11-sensors.service + +[Service] +Environment=LD_LIBRARY_PATH=/usr/lib/surface-pro-11-sensors +Restart=always +RestartSec=2 +TimeoutStopSec=10s diff --git a/pkgbuilds/surface-pro-11-sensors/PKGBUILD b/pkgbuilds/surface-pro-11-sensors/PKGBUILD new file mode 100644 index 000000000..fff58da60 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/PKGBUILD @@ -0,0 +1,84 @@ +# Maintainer: Brandon Williams + +# Ambient light sensor support for the Surface Pro 11 (Snapdragon X Elite). + +pkgname=surface-pro-11-sensors +pkgver=1 +pkgrel=1 +pkgdesc="Ambient light sensor service for the Microsoft Surface Pro 11 (Snapdragon X Elite)" +arch=('aarch64') +# Qualcomm-only: the pacman platform guard keeps it off other hardware. +groups=('omarchy-platform-qualcomm') +url="https://github.com/omacom/omarchy-pkgs" +license=('GPL-3.0-or-later' 'MIT') +depends=('glib2' 'iio-sensor-proxy' 'libqmi' 'libqrtr-glib' 'protobuf-c' 'util-linux') +# libssc builds GObject introspection whenever glib's .pc files are present. +makedepends=('git' 'gobject-introspection' 'meson' 'ninja') +options=('!debug') +_hexagonrpc_commit=dd9ac70c026e1bad93e8cffa3801255b8ceb551e +# Includes 54dd13e, which stops iio-sensor-proxy spinning when the sensor +# server never appears. +_libssc_commit=54dd13e36d65c21263cf5b0f4a70c230a77b26bc +source=( + "hexagonrpc::git+https://github.com/linux-msm/hexagonrpc.git#commit=${_hexagonrpc_commit}" + "libssc::git+https://codeberg.org/DylanVanAssche/libssc.git#commit=${_libssc_commit}" + '0001-apps-std-support-writable-sensor-registry.patch' + '0001-light-use-surface-color-sensor.patch' + 'surface-pro-11-sensors.service' + '10-surface-pro-11-sensors.conf' + 'surface-pro-11-sensors-sleep' + 'surface-pro-11-sensors-extract' + 'README.package.md' +) +sha256sums=('b612196e6a0069467cdccf0b663e620bfaab27c7d1659c240ae22c9614301cd8' + 'b044b855e29fd76deb22f922356a7924e4c4bcf4fc1b8f0b242cd13925c6897c' + '6c61e5978d443a151928c7a1519b14d56f39bd216915008a0b026eb6ef35baf2' + 'c657bda4f4d45c83c0db948d1f0c25931421e644c1629a0ba26e7a97f7169611' + '090b8cf979b1ca567c92edff1dfbc6214be7c4f496ec4cc0a0f3b7f88d89a211' + 'b4f6a0fb2b11e6a2999c355d11d68ee5e8807976d6ea0cbd68d7d56fe7688a61' + '23d60ba158a4a0e8f411fdd536871bc1232be8167d0ccc7f9bf670c6141b120b' + '7011002cccdc3e4bd341b79b7807db60ed64cea4096fa8516a7e69db019c3842' + 'fb06525c85680bc998ff1e44fd4b02a52702ccd5e53da48991dcfb2312a58eb5') + +_libdir=/usr/lib/surface-pro-11-sensors + +prepare() { + # Let the daemon write the sensor registry the DSP generates. + git -C hexagonrpc apply ../0001-apps-std-support-writable-sensor-registry.patch + # Read ambient light from the Surface's "color" sensor; "ambient_light" reports zero. + git -C libssc apply ../0001-light-use-surface-color-sensor.patch +} + +build() { + local project + for project in hexagonrpc libssc; do + arch-meson "$project" "build-$project" -D b_lto=false + meson compile -C "build-$project" + done +} + +check() { + meson test -C build-hexagonrpc --print-errorlogs +} + +package() { + local project + for project in hexagonrpc libssc; do + meson install -C "build-$project" --destdir "$srcdir/stage-$project" --no-rebuild + done + + # Private copies, loaded only by this package's service and by + # iio-sensor-proxy through its drop-in; other programs keep Arch's libssc. + install -Dm755 "$srcdir/stage-hexagonrpc/usr/bin/hexagonrpcd" "$pkgdir$_libdir/hexagonrpcd" + cp -P "$srcdir"/stage-hexagonrpc/usr/lib/libhexagonrpc.so* "$pkgdir$_libdir/" + cp -P "$srcdir"/stage-libssc/usr/lib/libssc.so* "$pkgdir$_libdir/" + + install -Dm644 surface-pro-11-sensors.service "$pkgdir/usr/lib/systemd/system/surface-pro-11-sensors.service" + install -Dm644 10-surface-pro-11-sensors.conf \ + "$pkgdir/usr/lib/systemd/system/iio-sensor-proxy.service.d/10-surface-pro-11-sensors.conf" + install -Dm755 surface-pro-11-sensors-sleep "$pkgdir/usr/lib/systemd/system-sleep/surface-pro-11-sensors" + install -Dm755 surface-pro-11-sensors-extract "$pkgdir/usr/bin/surface-pro-11-sensors-extract" + install -Dm644 README.package.md "$pkgdir/usr/share/doc/$pkgname/README.package.md" + install -Dm644 hexagonrpc/COPYING "$pkgdir/usr/share/licenses/$pkgname/COPYING.hexagonrpc" + install -Dm644 libssc/LICENSE "$pkgdir/usr/share/licenses/$pkgname/COPYING.libssc" +} diff --git a/pkgbuilds/surface-pro-11-sensors/README.package.md b/pkgbuilds/surface-pro-11-sensors/README.package.md new file mode 100644 index 000000000..3e0302e7a --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/README.package.md @@ -0,0 +1,29 @@ +# surface-pro-11-sensors + +Ambient light for the Microsoft Surface Pro 11 with Snapdragon X Elite. The AMS TCS3430 colour sensor is run by the Qualcomm sensor core on the ADSP, which needs its configuration served from the host before it reports anything: + +``` +TCS3430 -> sensor core on the ADSP <- hexagonrpcd (serves config and registry) + -> QRTR/QMI -> libssc -> iio-sensor-proxy -> desktop auto-brightness +``` + +## Contents + +- `hexagonrpcd` and `libhexagonrpc` from `linux-msm/hexagonrpc`, pinned, with a patch that lets the daemon write the registry the DSP generates. +- `libssc` from `DylanVanAssche/libssc`, pinned, with a patch that reads illuminance from Microsoft's `color` sensor, because the standard `ambient_light` sensor reports zero on this model. It is installed privately under `/usr/lib/surface-pro-11-sensors/` and used only by iio-sensor-proxy, through a drop-in. +- `surface-pro-11-sensors.service`, which runs hexagonrpcd sandboxed against `/var/lib/surface-pro-11-sensors/root`. It starts when the ADSP's FastRPC device appears, so a missing or failed DSP never holds up boot, and it runs only on the `microsoft,denali-oled` device tree. +- A sleep hook that stops iio-sensor-proxy across suspend: an open light stream otherwise wakes the SoC shortly into every deep suspend. +- `surface-pro-11-sensors-extract`. + +## Sensor configuration + +The configuration is Microsoft and Qualcomm's and is not redistributable, so the package contains none. `surface-pro-11-sensors-extract` copies it from the machine's own Windows installation (`DriverStore/FileRepository/surfacepro_snscfgcrd8380.inf_arm64_*`, read-only): the sensor JSON files and `json.lst`, `sns_reg_config` without carriage returns, and the platform identifiers. The SoC ID and revision come from the running system, because the Windows values do not match the ADSP. Omarchy runs it during installation. Afterwards, `sudo surface-pro-11-sensors-extract` rescans, or `-d DIR` takes a mounted driver store. BitLocker volumes cannot be read. + +The DSP generates its registry on first use. Validate after a full reboot, not by restarting the ADSP: `busctl get-property net.hadess.SensorProxy /net/hadess/SensorProxy net.hadess.SensorProxy LightLevel` should change sharply between a covered sensor and a bright light. + +## Retirement + +Drop once hexagonrpc is packaged with registry write support and libssc falls back to the `color` sensor. + +- hexagonrpc: registry writes are tracked in linux-msm/hexagonrpc issue 19 (pull request 21 proposes a writable copy under `/var`). Upstream `main` has since rewritten the method definitions, so this patch needs a rebase before it can be offered there. +- libssc: the `color` fallback has not been submitted yet. diff --git a/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-extract b/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-extract new file mode 100755 index 000000000..b8fe06260 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-extract @@ -0,0 +1,168 @@ +#!/bin/bash +# Build the Surface Pro 11 sensor configuration root from the machine's own +# Windows driver store. The files are Microsoft/Qualcomm configuration: they +# are copied locally and never downloaded or redistributed. + +set -euo pipefail + +ROOT=${SP11_SENSORS_ROOT:-/var/lib/surface-pro-11-sensors/root} +SOC=${SP11_SENSORS_SOC:-/sys/devices/soc0} +COMPATIBLE=${SP11_SENSORS_COMPATIBLE:-/proc/device-tree/compatible} +CONFIG_GLOB="surfacepro_snscfgcrd8380.inf_arm64_*" + +# The combination qualified on hardware. Newer Windows driver releases are +# accepted with a warning. +declare -A KNOWN=( + [json.lst]=386957a9a6d613c081782969e07ead4051accc2bf0eed52ac4d42f0b60bc3e90 + [8380_crd_tcs3430_0.json]=68c98cfd3a5533b828ac67bc067933a0c3fd449690c5a8341c2e5f21413a4f15 + [sns_surface_color.json]=ee0f236b0a001ba73a7029ca7b08186131f1a7f6ef74855ab84557af964cbc3c +) +REQUIRED=(json.lst 8380_crd_tcs3430_0.json sns_surface_color.json sns_reg_config + hw_platform platform_subtype platform_subtype_id platform_version) + +log() { printf 'surface-pro-11-sensors-extract: %s\n' "$*"; } +warn() { printf 'surface-pro-11-sensors-extract: %s\n' "$*" >&2; } + +usage() { + cat <&2; exit 2 ;; + esac +done + +# The qualified configuration is for the X Elite OLED model; the LCD and X +# Plus models share its DMI name but not its sensors. +if ! { [[ -r $COMPATIBLE ]] && tr '\0' '\n' <"$COMPATIBLE" | grep -qx 'microsoft,denali-oled'; }; then + log "not a Surface Pro 11 OLED; nothing to do" + exit 0 +fi + +if (( EUID != 0 )) && [[ -z ${SP11_SENSORS_ROOT:-} ]]; then + warn "must run as root to read Windows partitions and write $ROOT" + exit 1 +fi + +mounts=() +cleanup() { + local mount + for mount in "${mounts[@]}"; do + umount "$mount" 2>/dev/null && rmdir "$mount" + done + [[ -z ${stage:-} ]] || rm -rf "$stage" +} +trap cleanup EXIT + +# Pick the configuration directory under a driver store or a given path. +find_config() { + local base=$1 candidate + if [[ -f $base/json.lst ]]; then + printf '%s\n' "$base" + return 0 + fi + local -a found=() + for candidate in "$base"/$CONFIG_GLOB "$base"/Windows/System32/DriverStore/FileRepository/$CONFIG_GLOB; do + [[ -f $candidate/json.lst ]] && found+=("$candidate") + done + ((${#found[@]})) || return 1 + # Several driver versions can coexist; the most recently installed wins. + ls -1dt "${found[@]}" | head -n1 +} + +config="" +if [[ -n $source_dir ]]; then + config=$(find_config "$source_dir") || { warn "no sensor configuration under $source_dir"; exit 1; } +else + while read -r device fstype mountpoint; do + [[ $fstype == ntfs ]] || continue + # A partition that is already mounted cannot be mounted again read-only. + mountpoint=$(printf '%b' "$mountpoint") + if [[ -n $mountpoint ]]; then + if config=$(find_config "$mountpoint"); then + log "Windows driver store found on $device" + break + fi + continue + fi + mount=$(mktemp -d /run/surface-pro-11-sensors.XXXXXX) + if mount -o ro,noexec,nosuid,nodev -t ntfs3 "$device" "$mount" 2>/dev/null; then + mounts+=("$mount") + if config=$(find_config "$mount"); then + log "Windows driver store found on $device" + break + fi + else + rmdir "$mount" + fi + done < <(lsblk -rno PATH,FSTYPE,MOUNTPOINT) + if [[ -z $config ]]; then + warn "no Windows installation with the Surface sensor configuration found" + warn "(BitLocker volumes cannot be read; see -d)" + exit 1 + fi +fi + +for file in "${REQUIRED[@]}"; do + [[ -s $config/$file ]] || { warn "missing $file in $config"; exit 1; } +done +for file in "${!KNOWN[@]}"; do + if [[ $(sha256sum "$config/$file" | cut -d' ' -f1) != "${KNOWN[$file]}" ]]; then + warn "$file differs from the qualified version; continuing with the Windows copy" + fi +done +for file in soc_id revision; do + [[ -s $SOC/$file ]] || { warn "missing $SOC/$file"; exit 1; } +done + +mkdir -p "$(dirname "$ROOT")" +stage=$(mktemp -d "$(dirname "$ROOT")/.root.XXXXXX") +install -d -m0755 "$stage/sensors/config" "$stage/sensors/persist/registry" "$stage/socinfo" +find "$config" -maxdepth 1 -type f \( -name '*.json' -o -name 'json.lst' -o -name '*.bin' -o -name '*.pb' \) \ + -exec install -m0644 -t "$stage/sensors/config" {} + +tr -d '\r' <"$config/sns_reg_config" >"$stage/sensors/sns_reg.conf" + +# The ADSP accepts the CRD sensor profile only with the running SoC's ID and +# revision; the platform fields come from the Windows configuration. Each +# value is written as one line, like its sysfs counterpart. +for file in hw_platform platform_subtype platform_subtype_id platform_version; do + printf '%s\n' "$(tr -d '\r\n' <"$config/$file")" >"$stage/socinfo/$file" +done +for file in soc_id revision; do + printf '%s\n' "$(tr -d '\n' <"$SOC/$file")" >"$stage/socinfo/$file" +done + +# Keep the registry the DSP generated on earlier boots, but never an empty +# calibration record: it crashes the sensor process. +if [[ -d $ROOT/sensors/persist ]]; then + cp -a "$ROOT/sensors/persist/." "$stage/sensors/persist/" +fi +find "$stage/sensors/persist" -name 'color_calibration.bin' -size 0 -delete + +{ + printf 'source %s\n' "$config" + (cd "$stage" && find sensors/config sensors/sns_reg.conf socinfo -type f -print0 | sort -z | xargs -0 sha256sum) +} >"$stage/manifest" +find "$stage" -type d -exec chmod 0755 {} + +find "$stage" -type f -exec chmod 0644 {} + + +rm -rf "$ROOT.old" +if [[ -e $ROOT ]]; then + mv "$ROOT" "$ROOT.old" +fi +mv "$stage" "$ROOT" +stage="" +rm -rf "$ROOT.old" + +log "installed the sensor configuration from $config into $ROOT" +log "restart surface-pro-11-sensors.service or reboot to use it" diff --git a/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-sleep b/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-sleep new file mode 100755 index 000000000..6c145d0d1 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors-sleep @@ -0,0 +1,28 @@ +#!/bin/bash + +# Stop iio-sensor-proxy across suspend and restart it afterwards. Once a client +# claims the light sensor, the ADSP stream stays open, and its indications wake +# the SoC shortly into every deep suspend. The stream only ends when the +# daemon's QMI client exits; clients reconnect after resume. + +compatible=${SP11_SENSORS_COMPATIBLE:-/proc/device-tree/compatible} +unit=iio-sensor-proxy.service +flag=${SP11_SENSORS_SLEEP_FLAG:-/run/surface-pro-11-sensors-sleep.stopped} + +[[ -r $compatible ]] && tr '\0' '\n' <"$compatible" | grep -qx 'microsoft,denali-oled' || exit 0 + +case ${1:-} in + pre) + rm -f "$flag" + if systemctl is-active --quiet "$unit"; then + systemctl stop "$unit" && touch "$flag" + fi + ;; + post) + if [[ -e $flag ]]; then + rm -f "$flag" + systemctl start "$unit" + fi + ;; +esac +exit 0 diff --git a/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors.service b/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors.service new file mode 100644 index 000000000..44a8db7b5 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/surface-pro-11-sensors.service @@ -0,0 +1,36 @@ +[Unit] +Description=Surface Pro 11 Qualcomm sensor service +Documentation=file:///usr/share/doc/surface-pro-11-sensors/README.package.md +ConditionFirmware=device-tree-compatible(microsoft,denali-oled) +BindsTo=dev-fastrpc\x2dadsp.device +After=systemd-modules-load.service dev-fastrpc\x2dadsp.device +Before=iio-sensor-proxy.service +ConditionPathExists=/dev/fastrpc-adsp +ConditionPathExists=/var/lib/surface-pro-11-sensors/root/sensors/sns_reg.conf +ConditionPathExists=/var/lib/surface-pro-11-sensors/root/sensors/config/json.lst + +[Service] +Type=simple +Environment=LD_LIBRARY_PATH=/usr/lib/surface-pro-11-sensors +ExecStart=/usr/lib/surface-pro-11-sensors/hexagonrpcd -f /dev/fastrpc-adsp -d adsp -s -R /var/lib/surface-pro-11-sensors/root +Restart=on-failure +RestartSec=2 +UMask=0077 +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=false +ProtectControlGroups=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectClock=true +RestrictAddressFamilies=AF_UNIX +RestrictRealtime=true +MemoryDenyWriteExecute=true +ReadOnlyPaths=/var/lib/surface-pro-11-sensors/root +ReadWritePaths=/var/lib/surface-pro-11-sensors/root/sensors/persist + +[Install] +# Start when the ADSP appears, so a missing DSP never holds up boot. +WantedBy=dev-fastrpc\x2dadsp.device diff --git a/pkgbuilds/surface-pro-11-sensors/test.sh b/pkgbuilds/surface-pro-11-sensors/test.sh new file mode 100755 index 000000000..78a8b29e5 --- /dev/null +++ b/pkgbuilds/surface-pro-11-sensors/test.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +set -euo pipefail + +package_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +extractor="$package_dir/surface-pro-11-sensors-extract" +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT + +fail() { + echo "not ok - $1" >&2 + exit 1 +} + +store="$scratch/FileRepository" +config="$store/surfacepro_snscfgcrd8380.inf_arm64_f5d7e051fecc22c3" +mkdir -p "$config" "$scratch/soc0" +for file in json.lst 8380_crd_tcs3430_0.json sns_surface_color.json other_sensor.json golden_color_calibration.bin ov08x_2.pb; do + printf '%s\n' "$file" >"$config/$file" +done +printf 'config line\r\n' >"$config/sns_reg_config" +printf 'CRD\r\n' >"$config/hw_platform" +printf 'Unknown\r\n' >"$config/platform_subtype" +printf '0\r\n' >"$config/platform_subtype_id" +printf '65536\r\n' >"$config/platform_version" +printf '615\r\n' >"$config/soc_id" +printf '3.1\r\n' >"$config/revision" +printf 'x\n' >"$config/surfacepro_SnsCfgCRD8380.inf" +printf '555\n' >"$scratch/soc0/soc_id" +printf '2.1\n' >"$scratch/soc0/revision" +printf 'microsoft,denali-oled\0microsoft,denali\0qcom,x1e80100\0' >"$scratch/compatible" + +run() { + SP11_SENSORS_ROOT="$scratch/root" SP11_SENSORS_SOC="$scratch/soc0" \ + SP11_SENSORS_COMPATIBLE="$scratch/compatible" bash "$extractor" "$@" +} + +run -d "$store" >/dev/null 2>&1 || fail "the extractor accepts a driver store" +root="$scratch/root" +[[ -f $root/sensors/config/json.lst && -f $root/sensors/config/other_sensor.json && -f $root/sensors/config/ov08x_2.pb ]] || + fail "every sensor configuration file is copied" +[[ ! -e $root/sensors/config/surfacepro_SnsCfgCRD8380.inf && ! -e $root/sensors/config/sns_reg_config ]] || + fail "driver metadata stays out of the configuration directory" +[[ $(<"$root/sensors/sns_reg.conf") == "config line" ]] || + fail "sns_reg.conf is written without carriage returns" +[[ $(<"$root/socinfo/hw_platform") == CRD && $(<"$root/socinfo/platform_version") == 65536 ]] || + fail "platform identifiers come from the Windows configuration" +[[ $(<"$root/socinfo/soc_id") == 555 && $(<"$root/socinfo/revision") == 2.1 ]] || + fail "the SoC ID and revision come from the running system, not Windows" +[[ $(od -An -c "$root/socinfo/soc_id" | tr -d ' ') == '555\n' ]] || + fail "identifiers are written as single lines, like sysfs" +[[ -d $root/sensors/persist/registry && -f $root/manifest ]] || + fail "an empty registry and a manifest are created" + +printf 'generated\n' >"$root/sensors/persist/registry/sns_record" +: >"$root/sensors/persist/registry/color_calibration.bin" +run -d "$config" >/dev/null 2>&1 || fail "the extractor accepts the configuration directory itself" +[[ -f $root/sensors/persist/registry/sns_record ]] || + fail "the registry the DSP generated survives a rerun" +[[ ! -e $root/sensors/persist/registry/color_calibration.bin ]] || + fail "an empty calibration record, which crashes the sensor process, is removed" + +rm "$config/sns_surface_color.json" +if run -d "$store" >/dev/null 2>&1; then + fail "incomplete configuration is rejected" +fi +[[ -f $root/sensors/config/sns_surface_color.json ]] || + fail "a rejected run leaves the installed configuration untouched" + +printf '%s\n' sns_surface_color.json >"$config/sns_surface_color.json" +mkdir -p "$root.old/stale" +run -d "$store" >/dev/null 2>&1 || fail "a leftover backup from an interrupted run does not block extraction" +[[ ! -e $root.old && ! -e $root/root.old && -f $root/sensors/config/json.lst ]] || + fail "a leftover backup is cleared, not nested into the new configuration" + +if (( EUID != 0 )); then + if SP11_SENSORS_SOC="$scratch/soc0" SP11_SENSORS_COMPATIBLE="$scratch/compatible" \ + bash "$extractor" -d "$store" >/dev/null 2>&1; then + fail "an unprivileged run on the real configuration root stops with an error" + fi +fi + +printf 'microsoft,denali\0qcom,x1p64100\0' >"$scratch/compatible" +rm -rf "$root" +run -d "$store" >/dev/null 2>&1 || fail "other machines exit cleanly" +[[ ! -e $root ]] || fail "other machines get no sensor configuration" + +echo "ok - surface-pro-11-sensors-extract builds the sensor root from a Windows driver store"