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:
- Read RPK applications to understand the boundary.
- Follow Build an RPK from zero.
- Use only one page and one image for the first build.
- Verify launch, close, swipe-back, physical-button behavior, suspend, resume, and uninstall.
- 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.
- Follow Windows and Linux setup.
- Place legally obtained packages outside the Git repository.
- Calculate SHA-256 and record the result.
- Follow Full-package walkthrough.
- Compare your report with Research data and charts.
- 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.
- Inspect both packages independently.
- Compare metadata before extracted payloads.
- Match components by index, type, size, and role evidence.
- Calculate changed byte ranges rather than only a global hash.
- Separate expected metadata changes from executable changes.
- 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.
- Install Ghidra from its official release and a supported 64-bit JDK.
- Import the extracted main component as raw ARM little-endian code.
- Keep the file mapping at
0x08000000for the documented build. - Do not analyze the whole binary as one uninterrupted code stream.
- Use strings and
thumb_xrefs.pyto identify bounded regions. - Rename functions only when evidence supports the name.
- 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.
- Catalog records without extracting copyrighted data into Git.
- Select a non-critical record by path.
- Split its outer packet stream.
- Rebuild it unchanged and prove byte identity.
- Compare packet headers across several locally held records.
- Create synthetic fixtures for every proposed field.
- 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:
- Git for Windows;
- Python for Windows 3.11 or newer;
- optionally Ghidra and its required 64-bit JDK.
During Python installation, enable the launcher or ensure python is available in a new terminal.
2. Verify commands¶
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:
Do not weaken the machine-wide execution policy merely to activate a virtual environment.
4. Create private directories¶
5. Hash an input¶
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¶
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¶
Run the repository checks¶
From the repository root:
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 and python3 use different installations¶
Print both executable paths:
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:
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¶
- Inspect a full package
- Compare firmware versions
- Analyze the main image with Ghidra
- Study native GUI assets
- Build an RPK app from zero
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:
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¶
- Start with one proposed node or edge, not a whole subsystem.
- State what evidence could prove it: marker, cross-reference, packet trace, call site, or controlled behavior.
- Record model, version, file hash, component, offset, and command.
- Try to disprove the relationship with an alternative explanation.
- Mark it inferred until independent evidence closes the gap.
- 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:
- the exact edge or node being changed;
- evidence level before and after;
- command, offset, hash, trace, or official source;
- alternative explanations considered;
- 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:
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:
Linux/WSL:
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:
- reads the file size;
- checks the
fullmagic; - derives header size from declared body length;
- parses nine descriptors;
- confirms that component lengths equal the body length;
- hashes every component and validates stored CRCs;
- inspects known inner wrappers;
- 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:
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:
- Outer CRC matches: the OTA container region is internally consistent.
- SFU1 payload digest matches: the inner payload matches the digest stored in its header.
- 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:
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:
- preserve both files;
- confirm the report and extraction came from the same command/input;
- check disk errors and path confusion;
- 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¶
- To compare another package: Firmware comparison.
- To inspect component 6: GUI asset laboratory.
- To inspect main-image code: Ghidra and ARM workflow.
- To understand package fields: Firmware package formats.
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:
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:
Use:
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¶
- Reproduce the state on the device and record the exact action.
- Search visible strings in the firmware.
- Connect the string, handler, and graphics records using verifiable references.
- Compare stock and modified files built from the same base version.
- 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¶
- Extract component 0 with
firmware_pkg.py. - Verify its hash against the extraction report.
- Copy it into a private working directory.
- 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¶
- Start Ghidra.
- Choose File → New Project.
- Select Non-Shared Project unless you have deliberately configured a secure team server.
- Store the project outside this public Git repository.
- Use a name that contains model and version, not a generic
firmwarename.
Example: M2116W1_1.4.174_main_analysis.
Import as a raw binary¶
- Choose File → Import File.
- Select the extracted component.
- When Ghidra reports raw binary, choose an ARM little-endian language compatible with Thumb analysis.
- Do not guess a compiler specification as a fact; record the selection as an analysis setting.
- Open Options or the memory map and establish the documented image base
0x08000000.
The crucial mapping is:
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:
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.
Navigate to an offset in Ghidra¶
Convert file offset to runtime address, then use Go To:
At the candidate address:
- inspect bytes and instruction widths;
- verify that branch targets land on plausible boundaries;
- inspect incoming and outgoing references;
- locate prologue and return behavior;
- compare the same region in stock and modified components;
- 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_screenwhen only a string reference is known;bluetooth_receivewithout transport evidence;alexa_mainfor 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:
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 decimal is 0x130352.
Also verify equal lengths:
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:
- identify allocation and object lifetime;
- find every reader of buffer, length, and flags;
- determine UI text widget limits and scrolling behavior;
- replace inline storage with a bounded external allocation or paging model;
- update destructor/reset paths;
- test out-of-memory and malformed UTF-8 cases;
- 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¶
- Locate every code reference to
TSCFrameImagein the main image. - Translate the packet reader into a small host-side decoder.
- Export known native assets to individual TSC textures and then PNG files.
- Identify full-frame, repeated-frame, and delta-frame packet types.
- Test whether the loader accepts independent full TSC6A frames; this may avoid implementing delta compression initially.
- Implement the inverse packet builder.
- Prove an original asset can be decoded and rebuilt with identical rendering.
- 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:
animation.json records path, payload version, packet order, size, and hash.
Step 4: prove the lossless outer round trip¶
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:
- parses those bytes from several private packets;
- compares the value with independently known display dimensions;
- checks endianness;
- searches for the same value at other offsets;
- rejects packets too short for the proposed field;
- uses synthetic bytes in the public unit test.
A useful public fixture might be:
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:
- parse packet headers into named and
unknown_*fields; - emit a structured report without rendering;
- identify packet classes;
- decode one known full-image payload;
- export pixels to a standard format;
- decode repeated/delta packets;
- render a complete animation offline;
- compare rendered output against a device recording;
- implement inverse encoding;
- 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¶
- Start from a minimal page and one ordinary PNG icon.
- Build with the Xiaomi Watch IDE/toolchain available to you.
- Install through the same Mi Fitness route used for known working RPKs.
- Verify launch, back gesture, physical buttons, suspend/resume, and uninstall.
- 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:
- download and extract the Windows IDE;
- configure the signing material generated for your own project;
- start the IDE;
- choose File → New → Project → Js Project;
- run the project build command from the IDE terminal;
- 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:
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.jsowns application lifecycle hooks.pagesstores route pages.commonstores shared scripts and media.i18nstores 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:
- icon appears once and has the expected name;
- app opens to the static page;
- text and image are centered and not clipped;
- physical button behavior is recorded;
- swipe-back or platform exit behavior is recorded;
- screen sleep and wake do not leave a black page;
- reopening triggers the expected lifecycle;
- 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:
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.
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.
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.
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
--helpand 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:
- package parses;
- CRCs match;
- Mi Fitness accepts the file;
- Bluetooth transfer reaches 100%;
- the watch validates and installs it;
- the watch reboots and reconnects;
- the changed behavior works;
- unrelated critical behavior still works.
Never report level 3 or 4 as a successful firmware installation.
If the watch remains on the Xiaomi logo¶
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:
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:
Byte excerpts¶
Publish only the minimum bytes necessary to describe interoperability or a patch. Include the starting offset and interpretation. Avoid long contiguous dumps.
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
.envvalues; - 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, andonDestroy; - 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¶
- Python
venv— official virtual-environment creation and activation behavior. - Python
hashlib— SHA-256 and other secure hash APIs. - Python
zlib— CRC32 implementation used by the package tools. - Python
struct— explicit byte order and binary field conversion. - Python
argparse— command-line parsing used by repository scripts. - Python
unittest— synthetic regression tests.
Read binary fields with explicit endianness. A host machine's native byte order should never silently decide package interpretation.
Git and GitHub¶
- Installing Git — official Git book installation chapter.
- Git object integrity — useful background for repository hashes, which are different from firmware SHA-256 identity.
- GitHub encrypted secrets — credentials belong in protected settings, never committed files.
- GitHub Pages — hosting and deployment documentation for this guide.
- GitHub security advisories — responsible disclosure workflow for repository tooling issues.
This repository intentionally rejects firmware, packages, keys, and common signing-file extensions in CI.
Ghidra¶
- Official NSA Ghidra repository — releases, installation requirements, source, security advisories, and documentation.
- Ghidra releases — obtain official prebuilt archives; do not confuse source archives with release builds.
- Ghidra beginner guide — official project training material.
- Ghidra security advisories — check before opening unknown binaries.
For this project, raw-binary import settings and the verified base mapping matter more than decompiler prettiness. See Ghidra and ARM workflow.
Capstone¶
- Capstone official site — project overview and releases.
- Python tutorial — architecture/mode setup, instruction iteration, detailed operands, and semantic groups.
- Capstone source repository — code, issues, and version history.
thumb_xrefs.py enables detailed ARM operands because literal loads and MOVW/MOVT construction cannot be found reliably from mnemonic text alone.
Rizin¶
- Rizin official documentation — command-line analysis concepts and commands.
- Rizin source repository — releases and implementation.
- Cutter — graphical interface built around 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¶
- Arm developer documentation — authoritative architecture manuals and instruction references.
- Arm Architecture Reference Manual for A-profile architecture — useful for general instruction semantics; choose the correct profile for the analyzed core.
- CMSIS documentation — Cortex-M software interface and core concepts.
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¶
- ST X-CUBE-SBSFU user manual UM2262 — secure boot/update concepts, image authentication, and update flow.
- X-CUBE-SBSFU data brief — official package scope and supported STM32 context.
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¶
- Ambiq Apollo4 Display Kit / Nema GUI Builder guide — Nema graphics tooling and embedded display workflow.
- Ambiq Apollo4 Family SDK User's Guide — official SDK context for NemaGFX initialization, memory pools, synchronization, and GPU operation.
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¶
- Material for MkDocs diagrams — official Mermaid integration, supported diagram types, instant navigation, and theme behavior.
- Mermaid flowcharts — official syntax and edge semantics.
- Mermaid sequence diagrams — actor/message timeline syntax.
- Mermaid state diagrams — lifecycle/state modeling.
Project diagrams must still be evidence-backed. Rendering a relationship beautifully does not make it true.
Community research¶
- m0tral/UnpackMiColorFace — community tool for Xiaomi watch-face investigation.
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:
- Is it the original vendor/project documentation?
- Which version or date does it describe?
- Does it describe RPK apps, standard TouchGFX, STM32 SBSFU, or this exact watch integration?
- Which claim does it support directly?
- Which part remains an inference?
- 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.