Created
June 16, 2026 07:37
-
-
Save nohajc/2e4641d561e94ea87221983f3506c2c3 to your computer and use it in GitHub Desktop.
Build BlueZ deb package for Raspberry Pi, patched to handle a Sony TV remote clone that has botched GATT descriptors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| usage() { | |
| cat <<'EOF' | |
| Usage: build-bluez-sony-hogp-deb.sh [options] | |
| Build a Debian/Raspberry Pi OS BlueZ package with a local HOGP/GATT | |
| interoperability patch for devices that return malformed characteristic | |
| discovery data, such as SONY TV VRC 001. | |
| Options: | |
| --workdir DIR Build directory. Default: ~/src/bluez-sony-hogp-build-<timestamp> | |
| --source-version VER BlueZ source version to fetch. Default: installed/candidate bluez version, | |
| with any +sonyhogp suffix stripped. | |
| --local-suffix SUFFIX Local version suffix. Default: +sonyhogp3 | |
| --no-build-deps Do not run apt-get build-dep. | |
| --help Show this help. | |
| Outputs: | |
| Built .deb packages are written to the parent of the unpacked source tree, | |
| normally the selected workdir. | |
| EOF | |
| } | |
| WORKDIR="${WORKDIR:-}" | |
| SOURCE_VERSION="${BLUEZ_SOURCE_VERSION:-}" | |
| LOCAL_SUFFIX="${BLUEZ_LOCAL_SUFFIX:-+sonyhogp3}" | |
| INSTALL_BUILD_DEPS=1 | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| --workdir) | |
| WORKDIR="$2" | |
| shift 2 | |
| ;; | |
| --source-version) | |
| SOURCE_VERSION="$2" | |
| shift 2 | |
| ;; | |
| --local-suffix) | |
| LOCAL_SUFFIX="$2" | |
| shift 2 | |
| ;; | |
| --no-build-deps) | |
| INSTALL_BUILD_DEPS=0 | |
| shift | |
| ;; | |
| --help|-h) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown option: $1" >&2 | |
| usage >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| done | |
| if [ -z "$WORKDIR" ]; then | |
| WORKDIR="$HOME/src/bluez-sony-hogp-build-$(date +%Y%m%d-%H%M%S)" | |
| fi | |
| if [ "$(id -u)" -eq 0 ]; then | |
| SUDO=() | |
| else | |
| SUDO=(sudo) | |
| fi | |
| run_root() { | |
| "${SUDO[@]}" "$@" | |
| } | |
| require_cmd() { | |
| if ! command -v "$1" >/dev/null 2>&1; then | |
| echo "Missing required command: $1" >&2 | |
| exit 1 | |
| fi | |
| } | |
| enable_deb_src() { | |
| local stamp file tmp | |
| stamp="$(date +%Y%m%d-%H%M%S)" | |
| for file in /etc/apt/sources.list.d/*.sources; do | |
| [ -e "$file" ] || continue | |
| if awk ' | |
| /^Types:/ && | |
| $0 ~ /(^|[[:space:]])deb([[:space:]]|$)/ && | |
| $0 !~ /(^|[[:space:]])deb-src([[:space:]]|$)/ { | |
| found = 1 | |
| } | |
| END { exit found ? 0 : 1 } | |
| ' "$file"; then | |
| echo "Enabling deb-src in $file" | |
| run_root cp -a "$file" "$file.codex-bluez-src.bak-$stamp" | |
| tmp="$(mktemp)" | |
| awk ' | |
| /^Types:/ { | |
| line = $0 | |
| if (line ~ /(^|[[:space:]])deb([[:space:]]|$)/ && | |
| line !~ /(^|[[:space:]])deb-src([[:space:]]|$)/) | |
| line = line " deb-src" | |
| print line | |
| next | |
| } | |
| { print } | |
| ' "$file" >"$tmp" | |
| run_root install -m 0644 "$tmp" "$file" | |
| rm -f "$tmp" | |
| fi | |
| done | |
| if [ -f /etc/apt/sources.list ]; then | |
| if grep -qE '^[#[:space:]]*deb-src[[:space:]]' /etc/apt/sources.list && | |
| ! grep -qE '^[[:space:]]*deb-src[[:space:]]' /etc/apt/sources.list; then | |
| echo "Uncommenting deb-src entries in /etc/apt/sources.list" | |
| run_root cp -a /etc/apt/sources.list "/etc/apt/sources.list.codex-bluez-src.bak-$stamp" | |
| run_root sed -i -E 's/^[#[:space:]]*(deb-src[[:space:]])/\1/' /etc/apt/sources.list | |
| fi | |
| fi | |
| } | |
| detect_source_version() { | |
| local version candidate | |
| if [ -n "$SOURCE_VERSION" ]; then | |
| printf '%s\n' "$SOURCE_VERSION" | |
| return | |
| fi | |
| version="$(dpkg-query -W -f='${Version}' bluez 2>/dev/null || true)" | |
| if [ -z "$version" ]; then | |
| candidate="$(apt-cache policy bluez | awk '/Candidate:/ { print $2; exit }')" | |
| version="$candidate" | |
| fi | |
| if [ -z "$version" ] || [ "$version" = "(none)" ]; then | |
| echo "Could not determine a BlueZ source version. Use --source-version." >&2 | |
| exit 1 | |
| fi | |
| version="${version%%+sonyhogp*}" | |
| printf '%s\n' "$version" | |
| } | |
| install_packaging_deps() { | |
| run_root apt-get update | |
| run_root apt-get install -y \ | |
| build-essential \ | |
| ca-certificates \ | |
| devscripts \ | |
| dpkg-dev \ | |
| quilt \ | |
| python3 | |
| } | |
| fetch_source() { | |
| local base_version | |
| base_version="$1" | |
| mkdir -p "$WORKDIR" | |
| cd "$WORKDIR" | |
| echo "Fetching BlueZ source package: $base_version" | |
| if ! apt-get source "bluez=$base_version"; then | |
| echo "Exact source version fetch failed; trying apt-get source bluez" >&2 | |
| apt-get source bluez | |
| fi | |
| } | |
| find_source_dir() { | |
| find "$WORKDIR" -maxdepth 1 -type d -name 'bluez-*' | sort -V | tail -n 1 | |
| } | |
| apply_sony_hogp_patch() { | |
| local patch_name source_dir | |
| source_dir="$1" | |
| patch_name="sony-hogp-tolerate-malformed-characteristics.patch" | |
| cd "$source_dir" | |
| export QUILT_PATCHES=debian/patches | |
| if quilt series 2>/dev/null | grep -qx "$patch_name"; then | |
| echo "Patch $patch_name is already present in this source tree." >&2 | |
| exit 1 | |
| fi | |
| quilt push -a || true | |
| quilt new "$patch_name" | |
| quilt add src/shared/gatt-client.c src/shared/gatt-helpers.c | |
| python3 <<'PY' | |
| from pathlib import Path | |
| def replace_once(text, old, new, label): | |
| if old not in text: | |
| raise SystemExit(f"Could not find patch location: {label}") | |
| return text.replace(old, new, 1) | |
| client = Path("src/shared/gatt-client.c") | |
| s = client.read_text() | |
| old = '''\twhile ((chrc_data = queue_pop_head(op->pending_chrcs))) { | |
| \t\tstruct gatt_db_attribute *svc; | |
| \t\tuint16_t start, end; | |
| \t\t/* Adjust current service */ | |
| \t\tsvc = gatt_db_get_service(client->db, chrc_data->value_handle); | |
| ''' | |
| new = '''\twhile ((chrc_data = queue_pop_head(op->pending_chrcs))) { | |
| \t\tstruct gatt_db_attribute *svc; | |
| \t\tuint16_t start, end; | |
| \t\tsvc = gatt_db_get_service(client->db, chrc_data->start_handle); | |
| \t\tif (!svc) { | |
| \t\t\tDBG(client, "No service for characteristic declaration " | |
| \t\t\t\t\t\t"at 0x%04x; skipping", | |
| \t\t\t\t\t\tchrc_data->start_handle); | |
| \t\t\tfree(chrc_data); | |
| \t\t\tcontinue; | |
| \t\t} | |
| \t\tgatt_db_attribute_get_service_handles(svc, &start, &end); | |
| \t\tif (chrc_data->value_handle <= chrc_data->start_handle || | |
| \t\t\t\tchrc_data->value_handle > end) { | |
| \t\t\tDBG(client, "Invalid characteristic declaration 0x%04x " | |
| \t\t\t\t\t\t"value 0x%04x service 0x%04x-0x%04x; skipping", | |
| \t\t\t\t\t\tchrc_data->start_handle, | |
| \t\t\t\t\t\tchrc_data->value_handle, start, end); | |
| \t\t\tfree(chrc_data); | |
| \t\t\tcontinue; | |
| \t\t} | |
| \t\t/* Adjust current service */ | |
| \t\tsvc = gatt_db_get_service(client->db, chrc_data->value_handle); | |
| ''' | |
| s = replace_once(s, old, new, "gatt-client service range validation") | |
| old = '''\t\tif (gatt_db_attribute_get_handle(attr) != | |
| \t\t\t\t\t\t\tchrc_data->value_handle) | |
| \t\t\tgoto failed; | |
| ''' | |
| new = '''\t\tif (gatt_db_attribute_get_handle(attr) != | |
| \t\t\t\t\t\t\tchrc_data->value_handle) { | |
| \t\t\tDBG(client, "Invalid characteristic attribute 0x%04x " | |
| \t\t\t\t\t\t"expected 0x%04x; skipping", | |
| \t\t\t\t\t\tgatt_db_attribute_get_handle(attr), | |
| \t\t\t\t\t\tchrc_data->value_handle); | |
| \t\t\tfree(chrc_data); | |
| \t\t\tcontinue; | |
| \t\t} | |
| ''' | |
| s = replace_once(s, old, new, "gatt-client handle mismatch") | |
| client.write_text(s) | |
| helpers = Path("src/shared/gatt-helpers.c") | |
| s = helpers.read_text() | |
| start = s.index("static void discover_chrcs_cb(") | |
| end = s.index("struct bt_gatt_request *bt_gatt_discover_characteristics", start) | |
| body = s[start:end] | |
| body = replace_once( | |
| body, | |
| '''\tsize_t data_length; | |
| \tuint16_t last_handle; | |
| ''', | |
| '''\tsize_t data_length; | |
| \tuint16_t last_handle; | |
| \tuint16_t data_end; | |
| ''', | |
| "gatt-helpers data_end declaration", | |
| ) | |
| old = '''\tif ((data_length != 7 && data_length != 21) || | |
| \t\t\t\t\t((length - 1) % data_length)) { | |
| \t\tsuccess = false; | |
| \t\tgoto done; | |
| \t} | |
| \tif (!result_append(opcode, pdu + 1, length - 1, | |
| \t\t\t\t\t\t\tdata_length, op)) { | |
| \t\tsuccess = false; | |
| \t\tgoto done; | |
| \t} | |
| \tlast_handle = get_le16(pdu + length - data_length); | |
| \t/* | |
| \t * If last handle is lower from previous start handle then it is smth | |
| \t * wrong. Let's stop search, otherwise we might enter infinite loop. | |
| \t */ | |
| \tif (last_handle < op->start_handle) { | |
| \t\tsuccess = false; | |
| \t\tgoto done; | |
| \t} | |
| \top->start_handle = last_handle + 1; | |
| ''' | |
| new = '''\tif (data_length != 7 && data_length != 21) { | |
| \t\tsuccess = false; | |
| \t\tgoto done; | |
| \t} | |
| \tdata_end = 1 + (((length - 1) / data_length) * data_length); | |
| \tif (data_end == 1) { | |
| \t\tsuccess = false; | |
| \t\tgoto done; | |
| \t} | |
| \t{ | |
| \t\tuint8_t filtered[data_end - 1]; | |
| \t\tuint16_t pos, filtered_len = 0; | |
| \t\tuint16_t last_valid_handle = 0; | |
| \t\tfor (pos = 1; pos < data_end; pos += data_length) { | |
| \t\t\tuint16_t handle = get_le16(pdu + pos); | |
| \t\t\tuint16_t value_handle = get_le16(pdu + pos + 3); | |
| \t\t\t/* Characteristic values shall immediately follow their | |
| \t\t\t * declarations and remain within the requested range. Some | |
| \t\t\t * devices interleave fragments of 128-bit UUID declarations in a | |
| \t\t\t * 16-bit discovery response; keep valid entries and ignore the | |
| \t\t\t * malformed records so discovery can continue. | |
| \t\t\t */ | |
| \t\t\tif (handle < op->start_handle || handle > op->end_handle || | |
| \t\t\t\t\tvalue_handle != handle + 1 || | |
| \t\t\t\t\tvalue_handle > op->end_handle) | |
| \t\t\t\tcontinue; | |
| \t\t\tmemcpy(filtered + filtered_len, pdu + pos, data_length); | |
| \t\t\tfiltered_len += data_length; | |
| \t\t\tlast_valid_handle = handle; | |
| \t\t} | |
| \t\tif (!filtered_len) { | |
| \t\t\tsuccess = false; | |
| \t\t\tgoto done; | |
| \t\t} | |
| \t\tif (!result_append(opcode, filtered, filtered_len, | |
| \t\t\t\t\t\t\tdata_length, op)) { | |
| \t\t\tsuccess = false; | |
| \t\t\tgoto done; | |
| \t\t} | |
| \t\tlast_handle = last_valid_handle; | |
| \t} | |
| \top->start_handle = last_handle + 1; | |
| ''' | |
| body = replace_once(body, old, new, "gatt-helpers characteristic filtering") | |
| s = s[:start] + body + s[end:] | |
| helpers.write_text(s) | |
| PY | |
| quilt header --dep3 -a "$patch_name" <<'EOF' | |
| Description: Tolerate malformed remote characteristic discovery data | |
| Some BLE devices expose a valid HOGP service but later return malformed | |
| vendor characteristic declarations or incomplete trailing characteristic | |
| bytes. Keep valid characteristic records and skip unusable records so | |
| discovery can continue and HID over GATT can create UHID/evdev. | |
| Author: Codex local build <root@localhost> | |
| Forwarded: no | |
| Last-Update: 2026-06-16 | |
| EOF | |
| quilt refresh --no-timestamps --no-index | |
| } | |
| update_changelog() { | |
| local base_version local_version distribution source_dir | |
| base_version="$1" | |
| source_dir="$2" | |
| local_version="${base_version}${LOCAL_SUFFIX}" | |
| cd "$source_dir" | |
| distribution="$(dpkg-parsechangelog -S Distribution 2>/dev/null || true)" | |
| if [ -z "$distribution" ] || [ "$distribution" = "UNRELEASED" ]; then | |
| distribution="$(. /etc/os-release && printf '%s' "${VERSION_CODENAME:-unstable}")" | |
| fi | |
| DEBFULLNAME="${DEBFULLNAME:-Codex local build}" \ | |
| DEBEMAIL="${DEBEMAIL:-root@localhost}" \ | |
| dch -v "$local_version" -D "$distribution" \ | |
| "shared/gatt: tolerate malformed characteristic discovery data for Sony HOGP remote." | |
| } | |
| build_packages() { | |
| local source_dir | |
| source_dir="$1" | |
| cd "$source_dir" | |
| DEB_BUILD_OPTIONS="${DEB_BUILD_OPTIONS:-nocheck}" dpkg-buildpackage -b -uc -us | |
| } | |
| main() { | |
| require_cmd awk | |
| require_cmd date | |
| require_cmd find | |
| require_cmd sort | |
| enable_deb_src | |
| install_packaging_deps | |
| local base_version source_dir | |
| base_version="$(detect_source_version)" | |
| echo "Using BlueZ source version: $base_version" | |
| if [ "$INSTALL_BUILD_DEPS" -eq 1 ]; then | |
| echo "Installing BlueZ build dependencies" | |
| if ! run_root apt-get build-dep -y "bluez=$base_version"; then | |
| echo "Versioned build-dep failed; trying unversioned build-dep" >&2 | |
| run_root apt-get build-dep -y bluez | |
| fi | |
| fi | |
| fetch_source "$base_version" | |
| source_dir="$(find_source_dir)" | |
| if [ -z "$source_dir" ]; then | |
| echo "Could not find unpacked bluez-* source directory in $WORKDIR" >&2 | |
| exit 1 | |
| fi | |
| apply_sony_hogp_patch "$source_dir" | |
| update_changelog "$base_version" "$source_dir" | |
| build_packages "$source_dir" | |
| echo | |
| echo "Build complete. Packages:" | |
| find "$WORKDIR" -maxdepth 1 -type f -name '*.deb' -printf ' %p\n' | sort | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment