mirror of
https://github.com/google/pebble.git
synced 2025-05-17 17:04:53 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
89
python_libs/pebble-commander/.gitignore
vendored
Normal file
89
python_libs/pebble-commander/.gitignore
vendored
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
2
python_libs/pebble-commander/README.rst
Normal file
2
python_libs/pebble-commander/README.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
Pebble Commander
|
||||
================
|
15
python_libs/pebble-commander/pebble/__init__.py
Normal file
15
python_libs/pebble-commander/pebble/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
16
python_libs/pebble-commander/pebble/commander/__init__.py
Normal file
16
python_libs/pebble-commander/pebble/commander/__init__.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .commander import PebbleCommander
|
||||
from . import _commands
|
67
python_libs/pebble-commander/pebble/commander/__main__.py
Normal file
67
python_libs/pebble-commander/pebble/commander/__main__.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from . import interactive
|
||||
|
||||
def main(args=None):
|
||||
def reattach_handler(logger, formatter, handler):
|
||||
if handler is not None:
|
||||
logger.removeHandler(handler)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
return handler
|
||||
|
||||
if args is None:
|
||||
parser = argparse.ArgumentParser(description='Pebble Commander.')
|
||||
parser.add_argument('-v', '--verbose', help='verbose logging', action='count',
|
||||
default=0)
|
||||
parser.add_argument('-t', '--tty', help='serial port (defaults to auto-detect)', metavar='TTY',
|
||||
default=None)
|
||||
parser.add_argument('-c', '--pcap', metavar='FILE', default=None,
|
||||
help='write packet capture to pcap file')
|
||||
parser.add_argument('dict', help='log-hashing dictionary file', metavar='loghash_dict.json',
|
||||
nargs='?', default=None)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
log_level = (logging.DEBUG if args.verbose >= 2
|
||||
else logging.INFO if args.verbose >= 1
|
||||
else logging.WARNING)
|
||||
|
||||
use_colors = True
|
||||
formatter_string = '%(name)-12s: %(levelname)-8s %(message)s'
|
||||
if use_colors:
|
||||
formatter_string = '\x1b[33m%s\x1b[m' % formatter_string
|
||||
|
||||
formatter = logging.Formatter(formatter_string)
|
||||
handler = reattach_handler(logging.getLogger(), formatter, None)
|
||||
logging.getLogger().setLevel(log_level)
|
||||
|
||||
with interactive.InteractivePebbleCommander(
|
||||
loghash_path=args.dict, tty=args.tty, capfile=args.pcap) as cmdr:
|
||||
cmdr.attach_prompt_toolkit()
|
||||
# Re-create the logging handler to use the patched stdout
|
||||
handler = reattach_handler(logging.getLogger(), formatter, handler)
|
||||
cmdr.command_loop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import app
|
||||
from . import battery
|
||||
from . import bluetooth
|
||||
from . import clicks
|
||||
from . import flash
|
||||
from . import help
|
||||
from . import imaging
|
||||
from . import misc
|
||||
from . import pfs
|
||||
from . import resets
|
||||
from . import system
|
||||
from . import time
|
||||
from . import windows
|
|
@ -0,0 +1,85 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_list(cmdr):
|
||||
""" List applications.
|
||||
"""
|
||||
return cmdr.send_prompt_command("app list")
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_load_metadata(cmdr):
|
||||
""" Ghetto metadata loading for pbw_image.py
|
||||
"""
|
||||
ret = cmdr.send_prompt_command("app load metadata")
|
||||
if not ret[0].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_launch(cmdr, idnum):
|
||||
""" Launch an application.
|
||||
"""
|
||||
idnum = int(str(idnum), 0)
|
||||
if idnum == 0:
|
||||
raise exceptions.ParameterError('idnum out of range: %d' % idnum)
|
||||
ret = cmdr.send_prompt_command("app launch %d" % idnum)
|
||||
if not ret[0].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_remove(cmdr, idnum):
|
||||
""" Remove an application.
|
||||
"""
|
||||
idnum = int(str(idnum), 0)
|
||||
if idnum == 0:
|
||||
raise exceptions.ParameterError('idnum out of range: %d' % idnum)
|
||||
ret = cmdr.send_prompt_command("app remove %d" % idnum)
|
||||
if not ret[0].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_resource_bank(cmdr, idnum=0):
|
||||
""" Get resource bank info for an application.
|
||||
"""
|
||||
idnum = int(str(idnum), 0)
|
||||
if idnum < 0:
|
||||
raise exceptions.ParameterError('idnum out of range: %d' % idnum)
|
||||
ret = cmdr.send_prompt_command("resource bank info %d" % idnum)
|
||||
if not ret[0].startswith("OK "):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
return [ret[0][3:]]
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_next_id(cmdr):
|
||||
""" Get next free application ID.
|
||||
"""
|
||||
return cmdr.send_prompt_command("app next id")
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def app_available(cmdr, idnum):
|
||||
""" Check if an application is available.
|
||||
"""
|
||||
idnum = int(str(idnum), 0)
|
||||
if idnum == 0:
|
||||
raise exceptions.ParameterError('idnum out of range: %d' % idnum)
|
||||
return cmdr.send_prompt_command("app available %d" % idnum)
|
|
@ -0,0 +1,35 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def battery_force_charge(cmdr, charging=True):
|
||||
""" Force the device to believe it is or isn't charging.
|
||||
"""
|
||||
if parsers.str2bool(charging):
|
||||
charging = "enable"
|
||||
else:
|
||||
charging = "disable"
|
||||
ret = cmdr.send_prompt_command("battery chargeopt %s" % charging)
|
||||
if ret:
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def battery_status(cmdr):
|
||||
""" Get current battery status.
|
||||
"""
|
||||
return cmdr.send_prompt_command("battery status")
|
|
@ -0,0 +1,82 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def bt_airplane_mode(cmdr, enter=True):
|
||||
""" Enter or exit airplane mode.
|
||||
|
||||
`enter` should either be a boolean, "enter", or "exit".
|
||||
"""
|
||||
if parsers.str2bool(enter, also_true=["enter"], also_false=["exit"]):
|
||||
enter = "enter"
|
||||
else:
|
||||
enter = "exit"
|
||||
|
||||
ret = cmdr.send_prompt_command("bt airplane mode %s" % enter)
|
||||
if ret:
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def bt_prefs_wipe(cmdr):
|
||||
""" Wipe bluetooth preferences.
|
||||
"""
|
||||
ret = cmdr.send_prompt_command("bt prefs wipe")
|
||||
if ret:
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def bt_mac(cmdr):
|
||||
""" Get the bluetooth MAC address.
|
||||
"""
|
||||
ret = cmdr.send_prompt_command("bt mac")
|
||||
if not ret[0].startswith("0x"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
retstr = ret[0][2:]
|
||||
return [':'.join(retstr[i:i+2] for i in range(0, len(retstr), 2))]
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def bt_set_addr(cmdr, new_mac=None):
|
||||
""" Set the bluetooth MAC address.
|
||||
|
||||
Don't specify `new_mac` to revert to default.
|
||||
`new_mac` should be of the normal 6 hex octets split with colons.
|
||||
"""
|
||||
if not new_mac:
|
||||
new_mac = "00:00:00:00:00:00"
|
||||
mac = parsers.str2mac(new_mac)
|
||||
macstr = ''.join(["%02X" % byte for byte in mac])
|
||||
ret = cmdr.send_prompt_command("bt set addr %s" % macstr)
|
||||
if ret[0] != new_mac:
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def bt_set_name(cmdr, new_name=None):
|
||||
""" Set the bluetooth name.
|
||||
"""
|
||||
if not new_name:
|
||||
new_name = ""
|
||||
# Note: the only reason for this is because prompt sucks
|
||||
# This can probably be removed when prompt goes away
|
||||
if ' ' in new_name:
|
||||
raise exceptions.ParameterError("bluetooth name must not have spaces")
|
||||
ret = cmdr.send_prompt_command("bt set name %s" % new_name)
|
||||
if ret:
|
||||
raise exceptions.PromptResponseError(ret)
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def click_short(cmdr, button):
|
||||
""" Click a button.
|
||||
"""
|
||||
button = int(str(button), 0)
|
||||
if not 0 <= button <= 3:
|
||||
raise exceptions.ParameterError('button out of range: %d' % button)
|
||||
ret = cmdr.send_prompt_command("click short %d" % button)
|
||||
if not ret[0].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def click_long(cmdr, button, hold_ms=20):
|
||||
""" Hold a button.
|
||||
|
||||
`hold_ms` is how many ms to hold the button down before releasing.
|
||||
"""
|
||||
return cmdr.click_multiple(button, hold_ms=hold_ms)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def click_multiple(cmdr, button, count=1, hold_ms=20, delay_ms=0):
|
||||
""" Rhythmically click a button.
|
||||
"""
|
||||
button = int(str(button), 0)
|
||||
count = int(str(count), 0)
|
||||
hold_ms = int(str(hold_ms), 0)
|
||||
delay_ms = int(str(delay_ms), 0)
|
||||
if not 0 <= button <= 3:
|
||||
raise exceptions.ParameterError('button out of range: %d' % button)
|
||||
if not count > 0:
|
||||
raise exceptions.ParameterError('count out of range: %d' % count)
|
||||
if hold_ms < 0:
|
||||
raise exceptions.ParameterError('hold_ms out of range: %d' % hold_ms)
|
||||
if delay_ms < 0:
|
||||
raise exceptions.ParameterError('delay_ms out of range: %d' % delay_ms)
|
||||
ret = cmdr.send_prompt_command(
|
||||
"click multiple {button:d} {count:d} {hold_ms:d} {delay_ms:d}".format(**locals()))
|
||||
if not ret[0].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
|
@ -0,0 +1,61 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
# TODO: flash-write
|
||||
# Can't do it with pulse prompt :(
|
||||
|
||||
@PebbleCommander.command()
|
||||
def flash_erase(cmdr, address, length):
|
||||
""" Erase flash area.
|
||||
"""
|
||||
address = int(str(address), 0)
|
||||
length = int(str(length), 0)
|
||||
if address < 0:
|
||||
raise exceptions.ParameterError('address out of range: %d' % address)
|
||||
if length <= 0:
|
||||
raise exceptions.ParameterError('length out of range: %d' % length)
|
||||
# TODO: I guess catch errors
|
||||
ret = cmdr.send_prompt_command("erase flash 0x%X %d" % (address, length))
|
||||
if not ret[1].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def flash_crc(cmdr, address, length):
|
||||
""" Calculate CRC of flash area.
|
||||
"""
|
||||
address = int(str(address), 0)
|
||||
length = int(str(length), 0)
|
||||
if address < 0:
|
||||
raise exceptions.ParameterError('address out of range: %d' % address)
|
||||
if length <= 0:
|
||||
raise exceptions.ParameterError('length out of range: %d' % length)
|
||||
# TODO: I guess catch errors
|
||||
ret = cmdr.send_prompt_command("crc flash 0x%X %d" % (address, length))
|
||||
if not ret[0].startswith("CRC: "):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
return [ret[0][5:]]
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def prf_address(cmdr):
|
||||
""" Get address of PRF.
|
||||
"""
|
||||
ret = cmdr.send_prompt_command("prf image address")
|
||||
if not ret[0].startswith("OK "):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
return [ret[0][3:]]
|
133
python_libs/pebble-commander/pebble/commander/_commands/help.py
Normal file
133
python_libs/pebble-commander/pebble/commander/_commands/help.py
Normal file
|
@ -0,0 +1,133 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
def trim_docstring(var):
|
||||
return inspect.getdoc(var) or ''
|
||||
|
||||
|
||||
def get_help_short(cmdr, cmd_name, help_output=None):
|
||||
"""
|
||||
cmd_name is the command's name.
|
||||
help_output is the raw output of the `!help` command.
|
||||
"""
|
||||
output = None
|
||||
func = cmdr.get_command(cmd_name)
|
||||
if func: # Host command
|
||||
# cmdstr is the actual function name
|
||||
cmdstr = func.name
|
||||
spec = inspect.getargspec(func)
|
||||
|
||||
if len(spec.args) > 1:
|
||||
maxargs = len(spec.args) - 1
|
||||
if spec.defaults is None:
|
||||
cmdstr += " {%d args}" % maxargs
|
||||
else:
|
||||
minargs = maxargs - len(spec.defaults)
|
||||
cmdstr += " {%d~%d args}" % (minargs, maxargs)
|
||||
|
||||
if func.__doc__ is not None:
|
||||
output = "%-30s - %s" % (cmdstr, trim_docstring(func).splitlines()[0])
|
||||
else:
|
||||
output = cmdstr
|
||||
else: # Prompt command
|
||||
if cmd_name[0] == '!': # Strip the bang if it's there
|
||||
cmd_name = cmd_name[1:]
|
||||
|
||||
# Get the output if it wasn't provided
|
||||
if help_output is None:
|
||||
help_output = cmdr.send_prompt_command("help")
|
||||
|
||||
for prompt_cmd in help_output[1:]:
|
||||
# Match, even with argument count provided
|
||||
if prompt_cmd == cmd_name or prompt_cmd.startswith(cmd_name+" "):
|
||||
# Output should be the full argument string with the bang
|
||||
output = '!' + prompt_cmd
|
||||
break
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def help_arginfo_nodefault(arg):
|
||||
return "%s" % arg.upper()
|
||||
|
||||
|
||||
def help_arginfo_default(arg, dflt):
|
||||
return "[%s (default: %s)]" % (arg.upper(), str(dflt))
|
||||
|
||||
|
||||
def get_help_long(cmdr, cmd_name):
|
||||
output = ""
|
||||
|
||||
func = cmdr.get_command(cmd_name)
|
||||
|
||||
if func:
|
||||
spec = inspect.getargspec(func)
|
||||
specstr = []
|
||||
for i, arg in enumerate(spec.args[1:]):
|
||||
if spec.defaults is not None:
|
||||
minargs = len(spec.args[1:]) - len(spec.defaults)
|
||||
if i >= minargs:
|
||||
specstr.append(help_arginfo_default(arg, spec.defaults[i - minargs]))
|
||||
else:
|
||||
specstr.append(help_arginfo_nodefault(arg))
|
||||
else:
|
||||
specstr.append(help_arginfo_nodefault(arg))
|
||||
|
||||
specstr = ' '.join(specstr)
|
||||
cmdstr = func.name + " " + specstr
|
||||
if func.__doc__ is None:
|
||||
output = "%s\n\nNo help available." % cmdstr
|
||||
else:
|
||||
output = "%s - %s" % (cmdstr, trim_docstring(func))
|
||||
else: # Prompt command
|
||||
cmdstr = get_help_short(cmdr, cmd_name)
|
||||
if cmdstr is None:
|
||||
output = None
|
||||
else:
|
||||
output = "%s\n\nNo help available, due to being a prompt command." % cmdstr
|
||||
return output
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def help(cmdr, cmd=None):
|
||||
""" Show help.
|
||||
|
||||
You're lookin' at it, dummy!
|
||||
"""
|
||||
out = []
|
||||
if cmd is not None:
|
||||
helpstr = get_help_long(cmdr, cmd)
|
||||
if helpstr is None:
|
||||
raise exceptions.ParameterError("No command '%s' found." % cmd)
|
||||
out.append(helpstr)
|
||||
else: # List commands
|
||||
out.append("===Host commands===")
|
||||
# Bonus, this list is sorted for us already
|
||||
for cmd_name in dir(cmdr):
|
||||
if cmdr.get_command(cmd_name):
|
||||
out.append(get_help_short(cmdr, cmd_name))
|
||||
|
||||
out.append("\n===Prompt commands===")
|
||||
ret = cmdr.send_prompt_command("help")
|
||||
if ret[0] != 'Available Commands:':
|
||||
raise exceptions.PromptResponseError("'help' prompt command output invalid")
|
||||
for cmd_name in ret[1:]:
|
||||
out.append(get_help_short(cmdr, "!" + cmd_name, ret))
|
||||
return out
|
|
@ -0,0 +1,224 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from binascii import crc32
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pebble.pulse2.exceptions
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
from ..util import stm32_crc
|
||||
|
||||
|
||||
class PebbleFirmwareBinaryInfo(object):
|
||||
V1_STRUCT_VERSION = 1
|
||||
V1_STRUCT_DEFINTION = [
|
||||
('20s', 'build_id'),
|
||||
('L', 'version_timestamp'),
|
||||
('32s', 'version_tag'),
|
||||
('8s', 'version_short'),
|
||||
('?', 'is_recovery_firmware'),
|
||||
('B', 'hw_platform'),
|
||||
('B', 'metadata_version')
|
||||
]
|
||||
# The platforms which use a legacy defective crc32
|
||||
LEGACY_CRC_PLATFORMS = [
|
||||
0, # unknown (assume legacy)
|
||||
1, # OneEV1
|
||||
2, # OneEV2
|
||||
3, # OneEV2_3
|
||||
4, # OneEV2_4
|
||||
5, # OnePointFive
|
||||
6, # TwoPointFive
|
||||
7, # SnowyEVT2
|
||||
8, # SnowyDVT
|
||||
9, # SpaldingEVT
|
||||
10, # BobbyDVT
|
||||
11, # Spalding
|
||||
0xff, # OneBigboard
|
||||
0xfe, # OneBigboard2
|
||||
0xfd, # SnowyBigboard
|
||||
0xfc, # SnowyBigboard2
|
||||
0xfb, # SpaldingBigboard
|
||||
]
|
||||
|
||||
def get_crc(self):
|
||||
_, ext = os.path.splitext(self.path)
|
||||
assert ext == '.bin', 'Can only calculate crc for .bin files'
|
||||
with open(self.path, 'rb') as f:
|
||||
image = f.read()
|
||||
if self.hw_platform in self.LEGACY_CRC_PLATFORMS:
|
||||
# use the legacy defective crc
|
||||
return stm32_crc.crc32(image)
|
||||
else:
|
||||
# use a regular crc
|
||||
return crc32(image) & 0xFFFFFFFF
|
||||
|
||||
def _get_footer_struct(self):
|
||||
fmt = '<' + reduce(lambda s, t: s + t[0],
|
||||
PebbleFirmwareBinaryInfo.V1_STRUCT_DEFINTION, '')
|
||||
return struct.Struct(fmt)
|
||||
|
||||
def _get_footer_data_from_bin(self, path):
|
||||
with open(path, 'rb') as f:
|
||||
struct_size = self.struct.size
|
||||
f.seek(-struct_size, 2)
|
||||
footer_data = f.read()
|
||||
return footer_data
|
||||
|
||||
def _parse_footer_data(self, footer_data):
|
||||
z = zip(PebbleFirmwareBinaryInfo.V1_STRUCT_DEFINTION,
|
||||
self.struct.unpack(footer_data))
|
||||
return {entry[1]: data for entry, data in z}
|
||||
|
||||
def __init__(self, bin_path):
|
||||
self.path = bin_path
|
||||
self.struct = self._get_footer_struct()
|
||||
_, ext = os.path.splitext(bin_path)
|
||||
if ext != '.bin':
|
||||
raise ValueError('Unexpected extension. Must be ".bin"')
|
||||
footer_data = self._get_footer_data_from_bin(bin_path)
|
||||
self.info = self._parse_footer_data(footer_data)
|
||||
|
||||
# Trim leading NULLS on the strings:
|
||||
for k in ["version_tag", "version_short"]:
|
||||
self.info[k] = self.info[k].rstrip("\x00")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.info)
|
||||
|
||||
def __repr__(self):
|
||||
return self.info.__repr__()
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.info:
|
||||
return self.info[name]
|
||||
raise AttributeError
|
||||
|
||||
|
||||
# typedef struct ATTR_PACKED FirmwareDescription {
|
||||
# uint32_t description_length;
|
||||
# uint32_t firmware_length;
|
||||
# uint32_t checksum;
|
||||
# } FirmwareDescription;
|
||||
FW_DESCR_FORMAT = '<III'
|
||||
FW_DESCR_SIZE = struct.calcsize(FW_DESCR_FORMAT)
|
||||
|
||||
|
||||
def _generate_firmware_description_struct(firmware_length, firmware_crc):
|
||||
return struct.pack(FW_DESCR_FORMAT, FW_DESCR_SIZE, firmware_length, firmware_crc)
|
||||
|
||||
|
||||
def insert_firmware_description_struct(input_binary, output_binary=None):
|
||||
fw_bin_info = PebbleFirmwareBinaryInfo(input_binary)
|
||||
with open(input_binary, 'rb') as inf:
|
||||
fw_bin = inf.read()
|
||||
fw_crc = fw_bin_info.get_crc()
|
||||
return _generate_firmware_description_struct(len(fw_bin), fw_crc) + fw_bin
|
||||
|
||||
|
||||
def _load(connection, image, progress, verbose, address):
|
||||
image_crc = stm32_crc.crc32(image)
|
||||
|
||||
progress_cb = None
|
||||
if progress or verbose:
|
||||
def progress_cb(acked):
|
||||
print('.' if acked else 'R', end='')
|
||||
sys.stdout.flush()
|
||||
|
||||
if progress or verbose:
|
||||
print('Erasing... ', end='')
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
connection.flash.erase(address, len(image))
|
||||
except pebble.pulse2.exceptions.PulseException as e:
|
||||
detail = ''.join(traceback.format_exception_only(type(e), e))
|
||||
if verbose:
|
||||
detail = '\n' + traceback.format_exc()
|
||||
print('Erase failed! ' + detail)
|
||||
return False
|
||||
if progress or verbose:
|
||||
print('done.')
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
retries = connection.flash.write(address, image,
|
||||
progress_cb=progress_cb)
|
||||
except pebble.pulse2.exceptions.PulseException as e:
|
||||
detail = ''.join(traceback.format_exception_only(type(e), e))
|
||||
if verbose:
|
||||
detail = '\n' + traceback.format_exc()
|
||||
print('Write failed! ' + detail)
|
||||
return False
|
||||
|
||||
result_crc = connection.flash.crc(address, len(image))
|
||||
|
||||
if progress or verbose:
|
||||
print()
|
||||
if verbose:
|
||||
print('Retries: %d' % retries)
|
||||
|
||||
return result_crc == image_crc
|
||||
|
||||
|
||||
def load_firmware(connection, fin, progress, verbose, address=None):
|
||||
if address is None:
|
||||
# If address is unspecified, assume we want the prf address
|
||||
_, address, length = connection.flash.query_region_geometry(
|
||||
connection.flash.REGION_PRF)
|
||||
address = int(address)
|
||||
|
||||
image = insert_firmware_description_struct(fin)
|
||||
if _load(connection, image, progress, verbose, address):
|
||||
connection.flash.finalize_region(
|
||||
connection.flash.REGION_PRF)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def load_resources(connection, fin, progress, verbose):
|
||||
_, address, length = connection.flash.query_region_geometry(
|
||||
connection.flash.REGION_SYSTEM_RESOURCES)
|
||||
|
||||
with open(fin, 'rb') as f:
|
||||
data = f.read()
|
||||
assert len(data) <= length
|
||||
if _load(connection, data, progress, verbose, address):
|
||||
connection.flash.finalize_region(
|
||||
connection.flash.REGION_SYSTEM_RESOURCES)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def image_resources(cmdr, pack='build/system_resources.pbpack'):
|
||||
""" Image resources.
|
||||
"""
|
||||
load_resources(cmdr.connection, pack,
|
||||
progress=cmdr.interactive, verbose=cmdr.interactive)
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def image_firmware(cmdr, firm='build/prf/src/fw/tintin_fw.bin', address=None):
|
||||
""" Image recovery firmware.
|
||||
"""
|
||||
if address is not None:
|
||||
address = int(str(address), 0)
|
||||
load_firmware(cmdr.connection, firm, progress=cmdr.interactive,
|
||||
verbose=cmdr.interactive, address=address)
|
|
@ -0,0 +1,22 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def audit_delay(cmdr):
|
||||
""" Audit delay_us.
|
||||
"""
|
||||
return cmdr.send_prompt_command("audit delay")
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def pfs_prepare(cmdr, size):
|
||||
""" Prepare for file creation.
|
||||
"""
|
||||
size = int(str(size), 0)
|
||||
if size <= 0:
|
||||
raise exceptions.ParameterError('size out of range: %d' % size)
|
||||
# TODO: I guess catch errors
|
||||
ret = cmdr.send_prompt_command("pfs prepare %d" % size)
|
||||
if not ret[0].startswith("Success"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
|
||||
|
||||
# TODO: pfs-write
|
||||
# Can't do it with pulse prompt :(
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def pfs_litter(cmdr):
|
||||
""" Fragment the filesystem.
|
||||
|
||||
Creates a bunch of fragmentation in the filesystem by creating a large
|
||||
number of small files and only deleting a small number of them.
|
||||
"""
|
||||
ret = cmdr.send_prompt_command("litter pfs")
|
||||
if not ret[0].startswith("OK "):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
return [ret[0][3:]]
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def reset(cmdr):
|
||||
""" Reset the device.
|
||||
"""
|
||||
cmdr.send_prompt_command("reset")
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def crash(cmdr):
|
||||
""" Crash the device.
|
||||
"""
|
||||
cmdr.send_prompt_command("crash")
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def factory_reset(cmdr, fast=False):
|
||||
""" Perform a factory reset.
|
||||
|
||||
If `fast` is specified as true or "fast", do a fast factory reset.
|
||||
"""
|
||||
if parsers.str2bool(fast, also_true=["fast"]):
|
||||
fast = " fast"
|
||||
else:
|
||||
fast = ""
|
||||
|
||||
ret = cmdr.send_prompt_command("factory reset%s" % fast)
|
||||
if ret:
|
||||
raise exceptions.PromptResponseError(ret)
|
|
@ -0,0 +1,38 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def version(cmdr):
|
||||
""" Get version information.
|
||||
"""
|
||||
return cmdr.send_prompt_command("version")
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def boot_bit_set(cmdr, bit, value):
|
||||
""" Set some boot bits.
|
||||
|
||||
`bit` should be between 0 and 31.
|
||||
`value` should be a boolean.
|
||||
"""
|
||||
bit = int(str(bit), 0)
|
||||
value = int(parsers.str2bool(value))
|
||||
if not 0 <= bit <= 31:
|
||||
raise exceptions.ParameterError('bit index out of range: %d' % bit)
|
||||
ret = cmdr.send_prompt_command("boot bit set %d %d" % (bit, value))
|
||||
if not ret[0].startswith("OK"):
|
||||
raise exceptions.PromptResponseError(ret)
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def set_time(cmdr, new_time):
|
||||
""" Set the time.
|
||||
|
||||
`new_time` should be in epoch seconds.
|
||||
"""
|
||||
new_time = int(str(new_time), 0)
|
||||
if new_time < 1262304000:
|
||||
raise exceptions.ParameterError("time must be later than 2010-01-01")
|
||||
ret = cmdr.send_prompt_command("set time %s" % new_time)
|
||||
if not ret[0].startswith("Time is now"):
|
||||
raise exceptions.PromptResponseError(ret)
|
||||
return ret
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def timezone_clear(cmdr):
|
||||
""" Clear timezone settings.
|
||||
"""
|
||||
ret = cmdr.send_prompt_command("timezone clear")
|
||||
if ret:
|
||||
raise exceptions.PromptResponseError(ret)
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .. import PebbleCommander, exceptions, parsers
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def window_stack(cmdr):
|
||||
""" Dump the window stack.
|
||||
"""
|
||||
return cmdr.send_prompt_command("window stack")
|
||||
|
||||
|
||||
@PebbleCommander.command()
|
||||
def modal_stack(cmdr):
|
||||
""" Dump the modal stack.
|
||||
"""
|
||||
return cmdr.send_prompt_command("modal stack")
|
|
@ -0,0 +1,2 @@
|
|||
Application-layer PULSEv2 Protocols
|
||||
===================================
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Public aliases for the classes that users will interact with directly.
|
||||
from .bulkio import BulkIO
|
||||
from .flash_imaging import FlashImaging
|
||||
from .prompt import Prompt
|
||||
from .streaming_logs import StreamingLogs
|
451
python_libs/pebble-commander/pebble/commander/apps/bulkio.py
Normal file
451
python_libs/pebble-commander/pebble/commander/apps/bulkio.py
Normal file
|
@ -0,0 +1,451 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from ..exceptions import PebbleCommanderError
|
||||
|
||||
|
||||
class ResponseParseError(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class EraseError(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class OpenCommand(object):
|
||||
|
||||
command_type = 1
|
||||
command_struct = struct.Struct('<BB')
|
||||
|
||||
def __init__(self, domain, extra=None):
|
||||
self.domain = domain
|
||||
self.extra = extra
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
cmd = self.command_struct.pack(self.command_type, self.domain)
|
||||
if self.extra:
|
||||
cmd += self.extra
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
class CloseCommand(object):
|
||||
|
||||
command_type = 2
|
||||
command_struct = struct.Struct('<BB')
|
||||
|
||||
def __init__(self, fd):
|
||||
self.fd = fd
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.fd)
|
||||
|
||||
|
||||
class ReadCommand(object):
|
||||
|
||||
command_type = 3
|
||||
command_struct = struct.Struct('<BBII')
|
||||
|
||||
def __init__(self, fd, address, length):
|
||||
self.fd = fd
|
||||
self.address = address
|
||||
self.length = length
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.fd,
|
||||
self.address, self.length)
|
||||
|
||||
|
||||
class WriteCommand(object):
|
||||
|
||||
command_type = 4
|
||||
command_struct = struct.Struct('<BBI')
|
||||
header_size = command_struct.size
|
||||
|
||||
def __init__(self, fd, address, data):
|
||||
self.fd = fd
|
||||
self.address = address
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.fd,
|
||||
self.address) + self.data
|
||||
|
||||
|
||||
class CRCCommand(object):
|
||||
|
||||
command_type = 5
|
||||
command_struct = struct.Struct('<BBII')
|
||||
|
||||
def __init__(self, fd, address, length):
|
||||
self.fd = fd
|
||||
self.address = address
|
||||
self.length = length
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.fd,
|
||||
self.address, self.length)
|
||||
|
||||
|
||||
class StatCommand(object):
|
||||
|
||||
command_type = 6
|
||||
command_struct = struct.Struct('<BB')
|
||||
|
||||
def __init__(self, fd):
|
||||
self.fd = fd
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.fd)
|
||||
|
||||
|
||||
class EraseCommand(object):
|
||||
|
||||
command_type = 7
|
||||
command_struct = struct.Struct('<BB')
|
||||
|
||||
def __init__(self, domain, extra=None):
|
||||
self.domain = domain
|
||||
self.extra = extra
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
cmd = self.command_struct.pack(self.command_type, self.domain)
|
||||
if self.extra:
|
||||
cmd += self.extra
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
class OpenResponse(object):
|
||||
|
||||
response_type = 128
|
||||
response_format = '<xB'
|
||||
response_struct = struct.Struct(response_format)
|
||||
header_size = response_struct.size
|
||||
Response = collections.namedtuple('OpenResponse', 'fd')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
response_type = ord(response[0])
|
||||
if response_type != cls.response_type:
|
||||
raise ResponseParseError('Unexpected response type: %r' % response_type)
|
||||
return cls.Response._make(cls.response_struct.unpack(response))
|
||||
|
||||
|
||||
class CloseResponse(object):
|
||||
|
||||
response_type = 129
|
||||
response_format = '<xB'
|
||||
response_struct = struct.Struct(response_format)
|
||||
header_size = response_struct.size
|
||||
Response = collections.namedtuple('CloseResponse', 'fd')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
response_type = ord(response[0])
|
||||
if response_type != cls.response_type:
|
||||
raise ResponseParseError('Unexpected response type: %r' % response_type)
|
||||
return cls.Response._make(cls.response_struct.unpack(response))
|
||||
|
||||
|
||||
class ReadResponse(object):
|
||||
|
||||
response_type = 130
|
||||
response_format = '<xBI'
|
||||
response_struct = struct.Struct(response_format)
|
||||
header_size = response_struct.size
|
||||
Response = collections.namedtuple('ReadResponse', 'fd address data')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
if ord(response[0]) != cls.response_type:
|
||||
raise ResponseParseError('Unexpected response: %r' % response)
|
||||
header = response[:cls.header_size]
|
||||
body = response[cls.header_size:]
|
||||
fd, address, = cls.response_struct.unpack(header)
|
||||
return cls.Response(fd, address, body)
|
||||
|
||||
|
||||
class WriteResponse(object):
|
||||
|
||||
response_type = 131
|
||||
response_format = '<xBII'
|
||||
response_struct = struct.Struct(response_format)
|
||||
header_size = response_struct.size
|
||||
Response = collections.namedtuple('WriteResponse', 'fd address length')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
response_type = ord(response[0])
|
||||
if response_type != cls.response_type:
|
||||
raise ResponseParseError('Unexpected response type: %r' % response_type)
|
||||
return cls.Response._make(cls.response_struct.unpack(response))
|
||||
|
||||
|
||||
class CRCResponse(object):
|
||||
|
||||
response_type = 132
|
||||
response_format = '<xBIII'
|
||||
response_struct = struct.Struct(response_format)
|
||||
header_size = response_struct.size
|
||||
Response = collections.namedtuple('CRCResponse', 'fd address length crc')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
response_type = ord(response[0])
|
||||
if response_type != cls.response_type:
|
||||
raise ResponseParseError('Unexpected response type: %r' % response_type)
|
||||
return cls.Response._make(cls.response_struct.unpack(response))
|
||||
|
||||
|
||||
class StatResponse(object):
|
||||
response_type = 133
|
||||
|
||||
def __init__(self, name, format, fields):
|
||||
self.name = name
|
||||
self.struct = struct.Struct('<xBB' + format)
|
||||
self.tuple = collections.namedtuple(name, 'fd flags ' + fields)
|
||||
|
||||
def parse(self, response):
|
||||
response_type = ord(response[0])
|
||||
if response_type != self.response_type:
|
||||
raise ResponseParseError('Unexpected response type: %r' % response_type)
|
||||
return self.tuple._make(self.struct.unpack(response))
|
||||
|
||||
def __repr__(self):
|
||||
return 'StatResponse({self.name!r}, {self.struct!r}, {self.tuple!r})'.format(self=self)
|
||||
|
||||
|
||||
class EraseResponse(object):
|
||||
|
||||
response_type = 134
|
||||
response_format = '<xBb'
|
||||
response_struct = struct.Struct(response_format)
|
||||
header_size = response_struct.size
|
||||
Response = collections.namedtuple('EraseResponse', 'domain status')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
response_type = ord(response[0])
|
||||
if response_type != cls.response_type:
|
||||
raise ResponseParseError('Unexpected response type: %r' % response_type)
|
||||
return cls.Response._make(cls.response_struct.unpack(response))
|
||||
|
||||
|
||||
def enum(**enums):
|
||||
return type('Enum', (), enums)
|
||||
|
||||
ReadDomains = enum(
|
||||
MEMORY=1,
|
||||
EXTERNAL_FLASH=2,
|
||||
FRAMEBUFFER=3,
|
||||
COREDUMP=4,
|
||||
PFS=5
|
||||
)
|
||||
|
||||
|
||||
class PULSEIO_Base(object):
|
||||
|
||||
ERASE_FORMAT = None
|
||||
STAT_FORMAT = None
|
||||
DOMAIN = None
|
||||
|
||||
def __init__(self, socket, *args, **kwargs):
|
||||
self.socket = socket
|
||||
self.pos = 0
|
||||
|
||||
options = self._process_args(*args, **kwargs)
|
||||
resp = self._send_and_receive(OpenCommand, OpenResponse, self.DOMAIN, options)
|
||||
self.fd = resp.fd
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.close()
|
||||
|
||||
@staticmethod
|
||||
def _process_args(*args, **kwargs):
|
||||
return ""
|
||||
|
||||
def _send_and_receive(self, cmd_type, resp_type, *args):
|
||||
cmd = cmd_type(*args)
|
||||
self.socket.send(cmd.packet)
|
||||
ret = self.socket.receive(block=True)
|
||||
return resp_type.parse(ret)
|
||||
|
||||
def close(self):
|
||||
if self.fd is not None:
|
||||
resp = self._send_and_receive(CloseCommand, CloseResponse, self.fd)
|
||||
assert resp.fd == self.fd
|
||||
self.fd = None
|
||||
|
||||
def seek_absolute(self, pos):
|
||||
if pos < 0:
|
||||
raise ValueError('Cannot seek to before start of file')
|
||||
self.pos = pos
|
||||
|
||||
def seek_relative(self, num_bytes):
|
||||
if (self.pos + num_bytes) < 0:
|
||||
raise ValueError('Cannot seek to before start of file')
|
||||
self.pos += num_bytes
|
||||
|
||||
@classmethod
|
||||
def erase(cls, socket, *args):
|
||||
if cls.ERASE_FORMAT == "raw":
|
||||
options = "".join(args)
|
||||
elif cls.ERASE_FORMAT:
|
||||
options = struct.pack("<" + cls.ERASE_FORMAT, *args)
|
||||
else:
|
||||
raise NotImplementedError("Erase is not supported for domain %d" % cls.DOMAIN)
|
||||
cmd = EraseCommand(cls.DOMAIN, options)
|
||||
socket.send(cmd.packet)
|
||||
status = 1
|
||||
while status > 0:
|
||||
ret = socket.receive(block=True)
|
||||
resp = EraseResponse.parse(ret)
|
||||
logging.debug("ERASE: domain %d status %d", resp.domain, resp.status)
|
||||
status = resp.status
|
||||
|
||||
if status < 0:
|
||||
raise EraseError(status)
|
||||
|
||||
def write(self, data):
|
||||
if self.fd is None:
|
||||
raise ValueError('Handle is not open')
|
||||
|
||||
mss = self.socket.mtu - WriteCommand.header_size
|
||||
|
||||
for offset in xrange(0, len(data), mss):
|
||||
segment = data[offset:offset+mss]
|
||||
resp = self._send_and_receive(WriteCommand, WriteResponse, self.fd, self.pos, segment)
|
||||
assert resp.fd == self.fd
|
||||
assert resp.address == self.pos
|
||||
self.pos += len(segment)
|
||||
|
||||
def read(self, length):
|
||||
if self.fd is None:
|
||||
raise ValueError('Handle is not open')
|
||||
|
||||
cmd = ReadCommand(self.fd, self.pos, length)
|
||||
self.socket.send(cmd.packet)
|
||||
|
||||
data = bytearray()
|
||||
bytes_left = length
|
||||
while bytes_left > 0:
|
||||
packet = self.socket.receive(block=True)
|
||||
fd, chunk_offset, chunk = ReadResponse.parse(packet)
|
||||
assert fd == self.fd
|
||||
data.extend(chunk)
|
||||
|
||||
bytes_left -= len(chunk)
|
||||
return data
|
||||
|
||||
def crc(self, length):
|
||||
if self.fd is None:
|
||||
raise ValueError('Handle is not open')
|
||||
|
||||
resp = self._send_and_receive(CRCCommand, CRCResponse, self.fd, self.pos, length)
|
||||
return resp.crc
|
||||
|
||||
def stat(self):
|
||||
if self.fd is None:
|
||||
raise ValueError('Handle is not open')
|
||||
|
||||
if not self.STAT_FORMAT:
|
||||
raise NotImplementedError("Stat is not supported for domain %d" % self.DOMAIN)
|
||||
|
||||
return self._send_and_receive(StatCommand, self.STAT_FORMAT, self.fd)
|
||||
|
||||
|
||||
class PULSEIO_Memory(PULSEIO_Base):
|
||||
DOMAIN = ReadDomains.MEMORY
|
||||
|
||||
# uint32 for address, uint32 for length
|
||||
ERASE_FORMAT = "II"
|
||||
|
||||
|
||||
class PULSEIO_ExternalFlash(PULSEIO_Base):
|
||||
DOMAIN = ReadDomains.EXTERNAL_FLASH
|
||||
|
||||
# uint32 for address, uint32 for length
|
||||
ERASE_FORMAT = "II"
|
||||
|
||||
|
||||
class PULSEIO_Framebuffer(PULSEIO_Base):
|
||||
DOMAIN = ReadDomains.FRAMEBUFFER
|
||||
STAT_FORMAT = StatResponse('FramebufferAttributes', 'BBBI', 'width height bpp length')
|
||||
|
||||
|
||||
class PULSEIO_Coredump(PULSEIO_Base):
|
||||
DOMAIN = ReadDomains.COREDUMP
|
||||
STAT_FORMAT = StatResponse('CoredumpAttributes', 'BI', 'unread length')
|
||||
ERASE_FORMAT = "I"
|
||||
|
||||
@staticmethod
|
||||
def _process_args(slot):
|
||||
return struct.pack("<I", slot)
|
||||
|
||||
|
||||
class PULSEIO_PFS(PULSEIO_Base):
|
||||
DOMAIN = ReadDomains.PFS
|
||||
STAT_FORMAT = StatResponse('PFSFileAttributes', 'I', 'length')
|
||||
ERASE_FORMAT = "raw"
|
||||
|
||||
OP_FLAG_READ = 1 << 0
|
||||
OP_FLAG_WRITE = 1 << 1
|
||||
OP_FLAG_OVERWRITE = 1 << 2
|
||||
OP_FLAG_SKIP_HDR_CRC_CHECK = 1 << 3
|
||||
OP_FLAG_USE_PAGE_CACHE = 1 << 4
|
||||
|
||||
@staticmethod
|
||||
def _process_args(filename, mode='r', flags=0xFE, initial_size=0):
|
||||
mode_num = PULSEIO_PFS.OP_FLAG_READ
|
||||
if 'w' in mode:
|
||||
mode_num |= PULSEIO_PFS.OP_FLAG_WRITE
|
||||
return struct.pack("<BBI", mode_num, flags, initial_size) + filename
|
||||
|
||||
|
||||
class BulkIO(object):
|
||||
|
||||
PROTOCOL_NUMBER = 0x3e21
|
||||
DOMAIN_MAP = {
|
||||
'pfs': PULSEIO_PFS,
|
||||
'framebuffer': PULSEIO_Framebuffer
|
||||
}
|
||||
|
||||
def __init__(self, link):
|
||||
self.socket = link.open_socket('reliable', self.PROTOCOL_NUMBER)
|
||||
|
||||
def open(self, domain, *args, **kwargs):
|
||||
return self.DOMAIN_MAP[domain](self.socket, *args, **kwargs)
|
||||
|
||||
def erase(self, domain, *args, **kwargs):
|
||||
return self.DOMAIN_MAP[domain].erase(self.socket, *args, **kwargs)
|
||||
|
||||
def close(self):
|
||||
self.socket.close()
|
|
@ -0,0 +1,318 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import struct
|
||||
import time
|
||||
|
||||
import pebble.pulse2.exceptions
|
||||
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
class EraseCommand(object):
|
||||
|
||||
command_type = 1
|
||||
command_struct = struct.Struct('<BII')
|
||||
|
||||
response_type = 128
|
||||
response_struct = struct.Struct('<xII?')
|
||||
Response = collections.namedtuple(
|
||||
'EraseResponse', 'address length complete')
|
||||
|
||||
def __init__(self, address, length):
|
||||
self.address = address
|
||||
self.length = length
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(
|
||||
self.command_type, self.address, self.length)
|
||||
|
||||
def parse_response(self, response):
|
||||
if ord(response[0]) != self.response_type:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Unexpected response: %r' % response)
|
||||
unpacked = self.Response._make(self.response_struct.unpack(response))
|
||||
if unpacked.address != self.address or unpacked.length != self.length:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Response does not match command: '
|
||||
'address=%#.08x legnth=%d (expected %#.08x, %d)' % (
|
||||
unpacked.address, unpacked.length, self.address,
|
||||
self.length))
|
||||
return unpacked
|
||||
|
||||
|
||||
class WriteCommand(object):
|
||||
|
||||
command_type = 2
|
||||
command_struct = struct.Struct('<BI')
|
||||
header_len = command_struct.size
|
||||
|
||||
def __init__(self, address, data):
|
||||
self.address = address
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
header = self.command_struct.pack(self.command_type, self.address)
|
||||
return header + self.data
|
||||
|
||||
|
||||
class WriteResponse(object):
|
||||
|
||||
response_type = 129
|
||||
response_struct = struct.Struct('<xII?')
|
||||
Response = collections.namedtuple(
|
||||
'WriteResponse', 'address length complete')
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
if ord(response[0]) != cls.response_type:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Unexpected response: %r' % response)
|
||||
return cls.Response._make(cls.response_struct.unpack(response))
|
||||
|
||||
|
||||
class CrcCommand(object):
|
||||
|
||||
command_type = 3
|
||||
command_struct = struct.Struct('<BII')
|
||||
|
||||
response_type = 130
|
||||
response_struct = struct.Struct('<xIII')
|
||||
Response = collections.namedtuple('CrcResponse', 'address length crc')
|
||||
|
||||
def __init__(self, address, length):
|
||||
self.address = address
|
||||
self.length = length
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.address,
|
||||
self.length)
|
||||
|
||||
def parse_response(self, response):
|
||||
if ord(response[0]) != self.response_type:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Unexpected response: %r' % response)
|
||||
unpacked = self.Response._make(self.response_struct.unpack(response))
|
||||
if unpacked.address != self.address or unpacked.length != self.length:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Response does not match command: '
|
||||
'address=%#.08x legnth=%d (expected %#.08x, %d)' % (
|
||||
unpacked.address, unpacked.length, self.address,
|
||||
self.length))
|
||||
return unpacked
|
||||
|
||||
|
||||
class QueryFlashRegionCommand(object):
|
||||
|
||||
command_type = 4
|
||||
command_struct = struct.Struct('<BB')
|
||||
|
||||
REGION_PRF = 1
|
||||
REGION_SYSTEM_RESOURCES = 2
|
||||
|
||||
response_type = 131
|
||||
response_struct = struct.Struct('<xBII')
|
||||
Response = collections.namedtuple(
|
||||
'FlashRegionGeometry', 'region address length')
|
||||
|
||||
def __init__(self, region):
|
||||
self.region = region
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.region)
|
||||
|
||||
def parse_response(self, response):
|
||||
if ord(response[0]) != self.response_type:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Unexpected response: %r' % response)
|
||||
unpacked = self.Response._make(self.response_struct.unpack(response))
|
||||
if unpacked.address == 0 and unpacked.length == 0:
|
||||
raise exceptions.RegionDoesNotExist(self.region)
|
||||
return unpacked
|
||||
|
||||
|
||||
class FinalizeFlashRegionCommand(object):
|
||||
|
||||
command_type = 5
|
||||
command_struct = struct.Struct('<BB')
|
||||
|
||||
response_type = 132
|
||||
response_struct = struct.Struct('<xB')
|
||||
|
||||
def __init__(self, region):
|
||||
self.region = region
|
||||
|
||||
@property
|
||||
def packet(self):
|
||||
return self.command_struct.pack(self.command_type, self.region)
|
||||
|
||||
def parse_response(self, response):
|
||||
if ord(response[0]) != self.response_type:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Unexpected response: %r' % response)
|
||||
region, = self.response_struct.unpack(response)
|
||||
if region != self.region:
|
||||
raise exceptions.ResponseParseError(
|
||||
'Response does not match command: '
|
||||
'response is for region %d (expected %d)' % (
|
||||
region, self.region))
|
||||
|
||||
|
||||
class FlashImaging(object):
|
||||
|
||||
PORT_NUMBER = 0x0002
|
||||
|
||||
RESP_BAD_CMD = 192
|
||||
RESP_INTERNAL_ERROR = 193
|
||||
|
||||
REGION_PRF = QueryFlashRegionCommand.REGION_PRF
|
||||
REGION_SYSTEM_RESOURCES = QueryFlashRegionCommand.REGION_SYSTEM_RESOURCES
|
||||
|
||||
def __init__(self, link):
|
||||
self.socket = link.open_socket('best-effort', self.PORT_NUMBER)
|
||||
|
||||
def close(self):
|
||||
self.socket.close()
|
||||
|
||||
def erase(self, address, length):
|
||||
cmd = EraseCommand(address, length)
|
||||
ack_received = False
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
if not ack_received:
|
||||
self.socket.send(cmd.packet)
|
||||
try:
|
||||
packet = self.socket.receive(timeout=5 if ack_received else 1.5)
|
||||
response = cmd.parse_response(packet)
|
||||
ack_received = True
|
||||
if response.complete:
|
||||
return
|
||||
except pebble.pulse2.exceptions.ReceiveQueueEmpty:
|
||||
ack_received = False
|
||||
retries += 1
|
||||
continue
|
||||
raise exceptions.CommandTimedOut
|
||||
|
||||
def write(self, address, data, max_retries=5, max_in_flight=5,
|
||||
progress_cb=None):
|
||||
mtu = self.socket.mtu - WriteCommand.header_len
|
||||
assert(mtu > 0)
|
||||
unsent = collections.deque()
|
||||
for offset in xrange(0, len(data), mtu):
|
||||
segment = data[offset:offset+mtu]
|
||||
assert(len(segment))
|
||||
seg_address = address + offset
|
||||
unsent.appendleft(
|
||||
(seg_address, WriteCommand(seg_address, segment), 0))
|
||||
|
||||
in_flight = collections.OrderedDict()
|
||||
retries = 0
|
||||
while unsent or in_flight:
|
||||
try:
|
||||
while True:
|
||||
# Process ACKs (if any)
|
||||
ack = WriteResponse.parse(
|
||||
self.socket.receive(block=False))
|
||||
try:
|
||||
cmd, _, _ = in_flight[ack.address]
|
||||
del in_flight[ack.address]
|
||||
except KeyError:
|
||||
for seg_address, cmd, retry_count in unsent:
|
||||
if seg_address == ack.address:
|
||||
if retry_count == 0:
|
||||
# ACK for a segment we never sent?!
|
||||
raise exceptions.WriteError(
|
||||
'Received ACK for an unsent segment: '
|
||||
'%#.08x' % ack.address)
|
||||
|
||||
# Got an ACK for a sent (but timed out) segment
|
||||
unsent.remove((seg_address, cmd, retry_count))
|
||||
break
|
||||
else:
|
||||
raise exceptions.WriteError(
|
||||
'Received ACK for an unknown segment: '
|
||||
'%#.08x' % ack.address)
|
||||
|
||||
|
||||
if len(cmd.data) != ack.length:
|
||||
raise exceptions.WriteError(
|
||||
'ACK length %d != data length %d' % (
|
||||
ack.length, len(cmd.data)))
|
||||
assert(ack.complete)
|
||||
if progress_cb:
|
||||
progress_cb(True)
|
||||
except pebble.pulse2.exceptions.ReceiveQueueEmpty:
|
||||
pass
|
||||
|
||||
# Retry any in_flight writes where the ACK has timed out
|
||||
to_retry = []
|
||||
timeout_time = time.time() - 0.5
|
||||
for (seg_address,
|
||||
(cmd, send_time, retry_count)) in in_flight.iteritems():
|
||||
if send_time > timeout_time:
|
||||
# in_flight is an OrderedDict so iteration is in
|
||||
# chronological order.
|
||||
break
|
||||
if retry_count >= max_retries:
|
||||
raise exceptions.WriteError(
|
||||
'Segment %#.08x exceeded the max retry count (%d)' % (
|
||||
seg_address, max_retries))
|
||||
# Enqueue the packet again to resend later.
|
||||
del in_flight[seg_address]
|
||||
unsent.appendleft((seg_address, cmd, retry_count+1))
|
||||
retries += 1
|
||||
if progress_cb:
|
||||
progress_cb(False)
|
||||
|
||||
# Send out fresh segments
|
||||
try:
|
||||
while len(in_flight) < max_in_flight:
|
||||
seg_address, cmd, retry_count = unsent.pop()
|
||||
self.socket.send(cmd.packet)
|
||||
in_flight[cmd.address] = (cmd, time.time(), retry_count)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# Give other threads a chance to run
|
||||
time.sleep(0)
|
||||
return retries
|
||||
|
||||
def _command_and_response(self, cmd, timeout=0.5):
|
||||
for attempt in xrange(5):
|
||||
self.socket.send(cmd.packet)
|
||||
try:
|
||||
packet = self.socket.receive(timeout=timeout)
|
||||
return cmd.parse_response(packet)
|
||||
except pebble.pulse2.exceptions.ReceiveQueueEmpty:
|
||||
pass
|
||||
raise exceptions.CommandTimedOut
|
||||
|
||||
def crc(self, address, length):
|
||||
cmd = CrcCommand(address, length)
|
||||
return self._command_and_response(cmd, timeout=1).crc
|
||||
|
||||
def query_region_geometry(self, region):
|
||||
cmd = QueryFlashRegionCommand(region)
|
||||
return self._command_and_response(cmd)
|
||||
|
||||
def finalize_region(self, region):
|
||||
cmd = FinalizeFlashRegionCommand(region)
|
||||
return self._command_and_response(cmd)
|
78
python_libs/pebble-commander/pebble/commander/apps/prompt.py
Normal file
78
python_libs/pebble-commander/pebble/commander/apps/prompt.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import struct
|
||||
from datetime import datetime
|
||||
|
||||
import pebble.pulse2.exceptions
|
||||
|
||||
from .. import exceptions
|
||||
|
||||
|
||||
class Prompt(object):
|
||||
|
||||
PORT_NUMBER = 0x3e20
|
||||
|
||||
def __init__(self, link):
|
||||
self.socket = link.open_socket('reliable', self.PORT_NUMBER)
|
||||
|
||||
def command_and_response(self, command_string, timeout=20):
|
||||
log = []
|
||||
self.socket.send(bytes(command_string))
|
||||
|
||||
is_done = False
|
||||
while not is_done:
|
||||
try:
|
||||
response = PromptResponse.parse(
|
||||
self.socket.receive(timeout=timeout))
|
||||
if response.is_done_response:
|
||||
is_done = True
|
||||
elif response.is_message_response:
|
||||
log.append(response.message)
|
||||
except pebble.pulse2.exceptions.ReceiveQueueEmpty:
|
||||
raise exceptions.CommandTimedOut
|
||||
return log
|
||||
|
||||
def close(self):
|
||||
self.socket.close()
|
||||
|
||||
|
||||
class PromptResponse(collections.namedtuple('PromptResponse',
|
||||
'response_type timestamp message')):
|
||||
|
||||
DONE_RESPONSE = 101
|
||||
MESSAGE_RESPONSE = 102
|
||||
|
||||
response_struct = struct.Struct('<BQ')
|
||||
|
||||
@property
|
||||
def is_done_response(self):
|
||||
return self.response_type == self.DONE_RESPONSE
|
||||
|
||||
@property
|
||||
def is_message_response(self):
|
||||
return self.response_type == self.MESSAGE_RESPONSE
|
||||
|
||||
@classmethod
|
||||
def parse(cls, response):
|
||||
result = cls.response_struct.unpack(response[:cls.response_struct.size])
|
||||
|
||||
response_type = result[0]
|
||||
timestamp = datetime.fromtimestamp(result[1] / 1000.0)
|
||||
message = response[cls.response_struct.size:]
|
||||
|
||||
return cls(response_type, timestamp, message)
|
|
@ -0,0 +1,67 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
import struct
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class LogMessage(collections.namedtuple('LogMessage',
|
||||
'log_level task timestamp file_name line_number message')):
|
||||
|
||||
__slots__ = ()
|
||||
response_struct = struct.Struct('<c16sccQH')
|
||||
|
||||
def __str__(self):
|
||||
msec_timestamp = self.timestamp.strftime("%H:%M:%S.%f")[:-3]
|
||||
template = ('{self.log_level} {self.task} {msec_timestamp} '
|
||||
'{self.file_name}:{self.line_number}> {self.message}')
|
||||
|
||||
return template.format(self=self, msec_timestamp=msec_timestamp)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, packet):
|
||||
result = cls.response_struct.unpack(packet[:cls.response_struct.size])
|
||||
msg = packet[cls.response_struct.size:]
|
||||
|
||||
log_level = result[2]
|
||||
task = result[3]
|
||||
timestamp = datetime.fromtimestamp(result[4] / 1000.0)
|
||||
file_name = result[1].split('\x00', 1)[0] # NUL terminated
|
||||
line_number = result[5]
|
||||
|
||||
return cls(log_level, task, timestamp, file_name, line_number, msg)
|
||||
|
||||
|
||||
class StreamingLogs(object):
|
||||
'''App for receiving log messages streamed by the firmware.
|
||||
'''
|
||||
|
||||
PORT_NUMBER = 0x0003
|
||||
|
||||
def __init__(self, interface):
|
||||
try:
|
||||
self.socket = interface.simplex_transport.open_socket(
|
||||
self.PORT_NUMBER)
|
||||
except AttributeError:
|
||||
raise TypeError('LoggingApp must be bound directly '
|
||||
'to an Interface, not a Link')
|
||||
|
||||
def receive(self, block=True, timeout=None):
|
||||
return LogMessage.parse(self.socket.receive(block, timeout))
|
||||
|
||||
def close(self):
|
||||
self.socket.close()
|
167
python_libs/pebble-commander/pebble/commander/commander.py
Normal file
167
python_libs/pebble-commander/pebble/commander/commander.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
import threading
|
||||
import tokenize
|
||||
import types
|
||||
|
||||
from pebble import pulse2
|
||||
|
||||
from . import apps
|
||||
|
||||
|
||||
class Pulse2ConnectionAdapter(object):
|
||||
'''An adapter for the pulse2 API to look enough like pulse.Connection
|
||||
to make PebbleCommander work...ish.
|
||||
|
||||
Prompt will break spectacularly if the firmware reboots or the link
|
||||
state otherwise changes. Commander itself needs to be modified to be
|
||||
link-state aware.
|
||||
'''
|
||||
|
||||
def __init__(self, interface):
|
||||
self.interface = interface
|
||||
self.logging = apps.StreamingLogs(interface)
|
||||
link = interface.get_link()
|
||||
self.prompt = apps.Prompt(link)
|
||||
self.flash = apps.FlashImaging(link)
|
||||
|
||||
def close(self):
|
||||
self.interface.close()
|
||||
|
||||
|
||||
class PebbleCommander(object):
|
||||
""" Pebble Commander.
|
||||
Implements everything for interfacing with PULSE things.
|
||||
"""
|
||||
|
||||
def __init__(self, tty=None, interactive=False, capfile=None):
|
||||
if capfile is not None:
|
||||
interface = pulse2.Interface.open_dbgserial(
|
||||
url=tty, capture_stream=open(capfile, 'wb'))
|
||||
else:
|
||||
interface = pulse2.Interface.open_dbgserial(url=tty)
|
||||
|
||||
try:
|
||||
self.connection = Pulse2ConnectionAdapter(interface)
|
||||
except:
|
||||
interface.close()
|
||||
raise
|
||||
|
||||
self.interactive = interactive
|
||||
|
||||
self.log_listeners_lock = threading.Lock()
|
||||
self.log_listeners = []
|
||||
|
||||
# Start the logging thread
|
||||
self.log_thread = threading.Thread(target=self._start_logging)
|
||||
self.log_thread.daemon = True
|
||||
self.log_thread.start()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
@classmethod
|
||||
def command(cls, name=None):
|
||||
""" Registers a command.
|
||||
`name` is the command name. If `name` is unspecified, name will be the function name
|
||||
with underscores converted to hyphens.
|
||||
|
||||
The convention for `name` is to separate words with a hyphen. The function name
|
||||
will be the same as `name` with hyphens replaced with underscores.
|
||||
Example: `click-short` will result in a PebbleCommander.click_short function existing.
|
||||
|
||||
`fn` should return an array of strings (or None), and take the current
|
||||
`PebbleCommander` as the first argument, and the rest of the argument strings
|
||||
as subsequent arguments. For errors, `fn` should throw an exception.
|
||||
|
||||
# TODO: Probably make the return something structured instead of stringly typed.
|
||||
"""
|
||||
def decorator(fn):
|
||||
# Story time:
|
||||
# <cory> Things are fine as long as you only read from `name`, but assigning to `name`
|
||||
# creates a new local which shadows the outer scope's variable, even though it's
|
||||
# only assigned later on in the block
|
||||
# <cory> You could work around this by doing something like `name_ = name` and using
|
||||
# `name_` in the `decorator` scope
|
||||
cmdname = name
|
||||
if not cmdname:
|
||||
cmdname = fn.__name__.replace('_', '-')
|
||||
funcname = cmdname.replace('-', '_')
|
||||
if not re.match(tokenize.Name + '$', funcname):
|
||||
raise ValueError("command name %s isn't a valid name" % funcname)
|
||||
if hasattr(cls, funcname):
|
||||
raise ValueError('function name %s clashes with existing attribute' % funcname)
|
||||
fn.is_command = True
|
||||
fn.name = cmdname
|
||||
method = types.MethodType(fn, None, cls)
|
||||
setattr(cls, funcname, method)
|
||||
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
def close(self):
|
||||
self.connection.close()
|
||||
|
||||
def _start_logging(self):
|
||||
""" Thread to handle logging messages.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
msg = self.connection.logging.receive()
|
||||
except pulse2.exceptions.SocketClosed:
|
||||
break
|
||||
with self.log_listeners_lock:
|
||||
# TODO: Buffer log messages if no listeners attached?
|
||||
for listener in self.log_listeners:
|
||||
try:
|
||||
listener(msg)
|
||||
except:
|
||||
pass
|
||||
|
||||
def attach_log_listener(self, listener):
|
||||
""" Attaches a listener for log messages.
|
||||
Function takes message and returns are ignored.
|
||||
"""
|
||||
with self.log_listeners_lock:
|
||||
self.log_listeners.append(listener)
|
||||
|
||||
def detach_log_listener(self, listener):
|
||||
""" Removes a listener that was added with `attach_log_listener`
|
||||
"""
|
||||
with self.log_listeners_lock:
|
||||
self.log_listeners.remove(listener)
|
||||
|
||||
def send_prompt_command(self, cmd):
|
||||
""" Send a prompt command string.
|
||||
Unfortunately this is indeed stringly typed, a better solution is necessary.
|
||||
"""
|
||||
return self.connection.prompt.command_and_response(cmd)
|
||||
|
||||
def get_command(self, command):
|
||||
try:
|
||||
fn = getattr(self, command.replace('-', '_'))
|
||||
if fn.is_command:
|
||||
return fn
|
||||
except AttributeError:
|
||||
# Method doesn't exist, or isn't a command.
|
||||
pass
|
||||
|
||||
return None
|
40
python_libs/pebble-commander/pebble/commander/exceptions.py
Normal file
40
python_libs/pebble-commander/pebble/commander/exceptions.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
class PebbleCommanderError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParameterError(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class PromptResponseError(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class ResponseParseError(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class RegionDoesNotExist(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class CommandTimedOut(PebbleCommanderError):
|
||||
pass
|
||||
|
||||
|
||||
class WriteError(PebbleCommanderError):
|
||||
pass
|
137
python_libs/pebble-commander/pebble/commander/interactive.py
Normal file
137
python_libs/pebble-commander/pebble/commander/interactive.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import shlex
|
||||
import traceback
|
||||
|
||||
from log_hashing.logdehash import LogDehash
|
||||
import prompt_toolkit
|
||||
|
||||
from .commander import PebbleCommander
|
||||
|
||||
|
||||
class InteractivePebbleCommander(object):
|
||||
""" Interactive Pebble Commander.
|
||||
Most/all UI implementations should either use this directly or sub-class it.
|
||||
"""
|
||||
def __init__(self, loghash_path=None, tty=None, capfile=None):
|
||||
self.cmdr = PebbleCommander(tty=tty, interactive=True, capfile=capfile)
|
||||
if loghash_path is None:
|
||||
loghash_path = "build/src/fw/loghash_dict.json"
|
||||
self.dehasher = LogDehash(loghash_path)
|
||||
self.cmdr.attach_log_listener(self.log_listener)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.cmdr.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def attach_prompt_toolkit(self):
|
||||
""" Attaches prompt_toolkit things
|
||||
"""
|
||||
self.history = prompt_toolkit.history.InMemoryHistory()
|
||||
self.cli = prompt_toolkit.CommandLineInterface(
|
||||
application=prompt_toolkit.shortcuts.create_prompt_application(u"> ",
|
||||
history=self.history),
|
||||
eventloop=prompt_toolkit.shortcuts.create_eventloop())
|
||||
self.patch_context = self.cli.patch_stdout_context(raw=True)
|
||||
self.patch_context.__enter__()
|
||||
|
||||
def log_listener(self, msg):
|
||||
""" This is called on every incoming log message.
|
||||
`msg` is the raw log message class, without any dehashing.
|
||||
|
||||
Subclasses should override this probably.
|
||||
"""
|
||||
line_dict = self.dehasher.dehash(msg)
|
||||
line = self.dehasher.commander_format_line(line_dict)
|
||||
print line
|
||||
|
||||
def dispatch_command(self, string):
|
||||
""" Dispatches a command string.
|
||||
|
||||
Subclasses should not override this.
|
||||
"""
|
||||
args = shlex.split(string)
|
||||
# Starting with '!' passes the rest of the line directly to prompt.
|
||||
# Otherwise we try to run a command; if that fails, the line goes to prompt.
|
||||
if string.startswith("!"):
|
||||
string = string[1:] # Chop off the '!' marker
|
||||
else:
|
||||
cmd = self.cmdr.get_command(args[0])
|
||||
if cmd: # If we provide the command, run it.
|
||||
return cmd(*args[1:])
|
||||
|
||||
return self.cmdr.send_prompt_command(string)
|
||||
|
||||
def input_handle(self, string):
|
||||
""" Handles an input line.
|
||||
Generally the flow is to handle any UI-specific commands, then pass on to
|
||||
dispatch_command.
|
||||
|
||||
Subclasses should override this probably.
|
||||
"""
|
||||
# Handle "quit" strings
|
||||
if string in ["exit", "q", "quit"]:
|
||||
return False
|
||||
|
||||
try:
|
||||
resp = self.dispatch_command(string)
|
||||
if resp is not None:
|
||||
print "\x1b[1m" + '\n'.join(resp) + "\x1b[m"
|
||||
except:
|
||||
print "An error occurred!"
|
||||
traceback.print_exc()
|
||||
|
||||
return True
|
||||
|
||||
def get_command(self):
|
||||
""" Get a command input line.
|
||||
If there is no line, return an empty string or None.
|
||||
This may block.
|
||||
|
||||
Subclasses should override this probably.
|
||||
"""
|
||||
if self.cli is None:
|
||||
self.attach_prompt_toolkit()
|
||||
doc = self.cli.run(reset_current_buffer=True)
|
||||
if doc:
|
||||
return doc.text
|
||||
else:
|
||||
return None
|
||||
|
||||
def command_loop(self):
|
||||
""" The main command loop.
|
||||
|
||||
Subclasses could override this, but it's probably not useful to do.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
cmd = self.get_command()
|
||||
if cmd and not self.input_handle(cmd):
|
||||
break
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
38
python_libs/pebble-commander/pebble/commander/parsers.py
Normal file
38
python_libs/pebble-commander/pebble/commander/parsers.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from . import exceptions
|
||||
|
||||
|
||||
def str2bool(s, also_true=[], also_false=[]):
|
||||
s = str(s).lower()
|
||||
if s in ("yes", "on", "t", "1", "true", "enable") or s in also_true:
|
||||
return True
|
||||
if s in ("no", "off", "f", "0", "false", "disable") or s in also_false:
|
||||
return False
|
||||
raise exceptions.ParameterError("%s not a valid bool string." % s)
|
||||
|
||||
|
||||
def str2mac(s):
|
||||
s = str(s)
|
||||
if not re.match(r'[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}', s):
|
||||
raise exceptions.ParameterError('%s is not a valid MAC address' % s)
|
||||
mac = []
|
||||
for byte in str(s).split(':'):
|
||||
mac.append(int(byte, 16))
|
||||
return tuple(mac)
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
CRC_POLY = 0x04C11DB7
|
||||
|
||||
def precompute_table(bits):
|
||||
lookup_table = []
|
||||
for i in xrange(2**bits):
|
||||
rr = i << (32 - bits)
|
||||
for x in xrange(bits):
|
||||
if rr & 0x80000000:
|
||||
rr = (rr << 1) ^ CRC_POLY
|
||||
else:
|
||||
rr <<= 1
|
||||
lookup_table.append(rr & 0xffffffff)
|
||||
return lookup_table
|
||||
|
||||
lookup_table = precompute_table(8)
|
||||
|
||||
def process_word(data, crc=0xffffffff):
|
||||
if (len(data) < 4):
|
||||
# The CRC data is "padded" in a very unique and confusing fashion.
|
||||
data = data[::-1] + '\0' * (4 - len(data))
|
||||
|
||||
for char in reversed(data):
|
||||
b = ord(char)
|
||||
crc = ((crc << 8) ^ lookup_table[(crc >> 24) ^ b]) & 0xffffffff
|
||||
return crc
|
||||
|
||||
def process_buffer(buf, c=0xffffffff):
|
||||
word_count = (len(buf) + 3) / 4
|
||||
|
||||
crc = c
|
||||
for i in xrange(word_count):
|
||||
crc = process_word(buf[i * 4 : (i + 1) * 4], crc)
|
||||
return crc
|
||||
|
||||
def crc32(data):
|
||||
return process_buffer(data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
assert(0x89f3bab2 == process_buffer("123 567 901 34"))
|
||||
assert(0xaff19057 == process_buffer("123456789"))
|
||||
assert(0x519b130 == process_buffer("\xfe\xff\xfe\xff"))
|
||||
assert(0x495e02ca == process_buffer("\xfe\xff\xfe\xff\x88"))
|
||||
|
||||
print "All tests passed!"
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
b = open(sys.argv[1]).read()
|
||||
crc = crc32(b)
|
||||
print "%u or 0x%x" % (crc, crc)
|
56
python_libs/pebble-commander/setup.py
Normal file
56
python_libs/pebble-commander/setup.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from os import path
|
||||
import sys
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='pebble.commander',
|
||||
version='0.0.11',
|
||||
description='Pebble Commander',
|
||||
long_description=long_description,
|
||||
url='https://github.com/pebble/pebble-commander',
|
||||
author='Pebble Technology Corporation',
|
||||
author_email='cory@pebble.com',
|
||||
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||
namespace_packages = ['pebble'],
|
||||
|
||||
install_requires=[
|
||||
'pebble.pulse2>=0.0.7,<1',
|
||||
],
|
||||
|
||||
extras_require = {
|
||||
'Interactive': [
|
||||
'pebble.loghash>=2.6',
|
||||
'prompt_toolkit>=0.55',
|
||||
],
|
||||
},
|
||||
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'pebble-commander = pebble.commander.__main__:main [Interactive]',
|
||||
],
|
||||
},
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue