mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-05-12 11:22:59 -07:00
@@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file.
|
||||
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
|
||||
|
||||
## [unreleased][unreleased]
|
||||
- Added colors to CLI help (@doegox)
|
||||
- Changed massively CLI, cf https://github.com/RfidResearchGroup/ChameleonUltra/issues/164#issue-1930580576 (@doegox)
|
||||
- Changed CLI help: lists display and now all commands support `-h` (@doegox)
|
||||
- Added button action to show battery level (@doegox)
|
||||
- Added GUI Page docs (@GameTec-live)
|
||||
|
||||
+6
-6
@@ -241,7 +241,7 @@ Notes:
|
||||
* Response: 4+N*8 bytes: `uid[4]` followed by N tuples of `nt[4]|nt_enc[4]`. All values as U32.
|
||||
* CLI: cf `hf mf nested` on static nonce tag
|
||||
### 2004: MF1_DARKSIDE_ACQUIRE
|
||||
* Command: 4 bytes: `type_target|block_target|first_recover|sync_max`
|
||||
* Command: 4 bytes: `type_target|block_target|first_recover|sync_max`. Type=0x60 for key A, 0x61 for key B.
|
||||
* Response: 1 byte if Darkside failed, according to `mf1_darkside_status_t` enum,
|
||||
else 33 bytes `darkside_status|uid[4]|nt1[4]|par[8]|ks1[8]|nr[4]|ar[4]`
|
||||
* `darkside_status`
|
||||
@@ -253,29 +253,29 @@ Notes:
|
||||
* `ar[4]` U32
|
||||
* CLI: cf `hf mf darkside`
|
||||
### 2005: MF1_DETECT_NT_DIST
|
||||
* Command: 8 bytes: `type_known|block_known|key_known[6]`. Key as 6 bytes.
|
||||
* Command: 8 bytes: `type_known|block_known|key_known[6]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.
|
||||
* Response: 8 bytes: `uid[4]|dist[4]`
|
||||
* `uid[4]` U32 (format expected by `nested` tool)
|
||||
* `dist[4]` U32
|
||||
* CLI: cf `hf mf nested`
|
||||
### 2006: MF1_NESTED_ACQUIRE
|
||||
* Command: 10 bytes: `type_known|block_known|key_known[6]|type_target|block_target`. Key as 6 bytes.
|
||||
* Command: 10 bytes: `type_known|block_known|key_known[6]|type_target|block_target`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.
|
||||
* Response: N*9 bytes: N tuples of `nt[4]|nt_enc[4]|par`
|
||||
* `nt[4]` U32
|
||||
* `nt_enc[4]` U32
|
||||
* `par`
|
||||
* CLI: cf `hf mf nested`
|
||||
### 2007: MF1_AUTH_ONE_KEY_BLOCK
|
||||
* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes.
|
||||
* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.
|
||||
* Response: no data
|
||||
* Status will be `HF_TAG_OK` if auth succeeded, else `MF_ERR_AUTH`
|
||||
* CLI: cf `hf mf nested`
|
||||
### 2008: MF1_READ_ONE_BLOCK
|
||||
* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes.
|
||||
* Command: 8 bytes: `type|block|key[6]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.
|
||||
* Response: 16 bytes: `block_data[16]`
|
||||
* CLI: cf `hf mf rdbl`
|
||||
### 2009: MF1_WRITE_ONE_BLOCK
|
||||
* Command: 24 bytes: `type|block|key[6]|block_data[16]`. Key as 6 bytes.
|
||||
* Command: 24 bytes: `type|block|key[6]|block_data[16]`. Key as 6 bytes. Type=0x60 for key A, 0x61 for key B.
|
||||
* Response: no data
|
||||
* CLI: cf `hf mf wrbl`
|
||||
### 2010: HF14A_RAW
|
||||
|
||||
@@ -32,7 +32,7 @@ APP_FW_VER_MAJOR := $(word 1,$(subst ., ,$(APP_FW_SEMVER)))
|
||||
APP_FW_VER_MINOR := $(word 2,$(subst ., ,$(APP_FW_SEMVER)))
|
||||
|
||||
# Enable NRF_LOG on SWO pin as UART TX
|
||||
NRF_LOG_UART_ON_SWO_ENABLED := 0
|
||||
NRF_LOG_UART_ON_SWO_ENABLED := 1
|
||||
|
||||
# Enable SDK validation checks
|
||||
SDK_VALIDATION := 0
|
||||
|
||||
@@ -6,18 +6,18 @@ import chameleon_com
|
||||
import colorama
|
||||
import chameleon_cli_unit
|
||||
import chameleon_utils
|
||||
import os
|
||||
import pathlib
|
||||
import prompt_toolkit
|
||||
from datetime import datetime
|
||||
from prompt_toolkit.formatted_text import ANSI
|
||||
from prompt_toolkit.history import FileHistory
|
||||
|
||||
# Colorama shorthands
|
||||
CR = colorama.Fore.RED
|
||||
CG = colorama.Fore.GREEN
|
||||
CB = colorama.Fore.BLUE
|
||||
CC = colorama.Fore.CYAN
|
||||
CY = colorama.Fore.YELLOW
|
||||
CM = colorama.Fore.MAGENTA
|
||||
C0 = colorama.Style.RESET_ALL
|
||||
|
||||
ULTRA = r"""
|
||||
@@ -43,43 +43,13 @@ BANNER = """
|
||||
"""
|
||||
|
||||
|
||||
def dump_help(cmd_node, depth=0, dump_cmd_groups=False, dump_description=False):
|
||||
if cmd_node.cls:
|
||||
cmd_title = f"{CG}{cmd_node.fullname}{C0}"
|
||||
if dump_description:
|
||||
print(f" {cmd_title}".ljust(37) + f"{cmd_node.help_text}")
|
||||
else:
|
||||
print(f" {cmd_title}".ljust(37), end="")
|
||||
p = cmd_node.cls().args_parser()
|
||||
assert p is not None
|
||||
p.prog = ""
|
||||
usage = p.format_usage().removeprefix("usage: ").rstrip()
|
||||
if usage != "[-h]":
|
||||
usage = usage.removeprefix("[-h] ")
|
||||
if dump_description:
|
||||
print(f"{CG}{C0}".ljust(37), end="")
|
||||
print(f"{CY}{usage}{C0}")
|
||||
else:
|
||||
print("")
|
||||
else:
|
||||
if dump_cmd_groups:
|
||||
cmd_title = f"{CY}{cmd_node.fullname}{C0}"
|
||||
if dump_description:
|
||||
print(f" {cmd_title}".ljust(37) + f"{{ {cmd_node.help_text}... }}")
|
||||
else:
|
||||
print(f" {cmd_title}")
|
||||
for child in cmd_node.children:
|
||||
dump_help(child, depth + 1, dump_cmd_groups, dump_description)
|
||||
|
||||
|
||||
class ChameleonCLI:
|
||||
"""
|
||||
CLI for chameleon
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.completer = chameleon_utils.CustomNestedCompleter.from_nested_dict(
|
||||
chameleon_cli_unit.root_commands)
|
||||
self.completer = chameleon_utils.CustomNestedCompleter.from_clitree(chameleon_cli_unit.root)
|
||||
self.session = prompt_toolkit.PromptSession(completer=self.completer,
|
||||
history=FileHistory(pathlib.Path.home() / ".chameleon_history"))
|
||||
|
||||
@@ -132,7 +102,6 @@ class ChameleonCLI:
|
||||
raise Exception("This script requires at least Python 3.9")
|
||||
|
||||
self.print_banner()
|
||||
closing = False
|
||||
cmd_strs = []
|
||||
while True:
|
||||
if cmd_strs:
|
||||
@@ -145,64 +114,34 @@ class ChameleonCLI:
|
||||
cmd_strs = cmd_str.replace(
|
||||
"\r\n", "\n").replace("\r", "\n").split("\n")
|
||||
cmd_str = cmd_strs.pop(0)
|
||||
if cmd_str == "":
|
||||
continue
|
||||
except EOFError:
|
||||
closing = True
|
||||
cmd_str = 'exit'
|
||||
except KeyboardInterrupt:
|
||||
closing = True
|
||||
cmd_str = 'exit'
|
||||
|
||||
if closing or cmd_str in ["exit", "quit", "q", "e"]:
|
||||
print("Bye, thank you. ^.^ ")
|
||||
self.device_com.close()
|
||||
sys.exit(996)
|
||||
elif cmd_str == "clear":
|
||||
os.system('clear' if os.name == 'posix' else 'cls')
|
||||
continue
|
||||
elif cmd_str == "dumphelp":
|
||||
for _, cmd_node in chameleon_cli_unit.root_commands.items():
|
||||
dump_help(cmd_node)
|
||||
continue
|
||||
elif cmd_str == "":
|
||||
continue
|
||||
# look for alternate exit
|
||||
if cmd_str in ["quit", "q", "e"]:
|
||||
cmd_str = 'exit'
|
||||
|
||||
# look for alternate comments
|
||||
if cmd_str[0] in ";#%":
|
||||
cmd_str = 'rem ' + cmd_str[1:].lstrip()
|
||||
|
||||
# parse cmd
|
||||
argv = cmd_str.split()
|
||||
root_cmd = argv[0]
|
||||
# look for comments
|
||||
if root_cmd == "rem" or root_cmd[0] in ";#%":
|
||||
# precision: second
|
||||
# iso_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
# precision: nanosecond (note that the comment will take some time too, ~75ns, check your system)
|
||||
iso_timestamp = datetime.utcnow().isoformat() + 'Z'
|
||||
if root_cmd[0] in ";#%":
|
||||
comment = ' '.join([root_cmd[1:]]+argv[1:]).strip()
|
||||
else:
|
||||
comment = ' '.join(argv[1:]).strip()
|
||||
print(f"{iso_timestamp} remark: {comment}")
|
||||
continue
|
||||
if root_cmd not in chameleon_cli_unit.root_commands:
|
||||
# No matching command group
|
||||
print("".ljust(18, "-") + "".ljust(10) + "".ljust(30, "-"))
|
||||
for cmd_name, cmd_node in chameleon_cli_unit.root_commands.items():
|
||||
print(f" - {CG}{cmd_name}{C0}".ljust(37) + f"{{ {cmd_node.help_text}... }}")
|
||||
print(f" - {CG}clear{C0}".ljust(37) + "Clear screen")
|
||||
print(f" - {CG}exit{C0}".ljust(37) + "Exit program")
|
||||
print(f" - {CG}rem ...{C0}".ljust(37) + "Display a comment with a timestamp")
|
||||
continue
|
||||
|
||||
tree_node, arg_list = self.get_cmd_node(
|
||||
chameleon_cli_unit.root_commands[root_cmd], argv[1:])
|
||||
|
||||
tree_node, arg_list = self.get_cmd_node(chameleon_cli_unit.root, argv)
|
||||
if not tree_node.cls:
|
||||
# Found tree node is a group without an implementation, print children
|
||||
print("".ljust(18, "-") + "".ljust(10) + "".ljust(30, "-"))
|
||||
for child in tree_node.children:
|
||||
cmd_title = f"{CG}{child.name}{C0}"
|
||||
if not child.cls:
|
||||
help_line = (f" - {cmd_title}".ljust(37)
|
||||
) + f"{{ {child.help_text}... }}"
|
||||
help_line = (f" - {cmd_title}".ljust(37)) + f"{{ {child.help_text}... }}"
|
||||
else:
|
||||
help_line = (f" - {cmd_title}".ljust(37)
|
||||
) + f"{child.help_text}"
|
||||
help_line = (f" - {cmd_title}".ljust(37)) + f"{child.help_text}"
|
||||
print(help_line)
|
||||
continue
|
||||
|
||||
@@ -216,8 +155,8 @@ class ChameleonCLI:
|
||||
try:
|
||||
args_parse_result = args.parse_args(arg_list)
|
||||
except chameleon_utils.ArgsParserError as e:
|
||||
args.print_usage()
|
||||
print(str(e).strip(), end="\n\n")
|
||||
args.print_help()
|
||||
print(f'{CY}'+str(e).strip()+f'{C0}', end="\n\n")
|
||||
continue
|
||||
except chameleon_utils.ParserExitIntercept:
|
||||
# don't exit process.
|
||||
@@ -227,8 +166,16 @@ class ChameleonCLI:
|
||||
if not unit.before_exec(args_parse_result):
|
||||
continue
|
||||
|
||||
# start process cmd
|
||||
unit.on_exec(args_parse_result)
|
||||
# start process cmd, delay error to call after_exec firstly
|
||||
error = None
|
||||
try:
|
||||
unit.on_exec(args_parse_result)
|
||||
except Exception as e:
|
||||
error = e
|
||||
unit.after_exec(args_parse_result)
|
||||
if error is not None:
|
||||
raise error
|
||||
|
||||
except (chameleon_utils.UnexpectedResponseError, chameleon_utils.ArgsParserError) as e:
|
||||
print(f"{CR}{str(e)}{C0}")
|
||||
except Exception:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+223
-267
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import colorama
|
||||
from functools import wraps
|
||||
from typing import Union
|
||||
from prompt_toolkit.completion import Completer, NestedCompleter, WordCompleter
|
||||
@@ -7,6 +8,15 @@ from prompt_toolkit.document import Document
|
||||
|
||||
import chameleon_status
|
||||
|
||||
# Colorama shorthands
|
||||
CR = colorama.Fore.RED
|
||||
CG = colorama.Fore.GREEN
|
||||
CB = colorama.Fore.BLUE
|
||||
CC = colorama.Fore.CYAN
|
||||
CY = colorama.Fore.YELLOW
|
||||
CM = colorama.Fore.MAGENTA
|
||||
C0 = colorama.Style.RESET_ALL
|
||||
|
||||
|
||||
class ArgsParserError(Exception):
|
||||
pass
|
||||
@@ -41,6 +51,51 @@ class ArgumentParserNoExit(argparse.ArgumentParser):
|
||||
args = {'prog': self.prog, 'message': message}
|
||||
raise ArgsParserError('%(prog)s: error: %(message)s\n' % args)
|
||||
|
||||
def print_help(self):
|
||||
"""
|
||||
Colorize argparse help
|
||||
"""
|
||||
print("-" * 80)
|
||||
print(f"{CR}{self.prog}{C0}\n")
|
||||
lines = self.format_help().splitlines()
|
||||
usage = lines[:lines.index('')]
|
||||
assert usage[0].startswith('usage:')
|
||||
usage[0] = usage[0].replace('usage:', f'{CG}usage:{C0}\n ')
|
||||
usage[0] = usage[0].replace(self.prog, f'{CR}{self.prog}{C0}')
|
||||
usage = [usage[0]] + [x[4:] for x in usage[1:]] + ['']
|
||||
lines = lines[lines.index('')+1:]
|
||||
desc = lines[:lines.index('')]
|
||||
print(f'{CC}'+'\n'.join(desc)+f'{C0}\n')
|
||||
print('\n'.join(usage))
|
||||
lines = lines[lines.index('')+1:]
|
||||
if '' in lines:
|
||||
options = lines[:lines.index('')]
|
||||
lines = lines[lines.index('')+1:]
|
||||
else:
|
||||
options = lines
|
||||
lines = []
|
||||
if len(options) > 0 and options[0].strip() == 'positional arguments:':
|
||||
positional_args = options
|
||||
positional_args[0] = positional_args[0].replace('positional arguments:', f'{CG}positional arguments:{C0}')
|
||||
if len(positional_args) > 1:
|
||||
positional_args.append('')
|
||||
print('\n'.join(positional_args))
|
||||
if '' in lines:
|
||||
options = lines[:lines.index('')]
|
||||
lines = lines[lines.index('')+1:]
|
||||
else:
|
||||
options = lines
|
||||
lines = []
|
||||
if len(options) > 0:
|
||||
assert options[0].strip() == 'options:'
|
||||
options[0] = options[0].replace('options:', f'{CG}options:{C0}')
|
||||
if len(options) > 1:
|
||||
options.append('')
|
||||
print('\n'.join(options))
|
||||
if len(lines) > 0:
|
||||
lines[0] = f'{CG}{lines[0]}{C0}'
|
||||
print('\n'.join(lines))
|
||||
|
||||
|
||||
def expect_response(accepted_responses: Union[int, list[int]]):
|
||||
"""
|
||||
@@ -79,13 +134,14 @@ class CLITree:
|
||||
:param cls: A BaseCLIUnit instance handling the command
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, help_text=None, fullname=None, children=None, cls=None) -> None:
|
||||
def __init__(self, name=None, help_text=None, fullname=None, children=None, cls=None, root=False) -> None:
|
||||
self.name: str = name
|
||||
self.help_text: str = help_text
|
||||
self.fullname: str = fullname if fullname else name
|
||||
self.children: list[CLITree] = children if children else list()
|
||||
self.cls = cls
|
||||
if self.help_text is None:
|
||||
self.root = root
|
||||
if self.help_text is None and not root:
|
||||
assert self.cls is not None
|
||||
parser = self.cls().args_parser()
|
||||
assert parser is not None
|
||||
@@ -99,7 +155,9 @@ class CLITree:
|
||||
:param help_text: Hint displayed for the group
|
||||
"""
|
||||
child = CLITree(
|
||||
name=name, fullname=f'{self.fullname} {name}', help_text=help_text)
|
||||
name=name,
|
||||
fullname=f'{self.fullname} {name}' if not self.root else f'{name}',
|
||||
help_text=help_text)
|
||||
self.children.append(child)
|
||||
return child
|
||||
|
||||
@@ -110,8 +168,10 @@ class CLITree:
|
||||
:param name: Name of the command
|
||||
"""
|
||||
def decorator(cls):
|
||||
self.children.append(
|
||||
CLITree(name=name, fullname=f'{self.fullname} {name}', cls=cls))
|
||||
self.children.append(CLITree(
|
||||
name=name,
|
||||
fullname=f'{self.fullname} {name}' if not self.root else f'{name}',
|
||||
cls=cls))
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
@@ -132,32 +192,6 @@ class CustomNestedCompleter(NestedCompleter):
|
||||
def __repr__(self) -> str:
|
||||
return f"CustomNestedCompleter({self.options!r}, ignore_case={self.ignore_case!r})"
|
||||
|
||||
@classmethod
|
||||
def from_nested_dict(cls, data):
|
||||
options = {}
|
||||
meta_dict = {}
|
||||
for key, value in data.items():
|
||||
if isinstance(value, Completer):
|
||||
options[key] = value
|
||||
elif isinstance(value, dict):
|
||||
options[key] = cls.from_nested_dict(value)
|
||||
elif isinstance(value, set):
|
||||
options[key] = cls.from_nested_dict(
|
||||
{item: None for item in value})
|
||||
elif isinstance(value, CLITree):
|
||||
if value.cls:
|
||||
# CLITree is a standalone command
|
||||
options[key] = ArgparseCompleter(value.cls().args_parser())
|
||||
else:
|
||||
# CLITree is a command group
|
||||
options[key] = cls.from_clitree(value)
|
||||
meta_dict[key] = value.help_text
|
||||
else:
|
||||
assert value is None
|
||||
options[key] = None
|
||||
|
||||
return cls(options, meta_dict=meta_dict)
|
||||
|
||||
@classmethod
|
||||
def from_clitree(cls, node):
|
||||
options = {}
|
||||
|
||||
Reference in New Issue
Block a user