Helper utilities for parsing hex, DTC status bits, and DID/SID validation.
frompyudskit.utilsimportparse_hex,bytes_to_hex,to_json,to_yamlprint(parse_hex("22 F1 90"))print(bytes_to_hex(b"\x22\xF1\x90"))print(to_json({"ok":True}))print(to_yaml({"ok":True}))
defbuild_alfid(addr_len:int,size_len:int)->int:"""Build addressAndLengthFormatIdentifier = (addrLen << 4) | sizeLen."""ifnot(0<=addr_len<=0x0Fand0<=size_len<=0x0F):raiseValueError("addr_len and size_len must be 0..15")return(addr_len<<4)|size_len
defdtc_status_summary(status_byte:int)->str:"""Return comma-separated active DTC status flag names."""ifnotisinstance(status_byte,int):raiseValueError("status_byte must be int")flags=[nameforbit,nameinDTC_STATUS_BITS.items()ifstatus_byte&bit]return", ".join(flags)
defextract_json(text:str)->dict:""" Extract first JSON object from LLM response text. Handles: raw JSON, ```json fenced, embedded in prose. """ifnotisinstance(text,str):raiseValueError("text must be a string")fenced=re.search(r"```json\s*(\{.*?\})\s*```",text,re.DOTALL)iffenced:returnjson.loads(fenced.group(1))raw=text.strip()ifraw.startswith("{")andraw.endswith("}"):returnjson.loads(raw)# Find first JSON object by brace matchingstart=raw.find("{")ifstart==-1:raiseValueError("no JSON object found")depth=0foriinrange(start,len(raw)):ifraw[i]=="{":depth+=1elifraw[i]=="}":depth-=1ifdepth==0:returnjson.loads(raw[start:i+1])raiseValueError("no complete JSON object found")
defparse_hex(hex_str:str)->bytes:"""Accept hex with/without spaces and 0x prefix. Raise ValueError on bad input."""ifnotisinstance(hex_str,str):raiseValueError("hex_str must be a string")cleaned=hex_str.strip().replace(" ","")ifcleaned.startswith("0x")orcleaned.startswith("0X"):cleaned=cleaned[2:]ifcleaned=="":returnb""iflen(cleaned)%2!=0:raiseValueError("hex_str must have even length")ifnotHEX_RE.match(cleaned):raiseValueError("hex_str contains non-hex characters")returnbytes.fromhex(cleaned)
defpositive_response_sid(request_sid:int)->int:"""Return request SID + 0x40."""ifnotisinstance(request_sid,int):raiseValueError("request_sid must be int")returnrequest_sid+0x40
defsplit_dtc(dtc_3bytes:bytes)->tuple[str,str]:""" Parse 3-byte DTC representation into (category_letter, dtc_string). e.g. b'\x01\x23\x45' → ('P', 'P0123') """ifnotisinstance(dtc_3bytes,(bytes,bytearray))orlen(dtc_3bytes)!=3:raiseValueError("dtc_3bytes must be exactly 3 bytes")b1,b2,b3=dtc_3bytescategory_bits=(b1&0xC0)>>6category_letter={0:"P",1:"C",2:"B",3:"U"}[category_bits]code=((b1&0x3F)<<16)|(b2<<8)|b3returncategory_letter,f"{category_letter}{code:04X}"
defsuppress_pos_rsp(subfunc:int)->int:"""Set suppressPosRspMsgIndicationBit (bit 7) on a subFunction byte."""ifnotisinstance(subfunc,int):raiseValueError("subfunc must be int")returnsubfunc|0x80
defto_json(data:Any,indent:int=2)->str:"""Serialize to JSON with stable formatting."""returnjson.dumps(data,indent=indent,ensure_ascii=False,default=str)
defto_yaml(data:Any)->str:"""Serialize to YAML (requires PyYAML)."""try:importyaml# type: ignoreexceptExceptionasexc:# pragma: no coverraiseRuntimeError("PyYAML is required for YAML export")fromexcreturnyaml.safe_dump(data,sort_keys=False)