Skip to content

Instantly share code, notes, and snippets.

@timkarnold
Last active July 11, 2026 15:58
Show Gist options
  • Select an option

  • Save timkarnold/0b80ee8778781981878d10a458d77737 to your computer and use it in GitHub Desktop.

Select an option

Save timkarnold/0b80ee8778781981878d10a458d77737 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()
@Voltarians

Copy link
Copy Markdown

Looks great aiubda great and its all good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment