Skip to content

Instantly share code, notes, and snippets.

@Josephur
Forked from timkarnold/volt_unbrick.py
Created July 8, 2026 00:34
Show Gist options
  • Select an option

  • Save Josephur/0d0b2742caa5ad7c1d521f5c8c06752f to your computer and use it in GitHub Desktop.

Select an option

Save Josephur/0d0b2742caa5ad7c1d521f5c8c06752f to your computer and use it in GitHub Desktop.
Chevy Volt Unbrick (HPCM2 SHVCS Reset Tool)
#!/usr/bin/env python3
r"""
Volt SHVCS unbrick -- all in one.
Reads the HPCM2 security seed over a VXDIAG VCX Nano (J2534), auto-computes the
unlock key, and clears the SHVCS / "Propulsion Power Is Reduced" fault. No app,
no subscription, no Techline -- just the Nano you already own.
Pipeline (all to the HPCM2 at CAN 0x7E4, ISO15765 @ 500k):
27 01 -> read seed (67 01 XX XX)
key = f(seed) -> compute UDS SecurityAccess key locally
27 02 <key> -> unlock (expect 67 02)
AE FC .. 46 00 -> device-control clear
AE FC .. 49 00 -> device-control clear
============================ READ BEFORE RUNNING ============================
* DISCLAIMER: Provided AS-IS with NO WARRANTY of any kind. Use entirely at your
own risk. The author and contributors accept NO responsibility or liability
for any damage, injury, cost, data loss, or a locked/bricked module. You are
solely responsible for what you run on your own vehicle.
* SCOPE: Only tested on a 2013 Chevy Volt. Other model years, Opel/Vauxhall
Amperas, and Bolts may use a different seed, key, or algorithm -- do not
assume it transfers. Run the safe probe (no --clear) first and check the key.
* Writes security-access + device-control to your traction battery controller.
Do it PARKED, 12V healthy, in IGNITION ON (not Ready): press and HOLD the power
button ~10s WITHOUT pressing the brake (or anything else). Pressing the brake
puts it in Ready/drive -- you don't want that. Unofficial method; you accept the risk.
It clears the fault (band-aid); it does not fix a weak/imbalanced pack.
* SAFE BY DEFAULT: with no flags it reads the seed and PRINTS the key it would
send, then stops. Nothing is written until you add --clear.
* A wrong key returns "7F 27 xx" and the module stays locked (the clears are then
refused) -- no harm. Don't hammer wrong keys; repeats trigger a lockout (NRC 36/37).
REQUIREMENTS
* Windows, VXDIAG VX Manager installed, device set to J2534 mode, GDS2 closed.
* Python bitness must match the DLL (usually 32-bit -> use 32-bit Python).
Usage:
py volt_unbrick.py # read seed, show computed key (no writes)
py volt_unbrick.py --clear # read seed, compute key, unlock + clear
py volt_unbrick.py --key 1A2B # force a specific key instead of computing
py volt_unbrick.py --dll "C:\path\to\vxdiag_j2534.dll"
"""
# --- Credits / prior art -----------------------------------------------------
# Independent right-to-repair tooling that builds on published community work:
# * SHVCS clear sequence (HPCM2 at 7E4, UDS SecurityAccess 27 01 / 27 02, the
# AE FC device-control clears) and the VCX Nano J2534 approach:
# public gists by cjdell and jakka351.
# * GM 2-byte seed->key algorithm tables/tool: SCResearch/GM-Seed-key-Tester
# and YustasSwamp/gm-seed-key, tracing to gearhead-efi.com seed/key research.
# HPCM2 uses algorithm 0x10 of the "others" table.
# * The logged seed/key pair that pinned down which algorithm: Volt owner community.
# The key math and J2534 client below are a clean Python reimplementation of
# those published algorithms -- all credit to the authors above.
# -----------------------------------------------------------------------------
import argparse
import ctypes as C
import sys
M = 0xFFFF
# ---------------------------------------------------------------------------
# Key computation: GM 2-byte seed->key algorithm 0x10 ("others" table),
# identified by matching a real logged pair FBE9->5AE0 and cross-checked in two
# independent table copies. Seed is fixed per car, so the key never changes.
# ---------------------------------------------------------------------------
def hpcm2_key(seed):
v = seed & M
v = ((v << 8) | (v >> 8)) & M # byte-swap...
v = (v + 0x855F) & M # ...then add 0x855F
v = (-v) & M # two's complement (~v + 1)
v = (v + 0x8612) & M # add 0x8612
v = ((v << 2) | (v >> 14)) & M # rotate left by 2
return v
assert hpcm2_key(0xFBE9) == 0x5AE0, "key algorithm failed its known real pair"
assert hpcm2_key(0x78DA) == 0x98EC, "key algorithm regression"
# ---------------------------------------------------------------------------
# J2534 plumbing
# ---------------------------------------------------------------------------
ISO15765 = 6
ISO15765_FRAME_PAD = 0x40
FLOW_CONTROL_FILTER = 3
TX_MSG_TYPE = 0x01
START_OF_MESSAGE = 0x02
ERR_BUFFER_EMPTY = 0x10
STATUS_NOERROR = 0x00
HPCM2_TX = bytes([0x00, 0x00, 0x07, 0xE4])
HPCM2_RX = bytes([0x00, 0x00, 0x07, 0xEC])
ID_MASK = bytes([0x00, 0x00, 0x07, 0xFF])
NRC = {
0x11: "serviceNotSupported", 0x12: "subFunctionNotSupported",
0x13: "incorrectMessageLength/format", 0x22: "conditionsNotCorrect",
0x24: "requestSequenceError (request a seed first)", 0x31: "requestOutOfRange",
0x33: "securityAccessDenied", 0x35: "invalidKey (wrong key)",
0x36: "exceededNumberOfAttempts (LOCKED OUT -- stop, power-cycle, wait)",
0x37: "requiredTimeDelayNotExpired (in lockout delay -- wait, don't hammer)",
0x78: "responsePending",
}
class PASSTHRU_MSG(C.Structure):
_fields_ = [
("ProtocolID", C.c_ulong), ("RxStatus", C.c_ulong), ("TxFlags", C.c_ulong),
("Timestamp", C.c_ulong), ("DataSize", C.c_ulong), ("ExtraDataIndex", C.c_ulong),
("Data", C.c_ubyte * 4128),
]
def _msg(data):
m = PASSTHRU_MSG()
m.ProtocolID = ISO15765
m.TxFlags = ISO15765_FRAME_PAD
m.DataSize = len(data)
for i, b in enumerate(data):
m.Data[i] = b
return m
def hexs(b):
return " ".join(f"{x:02X}" for x in b) if b else "(no response)"
def explain_refusal(r):
if r and len(r) >= 3 and r[0] == 0x7F and r[1] == 0x27:
return NRC.get(r[2], f"unknown NRC 0x{r[2]:02X}")
return "unexpected response (expected 67 02)"
def find_dll():
import winreg
for root in (r"SOFTWARE\WOW6432Node\PassThruSupport.04.04",
r"SOFTWARE\PassThruSupport.04.04"):
try:
base = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, root)
except OSError:
continue
for i in range(winreg.QueryInfoKey(base)[0]):
name = winreg.EnumKey(base, i)
try:
k = winreg.OpenKey(base, name)
lib = winreg.QueryValueEx(k, "FunctionLibrary")[0]
except OSError:
continue
if "vxdiag" in (name + lib).lower() or "vx" in name.lower():
print(f"[dll] using '{name}': {lib}")
return lib
print(f"[dll] found J2534 device '{name}': {lib}")
return None
class J2534:
def __init__(self, dll_path):
try:
self.lib = C.WinDLL(dll_path)
except OSError as e:
sys.exit(f"Could not load {dll_path}: {e}\n"
"If this is WinError 193, your Python bitness != the DLL's (use 32-bit Python).")
self.dev = C.c_ulong()
self.chan = C.c_ulong()
for fn, args in {
"PassThruOpen": (C.c_void_p, C.POINTER(C.c_ulong)),
"PassThruClose": (C.c_ulong,),
"PassThruConnect": (C.c_ulong, C.c_ulong, C.c_ulong, C.c_ulong, C.POINTER(C.c_ulong)),
"PassThruDisconnect": (C.c_ulong,),
"PassThruReadMsgs": (C.c_ulong, C.POINTER(PASSTHRU_MSG), C.POINTER(C.c_ulong), C.c_ulong),
"PassThruWriteMsgs": (C.c_ulong, C.POINTER(PASSTHRU_MSG), C.POINTER(C.c_ulong), C.c_ulong),
"PassThruStartMsgFilter":(C.c_ulong, C.c_ulong, C.POINTER(PASSTHRU_MSG),
C.POINTER(PASSTHRU_MSG), C.POINTER(PASSTHRU_MSG), C.POINTER(C.c_ulong)),
"PassThruGetLastError": (C.c_char_p,),
}.items():
f = getattr(self.lib, fn)
f.argtypes = args
f.restype = C.c_long
def _check(self, rc, where):
if rc != STATUS_NOERROR:
buf = C.create_string_buffer(160)
self.lib.PassThruGetLastError(buf)
sys.exit(f"{where} failed (rc={rc}): {buf.value.decode(errors='replace')}")
def open(self):
self._check(self.lib.PassThruOpen(None, C.byref(self.dev)), "PassThruOpen")
def connect(self):
self._check(self.lib.PassThruConnect(self.dev, ISO15765, 0, 500000, C.byref(self.chan)),
"PassThruConnect")
def add_flow_control_filter(self):
mask, patt, flow = _msg(ID_MASK), _msg(HPCM2_RX), _msg(HPCM2_TX)
fid = C.c_ulong()
self._check(self.lib.PassThruStartMsgFilter(self.chan, FLOW_CONTROL_FILTER,
C.byref(mask), C.byref(patt), C.byref(flow), C.byref(fid)),
"PassThruStartMsgFilter")
def send(self, payload):
m = _msg(HPCM2_TX + bytes(payload))
n = C.c_ulong(1)
self._check(self.lib.PassThruWriteMsgs(self.chan, C.byref(m), C.byref(n), 1000),
"PassThruWriteMsgs")
def read(self, timeout_ms=1500):
for _ in range(6):
m = PASSTHRU_MSG()
n = C.c_ulong(1)
rc = self.lib.PassThruReadMsgs(self.chan, C.byref(m), C.byref(n), timeout_ms)
if rc == ERR_BUFFER_EMPTY or n.value == 0:
continue
if rc != STATUS_NOERROR:
return None
if m.RxStatus & (TX_MSG_TYPE | START_OF_MESSAGE):
continue
if m.DataSize <= 4:
continue
return bytes(m.Data[4:m.DataSize])
return None
def close(self):
try:
self.lib.PassThruDisconnect(self.chan)
self.lib.PassThruClose(self.dev)
except Exception:
pass
# ---------------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description="Read HPCM2 seed, compute key, unlock + clear SHVCS")
ap.add_argument("--clear", action="store_true",
help="actually send the key + clear commands (writes to the ECU)")
ap.add_argument("--key", help="force a specific 4-hex-digit key instead of computing it")
ap.add_argument("--dll", help="path to the VXDIAG J2534 DLL (else auto-detect)")
args = ap.parse_args()
key_override = None
if args.key is not None:
try:
key_override = int(args.key, 16)
assert 0 <= key_override <= 0xFFFF
except (ValueError, AssertionError):
sys.exit(f"--key must be 4 hex digits (0000-FFFF), got '{args.key}'")
dll = args.dll or find_dll()
if not dll:
sys.exit("No J2534 DLL found. Install VX Manager / set the Nano to J2534, or pass --dll.")
dev = J2534(dll)
dev.open()
dev.connect()
dev.add_flow_control_filter()
print("[j2534] connected to HPCM2 (tx 7E4 / rx 7EC), ISO15765 @ 500k\n")
# 1) read the seed
print(">> 27 01 (request security-access seed)")
dev.send([0x27, 0x01])
resp = dev.read()
print(f"<< {hexs(resp)}")
if not resp or resp[0] != 0x67 or len(resp) < 4:
dev.close()
sys.exit("No valid seed response. Check cable/ignition/adapter mode. (Nothing written.)")
seed = (resp[2] << 8) | resp[3]
print(f" seed = {seed:04X}")
# 2) compute (or take) the key
if key_override is not None:
key = key_override
print(f" using provided key = {key:04X}\n")
else:
key = hpcm2_key(seed)
print(f" computed key = {key:04X}\n")
if not args.clear:
print(f"Safe probe done. Re-run with --clear to send key {key:04X} and clear the fault.")
dev.close()
return
# 3) unlock
kb = [(key >> 8) & 0xFF, key & 0xFF]
print(f">> 27 02 {kb[0]:02X} {kb[1]:02X} (send unlock key)")
dev.send([0x27, 0x02] + kb)
r = dev.read()
print(f"<< {hexs(r)}")
if not r or r[0] != 0x67:
dev.close()
sys.exit(f"\nUnlock refused: {explain_refusal(r)}.\n"
"The fault was NOT cleared. Nothing harmful happened. Don't retry keys blindly "
"(lockout risk); if the computed key is wrong we need another seed/key pair.")
print(" HPCM2 unlocked!\n")
# 4) clear
for payload in ([0xAE, 0xFC, 0x02, 0x00, 0x00, 0x46, 0x00],
[0xAE, 0xFC, 0x02, 0x00, 0x00, 0x49, 0x00]):
print(f">> {hexs(payload)} (device-control clear)")
dev.send(payload)
print(f"<< {hexs(dev.read())}\n")
dev.close()
print("Done. Cycle the ignition and check that 'Propulsion Power Is Reduced' is gone.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment