| name | lang-python-code-writing |
| description | Use when writing, reviewing, or debugging Python scripts for embedded development workflows. Covers script structure, type hints, error handling, subprocess/ADB automation, serial communication, log parsing, REST API clients (Gerrit/Jira/Artifactory), data manipulation (JSON/XML/YAML), test harness utilities, and CI/CD tooling for automotive IVI / HUD / RSE projects targeting Android Automotive / Linux / QNX.
|
| argument-hint | <script-or-module> [write|review|debug] |
Python Scripting — Embedded Development
Expert practices for writing reliable, maintainable Python scripts and tools
that support Android Automotive (AOSP), Linux BSP (Yocto/Buildroot),
and QNX embedded development workflows.
Standards baseline: Python 3.10+ · PEP 8 style · type hints throughout.
When to Use This Skill
- Build automation and wrapper scripts around AOSP, Yocto, or CMake.
- ADB interaction scripts (install, logcat, shell commands, screenshots).
- Serial/UART console communication with target boards.
- Log parsing, report generation, and coredump analysis.
- REST API clients for Gerrit, Jira, Artifactory.
- Test harness utilities and on-target test runners.
- CI/CD pipeline helper tools (Jenkins, GitLab CI).
- Data conversion/migration between JSON/XML/YAML/CSV formats.
Script Skeleton — Always Start Here
"""Flash a built AOSP image to a connected ADB device.
Usage:
python flash_target.py --product aosp_car_x86_64 --variant userdebug
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger(__name__)
DEFAULT_PRODUCT = os.environ.get("PRODUCT", "aosp_car_x86_64")
DEFAULT_VARIANT = os.environ.get("VARIANT", "userdebug")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--product", default=DEFAULT_PRODUCT)
parser.add_argument("--variant", choices=["eng", "userdebug", "user"],
default=DEFAULT_VARIANT)
parser.add_argument("--out-dir", type=Path,
default=Path(os.environ.get("ANDROID_PRODUCT_OUT", "out")))
parser.add_argument("-v", "--verbose", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
log.info("Flashing %s (%s) from %s", args.product, args.variant, args.out_dir)
return 0
if __name__ == "__main__":
sys.exit(main())
Rules
#!/usr/bin/env python3 shebang — portable across Linux/macOS.
from __future__ import annotations — deferred annotation evaluation (Python 3.10 compat).
sys.exit(main()) — main() returns int exit code; never sys.exit() inside helper functions.
- Use
argparse with choices= and type=Path for structured argument validation.
- Use
logging — never bare print() for status output.
Error Handling
import subprocess
from subprocess import CalledProcessError
class FlashError(RuntimeError):
"""Raised when flashing a target device fails."""
def run_cmd(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
"""Run a command and raise on non-zero exit."""
log.debug("$ %s", " ".join(cmd))
return subprocess.run(cmd, check=True, text=True, **kwargs)
def get_adb_devices() -> str:
result = subprocess.run(["adb", "devices"], capture_output=True, text=True, check=True)
return result.stdout
try:
run_cmd(["adb", "install", "-r", str(apk_path)])
except CalledProcessError as e:
log.error("ADB install failed (exit %d): %s", e.returncode, e.stderr)
raise FlashError(f"Could not install {apk_path}") from e
except FileNotFoundError:
log.error("adb not found — is Android SDK platform-tools in PATH?")
sys.exit()
Pathlib — Filesystem Operations
from pathlib import Path
out_dir = Path(os.environ["ANDROID_PRODUCT_OUT"])
apk_path = out_dir / "system" / "app" / "MyCarApp" / "MyCarApp.apk"
log_file = Path("/tmp") / f"build_{timestamp}.log"
if not apk_path.exists():
raise FileNotFoundError(f"APK not found: {apk_path}")
apk_files = list(out_dir.glob("**/*.apk"))
out_dir.mkdir(parents=True, exist_ok=True)
content = log_file.read_text(encoding="utf-8")
log_file.write_text(report, encoding="utf-8")
apk_path.rename(apk_path.with_suffix(".apk.bak"))
tmp_file.unlink(missing_ok=True)
for line in log_file.read_text(encoding="utf-8").splitlines():
process_line(line)
ADB Automation
import subprocess
import time
def wait_for_device(timeout: int = 60) -> None:
subprocess.run(["adb", "wait-for-device"], check=True, timeout=timeout)
deadline = time.time() + timeout
while time.time() < deadline:
result = subprocess.run(
["adb", "shell", "getprop", "sys.boot_completed"],
capture_output=True, text=True
)
if result.stdout.strip() == "1":
log.info("Device booted and ready.")
return
time.sleep(2)
raise TimeoutError("Device did not complete boot within timeout.")
def install_apk(apk: Path, retries: int = 3) -> None:
for attempt in range(1, retries + 1):
try:
run_cmd(["adb", "install", "-r", "-t", str(apk)])
log.info("Installed: %s", apk.name)
return
except subprocess.CalledProcessError:
if attempt == retries:
raise
log.warning(, attempt)
time.sleep()
() -> Path:
output :
output = Path()
subprocess.run([, , ], check=)
proc = subprocess.Popen([, ], stdout=subprocess.PIPE, text=)
time.sleep(duration)
proc.terminate()
stdout, _ = proc.communicate()
output.write_text(stdout, encoding=)
log.info(, output)
output
() -> :
result = subprocess.run([, , cmd], capture_output=, text=, check=)
result.stdout.strip()
Serial Communication
import serial
import threading
def open_serial(port: str = "/dev/ttyUSB0", baud: int = 115200) -> serial.Serial:
return serial.Serial(port, baud, timeout=1)
def read_serial_until(ser: serial.Serial, marker: str, timeout: float = 30.0) -> str:
"""Read serial output until a marker string appears or timeout."""
buf: list[str] = []
deadline = time.time() + timeout
while time.time() < deadline:
line = ser.readline().decode("utf-8", errors="replace").rstrip()
if line:
buf.append(line)
log.debug("[serial] %s", line)
if marker in line:
return "\n".join(buf)
raise TimeoutError(f"Marker '{marker}' not seen within {timeout}s")
def send_and_wait(ser: serial.Serial, command: str, prompt: str = "#") -> str:
ser.write(f"{command}\n".encode())
return read_serial_until(ser, prompt)
Log Parsing
import re
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class LogEntry:
timestamp: datetime
level: str
tag: str
message: str
LOGCAT_RE = re.compile(
r"^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+"
r"(\d+)\s+(\d+)\s+"
r"([VDIWEF])\s+"
r"(.+?)\s*:\s*(.*)$"
)
def parse_logcat_line(line: str) -> LogEntry | None:
m = LOGCAT_RE.match(line)
if not m:
return None
ts_str, _pid, _tid, level, tag, message = m.groups()
ts = datetime.strptime(ts_str, "%m-%d %H:%M:%S.%f")
return LogEntry(timestamp=ts, level=level, tag=tag, message=message)
def filter_crashes(log_path: Path) -> list[LogEntry]:
entries = []
for line in log_path.read_text(encoding="utf-8").splitlines():
entry = parse_logcat_line(line)
if entry and entry.level in ("E", ) entry.message.lower():
entries.append(entry)
entries
REST API Clients (Gerrit / Jira / Artifactory)
import json
import os
from urllib.parse import urlencode
import requests
class GerritClient:
"""Thin client for Gerrit REST API."""
def __init__(self, base_url: str, username: str, password: str) -> None:
self._base = base_url.rstrip("/")
self._session = requests.Session()
self._session.auth = (username, password)
self._session.headers.update({"Accept": "application/json"})
def _get(self, path: str, params: dict | None = None) -> dict | list:
r = self._session.get(f"{self._base}/a{path}", params=params, timeout=30)
r.raise_for_status()
return json.loads(r.text.lstrip(")]}'\n"))
def get_open_changes(self, project: str) -> list[dict]:
return self._get(, params={: , : })
() -> :
local_path.() fh:
headers = {: token, : }
r = requests.put(url, data=fh, headers=headers, timeout=)
r.raise_for_status()
r.json()[]
Security: Never hardcode credentials.
GERRIT_USER = os.environ["GERRIT_USER"]
GERRIT_TOKEN = os.environ["GERRIT_TOKEN"]
YAML / JSON / XML Data Handling
import json
import xml.etree.ElementTree as ET
import yaml
config = json.loads(config_file.read_text())
config_file.write_text(json.dumps(config, indent=2))
with open("config.yaml") as f:
cfg = yaml.safe_load(f)
tree = ET.parse("AndroidManifest.xml")
root = tree.getroot()
pkg = root.get("{http://schemas.android.com/apk/res/android}package")
Data Classes and Type Hints
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class BuildConfig:
product: str
variant: str
out_dir: Path
modules: list[str] = field(default_factory=list)
clean: bool = False
max_jobs: int = os.cpu_count() or 4
def validate(self) -> None:
if self.variant not in ("eng", "userdebug", "user"):
raise ValueError(f"Invalid variant: {self.variant}")
if not self.out_dir.is_dir():
raise FileNotFoundError(f"out_dir not found: {self.out_dir}")
Virtual Environment and Dependencies
python3 -m venv .venv
source .venv/bin/activate
.\.venv\Scripts\Activate
pip install -r requirements.txt
# requirements.txt — pin major versions for reproducibility
requests>=2.31,<3
pyserial>=3.5,<4
PyYAML>=6.0,<7
CI/CD Patterns
import os
def is_ci() -> bool:
return os.environ.get("CI", "false").lower() == "true"
def set_output(key: str, value: str) -> None:
"""Write a key=value pair to GitHub Actions / GitLab CI output."""
if gh_output := os.environ.get("GITHUB_OUTPUT"):
with open(gh_output, "a") as f:
f.write(f"{key}={value}\n")
elif os.environ.get("GITLAB_CI"):
with open("build.env", "a") as f:
f.write(f"{key}={value}\n")
Prerequisites
- Python 3.8+ (
python3 --version to verify).
pip and venv available.
- For ADB scripts: Android Platform-Tools on PATH.
- Install project dependencies:
pip install -r requirements.txt (if present).
Step-by-Step Workflows
Step 1: Create the script file
Add #!/usr/bin/env python3, a module docstring, and a if __name__ == "__main__": guard.
Step 2: Add type hints
Annotate all function signatures with typing types; use Optional[T] for nullable params.
Step 3: Implement with proper error handling
Catch specific exception types; never use bare except:; log before re-raising.
Step 4: Use pathlib for file operations
Replace os.path with pathlib.Path; use .read_text() / .write_text().
Step 5: Run pylint / mypy
Fix all reported issues; annotate intentional suppressions with # pylint: disable=<code> and a comment.
Troubleshooting
ModuleNotFoundError — activate the correct virtual environment; run pip install -r requirements.txt.
UnicodeDecodeError — open files with an explicit encoding: open(path, encoding='utf-8', errors='replace').
subprocess hangs — always set a timeout= parameter; avoid shell=True with user-controlled input (command injection risk).
mypy cannot infer type — add explicit type annotations; use cast(T, expr) as a last resort with a comment.
Pre-Commit Checklist
References