Skip to content

Instantly share code, notes, and snippets.

@Kabouik
Created August 31, 2025 12:26
Show Gist options
  • Save Kabouik/ae20204997d4946f5d1fad217cf4430c to your computer and use it in GitHub Desktop.
Save Kabouik/ae20204997d4946f5d1fad217cf4430c to your computer and use it in GitHub Desktop.
Omial's work for Linux touchscreen support on GPD MicroPC 2
#!/usr/bin/env python3
#
# End-to-end helper to:
# - dump ACPI tables
# - extract & decompile DSDT
# - bump DefinitionBlock revision
# - insert Externals inside top-level braces
# - inject PTPL._OFF() workaround into TPL1._DSM in Scope(_SB.PC00.I2C0)
# - recompile AML (uppercase DSDT.aml) inside working dir
# - package minimal cpio at /boot/acpi_override.cpio
# - configure GRUB to always load it as an early initrd (persists across upgrades)
#
# Requires: acpidump, acpixtract, iasl, cpio, grub2-mkconfig
#
# Tested as working on Fedora Workstation 42
#
# WARNING: Largely ChatGPT5 generated, expect idiocy
#
# Licence: Public domain, do as you like
#
import os
import re
import sys
import glob
import shutil
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
# --------------------------- Helpers ---------------------------
def run(cmd, check=True, capture=False, cwd=None):
print("+", " ".join(cmd), f"(cwd={cwd})" if cwd else "")
if capture:
return subprocess.run(cmd, check=check, text=True, cwd=cwd,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout
else:
subprocess.run(cmd, check=check, cwd=cwd)
def which(prog):
from shutil import which as _which
return _which(prog)
def require_tools(tools):
missing = [t for t in tools if not which(t)]
if missing:
print("\nERROR: Missing tools:", ", ".join(missing))
print("Install them, e.g.:")
print(" sudo dnf install -y acpica-tools iasl cpio grub2-tools\n")
sys.exit(1)
def must_be_root():
if os.geteuid() != 0:
print("Please run as root (sudo).")
sys.exit(1)
def kernel_config_has_table_upgrade():
cfg = Path(f"/boot/config-{os.uname().release}")
if not cfg.exists():
# try fallback
cfg = Path("/proc/config.gz")
if cfg.exists():
out = run(["bash","-lc",
r"zgrep -E '^CONFIG_ACPI_TABLE_UPGRADE=y' /proc/config.gz || true"],
check=False, capture=True)
return "CONFIG_ACPI_TABLE_UPGRADE=y" in out
return False
txt = cfg.read_text(errors="ignore")
return "CONFIG_ACPI_TABLE_UPGRADE=y" in txt
def detect_grub_cfg_output_path():
"""
Choose the correct grub.cfg file to write.
Fedora/RHEL: /boot/grub2/grub.cfg (even on UEFI)
Debian/Ubuntu: /boot/grub/grub.cfg
"""
for p in ("/boot/grub2/grub.cfg", "/boot/grub/grub.cfg"):
if Path(p).exists():
return p
# Default sensibly for Fedora if nothing exists yet (grub2-mkconfig will create it)
return "/boot/grub2/grub.cfg"
def ensure_grub_early_initrd(entry="acpi_override.cpio"):
cfgfile = Path("/etc/default/grub")
cfgfile.parent.mkdir(parents=True, exist_ok=True)
cfg = cfgfile.read_text(errors="ignore") if cfgfile.exists() else ""
import re
line_re = re.compile(r'^\s*GRUB_EARLY_INITRD_LINUX_CUSTOM\s*=\s*"(.*)"\s*$', re.M)
m = line_re.search(cfg)
if m:
vals = m.group(1).split()
if entry not in vals:
vals.append(entry)
new = line_re.sub(f'GRUB_EARLY_INITRD_LINUX_CUSTOM="{" ".join(vals)}"', cfg)
cfgfile.write_text(new)
print(f"Updated {cfgfile} with GRUB_EARLY_INITRD_LINUX_CUSTOM.")
else:
print(f"{cfgfile}: GRUB_EARLY_INITRD_LINUX_CUSTOM already contains {entry}")
else:
with cfgfile.open("a") as f:
f.write(f'\nGRUB_EARLY_INITRD_LINUX_CUSTOM="{entry}"\n')
print(f"Added GRUB_EARLY_INITRD_LINUX_CUSTOM to {cfgfile}.")
outpath = detect_grub_cfg_output_path()
print(f"Regenerating GRUB config at {outpath} …")
run(["grub2-mkconfig", "-o", outpath])
# --------------------------- DSDT patching ---------------------------
def bump_definitionblock_revision(text):
pat = re.compile(
r'(DefinitionBlock\s*\(\s*"[^"]*"\s*,\s*"DSDT"\s*,\s*\d+\s*,\s*"[^"]*"\s*,\s*"[^"]*"\s*,\s*)(0x[0-9A-Fa-f]+)(\s*\))'
)
def repl(m):
hexv = m.group(2)
val = int(hexv, 16) + 1
bumped = f"0x{val:08X}"
print(f" - Bumped DefinitionBlock revision: {hexv} -> {bumped}")
return m.group(1) + bumped + m.group(3)
new_text, n = pat.subn(repl, text, count=1)
if n == 0:
print("WARNING: Could not find DefinitionBlock to bump revision.")
return text
return new_text
def insert_externals_inside_toplevel_brace(lines):
externals = [
' External (_SB_.PC00.I2C0.PTPL, UnknownObj)\n',
' External (_SB_.PC00.I2C0.PTPL._OFF, MethodObj) // 0 Arguments\n',
'\n',
]
out = []
inserted = False
seen_defblock = False
for ln in lines:
out.append(ln)
if not seen_defblock and "DefinitionBlock" in ln:
seen_defblock = True
elif seen_defblock and (not inserted) and "{" in ln:
out.extend(externals)
inserted = True
if inserted:
print(" - Inserted External() declarations inside top-level braces.")
else:
print("WARNING: Did not insert Externals (couldn't find top-level '{').")
return out
def inject_ptpl_off_in_tpl1_dsm(lines):
"""
Target:
Scope (_SB.PC00.I2C0)
Device (TPL1)
Method (_DSM, 4, ... )
Insert after the opening '{' of that _DSM:
If (CondRefOf(\\_SB.PC00.I2C0.PTPL))
{
\\_SB.PC00.I2C0.PTPL._OFF()
Sleep (200)
}
"""
whole = "".join(lines)
if r"\_SB.PC00.I2C0.PTPL._OFF" in whole:
print(" - PTPL._OFF already present; skipping injection.")
return lines
scope_pat = re.compile(r'^\s*Scope\s*\(\s*_SB\.PC00\.I2C0\s*\)', re.M)
device_pat = re.compile(r'^\s*Device\s*\(\s*TPL1\s*\)', re.M)
method_pat = re.compile(r'^\s*Method\s*\(\s*_DSM\s*,\s*4\b', re.M)
# Pre-scan counts for debugging
scope_hits = list(scope_pat.finditer(whole))
device_hits = list(device_pat.finditer(whole))
method_hits = list(method_pat.finditer(whole))
print(f" - Debug: Scope(_SB.PC00.I2C0) matches: {len(scope_hits)}")
print(f" - Debug: Device(TPL1) matches: {len(device_hits)}")
print(f" - Debug: Method(_DSM,4,...) matches: {len(method_hits)}")
if scope_hits:
print(f" first Scope hit at approx char {scope_hits[0].start()}")
if device_hits:
print(f" first Device hit at approx char {device_hits[0].start()}")
if method_hits:
print(f" first Method hit at approx char {method_hits[0].start()}")
out = []
depth = 0
in_target_scope = False
scope_depth = None
in_target_dev = False
dev_depth = None
seen_method_sig = False
inserted = False
def brace_delta(s: str) -> int:
return s.count("{") - s.count("}")
for idx, ln in enumerate(lines, start=1):
# Header matches before we change depth
if (not in_target_scope) and scope_pat.match(ln):
print(f" >> matched Scope(_SB.PC00.I2C0) at line {idx}")
elif in_target_scope and (not in_target_dev) and device_pat.match(ln):
print(f" >> matched Device(TPL1) at line {idx}")
elif in_target_scope and in_target_dev and (not seen_method_sig) and method_pat.match(ln):
seen_method_sig = True
print(f" >> matched Method(_DSM,4,...) at line {idx}")
out.append(ln)
# After method signature, on its '{' line inject
if in_target_scope and in_target_dev and seen_method_sig and (not inserted):
if "{" in ln:
indent = re.match(r'^(\s*)', ln).group(1) + " "
block = [
f"{indent}// Workaround: force a cold power cycle so the I2C slave will ACK at probe\n",
f"{indent}If (CondRefOf(\\_SB.PC00.I2C0.PTPL))\n",
f"{indent}{{\n",
f"{indent} \\_SB.PC00.I2C0.PTPL._OFF()\n",
f"{indent} Sleep (200)\n",
f"{indent}}}\n",
]
out.extend(block)
inserted = True
print(f" >> injected PTPL._OFF() block after line {idx}")
d = brace_delta(ln)
depth += d
# entry/exit tracking for scope
if (not in_target_scope) and scope_pat.match(ln):
in_target_scope = True
scope_depth = depth+1
elif in_target_scope and (depth < (scope_depth or 0)):
in_target_scope = False
scope_depth = None
in_target_dev = False
dev_depth = None
seen_method_sig = False
# entry/exit tracking for device
if in_target_scope and (not in_target_dev) and device_pat.match(ln):
in_target_dev = True
dev_depth = depth+1
elif in_target_dev and (depth < (dev_depth or 0)):
in_target_dev = False
dev_depth = None
seen_method_sig = False
if inserted:
print(" - Inserted PTPL._OFF() block in TPL1._DSM.")
return out
# Hard error + debugging advice
print("\nERROR: Did not find the target Method(_DSM,4,...) inside Device(TPL1) within Scope(_SB.PC00.I2C0).")
print("Tips:")
print(" * Confirm the TPL1 device really lives under Scope(_SB.PC00.I2C0).")
print(" * Confirm the _DSM signature is exactly Method (_DSM, 4, ...).")
print(" * Search the generated dsdt.dsl for 'Scope (_SB.PC00.I2C0)' and 'Device (TPL1)'.")
print(" * Counts above show how many raw matches were seen in the file.")
sys.exit(3)
# --------------------------- Main flow ---------------------------
def main():
must_be_root()
if not kernel_config_has_table_upgrade():
print("\nERROR: Your kernel does not have CONFIG_ACPI_TABLE_UPGRADE=y.")
print("This method relies on initrd ACPI table override. Please enable it or use a kernel that has it.\n")
sys.exit(1)
require_tools(["acpidump", "acpixtract", "iasl", "cpio", "grub2-mkconfig"])
with TemporaryDirectory(prefix="acpi_fix_") as wd_tmp:
wd = Path(wd_tmp)
print(f"Work dir: {wd}")
# 1) Dump & extract (outputs to wd via cwd)
acpi_out = wd/"acpi.out"
run(["acpidump", "-o", str(acpi_out)], cwd=wd)
run(["acpixtract", "-a", str(acpi_out)], cwd=wd)
# 2) Find DSDT.dat (case-insensitive)
dsdt_dat = None
for cand in ["DSDT.dat", "dsdt.dat"]:
p = wd/cand
if p.exists():
dsdt_dat = p
break
if not dsdt_dat:
# very unlikely if cwd=wd, but try pulling from CWD as a fallback
for f in glob.glob("DSDT.dat") + glob.glob("dsdt.dat"):
src = Path(f).resolve()
dst = wd/src.name
shutil.copy2(src, dst)
dsdt_dat = dst
break
if not dsdt_dat or not dsdt_dat.exists():
print("ERROR: Could not find DSDT.dat after acpixtract.")
sys.exit(1)
print(f"Using {dsdt_dat.name}")
# 3) Decompile -> dsdt.dsl *in wd*, hide warnings
# -p sets the output prefix (so output will be wd/dsdt.dsl)
run(["iasl", "-d", "-p", "dsdt", str(dsdt_dat.name)], cwd=wd)
dsl = wd/"dsdt.dsl"
if not dsl.exists():
print("ERROR: dsdt.dsl not found after iasl -d.")
sys.exit(1)
# 4) Patch DSDT.dsl
original = dsl.read_text(errors="ignore")
patched = bump_definitionblock_revision(original)
lines = patched.splitlines(True)
lines = insert_externals_inside_toplevel_brace(lines)
lines = inject_ptpl_off_in_tpl1_dsm(lines)
backup = wd/"dsdt.dsl.bak"
backup.write_text(original)
dsl.write_text("".join(lines))
print(f"Patched DSDT written: {dsl}")
# 5) Recompile AML into wd, uppercase DSDT.aml, hide warnings
# -p DSDT ensures output file is wd/DSDT.aml
run(["iasl", "-tc", "-ve", "-oa", "-p", "DSDT", "dsdt.dsl"], cwd=wd)
aml = wd/"DSDT.aml"
if not aml.exists():
print("ERROR: DSDT.aml not produced by iasl.")
sys.exit(1)
# 6) Build minimal override cpio at /boot/acpi_override.cpio
stage = wd/"acpi_override"/"kernel"/"firmware"/"acpi"
stage.mkdir(parents=True, exist_ok=True)
shutil.copy2(aml, stage/"DSDT.aml")
target_cpio = Path("/boot/acpi_override.cpio")
print(f"Creating {target_cpio} …")
run(["bash","-lc",
f"cd '{(wd/'acpi_override').as_posix()}' && "
f"find kernel | cpio -H newc -o > '{target_cpio.as_posix()}'"])
# 7) Configure GRUB to always load early initrd
ensure_grub_early_initrd("acpi_override.cpio")
print("\nDone.")
print("Next steps:")
print(" 1) Reboot")
print(" 2) See if touchscreen works?!")
print(" 3) Otherwise, verify that the kernel finds the replacement: sudo dmesg | grep -i 'DSDT ACPI table found in initrd'")
print(" 4) Also verify that the loaded table contains the patch:")
print(" sudo cat /sys/firmware/acpi/tables/DSDT > /tmp/DSDT.bin")
print(" iasl -d /tmp/DSDT.bin")
print(" grep -n 'PTPL\\._OFF' /tmp/DSDT.dsl")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment