Skip to content

Instantly share code, notes, and snippets.

@nohajc
Created June 16, 2026 07:37
Show Gist options
  • Select an option

  • Save nohajc/2e4641d561e94ea87221983f3506c2c3 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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