Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bombsimon committed Aug 10, 2024
0 parents commit 3a869fb
Show file tree
Hide file tree
Showing 9 changed files with 897 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/python.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: Lint and test
on:
push:
branches:
- main
pull_request:

jobs:
ci:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}

- name: Install Python
uses: actions/setup-python@v4
with:
python-version: "3.10"

- name: Install poetry
uses: abatilo/actions-poetry@v2

- name: Install the project dependencies
run: poetry install

- name: Run the automated tests
run: poetry run pytest -v ./tests

- name: Run ruff
run: poetry run ruff check

- name: Run mypy
run: poetry run mypy .
Binary file added .gitignore
Binary file not shown.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Garmin Screenshot

Build your project for all configured devices, fire it up in the Connect IQ
Simulator and take a screenshot.

> [!NOTE]
> I only do Garmin development under Windows so this isn't tested for macOS or
> Linux. Any contributions ensuring this would work on other OSes would be much
> appreciated!
![gif](./screenshots/screenshotter.gif)

## Prerequisites

- [Connect IQ SDK]
- [Python] (with [`poetry`][poetry])

## Usage

Ensure you have Python and `poetry` installed and run:

```sh
poetry run garmin-screenshot --help
```

## What does it do?

It's very simple, the tool works by

- Parsing your `manifest.xml` to find all supported devices
- Starts the simulator
- Builds the project for each device
```sh
monkeyc \
-d <device-name> \
-f <jungle-file> \
-o <output-prg> \
-y <developer-key>
```
- Runs the build in the simulator
```sh
monkeydo <output-prg> <device-name>
```

[Connect IQ SDK]: https://developer.garmin.com/connect-iq/sdk/
[Python]: https://www.python.org/
[poetry]: https://python-poetry.org/
Empty file added garmin_screenshot/__init__.py
Empty file.
211 changes: 211 additions & 0 deletions garmin_screenshot/garmin_screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import argparse
import shutil
import subprocess
import time
from pathlib import Path
from xml.etree import ElementTree

import pyautogui
import pygetwindow


def get_devices(app_path: Path) -> list[str]:
"""Get all devices.
Look at the products in the `manifest.xml` and get all supported products.
:param app_path: Path to Garmin app
:returns: List of all supported devices
"""
tree = ElementTree.parse(app_path / "manifest.xml")
root = tree.getroot()
namespaces = {"iq": "http://www.garmin.com/xml/connectiq"}

product_elements = root.findall(".//iq:product", namespaces)

return [
product_id for product in product_elements if (product_id := product.get("id"))
]


def start_simulator(sdk_path: Path) -> None:
"""Start the simulator.
Ensure the simulator is started so we can load our applications.
:param sdk_path: Path to the Garmin SDK. This should _exclude_ the `bin`
directory and only contain the path up until
`[...]/Sdks/connectiq-sdk-xxx`.
"""
print("Starting simulator...")

simulator = str(sdk_path / "bin" / "simulator")

result = subprocess.Popen(
[simulator],
shell=False,
)

if result.returncode:
raise RuntimeError(
f"failed to run simulator, stdout={result.stdout}, stderr={result.stderr}"
)


def build_and_load(
device: str,
sdk_path: Path,
dev_key_path: Path,
app_path: Path,
prg_path: Path,
) -> None:
"""Build and load the app.
Build and load the app for the specified device.
:param device: Garmin name of devices, e.g. `fr965`
:param sdk_path: Path to the Garmin SDK. This should _exclude_ the `bin`
directory and only contain the path up until
`[...]/Sdks/connectiq-sdk-xxx`
:param dev_key_path: Path to your developer key in `.der` format
:param app_path: Path to the Garmin application
:param prg_path: Path to where to write your compiled application
"""
app = str(prg_path / "app.prg")
monkeyc = str(sdk_path / "bin" / "monkeyc")
monkeydo = str(sdk_path / "bin" / "monkeydo")
jungle_file = str(app_path / "monkey.jungle")

monkeyc_result = subprocess.run(
[
monkeyc,
"-d",
device,
"-f",
jungle_file,
"-o",
app,
"-y",
dev_key_path,
],
capture_output=True,
check=False,
shell=True,
)

if monkeyc_result.returncode:
raise RuntimeError(
f"failed to run monkeyc, stdout={monkeyc_result.stdout.decode()}, "
f"stderr={monkeyc_result.stderr.decode()}"
)

monkeydo_result = subprocess.Popen(
[
monkeydo,
app,
device,
],
shell=True,
)

if monkeydo_result.returncode:
raise RuntimeError(
"failed to run monkeydo, stdout={result.stdout}, stderr={result.stderr}"
)


def screenshot(filename: Path, wait_for_focus: bool = False) -> None:
"""Take a screenshot.
Focus on the simulator (or whatever app has the title `CIQ Simulator`, take
a screenshot and save it to `filename`.
:param filename: The file (path and name) to save the screenshot as
"""
windows = [w for w in pygetwindow.getAllWindows() if "CIQ Simulator" in w.title]
if len(windows) == 0:
raise RuntimeError("Didn't find a window with the title 'CIQ Simulator'")

window = windows[0]
window.activate()

if wait_for_focus:
# Small delay to ensure the window is focused.
# Only needed first time
time.sleep(1)

left, top, right, bottom = window.left, window.top, window.right, window.bottom
width, height = right - left, bottom - top

screenshot = pyautogui.screenshot(region=(left, top, width, height))

screenshot.save(filename)
print(f"Screenshot saved as {filename}")


def main():
parser = argparse.ArgumentParser(
description="Garmin Screenshotter",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--wait-time",
type=int,
default=5,
help="Time to wait between launching and taking screenshot",
)
parser.add_argument(
"--sdk-path",
required=True,
type=Path,
help="Path to where your Garmin SDK is installed (excluding /bin)",
)
parser.add_argument(
"--dev-key-path",
required=True,
type=Path,
help="Path to your developer key (.der)",
)
parser.add_argument(
"--garmin-app-path",
required=True,
type=Path,
help="Path to your garmin app (needed for manifest.xml and monkey.jungle)",
)
parser.add_argument(
"--output",
default="screenshots",
type=Path,
help="Output director of where to put screenshots",
)

args = parser.parse_args()

if not args.output.exists():
args.output.mkdir(parents=True, exist_ok=True)

prg_output = Path("__prg")
if not prg_output.exists():
prg_output.mkdir(parents=True, exist_ok=True)

start_simulator(args.sdk_path)
time.sleep(args.wait_time)

wait_for_focus = True

for device in get_devices(args.garmin_app_path):
build_and_load(
device,
args.sdk_path,
args.dev_key_path,
args.garmin_app_path,
prg_output,
)
time.sleep(args.wait_time) # Arbitrary sleep in hope of app is loaded.

filename = args.output / f"screenshot-{device}.png"
screenshot(filename, wait_for_focus)

wait_for_focus = False

shutil.rmtree(prg_output)
Loading

0 comments on commit 3a869fb

Please sign in to comment.