Last active
July 11, 2026 15:58
-
-
Save timkarnold/0b80ee8778781981878d10a458d77737 to your computer and use it in GitHub Desktop.
Chevy Volt Unbrick (HPCM2 SHVCS Reset Tool)
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 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
Looks great aiubda great and its all good