Skip to content

Continuous guide

This reading mode combines the core documentation into one long, scrollable page. It is useful when learning the project in order or searching across several chapters without changing pages. The normal page-by-page guide remains available from the navigation menu.

Generated from the canonical pages

The sections below are included from the same Markdown files used by the rest of this site. Corrections therefore appear in both reading modes without maintaining two copies.

Learning path

This guide can be approached without prior firmware experience, but the order matters. Do not begin with an OTA build. Begin by proving that you can identify an input, reproduce parser output, and explain one byte range.

Choose your goal

flowchart TD
  START[What do you want to do?] --> APPS{Create a new app?}
  APPS -->|Yes| RPK[RPK path]
  APPS -->|No| NATIVE{Change a built-in feature?}
  NATIVE -->|Yes| FW[Firmware research path]
  NATIVE -->|No| DOC{Understand or contribute?}
  DOC --> DATA[Reproduce data and improve docs]

  RPK --> R1[Install Xiaomi Watch IDE]
  R1 --> R2[Build a static page]
  R2 --> R3[Test lifecycle and navigation]
  R3 --> R4[Add one documented API]

  FW --> F1[Create an offline workspace]
  F1 --> F2[Hash and inspect package]
  F2 --> F3[Extract one component]
  F3 --> F4[Form and test one hypothesis]
  F4 --> F5[Build a guarded patch on a copy]
  F5 --> F6[Device test only with recovery available]

  DATA --> D1[Run tests]
  D1 --> D2[Reproduce a report]
  D2 --> D3[Submit sanitized evidence]

Path A: new installable application

Choose RPK when the goal is a new screen, utility, game, or phone-assisted feature that can operate through documented APIs. This path is safer because the app is installed and removed separately from system firmware.

Recommended order:

  1. Read RPK applications to understand the boundary.
  2. Follow Build an RPK from zero.
  3. Use only one page and one image for the first build.
  4. Verify launch, close, swipe-back, physical-button behavior, suspend, resume, and uninstall.
  5. Add one API at a time from the official Xiaomi/70mai API reference.

Do not start by copying a large third-party app. A minimal project makes a black screen, unsupported component, or lifecycle problem diagnosable.

Path B: inspect firmware without modifying it

This is the correct first firmware path for every contributor.

  1. Follow Windows and Linux setup.
  2. Place legally obtained packages outside the Git repository.
  3. Calculate SHA-256 and record the result.
  4. Follow Full-package walkthrough.
  5. Compare your report with Research data and charts.
  6. Stop if model, version, count, sizes, or CRCs differ.

Completion means you can answer:

  • Which exact file did I inspect?
  • What format marker is present?
  • Where does the body begin?
  • How many components are declared?
  • Do sizes and CRCs agree?
  • Which statements are observed and which are inferred?

Path C: compare versions or mods

Comparison is useful only when both inputs are identified and normalized.

  1. Inspect both packages independently.
  2. Compare metadata before extracted payloads.
  3. Match components by index, type, size, and role evidence.
  4. Calculate changed byte ranges rather than only a global hash.
  5. Separate expected metadata changes from executable changes.
  6. Follow cross-references for a small changed region.

Use Firmware comparison for exact commands and a report template.

Path D: reverse engineer native code

You should already understand package extraction and address mapping.

  1. Install Ghidra from its official release and a supported 64-bit JDK.
  2. Import the extracted main component as raw ARM little-endian code.
  3. Keep the file mapping at 0x08000000 for the documented build.
  4. Do not analyze the whole binary as one uninterrupted code stream.
  5. Use strings and thumb_xrefs.py to identify bounded regions.
  6. Rename functions only when evidence supports the name.
  7. Export offset, bytes, disassembly, and callers into the lab notebook.

Follow Ghidra and ARM workflow step by step.

Path E: native graphics

The outer component-6 record format is understood; the inner TSCFrameImage payload is not yet fully decoded.

  1. Catalog records without extracting copyrighted data into Git.
  2. Select a non-critical record by path.
  3. Split its outer packet stream.
  4. Rebuild it unchanged and prove byte identity.
  5. Compare packet headers across several locally held records.
  6. Create synthetic fixtures for every proposed field.
  7. Implement a decoder before an encoder.

Follow GUI asset laboratory. Do not begin with a boot logo or critical assistant asset.

Milestones

Level You can… You should not yet…
0 · Reader explain native vs RPK and identify the model modify any binary
1 · Reproducer run tests and reproduce package metadata change package fields
2 · Analyst map offsets, compare regions, document evidence flash an experimental image
3 · Patch author write a guarded patch with synthetic tests claim compatibility beyond tested builds
4 · Device tester run a controlled test with logs and recovery planning distribute proprietary or universal images

Progress is defined by reproducible evidence, not by how many tools were opened.

Windows and Linux setup

The project works on Windows PowerShell, Windows Subsystem for Linux, and ordinary Linux. Pick one environment for each experiment and record it. Do not mix Windows and WSL paths inside the same command unless you understand how they map.

Directory layout

Create a parent directory with separate public and private areas:

S1ActiveResearch/
├── Xiaomi-Watch-S1-Active-Modding/   public Git clone
├── private-inputs/                    firmware and purchased files
├── generated/                         extracted components and reports
└── notes/                             sanitized experiment notes

private-inputs and generated must remain outside the repository. The repository .gitignore is a second defense, not permission to store proprietary files in its working tree.

Windows PowerShell

1. Install prerequisites

Install:

During Python installation, enable the launcher or ensure python is available in a new terminal.

2. Verify commands

git --version
python --version
python -m pip --version

Expected result: each command prints a version and exits without an error. If the Microsoft Store opens instead of Python, disable the Python App Installer aliases in Windows settings or use the py launcher.

3. Clone and create an isolated environment

New-Item -ItemType Directory -Path "$HOME\Documents\S1ActiveResearch"
Set-Location "$HOME\Documents\S1ActiveResearch"
git clone https://github.com/Just-Nova23/Xiaomi-Watch-S1-Active-Modding.git
Set-Location Xiaomi-Watch-S1-Active-Modding
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt

If PowerShell blocks activation, you can call the environment interpreter directly:

.\.venv\Scripts\python.exe -m unittest discover -s tests -v

Do not weaken the machine-wide execution policy merely to activate a virtual environment.

4. Create private directories

New-Item -ItemType Directory -Force -Path "..\private-inputs", "..\generated", "..\notes"

5. Hash an input

Get-FileHash "..\private-inputs\stock.pkg" -Algorithm SHA256 | Format-List

Copy the hash into a private notebook. Do not rename two different files to the same generic name without recording their hashes.

Linux or WSL

1. Verify prerequisites

git --version
python3 --version
python3 -m pip --version

Install missing packages through your distribution. On Debian or Ubuntu, the virtual-environment module may be packaged separately as python3-venv.

2. Clone and create an environment

mkdir -p "$HOME/S1ActiveResearch"
cd "$HOME/S1ActiveResearch"
git clone https://github.com/Just-Nova23/Xiaomi-Watch-S1-Active-Modding.git
cd Xiaomi-Watch-S1-Active-Modding
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
mkdir -p ../private-inputs ../generated ../notes

Python's official venv documentation explains that environments are isolated, disposable, and should not be committed.

3. Hash an input

sha256sum ../private-inputs/stock.pkg

Run the repository checks

From the repository root:

python -m unittest discover -s tests -v
python -m compileall -q tools tests

Expected tests:

  • an outer GUI record survives a byte-identical build/split round trip;
  • a bad path CRC is rejected;
  • the assistant patch changes exactly one synthetic byte;
  • a mismatched patch context is rejected without output.

These tests validate the public tools. They do not validate your private firmware automatically.

Optional analysis tools

Tool Purpose Required?
Ghidra interactive disassembly, cross-references, decompilation no
Capstone scripted instruction decoding used by thumb_xrefs.py installed from requirements.txt
Rizin alternative command-line analysis no
hex editor inspect exact byte ranges helpful
Git version public scripts and notes, never firmware yes

Download tools from their official projects. Avoid random repackaged executables, especially for tools that will open untrusted or malformed binaries.

Common setup failures

ModuleNotFoundError: capstone

Activate the same virtual environment in which dependencies were installed, then run:

python -m pip install -r requirements.txt
python -c "import capstone; print(capstone.__version__)"

python and python3 use different installations

Print both executable paths:

python -c "import sys; print(sys.executable)"
python3 -c "import sys; print(sys.executable)"

Use one interpreter consistently.

File paths fail in WSL

A Windows path such as C:\Users\name\file.pkg maps under WSL to a path similar to /mnt/c/Users/name/file.pkg. Prefer keeping the project and temporary analysis data on the same filesystem for predictable performance.

Ghidra imports the file but shows nonsense

A raw binary has no embedded loader metadata. Import settings, processor mode, base address, and code/data boundaries must be supplied correctly. Follow Ghidra and ARM workflow rather than accepting every auto-analysis result.

Safe research setup

This guide prepares an offline workspace. It does not flash a watch.

This page is the short quick start. For full installation troubleshooting, PowerShell and WSL commands, follow Windows and Linux setup. If you are unsure whether you need RPK development or firmware analysis, begin with the Learning path.

Requirements

  • Python 3.11 or newer;
  • Git;
  • optional: Capstone for thumb_xrefs.py;
  • optional: Ghidra, Rizin, or another ARM Thumb disassembler;
  • firmware obtained legally by the researcher.

Keep firmware outside the repository:

watch-research/
├── project/          cloned Git repository
└── private-firmware/ files that must never be committed

Clone and test

git clone https://github.com/Just-Nova23/Xiaomi-Watch-S1-Active-Modding.git
cd Xiaomi-Watch-S1-Active-Modding
python -m unittest discover -s tests -v
python -m compileall -q tools tests

Inspect a full package

python tools/firmware_pkg.py \
  ../private-firmware/stock.pkg \
  --extract-dir ../private-firmware/components \
  --json-out ../private-firmware/package-report.json

Review these fields before trusting the result:

  • package model and version;
  • declared body size and calculated file size;
  • component sizes and offsets;
  • header and component CRC matches;
  • SFU1 payload length and digest state.

Catalog GUI assets

python tools/asset_catalog.py \
  ../private-firmware/stock.pkg \
  --extract-dir ../private-firmware/gui-records \
  --json-out ../private-firmware/gui-assets.json

Rules for reproducible notes

Record hashes rather than uploading firmware:

sha256sum ../private-firmware/stock.pkg

For every patch, record the component index, exact file offset, original bytes, replacement bytes, reason, expected behavior, and validation state.

Continue with a complete tutorial

Evidence-backed architecture

This page replaces the earlier simplified concept sketch. That sketch connected the phone, BES image, and STM32 image as if the complete runtime message path had been proved. It had not. The diagrams below show only relationships supported by parsed package structure, embedded markers, cross-references, or repeatable device observations.

For exact component sizes, see Research data and charts.

Reading the maps

Every diagram uses the following evidence vocabulary:

  • Observed: directly parsed bytes, a verified checksum, an embedded marker, or behavior reproduced on the device.
  • Inferred: the best current explanation of several observations, but not confirmed by vendor symbols or source code.
  • Unknown: a field or relationship deliberately left unnamed.

Solid arrows mean structural containment or ordering proved by the package parser. Dashed arrows mean a research relationship or inference and are labeled accordingly.

Verified OTA containment map

flowchart TB
  PKG[full OTA package<br/>185,221,135 bytes] -->|parsed header| HDR[Header<br/>2,487 bytes]
  PKG -->|body in descriptor order| BODY[Component body<br/>185,218,648 bytes]
  BODY --> C0[0 · Main image<br/>3,600,374 B]
  BODY --> C1[1 · Wrapped resource<br/>20,388,275 B]
  BODY --> C2[2 · Wrapped resource<br/>40,003,605 B]
  BODY --> C3[3 · Audio resources<br/>22,681,678 B]
  BODY --> C4[4 · STM32 boot/secondary<br/>250,648 B]
  BODY --> C5[5 · BREAM PATCH wrapper<br/>257,336 B]
  BODY --> C6[6 · GUI asset container<br/>95,071,184 B]
  BODY --> C7[7 · Calibration/config<br/>89,224 B]
  BODY --> C8[8 · BES best1501 image<br/>2,876,324 B]
  C0 -->|first 0x2000 bytes| SFU[SFU1 header]
  C0 -->|remaining declared bytes| MAIN[Main payload]
  C6 -->|concatenated records| GUI[GUI records<br/>path + CRC + packet body]

  classDef observed stroke-width:2px;
  classDef inferred stroke-width:2px,stroke-dasharray:6 4;
  classDef unknown stroke-width:2px,stroke-dasharray:2 3;
  class PKG,HDR,BODY,C0,C3,C4,C5,C6,C7,C8,SFU,MAIN,GUI observed;
  class C1,C2 unknown;

Why these edges are real

The full parser reads the declared body length, calculates the header length from the file size, then reads nine descriptors. The sum of the nine component sizes equals the body length exactly. Each component begins at the end of the previous component, and all stored CRC values match the corresponding extracted region in the documented package. This proves containment and order. It does not prove every component's runtime responsibility.

The labels for components 1 and 2 remain deliberately broad. Their wrappers and sizes are observed; their exact user-facing role is not.

Package validation chain

flowchart LR
  F[Input file] --> M{Magic}
  M -->|full| H[Parse model, version,<br/>body length and descriptors]
  M -->|anything else| STOP[Reject input]
  H --> S{Sum of component sizes<br/>equals body length?}
  S -->|No| STOP
  S -->|Yes| X[Walk nine byte ranges]
  X --> C{Every stored CRC<br/>matches?}
  C -->|No| REPORT[Report mismatch;<br/>do not treat as valid]
  C -->|Yes| I[Inspect inner wrappers]
  I --> Q{Component 0 starts SFU1?}
  Q -->|Yes| D[Check payload length<br/>and SHA-256 fields]
  Q -->|No| REPORT
  D --> R[Emit JSON report and,<br/>optionally, extracted copies]

  classDef safe stroke-width:2px;
  classDef stop stroke-width:3px,stroke-dasharray:3 2;
  class F,M,H,S,X,C,I,Q,D,R safe;
  class STOP,REPORT stop;

This is the actual decision sequence implemented by tools/firmware_pkg.py. The extractor writes new files only when requested; it never modifies the input package.

Main-image address map

For the documented M2116W1 main component, embedded pointers consistently satisfy:

runtime_address = 0x08000000 + file_offset
file_offset     = runtime_address - 0x08000000
flowchart LR
  O[File offset<br/>0x0013024C] -->|add 0x08000000| A[Runtime address<br/>0x0813024C]
  A -->|encode little-endian pointer| P[4C 02 13 08]
  P -->|search main image| L[Literal-pool occurrences]
  L -->|decode nearby Thumb LDR| X[Candidate cross-reference]
  X -->|verify control flow and callers| V[Evidence-backed function context]

  classDef observed stroke-width:2px;
  class O,A,P,L,X,V observed;

Examples checked in the analyzed build:

File offset Runtime address Use
0x0013024c 0x0813024c native assistant handler region
0x00130358 0x08130358 address inside the same handler neighborhood
0x002eda06 0x082eda06 independent mapping check in a later region

An earlier 0x08280000 base assumption was wrong because it treated the 0x2000-byte SFU1 header as if it were outside the mapped image. Broad pointer comparison corrected the model. The repository defaults now use image offset 0 and base 0x08000000.

Native graphics containment map

flowchart TB
  C6[Component 6<br/>95,071,184 bytes] --> R1[Record 0]
  C6 --> RN[Record n]
  C6 --> RL[Last record]
  RN --> RV[Record version · 4 B]
  RN --> PATH[Path length · 1 B<br/>Path CRC · 4 B<br/>ASCII path · variable]
  RN --> PV[Payload version · 1 B]
  RN --> PL[Body length · 4 B<br/>Body CRC · 4 B]
  RN --> STREAM[Length-prefixed packet stream]
  STREAM --> P0[Packet length · 2 B<br/>packet bytes]
  STREAM --> PX[Repeated until body end]
  P0 -. inner interpretation unknown .-> TSC[TSCFrameImage layer]

  classDef observed stroke-width:2px;
  classDef unknown stroke-width:2px,stroke-dasharray:2 3;
  class C6,R1,RN,RL,RV,PATH,PV,PL,STREAM,P0,PX observed;
  class TSC unknown;

The parser can split and rebuild this outer structure byte-for-byte. It cannot yet render the inner packet payload. Packet boundaries are observed, but calling each packet an animation frame would be an unsupported claim.

Native and RPK software boundaries

flowchart LR
  subgraph Firmware[Native firmware domain]
    MAINAPP[Compiled native application logic]
    NATIVERES[Shared native resources]
    GUIARC[Component 6 GUI archive]
    MAINAPP --> NATIVERES
    MAINAPP --> GUIARC
  end

  subgraph RPK[Installable RPK domain]
    MANIFEST[manifest files]
    JS[JavaScript logic]
    PAGE[HTML-like page templates]
    CSS[CSS styles]
    MEDIA[PNG/JPG resources]
    MANIFEST --> JS
    MANIFEST --> PAGE
    PAGE --> CSS
    PAGE --> MEDIA
  end

  RPK -. only documented runtime APIs .-> Firmware

  classDef observed stroke-width:2px;
  classDef boundary stroke-width:2px,stroke-dasharray:6 4;
  class MAINAPP,NATIVERES,GUIARC,MANIFEST,JS,PAGE,CSS,MEDIA observed;

The RPK structure is supported by Xiaomi/70mai framework documentation and inspected working packages. It does not imply that RPK JavaScript can access native C++ objects, Bluetooth internals, microphone streaming, or privileged watch actions. Each capability must be tied to a documented API or separately verified behavior.

Runtime relationships still unknown

The package proves that a BES image and STM32-related images are delivered together. It does not yet prove:

  • which processor terminates each Bluetooth profile;
  • the exact inter-processor transport between BES and STM32 sides;
  • whether assistant audio is encoded, buffered, or forwarded by one or both processors;
  • which component owns every system service;
  • whether a named asset is decoded directly by TouchGFX, a GPU layer, or an intermediate loader.

Those relationships should not be drawn as solid architecture arrows until traces, symbols, code paths, or controlled experiments establish them.

How to verify or extend a map

  1. Start with one proposed node or edge, not a whole subsystem.
  2. State what evidence could prove it: marker, cross-reference, packet trace, call site, or controlled behavior.
  3. Record model, version, file hash, component, offset, and command.
  4. Try to disprove the relationship with an alternative explanation.
  5. Mark it inferred until independent evidence closes the gap.
  6. Update the diagram, prose, test fixture, and evidence table in one pull request.

See Research methodology, Lab notebook, and Concept maps for complete templates.

Verified concept maps

These maps summarize the project without inventing missing architecture. Each relationship corresponds to a parser rule, verified byte mapping, documented framework rule, or explicit research dependency.

Evidence production map

flowchart TD
  INPUT[Legally obtained private input] --> ID[Identity record<br/>model · version · size · SHA-256]
  ID --> TOOL[Versioned public tool<br/>repository commit]
  TOOL --> OUTPUT[Sanitized JSON or text report]
  OUTPUT --> CHECK[Independent invariant checks<br/>length · CRC · address · round trip]
  CHECK --> CLAIM{Claim type}
  CLAIM --> O[Observed]
  CLAIM --> R[Reproduced]
  CLAIM --> I[Inferred]
  CLAIM --> H[Hypothesis]
  O --> DOC[Documentation + test]
  R --> DOC
  I --> DOC
  H --> ISSUE[Research issue]

The map is also a review rule: a technical claim without an identified input, tool version, output, and check is not ready to be marked observed.

Artifact ownership map

flowchart LR
  PRIVATE[Private workspace] --> P1[Original firmware/OTA]
  PRIVATE --> P2[Purchased packages]
  PRIVATE --> P3[Extracted components]
  PRIVATE --> P4[Raw captures and device identifiers]

  PUBLIC[Public repository] --> U1[Original parser code]
  PUBLIC --> U2[Synthetic test fixtures]
  PUBLIC --> U3[Offsets and minimal byte context]
  PUBLIC --> U4[Sanitized measurements]
  PUBLIC --> U5[Guides and diagrams]

  PRIVATE -. transform and sanitize .-> PUBLIC

There is intentionally no arrow from the public repository back to ready-to-flash firmware. The project teaches reproducible analysis while excluding vendor and purchased binaries.

Full-package byte layout

flowchart LR
  H[Header<br/>2,487 B] --> C0[C0<br/>3,600,374 B]
  C0 --> C1[C1<br/>20,388,275 B]
  C1 --> C2[C2<br/>40,003,605 B]
  C2 --> C3[C3<br/>22,681,678 B]
  C3 --> C4[C4<br/>250,648 B]
  C4 --> C5[C5<br/>257,336 B]
  C5 --> C6[C6<br/>95,071,184 B]
  C6 --> C7[C7<br/>89,224 B]
  C7 --> C8[C8<br/>2,876,324 B]

This is ordered containment from the documented stock package. The diagram is not drawn to scale; use the component-size chart for quantitative comparison.

Patch trust chain

flowchart TD
  SOURCE[Source component] --> HASH{Known identity?}
  HASH -->|No| REJECT[Reject]
  HASH -->|Yes| CONTEXT{Expected bytes and<br/>surrounding context match?}
  CONTEXT -->|No| REJECT
  CONTEXT -->|Yes| COPY[Create output copy]
  COPY --> CHANGE[Apply minimum change]
  CHANGE --> DIFF{Changed offsets exactly<br/>equal allowlist?}
  DIFF -->|No| REJECT
  DIFF -->|Yes| REPORT[Write hashes and JSON report]
  REPORT --> PACKAGE[Optional package build]
  PACKAGE --> OFFLINE[Reparse and validate offline]
  OFFLINE --> DEVICE{Recovery and test<br/>conditions satisfied?}
  DEVICE -->|No| HOLD[Hold; do not install]
  DEVICE -->|Yes| TEST[Controlled device test]

The repository's assistant-capacity patch implements the context, copy, exact-diff, and report stages. It does not automatically package or install anything.

Firmware research dependency map

flowchart BT
  OTA[Device OTA test] --> PACKAGE[Valid package reconstruction]
  PACKAGE --> COMPONENT[Correct component parser]
  COMPONENT --> FORMAT[Verified outer format]
  PACKAGE --> BOOT[Boot-chain acceptance knowledge]
  PATCH[Behavioral patch] --> CODE[Function and instruction evidence]
  CODE --> MAP[Correct file/runtime mapping]
  CODE --> XREF[Cross-references and control flow]
  OTA --> PATCH
  OTA --> RECOVERY[Recovery planning]
  GRAPHICS[Native asset replacement] --> OUTER[Lossless outer record builder]
  GRAPHICS --> INNER[Working inner decoder/encoder]
  GRAPHICS --> PACKAGE

This dependency graph explains why “change a logo” can be harder than changing one numeric instruction: native artwork depends on an unresolved inner codec, while the text-capacity instruction uses a known fixed-width context.

RPK lifecycle map

The following sequence comes from the public Xiaomi/70mai framework specification.

stateDiagram-v2
  [*] --> Init: onInit
  Init --> Ready: onReady
  Ready --> Visible: onShow
  Visible --> Hidden: onHide
  Hidden --> Visible: onShow after foreground restore
  Hidden --> Destroyed: onDestroy when page exits
  Destroyed --> [*]

Opening another page can destroy the earlier page in the documented lightweight runtime; returning may create it again. Persistent state should not rely on a page object surviving navigation.

Native assistant data structure map

The fields below were reconstructed from instruction behavior around the documented handler. Names describe observed use, not vendor source symbols.

classDiagram
  class AssistantObject {
    +0x2B80 flag_observed
    +0x2B82 text_length_observed
    +0x2B84 flag_observed
    +0x2B86 utf16_buffer_800_bytes
    +0x2EA6 buffer_end
    +0x2EA8 observed_object_extent
  }
  class ConversionCall {
    destination object+0x2B86
    replacement character 0x2A
    capacity stock 300
    capacity patched 400
  }
  ConversionCall --> AssistantObject : writes bounded UTF-16 text

The converter reserves a NUL code unit, so capacities 300 and 400 produce at most 299 and 399 visible characters respectively.

Updating these maps

A map change must include:

  1. the exact edge or node being changed;
  2. evidence level before and after;
  3. command, offset, hash, trace, or official source;
  4. alternative explanations considered;
  5. documentation and synthetic tests where applicable.

Do not make a map look complete by connecting unknown subsystems. An honest gap is more useful than a polished false relationship.

Firmware package formats

Two outer formats are currently recognized: full and diff. Both store multi-byte lengths and CRCs in big-endian form unless noted otherwise.

Xiaomi CRC

Package CRC fields observed in this device use CRC-32/ISO-HDLC with an initial value of 0xffffffff and a final XOR of 0xffffffff:

(zlib.crc32(data, 0xFFFFFFFF) ^ 0xFFFFFFFF) & 0xFFFFFFFF

full package

Important prefix fields:

Offset Size Meaning
0x00 4 ASCII full
0x08 4 Total component body size
0x0c 4 Target version bytes
0x10 16 NUL-padded model identifier

The analyzed stock package contains nine components. Descriptor fields include type, flag, reserved bytes, component size, and a 256-byte unknown/reserved area. The CRC following one component is stored adjacent to the next descriptor; the final component CRC and header CRC terminate the header.

Do not write a base version into offsets 0x04..0x07 of a stock full package. Those bytes are not the same field used by diff. Doing so caused the watch to reject an OTA with OTA DIFF PTK OLD VERSION ERROR before transfer.

diff package

Important prefix fields:

Offset Size Meaning
0x00 4 ASCII diff
0x04 4 Required installed/base version
0x08 4 Component body size
0x0c 4 Target version
0x10 16 NUL-padded model identifier

A known compact mod package uses a 597-byte header followed by bootloader and main images. A generalized partial container uses a 53-byte prefix, one 270-byte descriptor per included component, a four-byte header CRC, then the component bodies.

SFU1 inner image

Component 0 begins with an SFU1 header of 0x2000 bytes. Observed fields include payload length, partial-firmware fields, repeated SHA-256 values, and a 64-byte signature candidate. Modified images can deliberately have a payload digest that no longer matches the stock signed header; whether they boot depends on the installed boot chain. Outer CRC validity alone does not establish that an SFU1 image will run.

Full-package walkthrough

This tutorial inspects one legally obtained full package without changing it. It explains every command, expected output, and stop condition.

Before you begin

You need:

  • a package you are legally allowed to inspect;
  • its original filename or download context;
  • the watch model and installed/target version if known;
  • the repository environment from Windows and Linux setup.

Do not assume a file is a full package because its extension is .pkg. The first four bytes must identify the format.

Step 1: preserve identity

Calculate a SHA-256 hash before extraction.

PowerShell:

Get-FileHash "..\private-inputs\stock.pkg" -Algorithm SHA256

Linux/WSL:

sha256sum ../private-inputs/stock.pkg

Record the complete 64-character hash, byte size, acquisition date, model, and reported version. SHA-256 identifies the bytes; it does not establish legality, authenticity, or compatibility.

Step 2: inspect without extraction

python tools/firmware_pkg.py \
  ../private-inputs/stock.pkg \
  --json-out ../generated/stock-package-report.json

The tool:

  1. reads the file size;
  2. checks the full magic;
  3. derives header size from declared body length;
  4. parses nine descriptors;
  5. confirms that component lengths equal the body length;
  6. hashes every component and validates stored CRCs;
  7. inspects known inner wrappers;
  8. emits a JSON report.

It does not modify the package.

Step 3: check the report identity

Open stock-package-report.json in a text editor. Start at the top:

{
  "format": "Xiaomi full package (reverse-engineered subset)",
  "file_size": 185221135,
  "header_size": 2487,
  "body_size": 185218648,
  "model": "M2116W1"
}

The snippet shows the important shape of the documented stock report; additional keys are omitted here. Your file should be treated as a different build if any identity field differs.

Check the invariant:

header_size + body_size = file_size
2,487 + 185,218,648 = 185,221,135

If it fails, stop. Do not repair lengths by hand before understanding the discrepancy.

Step 4: inspect component descriptors

Each report entry contains:

  • component index and observed type;
  • inferred role hint;
  • file offset and size in decimal and hexadecimal;
  • SHA-256;
  • stored and calculated CRC state;
  • first and last bytes;
  • any recognized inner format.

For stock 1.4.174, exact component sizes are listed under Research data and charts. Compare all nine, not only component 0.

Stop if:

  • the component count is not nine;
  • sizes do not add up;
  • any CRC fails;
  • model is not the expected M2116W1;
  • an expected wrapper is absent;
  • the report comes from a different hash than the file you intend to study.

Step 5: inspect the SFU1 layer

Component 0 begins with a 0x2000-byte SFU1 header in the documented package. The parser checks:

  • magic;
  • header size assumption;
  • declared payload size;
  • component size relationship;
  • stored and repeated SHA-256 fields;
  • calculated payload SHA-256;
  • a 64-byte signature candidate.

Three different statements must remain separate:

  1. Outer CRC matches: the OTA container region is internally consistent.
  2. SFU1 payload digest matches: the inner payload matches the digest stored in its header.
  3. Boot chain accepts it: only a controlled device test can establish this, and acceptance may depend on signatures, installed bootloader, rollback policy, and base version.

A valid outer CRC does not imply a bootable image.

Step 6: extract components

Only after the report passes basic checks:

python tools/firmware_pkg.py \
  ../private-inputs/stock.pkg \
  --extract-dir ../generated/stock-components \
  --json-out ../generated/stock-package-report.json

The output names include component index and type. Immediately hash the extracted directory and keep it read-only when practical.

Linux/WSL example:

sha256sum ../generated/stock-components/* > ../notes/stock-component-hashes.txt

PowerShell example:

Get-ChildItem "..\generated\stock-components" -File |
  Get-FileHash -Algorithm SHA256 |
  Format-Table Path, Hash

Step 7: verify extraction offsets

An extracted component should equal the exact source slice described by offset and size. The parser calculates hashes while streaming the source region, so the report hash and extracted-file hash should match.

If they do not:

  1. preserve both files;
  2. confirm the report and extraction came from the same command/input;
  3. check disk errors and path confusion;
  4. do not continue to patching.

Step 8: write a reproducible result

Record:

## Input identity
- model: M2116W1
- claimed version: 1.4.174
- file size: 185221135
- SHA-256: <complete local hash>

## Parser
- repository commit: <git rev-parse HEAD>
- Python: <python --version>
- command: <exact command>

## Result
- header/body invariant: pass/fail
- component count: 9
- component CRCs: 9/9 pass
- SFU1 payload digest: pass/fail
- differences from public matrix: none/list

## Evidence level
- observed/reproduced/inferred/hypothesis

Do not paste personal directories, account data, Bluetooth identifiers, firmware payloads, or purchased files into a public issue.

Next steps

Firmware comparison workflow

A useful comparison answers what changed, where, and under which base assumptions. A global “files differ” result is not enough, while blindly listing millions of changed offsets is usually noise.

Define the comparison

Before running a tool, write one sentence:

Compare stock M2116W1 version A with candidate version B to identify changed components and isolate the smallest code or resource regions.

Do not compare files from different models or unknown provenance and then attribute every difference to one feature.

Step 1: identify both packages independently

python tools/firmware_pkg.py ../private-inputs/a.pkg \
  --extract-dir ../generated/a-components \
  --json-out ../generated/a-report.json

python tools/firmware_pkg.py ../private-inputs/b.pkg \
  --extract-dir ../generated/b-components \
  --json-out ../generated/b-report.json

Confirm separately:

  • SHA-256 and file size;
  • model identifier;
  • declared version;
  • component count and sizes;
  • CRC and inner digest state.

If package B is a compact diff, inspect it with diff_pkg.py instead of forcing the full-package parser.

Step 2: compare metadata first

Use a structured JSON diff tool or a short local script. Focus on:

Field Why it matters
format magic determines the parser and version semantics
base version required for differential-package acceptance
target version identifies the intended result, not necessarily content identity
model prevents cross-model conclusions
descriptor index/type/flag defines component interpretation and wrapper handling
size detects insertion, deletion, or replacement
SHA-256 detects any byte difference
CRC status distinguishes consistent packaging from damaged/rebuilt data

Metadata differences can explain an OTA rejection before executable code is even read.

Step 3: produce a component matrix

| Index | A size/hash | B size/hash | State |
|---:|---|---|---|
| 0 | … | … | changed |
| 1 | … | … | identical |

Classify each component as:

  • identical size and hash;
  • same size, changed content;
  • changed size;
  • missing or newly introduced;
  • not comparable because package structure differs.

Step 4: calculate changed ranges

For equal-sized components, group adjacent changed bytes into ranges. The following read-only example prints range starts and lengths without writing either input:

from pathlib import Path

a = Path("../generated/a-components/component-00-type-00.bin").read_bytes()
b = Path("../generated/b-components/component-00-type-00.bin").read_bytes()

if len(a) != len(b):
    raise SystemExit("sizes differ; normalize the comparison first")

start = None
for offset, (left, right) in enumerate(zip(a, b)):
    changed = left != right
    if changed and start is None:
        start = offset
    if not changed and start is not None:
        print(f"0x{start:08x} length={offset-start}")
        start = None
if start is not None:
    print(f"0x{start:08x} length={len(a)-start}")

Keep this as an analysis snippet; do not commit the input or generated binary output.

Step 5: interpret by layer

For component 0, separate at least:

0x00000000 .. 0x00001fff  SFU1 header
0x00002000 .. end         payload in the documented build

A digest or signature-field difference is not automatically an application-code change. Conversely, one changed instruction inside the payload may alter behavior even when component size stays constant.

For component 6, compare record catalogs by path, size, packet count, and hash. Do not compare only global offsets: changing one record size shifts every later record.

Step 6: map code offsets to runtime addresses

For the documented main image:

runtime = 0x08000000 + file offset

Use:

python tools/thumb_xrefs.py main-b.bin \
  --disasm-offset 0x130340 \
  --bytes 0x60

Inspect the same window in both files. Record instruction boundaries, not merely hexadecimal differences.

Step 7: avoid causal overclaiming

Suppose a mod changes 200 ranges and enables tap-to-wake. The comparison proves those ranges differ; it does not prove which range implements tap-to-wake. Stronger evidence requires one or more of:

  • a unique string or configuration reference;
  • code reaching the relevant input/power handler;
  • a minimal isolated patch;
  • controlled A/B device behavior;
  • independent reproduction on another build.

Comparison report template

# Comparison: A → B

## Identity
| | A | B |
|---|---|---|
| model | | |
| version | | |
| SHA-256 | | |
| package format | | |

## Component summary

## Changed ranges

## Interpreted changes
- Observed:
- Inferred:
- Unknown:

## Commands

## Device evidence

## Alternative explanations

Stop conditions

Stop and re-identify inputs when a model differs, a parser reports failed CRCs, a package was already modified by an unknown tool, component sizes cannot be aligned, or a report cannot be traced to its exact input hash.

Native system apps

Preinstalled applications are not equivalent to RPK apps. Their UI, strings, and logic can live in the main component and GUI container, while audio, Bluetooth, and peripheral services may depend on other components.

Meaning of “native app”

In this project, it means a feature compiled and distributed with the firmware. No separate, reinstallable archive has been identified for every system app.

Where to look

Element Likely area Method
logic and event handling main component strings, cross-references, Thumb disassembly
images and animations GUI component 6 record catalog and visual comparison
audio and prompts audio resource component signatures, indexes, version comparison
phone connectivity firmware and companion app Android logs, traffic, and Bluetooth state
independent apps RPK packages manifest, JavaScript, and web resources

Analysis workflow

  1. Reproduce the state on the device and record the exact action.
  2. Search visible strings in the firmware.
  3. Connect the string, handler, and graphics records using verifiable references.
  4. Compare stock and modified files built from the same base version.
  5. Change one item in a copy and inspect the diff.

Current limits

There is no general system for rebuilding every native app, and no complete verified encoder for TSCFrameImage. Specific cases can be documented and patched, but a universal visual editor cannot yet be promised.

Native and RPK compared

Property Native RPK
distribution firmware installable package
privileges potentially system-level APIs exposed by the runtime
images native GUI format usually PNG/JPG
modification risk high; can affect boot or OTA more isolated and uninstallable
microphone/services possible when implemented by the system depends on public APIs

For UI prototypes, RPK is the lower-risk starting point. Deep changes require firmware research and a real recovery path.

Ghidra and ARM Thumb workflow

This walkthrough creates a bounded Ghidra analysis for the extracted main component. It does not turn a raw binary into original source code, and auto-analysis output is not automatically correct.

Install Ghidra safely

Use the official NSA Ghidra repository and releases. The official installation notes require a compatible 64-bit JDK, extraction of the release archive, and launching ghidraRun.bat on Windows or ghidraRun on Linux. Read current security advisories before opening untrusted files.

Prepare the input

  1. Extract component 0 with firmware_pkg.py.
  2. Verify its hash against the extraction report.
  3. Copy it into a private working directory.
  4. Record repository commit, package hash, component hash, size, model, and version.

For the documented build, the component size is 3,600,374 bytes and begins with an SFU1 header.

Create a project

  1. Start Ghidra.
  2. Choose File → New Project.
  3. Select Non-Shared Project unless you have deliberately configured a secure team server.
  4. Store the project outside this public Git repository.
  5. Use a name that contains model and version, not a generic firmware name.

Example: M2116W1_1.4.174_main_analysis.

Import as a raw binary

  1. Choose File → Import File.
  2. Select the extracted component.
  3. When Ghidra reports raw binary, choose an ARM little-endian language compatible with Thumb analysis.
  4. Do not guess a compiler specification as a fact; record the selection as an analysis setting.
  5. Open Options or the memory map and establish the documented image base 0x08000000.

The crucial mapping is:

file 0x00000000 → runtime 0x08000000
file 0x0013024c → runtime 0x0813024c

Do not strip 0x2000 from file offsets when using this mapping.

Treat the SFU1 header as data

The first 0x2000 bytes contain the observed SFU1 header, digests, and signature candidate. Mark this region as data rather than disassembling it as Thumb instructions.

Suggested region note:

0x08000000–0x08001fff: observed SFU1 header, not application code

The payload begins after that header in the documented component, but code and non-code data can still be mixed inside the payload.

Run conservative auto-analysis

Enable basic reference, function, and ARM analysis, but expect false positives. Raw firmware lacks standard executable metadata, so Ghidra may:

  • create functions in tables;
  • miss Thumb entry points;
  • apply the wrong function boundary;
  • interpret UTF-16 or packed assets as instructions;
  • propagate an incorrect type through many callers.

Do not bulk-rename hundreds of functions based on decompiler guesses.

Use the repository locator first

Find a UTF-8 string and candidate cross-references:

python tools/thumb_xrefs.py \
  ../generated/stock-components/component-00-type-00.bin \
  --text "listening"

Or disassemble a verified window:

python tools/thumb_xrefs.py \
  ../generated/stock-components/component-00-type-00.bin \
  --disasm-offset 0x130240 \
  --bytes 0x100

The script uses Capstone in ARM Thumb little-endian mode. Capstone's official Python tutorial explains architecture/mode selection and why detailed operand information must be enabled explicitly.

Convert file offset to runtime address, then use Go To:

0x08000000 + 0x0013024c = 0x0813024c

At the candidate address:

  1. inspect bytes and instruction widths;
  2. verify that branch targets land on plausible boundaries;
  3. inspect incoming and outgoing references;
  4. locate prologue and return behavior;
  5. compare the same region in stock and modified components;
  6. check whether the decompiler agrees with assembly, not the reverse.

Follow a literal reference

Thumb code often loads a nearby literal using a PC-relative ldr. The effective literal address depends on the aligned program counter. The repository script calculates this and limits its search window to the reachable region.

Other addresses may be constructed with movw and movt. A textual search for a four-byte pointer alone will miss those pairs, so thumb_xrefs.py checks both strategies.

Name evidence, not wishes

Good labels:

  • candidate_utf8_to_utf16_wrapper;
  • assistant_text_handler_observed_0813024c;
  • field_2b82_length_inferred.

Weak labels:

  • send_to_screen when only a string reference is known;
  • bluetooth_receive without transport evidence;
  • alexa_main for a function merely located near one Alexa string.

Add comments containing the evidence source and date.

Verify the native assistant capacity region

The documented patch expects this context at file offset 0x130344:

4f f4 48 72 38 46 6a f1 b4 fe 2a 23
4f f4 96 72
39 46 70 68 c8 f0 b6 fa

The four bytes at 0x130350 decode as mov.w r2, #300. The guarded patch replaces them with the encoding for mov.w r2, #400; exactly one byte differs because of Thumb immediate encoding.

Before accepting the explanation, verify:

  • the preceding call clears 800 bytes at the inline buffer;
  • the destination object and field offsets remain consistent;
  • the converter reserves a terminator;
  • only the intended instruction changes;
  • the context matches the documented build exactly.

Export a reviewable finding

Include:

## Location
- file offset:
- runtime address:
- component hash:

## Bytes
- context before:
- instruction before:
- instruction after:

## Cross-references
- callers:
- literals/strings:

## Interpretation
- observed:
- inferred:
- alternative explanation:

## Reproduction
- command:
- Ghidra language/base settings:

Do not upload a Ghidra project containing imported proprietary firmware. Share scripts, offsets, screenshots limited to necessary context, and synthetic fixtures.

Native assistant research

The text-capacity chart shows the measured stock and patched limits against the existing buffer.

The stock assistant path transports phone-produced instructions to a native watch UI. The phone-side serializer, BES-side decoder, and Bluetooth framing all preserve strings longer than 300 characters. The observed truncation occurs in the main firmware's UTF-8 to UTF-16 conversion.

Verified handler

The relevant handler starts near runtime 0x0813024c. For instruction/message type 8, it clears an 800-byte inline buffer and then passes capacity 300 to the converter at runtime 0x081f88c8:

08130344  mov.w r2, #800      ; memset size in bytes
08130348  mov   r0, r7
0813034a  bl    memset
0813034e  movs  r3, #42       ; replacement character '*'
08130350  mov.w r2, #300      ; UTF-16 capacity
08130354  mov   r1, r7
08130356  ldr   r0, [r6, #4]  ; UTF-8 source
08130358  bl    0x081f88c8    ; UTF-8 -> UTF-16

The converter reserves one code unit for a NUL terminator. Capacity 300 therefore produces at most 299 visible UTF-16 code units.

Object layout

Observed fields relative to the containing object:

Offset Meaning
+0x2b80 State flag
+0x2b82 Converted/displayed length
+0x2b84 Additional flag
+0x2b86 Start of 800-byte inline text buffer

The object size is 0x2ea8 bytes. The text buffer ends immediately before the object boundary; it cannot safely hold more than 400 UTF-16 code units.

Conservative capacity patch

At component file offset 0x130350:

old instruction bytes: 4f f4 96 72   ; mov.w r2, #300
new instruction bytes: 4f f4 c8 72   ; mov.w r2, #400

Only byte 0x130352 changes. The safe maximum becomes 399 visible code units. Setting a larger immediate would write beyond the object and is not a valid way to remove the limit.

Going beyond 399 in one response requires either enlarging every dependent object layout or allocating a separate dynamic buffer and redirecting all producers and consumers. That work is not complete.

Native assistant capacity patch walkthrough

This page explains the verified text-capacity patch from observation to offline output. It is specific to the documented main component and is not a universal firmware patch.

Problem statement

The native assistant display truncates long responses. Static analysis found an inline UTF-16 buffer of 800 bytes and a conversion call receiving capacity 300. Because the converter reserves one UTF-16 code unit for the NUL terminator, stock displays at most 299 code units in one response.

Evidence chain

flowchart LR
  UI[Observed truncated text] --> STR[Locate assistant strings]
  STR --> XREF[Trace Thumb cross-references]
  XREF --> HANDLER[Handler region 0x0813024c]
  HANDLER --> CLEAR[800-byte buffer clear]
  CLEAR --> CALL[UTF-8 to UTF-16 conversion]
  CALL --> IMM[mov.w r2,#300 at file 0x130350]
  IMM --> BOUND[Existing buffer allows capacity 400]
  BOUND --> PATCH[Guarded one-byte difference]
  PATCH --> TEST[399 visible characters observed]

The final device result supports the chain, but the offline checks are what make the change reviewable.

Object layout reconstructed from use

Relative offset Observed use
+0x2b80 state/validity flag
+0x2b82 text length field
+0x2b84 second state flag
+0x2b86 start of inline UTF-16 buffer
+0x2ea8 observed object extent

The buffer region from 0x2b86 spans 800 bytes in the analyzed code path. This yields 400 two-byte UTF-16 code units.

Instruction change

file offset:     0x00130350
runtime address: 0x08130350
before:          4f f4 96 72  → mov.w r2, #300
after:           4f f4 c8 72  → mov.w r2, #400
changed byte:    file offset 0x00130352, 0x96 → 0xc8

The instruction is four bytes wide, but Thumb immediate encoding means this value change alters one stored byte.

Step 1: verify the source identity

Use the report produced during package extraction. Confirm model, base version, component size, and complete SHA-256. Do not use a file renamed from an unknown mod.

Step 2: inspect the region

python tools/thumb_xrefs.py ../generated/main-original.bin \
  --disasm-offset 0x130340 \
  --bytes 0x40

Confirm the expected conversion setup and instruction boundary. If Capstone output differs, stop.

Step 3: run the guarded patch

python tools/patch_native_assistant_text_capacity.py \
  ../generated/main-original.bin \
  ../generated/main-capacity-400.bin \
  --report ../generated/assistant-capacity-report.json

The script checks the complete expected context beginning at file offset 0x130344. A mismatch raises an error before it writes a valid patched output.

Step 4: review the report

Confirm:

  • input and output SHA-256 differ;
  • instruction offset is 0x130350;
  • old/new instruction bytes match the documented values;
  • changed offsets contains only 0x130352;
  • old/new capacities are 300 and 400;
  • buffer size remains 800 bytes.

Step 5: independently compare bytes

from pathlib import Path

a = Path("../generated/main-original.bin").read_bytes()
b = Path("../generated/main-capacity-400.bin").read_bytes()
changes = [i for i, pair in enumerate(zip(a, b)) if pair[0] != pair[1]]
print(changes)

Expected output:

[1246034]

1246034 decimal is 0x130352.

Also verify equal lengths:

print(len(a), len(b), len(a) == len(b))

Step 6: understand what is not changed

The patch does not:

  • allocate a new buffer;
  • paginate a response;
  • remove protocol-side limits;
  • change the phone-side model response;
  • guarantee that every Unicode sequence occupies one visible glyph;
  • support other firmware versions automatically;
  • build or flash an OTA.

UTF-16 code units and user-perceived characters are not always one-to-one. Emoji and some characters may consume surrogate pairs, so 399 code units can represent fewer than 399 visible glyphs.

Why not exceed 400

Passing 401 would allow the converter to write beyond the observed 800-byte region if it interprets capacity in UTF-16 code units. Without redesigning the object, allocation, and all consumers, that risks corrupting adjacent memory.

Toward a dynamic solution

A larger or unlimited response requires more than changing an immediate:

  1. identify allocation and object lifetime;
  2. find every reader of buffer, length, and flags;
  3. determine UI text widget limits and scrolling behavior;
  4. replace inline storage with a bounded external allocation or paging model;
  5. update destructor/reset paths;
  6. test out-of-memory and malformed UTF-8 cases;
  7. preserve protocol and UI timing.

Until those dependencies are mapped, 400 is the maximum justified by the existing buffer evidence.

Native graphics and TSCFrameImage

The native GUI record chart compares exact sizes and packet counts for three verified assistant assets.

What is known

Component 6 is a concatenation of native GUI asset records. Each observed record contains:

record version (4 bytes)
path length (1 byte)
path CRC (4 bytes)
ASCII path such as nand/asset/guiimage_anim_logo.bin
payload version (1 byte)
payload body length (4 bytes)
payload body CRC (4 bytes)
repeated: packet length (2 bytes) + packet data

The outer record parser and builder are implemented in tools/gui_animation.py. Unmodified packets rebuild byte-for-byte.

Examples from stock 1.4.174:

Asset Record size Packets
guiimage_anim_logo.bin 26,739 bytes 20
guiimage_anim_alexa_listen.bin 31,473 bytes 18
guiimage_anim_alexa_think.bin 107,032 bytes 57
guiimage_anim_xiaoai_transition.bin 682,484 bytes 167

The main image contains the class/name string TSCFrameImage. Packet boundaries are proven; their exact relationship to frames is not yet proven.

Compression layers

Ambiq Nema PixPresso can convert PNG artwork to GPU texture formats such as TSC4, TSC6, and TSC6A. TSC6A retains alpha and produced a correct standalone texture in offline tests. Xiaomi's native asset, however, adds its own TSCFrameImage framing or delta layer. Raw PixPresso output is not a drop-in replacement.

RPK applications are different: their source packages use ordinary PNG/JPG assets and do not require contributors to author TSCFrameImage data.

Encoder completion plan

  1. Locate every code reference to TSCFrameImage in the main image.
  2. Translate the packet reader into a small host-side decoder.
  3. Export known native assets to individual TSC textures and then PNG files.
  4. Identify full-frame, repeated-frame, and delta-frame packet types.
  5. Test whether the loader accepts independent full TSC6A frames; this may avoid implementing delta compression initially.
  6. Implement the inverse packet builder.
  7. Prove an original asset can be decoded and rebuilt with identical rendering.
  8. Replace a non-critical asset before any boot or assistant artwork.

Contribution target

A useful graphics contribution includes packet hex limited to the relevant header, inferred field names, comparison against at least two different assets, and a script/test using synthetic data. Do not upload extracted asset payloads.

Native GUI asset laboratory

This laboratory studies component 6 without claiming a complete TSCFrameImage decoder. The safe objective is to prove the outer record format, catalog assets, and design experiments that can reveal inner fields.

Evidence boundary

Currently verified:

  • component 6 is exactly covered by concatenated records in stock 1.4.174;
  • each record has a path, CRC-protected body, and length-prefixed packet stream;
  • the parser can split and byte-perfectly rebuild an unchanged record;
  • native assistant paths identify listen, think, and transition assets;
  • raw GPU texture output is not a drop-in replacement for the complete record.

Not yet verified:

  • one packet equals one displayed frame;
  • width, height, pixel format, or frame timing fields for every packet type;
  • delta-frame opcode meanings;
  • a general decoder or encoder;
  • safe replacement of boot-critical artwork.

Step 1: catalog the package

python tools/asset_catalog.py \
  ../private-inputs/stock.pkg \
  --json-out ../generated/gui-assets.json

Optionally extract individual records into a private directory:

python tools/asset_catalog.py \
  ../private-inputs/stock.pkg \
  --extract-dir ../generated/gui-records \
  --json-out ../generated/gui-assets.json

The report should state:

  • package component index and component size;
  • record count;
  • whether records cover the entire component;
  • for each record: path, offsets, sizes, hashes, CRC state, packet count, unique packet count, and repetition statistics.

If records do not cover the entire component, do not assume the remainder is padding. Preserve and investigate it.

Step 2: select a record by path

Choose a non-critical asset. Avoid boot logos, update UI, pairing UI, or the only visible recovery screen for initial experiments.

Search the JSON report:

python - <<'PY'
import json
from pathlib import Path

report = json.loads(Path("../generated/gui-assets.json").read_text())
for record in report["records"]:
    path = record["path"]
    if "alexa" in path.lower():
        print(record["index"], record["size"], record["length_prefixed_chunks"], path)
PY

This prints metadata only. It does not publish payload bytes.

Step 3: split one outer record

python tools/gui_animation.py extract \
  ../generated/gui-records/your-record.bin \
  ../generated/one-record

The output contains:

one-record/
├── animation.json
├── packet-0000.bin
├── packet-0001.bin
└── …

animation.json records path, payload version, packet order, size, and hash.

Step 4: prove the lossless outer round trip

python tools/gui_animation.py rebuild \
  ../generated/one-record \
  ../generated/rebuilt-record.bin

Linux/WSL:

cmp ../generated/gui-records/your-record.bin ../generated/rebuilt-record.bin
sha256sum ../generated/gui-records/your-record.bin ../generated/rebuilt-record.bin

PowerShell:

Get-FileHash "..\generated\gui-records\your-record.bin"
Get-FileHash "..\generated\rebuilt-record.bin"

Expected result: identical SHA-256 values and no cmp output. This proves the tool preserved bytes and packet order. It does not decode the picture.

Step 5: compare packet-level metadata

Build a table for at least three records:

Record Total bytes Packets Unique packets Repeated packets First packet size
A
B
C

Questions worth testing:

  • Do packets at the same index share a fixed prefix?
  • Are dimensions repeated in every packet or only the first?
  • Do identical packet hashes correspond to visually repeated states?
  • Does packet size correlate with visible complexity?
  • Are there distinct full-frame and delta-like size clusters?

Correlation is not field identification. Keep alternative explanations.

Step 6: create a synthetic hypothesis test

Suppose bytes 0..1 might be width. Do not rename them immediately. Write a test that:

  1. parses those bytes from several private packets;
  2. compares the value with independently known display dimensions;
  3. checks endianness;
  4. searches for the same value at other offsets;
  5. rejects packets too short for the proposed field;
  6. uses synthetic bytes in the public unit test.

A useful public fixture might be:

packet = bytes.fromhex("01c801c8") + b"synthetic-payload"

The fixture must be invented, not copied from proprietary content.

Step 7: distinguish texture formats from framing

TouchGFX officially supports several bitmap formats, including RGB565, RGB888, ARGB8888, indexed variants, and compressed forms. Ambiq GPU tooling can introduce other texture encodings. Neither fact proves the inner Xiaomi packet layout.

Keep layers separate:

flowchart LR
  RECORD[Component-6 record] --> OUTER[Observed Xiaomi outer framing]
  OUTER --> PACKET[Length-prefixed packet]
  PACKET -. unknown fields .-> FRAME[TSCFrameImage framing or delta layer]
  FRAME -. possible payload .-> TEXTURE[GPU/bitmap texture]
  TEXTURE -. decoded result .-> PIXELS[Visible pixels]

An encoder is complete only when it can reverse every required layer accepted by the watch loader.

Step 8: decoder before encoder

Recommended implementation order:

  1. parse packet headers into named and unknown_* fields;
  2. emit a structured report without rendering;
  3. identify packet classes;
  4. decode one known full-image payload;
  5. export pixels to a standard format;
  6. decode repeated/delta packets;
  7. render a complete animation offline;
  8. compare rendered output against a device recording;
  9. implement inverse encoding;
  10. rebuild a non-critical record and test only with recovery available.

Device-test gate

Do not test a replacement unless all are true:

  • original and output package identities are recorded;
  • component and record sizes are intentionally handled;
  • every outer CRC is recalculated and verified;
  • the boot-chain/signature implication is understood for that package path;
  • a non-critical asset was selected;
  • battery and connection are stable;
  • a recovery route has been independently verified as far as possible.

See OTA safety and Recovery.

RPK application development

Third-party applications use Xiaomi's JavaScript quick application framework. They are separate from native firmware applications and are the safest path for new watch experiences.

Typical package contents

Working S1 Active applications commonly contain:

app.js
manifest.json
manifest-watch.json
Common/logo.png
pages or feature directories
entry-release-unsigned.bin

The source UI uses JavaScript, HTML-like templates, and CSS. Image components and frame animations accept PNG/JPG resources. The platform compiler creates the RPK and its compiled entry binary.

RPK versus native firmware

RPK application Native application
Installed and removed through Mi Fitness Compiled into system firmware
PNG/JPG source assets Shared native GUI archive
Sandboxed JavaScript APIs C/C++ system privileges
Lower device risk OTA/boot risk
Best for new applications Best for modifying existing system behavior

An RPK cannot assume access to microphone streaming, Bluetooth internals, or system actions merely because a built-in application has those capabilities. Phone assistance may be required, and available APIs differ by firmware.

Safe development loop

  1. Start from a minimal page and one ordinary PNG icon.
  2. Build with the Xiaomi Watch IDE/toolchain available to you.
  3. Install through the same Mi Fitness route used for known working RPKs.
  4. Verify launch, back gesture, physical buttons, suspend/resume, and uninstall.
  5. Add one capability at a time and record the API/firmware requirement.

Do not include third-party sample RPK binaries in this repository. Document their public source URL or describe the structure instead.

Build an RPK from zero

This tutorial follows the public Xiaomi/70mai lightweight-watch framework. Tool versions differ, so prefer the template created by your installed IDE over manually inventing every manifest field.

What you are building

An RPK app is a separately installable application containing configuration, JavaScript behavior, HTML-like page templates, CSS, and ordinary image resources. It is not a native firmware app and does not inherit native privileges.

flowchart LR
  IDE[Xiaomi Watch IDE project] --> CONFIG[configuration<br/>bundle, version, routes]
  IDE --> APP[app.js<br/>application lifecycle]
  IDE --> PAGES[pages/index/index<br/>HTML + CSS + JS]
  IDE --> COMMON[common<br/>PNG/JPG + shared JS]
  CONFIG --> BUILD[IDE build command]
  APP --> BUILD
  PAGES --> BUILD
  COMMON --> BUILD
  BUILD --> RPK[installable RPK]
  RPK --> INSTALL[Mi Fitness installation route]
  INSTALL --> TEST[watch lifecycle test]

Step 1: install the official toolchain

Use the Xiaomi Watch development-tools page. Its documented flow is:

  1. download and extract the Windows IDE;
  2. configure the signing material generated for your own project;
  3. start the IDE;
  4. choose File → New → Project → Js Project;
  5. run the project build command from the IDE terminal;
  6. locate the generated RPK under the project's build output.

Keep private keys outside Git. Never use example passwords or identities from documentation for a real published app.

Step 2: create the smallest project

Use a package identifier you control, for example:

com.justnova23.watchhello

The official example shows the configuration shape:

{
  "app": {
    "bundleName": "com.justnova23.watchhello",
    "version": {
      "code": 1,
      "name": "1.0"
    },
    "vendor": "Just-Nova23"
  },
  "module": {
    "js": [
      {
        "name": "default",
        "pages": ["pages/index/index"]
      }
    ]
  }
}

Do not replace the complete IDE-generated configuration with this abbreviated example. Preserve required module, abilities, device, icon, and API-version fields supplied by the template.

The homepage path is fixed as pages/index/index in the public framework specification.

Step 3: understand the file tree

project/
├── app.js
├── common/
│   └── logo.png
├── i18n/
│   └── en-US.json
└── pages/
    └── index/
        ├── index.html
        ├── index.css
        └── index.js
  • app.js owns application lifecycle hooks.
  • pages stores route pages.
  • common stores shared scripts and media.
  • i18n stores localized JSON resources and must not be renamed.
  • page HTML defines structure, CSS defines supported styles, and JavaScript defines data and handlers.

Your IDE may wrap these files under a deeper module directory. Follow the generated project rather than moving files until the first build succeeds.

Step 4: add application lifecycle logging

export default {
  onCreate() {
    console.info("WatchHello app created");
  },
  onDestroy() {
    console.info("WatchHello app destroyed");
  },
};

The official framework defines onCreate and onDestroy for the application. Logs confirm that the runtime entered your code; they do not prove the page rendered.

Step 5: create one static page

pages/index/index.html:

<div class="page">
  <image class="logo" src="/common/logo.png"></image>
  <text class="title">Hello, watch</text>
  <text class="status">Static page loaded</text>
</div>

pages/index/index.css:

.page {
  width: 100%;
  height: 100%;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #000000;
}

.logo {
  width: 72px;
  height: 72px;
}

.title {
  margin-top: 14px;
  font-size: 28px;
  color: #ffffff;
}

.status {
  margin-top: 8px;
  font-size: 18px;
  color: #aaaaaa;
}

pages/index/index.js:

export default {
  onInit() {
    console.info("index onInit");
  },
  onReady() {
    console.info("index onReady");
  },
  onShow() {
    console.info("index onShow");
  },
  onHide() {
    console.info("index onHide");
  },
  onDestroy() {
    console.info("index onDestroy");
  },
};

The framework documents the initial page sequence as onInit → onReady → onShow. Hiding or leaving the page invokes onHide; destruction invokes onDestroy.

Step 6: use resource paths correctly

The public framework recommends:

  • relative paths for importing code, such as ../common/utils.js;
  • absolute resource paths, such as /common/logo.png;
  • url(/common/file.png) inside CSS;
  • no ../ traversal into protected storage.

For lightweight wearable API version 3+, the documentation lists BMP, JPEG, and PNG image support. Begin with a small ordinary PNG and avoid unsupported metadata or extreme dimensions.

Step 7: build before adding APIs

Run the IDE template's build command, commonly exposed as run.bat in the IDE terminal according to the official tools guide.

Record:

  • IDE version/archive date;
  • project package name;
  • API version from the generated configuration;
  • build command;
  • output filename and SHA-256;
  • warnings and errors.

Do not suppress a compiler error by copying compiled files from another package.

Step 8: test installation and lifecycle

Install through the same Mi Fitness workflow already proven to install compatible third-party RPKs. Then test in this order:

  1. icon appears once and has the expected name;
  2. app opens to the static page;
  3. text and image are centered and not clipped;
  4. physical button behavior is recorded;
  5. swipe-back or platform exit behavior is recorded;
  6. screen sleep and wake do not leave a black page;
  7. reopening triggers the expected lifecycle;
  8. uninstall removes the app and its private storage.

If the screen is black, revert to text only. A successful install proves package acceptance, not page compatibility.

Step 9: add one interaction

HTML:

<div class="page" onclick="changeMessage">
  <text class="title">{{message}}</text>
</div>

JavaScript:

export default {
  data: {
    message: "Tap the screen",
  },
  changeMessage() {
    this.message = "Tap received";
  },
};

Verify event binding before adding routing, networking, phone communication, or animation.

Step 10: add APIs defensively

The official API reference distinguishes synchronous and asynchronous calls. Asynchronous APIs may expose success, fail, cancel, and complete callbacks. Handle failure visibly and log error codes without exposing private data.

Before using an API, document:

  • import module;
  • minimum API version;
  • parameters;
  • callbacks and error codes;
  • behavior when unavailable;
  • device/firmware on which it was reproduced.

Never infer microphone, Bluetooth, or privileged action support from the existence of a similarly named system feature.

Animation without native TSCFrameImage

RPK pages can use ordinary image resources. The framework's image-animator example passes an array of PNG paths and exposes start, pause, resume, and state methods through a referenced element. This is unrelated to the native component-6 TSCFrameImage format.

Start with two or three small images and a slow duration. Confirm memory use and lifecycle behavior before increasing frame count.

Release checklist

  • unique bundle name;
  • intentional version code/name;
  • private signing key excluded from source control;
  • no copied proprietary assets;
  • correct icon and localized app name;
  • launch, exit, suspend, resume, and uninstall tested;
  • unsupported capabilities hidden or explained;
  • build hash recorded;
  • source and license ready before public distribution.

Tool reference

The scripts are intentionally small and inspectable. None communicates with the watch or starts a flash operation: they only read or transform local files.

Common setup

Run commands from the repository root. Keep inputs outside the repository and save outputs under generated/, which Git ignores.

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
mkdir -p generated

firmware_pkg.py

Inspects a complete OTA package, validates lengths and CRCs, calculates SHA-256 hashes, and can extract components.

python tools/firmware_pkg.py /path/to/update.pkg
python tools/firmware_pkg.py /path/to/update.pkg \
  --extract-dir generated/components \
  --json-out generated/package-report.json

Extraction is not decryption and does not prove that a component is executable. Keep the JSON report with the input hash.

diff_pkg.py

Inspects a differential package and reports the metadata required to associate it precisely with an earlier version.

python tools/diff_pkg.py /path/to/update-diff.pkg

build_partial_diff.py

Rebuilds a research package while changing only selected components. This is an advanced tool: it does not bypass signatures and does not guarantee that a bootloader will accept its output.

python tools/build_partial_diff.py --help

asset_catalog.py

Indexes records from the GUI component and produces a catalog containing paths, sizes, CRCs, and offsets.

python tools/asset_catalog.py /path/to/update.pkg \
  --extract-dir generated/gui-records \
  --json-out generated/assets.json

gui_animation.py

Splits and losslessly rebuilds the packets of one GUI record. The inner packets remain opaque: the project does not call them “frames” until evidence supports that name.

python tools/gui_animation.py extract record.bin generated/record
python tools/gui_animation.py rebuild generated/record generated/rebuilt.bin
cmp record.bin generated/rebuilt.bin

A byte-identical comparison proves only that the outer container was preserved.

bitmap_catalog.py

Searches for known bitmap signatures and structures, then prepares an inventory for comparative analysis.

python tools/bitmap_catalog.py component.bin --json-out generated/bitmaps.json

thumb_xrefs.py

Uses Capstone to find strings, literal pointers, MOVW/MOVT pairs, and immediate values in ARM Thumb code.

python tools/thumb_xrefs.py main.bin --text "listening"
python tools/thumb_xrefs.py main.bin --offset 0x13024c
python tools/thumb_xrefs.py main.bin --disasm-offset 0x130240 --bytes 0x80

For the documented build, the defaults are --image-offset 0 and --base 0x08000000. Do not reuse them on another build without verification.

patch_native_assistant_text_capacity.py

Applies a version-specific patch protected by a context check. If the surrounding bytes do not match exactly, it exits rather than producing an apparently valid output.

python tools/patch_native_assistant_text_capacity.py \
  main-original.bin generated/main-patched.bin \
  --report generated/assistant-capacity.json

It is compatible only with the documented build. Read Native assistant before using it.

Exit errors and diagnosis

  • ValueError: the format, size, CRC, or expected context does not match;
  • FileNotFoundError: the path is wrong or the component has not been extracted;
  • no useful output: check --help and argument order;
  • different output after a round trip: stop and preserve both hashes for investigation.

OTA safety and recovery

Before any transfer

  • charge the watch to at least 40%; 60% or more is preferable;
  • keep the phone charged and disable aggressive battery saving for Mi Fitness;
  • confirm the exact watch model, installed firmware, OTA model identifier, base version, target version, file size, MD5/SHA-256, and all outer CRCs;
  • keep an independently verified stock package and recovery notes;
  • test parsers and repackers without the watch first;
  • change one behavior per experimental package.

Validation levels

These are separate milestones:

  1. package parses;
  2. CRCs match;
  3. Mi Fitness accepts the file;
  4. Bluetooth transfer reaches 100%;
  5. the watch validates and installs it;
  6. the watch reboots and reconnects;
  7. the changed behavior works;
  8. unrelated critical behavior still works.

Never report level 3 or 4 as a successful firmware installation.

Do not repeatedly send more experimental packages. Keep it charged, allow a full boot interval, try the documented hardware restart, and preserve phone/watch pairing state and logs. Recovery options depend on which boot stage still runs; there is currently no universal software unbrick procedure in this project.

Avoid high-risk first tests

Do not begin with:

  • bootloader logic;
  • boot artwork or early boot resources;
  • memory layout or object-size changes;
  • power management and charging code;
  • updates containing many unrelated modifications.

Prefer a non-critical native constant or secondary visual asset with a known stock rollback path.

Recovery and failure handling

There is currently no public, verified procedure guaranteed to recover every S1 Active brick. This page reduces risk; it does not remove it.

Before any OTA

  • keep both watch and phone comfortably above the minimum battery threshold;
  • use a stable Bluetooth connection and disable phone battery restrictions;
  • verify the model and exact base version;
  • check files and hashes;
  • retain a legally obtained original firmware copy;
  • change only one component at a time;
  • capture logs and note the start time.

Classify the problem

Symptom First cautious action
frozen UI but notifications still arrive wait, document, then try a normal reboot
logo remains after an update do not repeat OTA blindly; preserve logs and version details
companion app cannot see the watch check Bluetooth pairing and account state before repeated resets
OLD VERSION error the delta does not match the base; do not force it
progress appears frozen determine whether progress is real or merely stale before interrupting

What not to do

  • do not send the same package repeatedly hoping for a different result;
  • do not change firmware, companion app, and pairing at the same time;
  • do not use packages for similar-looking models;
  • do not delete logs or original inputs;
  • do not present a factory reset as a universal firmware recovery method.

Useful failure report

Open an issue containing the model, version before and after, package SHA-256 without attaching it, progress percentage, complete error message, timeline, and observable device state. Remove personal identifiers and full Bluetooth details.

Reproducible reverse engineering

Record facts, not conclusions alone

For each discovery, keep:

  • device and firmware identity;
  • SHA-256 of the input file;
  • component index and size;
  • file offset and mapped runtime address;
  • raw bytes and disassembly;
  • all references and callers examined;
  • alternative explanations considered;
  • offline and device tests performed.

Correcting earlier work

Reverse engineering changes as evidence improves. Keep corrections explicit. For example, early work treated the 0x2000 SFU1 header as unmapped and used an incorrect code base. A broad embedded-pointer comparison later established the main-component mapping 0x08000000 + file_offset. Tools and documentation must use the newer verified result.

Patch design

Prefer modifications that:

  • preserve component size and offsets;
  • preserve surrounding instruction width;
  • verify the original context before writing;
  • change the smallest possible byte range;
  • reject unknown firmware builds;
  • emit hashes and a machine-readable report;
  • can be compared against an unmodified input.

Publication

Publish scripts, synthetic fixtures, offsets, minimal byte excerpts, and reasoning. Do not publish copyrighted firmware or paid packages. A result that cannot be shared as a binary can still be independently reproducible when the tool accepts a contributor's own input.

Reproducible lab notebook

A lab notebook prevents memory, filenames, and conclusions from drifting across long experiments. Keep a private notebook for sensitive paths and hashes; publish a sanitized version when it helps others reproduce the result.

One experiment per entry

Use a stable identifier such as:

EXP-2026-08-27-001-package-parse
EXP-2026-08-27-002-assistant-xref

Do not overwrite an old result after tools or assumptions change. Add a correction entry linking the earlier one.

Complete template

# Experiment ID and title

## Question
One sentence that can be answered by evidence.

## Safety boundary
- read-only / writes copy / package build / device test
- proprietary inputs remain outside repository: yes/no
- recovery prerequisite: not applicable / required / verified

## Environment
- date and timezone:
- operator:
- operating system:
- Python:
- repository commit:
- tool versions:

## Device
- model:
- firmware before:
- relevant mod/boot state:
- battery before:

## Inputs
| Name | Size | SHA-256 | Provenance |
|---|---:|---|---|

## Hypothesis

## Alternative explanations
1.

## Procedure
1. Exact command or action.

## Raw observations
- timestamps:
- offsets:
- byte context:
- return codes:
- logs:

## Derived values
- calculations:
- address mapping:
- grouped ranges:

## Result
- observed:
- reproduced:
- inferred:
- unknown:

## Validation
- repeated run:
- independent method:
- synthetic test:
- device result:

## Artifacts
- private:
- safe to publish:

## Next experiment

Command capture

Copy commands exactly, including working directory and tool commit. A command without its input identity is incomplete.

Good:

cwd: S1ActiveResearch/Xiaomi-Watch-S1-Active-Modding
commit: 0f455b64883b1bb11c08b0b3f9d1b8118d327aa5
input SHA-256: <full private hash>
command: python tools/thumb_xrefs.py ../generated/main.bin --text listening
exit code: 0

Weak:

ran the scanner and found the function

Byte excerpts

Publish only the minimum bytes necessary to describe interoperability or a patch. Include the starting offset and interpretation. Avoid long contiguous dumps.

file offset 0x130350
before: 4f f4 96 72  ; mov.w r2,#300
after:  4f f4 c8 72  ; mov.w r2,#400

Screenshot and video evidence

Record:

  • exact build/package hash;
  • start and end state;
  • action performed;
  • visible result;
  • timestamp aligned with logs.

A video proves visible behavior but usually not internal causality. Pair it with a minimal binary diff or code path when claiming that a patch caused the result.

Negative results

Document failures. A rejected package, unmatched context, or packet hypothesis that fails across assets prevents others from repeating the same dead end.

Use:

### Negative result
Hypothesis: bytes 0..1 are width.
Test: compared 12 records with known visible dimensions.
Observation: value varied independently of width.
Conclusion: hypothesis rejected for payload version 2.

Sanitization checklist

Before publishing:

  • replace personal absolute paths;
  • remove account IDs and Bluetooth addresses;
  • remove tokens, cookies, keys, certificates, and .env values;
  • do not attach firmware, APKs, RPKs, extracted assets, or purchased files;
  • keep complete hashes only when they identify an input without exposing it;
  • confirm screenshots contain no notifications or personal device names.

Evidence review

Ask another contributor to reproduce the procedure using their own legally obtained input. Independent reproduction is stronger than repeating the same command on the same file.

Resource library

This library prioritizes primary documentation, official tool repositories, and clearly identified community work. A source explains its own platform; it does not automatically prove how Xiaomi integrated that technology into this watch.

Xiaomi/70mai watch application framework

Essential

  • Development tools — Windows IDE setup, project creation, build command, signing setup, and RPK output location.
  • Framework specifications — file organization, page routes, resource paths, supported syntax, lifecycle, i18n, and media formats.
  • API reference — system modules, synchronous/asynchronous behavior, callbacks, parameters, and error codes.
  • Component reference — UI elements, touch/swipe events, images, inputs, lists, and animation components. Some sections are available only on the Chinese-language site.

How to use these sources

Use them for RPK app behavior. They support claims such as:

  • the homepage route is pages/index/index;
  • application logic lives in app.js;
  • page lifecycle includes onInit, onReady, onShow, onHide, and onDestroy;
  • ordinary application resources can use PNG/JPEG/BMP under documented API versions;
  • system functions are imported through modules such as @system.app.

They do not document the native component-6 TSCFrameImage format, private assistant privileges, firmware signing chain, or internal inter-processor transport.

Python and project isolation

Read binary fields with explicit endianness. A host machine's native byte order should never silently decide package interpretation.

Git and GitHub

This repository intentionally rejects firmware, packages, keys, and common signing-file extensions in CI.

Ghidra

For this project, raw-binary import settings and the verified base mapping matter more than decompiler prettiness. See Ghidra and ARM workflow.

Capstone

thumb_xrefs.py enables detailed ARM operands because literal loads and MOVW/MOVT construction cannot be found reliably from mnemonic text alone.

Rizin

Rizin and Ghidra are alternative analysis environments. Agreement between independent decoders can strengthen an instruction interpretation, but both can share the same wrong base address if configured identically.

ARM architecture

Do not infer the exact MCU or core revision solely from an instruction that exists in several ARM profiles. Device-specific identification requires independent evidence.

STM32 secure boot and firmware update

The observed SFU1 header resembles an SBSFU layout and includes digest/signature-related regions. The ST documents provide format and security context; they do not prove that Xiaomi uses an unmodified reference implementation or identical keys/policies.

TouchGFX

  • Graphics engine — retained-mode scene model, event collection, scene updates, and rendering loop.
  • Code structure — generated and user-code boundaries in standard TouchGFX projects.
  • Image widget — bitmap association, sizing, alpha, visibility, and performance.
  • Image formats — supported framebuffer and bitmap formats.
  • Image compression — official lossless-compression support and version context.
  • Widgets and containers — UI hierarchy concepts.
  • SVG support — supported and unsupported vector features in recent TouchGFX versions.

The watch firmware contains TouchGFX-related structures and a bitmap database, but the component-6 animated asset layer is not fully explained by standard TouchGFX documentation.

Ambiq and texture compression

PixPresso and Nema tooling help investigate TSC texture payloads. A texture generated by a vendor tool is not automatically a complete Xiaomi TSCFrameImage record.

Documentation diagrams

Project diagrams must still be evidence-backed. Rendering a relationship beautifully does not make it true.

Community research

Community projects can provide valuable leads, but check license, supported model, version, and reproducibility. Paid or extracted binaries must not be copied into this repository.

Source evaluation checklist

Before adding a technical reference, ask:

  1. Is it the original vendor/project documentation?
  2. Which version or date does it describe?
  3. Does it describe RPK apps, standard TouchGFX, STM32 SBSFU, or this exact watch integration?
  4. Which claim does it support directly?
  5. Which part remains an inference?
  6. Can another contributor access it without purchasing or redistributing protected material?

When a source conflicts with device evidence, preserve both and document the scope difference rather than silently choosing the more convenient statement.