diff --git a/hxtool/cli/__init__.py b/hxtool/cli/__init__.py index f6ad6ef..e2b8b6b 100644 --- a/hxtool/cli/__init__.py +++ b/hxtool/cli/__init__.py @@ -8,6 +8,7 @@ from . import id from . import info from . import nmea +from . import poke __all__ = [ "run", @@ -17,5 +18,6 @@ "gpslog", "id", "info", - "nmea" + "nmea", + "poke", ] diff --git a/hxtool/cli/poke.py b/hxtool/cli/poke.py new file mode 100644 index 0000000..ae68528 --- /dev/null +++ b/hxtool/cli/poke.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +from argparse import ArgumentError, ArgumentTypeError +from binascii import hexlify, unhexlify +from logging import getLogger +import os +import re + +import hxtool +from .base import CliCommand + +logger = getLogger(__name__) + + +def default_length(): + try: + length = os.environ.get("HXTOOL_PEEK_LENGTH", "1") + return hexadecimal_number(length) + except (ArgumentTypeError, ValueError): + return 1 + + +def hexadecimal_number(string): + number = int(string, 16) + if number < 0: + raise ArgumentTypeError("must be positive") + return number + + +def hexadecimal_data(string): + return unhexlify(bytes(re.sub(r"\A0x", "", string), 'utf-8')) + + +class PokeCommand(CliCommand): + + name = "poke" + help = "Read/write hex bytes in the device memory (advanced feature, use caution)" + + @staticmethod + def setup_args(parser) -> None: + + parser.add_argument('offset', + type=hexadecimal_number, + help="hex address to poke/peek") + + parser.add_argument('data', + nargs='?', + type=hexadecimal_data, + help="data to poke (when omitted: peek, don't poke)") + + parser.add_argument("-l", "--length", + type=hexadecimal_number, + help="hex number of bytes to poke/peek") + + def run(self): + hx = hxtool.get(self.args) + if hx is None: + return 10 + + if not hx.comm.cp_mode: + logger.critical("Handset not in CP mode (MENU + ON)") + return 11 + + offset = self.args.offset + data = self.args.data + length = self.args.length + + if data is None: + # peek + if length is None: + length = default_length() + else: + # poke + if length is None: + length = len(data) + elif len(data) > length: + data = data[0:length] + logger.warning(f"Truncating data to " + f"{'0x%x' % length} byte{'s' if length != 1 else ''}") + elif len(data) < length: + raise ArgumentError(None, f"Data to poke is shorter than {'0x%x' % length} bytes") + + if length > hx.config.CHUNK_SIZE: + raise ArgumentError(None, + f"Can't {'peek' if data is None else 'poke'} " + f"more than {'0x%X' % hx.config.CHUNK_SIZE} bytes at once " + f"on {type(hx).__name__}") + if offset + length > hx.config.CONFIG_SIZE: + raise ArgumentError(None, + f"Can't {'peek' if data is None else 'poke'} " + f"past the end of the {type(hx).__name__}'s memory " + f"at offset {'0x%X' % hx.config.CONFIG_SIZE}") + + hx.comm.sync() + + if data is None: + print(hexlify(hx.comm.read_config_memory(offset, length)).decode()) + else: + logger.info(f"Writing {hexlify(data)} to device at offset {'0x%04x' % offset}") + hx.comm.write_config_memory(offset, data) + + logger.info("Operation successful") + return 0 diff --git a/tests/poke_test.py b/tests/poke_test.py new file mode 100644 index 0000000..9b07784 --- /dev/null +++ b/tests/poke_test.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +import argparse +import pytest + +from hxtool.main import main + + +def test_hxtool_peek(capsys): + ret = main("--simulator -t 0 poke 0100 --length 0x6".split()) + assert ret == 0 + + outerr = capsys.readouterr() + assert "Operation successful" in outerr.err + assert "414d3035374e\n" == outerr.out, "read HX870 flash id" + + +def test_hxtool_peek_no_length(capsys): + ret = main("--simulator -t 0 poke 1c".split()) + assert ret == 0 + + outerr = capsys.readouterr() + assert "Operation successful" in outerr.err + assert "ff\n" == outerr.out, "peek length defaults to 1" + + +def test_hxtool_poke(capsys): + ret = main("--debug --simulator -t 0 poke 1234 aBCd".split()) + assert ret == 0 + + outerr = capsys.readouterr() + assert "Operation successful" in outerr.err + assert """CP simulator processing message b'#CEPWR\\t1234\\t02\\tABCD\\t72\\r\\n'""" in outerr.err + + +def test_hxtool_poke_truncate(capsys): + ret = main("--debug --simulator -t 0 poke 10 0x01020304 -l 1".split()) + assert ret == 0 + + outerr = capsys.readouterr() + assert "Truncating data to 0x1 byte" in outerr.err + assert "Operation successful" in outerr.err + assert """CP simulator processing message b'#CEPWR\\t0010\\t01\\t01\\t71\\r\\n'""" in outerr.err + + +def test_hxtool_peek_too_long_data(): + with pytest.raises(argparse.ArgumentError): + main("--simulator -t 0 poke 005a -l 41".split()) + + +def test_hxtool_poke_too_short_data(): + with pytest.raises(argparse.ArgumentError): + main("--simulator -t 0 poke 12 00 -l 2".split()) + + +def test_hxtool_poke_too_high_address(): + with pytest.raises(argparse.ArgumentError): + main("--simulator -t 0 poke 7fff 0100".split())