add a python script to automatic generate all screenshots
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
32
flake.lock
generated
@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
13
flake.nix
@ -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
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
BIN
screenshots/basalt/01_white-black-black.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
screenshots/basalt/02_black-white-gray.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
screenshots/basalt/03_yellow-blue-blue.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
screenshots/chalk/01_white-black-black.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
screenshots/chalk/02_black-white-gray.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
screenshots/chalk/03_yellow-blue-blue.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
screenshots/diorite/01_white-black-black.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
screenshots/diorite/02_black-white-gray.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
@ -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)
|
||||