Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View 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, InteractivePebbleCommander
from . import _commands

View file

@ -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

View file

@ -0,0 +1,84 @@
# 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)

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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:]]

View 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

View file

@ -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 image_resources(cmdr, pack='build/system_resources.pbpack'):
""" Image resources.
"""
import pulse_flash_imaging
pulse_flash_imaging.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.
"""
import pulse_flash_imaging
if address is not None:
address = int(str(address), 0)
pulse_flash_imaging.load_firmware(cmdr.connection, firm,
verbose=cmdr.interactive, address=address)

View file

@ -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")

View file

@ -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:]]

View file

@ -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)

View 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 .. 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)

View file

@ -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)

View file

@ -0,0 +1,86 @@
# 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 tempfile
import png
from bitarray import bitarray
from .. import PebbleCommander, exceptions, parsers
def chunks(l, n):
for i in xrange(0, len(l), n):
yield l[i:i+n]
def convert_8bpp(data):
ret = []
for pixel in data:
pixel = ord(pixel)
red, green, blue = (((pixel >> n) & 0b11) * 255 / 3 for n in (4, 2, 0))
ret.extend((red, green, blue))
return ret
def convert_1bpp(data, width):
# Chop off the unused bytes at the end of each row
bytes_per_row = (width / 32 + 1) * 4
data = ''.join(c[:width/8] for c in chunks(data, bytes_per_row))
ba = bitarray(endian='little')
ba.frombytes(data)
return ba.tolist()
def framebuffer_to_png(data, png_path, width, bpp):
if bpp == 1:
data = convert_1bpp(data, width)
channels = 'L'
elif bpp == 8:
data = convert_8bpp(data)
channels = 'RGB'
data = list(chunks(data, width*len(channels)))
png.from_array(data, mode='%s;%d' % (channels, bpp)).save(png_path)
@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")
@PebbleCommander.command()
def screenshot(cmdr, filename=None):
""" Take a screenshot.
"""
if filename is None:
filename = tempfile.mktemp(suffix='.png')
with cmdr.connection.bulkio.open('framebuffer') as handle:
attrs = handle.stat()
data = str(handle.read(attrs.length))
framebuffer_to_png(data, filename, attrs.width, attrs.bpp)
return filename

View file

@ -0,0 +1,266 @@
#!/usr/bin/env python
# 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 datetime import datetime
import json
import logging
import os
import re
import readline
import shlex
import sys
import threading
import time
import tokenize
import traceback
import types
import unicodedata as ud
import prompt_toolkit
from log_hashing.logdehash import LogDehash
import pulse
class PebbleCommander(object):
""" Pebble Commander.
Implements everything for interfacing with PULSE things.
"""
def __init__(self, tty=None, interactive=False):
self.connection = pulse.socket.Connection.open_dbgserial(
url=tty, infinite_reconnect=interactive)
self.connection.change_baud_rate(921600)
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()
def __del__(self):
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):
try:
self.connection.close()
except:
pass
def _start_logging(self):
""" Thread to handle logging messages.
"""
while True:
msg = self.connection.logging.receive()
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
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):
self.cmdr = PebbleCommander(tty=tty, interactive=True)
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

View file

@ -0,0 +1,24 @@
# 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

View file

@ -0,0 +1,36 @@
# 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 re
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)