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

14
sdk/tools/__init__.py Normal file
View file

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

309
sdk/tools/inject_metadata.py Executable file
View file

@ -0,0 +1,309 @@
#!/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 __future__ import with_statement
from struct import pack, unpack
import os
import os.path
import sys
import time
from subprocess import Popen, PIPE
from shutil import copy2
from binascii import crc32
from struct import pack
from pbpack import ResourcePack
import stm32_crc
# Pebble App Metadata Struct
# These are offsets of the PebbleProcessInfo struct in src/fw/app_management/pebble_process_info.h
HEADER_ADDR = 0x0 # 8 bytes
STRUCT_VERSION_ADDR = 0x8 # 2 bytes
SDK_VERSION_ADDR = 0xa # 2 bytes
APP_VERSION_ADDR = 0xc # 2 bytes
LOAD_SIZE_ADDR = 0xe # 2 bytes
OFFSET_ADDR = 0x10 # 4 bytes
CRC_ADDR = 0x14 # 4 bytes
NAME_ADDR = 0x18 # 32 bytes
COMPANY_ADDR = 0x38 # 32 bytes
ICON_RES_ID_ADDR = 0x58 # 4 bytes
JUMP_TABLE_ADDR = 0x5c # 4 bytes
FLAGS_ADDR = 0x60 # 4 bytes
NUM_RELOC_ENTRIES_ADDR = 0x64 # 4 bytes
UUID_ADDR = 0x68 # 16 bytes
RESOURCE_CRC_ADDR = 0x78 # 4 bytes
RESOURCE_TIMESTAMP_ADDR = 0x7c # 4 bytes
VIRTUAL_SIZE_ADDR = 0x80 # 2 bytes
STRUCT_SIZE_BYTES = 0x82
# Pebble App Flags
# These are PebbleAppFlags from src/fw/app_management/pebble_process_info.h
PROCESS_INFO_STANDARD_APP = (0)
PROCESS_INFO_WATCH_FACE = (1 << 0)
PROCESS_INFO_VISIBILITY_HIDDEN = (1 << 1)
PROCESS_INFO_VISIBILITY_SHOWN_ON_COMMUNICATION = (1 << 2)
PROCESS_INFO_ALLOW_JS = (1 << 3)
PROCESS_INFO_HAS_WORKER = (1 << 4)
# Max app size, including the struct and reloc table
# Note that even if the app is smaller than this, it still may be too big, as it needs to share this
# space with applib/ which changes in size from release to release.
MAX_APP_BINARY_SIZE = 0x10000
# This number is a rough estimate, but should not be less than the available space.
# Currently, app_state uses up a small part of the app space.
# See also APP_RAM in stm32f2xx_flash_fw.ld and APP in pebble_app.ld.
MAX_APP_MEMORY_SIZE = 24 * 1024
# This number is a rough estimate, but should not be less than the available space.
# Currently, worker_state uses up a small part of the worker space.
# See also WORKER_RAM in stm32f2xx_flash_fw.ld
MAX_WORKER_MEMORY_SIZE = 10 * 1024
ENTRY_PT_SYMBOL = 'main'
JUMP_TABLE_ADDR_SYMBOL = 'pbl_table_addr'
DEBUG = False
class InvalidBinaryError(Exception):
pass
def inject_metadata(target_binary, target_elf, resources_file, timestamp, allow_js=False,
has_worker=False):
if target_binary[-4:] != '.bin':
raise Exception("Invalid filename <%s>! The filename should end in .bin" % target_binary)
def get_nm_output(elf_file):
nm_process = Popen(['arm-none-eabi-nm', elf_file], stdout=PIPE)
# Popen.communicate returns a tuple of (stdout, stderr)
nm_output = nm_process.communicate()[0]
if not nm_output:
raise InvalidBinaryError()
nm_output = [ line.split() for line in nm_output.splitlines() ]
return nm_output
def get_symbol_addr(nm_output, symbol):
# nm output looks like the following...
#
# U _ITM_registerTMCloneTable
# 00000084 t jump_to_pbl_function
# U _Jv_RegisterClasses
# 0000009c T main
# 00000130 T memset
#
# We don't care about the lines that only have two columns, they're not functions.
for sym in nm_output:
if symbol == sym[-1] and len(sym) == 3:
return int(sym[0], 16)
raise Exception("Could not locate symbol <%s> in binary! Failed to inject app metadata" %
(symbol))
def get_virtual_size(elf_file):
""" returns the virtual size (static memory usage, .text + .data + .bss) in bytes """
readelf_bss_process = Popen("arm-none-eabi-readelf -S '%s'" % elf_file,
shell=True, stdout=PIPE)
readelf_bss_output = readelf_bss_process.communicate()[0]
# readelf -S output looks like the following...
#
# [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
# [ 0] NULL 00000000 000000 000000 00 0 0 0
# [ 1] .header PROGBITS 00000000 008000 000082 00 A 0 0 1
# [ 2] .text PROGBITS 00000084 008084 0006be 00 AX 0 0 4
# [ 3] .rel.text REL 00000000 00b66c 0004d0 08 23 2 4
# [ 4] .data PROGBITS 00000744 008744 000004 00 WA 0 0 4
# [ 5] .bss NOBITS 00000748 008748 000054 00 WA 0 0 4
last_section_end_addr = 0
# Find the .bss section and calculate the size based on the end of the .bss section
for line in readelf_bss_output.splitlines():
if len(line) < 10:
continue
# Carve off the first column, since it sometimes has a space in it which screws up the
# split. Two leading spaces, a square bracket, 2 digits (with space padding),
# a second square brack is 6
line = line[6:]
columns = line.split()
if len(columns) < 6:
continue
if columns[0] == '.bss':
addr = int(columns[2], 16)
size = int(columns[4], 16)
last_section_end_addr = addr + size
elif columns[0] == '.data' and last_section_end_addr == 0:
addr = int(columns[2], 16)
size = int(columns[4], 16)
last_section_end_addr = addr + size
if last_section_end_addr != 0:
return last_section_end_addr
sys.stderr.writeline("Failed to parse ELF sections while calculating the virtual size\n")
sys.stderr.write(readelf_bss_output)
raise Exception("Failed to parse ELF sections while calculating the virtual size")
def get_relocate_entries(elf_file):
""" returns a list of all the locations requiring an offset"""
# TODO: insert link to the wiki page I'm about to write about PIC and relocatable values
entries = []
# get the .data locations
readelf_relocs_process = Popen(['arm-none-eabi-readelf', '-r', elf_file], stdout=PIPE)
readelf_relocs_output = readelf_relocs_process.communicate()[0]
lines = readelf_relocs_output.splitlines()
i = 0
reading_section = False
while i < len(lines):
if not reading_section:
# look for the next section
if lines[i].startswith("Relocation section '.rel.data"):
reading_section = True
i += 1 # skip the column title section
else:
if len(lines[i]) == 0:
# end of the section
reading_section = False
else:
entries.append(int(lines[i].split(' ')[0], 16))
i += 1
# get any Global Offset Table (.got) entries
readelf_relocs_process = Popen(['arm-none-eabi-readelf', '--sections', elf_file],
stdout=PIPE)
readelf_relocs_output = readelf_relocs_process.communicate()[0]
lines = readelf_relocs_output.splitlines()
for line in lines:
# We shouldn't need to do anything with the Procedure Linkage Table since we don't
# actually export functions
if '.got' in line and '.got.plt' not in line:
words = line.split(' ')
while '' in words:
words.remove('')
section_label_idx = words.index('.got')
addr = int(words[section_label_idx + 2], 16)
length = int(words[section_label_idx + 4], 16)
for i in range(addr, addr + length, 4):
entries.append(i)
break
return entries
nm_output = get_nm_output(target_elf)
try:
app_entry_address = get_symbol_addr(nm_output, ENTRY_PT_SYMBOL)
except:
raise Exception("Missing app entry point! Must be `int main(void) { ... }` ")
jump_table_address = get_symbol_addr(nm_output, JUMP_TABLE_ADDR_SYMBOL)
reloc_entries = get_relocate_entries(target_elf)
statinfo = os.stat(target_binary)
app_load_size = statinfo.st_size
if resources_file is not None:
with open(resources_file, 'rb') as f:
pbpack = ResourcePack.deserialize(f, is_system=False)
resource_crc = pbpack.get_content_crc()
else:
resource_crc = 0
if DEBUG:
copy2(target_binary, target_binary + ".orig")
with open(target_binary, 'r+b') as f:
total_app_image_size = app_load_size + (len(reloc_entries) * 4)
if total_app_image_size > MAX_APP_BINARY_SIZE:
raise Exception("App image size is %u (app %u relocation table %u). Must be smaller "
"than %u bytes" % (total_app_image_size,
app_load_size,
len(reloc_entries) * 4,
MAX_APP_BINARY_SIZE))
def read_value_at_offset(offset, format_str, size):
f.seek(offset)
return unpack(format_str, f.read(size))
app_bin = f.read()
app_crc = stm32_crc.crc32(app_bin[STRUCT_SIZE_BYTES:])
[app_flags] = read_value_at_offset(FLAGS_ADDR, '<L', 4)
if allow_js:
app_flags = app_flags | PROCESS_INFO_ALLOW_JS
if has_worker:
app_flags = app_flags | PROCESS_INFO_HAS_WORKER
app_virtual_size = get_virtual_size(target_elf)
struct_changes = {
'load_size' : app_load_size,
'entry_point' : "0x%08x" % app_entry_address,
'symbol_table' : "0x%08x" % jump_table_address,
'flags' : app_flags,
'crc' : "0x%08x" % app_crc,
'num_reloc_entries': "0x%08x" % len(reloc_entries),
'resource_crc' : "0x%08x" % resource_crc,
'timestamp' : timestamp,
'virtual_size': app_virtual_size
}
def write_value_at_offset(offset, format_str, value):
f.seek(offset)
f.write(pack(format_str, value))
write_value_at_offset(LOAD_SIZE_ADDR, '<H', app_load_size)
write_value_at_offset(OFFSET_ADDR, '<L', app_entry_address)
write_value_at_offset(CRC_ADDR, '<L', app_crc)
write_value_at_offset(RESOURCE_CRC_ADDR, '<L', resource_crc)
write_value_at_offset(RESOURCE_TIMESTAMP_ADDR, '<L', timestamp)
write_value_at_offset(JUMP_TABLE_ADDR, '<L', jump_table_address)
write_value_at_offset(FLAGS_ADDR, '<L', app_flags)
write_value_at_offset(NUM_RELOC_ENTRIES_ADDR, '<L', len(reloc_entries))
write_value_at_offset(VIRTUAL_SIZE_ADDR, "<H", app_virtual_size)
# Write the reloc_entries past the end of the binary. This expands the size of the binary,
# but this new stuff won't actually be loaded into ram.
f.seek(app_load_size)
for entry in reloc_entries:
f.write(pack('<L', entry))
f.flush()
return struct_changes

138
sdk/tools/memory_reports.py Normal file
View file

@ -0,0 +1,138 @@
# 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.
def _convert_bytes_to_kilobytes(number_bytes):
"""
Convert the input from bytes into kilobytes
:param number_bytes: the number of bytes to convert
:return: the input value converted to kilobytes
"""
NUMBER_BYTES_IN_KBYTE = 1024
return int(number_bytes) / NUMBER_BYTES_IN_KBYTE
def app_memory_report(platform_name, bin_type, app_size, max_ram, free_ram, resource_size=None,
max_resource_size=None):
"""
This method provides a formatted string for printing the memory usage of this binary to the
console.
:param platform_name: the name of the current HW platform being targeted
:param bin_type: the type of binary being built (app, lib, worker)
:param app_size: the size of the binary
:param max_ram: the maximum allowed size of the binary
:param free_ram: the amount of remaining memory
:param resource_size: the size of the resource pack
:param max_resource_size: the maximum allowed size of the resource pack
:return: a tuple containing the color for the string print, and the string to print
"""
LABEL = "-------------------------------------------------------\n{} {} MEMORY USAGE\n"
RESOURCE_SIZE = "Total size of resources: {} bytes / {}KB\n"
MEMORY_USAGE = ("Total footprint in RAM: {} bytes / {}KB\n"
"Free RAM available (heap): {} bytes\n"
"-------------------------------------------------------")
if resource_size and max_resource_size:
report = (LABEL.format(platform_name.upper(), bin_type.upper()) +
RESOURCE_SIZE.format(resource_size,
_convert_bytes_to_kilobytes(max_resource_size)) +
MEMORY_USAGE.format(app_size, _convert_bytes_to_kilobytes(max_ram), free_ram))
else:
report = (LABEL.format(platform_name.upper(), bin_type.upper()) +
MEMORY_USAGE.format(app_size, _convert_bytes_to_kilobytes(max_ram), free_ram))
return 'YELLOW', report
def app_resource_memory_error(platform_name, resource_size, max_resource_size):
"""
This method provides a formatted error message for printing to the console when the resource
size exceeds the maximum resource size supported by the Pebble firmware.
:param platform_name: the name of the current HW platform being targeted
:param resource_size: the size of the resource pack
:param max_resource_size: the maximum allowed size of the resource pack
:return: a tuple containing the color for the string print, and the string to print
"""
report = ("======================================================\n"
"Build failed: {}\n"
"Error: Resource pack is too large ({}KB / {}KB)\n"
"======================================================\n".
format(platform_name,
_convert_bytes_to_kilobytes(resource_size),
_convert_bytes_to_kilobytes(max_resource_size)))
return 'RED', report
def app_appstore_resource_memory_error(platform_name, resource_size, max_appstore_resource_size):
"""
This method provides a formatted warning message for printing to the console when the resource
pack size exceeds the maximum allowed resource size for the appstore.
:param platform_name: the name of the current HW platform being targeted
:param resource_size: the size of the resource pack
:param max_appstore_resource_size: the maximum appstore-allowed size of the resource pack
:return: a tuple containing the color for the string print, and the string to print
"""
report = ("WARNING: Your {} app resources are too large ({}KB / {}KB). You will not be "
"able "
"to publish your app.\n".
format(platform_name,
_convert_bytes_to_kilobytes(resource_size),
_convert_bytes_to_kilobytes(max_appstore_resource_size)))
return 'RED', report
def bytecode_memory_report(platform_name, bytecode_size, bytecode_max):
"""
This method provides a formatted string for printing the memory usage for this Rocky bytecode
file to the console.
:param platform_name: the name of the current HW platform being targeted
:param bytecode_size: the size of the bytecode file, in bytes
:param bytecode_max: the max allowed size of the bytecode file, in bytes
:return: a tuple containing the color for the string print, and the string to print
"""
LABEL = "-------------------------------------------------------\n{} MEMORY USAGE\n"
BYTECODE_USAGE = ("Total size of snapshot: {}KB / {}KB\n"
"-------------------------------------------------------")
report = (LABEL.format(platform_name.upper()) +
BYTECODE_USAGE.format(_convert_bytes_to_kilobytes(bytecode_size),
_convert_bytes_to_kilobytes(bytecode_max)))
return 'YELLOW', report
def simple_memory_report(platform_name, bin_size, resource_size=None):
"""
This method provides a formatted string for printing the memory usage for this binary to the
console.
:param platform_name: the name of the current HW platform being targeted
:param bin_size: the size of the binary
:param resource_size: the size of the resource pack
:return: a tuple containing the color for the string print, and the string to print
"""
LABEL = "-------------------------------------------------------\n{} MEMORY USAGE\n"
RESOURCE_SIZE = "Total size of resources: {} bytes\n"
MEMORY_USAGE = ("Total footprint in RAM: {} bytes\n"
"-------------------------------------------------------")
if resource_size:
report = (LABEL.format(platform_name.upper()) +
RESOURCE_SIZE.format(resource_size) +
MEMORY_USAGE.format(bin_size))
else:
report = (LABEL.format(platform_name.upper()) +
MEMORY_USAGE.format(bin_size))
return 'YELLOW', report

112
sdk/tools/pebble_package.py Normal file
View file

@ -0,0 +1,112 @@
# 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
import argparse
import errno
import os
from shutil import rmtree
import zipfile
class MissingFileException(Exception):
pass
class DuplicatePackageFileException(Exception):
pass
def _calculate_file_size(path):
return os.stat(path).st_size
def _calculate_crc(path):
pass
class PebblePackage(object):
def __init__(self, package_filename):
self.package_filename = package_filename
self.package_files = {}
def add_file(self, name, file_path):
if not os.path.exists(file_path):
raise MissingFileException("The file '{}' does not exist".format(file_path))
if name in self.package_files and self.package_files.get(name) != file_path:
raise DuplicatePackageFileException("The file '{}' cannot be added to the package "
"because `{}` has already been assigned to `{}`".
format(file_path,
self.package_files.get(name),
name))
else:
self.package_files[name] = file_path
def pack(self, package_path=None):
with zipfile.ZipFile(os.path.join(package_path, self.package_filename), 'w') as zip_file:
for filename, file_path in self.package_files.iteritems():
zip_file.write(file_path, filename)
zip_file.comment = type(self).__name__
def unpack(self, package_path=''):
try:
rmtree(package_path)
except OSError as e:
if e.errno != errno.ENOENT:
raise e
with zipfile.ZipFile(self.package_filename, 'r') as zip_file:
zip_file.extractall(package_path)
class RockyPackage(PebblePackage):
def __init__(self, package_filename):
super(RockyPackage, self).__init__(package_filename)
def add_files(self, rockyjs, binaries, resources, pkjs, platforms):
for platform in platforms:
self.add_file(os.path.join(platform, rockyjs[platform]), rockyjs[platform])
self.add_file(os.path.join(platform, binaries[platform]), binaries[platform])
self.add_file(os.path.join(platform, resources[platform]), resources[platform])
self.add_file(pkjs, pkjs)
def write_manifest(self):
pass
class LibraryPackage(PebblePackage):
def __init__(self, package_filename="dist.zip"):
super(LibraryPackage, self).__init__(package_filename)
def add_files(self, includes, binaries, resources, js):
for include, include_path in includes.iteritems():
self.add_file(os.path.join('include', include), include_path)
for binary, binary_path in binaries.iteritems():
self.add_file(os.path.join('binaries', binary), binary_path)
for resource, resource_path in resources.iteritems():
self.add_file(os.path.join('resources', resource), resource_path)
for js_file, js_file_path in js.iteritems():
self.add_file(os.path.join('js', js_file), js_file_path)
def unpack(self, package_path='dist'):
super(LibraryPackage, self).unpack(package_path)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Manage Pebble packages")
parser.add_argument('command', type=str, help="Command to use")
parser.add_argument('filename', type=str, help="Path to your Pebble package")
args = parser.parse_args()
with zipfile.ZipFile(args.filename, 'r') as package:
cls = globals()[package.comment](args.filename)
getattr(cls, args.command)()

212
sdk/tools/rocky-lint/rocky.d.ts vendored Normal file
View file

@ -0,0 +1,212 @@
/**
* 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.
*/
declare namespace rocky {
// helper type to indicate that a commonly expected feature is planned but not implement, yet
interface IsNotImplementedInRockyYet {
_doesNotWork: any
}
interface Event {
type: string
}
interface DrawEvent extends Event {
context: CanvasRenderingContext2D
}
interface TickEvent extends Event {
date: Date
}
interface MemoryPressureEvent extends Event {
level: 'high';
}
interface MessageEvent extends Event {
data: any;
}
interface PostMessageConnectionEvent extends Event {
}
interface AnyEvent extends Event, DrawEvent, TickEvent, MemoryPressureEvent, MessageEvent, PostMessageConnectionEvent { }
interface CanvasRenderingContext2D {
canvas: CanvasElement
fillStyle: string
font: string // TODO list actually supported fonts
lineWidth: number
strokeStyle: string
textAlign: string // TODO list actually supported values
textBaseline: IsNotImplementedInRockyYet
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
arcTo(IsNotImplementedInRockyYet : number, y1: number, x2: number, y2: number, radius: number): void
beginPath(): void
bezierCurveTo(cp1x: IsNotImplementedInRockyYet , cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
clearRect(x: number, y: number, w: number, h: number): void
closePath(): void
drawImage(image: IsNotImplementedInRockyYet, offsetX: number, offsetY: number, width?: number, height?: number, canvasOffsetX?: number, canvasOffsetY?: number, canvasImageWidth?: number, canvasImageHeight?: number): void
fill(fillRule?: string): void
fillRect(x: number, y: number, w: number, h: number): void
fillText(text: string, x: number, y: number, maxWidth?: number): void
lineTo(x: number, y: number): void
measureText(text: string): TextMetrics
moveTo(x: number, y: number): void
quadraticCurveTo(cpx: IsNotImplementedInRockyYet, cpy: number, x: number, y: number): void
rect(x: number, y: number, w: number, h: number): void
restore(): void
rotate(angle: IsNotImplementedInRockyYet): void
save(): void
scale(x: IsNotImplementedInRockyYet , y: number): void
setTransform(m11: IsNotImplementedInRockyYet, m12: number, m21: number, m22: number, dx: number, dy: number): void
stroke(): void
strokeRect(x: number, y: number, w: number, h: number): void
transform(m11: IsNotImplementedInRockyYet, m12: number, m21: number, m22: number, dx: number, dy: number): void
translate(x: IsNotImplementedInRockyYet , y: number): void
rockyFillRadial(x: number, y: number, innerRadius: number, outerRadius: number, startAngle: number, endAngle: number): void
}
interface TextMetrics {
width: number
height: number
}
interface CanvasElement {
clientWidth: number
clientHeight: number
unobstructedWidth: number
unobstructedHeight: number
unobstructedTop: number
unobstructedLeft: number
}
interface WatchInfo {
platform: string
model: string
language: string
firmware: { major: number, minor: number, patch: number, suffix: string }
}
interface UserPreferences {
contentSize: "small" | "medium" | "large" | "x-large"
}
interface Rocky {
on(eventName: "draw", eventListener: (event: DrawEvent) => void): void
on(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
on(eventName: "message", eventListener: (event: MessageEvent) => void): void
on(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
on(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
on(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
on(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
on(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
on(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
on(eventName: "daychange", eventListener: (event: TickEvent) => void): void
on(eventName: string, eventListener: (event: AnyEvent) => void): void
addEventListener(eventName: "draw", eventListener: (event: DrawEvent) => void): void
addEventListener(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
addEventListener(eventName: "message", eventListener: (event: MessageEvent) => void): void
addEventListener(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
addEventListener(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
addEventListener(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
addEventListener(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
addEventListener(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
addEventListener(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
addEventListener(eventName: "daychange", eventListener: (event: TickEvent) => void): void
addEventListener(eventName: string, eventListener: (event: AnyEvent) => void): void
off(eventName: "draw", eventListener: (event: DrawEvent) => void): void
off(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
off(eventName: "message", eventListener: (event: MessageEvent) => void): void
off(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
off(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
off(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
off(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
off(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
off(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
off(eventName: "daychange", eventListener: (event: TickEvent) => void): void
off(eventName: string, eventListener: (event: AnyEvent) => void): void
removeEventListener(eventName: "draw", eventListener: (event: DrawEvent) => void): void
removeEventListener(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
removeEventListener(eventName: "message", eventListener: (event: MessageEvent) => void): void
removeEventListener(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
removeEventListener(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
removeEventListener(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
removeEventListener(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
removeEventListener(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
removeEventListener(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
removeEventListener(eventName: "daychange", eventListener: (event: TickEvent) => void): void
removeEventListener(eventName: string, eventListener: (event: AnyEvent) => void): void
postMessage(message: any): void
requestDraw(): void
watchInfo: WatchInfo
userPreferences: UserPreferences
Event: Event
CanvasRenderingContext2D: CanvasRenderingContext2D
CanvasElement: CanvasElement
}
}
declare module 'rocky' {
var rocky: rocky.Rocky;
export = rocky
}
interface Console {
error(message?: string, ...optionalParams: any[]): void
log(message?: string, ...optionalParams: any[]): void
warn(message?: string, ...optionalParams: any[]): void
}
declare var console: Console;
interface clearInterval {
(handle: number): void
}
declare var clearInterval: clearInterval;
interface clearTimeout {
(handle: number): void
}
declare var clearTimeout: clearTimeout;
interface setInterval {
(handler: (...args: any[]) => void, timeout: number): number
}
declare var setInterval: setInterval;
interface setTimeout {
(handler: (...args: any[]) => void, timeout: number): number
}
declare var setTimeout: setTimeout;
interface Require {
(id: string): any
}
interface RockyRequire extends Require {
(id: 'rocky'): rocky.Rocky
}
declare var require: RockyRequire;
interface Module {
exports: any
}
declare var module: Module;

View file

@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema",
"description": "A project containing a Pebble application",
"type": "object",
"$ref": "file_types.json#/appinfo-json"
}

View file

@ -0,0 +1,76 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema for Attributes",
"description": "Schema for each type of valid attribute in Pebble projects",
"appKeys": {
"type": "object",
"patternProperties": {
"^\\w*$": { "$ref": "data_types.json#/UInt32" }
},
"additionalProperties": false
},
"capabilities": {
"type": "array",
"items": { "enum": ["location", "configurable", "health"] },
"uniqueItems": true
},
"messageKeys": {
"oneOf": [
{ "$ref": "attributes.json#/appKeys" },
{ "$ref": "data_types.json#/identifierArray" }
]
},
"resources": {
"type": "object",
"properties": {
"media": {
"type": "array",
"items": {
"type": "object",
"oneOf": [
{ "$ref": "resource_types.json#/bitmap" },
{ "$ref": "resource_types.json#/deprecatedImageFormat" },
{ "$ref": "resource_types.json#/font" },
{ "$ref": "resource_types.json#/raw" }
]
},
"uniqueItems": true
},
"publishedMedia": {
"type": "array",
"items": {
"type": "object",
"oneOf": [
{ "$ref": "resource_types.json#/publishedMediaAlias" },
{ "$ref": "resource_types.json#/publishedMediaGlance" },
{ "$ref": "resource_types.json#/publishedMediaTimeline" }
]
},
"uniqueItems": true
}
},
"additionalProperties": false,
"dependencies": {
"publishedMedia": [ "media" ]
}
},
"sdkVersion": { "enum": [ "2", "3" ] },
"targetPlatforms": {
"type": "array",
"items": { "enum": [ "aplite", "basalt", "chalk", "diorite" ] },
"uniqueItems": true
},
"uuid": {
"type": "string",
"pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
},
"watchapp": {
"type": "object",
"properties": {
"watchface": { "type": "boolean" },
"hiddenApp": { "type": "boolean" },
"onlyShownOnCommunication": { "type": "boolean" }
},
"additionalProperties": false
}
}

View file

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema for Types",
"description": "Schema for complex data types in Pebble projects",
"UInt8": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"UInt32": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"identifier": {
"type": "string",
"pattern": "^\\w*$"
},
"identifierArray": {
"type": "array",
"items": { "$ref": "#/identifier" }
},
"stringArray": {
"type": "array",
"items": { "type": "string" }
}
}

View file

@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema for Project JSON Files",
"description": "Schema for supported JSON Pebble project files",
"package-json": {
"properties": {
"name": { "type": "string" },
"author": {
"description": "https://docs.npmjs.com/files/package.json#people-fields-author-contributors",
"oneOf": [
{
"type": "string",
"pattern": "^([^<(]+?)?[ \\\\t]*(?:<([^>(]+?)>)?[ \\\\t]*(?:\\\\(([^)]+?)\\\\)|$)"
},
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"url": { "type": "string" }
},
"additionalProperties": false
}
]
},
"version": { "type": "string" },
"keywords": { "$ref": "data_types.json#/stringArray" },
"private": { "type": "boolean" },
"dependencies": {
"type": "object",
"patternProperties": {
".": { "type": "string" }
},
"additionalProperties": false
},
"files": { "$ref": "data_types.json#/stringArray" },
"pebble": {
"type": "object",
"oneOf": [
{ "$ref": "project_types.json#/native-app" },
{ "$ref": "project_types.json#/rocky-app" },
{ "$ref": "project_types.json#/package" }
]
}
},
"required": [ "name", "author", "version", "pebble" ]
},
"appinfo-json": {
"properties": {
"uuid": { "$ref": "attributes.json#/uuid" },
"shortName": { "type": "string" },
"longName": { "type": "string" },
"companyName": { "type": "string" },
"versionCode": { "$ref": "data_types.json#/UInt8" },
"versionLabel": { "type": "string" },
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
"watchapp": { "$ref": "attributes.json#/watchapp" },
"appKeys": { "$ref": "attributes.json#/appKeys" },
"resources": { "$ref": "attributes.json#/resources" },
"capabilities": { "$ref": "attributes.json#/capabilities" },
"enableMultiJS": { "type": "boolean" },
"projectType": { "enum": [ "native", "pebblejs" ] }
},
"required": ["uuid", "longName", "companyName", "versionLabel"]
}
}

View file

@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema",
"description": "A project containing a Pebble application",
"type": "object",
"$ref": "file_types.json#/package-json"
}

View file

@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema for Project Types",
"description": "Schema for each type of valid Pebble project",
"native-app": {
"properties": {
"displayName": { "type": "string" },
"uuid": { "$ref": "attributes.json#/uuid" },
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
"projectType": { "enum": [ "native" ] },
"enableMultiJS": { "type": "boolean" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
"watchapp": { "$ref": "attributes.json#/watchapp" },
"capabilities": { "$ref": "attributes.json#/capabilities" },
"appKeys": { "$ref": "attributes.json#/appKeys" },
"messageKeys": { "$ref": "attributes.json#/messageKeys" }
},
"required": [ "displayName", "uuid", "sdkVersion" ]
},
"rocky-app": {
"properties": {
"displayName": { "type": "string" },
"uuid": { "$ref": "attributes.json#/uuid" },
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
"projectType": { "enum": [ "rocky" ] },
"enableMultiJS": { "type": "boolean" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
"watchapp": { "$ref": "attributes.json#/watchapp" },
"capabilities": { "$ref": "attributes.json#/capabilities" }
},
"required": [ "displayName", "uuid", "sdkVersion", "projectType" ]
},
"package": {
"properties": {
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
"projectType": { "enum": [ "package" ] },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
"capabilities": { "$ref": "attributes.json#/capabilities" },
"messageKeys": { "$ref": "attributes.json#/messageKeys" }
},
"required": [ "sdkVersion", "projectType" ]
}
}

View file

@ -0,0 +1,84 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Pebble JSON Schema for Resource Types",
"description": "Schema for each type of valid resource in Pebble projects",
"bitmap": {
"properties": {
"name": { "$ref": "data_types.json#/identifier" },
"type": { "enum": ["bitmap"] },
"file": { "type": "string" },
"menuIcon": { "type": "boolean" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
"storageFormat": { "enum": [ "pbi", "png" ] },
"memoryFormat": {
"enum": [
"Smallest",
"SmallestPalette",
"1Bit",
"8Bit",
"1BitPalette",
"2BitPalette",
"4BitPalette"
]
},
"spaceOptimization": { "enum": [ "storage", "memory" ] }
}
},
"deprecatedImageFormat": {
"properties": {
"name": { "$ref": "data_types.json#/identifier" },
"type": { "enum": ["png", "pbi", "pbi8", "png-trans"] },
"file": { "type": "string" },
"menuIcon": { "type": "boolean" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" }
},
"required": ["name", "type", "file"]
},
"font": {
"properties": {
"name": { "$ref": "data_types.json#/identifier" },
"type": { "enum": ["font"] },
"file": { "type": "string" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
"characterRegex": { "type": "string" }
},
"required": ["name", "type", "file"]
},
"raw": {
"properties": {
"name": { "$ref": "data_types.json#/identifier" },
"type": { "enum": ["raw"] },
"file": { "type": "string" },
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" }
},
"required": ["name", "type", "file"]
},
"publishedMediaItem": {
"name": { "$ref": "data_types.json#/identifier" },
"id": { "$ref": "data_types.json#/UInt32" },
"alias": { "$ref": "data_types.json#/identifier" },
"glance": { "$ref": "data_types.json#/identifier" },
"timeline": {
"type": "object",
"properties": {
"tiny": { "$ref": "data_types.json#/identifier" },
"small": { "$ref": "data_types.json#/identifier" },
"large": { "$ref": "data_types.json#/identifier" }
},
"required": [ "tiny" ]
}
},
"publishedMediaAlias": {
"properties": { "$ref": "#/publishedMediaItem" },
"required": ["name", "id", "alias"]
},
"publishedMediaGlance": {
"properties": { "$ref": "#/publishedMediaItem" },
"required": ["name", "id", "glance"]
},
"publishedMediaTimeline": {
"properties": { "$ref": "#/publishedMediaItem" },
"required": ["name", "id", "timeline"]
}
}

View 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.
*/
var fs = require('fs');
module.exports = function(source) {
// Set this loader to cacheable
this.cacheable();
// Whitelist files in the current project
var whitelisted_folders = [this.options.context];
// Whitelist files from the SDK-appended search paths
whitelisted_folders = whitelisted_folders.concat(this.options.resolve.root);
// Iterate over whitelisted file paths
for (var i=0; i<whitelisted_folders.length; i++) {
// If resource file is from a whitelisted path, return source
if (~this.resourcePath.indexOf(fs.realpathSync(whitelisted_folders[i]))) {
return source;
}
}
// If the resource file is not from a whitelisted path, emit an error and fail the build
this.emitError("Requiring a file outside of the current project folder is not permitted.");
return "";
};

View file

@ -0,0 +1,96 @@
////////////////////////////////////////////////////////////////////////////////
// Template vars injected by projess_js.py:
// boolean
const isSandbox = ${IS_SANDBOX};
// Array with absolute file path strings
const entryFilenames = ${ENTRY_FILENAMES};
// folder path string
const outputPath = ${OUTPUT_PATH};
// file name string
const outputFilename = ${OUTPUT_FILENAME};
// Array with absolute folder path strings
const resolveRoots = ${RESOLVE_ROOTS};
// Object, { alias1: 'path1', ... }
const resolveAliases = ${RESOLVE_ALIASES};
// null or Object with key 'sourceMapFilename'
const sourceMapConfig = ${SOURCE_MAP_CONFIG};
////////////////////////////////////////////////////////////////////////////////
// NOTE: Must escape dollar-signs, because this is a Python template!
const webpack = require('webpack');
module.exports = (() => {
// The basic config:
const config = {
entry: entryFilenames,
output: {
path: outputPath,
filename: outputFilename
},
target: 'node',
resolve: {
root: resolveRoots,
extensions: ['', '.js', '.json'],
alias: resolveAliases
},
resolveLoader: {
root: resolveRoots
}
};
if (sourceMapConfig) {
// Enable webpack's source map output:
config.devtool = 'source-map';
config.output.sourceMapFilename = sourceMapConfig.sourceMapFilename;
config.output.devtoolModuleFilenameTemplate = '[resource-path]';
config.output.devtoolFallbackModuleFilenameTemplate = '[resourcePath]?[hash]';
}
return config;
})();
module.exports.plugins = (() => {
const plugins = [
// Returns a non-zero exit code when webpack reports an error:
require('webpack-fail-plugin'),
// Includes _message_keys_wrapper in every build to mimic old loader.js:
new webpack.ProvidePlugin({ require: '_message_key_wrapper' })
];
if (isSandbox) {
// Prevents using `require('evil_loader!mymodule')` to execute custom
// loader code during the webpack build.
const RestrictResourcePlugin = require('restrict-resource-webpack-plugin');
const plugin = new RestrictResourcePlugin(/!+/,
'Custom inline loaders are not permitted.');
plugins.push(plugin);
}
return plugins;
})();
module.exports.module = {
loaders: (() => {
const loaders = [{'test': /\.json$$/, 'loader': 'json-loader'}];
if (isSandbox) {
// See restricted-resource-loader.js, prevents loading files outside
// of the project folder, i.e. `require(../../not_your_business)`:
const restrictLoader = {
'test': /^.*/, 'loader': 'restricted-resource-loader'
};
loaders.push(restrictLoader);
}
return loaders;
})()
};