@@ -718,6 +718,45 @@ def on_exec(self, args: argparse.Namespace):
718718 raise NotImplementedError ("Please implement this" )
719719
720720
721+ class LFIndalaIdArgsUnit (DeviceRequiredUnit ):
722+ @staticmethod
723+ def add_card_arg (parser : ArgumentParserNoExit , required = False ):
724+ group = parser .add_mutually_exclusive_group (required = required )
725+ group .add_argument ("-r" , "--raw" , type = str ,
726+ help = "Raw 64-bit frame (16 hex chars)" ,
727+ metavar = "<hex>" )
728+ group .add_argument ("--fc" , type = int , dest = "_indala_fc" ,
729+ help = "Facility code (26-bit format, use with --cn)" ,
730+ metavar = "<dec>" )
731+ parser .add_argument ("--cn" , type = int , dest = "_indala_cn" ,
732+ help = "Card number (26-bit format, use with --fc)" ,
733+ metavar = "<dec>" )
734+ return parser
735+
736+ def before_exec (self , args : argparse .Namespace ):
737+ if not super ().before_exec (args ):
738+ return False
739+ fc = getattr (args , '_indala_fc' , None )
740+ cn = getattr (args , '_indala_cn' , None )
741+ if fc is not None or cn is not None :
742+ if fc is None or cn is None :
743+ raise ArgsParserError ("--fc and --cn must be used together" )
744+ if not (0 <= fc <= 255 ):
745+ raise ArgsParserError ("FC must be 0-255" )
746+ if not (0 <= cn <= 65535 ):
747+ raise ArgsParserError ("CN must be 0-65535" )
748+ args .raw = indala_encode_raw (fc , cn ).hex ()
749+ if args .raw is not None and not re .match (r"^[a-fA-F0-9]{16}$" , args .raw ):
750+ raise ArgsParserError ("Raw must be exactly 16 hex characters" )
751+ return True
752+
753+ def args_parser (self ) -> ArgumentParserNoExit :
754+ raise NotImplementedError ("Please implement this" )
755+
756+ def on_exec (self , args : argparse .Namespace ):
757+ raise NotImplementedError ("Please implement this" )
758+
759+
721760class TagTypeArgsUnit (DeviceRequiredUnit ):
722761 @staticmethod
723762 def add_type_args (parser : ArgumentParserNoExit ):
@@ -763,6 +802,7 @@ def on_exec(self, args: argparse.Namespace):
763802lf_ioprox = lf .subgroup ("ioprox" , "ioProx commands" )
764803lf_pac = lf .subgroup ("pac" , "PAC/Stanley commands" )
765804lf_viking = lf .subgroup ("viking" , "Viking commands" )
805+ lf_indala = lf .subgroup ("indala" , "Indala commands" )
766806lf_generic = lf .subgroup ("generic" , "Generic commands" )
767807
768808
@@ -6477,6 +6517,12 @@ def on_exec(self, args: argparse.Namespace):
64776517 raw = pac_encode_raw (id )
64786518 print (f" { 'CN:' :40} { color_string ((CY , id_ascii ))} " )
64796519 print (f" { 'Raw:' :40} { color_string ((CY , raw .hex ().upper ()))} " )
6520+ if lf_tag_type == TagSpecificType .Indala :
6521+ raw = self .cmd .indala_get_emu_id ()
6522+ fc , cn = indala_decode_raw (raw )
6523+ print (f" { 'Raw:' :40} { color_string ((CY , raw .hex ().upper ()))} " )
6524+ print (f" { 'FC:' :40} { color_string ((CG , fc ))} " )
6525+ print (f" { 'Card:' :40} { color_string ((CG , cn ))} " )
64806526 if current != selected :
64816527 self .cmd .set_active_slot (selected )
64826528
@@ -6639,6 +6685,166 @@ def on_exec(self, args: argparse.Namespace):
66396685 print (f"ID: { response .hex ().upper ()} " )
66406686
66416687
6688+ def indala_decode_raw (raw : bytes ):
6689+ """Decode Indala 26-bit FC/CN from 8-byte raw frame (PM3-compatible bit mapping)."""
6690+ bits = []
6691+ for b in raw :
6692+ for i in range (7 , - 1 , - 1 ):
6693+ bits .append ((b >> i ) & 1 )
6694+
6695+ fc = 0
6696+ fc |= bits [57 ] << 7
6697+ fc |= bits [49 ] << 6
6698+ fc |= bits [44 ] << 5
6699+ fc |= bits [47 ] << 4
6700+ fc |= bits [48 ] << 3
6701+ fc |= bits [53 ] << 2
6702+ fc |= bits [39 ] << 1
6703+ fc |= bits [58 ] << 0
6704+
6705+ cn = 0
6706+ cn |= bits [42 ] << 15
6707+ cn |= bits [45 ] << 14
6708+ cn |= bits [43 ] << 13
6709+ cn |= bits [40 ] << 12
6710+ cn |= bits [52 ] << 11
6711+ cn |= bits [36 ] << 10
6712+ cn |= bits [35 ] << 9
6713+ cn |= bits [51 ] << 8
6714+ cn |= bits [46 ] << 7
6715+ cn |= bits [33 ] << 6
6716+ cn |= bits [37 ] << 5
6717+ cn |= bits [54 ] << 4
6718+ cn |= bits [56 ] << 3
6719+ cn |= bits [59 ] << 2
6720+ cn |= bits [50 ] << 1
6721+ cn |= bits [41 ] << 0
6722+
6723+ return fc , cn
6724+
6725+
6726+ def indala_encode_raw (fc : int , cn : int ) -> bytes :
6727+ """Encode FC/CN into 8-byte Indala 26-bit raw frame (PM3-compatible bit mapping)."""
6728+ bits = [0 ] * 64
6729+
6730+ # preamble
6731+ bits [0 ] = 1
6732+ bits [2 ] = 1
6733+ bits [32 ] = 1
6734+
6735+ # fc
6736+ bits [57 ] = (fc >> 7 ) & 1
6737+ bits [49 ] = (fc >> 6 ) & 1
6738+ bits [44 ] = (fc >> 5 ) & 1
6739+ bits [47 ] = (fc >> 4 ) & 1
6740+ bits [48 ] = (fc >> 3 ) & 1
6741+ bits [53 ] = (fc >> 2 ) & 1
6742+ bits [39 ] = (fc >> 1 ) & 1
6743+ bits [58 ] = fc & 1
6744+
6745+ # cn
6746+ bits [42 ] = (cn >> 15 ) & 1
6747+ bits [45 ] = (cn >> 14 ) & 1
6748+ bits [43 ] = (cn >> 13 ) & 1
6749+ bits [40 ] = (cn >> 12 ) & 1
6750+ bits [52 ] = (cn >> 11 ) & 1
6751+ bits [36 ] = (cn >> 10 ) & 1
6752+ bits [35 ] = (cn >> 9 ) & 1
6753+ bits [51 ] = (cn >> 8 ) & 1
6754+ bits [46 ] = (cn >> 7 ) & 1
6755+ bits [33 ] = (cn >> 6 ) & 1
6756+ bits [37 ] = (cn >> 5 ) & 1
6757+ bits [54 ] = (cn >> 4 ) & 1
6758+ bits [56 ] = (cn >> 3 ) & 1
6759+ bits [59 ] = (cn >> 2 ) & 1
6760+ bits [50 ] = (cn >> 1 ) & 1
6761+ bits [41 ] = cn & 1
6762+
6763+ # checksum (sum of specific cn bits)
6764+ chk = sum ([
6765+ (cn >> 14 ) & 1 , (cn >> 12 ) & 1 , (cn >> 9 ) & 1 , (cn >> 8 ) & 1 ,
6766+ (cn >> 6 ) & 1 , (cn >> 5 ) & 1 , (cn >> 2 ) & 1 , cn & 1 ,
6767+ ])
6768+ if chk % 2 == 0 :
6769+ bits [62 ], bits [63 ] = 1 , 0
6770+ else :
6771+ bits [62 ], bits [63 ] = 0 , 1
6772+
6773+ # parity
6774+ p1 , p2 = 1 , 1
6775+ for i in range (33 , 64 ):
6776+ if i % 2 :
6777+ p1 ^= bits [i ]
6778+ else :
6779+ p2 ^= bits [i ]
6780+ bits [34 ] = p1
6781+ bits [38 ] = p2
6782+
6783+ raw = bytearray (8 )
6784+ for i in range (64 ):
6785+ if bits [i ]:
6786+ raw [i // 8 ] |= 1 << (7 - (i % 8 ))
6787+ return bytes (raw )
6788+
6789+
6790+ def indala_format_output (raw : bytes ) -> str :
6791+ """Format Indala raw bytes as PM3-style output string."""
6792+ fc , cn = indala_decode_raw (raw )
6793+ lines = [f"Indala (len 64) Raw: { raw .hex ().upper ()} " ]
6794+ lines .append (f" Fmt 26 FC: { fc } Card: { cn } " )
6795+ return "\n " .join (lines )
6796+
6797+
6798+ @lf_indala .command ("read" )
6799+ class LFIndalaRead (ReaderRequiredUnit ):
6800+ def args_parser (self ) -> ArgumentParserNoExit :
6801+ parser = ArgumentParserNoExit ()
6802+ parser .description = "Scan for an Indala tag"
6803+ return parser
6804+
6805+ def on_exec (self , args : argparse .Namespace ):
6806+ resp = self .cmd .indala_scan ()
6807+ if resp .status != Status .LF_TAG_OK :
6808+ print (f" Indala scan failed: { resp .status } " )
6809+ return
6810+ print (f" { indala_format_output (resp .parsed )} " )
6811+
6812+
6813+ @lf_indala .command ("write" )
6814+ class LFIndalaWriteT55xx (LFIndalaIdArgsUnit , ReaderRequiredUnit ):
6815+ def args_parser (self ) -> ArgumentParserNoExit :
6816+ parser = ArgumentParserNoExit ()
6817+ parser .description = "Clone Indala tag to T55XX (use -r <hex>, or --fc <n> --cn <n>)"
6818+ return self .add_card_arg (parser , required = True )
6819+
6820+ def on_exec (self , args : argparse .Namespace ):
6821+ id_bytes = bytes .fromhex (args .raw )
6822+ self .cmd .indala_write_to_t55xx (id_bytes )
6823+ print (f" { indala_format_output (id_bytes )} " )
6824+ print (f" Write done. Verify with 'lf indala read'." )
6825+
6826+
6827+ @lf_indala .command ("econfig" )
6828+ class LFIndalaEconfig (SlotIndexArgsAndGoUnit , LFIndalaIdArgsUnit ):
6829+ def args_parser (self ) -> ArgumentParserNoExit :
6830+ parser = ArgumentParserNoExit ()
6831+ parser .description = "Set or get emulated Indala card (use -r/--fc+--cn to set, omit to get)"
6832+ self .add_slot_args (parser )
6833+ self .add_card_arg (parser )
6834+ return parser
6835+
6836+ def on_exec (self , args : argparse .Namespace ):
6837+ if args .raw is not None :
6838+ id_bytes = bytes .fromhex (args .raw )
6839+ self .cmd .indala_set_emu_id (id_bytes )
6840+ print (f" - Set emulated Indala:" )
6841+ print (f" { indala_format_output (id_bytes )} " )
6842+ else :
6843+ response = self .cmd .indala_get_emu_id ()
6844+ print (f" - Get emulated Indala:" )
6845+ print (f" { indala_format_output (response )} " )
6846+
6847+
66426848@hw_slot .command ("nick" )
66436849class HWSlotNick (SlotIndexArgsUnit , SenseTypeArgsUnit ):
66446850 def args_parser (self ) -> ArgumentParserNoExit :
0 commit comments