Merge remote-tracking branch 'origin/main' into cli-overhaul

This commit is contained in:
Szymon Borecki
2023-08-19 14:19:40 +02:00
34 changed files with 1123 additions and 401 deletions
+6 -7
View File
@@ -29,13 +29,12 @@ LITE = r"""
# create by http://patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=Chameleon%20Ultra
BANNER = f"""
██████╗██╗ ██╗ █████╗ ████╗███████╗██╗ ███████╗ █████╗ ███╗ ██╗
██╔════╝██║ ██║██╔══██╗██████║██╔════╝██║ ██╔════╝██╔══██╗████╗ ██║
██║ ███████║███████║████████║█████╗ ██║ █████╗ ██║ ██║████╗ ██║
██║ ██╔══██║██╔══██║██║╚██╔██║██╔══╝ ██║ ██╔══╝ ██║ ██║██║╚████║
╚██████╗██║ ██║██║ ██║██║ ╚═╝ ██║███████╗███████╗███████╗╚█████╔╝██║ ╚████║
╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚════╝ ╚═╝ ╚═══╝
██████╗██╗ ██╗ █████╗ ██╗ ██╗███████╗██╗ ███████╗ █████╗ ██ ██╗
██╔════╝██║ ██║██╔══██╗███╗ ███║██╔════╝██║ ██╔════╝██╔══██╗███ ██║
██║ ███████║███████║████████║█████╗ ██║ █████╗ ██║ ██║████╗██║
██║ ██╔══██║██╔══██║████╔██║██╔══╝ ██║ ██╔══╝ ██║ ██║██████║
╚██████╗██║ ██║██║ ██║██║╚═╝██║███████╗███████╗███████╗╚█████╔╝██║███║
╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚════╝ ╚═╝ ══╝
"""
+138 -14
View File
@@ -157,6 +157,8 @@ hw_address = hw.subgroup('address', 'Device address get')
hw_mode = hw.subgroup('mode', 'Device mode get/set')
hw_slot = hw.subgroup('slot', 'Emulation tag slot.')
hw_slot_nick = hw_slot.subgroup('nick', 'Get/Set tag nick name for slot')
hw_settings = hw.subgroup('settings', 'Chameleon settings management')
hw_settings_animation = hw_settings.subgroup('animation', 'Manage wake-up and sleep animation modes')
hf = CLITree('hf', 'high frequency tag/reader')
hf_14a = hf.subgroup('14a', 'ISO14443-a tag read/write/info...')
@@ -248,6 +250,19 @@ class HWAddressGet(DeviceRequiredUnit):
print(f' - Device address: ' + self.cmd.get_device_address())
@hw.command('version', 'Get current device firmware version')
class HWVersion(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
return None
def on_exec(self, args: argparse.Namespace):
fw_version_int = self.cmd.get_firmware_version()
fw_version = f'v{fw_version_int // 256}.{fw_version_int % 256}'
git_version = self.cmd.get_git_version()
print(f' - Version: {fw_version} ({git_version})')
@hf_14a.command('scan', 'Scan 14a tag, and print basic information')
class HF14AScan(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
@@ -795,11 +810,14 @@ class SlotIndexRequireUnit(DeviceRequiredUnit):
@staticmethod
def add_slot_args(parser: ArgumentParserNoExit):
slot_choices = [1, 2, 3, 4, 5, 6, 7, 8]
slot_choices = [x.value for x in chameleon_cmd.SlotNumber]
help_str = f"Slot Indexes: {slot_choices}"
parser.add_argument('-s', "--slot", type=int, required=True,
help="Slot index", metavar="number", choices=slot_choices)
help=help_str, metavar="number", choices=slot_choices)
return parser
class SenseTypeRequireUnit(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
@@ -810,12 +828,34 @@ class SenseTypeRequireUnit(DeviceRequiredUnit):
@staticmethod
def add_sense_type_args(parser: ArgumentParserNoExit):
slot_choices = [1, 2]
sense_choices = chameleon_cmd.TagSenseType.list()
help_str = ""
for s in chameleon_cmd.TagSenseType:
if s == chameleon_cmd.TagSenseType.TAG_SENSE_NO:
continue
help_str += f"{s.value} = {s}, "
parser.add_argument('-st', "--sense_type", type=int, required=True,
help="Sense type", metavar="number", choices=slot_choices)
help=help_str, metavar="number", choices=sense_choices)
return parser
@hw_slot.command('info', 'Get information about slots')
class HWSlotInfo(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
return
# hw slot info
def on_exec(self, args: argparse.Namespace):
data = self.cmd.get_slot_info().data
selected = chameleon_cmd.SlotNumber.from_fw(self.cmd.get_active_slot().data[0])
for slot in chameleon_cmd.SlotNumber:
print(f' - Slot {slot} data{" (active)" if slot == selected else ""}:')
print(f' HF: {chameleon_cmd.TagSpecificType(data[chameleon_cmd.SlotNumber.to_fw(slot) * 2])}')
print(f' LF: {chameleon_cmd.TagSpecificType(data[chameleon_cmd.SlotNumber.to_fw(slot) * 2 + 1])}')
@hw_slot.command('change', 'Set emulation tag slot activated.')
class HWSlotSet(SlotIndexRequireUnit):
@@ -836,10 +876,10 @@ class TagTypeRequiredUnit(DeviceRequiredUnit):
def add_type_args(parser: ArgumentParserNoExit):
type_choices = chameleon_cmd.TagSpecificType.list()
help_str = ""
for name, value in chameleon_cmd.TagSpecificType.__members__.items():
if value == chameleon_cmd.TagSpecificType.TAG_TYPE_UNKNOWN:
for t in chameleon_cmd.TagSpecificType:
if t == chameleon_cmd.TagSpecificType.TAG_TYPE_UNKNOWN:
continue
help_str += f"{value} = {name.replace('TAG_TYPE_', '')}, "
help_str += f"{t.value} = {t}, "
parser.add_argument('-t', "--type", type=int, required=True, help=help_str,
metavar="number", choices=type_choices)
return parser
@@ -923,7 +963,7 @@ class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit):
parser = ArgumentParserNoExit()
self.add_slot_args(parser)
self.add_sense_type_args(parser)
parser.add_argument('-n', '--name', type=str, required=True, help="Yout tag nick name for slot")
parser.add_argument('-n', '--name', type=str, required=True, help="Your tag nick name for slot")
return parser
# hw slot nick set -s 1 -st 1 -n 测试名称保存
@@ -931,7 +971,8 @@ class HWSlotNickSet(SlotIndexRequireUnit, SenseTypeRequireUnit):
slot_num = args.slot
sense_type = args.sense_type
name: str = args.name
if len(name.encode(encoding="gbk")) > 32:
uname = name.encode(encoding="utf8")
if len(uname) > 32:
raise ValueError("Your tag nick name too long.")
self.cmd.set_slot_tag_nick_name(slot_num, sense_type, name)
print(f' - Set tag nick name for slot {slot_num} success.')
@@ -950,7 +991,7 @@ class HWSlotNickGet(SlotIndexRequireUnit, SenseTypeRequireUnit):
slot_num = args.slot
sense_type = args.sense_type
res = self.cmd.get_slot_tag_nick_name(slot_num, sense_type)
print(f' - Get tag nick name for slot {slot_num}: {res.data.decode(encoding="gbk")}')
print(f' - Get tag nick name for slot {slot_num}: {res.data.decode(encoding="utf8")}')
@hw_slot.command('update', 'Update config & data to device flash')
@@ -978,8 +1019,8 @@ class HWSlotOpenAll(DeviceRequiredUnit):
lf_type = chameleon_cmd.TagSpecificType.TAG_TYPE_EM410X
# set all slot
for slot in range(1,9):
print(f' Slot{slot} setting...')
for slot in chameleon_cmd.SlotNumber:
print(f' Slot {slot} setting...')
# first to set tag type
self.cmd.set_slot_tag_type(slot, hf_type)
self.cmd.set_slot_tag_type(slot, lf_type)
@@ -988,11 +1029,11 @@ class HWSlotOpenAll(DeviceRequiredUnit):
self.cmd.set_slot_data_default(slot, lf_type)
# finally, we can enable this slot.
self.cmd.set_slot_enable(slot, True)
print(f' Open slot{slot} finish')
print(f' Slot {slot} setting done.')
# update config and save to flash
self.cmd.update_slot_data_config()
print(f' - Open all slot and set data to default success.')
print(f' - Succeeded opening all slots and setting data to default.')
@hw.command('dfu', 'Restart application to bootloader mode(Not yet implement dfu).')
@@ -1011,3 +1052,86 @@ class HWDFU(DeviceRequiredUnit):
print(" - Enter success @.@~")
# let time for comm thread to send dfu cmd and close port
time.sleep(0.1)
@hw_settings_animation.command('get', 'Get current animation mode value')
class HWSettingsAnimationGet(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
return None
def on_exec(self, args: argparse.Namespace):
resp: chameleon_com.Response = self.cmd.get_settings_animation()
if resp.data[0] == 0:
print("Full animation")
elif resp.data[0] == 1:
print("Minimal animation")
elif resp.data[0] == 2:
print("No animation")
else:
print("Unknown setting value, something failed.")
@hw_settings_animation.command('set', 'Change chameleon animation mode')
class HWSettingsAnimationSet(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
parser = ArgumentParserNoExit()
parser.add_argument('-m', '--mode', type=int, required=True, help="0 is full (default), 1 is minimal (only single pass on button wakeup), 2 is none", choices=[0, 1, 2])
return parser
def on_exec(self, args: argparse.Namespace):
mode = args.mode
self.cmd.set_settings_animation(mode)
print("Animation mode change success. Do not forget to store your settings in flash!")
@hw_settings.command('store', 'Store current settings to flash')
class HWSettingsStore(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
return None
def on_exec(self, args: argparse.Namespace):
print("Storing settings...")
resp: chameleon_com.Response = self.cmd.store_settings()
if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS:
print(" - Store success @.@~")
else:
print(" - Store failed")
@hw_settings.command('reset', 'Reset settings to default values')
class HWSettingsReset(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit or None:
return None
def on_exec(self, args: argparse.Namespace):
print("Initializing settings...")
resp: chameleon_com.Response = self.cmd.reset_settings()
if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS:
print(" - Reset success @.@~")
else:
print(" - Reset failed")
@hw.command('factory_reset', 'Wipe all data and return to factory settings')
class HWFactoryReset(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Permanently wipes Chameleon to factory settings. " \
"This will delete all your slot data and custom settings. " \
"There's no going back."
parser.add_argument(
"--i-know-what-im-doing",
default=False,
action="store_true",
help="Just to be sure :)"
)
return parser
def on_exec(self, args: argparse.Namespace):
if not args.i_know_what_im_doing:
print("This time your data's safe. Read the command documentation next time.")
return
resp = self.cmd.factory_reset()
if resp.status == chameleon_status.Device.STATUS_DEVICE_SUCCESS:
print(" - Reset successful! Please reconnect.")
print(" - A Serial Error below is normal, please ignore it")
else:
print(" - Reset failed!")
+133 -22
View File
@@ -21,6 +21,18 @@ DATA_CMD_ENTER_BOOTLOADER = 1010
DATA_CMD_GET_DEVICE_CHIP_ID = 1011
DATA_CMD_GET_DEVICE_ADDRESS = 1012
DATA_CMD_SAVE_SETTINGS = 1013
DATA_CMD_RESET_SETTINGS = 1014
DATA_CMD_SET_ANIMATION_MODE = 1015
DATA_CMD_GET_ANIMATION_MODE = 1016
DATA_CMD_GET_GIT_VERSION = 1017
DATA_CMD_GET_ACTIVE_SLOT = 1018
DATA_CMD_GET_SLOT_INFO = 1019
DATA_CMD_WIPE_FDS = 1020
DATA_CMD_SCAN_14A_TAG = 2000
DATA_CMD_MF1_SUPPORT_DETECT = 2001
DATA_CMD_MF1_NT_LEVEL_DETECT = 2002
@@ -43,6 +55,27 @@ DATA_CMD_SET_MF1_DETECTION_ENABLE = 5003
DATA_CMD_GET_MF1_DETECTION_COUNT = 5004
DATA_CMD_GET_MF1_DETECTION_RESULT = 5005
@enum.unique
class SlotNumber(enum.IntEnum):
SLOT_1 = 1,
SLOT_2 = 2,
SLOT_3 = 3,
SLOT_4 = 4,
SLOT_5 = 5,
SLOT_6 = 6,
SLOT_7 = 7,
SLOT_8 = 8,
@staticmethod
def to_fw(index: int): # can be int or SlotNumber
# SlotNumber() will raise error for us if index not in slot range
return SlotNumber(index).value - 1
@staticmethod
def from_fw(index: int):
# SlotNumber() will raise error for us if index not in fw range
return SlotNumber(index + 1)
@enum.unique
class TagSenseType(enum.IntEnum):
@@ -54,6 +87,20 @@ class TagSenseType(enum.IntEnum):
TAG_SENSE_HF = 2,
@staticmethod
def list(exclude_unknown=True):
enum_list = list(map(int, TagSenseType))
if exclude_unknown:
enum_list.remove(TagSenseType.TAG_SENSE_NO)
return enum_list
def __str__(self):
if self == TagSenseType.TAG_SENSE_LF:
return "LF"
elif self == TagSenseType.TAG_SENSE_HF:
return "HF"
return "None"
@enum.unique
class TagSpecificType(enum.IntEnum):
# 特定的且必须存在的标志不存在的类型
@@ -77,6 +124,25 @@ class TagSpecificType(enum.IntEnum):
enum_list.remove(TagSpecificType.TAG_TYPE_UNKNOWN)
return enum_list
def __str__(self):
if self == TagSpecificType.TAG_TYPE_EM410X:
return "EM410X"
elif self == TagSpecificType.TAG_TYPE_MIFARE_Mini:
return "Mifare Mini"
elif self == TagSpecificType.TAG_TYPE_MIFARE_1024:
return "Mifare Classic 1k"
elif self == TagSpecificType.TAG_TYPE_MIFARE_2048:
return "Mifare Classic 2k"
elif self == TagSpecificType.TAG_TYPE_MIFARE_4096:
return "Mifare Classic 4k"
elif self == TagSpecificType.TAG_TYPE_NTAG_213:
return "NTAG 213"
elif self == TagSpecificType.TAG_TYPE_NTAG_215:
return "NTAG 215"
elif self == TagSpecificType.TAG_TYPE_NTAG_216:
return "NTAG 216"
return "Unknown"
class ChameleonCMD:
"""
@@ -109,7 +175,10 @@ class ChameleonCMD:
"""
resp = self.device.send_cmd_sync(DATA_CMD_GET_DEVICE_ADDRESS, 0x00, None)
return resp.data[::-1].hex()
def get_git_version(self) -> str:
resp = self.device.send_cmd_sync(DATA_CMD_GET_GIT_VERSION, 0x00, None)
return resp.data.decode('utf-8')
def is_reader_device_mode(self) -> bool:
"""
@@ -280,21 +349,34 @@ class ChameleonCMD:
data.extend(key)
return self.device.send_cmd_sync(DATA_CMD_WRITE_EM410X_TO_T5577, 0x00, data)
def get_slot_info(self):
"""
Get slots info
:return:
"""
return self.device.send_cmd_sync(DATA_CMD_GET_SLOT_INFO, 0x00, None)
def get_active_slot(self):
"""
Get selected slot
:return:
"""
return self.device.send_cmd_sync(DATA_CMD_GET_ACTIVE_SLOT, 0x00, None)
@expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS)
def set_slot_activated(self, slot_index):
def set_slot_activated(self, slot_index: SlotNumber):
"""
设置当前激活使用的卡槽
:param slot_index: 卡槽索引,从 1 - 8(不是从0下标开始)
:return:
"""
if slot_index < 1 or slot_index > 8:
raise ValueError("The slot index range error(1-8)")
# SlotNumber() will raise error for us if slot_index not in slot range
data = bytearray()
data.append(slot_index - 1)
data.append(SlotNumber.to_fw(slot_index))
return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_ACTIVATED, 0x00, data)
@expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS)
def set_slot_tag_type(self, slot_index: int, tag_type: TagSpecificType):
def set_slot_tag_type(self, slot_index: SlotNumber, tag_type: TagSpecificType):
"""
设置当前卡槽的模拟卡的标签类型
注意:此操作并不会更改flash中的数据,flash中的数据的变动仅在下次保存时更新
@@ -302,15 +384,14 @@ class ChameleonCMD:
:param tag_type: 标签类型
:return:
"""
if slot_index < 1 or slot_index > 8:
raise ValueError("The slot index range error(1-8)")
# SlotNumber() will raise error for us if slot_index not in slot range
data = bytearray()
data.append(slot_index - 1)
data.append(SlotNumber.to_fw(slot_index))
data.append(tag_type)
return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_TAG_TYPE, 0x00, data)
@expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS)
def set_slot_data_default(self, slot_index: int, tag_type: TagSpecificType):
def set_slot_data_default(self, slot_index: SlotNumber, tag_type: TagSpecificType):
"""
设置指定卡槽的模拟卡的数据为缺省数据
注意:此API会将flash中的数据一并进行设置
@@ -318,25 +399,23 @@ class ChameleonCMD:
:param tag_type: 要设置的缺省标签类型
:return:
"""
if slot_index < 1 or slot_index > 8:
raise ValueError("The slot index range error(1-8)")
# SlotNumber() will raise error for us if slot_index not in slot range
data = bytearray()
data.append(slot_index - 1)
data.append(SlotNumber.to_fw(slot_index))
data.append(tag_type)
return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_DATA_DEFAULT, 0x00, data)
@expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS)
def set_slot_enable(self, slot_index: int, enable: bool):
def set_slot_enable(self, slot_index: SlotNumber, enable: bool):
"""
设置指定的卡槽是否使能
:param slot_index: 卡槽号码
:param enable: 是否使能
:return:
"""
if slot_index < 1 or slot_index > 8:
raise ValueError("The slot index range error(1-8)")
# SlotNumber() will raise error for us if slot_index not in slot range
data = bytearray()
data.append(slot_index - 1)
data.append(SlotNumber.to_fw(slot_index))
data.append(0x01 if enable else 0x00)
return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_ENABLE, 0X00, data)
@@ -409,7 +488,7 @@ class ChameleonCMD:
return self.device.send_cmd_sync(DATA_CMD_SET_MF1_ANTI_COLLISION_RES, 0X00, data)
@expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS)
def set_slot_tag_nick_name(self, slot: int, sense_type: int, name: str):
def set_slot_tag_nick_name(self, slot: SlotNumber, sense_type: TagSenseType, name: bytes):
"""
设置MF1的模拟卡的防冲撞资源信息
:param slot: 卡槽号码
@@ -417,13 +496,14 @@ class ChameleonCMD:
:param name: 卡槽昵称
:return:
"""
# SlotNumber() will raise error for us if slot not in slot range
data = bytearray()
data.extend([slot, sense_type])
data.extend(name.encode(encoding="gbk"))
data.extend([SlotNumber.to_fw(slot), sense_type])
data.extend(name)
return self.device.send_cmd_sync(DATA_CMD_SET_SLOT_TAG_NICK, 0x00, data)
@expect_response(chameleon_status.Device.STATUS_DEVICE_SUCCESS)
def get_slot_tag_nick_name(self, slot: int, sense_type: int):
def get_slot_tag_nick_name(self, slot: SlotNumber, sense_type: TagSenseType):
"""
设置MF1的模拟卡的防冲撞资源信息
:param slot: 卡槽号码
@@ -431,8 +511,9 @@ class ChameleonCMD:
:param name: 卡槽昵称
:return:
"""
# SlotNumber() will raise error for us if slot not in slot range
data = bytearray()
data.extend([slot, sense_type])
data.extend([SlotNumber.to_fw(slot), sense_type])
return self.device.send_cmd_sync(DATA_CMD_GET_SLOT_TAG_NICK, 0x00, data)
def update_slot_data_config(self):
@@ -448,6 +529,36 @@ class ChameleonCMD:
:return:
"""
return self.device.send_cmd_auto(DATA_CMD_ENTER_BOOTLOADER, 0x00, close=True)
def get_settings_animation(self):
"""
Get animation mode value
"""
return self.device.send_cmd_sync(DATA_CMD_GET_ANIMATION_MODE, 0x00, None)
def set_settings_animation(self, value: int):
"""
Set animation mode value
"""
return self.device.send_cmd_sync(DATA_CMD_SET_ANIMATION_MODE, 0x00, bytearray([value]))
def reset_settings(self):
"""
Reset settings stored in flash memory
"""
return self.device.send_cmd_sync(DATA_CMD_RESET_SETTINGS, 0x00)
def store_settings(self):
"""
Store settings to flash memory
"""
return self.device.send_cmd_sync(DATA_CMD_SAVE_SETTINGS, 0x00)
def factory_reset(self):
"""
Reset to factory settings
"""
return self.device.send_cmd_sync(DATA_CMD_WIPE_FDS, 0x00)
if __name__ == '__main__':
+4
View File
@@ -41,6 +41,8 @@ class Device(metaclass=MetaDevice):
STATUS_INVALID_CMD = 0x67 # 无效的指令
STATUS_DEVICE_SUCCESS = 0x68 # 设备相关操作成功执行
STATUS_NOT_IMPLEMENTED = 0x69 # 调用了某些未实现的操作,属于开发者遗漏的错误
STATUS_FLASH_WRITE_FAIL = 0x70 # flash写入失败
STATUS_FLASH_READ_FAIL = 0x71 # flash读取失败
message = {
@@ -68,4 +70,6 @@ message = {
Device.STATUS_INVALID_CMD : "API request fail, cmd invalid",
Device.STATUS_DEVICE_SUCCESS : "Device operation succeeded",
Device.STATUS_NOT_IMPLEMENTED : "Some api not implemented",
Device.STATUS_FLASH_WRITE_FAIL : "Flash write failed",
Device.STATUS_FLASH_READ_FAIL : "Flash read failed"
}