add a python script to automatic generate all screenshots

This commit is contained in:
Jenny Diomidova 2025-12-10 16:06:21 +01:00
parent d30dfa5d57
commit 531d5bdb6f
17 changed files with 203 additions and 40 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

32
flake.lock generated
View File

@ -113,6 +113,20 @@
}
},
"nixpkgs": {
"locked": {
"lastModified": 1765270179,
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1734649271,
"narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=",
@ -133,24 +147,26 @@
"commit-hooks": "commit-hooks",
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs_2"
},
"locked": {
"dirtyRev": "3fe382c507afd73a2c6a51ece280c3fb567ac53a-dirty",
"dirtyShortRev": "3fe382c-dirty",
"lastModified": 1764961369,
"narHash": "sha256-TE0XcWkJNwMVKonZ8y/c4kVLEp71A+45T5Fys2Jv2uI=",
"type": "git",
"url": "file:///home/diomidova/Pebble/pebble.nix"
"narHash": "sha256-FPMMcBAoeQgA/W92XTOAmTdDMmhbpNgsuSia7BMOBUs=",
"owner": "diomidova",
"repo": "pebble.nix",
"rev": "3fe382c507afd73a2c6a51ece280c3fb567ac53a",
"type": "github"
},
"original": {
"type": "git",
"url": "file:///home/diomidova/Pebble/pebble.nix"
"owner": "diomidova",
"repo": "pebble.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"pebble": "pebble"
}
},

View File

@ -8,10 +8,19 @@
};
outputs =
{ pebble, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system: {
{ pebble, flake-utils, nixpkgs, ... }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs {
overlays = [ ];
inherit system;
};
in
{
devShell = pebble.pebbleEnv.${system} {
withCoreDevices = true;
packages = [
pkgs.python312Packages.ipython
];
};
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -15,11 +15,13 @@ namespace {
void DigitLayerInner::OnUpdate(const Pebble::GContext& ctx) {
auto bounds = this->GetBounds();
// LOG("Digit layer update. %d digits", this->digitCount);
ctx.SetCompositingMode(GCompOpSet);
for (int i = 0; i <= this->digitCount + 2 * this->extraDigits; i++) {
Pebble::GRect rect(0, i * this->digitHeight, bounds.size.w, this->digitHeight);
uint16_t digit = ((i - this->extraDigits) % this->digitCount + this->digitCount) % this->digitCount % 10;
// LOG("Digit %d -> %d", i - this->extraDigits, digit);
draw_digit(ctx, rect, digit);
}
}
@ -46,6 +48,7 @@ int16_t DigitLayer::ComputeOffset(uint16_t digit) {
}
void DigitLayer::RotateTo(uint16_t digit, bool animate) {
// LOG("Digit %d, out of %d", digit, innerLayer.digitCount);
digit %= innerLayer.digitCount;
auto currentOffset = this->scrollLayer.GetContentOffset();
if (digit == 0 && currentOffset.y <= ComputeOffset(this->innerLayer.digitCount / 2)) {

View File

@ -9,11 +9,6 @@ class DigitLayerInner : public Pebble::CustomLayer {
DigitLayerInner() : Pebble::CustomLayer(Pebble::GRect()) {}
void SetDigitCount(uint16_t cnt = 10) {
this->digitCount = cnt;
this->MarkDirty();
}
static void DrawDigit(const Pebble::GContext& ctx, const Pebble::GRect& bounds, const uint16_t digit);
friend class DigitLayer;
@ -29,8 +24,10 @@ public:
DigitLayer();
void SetDigitCount(uint16_t cnt = 10) {
this->innerLayer.SetDigitCount(cnt);
this->MarkDirty();
if (this->innerLayer.digitCount != cnt) {
this->innerLayer.digitCount = cnt;
this->UpdateLayout();
}
}
void UpdateLayout();
void RotateTo(uint16_t digit, bool animate=true);

View File

@ -7,7 +7,8 @@
using namespace Pebble;
#undef DEBUG
// For debugging
const bool USE_SECONDS_INSTEAD_OF_MINUTES = false;
class MainWindow : public AppWindow, public TickTimer {
DigitLayer digitLayers[4];
@ -22,10 +23,6 @@ protected:
digitLayers[1].SetFrame(Pebble::GRect(w2-36, 0, 32, h));
digitLayers[2].SetFrame(Pebble::GRect(w2+4, 0, 32, h));
digitLayers[3].SetFrame(Pebble::GRect(w2+36, 0, 32, h));
digitLayers[0].SetDigitCount(clock_is_24h_style() ? 3 : 2);
digitLayers[1].SetDigitCount(clock_is_24h_style() ? 24 : 12);
digitLayers[2].SetDigitCount(6);
digitLayers[3].SetDigitCount(10);
for (size_t i = 0; i < 4; i++) {
this->digitLayers[i].UpdateLayout();
this->AddChild(this->digitLayers[i]);
@ -36,23 +33,25 @@ protected:
this->frameLayer.SetCompositingMode(GCompOpSet);
this->AddChild(this->frameLayer);
#ifdef DEBUG
TickTimerStart(SECOND_UNIT);
#else
TickTimerStart(MINUTE_UNIT);
#endif
// MINUTE_UNIT doesn't seem to be called if only the hours change
// This can happen when time zone changes or in tests
// So we also update time on HOUR_UNIT
TickTimerStart(HOUR_UNIT);
TickTimerStart(USE_SECONDS_INSTEAD_OF_MINUTES ? SECOND_UNIT : MINUTE_UNIT);
UpdateTime(false);
}
void UpdateTime(bool animate=true) {
DateTime date = DateTime::Now();
int hour = date.GetHour();
#ifdef DEBUG
int minute = date.GetSecond();
#else
int minute = date.GetMinute();
#endif
if (!clock_is_24h_style()) hour %= 12;
DateTime dateTime = DateTime::Now();
int hour = dateTime.GetHour();
int minute = USE_SECONDS_INSTEAD_OF_MINUTES ? dateTime.GetSecond() : dateTime.GetMinute();
bool is_24h = clock_is_24h_style();
if (!is_24h) hour %= 12;
digitLayers[0].SetDigitCount(is_24h ? 3 : 2);
digitLayers[1].SetDigitCount(is_24h ? 24 : 12);
digitLayers[2].SetDigitCount(6);
digitLayers[3].SetDigitCount(10);
this->digitLayers[0].RotateTo(hour / 10, animate);
this->digitLayers[1].RotateTo(hour, animate);
this->digitLayers[2].RotateTo(minute / 10, animate);
@ -66,7 +65,6 @@ protected:
namespace {
void ApplySettings() {
Pebble::Window window = window_stack_get_top_window();
window.SetBackgroundColor(settings::GetBackgroundColor());
window.MarkDirty();

View File

@ -31,7 +31,6 @@ namespace settings {
}
void HandleMsg(DictionaryIterator *iter) {
if (auto tuple = dict_find(iter, MESSAGE_KEY_BACKGROUND_COLOR)) backgroundColor = GColorFromHEX(tuple->value->int32);
if (auto tuple = dict_find(iter, MESSAGE_KEY_DIGIT_COLOR)) digitColor = GColorFromHEX(tuple->value->int32);
if (auto tuple = dict_find(iter, MESSAGE_KEY_FRAME_COLOR)) frameColor = GColorFromHEX(tuple->value->int32);

141
take_screenshots.py Normal file
View File

@ -0,0 +1,141 @@
# A simple script to automatically take app screenshots
# Hopefully some day it'll grow into an actual testing framework
from uuid import UUID
from datetime import datetime, timezone
from time import sleep
import os
import png
import json
from pebble_tool.util.logs import PebbleLogPrinter
from pebble_tool.sdk.emulator import ManagedEmulatorTransport
from pebble_tool.commands.emucontrol import send_data_to_qemu
from libpebble2.communication import PebbleConnection
from libpebble2.communication.transports.qemu.protocol import QemuButton, QemuTimeFormat
from libpebble2.services.install import AppInstaller
from libpebble2.services.appmessage import AppMessageService, Uint32
from libpebble2.services.screenshot import Screenshot
from libpebble2.protocol.system import TimeMessage, SetUTC
package_json = None
with open('package.json', 'r') as file:
package_json = json.load(file)
app_uuid = UUID(package_json['pebble']['uuid'])
appname = package_json['name']
target_platforms = package_json['pebble']['targetPlatforms']
msg_keys = {
key: 10000 + i
for i, key in enumerate(package_json['pebble']['messageKeys'])
}
def emulator(platform):
print(f'Launching `{platform}` emulator')
pebble = PebbleConnection(ManagedEmulatorTransport(platform))
pebble.connect()
pebble.run_async()
print(f'Installing `{appname}.pbw`')
AppInstaller(pebble, f'./build/{appname}.pbw').install()
sleep(1)
return pebble
def for_all_platforms(fn, logs=True):
for platform in target_platforms:
pebble = emulator(platform)
if logs:
logger = PebbleLogPrinter(pebble)
fn(pebble)
logger.stop()
else:
fn(pebble)
def is_color(pebble):
return pebble.watch_platform in ['basalt', 'chalk', 'emery']
def send_app_message(pebble, msg):
ams = AppMessageService(pebble)
tid = ams.send_message(app_uuid, msg)
# unfortunately the following doesn't work
# wait_for_event assumes only one value is returned
# but ack returns 2 :(
# tid2, uuid = ams.wait_for_event('ack')
# assert uuid == app_uuid
# assert tid2 == tid
def set_time(pebble, hour, minute, year=2013, month=1, day=23, tzinfo=timezone.utc):
dt = int(datetime(year, month, day, hour, minute, tzinfo=tzinfo).timestamp())
pebble.send_packet(TimeMessage(message=SetUTC(unix_time=dt, utc_offset=0, tz_name="UTC")))
def press_buttons(pebble, buttons, duration=0.1):
send_data_to_qemu(pebble.transport, QemuButton(state=buttons))
sleep(duration)
send_data_to_qemu(pebble.transport, QemuButton(state=0))
sleep(0.1)
def press_back(pebble, duration=0.1):
press_buttons(pebble, QemuButton.Button.Back, duration)
def press_up(pebble, duration=0.1):
press_buttons(pebble, QemuButton.Button.Up, duration)
def press_select(pebble, duration=0.1):
press_buttons(pebble, QemuButton.Button.Select, duration)
def press_down(pebble, duration=0.1):
press_buttons(pebble, QemuButton.Button.Down, duration)
def set_24_hour_mode(pebble, is_24_hour=True):
send_data_to_qemu(pebble.transport, QemuTimeFormat(is_24_hour=is_24_hour))
def screenshot(pebble, name):
filename = f'{pebble.watch_platform}/{name}.png'
path = './screenshots/' + filename
print(f'Taking screenshot `{filename}`')
os.makedirs(os.path.dirname(path), exist_ok=True)
image = Screenshot(pebble).grab_image()
png.from_array(image, 'RGB').save(path)
# ============================================================
# Code above this line is not specific to the watchface and
# should probably be eventually split off into its own library
# ============================================================
def set_colors(pebble, background, digits, frame):
def wrap_color(color):
if isinstance(color, int):
color = hex(color)
color = color.removeprefix('0x').removeprefix('#')
if len(color) == 3:
r, g, b = color
color = r + r + g + g + b + b
return Uint32(int(color, 16))
send_app_message(pebble, {
msg_keys["BACKGROUND_COLOR"]: wrap_color(background),
msg_keys["DIGIT_COLOR"]: wrap_color(digits),
msg_keys["FRAME_COLOR"]: wrap_color(frame),
})
def run(pebble):
set_24_hour_mode(pebble, True)
set_time(pebble, 12, 34)
set_colors(pebble, 'FFF', '000', '000')
sleep(1.25)
screenshot(pebble, '01_white-black-black')
set_24_hour_mode(pebble, False)
set_time(pebble, 16, 59)
set_colors(pebble, '000', 'FFF', 'AAA')
sleep(1.25)
screenshot(pebble, '02_black-white-gray')
if is_color(pebble):
set_24_hour_mode(pebble, True)
set_time(pebble, 4, 20)
set_colors(pebble, 'FF5', '00F', '00A')
sleep(1.25)
screenshot(pebble, '03_yellow-blue-blue')
if __name__ == '__main__':
for_all_platforms(run)