
بروتوكول BLE مُهندس عكسيًا لـ CMF Watch Pro 2، مع توثيق تخطيط GATT، إطارات أوامر مشفرة بـ AES-128-CBC، مصافحة توثيق، ومزامنة بيانات الصحة لتطوير تطبيق رفيق بديل.
غير رسمي. يصف هذا المستند بروتوكول البلوتوث منخفض الطاقة (BLE) الخاص بساعة CMF Watch Pro 2 (CMF by Nothing)، الذي أُعيد بناؤه عبر الهندسة العكسية لتطبيق مصاحب بديل. لا يرتبط هذا المستند بـ Nothing/CMF ولا يحظى بتأييدها. استخدمه على مسؤوليتك الخاصة.
جميع الأعداد الصحيحة متعددة البايت في ترويسة الإطار ورموز التشغيل تكون بالترتيب الكبير (big-endian). الأعداد الصحيحة داخل
حمولات الأوامر تكون بالترتيب الصغير (little-endian) ما لم يُذكر خلاف ذلك (وهذا يعكس برنامج الساعة الثابت) —
انتبه للاستثناءات (GOALS_SET، GPS_PUSH، إزاحة/طول النقل الجماعي تكون بالترتيب الكبير).
كل ادعاء غير بديهي أدناه مُوسوم بكيفية إثباته:
حيثما يصحح قسم لاحق قسمًا سابقًا، يُحتفظ بالنص السابق مع مؤشر بدلًا من حذفه — معرفة القراءات التي جُرّبت ودُحضت توفر على الشخص التالي نفس الالتفاف.
جهاز الاختبار لجميع الالتقاطات: CMF Watch Pro 2-5485، البرنامج الثابت 1.0.0.73، الرقم التسلسلي CI04102520008192،
MCU Actions ATS3089C (Cortex-M4)، الشاشة 466×360.
الهاتف هو عميل GATT؛ الساعة هي الطرف المحيط، وتُعلن عن نفسها باسم CMF Watch Pro 2-XXXX
(4 أحرف سداسية عشرية).
فعّل الإشعارات بكتابة 01 00 إلى كل CCCD (00002902-…). قناة الأوامر
(fff1/fff2) تحمل البروتوكول المؤطَّر أدناه. قناة الصدفة (77d4…) تحمل نصًا
عاديًا بنمط AT (مثل AT GETSECRET؛ انظر §14). قناة البيانات (02f0…) تحمل كتلًا ثنائية
كبيرة (واجهة الساعة، البرنامج الثابت، AGPS)، تُنسَّق عبر رموز تشغيل تحكم على قناة الأوامر.
معرّفات الخدمة UUID — تُعلن الساعة عن حوالي 10 خدمات أساسية. جرى تعدادها على وحدة حقيقية:
0xfff0 (الأوامر)، 0x180f (البطارية)، 0x180a (معلومات الجهاز)، 0xefe7، 0xffd0،
02f00000-…ffe0 و02f00000-…fe00 (البيانات)، 77d4e67c-2fe2-2334-0d35-9ccd078f529c (الصدفة /
الاقتران)، e49a3001-f69a-11e8-8eb2-f2801f1b9fd1، f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ معرّف خدمة الصدفة هو
77d4e67c-…، وليس77d4ff00-…. افترضت المراجعات السابقة لهذا المستند أن الخدمة تشترك في بادئةff00لخصائصها (77d4ff01/77d4ff02، §14) — لكنها لا تفعل، على الأقل على الوحدة التي فُحصت عليها (نتيجة من freethinkel/fmc، انظر §المصادر). معرّفات الخصائص لم تتغير. لم يُتحقق مما إذا كان77d4e67cثابتًا عبر الوحدات — عدِّد بدلًا من الترميز الثابت.
🌐 ملاحظة حول Web Bluetooth. لا يكتشف Chromium إلا الخدمات التي أدرجتها الصفحة في
optionalServices، حتى عند استدعاءgetPrimaryServices()غير المفلتر — صفحة تُدرج 3 خدمات ترى 3، بينما يُظهرchrome://bluetooth-internals(طبقة C++ الخاصة بـ Chrome، غير مُقيَّدة) كل الـ 10. إذا كتبت عميل متصفح، أدرج كل معرّف UUID أعلاه مسبقًا أو سيفشل الاقتران مع خدمات موجودة بوضوح. لا يوجد Web Bluetooth في Firefox/Safari؛ يتطلب إيماءة مستخدم + HTTPS/localhost.
✅ جرى تشغيل جلسة حقيقية كاملة على قناة الأوامر الواحدة — خلال التقاط استخدام كثيف لمدة 160 ثانية لم يكن هناك أي حركة مرور على قنوات البيانات/البرنامج الثابت أو الصدفة باستثناء أثناء نقل OTA/واجهة ساعة صريح.
0xF5)كل رسالة على قناة الأوامر مغلفة في إطار واحد أو أكثر بترويسة من 11 بايت:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 0xF5 | chunkLen | cmd1 | chunkCount | chunkIndex | cmd2 | chunk bytes … | | 1 B | 2 B (BE) | 2 B BE | 2 B BE | 2 B BE | 2 B BE | chunkLen bytes | +------+-----------+--------+-------------+-------------+--------+-------------------+ __________________________ 11-byte header ____________________________/
- يشكّل `cmd1`/`cmd2` معًا **رمز العملية** (انظر §6). 🔎 تم التحقق منه مقابل مُنشئ الإطارات في التطبيق الرسمي
(`C6117b.m30831g`).
- `chunkCount` = إجمالي الأجزاء لهذا الأمر؛ `chunkIndex` يبدأ من **1**.
- `chunkLen` = عدد بايتات `chunk` في هذا الإطار.
- قد يتم تجزئة كتابة BLE واحدة بواسطة MTU للرابط؛ يقوم المستقبِل بتخزين البايتات الخام مؤقتًا و
إعادة استخراج الإطارات الكاملة. يتم تقسيم الحمولات الكبيرة إلى أجزاء متعددة (نفس `cmd1/cmd2`،
مع زيادة `chunkIndex`) وإعادة تجميعها بالترتيب.
### اصطلاح رمز العملية (✅ تم تأكيده على الشبكة)
- `cmd1 = 0xFFFF`: `cmd2` في `0x80xx`/`0x90xx` = من الهاتف إلى الساعة (طلب/تعيين)؛ `0x00xx`/`0xa0xx` =
من الساعة إلى الهاتف (رد). تتطابق الأزواج بالبايت المنخفض (`0x9055`↔`0xa055`، `0x8051`↔`0x0051`).
- `cmd1` خاص بالميزة: لاحقة `cmd2` = `0x0001` **SET**، `0x0002` **GET**، `0x0003` **ACK**.
### جسم الجزء
لكل جزء، الجسم هو `payloadPiece ‖ CRC32_LE(payloadPiece)` (CRC من 4 بايتات، بترتيب البايت الأقل أهمية،
zlib/IEEE). إذا كان الأمر **مشفّرًا** (انظر §3)، يتم بعد ذلك تشفير `payloadPiece ‖ CRC` بالكامل
باستخدام AES-128-CBC/PKCS7 ويصبح هذا النص المشفر هو `chunk` للإطار.
**خصوصية النص الصريح:** لرموز العمليات غير المشفرة، *تحسب* الساعة CRC من 4 بايتات في `chunkLen` ولكنها
**لا** ترسله. لذا عند فك تشفير إطار غير مشفر، يكون طول البيانات الفعلي هو `chunkLen − 4`.
(تحمل الإطارات المشفرة CRC داخل النص المشفر كالمعتاد.)
حجم الأجزاء (بحيث تقع الأجزاء المشفرة على حدود كتل AES)، مع `maxWrite = mtu − 3`:
- مشفّر: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- غير مشفّر: `maxWrite − 11 − 4 − 1`
✅ كانت جميع قيم `chunkLen` المرصودة للإطارات المشفرة مضاعفات للعدد 16 (محاذاة الكتل صحيحة).
---
## 3. الأوليات التشفيرية
- **AES-128-CBC** مع حشو **PKCS7** و**IV ثابت** (من البرنامج الثابت
`CmfCharacteristic.AES_IV`):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE)، يُرسل كـ 4 بايتات بترتيب البايت الأقل أهمية.
- **SHA-256** على تسلسل الأجزاء.
اشتقاق المفتاح:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = سر الجهاز (16 بايت) (يمكن الحصول عليه من الساعة عبر أمر الصدفة
AT GETSECRET → GETSECRET:<32-hex>,OK).rnd1 = 16 بايت عشوائي يختارها الهاتف؛ rnd2 = 16 بايت عشوائي من الساعة.nonce = بايتات من رد nonce الخاص بالساعة.بعد تعيين المفتاح، جميع إطارات قناة الأوامر مشفرة بـ AES باستثناء رموز التشغيل النصية العادية المدرجة في §5.
✅ تم التحقق من كلا الاشتقاقين: تم استرداد authkey من ntwatch.db لهاتف مُجذّر وتطابق مع
القيمة المشتقة من rnd1/rnd2/secret الملتقطة؛ وتم إعادة إنتاج sessionKey من nonce ملتقط
يفك تشفير الإطارات الحية.
يشارك مسارا الدخول نفس ذيل nonce/confirm.
phone → (shell) AT GETSECRET watch → (shell) GETSECRET:<32hex>,OK phone: rnd1 = random16 ; signed1 = SHA256(rnd1 ‖ secret) phone → AUTH_PAIR_REQUEST (plaintext) payload = rnd1(16) ‖ signed1(32) // 48 B watch → AUTH_PAIR_REPLY (plaintext) payload = rnd2(16) ‖ signed2(32) // 48 B phone verifies signed2 == SHA256(rnd2 ‖ secret) phone: authkey = SHA256(rnd1 ‖ rnd2 ‖ secret)[0..16] → set crypto key = authkey phone → AUTH_PHONE_NAME (encrypted) payload = 0xA5 ‖ model(UTF-8) // e.g. "CMF Watch Pro 2" watch → AUTH_WATCH_MAC (encrypted) phone → AUTH_NONCE_REQUEST (encrypted) payload = 0xA5 watch → AUTH_NONCE_REPLY (encrypted) payload = nonce phone: sessionKey = SHA256(nonce ‖ authkey)[0..16] → set crypto key = sessionKey phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted) payload = 0xA5 watch → AUTHENTICATED_CONFIRM_REPLY (encrypted) → state = Initialized
عند حدوث `AUTH_FAILED (0xFFFF,0xA061)` أو عدم تطابق التوقيع، يفشل المصادقة.
### 4.2 إعادة الاتصال (مفتاح المصادقة معروف بالفعل)```
set crypto key = authkey (persisted)
phone → AUTH_PHONE_NAME (encrypted) payload = 0xA5 ‖ model
watch → AUTH_WATCH_MAC (encrypted)
phone → AUTH_NONCE_REQUEST (encrypted) payload = 0xA5
watch → AUTH_NONCE_REPLY (encrypted) payload = nonce
sessionKey = SHA256(nonce ‖ authkey)[0..16] → set crypto key = sessionKey
phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted) payload = 0xA5
watch → AUTHENTICATED_CONFIRM_REPLY (encrypted) → Initialized
✅ تم رصد ترتيب إعادة الاتصال (بدون حركة مرور shell) سليمًا في التقاط حقيقي.
⚠️→✅
TIMEإلزامي قبل استعلامات البيانات. بعدInitialized، لن تستجيب الساعة لـBATTERYأوSERIAL_NUMBER_GETأو مصافحةACTIVITY_FETCH_*حتى يتم إرسالTIME (FFFF 8004)في الجلسة — وبدونها، يصل فقطFIRMWARE_VERSION_RETغير المطلوب وينتهي كل شيء آخر بوقت انتهاء. ✅ تم التأكيد مباشرة (Pixel 8a): إرسال طلبات GET الثلاثة بدونTIME→ استجابة البرنامج الثابت فقط؛ إرسالTIMEأولاً → البطارية والرقم التسلسلي يبدآن بالاستجابة.
الترتيب الموصى به للمرحلة 2: TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET →
BATTERY (0xA5) → دفع الإعدادات → مزامنة الصحة (§8).
لا يوجد كود تشغيل "قراءة" منفصل لمعظم الإعدادات. إرسال *_GET (cmd2 = 0x0002، الحمولة
0xA5) يجعل الساعة ترد بكود تشغيل SET (cmd2 = 0x0001) حاملًا القيمة الحالية.
يتم تأكيد أوامر SET بـ cmd2 = 0x0003 وجسم فارغ.
الإطارات مشفرة بـ AES بمجرد تعيين مفتاح، باستثناء أكواد التشغيل هذه، التي تكون دائمًا نصًا عاديًا:
AUTH_PAIR_REQUEST (FFFF 8047)، AUTH_PAIR_REPLY (FFFF 0048)DATA_CHUNK_WRITE_WATCHFACE (FFFF 9064)، DATA_CHUNK_WRITE_FIRMWARE (FFFF 9042)،
DATA_CHUNK_WRITE_AGPS (FFFF 905F)ترويسات الإطارات (cmd1/cmd2) تسافر دائمًا بشكل واضح، لذا فإن تسلسل الأوامر مرئي في
أي التقاط حتى بدون المفتاح — فقط الحمولات المشفرة تحتاج إلى sessionKey.
(cmd1, cmd2)GET/SET/REQUEST = هاتف→ساعة؛ RET/REPLY/ACK/RESPONSE/DATA = ساعة→هاتف.
| الاسم | cmd1,cmd2 |
|---|---|
| MUSIC_INFO_SET / _ACK | FFFF 905C / FFFF A05C |
| MUSIC_BUTTON | FFFF A05D |
| الاسم | cmd1,cmd2 |
|---|---|
| WEATHER_SET_1 (الذي يعمل) | FFFF 906B |
| WEATHER_SET_2 (مُتجاهَل على Pro 2 — انظر §9) | 0066 0001 |
أكواد تشغيل JS فقط (
FFFF 8051،FFFF 0051،FFFF 90A2،FFFF 90C5،FFFF A056،FFFF 908A/908Bحالة ChatGPT/الدعم) تتم معالجتها في bytecode Hermes الخاص بالتطبيق، وليس في طبقة Java. تظهر ترويساتها في الالتقاطات لكن دلالات الحمولة ⚠️ [غير مؤكدة].
تستخدم واجهة الساعة / البرنامج الثابت / AGPS حلقة تهيئة → طلب/كتابة أجزاء → تأكيد إنهاء:
(كل cmd1 = FFFF.) تقود الساعة الحلقة بإصدار DATA_CHUNK_REQUEST_*(offset, length)
(offset/length = u32 big-endian)؛ يرد الهاتف بـ DATA_CHUNK_WRITE_* حاملًا
payload[offset..offset+length] على خاصية البيانات. انظر §11–§12 للتفاصيل.
حمولة TIME (FFFF 8004) = epochSeconds(i32, BE) ‖ utcOffsetMillis(i32, BE). تُرسل مباشرة بعد
المصادقة حتى تعرض الساعة الوقت المحلي (وتفتح استعلامات البيانات — انظر §4.3).
⚠️ الطوابع الزمنية الصحية من الساعة بتوقيت UTC. يجب على التطبيق المصاحب إضافة إزاحة UTC المحلية قبل اشتقاق اليوم التقويمي المحلي / وقت اليوم. (تجميع الصحة حسب يوم UTC الخام يقلب اليوم في وقت محلي خاطئ.)
حمولة TIME_FORMAT (005F 0001) = 1 بايت: 00 = 24 ساعة، 01 = 12 ساعة.
ACTIVITY_FETCH_1؛ ترد الساعة بـ ACTIVITY_FETCH_ACK_1 (البايت الأول 01 ⇒ جاهزة).ACTIVITY_FETCH_2؛ ثم تدفع الساعة دفعة من إطارات البيانات:
ACTIVITY_DATA، HEART_RATE_*، SPO2، STRESS، SLEEP_DATA، WORKOUT_SUMMARY[_V3].المزامنة تسلسلية (يجب أن تتبع TIME؛ تطلق الساعة التدفقات بعد ACK_2)، وليست
دفعة واحدة. جلسة ثقيلة تدفع ~170–210 إطار إشعار في ~160 ثانية. ✅
ACTIVITY_DATA (32 بايت لكل منها، LE) ✅وحدة السعرات الحرارية: يتم الإبلاغ عن سعرات النشاط بـ cal (سعرات غرامية). اقسم المجموع اليومي على 1000 للحصول على kcal. (سعرات ملخص التمرين، على النقيض، بالفعل بـ kcal.)
timestamp(i32 LE) ‖ value(i32 LE)
(القيمة = نبضة/دقيقة / نسبة SpO₂ / مؤشر الإجهاد).00DA 0001) مختلف — 5 بايت: timestamp(i32 LE) ‖ hr(u8).
✅ مثال مباشر 5e dc 29 6a 4e → ts، hr = 78 نبضة/دقيقة. نطاقات درجة الإجهاد: 1–29 / 30–59 / 60–79 / 80–99.SLEEP_DATA (ترويسة 18 بايت + N × سجلات 8 بايت) ✅SLEEP_DATA واحد = جلسة نوم واحدة؛ قد تحتوي الليلة على عدة جلسات (الاستيقاظات الدقيقة تقسم الجلسات).
الترويسة:
كل سجل 8 بايت: timestamp(u32) ‖ duration_s(u16) ‖ stage(u16).
أكواد المرحلة: 1 = عميق، 2 = أساسي/خفيف، 3 = REM، 4 = مستيقظ. ✅ تم التحقق مقابل ليلة كاملة
(جلستان، إجماليات D/C/R/A تتطابق).
WORKOUT_SUMMARY v1 (54 بايت) / _V3 (0160 0001)v1: start(u32)، end(u32)، duration_s(u32)، ثم النوع/السعرات/الخطوات/المسافة/متوسط HR وكتلة
GPS/ممتدة. ✅ تم تأكيد تخطيط v1 مقابل البرنامج الثابت. WORKOUT_SUMMARY_V3 هو تخطيط أحدث
لنفس البيانات بالإضافة إلى كتلة ممتدة ~40 بايت (exerciseLoad، هوائي/لاهوائي، recoveryTime،
VO₂max، إيقاع، PAI، أفضل أوقات الجري…). مجموعة الحقول معروفة (من قاعدة بيانات Room الخاصة بالتطبيق) لكن
إزاحات البايت الدقيقة داخل تلك الكتلة 40 بايت ⚠️ [غير مؤكدة] — إغلاقها يتطلب التقاطًا خامًا واحدًا
لتمرين GPS.
السلاسل UTF-8، مقتطعة بالبايت إلى حجم الحقل (قد يقسم الاقتطاع حرفًا متعدد البايت،
مطابقًا لسلوك s.encode()[:max] في البرنامج الثابت)؛ الحقول القصيرة مبطنة بالأصفار على اليمين.
0065 0001) ✅: iconCode(1) ‖ 0x00 ‖ when(u32 BE) ‖ titleLen(1) ‖ title ‖ body.
يحدد iconCode أيقونة التطبيق (WhatsApp=8، Telegram=12، Instagram=18، Gmail=27؛ غير معروف=0xFF).
العنوان ≤ 20 بايت، الجسم ≤ 128 بايت. أُرسل من عميل → عرضته الساعة + ACK 0065 0003.005C 0001) ✅: الرد = level(1) ‖ charging(1) (مثل 3b 00 = 59 %، غير مشحونة).00DE 0001) ✅: len(1) ‖ ASCII (مثل 10 + "CI04102520008192").0095 0001) ✅: height_cm(1) ‖ weight_kg(1) ‖ age(1) ‖ gender(1: 1=M)
(مثل = 172 سم / 73 كجم / 31 / ذكر).now/utc_offset كمعاملات صريحة
(حتمية، قابلة للاختبار). يوفر النقل الوقت الحقيقي.TIME يبوّب كل شيء (§4.3) — أرسله أولاً أو تبقى الساعة صامتة في استعلامات البيانات.GOALS_SET و
GPS_PUSH big-endian، وإزاحة/طول النقل بالجملة big-endian.تدعم الساعة (أ) أقراص الصور/المخصصة (صورة خلفية + ساعة رقمية مرسومة بالبرنامج الثابت) و(ب) الأقراص المنظمة (أقراص مدمجة/من المتجر: خلفية بالإضافة إلى طبقات sprites موضوعة، عقارب، وأدوات نصية). ينتقل كلاهما عبر قناة البيانات عبر حلقة التهيئة → الأجزاء في §6.
ما يعمل فعليًا (✅ تم التحقق مباشرة): بناء قرص صورة من أي صورة وتثبيته؛ تثبيت أي من أقراص المتجر الـ 103 دون اتصال؛ إعادة تلوين قرص منظم (استبدال الخلفية أو أي sprite غير الخلفية) وتحريك طبقاته؛ إعادة ترتيب / تبديل القرص النشط؛ وبناء قرص منظم من الصفر — غلاف المشهد
0x20مفكوك الترميز والمنشئ منفذ (§11.7)، مثبت دون اتصال لإعادة تدوير جميع أقراص المتجر الـ 103 بايتًا ببايت ولإصدار حاويات اصطناعية تجتاز مدقق البرنامج الثابت الخاص به. 🟡 الخطوة الوحيدة غير المثبتة هي مشاهدة عرض اصطناعي من الصفر على الجهاز عبر9075(الدليل الهيكلي دون اتصال يغطي بالفعل ما كان يسبب رفض0a). لا يوجد حاجز برنامج ترميز أو نقل ولا حاجة لأدوات البائع. الادعاءات القديمة "العرض المنظم مخبوز في RES / مستحيل عبر BLE" و"برنامج ترميز cf=0x1f من جانب الخادم" كانت خاطئة (خطأ إزاحة+بايت لكل بكسل) — يعرض البرنامج الثابت الأقراص المنظمة مدفوعة بالبيانات من الملف الذي ترسله.
DIAL_COMMAND (9055 / a055) ✅a055 = result(u8) ‖ selectIndex(u8) ‖ total(u8) ‖ max(u8) ‖ N × dialId(u32 LE) ‖ ffffffff. مثال: 01 05 06 07 … = نشط #5، 6 أقراص، الحد الأقصى 7.CHANGE_DIAL (009F 0001) خامل على fw 1.0.0.73 (يعيد ثابتًا، لا يبدّل) — لا
تستخدمه.INIT1 8052 (payload A5) → 0052 [0]=01 INIT2 9063 (photo, APPEND) | 9075 (structured, REPLACE) → A063 / A075 [0]=01 [ watch → DATA_CHUNK_REQUEST A064 (offset, length; u32 BE, +progress u8) phone → DATA_CHUNK_WRITE 9064 (bytes[offset..offset+length], plaintext) ] × N FINISH A065 → 9065 (payload A5)
Finish reply byte: `01` = تم التفعيل والحفظ؛ `0a` = تم التخزين لكن **لم** يتم التفعيل / مرفوض. على
أندرويد، يجب إرسال كل `DATA_CHUNK_WRITE` كـ **كتابة BLE واحدة لكل إطار** — دمج
وإعادة التقسيم حسب MTU يفقد مزامنة الرؤوس وتدور الساعة في طلب الإزاحة 0.
- **`9063` (صورة) = إلحاق.** قائمة الواجهات تنمو (6→7)؛ `watchfaceId = 0xFFFFFFFF` (قيمة
مخصصة) بحيث لا يتم رفضها أبدًا كمكررة، وتقوم الساعة بتفعيلها تلقائيًا.
- **`9075` (منظم) = استبدال** خانة `old_id`. يجب أن يكون `old_id` **موجودًا بالفعل** في القائمة
(وإلا `0a`). لإعادة تثبيت معرّف موجود بالفعل، **احذفه أولاً** (9055 قائمة ناقص المعرّف)
ثم ارفع "جديدًا" — إعادة استخدام معرّف في مكانه تعطي `0a`.
### 11.3 واجهة صورة / مخصصة — ✅ تم التحقق منها بالكامل من البداية إلى النهاية
**الحاوية** (رحلة ذهاب وإياب تم التحقق منها بالبايت؛ جميع الحقول little-endian):```
0x00 magic 6c 8d c4 a5
0x04 count 12 00 00 00 (=18) [constant, NOT an element count]
0x08 00 × 8
0x10 lenFull u32 LE (length of the whole FULL block: tag+len+payload)
0x14 FULL tag 04 48 47 3a ‖ payloadLen(u32 LE) ‖ LZ4(RGB565-LE) → 466×466 [raw 434312 B]
THUMB tag 04 38 c4 21 ‖ payloadLen(u32 LE) ‖ LZ4(RGB565-LE) → 270×270 [raw 145800 B]
EOF-4 magic 6c 8d c4 a5 [trailer = magic repeated]
Codec = معيار LZ4 block عبر RGB565 little-endian، من الأعلى إلى الأسفل (payloadLen يُحسب من
أول بايت LZ4). يستخدم التطبيق الرسمي LZ4-HC ويجرد رأس/ذيل كتلة LZ4 البالغ 21 بايت؛ كما يعمل
مُرمّز LZ4 عادي يعتمد على literals فقط — يقبل الساعة أي LZ4 صالح، ولا يُشترط تطابق البايتات.
تُضبط البكسلات خارج الدائرة المنقوشة (المركز 233,233، نصف القطر 233) على 0x0000.
INIT_2 لـ 9063 — الرأس الدقيق (✅ هذا هو الذي يعمل):```
01 ‖ size(u32 BE) ‖ FF FF FF FF ‖ 01 01 01 ‖ styleId(u16 BE) ‖ posX(u16 BE) ‖ posY(u16 BE) ‖
color565(u16 BE) ‖ FF × 8
`size` = الطول الدقيق لملف `.bin`؛ `FFFFFFFF` = قيمة `watchfaceId` مخصصة؛ `styleId` من 0 إلى 4 يحدد
تخطيط الساعة الرقمية المدمج (يُرسَم دائمًا — لا يوجد خيار "إيقاف")؛ `posX/posY` يحددان موضعه (قيم معروفة
56 / 77)؛ `color565` يلوّنه (مثل `FFFF` = أبيض). ⚠️ الصيغة الأقصر `A5 ‖ size ‖ watchfaceId` **مرفوضة**
مع رمز الإنهاء `0a` — استخدم الترويسة الكاملة أعلاه. (التنفيذ المرجعي:
`core-rust/engine.rs::build_wf_init2`، بما يطابق `C6135t.m31104u` في التطبيق الرسمي.)
**الوصفة:** غيّر حجم الصورة إلى 466×466 (وصورة مصغرة 270×270)، وحوّلها إلى RGB565-LE من الأعلى للأسفل،
واختياريًا صفّر البكسلات خارج الدائرة، واضغط كلًا منها بـ LZ4، وجمّع الحاوية أعلاه، وارفعها
عبر خط أنابيب `9063` مع `watchfaceId = 0xFFFFFFFF`. (الترميز المرجعي: `core-rust/watchface.rs`،
`work/codec_dfa.py`.)
### 11.4 قرص منظم / متجر — الحاوية والترميزات ✅
**تخطيط الملف** — ترويسة الـ 36 بايت تتكرر **بايتًا ببايت كتذييل من 36 بايت** في نهاية الملف
(✅ تم التحقق على 15 قرصًا؛ يجب أن يرفض المحلل ملفًا يختلف فيه الاثنان):```
[36-byte header][scene TLV (§11.7)][asset pool][36-byte header again]
Header (بنية متطابقة عبر جميع أقراص المتجر الـ 103؛ جميع الحقول بنظام little-endian):``` 0x00 crc_tree u32 LE [CRC32-raw of header[0x04:0x24] ‖ scene section] ✅ see below 0x04 magic 01 00 00 XX [XX = 0x00 or 0x02; both seen, meaning of 0x02 unknown] 0x08 name char[16] [NUL-terminated, e.g. "SlopeTime", "Metaball"; may carry a non-zero tail after the NUL (@0x17) — round-trip it verbatim] 0x18 size_a u32 LE [= filesize − 36 = footer offset = header+body] ✅ 103 dials 0x1c size_b u32 LE [asset-pool length, exactly] ✅ 15 dials 0x20 crc_assets u32 LE [CRC32-raw of the asset pool] ✅ see below 0x24 … [body starts here: the 0x20 scene container, §11.7]
> ⚠️ **تصحيح (يلغي "لا يوجد فحص حظر").** قراءات سابقة فسّرت `@0x00` على أنه
> معرّف/تجزئة لكل حوار و`@0x20` على أنه "3× كلمات u32 للمعرّف/التجزئة \[ليست CRC]"، وذكرت أن CRC32/Adler32/
> مجموع البايتات جميعها تفشل في المطابقة. كلتا الكلمتين **هما CRC32** — الاختبارات السابقة فاتتها لأن
> النمط غير قياسي، ولأن قراءة "3 كلمات عند `0x20`" كانت تخلط بين كلمة CRC الواحدة
> وأول بايتات حاوية المشهد التي تبدأ عند `0x24` (وكذلك "الاسم المكرر عند
> `0x2c`" هو عقدة اسم المشهد `0x86`، §11.11). الاكتشاف من
> [freethinkel/fmc](https://github.com/freethinkel/fmc)؛ تم التحقق منه مجددًا هنا.
**CRC32-raw** = متعدد الحدود IEEE المنعكس `0xEDB88320`، **`init = 0`**، و**بدون XOR نهائي** — أي
ليست `init=0xFFFFFFFF` ولا `^0xFFFFFFFF` الخاصة بـ `crc32` القياسي. هذا هو السبب الكامل
وراء عدم تطابق CRC32 الجاهز أبدًا. لاحظ اعتماد الترتيب: `crc_assets` يقع داخل النطاق
المغطى بواسطة `crc_tree`، لذا **اكتب `@0x20` أولاً، ثم احسب `@0x00`**.```python
def crc32_raw(data: bytes) -> int: # tab = standard 0xEDB88320 reflected table
c = 0 # init 0, no final inversion
for b in data: c = tab[(c ^ b) & 0xFF] ^ (c >> 8)
return c & 0xFFFFFFFF
crc_tree = crc32_raw(f[0x04:0x24] + f[0x24:first_asset])
crc_assets = crc32_raw(f[first_asset:len(f)-36])
تم التحقق: 9/9 من أقراص المتجر الأصلية تتطابق على كلتا الكلمتين، و6/6 من قوالب هذا المستودع الخاصة
تتطابق على crc_tree.
🟡 يبدو أن البرنامج الثابت لا يفرض أيًا من CRC. كل قرص ثبّته هذا المستودع فوق
9075— بما في ذلك إعادة التلوين الناتجة عن مسار التعديل الموضعي بنفس البصمة (§11.6)، الذي يغيّر حمولات الأصول وبايتات X/Y دون إعادة حساب الترويسة — عُرض بشكل سليم على الجهاز. لذا فإن CRC قديم ليس هو ما يسبب رفض0a(ذلك هو ثابت نافذة الحاوية، §11.7). تعامل مع حقول CRC على أنها اكتبها صحيحة على أي حال: رخيصة، وهي حقل النزاهة الوحيد المعروف في الصيغة. أي شيء يعيد كتابة المشهد أو مجموعة الأصول يجب أن يعيد حساب الكلمتين.
أقراص Stub (~173 بايت، مثل المعرّفات 273/274/277) هي عناصر نائبة لوجوه مدمجة في ROM: ترويسة + دليل، بدون أصول حقيقية.
الأصول — كل منها هو dimsWord(u32 LE) ‖ len(u32 LE) ‖ LZ4(payload)، حيث
cf = dimsWord & 0x1f، w = (dimsWord >> 10) & 0x7FF، h = (dimsWord >> 21) & 0x7FF، وlen
يُحسب من أول بايت LZ4 (الـ 1f 00 01 00 الذي تراه غالبًا هناك هو أول رمز LZ4 — لا
تتخطّه). الحجم بعد فك الضغط = w·h·bpp:
✅ جميع الأصول 4151/4151 عبر الأقراص الـ 103 تُفك ترميزها بدقة باستخدام مفكك ضغط lz4.block
قياسي عند w·h·bpp. الشفافية هي بايت ألفا (cf=5/24) أو 0x0000 (cf=4 خارج
الدائرة) — لا يوجد RLE ولا "هروب". الترميز = إعادة إنشاء البيانات النقطية → LZ4 قياسي → [dimsWord][len][LZ4].
INIT_2 لـ 9075 — جسم مشفّر بـ AES:```
kind(1) ‖ old_id(u32 LE) ‖ new_id(u32 LE) ‖ file_len(u32 LE)
`kind` = `0x02`/`0x03`؛ `old_id` = الاتصال النشط الحالي (من `9055`)؛ `file_len` = الحجم الحقيقي لملف
`.bin` (= `@0x18 + 36`). تثبيت ملف `.bin` من المتجر كما هو هو المسار المضمون (معرّف Ring Data 359 +
102 أخرى مؤكدة). (المرجع: `core-rust/engine.rs::build_dial_replace_init`.)
### 11.5 قواعد بنية الدليل المنظمة ✅ (تم فك الترميز والتنفيذ — مُنقّح 2026-07-02)
> **⚠️ تنقيح (2026-07-02): مخطط السجل المسطّح `61 01 00` أدناه كان به خطأ منهجي
> OFF-BY-ONE.** جسم المشهد هو TLV نظيف (§11.7)؛ **جسم عنصر قابل للرسم** (وسوم `0x30`/`0x38`
> ثابتة، `0x70` مؤشر) هو:
>
> ```
> 01 xx 00 [X u16][Y u16] …attrs… 61 [count u16][base u32][count×id u16] [05 05 00 01 pivX pivY]
> ```
>
> - السمة `0x01` تفتح الجسم: **X,Y = الزاوية العلوية اليسرى** على لوحة 466² (الـ `s16 x,y` في
> `sty_picture_t` الخاص بـ SDK).
> - **جدول الإطارات `61 …` يغلق الجسم** (`base` = مؤشر الأصل؛ `count` 1 = صورة، 10/11 =
> أطلس أرقام — "نوع السجل `0a/0b`" القديم كان في الواقع هذا العدد! — 7/13/2 = ورقة إطارات
> التعقيد).
> - إضافات المؤشر: المصدر+المقياس `[src] 00 3c 00` داخل سمة `0x01`؛ نقطة الارتكاز في
> **المقطورة** `05 05 00 01 [pivX][pivY]`. **مركز الدوران = `(X+pivX, Y+pivY)` لكل مؤشر** —
> وليس نقطة ثابتة (233,233): توجد عقارب فرعية خارج المركز (مثلًا عقارب الاتصال 366 تدور حول 150,150).
>
> المسح الخطي لـ `61 01 00` كان يربط جدول الإطارات+نقطة الارتكاز للعنصر **N** بإحداثيات X/Y
> (وبايت الوسم، "f3" القديم) للعنصر **N+1** — بدا صحيحًا فقط على الاتصالات التناظرية التي
> تتشارك عقاربها المتجاورة هندسة شبه متطابقة. "جدار المتغير المضغوط" (المواصفة 24 §24.4.5) كان
> نفس هذا الفهم الخاطئ. تم تنفيذه كـ `scan_scene_drawables` في `core-rust/watchface_struct.rs`
> و`wfweb/src/codec/parse.ts` (المشهد = المصدر الأساسي للصور/المؤشرات؛ المسح المسطّح محفوظ
> للنصوص والبديل غير المغلف). تم التحقق منه عبر أوراكل `wfweb/compare.html` (العرض مقابل
> صور PNG الرسمية من المتجر، 99 اتصالًا): 64→72 جيد، 8→5 سيئ، متوسط الفرق 9.3→7.4%.
القراءة التاريخية للسجل المسطّح (مستبدلة، محفوظة للسياق):
- **صورة ثابتة** (`61 01 00`): `asset_ptr(u32) ‖ elemId(u16) ‖ 05 05 00 01 ‖ pivotX(u16) ‖
pivotY(u16) ‖ 3B ‖ 01 ‖ 1b 00 ‖ X(u16) ‖ Y(u16)`. الزاوية العلوية اليسرى على لوحة 466² = `(X−pivotX, Y−pivotY)`.
- **مؤشر/عقرب** — نفس سجل الصورة، يُدار وقت التشغيل. **مركز الدوران = `(X+pivotX, Y+pivotY)`**
(≈ 233,233 على الاتصالات التناظرية). **مصدر البيانات هو `u8` عند إزاحة السجل `+36`**، المقياس `u16` عند
`+38` (=60): `0x0a`/`0x70` = الساعة (`h·30°+m·0.5°`)، `0x0e`/`0x71` = الدقيقة (`m·6°+s·0.1°`)،
`0x12`/`0x72` = الثانية (`s·6°`). ✅ مؤكد عبر تفكيك أدوات الاسترجاع (البديل RTC 10:10:30).
- **أداة نص / رقم** (`61 0a 00`): `asset_ptr(u32) ‖ [10×u16 font metrics] ‖ 40 01 00 ‖ flag ‖
3B ‖ 01 ‖ u16 ‖ X(u16) ‖ Y(u16)`. `asset_ptr` يشير إلى الصورة الرمزية "0"؛ الرقم *d* = الأصل عند
`index("0") + d` (10 صور cf=5 متتالية، مثل `0123456789` وعلامات الترقيم `,°`). ✅ تم عرضه.
- **تعبئة التعقيد = فهرس الإطار** (✅ مؤكد لتعقيدات الأرقام/التعداد/المقياس، count>1): القيمة
تفهرس **ورقة إطارات مُعدّة مسبقًا** في `.bin` — `frame = (count−1)·val/100` (نسبة مئوية) أو
`frame = value` (رقم قلّاب / تعداد). جدول الإطارات = سجل فرعي `61 ‖ count(u16) ‖ base(u32) ‖
count×id(u16)`. مثلًا ساعة 327 Digit Max الكبيرة هي ورقة من 13 إطارًا (الأرقام 0–12)، `frame = hour`.
- **حلقة التقدم / القوس = قص قطاع وقت التشغيل** (✅ 2026-07-02، **يصحح قراءة "الحلقات كأوراق إطارات"
في المواصفة 25 §2**): وسم العنصر **`0x81`** يحمل **قرصًا كاملًا واحدًا** (`61` جدول إطارات
`count == 1`)، والإسفين الجزئي هو ذلك القرص **مقصّصًا إلى قطاع دائري** (`frac = value/max`،
باتجاه عقارب الساعة من الساعة 12) — تم التحقق منه بكسلًا بكسلًا على 322 Glare 2 وتأكيد `count==1` عبر
**20 اتصالًا**. على القرص: جسم `0x81` = فرعي `0x01` (هندسة `x@+0 y@+2 w@+4 h@+6`، `61 1 base` مضمّن
= القرص) + فرعي `0x5b` (مواصفات القوس). تم تنفيذه في wfweb (`blendSector`).
⚠️ السجل الفرعي `0x5b` **ليس** مجرد "`max` u16 `@+4`" كما وُثّق سابقًا — ذلك قرأ
النصف المنخفض من `max i32` وفاته **زاويتا البداية/النهاية وعرض الخط** التي تقع
بعده مباشرة. يوجد أيضًا شقيق إجرائي `0x80`/`0x5a` (مع نصف قطر صريح) لم يغطّه هذا
المستند مطلقًا. تخطيط السجل الكامل، وما أخطأت فيه افتراضية "باتجاه عقارب الساعة من الساعة 12"
السابقة: **§11.15**.
- **معرّف مصدر البيانات** — كتلة سمات `82` للعنصر تقع عند `delim+3` (بعد آخر `40 01 00`)،
و**معرّف المصدر هو `u8` عند `+0x14`** (أيضًا `relX@+0x07 s16`، `relY@+0x09 s16`، `anchor@+0x0C/0E`،
`mode@+0x15`، `frame-count@+0x1A`). المرساة < 0 = المحاذاة إلى حافة العنصر الأب. 🔎 البرنامج الثابت
يحل المعرّف عبر جدول أدوات استرجاع من 142 إدخالًا عند `0x101f371c` (كل منها يستدعي `ux2sys_get(type)`).
⚠️ **يُفضَّل قراءة المعرّف كـ `meta[9]` من البنية (§11.11)** — حقل ثابت — بدلًا من هذا المسح
الأمامي لسمة `82`، وهو مصدر خطأ off-by-one الموصوف في §11.8/§11.9. جدول المعرّفات الكامل في §16؛ لاحظ
أن تسميات الصحة/الطقس التي أدرجها هذا القسم سابقًا سطريًا (`0x19` معدل ضربات القلب، `0x1b` البطارية،
`0x24` درجة الحرارة، `0x36` الخطوات) **متنازع عليها وربما خاطئة** — انظر صندوق ⚠️ في §16.
(مثال مجموعة `0x07:0x0b:0x0f` = HH:MM:SS أدناه غير متأثر.)
- **عقدة مجموعة** (`0x68`): تتداخل أطفالها داخل جسم TLV الخاص بها (`0x60` = قيمة/نص،
`0x30` = ثابت)؛ كل `0x60` يحمل معرّف مصدره عند `data+16`. مثلًا مجموعة `0x07:0x0b:0x0f` =
ساعة HH:MM:SS. محلل عناصر TLV = `0x100db55c` (جدول قفزات مفهرس بـ `tag−0x70`).
### 11.6 مصفوفة التأليف
| المسار | الحالة | ملاحظات |
|---|---|---|
| اتصال صورة من أي صورة | ✅ **منجز** | §11.3؛ تم التحقق منه على الجهاز |
| تثبيت أي من 103 اتصالات المتجر | ✅ **منجز** | §11.4؛ `9075`، `old_id`=النشط |
| إعادة تلوين خلفية cf=4 لاتصال متجر | ✅ **يعمل مباشرة** | استبدال الحمولة الكاملة في مكانها، ضبط `len` للأصل إلى **الحجم الجديد** للكتلة (≤ القديم)، الحفاظ على نفس بصمة الملف، تثبيت جديد |
| إعادة التأليف عبر القوالب (تبديل بكسلات أي طبقة + تحريك الهندسة) | ✅ **يُعرض عبر BLE** | الاتصال 373: الخلفية→سماوي + صورة cf=5→أحمر + تحريك X 224→100، كلها معروضة، العقارب حية |
| اتصال منظم اصطناعي 100% من الصفر | ✅ **الباني منجز، تم التحقق منه دون اتصال** | باني غلاف `0x20` في `watchface_struct.rs` (`build_container`/`serialize`/`validate_container`)؛ يعيد تدوير جميع الاتصالات الـ 103 بايتًا بايتًا + الاصطناعي يجتاز مدقق البرنامج الثابت (§11.7). 🟡 عرض على الجهاز عبر `9075` لم يُصوَّر بعد |
| خطوط النظام (`.font`) | ✅ **فك ترميز/عرض (الكل)** | LVGL bin (ليس ملكيًا)؛ 32 خط أرقام (`num*/nm*`، غير مضغوط) + 24 خط نص (`font*`، LVGL RLE `comp=1`) كلها تُفك ترميزها — 12208 حرفًا رسوميًا، 0 تجاوز، ASCII كامل. RLE = LVGL v8.3 `lv_font_fmt_txt.c` (حالات SINGLE/REPEATE/COUNTER الثلاث + مرشح XOR المسبق لكل صف)، منقول 1:1، بدون تفكيك |
⚠️ مزالق إعادة التلوين/إعادة التأليف التي تسبب شاشة سوداء أو `0a`: ترك **`len` الأصلي القديم** (الساعة
تقرأ ما بعد الكتلة → تجاوز → أسود)؛ **زيادة حجم الملف** (مرفوض عند التثبيت)؛ إعادة استخدام معرّف
**في مكانه** بدلًا من تثبيت جديد؛ **إعادة ترتيب الأصول** دون إصلاح سلسلة حجم كتلة الذيل المرجعية
(§11.11). ليس قاتلًا اليوم لكن اكتبه بشكل صحيح على أي حال: أي تغيير في المشهد أو مجموعة الأصول
يبطل كلمتي **CRC32** في الترويسة — أعد حساب كلتيهما (§11.4)، `@0x20` قبل `@0x00`.
### 11.7 غلاف المشهد `0x20` — تم فك ترميزه والباني منفذ ✅
اتصال **حقيقي** مُعاد تأليفه يُعرض لأنه يحافظ على غلاف مشهد الملف. جسم اصطناعي بحت من سجلات
`61 …` المسطحة **مرفوض** — محلل البرنامج الثابت (`WFManager_Parser`، `0xdb35c`)
يتطلب أن يبدأ الجسم (من الإزاحة `0x24`) بغلاف مشهد `0x20`. الملف الكامل هو:```
[0x00,0x24) header: perDialId@0 · version=1@4 · name[16]@8 · size_a@0x18 · size_b@0x1c · idWord0@0x20
[0x24, fa) scene: 20 <u16 L0> ( 21 <u16 L1> ( 86 <len>=name , 30/70/80/81… drawables ) [ 22 … AOD ] )
[fa, EOF) assets: [dimsWord u32][len u32][payload = 1f 00 01 00 + LZ4] …
size_a = filesize−36 · size_b = filesize−36−first_asset · 0x27+L0 == first_asset
المشهد هو TLV متداخل نظيف — [tag u8][len u16 LE][body]، حاويات الوسوم 0x20/0x21/0x22/0x68
تتكرر، والعناصر القابلة للرسم الطرفية 0x30 (ثابت) / 0x70 (عنصر/مؤشر) / 0x80 / 0x81 / 0x86 (اسم).
(سجلات 61 01 00 / 61 0a 00 المسطحة هي أنماط تعيش داخل أجسام العناصر القابلة للرسم؛
المحلل القديم وجدها استدلاليًا — وقام بلصق الأجسام المتجاورة معًا، انظر مراجعة §11.5.
تخطيط جسم العنصر القابل للرسم أصبح الآن مفكوكًا بالكامل هناك.) يجب أن يتناسب offset+len لكل طفل داخل نافذة
والده؛
أول بايت في الجسم ≠ 0x20 → خطأ محلل −16؛ طفل يتجاوز نافذته → −2؛ أي منهما يجعل
معالج 9065 (0xeb50c) يكتب إنهاء .
المنشئ مُنفَّذ ومُتحقق منه دون اتصال (core-rust/watchface_struct.rs:
SceneNode / serialize / parse_scene / validate_container / build_container /
build_container_raw؛ واجهة الأوامر cmfwatch-wfgen reframe):
scene_roundtrip_identity — جميع أقراص المتجر الـ103: parse_scene→serialize يعيد إنتاج المشهد
بايتًا ببايت (قيم len المتداخلة المعاد حسابها تطابق) وvalidate_container ينجح على كل واحد منها.build_reframe_identity / واجهة الأوامر reframe — إعادة تجميع ملف .bin بالكامل من الصفر يعيد إنتاج
الملف بايتًا ببايت باستثناء بايت حشو اسم واحد (@0x17؛ ليس مجموع تحقق).build_container_synthetic — يؤلف قرصًا جديدًا (خلفية + عنصر قابل للرسم متداخل في 20→21) يمر
بالثابت الدقيق للبرنامج الثابت (build_container يُصدر نوافذ متداخلة صحيحة).validate_rejects_bad_containers — يرفض جسم مسطحًا (→ ، خطأ التاريخي)
وطفلًا يتجاوز نافذته (→ ).🟡 لا يزال غير مُثبت (يحتاج الساعة، غير معطِّل): رفع عنصر اصطناعي من الصفر عبر 9075
ومشاهدته يُعرض — الإثبات الهيكلي دون اتصال يغطي بالفعل ما سبّب رفض 0a.
قارنت عرض wfweb مع الصور المصغرة الرسمية للمتجر (مرجع البكسل عبر جميع الأقراص الـ103) وسدّدت أربع فجوات:
i16 (موقّعان). ✅ يمكن أن تكون نقاط الارتكاز سالبة للعناصر التي
تمتد خارج اللوحة — مثل عقرب الثواني الأحمر في 275 عند Y = 0xFFFC = −4 (صورة 30×281،
مصدر 0x12، مائلة من المركز فوق الحافة العلوية). قراءة X/Y كـ u16 (65532) جعلت
الحارس يُسقطه. اقرأ كليهما كموقّعين واسمح بنطاق سالب صغير.0x60 img_numbers على المستوى الأعلى (ليس فقط داخل مجموعة 0x68)، و
مصدر البيانات الحقيقي هو u8 عند إزاحة السجل −5 — فحص السمة الأمامي 82 هو
منحرف ببايت واحد هنا بشكل منهجي ويلتقط سمة الأخ التالي (في 275 التقط رقم الدقيقة
سمة يوم الأسبوع 0x18). "10:10" في 275 = الساعة 0x07@X≈306 + الدقيقة 0x0b@X≈369 مع :
كعنصر ثابت مجاور بينهما، كل منهما أطلس 11 حرفًا (). ⚠️ عند تصحيح X/Y
من ، يجب أن تتحرك ، أو يعيد التصدير إفساد تلك البايتات (يكسر
نفس البصمة → ).أيضًا: الصور المصغرة الرسمية للمتجر تُعرض عند 10:10 (وقت تسويقي كلاسيكي)، وليس 10:12 — مطابقة وقت المرجع إلى 10:10 تُقلل فرق البكسل المتوسط بشكل ملحوظ. محلل wfweb الآن يعيد تدوير جميع الأقراص الـ103 بايتًا دقيقًا (إصلاح إزاحة كتابة X/Y أعلاه أزال آخر عدم تطابق).
0x22 الخاصة بـ AOD في عرضها الخاص. ✅ متجول المشهد يتخطى بالفعل 0x22،
لكن فحص النص/الرقم المسطح اجتاز كامل [0x30, firstAsset) — لذا أصدر
متغير التشغيل الدائم (AOD) لكل عنصر كطبقة عادية. على "Gradient" أطلس التاريخ الرمادي الخاص بـ AOD
(الإزاحة في 0x22) رُسم فوق الأحمر العادي. الإصلاح: وسم كل سجل 0x22 بـ
layer.aod=true (مع مجموعة إزالة تكرار خاصة به) ودع renderAt(…, aod) يعرضها فقط في وضع AOD
(الوضع العادي يخفي طبقات aod؛ وضع AOD يخفي الطبقات العادية؛ الخلفية تُبدَّل بواسطة setAod
وتُرسَم دائمًا). صافي ربح المرجع في الوضع العادي عبر المجموعة (284: 31%→21%، +18 أخرى) —
متغيرات AOD كانت ترسم فوق العديد من الأقراص — ومفتاح AOD في المحرر الآن يعرض التخطيط
الدائم الحقيقي بدلًا من العادي. AOD الحقيقي هو شاشة سوداء (لا يوجد مشهد خافت):
إذا لم يكن للقرص إطار خلفية AOD مخصص (dial.aod)، يُخفى المشهد العادي في وضع AOD
فيُعرض أسود + عناصر 0x22 بألوانها الخاصة. AOD تُحلل عبر متجول
المشهد أيضًا (الآن يتكرر في حاوية موسومًا العناصر القابلة للرسم ، بدلًا من تركها
للفحص المسطح حيث لم يطابق محور دورانها → "غير موضعة")؛ عقارب AOD تدور عند
مركز اللوحة (أحيانًا تحمل x/y عقرب خارج المركز يتجاهله البرنامج الثابت — مثل
ساعة Gradient عند ). المحرر يعرض هذا أيضًا كـ
(§UI): كل شاشة تعرض طبقاتها فقط والتعديلات تُحفظ بشكل مستقل. عرض الوضع العادي
متطابق بايتًا طوال الوقت؛ إعادة التدوير تبقى بايتًا دقيقة على جميع الأقراص الـ103.40 01 00 XX (✅ مؤكد من البرنامج الثابت)كم عدد الأرقام التي يرسمها img_number هو بايت واحد في سجل الحقل — بايت البيانات XX
من سجل السمة الفرعي 40 01 00 XX للعنصر (السجل الفرعي 0x40 الذي يجلس بعد
جدول الإطارات 61 [count][base][glyph-ids]):
XX & 0x0F = عدد فتحات الأرقام (0 ⇒ افتراضي البرنامج الثابت 7).0x80 = حشو صفري (عرض أصفار بادئة، مثل "09" مقابل "9").مؤكد بتفكيك البرنامج الثابت (صورة XIP 0x10000000؛ روتين العرض 0x100d8e60):
NDIG = ldrb[40sub+3] & 0x0F (→7 إذا 0)؛ القيمة تُقيَّد value % 10^NDIG ويُرسَم بالضبط NDIG
حروفًا MS أولًا، الأصفار البادئة مكبوتة ما لم يكن البت 7. u16 بعد المصدر (60 للتاريخ،
1000 للسعرات) ليس العدد — يغذي فقط إدراج حرف فاصل الآلاف/الملايين (cmp #1000/#1000000)،
ولهذا لم يفعل تعديله شيئًا. معرّف المصدر لا يحد أيضًا.
مدرج تكراري للمجموعة عبر جميع حقول الأرقام الـ620 يطابق: حقول رقمين (ساعة/دقيقة/ثانية/تاريخ/درجة حرارة/نبض) تنتهي
40 01 00 02/0x82؛ السعرات …04؛ الخطوات …05؛ تقسيمات الساعة برقم واحد 0x81. لذا حقل التاريخ
40 01 00 82 = رقمين، بحشو صفري — هذا هو السبب الكامل وراء اقتطاع درجة حرارة فهرنهايت المعاد ربطها (≥100).
الإصلاح / المحرر: wfweb يحلل digitCount/digitZeroPad (+digitCountOff) لحقول الأرقام،
يعرض "Digits" + "Zero-pad" في المفتش، يكتب البايت في مكانه (نفس البصمة)،
والمعاينة تقيّد/تحشو إلى digitCount لتعكس البرنامج الثابت. لذا إعادة ربط مصدر حقل و
ضبط عدد أرقامه يعمل لأي حقل (مثل التاريخ→درجة حرارة °F → Digits 3). مرجع الوضع العادي
دون تغيير (0 تراجعات، 3 تحسينات صغيرة)؛ إعادة التدوير بايتًا دقيقة على جميع الأقراص الـ103.
(فرضية "عرض الأرقام"/rectW السابقة كانت خاطئة — العرض للتخطيط فقط، وليس العدد.)
struct، وذيل مرجع الموارد ✅المشهد (§11.7) هو TLV متداخل نظيف — [tag u8][len u16 LE][body]. جرد الوسوم الكامل كما
لوحظ عبر المجموعة:
✅ هذا الجرد مكتمل للمجموعة. بالتكرار فقط في وسوم الحاويات أعلاه، TLV المشهد
لجميع الأقراص الـ15 المفحوصة يمشي بالضبط إلى طول جذره المعلن مع صفر وسوم
غير معروفة — لذا محلل يتعامل مع هذا الجدول يتعامل مع التنسيق بالكامل، والوسم غير المعروف يعني
قراءة غير محاذاة، وليس نوع عقدة جديد. (احذر: متجول يتكرر في كل عقدة طولها
يحدث أن يكون ≥ 3 سينزل إلى أجسام struct/0x5b ويخترع ذيلًا طويلًا من "وسوم" لمرة واحدة —
أجسام الأوراق ليست TLV.)
💡 اختصار التأليف: يمكن تخطي التخطيط التلقائي
0x48/0x68تمامًا — كل عنصر واجهة يمكن وضعه بإحداثياتx,yمطلقة مباشرة على المستوى الأعلى للشاشة، وهذا ما يفعله المنشئ من الصفر (§11.7). مطلوب فقط لقراءة الأقراص الموجودة. عرضmetaبقيمة0x8000يعلّم struct كـ طفل تخطيط تلقائي لإطار (الموضع يأتي من الوالد، وليس منx,y).
جسم 0x01 struct — بادئة ثابتة من 18 بايت متبوعة بذيل مرجع موارد اختياري:```
+0x00 x i16 [signed — can be negative, see §11.8]
+0x02 y i16
+0x04 meta[14] ────────────────────────────────────────────────────
meta[0..1] w u16 [0x8000 = auto-layout child of a frame]
meta[2..3] h u16
meta[4..6] unknown [placeholder-looking (1,0,0)/(4,0,0); see §11.14]
meta[7] accent-tint capability flag — 4 = tintable (§11.14)
meta[9] DATA SOURCE ID (§16)
meta[10] sub / variant
meta[11..13] max u24 LE [the metric's nominal full-scale value]
+0x12 ref tail [61 …] — absent on imageless rings (0x80/0x81, §11.15)
> ✅ هذا يوحّد "الإزاحات السحرية" في الأقسام §11.5/§11.8/§11.9. تلك الأقسام تحدد الحقول *نسبيًا إلى
> البايت `0x61` لجدول الإطارات* — وهو ببساطة `+0x12` من هذا الهيكل، لذا `−18`/`−16` = `x`/`y` و
> **`−5` = `meta[9]`، معرف المصدر**. نفس البايتات، تخطيط واحد نظيف. الاستدلال الاستكشافي الأمامي لخاصية `82`
> الذي كان يخطئ بشكل منهجي بمقدار واحد ليس مطلوبًا على الإطلاق: اقرأ `meta[9]` من الهيكل.
> قيمة `max` لحقل رقمي هي أيضًا مجرد `meta[11..13]` (مثلًا، حقول يوم الشهر تحمل `max = 99`).
**ذيل المرجع** (`61`) — كيف تشير العقدة إلى خرائط البت الخاصة بها:```
+0x00 0x61 [tail type]
+0x01 count u16 [1 = single image · 10/11 = digit atlas · N = pick-list / frame sheet]
+0x03 base u32 [ABSOLUTE FILE OFFSET of the first asset block]
+0x07 count × u16 = the BLOCK SIZE (8 + payload len) of each referenced asset, in order
⚠️ كانت تلك u16 الزائدة موثقة سابقًا على أنها "count×id(u16)" / معرّفات الحروف الرسومية. وهي
أحجام كتل: base، بالإضافة إلى المجموع التراكمي لها، يجتاز مدخلات تجميع الأصول مدخلًا بمدخل (✅ تم التحقق
بدقة لجميع المدخلات العشرة لأطلس الأرقام). نتيجتان:
count−1 أحجام هي الحاملة للوزن؛ قيمة المدخل الأخير لا تُتبع أبدًا، لذا
تحمل الملفات في البرية أحيانًا قيمة قديمة هناك. لا تعامل عدم التطابق في المدخل
الأخير كمرجع مكسور.0x28 معاينة — الصورة المصغرة للمتجر/الكتالوج مضمّنة في ملف .bin نفسه (27 عقدة معاينة
عبر 15 قرصًا)، كبنية 0x08 pvStruct: بادئة من 5 بايتات بالإضافة إلى نفس ذيل المرجع، مع بدون x/y.
مفيدة لبناء واجهة معرض دون شحن صور PNG منفصلة.
0x02 ✅الأخ 0x02 لعنصر واجهة يجعلها شرطية. بدونها، يُرسم العنصر دائمًا. القواعد:```
count u8 , count × ( id u8 , op u8 , val u24 LE signed ) [5 bytes per entry]
`id` هو معرّف مصدر بيانات (§16) — بما في ذلك **معرّفات الفتحات الاصطناعية** الواردة في §11.13. العوامل، مع
عدد مرات الظهور المُقاس عبر 15 قرصًا:
| op | المعنى | الظهور |
|---|---|---|
| `0x01` | ارسم إذا كانت `value == val` | 99 |
| `0x81` | نفس `0x01` (البت `0x80` مضبوط — يظهر في المتغيرات المتنافية حصريًا) | 48 |
| `0x02` | **إخفاء** إذا كانت `value == val` | 7 |
| `0x03` | ارسم إذا كانت `value == val`، حيث `val` هو **علامة عدم وجود بيانات** (مثل HR `1000`) | 13 |
| `0x05` | ارسم إذا كانت `value >= val` | 58 |
| `0x06` | ارسم إذا كانت `value <= val` | 50 |
| `0x04` | ⚠️ **غير معروف** — 15 ظهورًا، لا دلالات مؤكدة | 15 |
قاعدة الدمج (كما نفّذها العارض المرجعي، القناع `op & 0x7f`): إدخالات المساواة
تُدمج باستخدام **OR**، ثم يجب أن تتحقق **جميع** إدخالات الإخفاء/`>=`/`<=`.
هذه الآلية الواحدة تغطي معظم التباين الزمني للتنسيق، وتفسّر البنى التي تبدو
كعناصر واجهة مكررة:
- **تخطيطات 12 ساعة / 24 ساعة والوحدات المترية / الإمبراطورية** — مجموعتان من العناصر مكدستان في نفس الموضع، كل منهما مرتبطة
بـ `id 0x73` (علامة الوحدات) مع `val` بقيمة 0 أو 1. يحتوي القرص 275 على ستة أزواج من هذا القبيل (12 عقدة).
- **عناصر نائبة لـ"لا توجد بيانات"** — العامل `0x03` مقابل قيمة حارسة، مثل `id 0x5f, val 1000` (القرص 275، مرتين):
ارسم فن الشرطة الطويلة بدلاً من درجة الحرارة عندما يكون المقياس غير متاح.
- **تمييزات مجمّعة** — نطاق مقترن `0x05`/`0x06`، مثل سلسلة Metaball حيث يضيء كل رابط
لنافذة مدتها 5 دقائق خاصة به.
- **بدائل فتحات التعقيد** — مرتبطة بمعرّفات الفتحات الاصطناعية الواردة في §11.13.
### 11.13 فتحات التعقيد القابلة للتكوين — `0x85` + `0x5f` ✅ (يحل محل §11.8)
خلص §11.8 إلى أن المقياس النشط لتعقيد قابل للتكوين هو حالة ذاكرة الجهاز ولا يمكن
استعادته من الملف. **كان ذلك خاطئًا** — كل من قائمة مقاييس الفتحة واختيارها الافتراضي
موجودان في ملف `.bin`. تحمل كل عقدة `0x85` شقيقًا `0x5f`:```
+0x00 slotIndex u8 [0-based position among sibling 0x85 nodes]
+0x01 count u8 [how many metrics this slot offers]
+0x02 activeIdx u8 [index into the list below = the DEFAULT SHOWN METRIC]
+0x03 count × u8 [the metric ids themselves (§16)] … NUL padding
البدائل التي تُرسم فعلياً هي مجموعات 0x68 عادية في مكان آخر من الشجرة، كلٌّ منها مُقيَّد بشرط
0x02 (§11.12) على المعرّف الاصطناعي 0x79 + slotIndex — لذا فإن متغيرات الفتحة 0 ترتبط بـ
0x79، وفتحة 1 بـ 0x7a، وهكذا. لعرض فتحة: اقرأ activeIdx، ثم ارسم المتغير الذي يطابق شرطه ذلك الفهرس.
مُقاس على أقراص حقيقية:
فتحتا القرص 275 بمقاييسهما الستة تمثلان 12 من عقد 0x02 الـ 26 لديه، تماماً كما توقعنا:
01 79 81 0X 00 00 و 01 7a 81 0X 00 00 لـ X = 0..5 — ستة بدائل مفتاحية على 0x79 (الفتحة 0)
وستة على 0x7a (الفتحة 1). (البايتان 0x79/0x7a اللذان أسماهما §11.8 "بايت المثيل" هما معرّفا الربط هذان.)
أما الـ 14 المتبقية فهي غير مرتبطة: 12 على 0x73 (علم 24 ساعة/وحدات المقاييس، val 0 أو 1 — ستة
أزواج عناصر تتبدل بين تخطيطي 12 ساعة و24 ساعة) و2 على 0x5f مع op 0x03 و val = 1000،
وهو العنصر النائب لغياب بيانات درجة الحرارة.
ما زال حالة جهاز حقيقية: أيّاً كان ما يختاره المستخدم لاحقاً في التطبيق المصاحب فإنه يتجاوز
activeIdx وقت التشغيل، لذا فإن المعاينة تعيد إنتاج الافتراضي للملف، وليس بالضرورة ما يعرضه قرص
معيّن. imgs[0] لعقدة 0x85 هو عنصر نائب "اضغط للتهيئة" يرسمه البرنامج الثابت فقط
في وضع التحرير الخاص به — تخطَّه عند معاينة عرض الوقت العادي.
meta[7] == 4 ✅تتيح بعض الأقراص للمستخدم اختيار لون تمييز على الجهاز نفسه، ويستبدله البرنامج الثابت في
الصور النقطية للعنصر وقت العرض. المفتاح هو بايت واحد: meta[7] من البنية (§11.11) —
أي البايت +0x0B من جسم 0x01 — يساوي 4 يعلّم مورد(ات) ذلك العنصر كقابلة للتلوين.
.bin المُسلَّم ببكسلاته الأصلية وإلا فقدت اختيار المستخدم نهائياً. طبّق
الصبغة فقط في مسار المعاينة/اللوحة.⚠️ لا "تحسّن" هذا إلى استدلال لوني — ذلك المسار طريق مسدود مثبت (موثّق من fmc بعد فعله بالطريقة الصعبة). النظرية البديهية هي أن البكسلات المُعلَّمة مدمجة بلون نائب يمكن التعرف عليه يستبدله البرنامج الثابت. لا يمكن أن ينجح: حلقة القرص 348 Tumbler القابلة للتلوين وشرائط الأرقام العادية غير القابلة للتلوين في القرصين 282 Radar Sweep / 291 Vertical تخبز نفس RGB
(255,72,32)تماماً (مُفحَص بشكل شامل، كل بكسل)؛ والقرصان 305 Dots (عقرب الساعات) و306 Large Number (الأرقام) قابلان للتلوين بينما يُخبزان أبيض خالص، لذا فإن اختبار اللون سيفوتهما تماماً. التحسينات المتتالية (1 → 4 ألوان مرجعية، بالإضافة إلى قائمة سماح لأدوار العناصر) كلها فشلت. اقرأ العلم.مُتحقق منه مقابل الجهاز الحقيقي / التطبيق المصاحب على 7 أقراص، مُختارة لاختبار الاتجاهين — 349 Theatre و376 Digits time و305 Dots و306 Large Number و304 Elaborate 2 كلها تقدم إعداد التمييز وكلها تحتوي عناصر
meta[7]==4؛ و316 Trailing (عقرب محمر، بلا إعداد) و312 Disc و295 Vortex لا تقدم أي شيء ولديها صفر عناصر مُعلَّمة.
meta[4..6] يقع بجوار العلم مباشرة ويبدو وكأنه قد يرمّز لوناً في بعض البنيات
(RGB يبدو حقيقياً مع ذيل f1=1,f2=255، مقابل العنصر النائب (1,0,0)/(4,0,0) في البنيات المُعلَّمة).
إنه لا يرتبط بقدرة التمييز. ⚠️ غير محلول؛ تجاهله.
0x80/0x5a (إجرائية) و 0x81/0x5b (مقصوصة بالصورة) ✅كلا النوعين من الحلقات يقرن بنية قصيرة (x، y، meta مع معرّف المصدر — عادةً بدون
ذيل ref إطلاقاً) بأخٍ يحدد القوس. §11.5 وثّق فقط "0x5b: max u16 @+4"، وهو
النصف المنخفض من max i32 ويفوته هندسة المسح. السجل الكامل:```
+0x00 min i32 LE [always 0 in the corpus]
+0x04 max i32 LE [100 in the corpus, except dial 332 = 60]
+0x08 start i16 LE [sweep start, units of 0.1° — SIGNED]
+0x0a end i16 LE [sweep end, units of 0.1° — SIGNED]
+0x0c width u16 LE [stroke width in px]
+0x0e radius u16 LE [0x5a ONLY — 0x81 takes its radius from the clipped image]
+0x0e / +0x10 trailer 01 00 kk ⚠️ unresolved (see below)
الاقتران صارم: `0x80` يحمل دائمًا `0x01` + **19 بايت** من `0x5a`، و`0x81` يحمل دائمًا `0x01` + **17 بايت** من `0x5b` (✅ 26/26 حلقة عبر 15 قرصًا). ⚠️ لكن **`0x81` المقتطع بالصورة يهيمن** — 25 من تلك الـ 26. المتغير الإجرائي `0x80`/`0x5a` ظهر **مرة واحدة** (القرص 273)، لذا فإن حقل `radius` الخاص به وتخطيط الـ 19 بايت يعتمدان على عينة واحدة؛ تعامل معه بحذر حتى يُرى مرة أخرى.
`frac = clamp((value − min) / (max − min), 0, 1)`، والقوس المملوء يمتد من `start` نحو `end`. الزاوية صفر عند **الساعة 3**، موجبة في اتجاه عقارب الساعة. أمثلة مُقاسة:
| القرص | الوسم | min..max | start ← end | العرض | نصف القطر |
|---|---|---|---|---|---|
| 273 Activity Mood | `0x5a` | 0..100 | **−102.8° ← 102.8°** | 42 | 222 |
| 273 Activity Mood | `0x5b` | 0..100 | 270.0° ← 90.0° | 80 | (صورة) |
| 276 Dichotomy | `0x5b` | 0..100 | 60.0° ← −120.0° | 23 | (صورة) |
| 304 Elaborate 2 | `0x5b` | 0..100 | −2.0° ← 358.0° | 24 / 80 | (صورة) |
| 366 Combo | `0x5b` | 0..100 | 0.0° ← 270.0° | 18 | (صورة) |
| 368 Function | `0x5b` | 0..100 | 0.0° ← 360.0° | 20 | (صورة) |
> ⚠️ **هذا يصحح افتراض "في اتجاه عقارب الساعة من الساعة 12"** في §11.5. ذلك هو فقط الحالة الخاصة
> `start = 0, end = 3600` (مسح كامل، حيث يكون الاصطلاح غير قابل للملاحظة). الأقراص الحقيقية تستخدم
> **مقاييس جزئية** (مروحة ±102.8° للقرص 273، وحلقة ثلاثة أرباع 270° للقرص 366) و**مسحات سالبة**
> (60° ← −120° للقرص 276)، لذا فإن أي عارض يمسح دائمًا دائرة كاملة من الأعلى يرسم تلك بشكل خاطئ.
> 🟡 الاصطلاح الدقيق لزاوية الصفر وقاعدة الاتجاه يأتيان من عارض fmc، تم التحقق منهما بالتقاطع
> مع هذه القيم على القرص — لم يتم التحقق منهما بكسلًا-بكسلًا على الجهاز بواسطة هذا المستودع.
> لاحظ أن `frac = value/max` ونتيجة قص القطاع `0x81` في §11.5 **تم التحقق منهما** (القرص 322).
⚠️ **غير محلول: الذيل ثلاثي البايت `01 00 kk`.** يأخذ `kk` قيمًا تبدو معقولة (104، 152، 216،
232، 248) والفرضية الواضحة هي نصف قطر — **تم اختبارها ودحضها**: القرص 366 يستخدم `kk = 104`
لكل من حلقة 82×82 وحلقة 166×166، والقرص 273 يستخدم `kk = 232` لكل من حلقة 440×440 وحلقة 284×284.
ليس نصف قطر، وليس قطرًا. ربما شفافية/نمط. أعد إرساله كما هو حرفيًا.
🟡 **مأزق مُبلَّغ عنه، غير مُتحقق منه هنا:** في عارض fmc، رقم `0x60` الذي يساوي معرف مصدره معرف
**أي** حلقة على نفس الشاشة يعرض `round(frac × 100)` بدلاً من القيمة الخام — لذا
رقم معدل ضربات القلب بجوار حلقة معدل ضربات القلب يعرض `36` بدلاً من `71 bpm`. الحل البديل الموثق
لديهم هو وضع الحلقة والرقم على **معرفين مستعارين** لنفس المقياس (الخطوات
`0x19`/`0x26`/`0x49`، السعرات `0x1c`/`0x1e`/`0x48`). ما إذا كان هذا سلوكًا للبرنامج الثابت أم خاصًا
بعارضهم **غير مثبت** — يستحق فحصًا مباشرًا قبل التصميم حوله.
---
## 12. تفاصيل النقل الجماعي وOTA
جدول النقل موجود في §6. نقاط إضافية مؤكدة:
- **AGPS/EPO** ✅: أول كتلة مكتوبة تبدأ بالترويسة ASCII `000000010000…`. الحلقة الكاملة
init ← `[A05F ↔ 905F]×N` ← finish لوحظت على السلك (~892 كتلة).
- **تحديث البرنامج الثابت OTA** (`9040`–`9042`، finish `9041`) 🔎: البنية مُخططة؛ حمولة INIT2 = بايتات الإصدار
(مثل `0b 00 00 39` = 11.0.0.57). **لم يُختبر ميدانيًا** (التطبيق يعطل تحديث البرنامج الثابت هنا). صور
البرنامج الثابت تبدو **غير موقعة — التكامل هو CRC32 فقط** (لا توجد توقيعات غير متماثلة لوحظت في الهندسة العكسية).
- ⚠️ لأن OTA و`FACTORY_RESET (009A 0001)` يتشاركان الجلسة المصادق عليها، فإن مصادقة BLE صالحة واحدة
كافية لمسح أو (من حيث المبدأ) تعطيل الساعة. تعامل بحذر.
---
## 13. المستشعرات
✅ الأجهزة المكشوفة عبر BLE:
- **PPG بصري** — معدل ضربات القلب (يدوي/تلقائي/تمرين/راحة)، SpO₂، والإجهاد المشتق من HRV.
- **مقياس تسارع ثلاثي المحاور** — الخطوات، المسافة، السعرات، مراحل النوم، رفع المعصم، الإيقاع.
- **GNSS/GPS** (بمساعدة AGPS) — مسار التمرين (`WORKOUT_GPS`) ودفع الموقع (`GPS_PUSH`).
لا يوجد **بارومتر/مقياس ارتفاع، بوصلة، جيروسكوب، أو مستشعر حرارة الجلد/الجسم**. يوجد
ثيرمستور NTC داخلي (حرارة اللوحة/البطارية) لكنه قابل للقراءة **فقط** عبر قناة AT
(`AT GETNTCTEMP`، §14) — تدفق سجل حرارة الجلد `0155` فارغ على هذا الطراز.
**اختطاف عناصر البيانات** (لا توجد واجهة برمجة تطبيقات حقيقية للتعقيدات/ربط البيانات — انظر §11.5): يمكن
إعادة استخدام حقول النص الموجودة في الساعة لعرض بيانات خارجية سريعة. مُثبت ✅: سلسلة **المدينة**
للطقس (`WEATHER_SET_1`، مثل `"BRA 2x1 ARG"` ظهرت على العنصر) وحقول **المسار/الفنان**
للموسيقى؛ قائمة **جهات الاتصال** (20 × name[32]+number[25]) تعمل كلوحة بيانات قابلة للتمرير.
كلها دفعات، وليست تعقيدات دائمة.
---
## 14. قناة AT للمصنع / الصدفة (`77d4ff01` / `77d4ff02`)
قناة أوامر AT نصية عادية منفصلة، مستقلة عن البروتوكول المؤطر. ✅ مُختبرة مباشرة:
- **قراءة:** `AT GETSECRET` (سر الاقتران 16 بايت)، `GETVERSION`، `GETSN`، `GETNAME`، `GETPID`،
`GETBATLV` (mV خام، مثل `3853mv`)، `GETGSENSOR` (تسارع خام بوحدة g، `X=… Y=… Z=…`)،
`GETNTCTEMP` (°C، NTC داخلي).
- **كتابة / تشغيل:** `AT SETMOTOR=1` (اهتزاز المحرك)، `SETHR/SETHRV/SETSPO2=…` (حقن اختبار
المستشعر)، `SETLCDSWITCH/SETGPSSWITCH/SETKEYSWITCH`.
الردود تنتهي بـ `,OK`. أوامر `SET*` تنفذ عمومًا لكنها قد لا تعيد صدى `,OK` عبر BLE — تأكد
حالة بحالة.
---
## 15. الميزات المقيدة بالبرنامج الثابت / غير المتاحة (🔎 هندسة عكسية للبرنامج الثابت)
بعض الميزات موجودة في البرنامج الثابت لكنها معطلة حسب الطراز/المنطقة و**لا يمكن الوصول إليها من
الهاتف/BLE** — تحتاج تعديلًا على البرنامج الثابت، وهو خارج النطاق هنا:
- **صوت ChatGPT** — البوابة = معرف ميزة `ux2sys` `0x9e`، مُغذى من NVRAM/EFUSE/المنطقة عند الإقلاع؛ على
هذا الطراز علم الدعم `908b = 00`. غير قابل للتأثير من الهاتف أو الحساب أو BLE (مؤكد بالتجربة
+ الهندسة العكسية). التطبيق مجرد مرحّل؛ الصوت يذهب من الهاتف ← سحابة Nothing.
- **ضغط الدم** — نظام فرعي كامل موجود في البرنامج الثابت، مُعطل حسب الطراز/المنطقة.
- **Alipay / دفع NFC** — واجهة مستخدم كاملة موجودة، لطراز الصين فقط.
- **غائب في الأجهزة/البرنامج الثابت:** ECG، SOS/طوارئ، NFC عام.
---
## 16. معرفات مصدر البيانات / التعقيدات
ليست ضرورية لبناء عميل BLE، لكنها **أساسية لتأليف أو عرض قرص**: هذه هي قيمة
`meta[9]` (§11.11) التي تربط عنصرًا بالبيانات الحية، و`id` شرط الرؤية (§11.12)، و
إدخالات قائمة مقاييس الفتحة (§11.13). البرنامج الثابت يحلها عبر **جدول توزيع getter بـ 142 إدخالًا**
عند `0x101f371c`، كل إدخال يستدعي `ux2sys_get(type)` (🔎 هندسة عكسية للبرنامج الثابت).
### الوقت / التاريخ — ✅ راسخ جيدًا
| id | المعنى | id | المعنى |
|---|---|---|---|
| `0x01` | الساعة (12/24 ساعة حسب إعداد الجهاز) | `0x0f`, `0x12` | الثانية (ناعمة) |
| `0x04` | الساعة (24 ساعة) | `0x10`, `0x11` | عشرات / آحاد الثانية |
| `0x07` | الساعة (24 ساعة إجباري) | `0x71`, `0x72` | الثانية (نابضة / زاوية العقرب) |
| `0x02`, `0x03` | عشرات / آحاد الساعة-12 | `0x13` | علم AM/PM (0 = صباحًا، 1 = مساءً) |
| `0x05`, `0x06`, `0x08`, `0x09` | عشرات / آحاد الساعة | `0x15`, `0x16` | الشهر |
| `0x0a`, `0x70` | زاوية عقرب الساعة | `0x17` | يوم الشهر |
| `0x0b` | الدقيقة | `0x18` | يوم الأسبوع (0 = الاثنين ⚠️) |
| `0x0c`, `0x0d` | عشرات / آحاد الدقيقة | `0x0e`, `0x71` | زاوية عقرب الدقيقة |
معرفات العشرات/الآحاد ترسم **رقمًا واحدًا** — عنصر مرتبط بأحدها يعرض حرفًا واحدًا، وليس القيمة
الكاملة (القرص 284 Square). معرفات زاوية العقرب (§11.5) هي `0x0a`/`0x70` للساعة = `h·30° + m·0.5°`،
`0x0e`/`0x71` للدقيقة = `m·6° + s·0.1°`، `0x12`/`0x72` للثانية = `s·6°` (✅ مؤكد بفك تجميع
الـ getters، احتياط RTC 10:10:30).
### الصحة / المستشعرات / الطقس — ⚠️ متنازع عليه، اقرأ عمود الأدلة
| id | المعنى (أفضل قراءة حاليًا) | الدليل |
|---|---|---|
| `0x19` | **الخطوات** | في قائمة فتحات 368 بجانب `0x1a`؛ يطابق `parse.ts` لهذا المستودع |
| `0x1a` | **معدل ضربات القلب** | في قائمة فتحات 368 بجانب `0x5f` و`0x19` |
| `0x1c` | السعرات | قائمة الفتحات، أيقونة اللهب في التطبيق المصاحب |
| `0x1e` | السعرات (اسم مستعار) | المجموعة |
| `0x22` / `0x23` | المسافة كم / ميل (الجزء الصحيح) | المجموعة |
| `0x74` / `0x75` | المسافة كم / ميل (الجزء الكسري) | المجموعة |
| `0x76` | المسافة (نموذج الفتحة) | قائمة الفتحات، أيقونة الطريق |
| `0x24` | **نسبة البطارية %** | قائمة الفتحات، أيقونة البرق |
| `0x30` | نسبة البطارية % | المجموعة |
| `0x36` / `0x5f` | درجة الحرارة | `0x5f` = قائمة الفتحات، أيقونة سحابة-شمس؛ 361 TempoG يربط رقمًا عاديًا |
| `0x48` | الوقوف (ساعات الوقوف) | قائمة الفتحات، أيقونة شخص واقف |
| `0x8b` | AQI | قائمة الفتحات |
| `0x73` | علم 24 ساعة / وحدات مترية | المجموعة |
| `0x25`–`0x27`, `0x49`, `0x6c`, `0x6f` | نسبة الهدف % / أسماء مستعارة للفتحات للخطوات والسعرات | المجموعة |
| `0x6a` | ⚠️ مقياس فتحة غير محدد | يظهر في 4 قوائم فتحات |
| `0x79 + slotIndex` | **اصطناعي** — ليس مقياسًا؛ معرف اختيار الفتحة (§11.13) | ✅ §11.13 |
> ⚠️ **ثلاثة جداول في هذا المستودع اختلفت؛ هذا الجدول هو التوفيق.** مراجعات سابقة من §16
> و§11.5 قرأت `0x19` كمعدل ضربات القلب، و`0x1b` كبطارية، و`0x24` كدرجة حرارة و`0x36` كخطوات —
> و`wfweb/src/codec/mock.ts` لا يزال يرمز تلك القراءة، بينما `wfweb/src/codec/parse.ts` يرمز
> قراءة مختلفة (`0x19` خطوات، `0x24` نسبة هدف %، `0x48` وقوف، `0x1a` طقس). الجدول أعلاه يتبع
> القراءة الأفضل دليلًا (التسميات معايرة بواسطة fmc ضد أيقونات قائمة فتحات عناصر التطبيق المصاحب،
> انظر §Sources). **الكود لم يتغير بعد — `mock.ts` و`parse.ts` لا يزالان غير متسقين
> مع بعضهما البعض ومع هذا الجدول.** تعامل مع تسميات معرفات الصحة كـ ⚠️ حتى يربط شخص ما
> حقلًا بكل معرف ويقرأ الساعة.
>
> أقوى دليل فردي هو **قائمة فتحات القرص 368 Function** (§11.13)، التي تقدم
> `0x5f 0x1c 0x19 0x48 0x24 0x76 0x1a 0x8b` كـ **ثمانية مقاييس مميزة قابلة للاختيار من قبل المستخدم** في قائمة واحدة.
> مهما كانت التسميات، لا يمكن أن يكون اثنان من تلك الثمانية نفس المقياس — مما يستبعد
> `0x19` = `0x1a` = معدل ضربات القلب و`0x24` = `0x5f` = درجة الحرارة في نفس الوقت.
التعقيدات الحلقية/القوسية مع **ورقة إطارات** تفهرس إطارًا مُصيَّرًا مسبقًا (مثل 50 % = إطار 50 من 100)
مدمجًا في `.bin` الذي ترسله (§11.5)، لذا لا حاجة لحزمة RES خارجية؛ الحلقات بدون صور تُرسم
من مواصفة القوس بدلاً من ذلك (§11.15).
---
### البطاقات السريعة (بلاطات الرئيسية) — `QUICK_CARD (906D)` ✅
بلاطات الرئيسية للساعة. الهاتف يختار فقط **أي** البلاطات تظهر و**بأي ترتيب** — البلاطات
تُعرض بواسطة البرنامج الثابت (لا توجد قناة محتوى). أول بايت من الحمولة = أمر فرعي: `00` = GET، `01` =
SET؛ **كلاهما يستخدم `0x906D`** (`0x906C` مذكور لكنه غير مستخدم — الاستعلام عنه ينتهي بوقت انتهاء). الرد = `A06D`.
> 🛑 إرسال `assemblyId` مختلق **يمسح شاشات الساعة** (يقبل القائمة، لا يمكنه مطابقة
> المعرفات، لا يعرض شيئًا). أرسل فقط المعرفات التي **قرأتها** عبر GET؛ استعد عبر التطبيق الرسمي أو
> إعادة ضبط المصنع.
**رد GET** ✅: `status(1) ‖ 00 ‖ N(1) ‖ N × group`، group = `tag=01 ‖ K(1) ‖ K×(assemblyId, sportId)`.
إطار حقيقي: `01 00 04 01 02 5d00 6100 01 03 1900 2e00 2300 01 03 5c00 0400 5a02 01 03 4800 5100 5300`
= 4 شاشات / 11 بطاقة (`5a02` = بطاقة الرياضة، sportId 2).
**الفتحات:** كل شاشة بها **4 فتحات**. نوع البطاقة يحدد حجمها — `circular`/`square` = فتحة واحدة،
`rectangle` = فتحتان. التحقق هو حساب فتحات بسيط (Σ ≤ 4 لكل شاشة)؛ لا توجد بطاقات متنافية.
`sportId` هو `0` باستثناء بطاقات الرياضة (87–91). المعرفات `64` و`95` غير موجودة.
**كتالوج `assemblyId`** (كل نوع منطقي = نطاق متصل من 6 متغيرات نمط `_0`..`_5`؛
`0` = فتحة فارغة):
| عشري | البطاقة | عشري | البطاقة |
|----|----|----|----|
| 0 | فتحة فارغة | 49–53,97–98 | الطقس |
| 1–6 | الخطوات | 54–58 | المؤقت |
| 7–12 | السعرات | 59–62 | التنفس |
| 13–18 | الوقوف | 63,65–67 | ساعة الإيقاف |
| 19–24 | نشاط معتدل | 68–71 | البطارية |
| 25–30 | معدل ضربات القلب | 72–76 | الأخيرة |
| 31–36 | SpO₂ | 77–81 | جهات الاتصال |
| 37–42 | الإجهاد | 82–86 | القرص / الهاتف |
| 43–48 | النوم | 87–91 | الرياضة (`sportId` ≠ 0) |
| 92 | الموسيقى | 93/94/96 | سجل النشاط / PAI / الدورة |
---
## الملحق أ. كتالوج الأقراص المخزنة (id ← الاسم)
§11 يشير إلى الأقراص بالمعرف الرقمي في كل مكان (275 SlopeTime، 322 Glare 2، 357 Silhouette، …). المعرفات
`273`–`376` هي أوجه المتجر/المخزون؛ المعرف هو ما يبلغ عنه `DIAL_COMMAND (9055/a055)` كنشط و
ما يأخذه `9075` كـ `old_id` (§11.4). 100 من أصل 103 معرفات معروفة مُسمّاة أدناه؛ الباقية هي
أكواد ROM (§11.4). الأسماء والتجميع كما يعرضها التطبيق المصاحب الرسمي.
- **الافتراضي** (6) — `273` Activity Mood · `274` Sun Circle · `275` SlopeTime · `276` Dichotomy · `277` Prismatic Time · `280` Multifunction
- **التناظري** (34) — `286` Sundial · `287` Simple Dial · `292` City · `294` Sudoku · `305` Dots · `306` Large Number · `309` Gradient · `310` Glare · `311` Bold · `313` Classical · `314` Fragment · `315` Infinite · `316` Trailing · `322` Glare 2 · `326` Chrono Master · `327` Digit Max · `328` Coherent · `329` Wheel · `330` Zenith · `331` Intersection · `335` Time Phase · `336` Energetic · `338` Chronos · `341` Dual View · `346` Time Windmill · `347` Large Panel · `349` Theatre · `352` Elegant Sweep · `360` Explorer · `364` SportPulse · `370` Hemisphere · `371` ActiveTrio · `372` Time Wheel · `373` Traditional Pointer
- **الرقمي** (41) — `281` Metaball · `282` Radar Sweep · `283` Radio · `285` Widgets · `288` Type · `289` Rotate · `290` Gradual · `291` Vertical · `293` Stairs · `296` Ladder · `297` Ray · `298` Eclectic · `299` Echo · `300` Mono Dial · `301` Orbit · `302` Calendar · `303` Space · `307` Sprung · `308` Sundial 2 · `319` One Line · `320` Orienteer · `321` Revolution · `323` Dash · `324` Finesse · `325` Metric · `333` Circularity · `334` Globe of Time · `337` Time Finder · `339` Suprematism · `340` Sport Mode · `345` Time Dot · `350` Timeline · `351` Cyclopes · `353` Dual phase · `357` Silhouette · `359` Ring data · `361` TempoG · `362` Steady · `365` Elegance · `369` Solar System · `376` Digits time
- **متعدد الوظائف** (10) — `304` Elaborate 2 · `344` InfoMeter · `348` Tumbler · `354` Dual · `363` Vintage · `366` Combo · `367` Complex Figure · `368` Function · `374` Cirquary · `375` InfoHub
- **الإبداعي** (8) — `284` Square · `312` Disc · `317` Disc 2 · `318` Dominos · `332` Flux · `342` Perfect Match · `343` Progress Day · `358` Asteroid
- **Diwali** (1) — `295` Vortex
---
## المصادر
تم إعادة بناء تخطيطات البايت أعلاه من البرنامج الثابت (1.0.0.73)، وAPK الرسمي (3.5.7)، و
التقاطات حية مفككة التشفير ضد جهاز حقيقي. التنفيذ المرجعي لهذا المشروع موجود في
`core-rust/src/{commands,frame,crypto,health,session}.rs` (Rust)، ومحرر TypeScript `wfweb/`، و
أدوات Python `cmftool/` (`pair.py`، `session.py`، `wf_codec.py`، `upload_custom.py`، …).
**عمل مستقل مدمج هنا.** [freethinkel/fmc](https://github.com/freethinkel/fmc) — محرر
أوجه ساعات SvelteKit + سوق لنفس الساعة — قام بشكل مستقل بالهندسة العكسية
لتنسيق `.bin` من مجموعة ~100 وجه ووصل إلى عدة نتائج كانت هذه الوثيقة تفتقدها أو كانت
خاطئة فيها. مستنداتهم `docs/cmf-protocol.md`، و`src/lib/modules/editor/lib/{wf,render}.ts` و
`src/lib/modules/device/lib/ble.ts` تستحق القراءة مباشرة. النتائج المعتمدة، كل منها أُعيد التحقق
منه ضد بايتات الأقراص قبل كتابته هنا:
| النتيجة | المكان | الحالة هنا |
|---|---|---|
| كلمتا الترويسة كلتاهما CRC32 (متغير غير قياسي) | §11.4 | ✅ أُعيد التحقق 9/9 أقراص؛ **تصحيح** "لا يوجد مجموع اختباري حاجب" |
| شروط الرؤية، الوسم `0x02` | §11.12 | ✅ أُعيد التحقق؛ كان غير موثق |
| قائمة مقاييس الفتحة + `activeIdx` الافتراضي، `0x79 + slotIndex` | §11.13 | ✅ أُعيد التحقق على 275/368/273/304؛ **يحل محل** §11.8 |
| علم قدرة الصبغة المميزة `meta[7] == 4` | §11.14 | ✅ أُعيد التحقق من الانتشار؛ كان غير موثق |
| مواصفة القوس الكاملة (زوايا المسح، العرض، نصف القطر) | §11.15 | ✅ أُعيد التحقق؛ **تصحيح** "`max` u16 `@+4`" |
| `u16` ذيل المرجع هي أحجام كتل، وليست معرفات حروف | §11.11 | ✅ أُعيد التحقق بدقة على أطلس 10 أحرف |
| تذييل 36 بايت؛ متغير البايت السحري `0x02`؛ `0x86` = 64 ب؛ معاينة مدمجة `0x28` | §11.4، §11.11 | ✅ أُعيد التحقق |
| تسميات معرفات الصحة/الطقس معايرة ضد قائمة فتحات التطبيق المصاحب | §16 | ⚠️ معتمد كأفضل قراءة؛ التعارضات مُعلَّمة |
| UUID خدمة الصدفة هو `77d4e67c-…`، ونطاق `optionalServices` في Web Bluetooth | §1 | ⚠️ تقرير وحدة واحدة، لم يُعد التحقق هنا |
| مشاركة معرف الرقم مع الحلقة تعرض نسبة مئوية | §11.15 | 🟡 مُبلَّغ عنه، **غير** مُتحقق منه هنا |
| كتالوج معرف القرص المخزن ← الاسم | الملحق أ | ✅ معتمد كما هو |
| الغرض | الخدمة | الخاصية | الخصائص |
|---|
| كتابة الأوامر | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | كتابة |
| إشعار الأوامر | 0000fff0-… | 0000fff1-… | إشعار |
| كتابة الصدفة (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | كتابة |
| إشعار الصدفة (AT) | — | 77d4ff02-… | إشعار |
| كتابة البيانات الجماعية | — | 02f00000-0000-0000-0000-00000000ffe1 | كتابة |
| إشعار البيانات الجماعية | — | 02f00000-…ffe2 | إشعار |
| الاسم | cmd1,cmd2 |
|---|
| TIME | FFFF 8004 |
| FIRMWARE_VERSION_GET / _RET | FFFF 8006 / FFFF 0006 |
| SERIAL_NUMBER_GET / _RET | 00DE 0002 / 00DE 0001 |
| BATTERY | 005C 0001 |
| TRIGGER_SYNC | 005C 0002 |
| USER_INFO_SET / _RET 🔎✅ | 0095 0001 / 0095 0003 |
| FACTORY_RESET | 009A 0001 |
| DEVICE_REBOOT 🔎 | FFFF 9080 |
| RESOLUTION_GET 🔎 (→ 466×360) | FFFF 907F |
| GPS_PUSH / _RET | FFFF 906A / FFFF A06A |
| UNBIND_SET / _RET | FFFF 907A / FFFF A07A |
| الاسم | cmd1,cmd2 |
|---|
| AUTH_PHONE_NAME | FFFF 8049 |
| AUTH_WATCH_MAC | FFFF 0049 |
| AUTH_PAIR_REQUEST / _REPLY | FFFF 8047 / FFFF 0048 |
| AUTH_NONCE_REQUEST / _REPLY | FFFF 804B / FFFF 004C |
| AUTHENTICATED_CONFIRM_REQUEST / _REPLY | FFFF 804D / FFFF 0004 |
| AUTH_FAILED | FFFF A061 |
| الاسم | cmd1,cmd2 |
|---|
| APP_NOTIFICATION | 0065 0001 |
| INCOMING_CALL ⚠️ | 0064 0001 |
| CALL_REMINDER_REQUEST / _RESPONSE | FFFF 9066 / FFFF A066 |
| FIND_PHONE | 005B 0001 |
| FIND_WATCH | 005D 0001 |
| FIND_WATCH_TOGGLE | FFFF 9069 |
| SMS_MESSAGE_PUSH / _RET | FFFF 906E / FFFF A06E |
| QUICK_REPLY_SET / _RET | FFFF 9073 / FFFF A073 |
| الاسم | cmd1,cmd2 |
|---|
| ALARMS_SET / _GET | 0063 0001 / 0063 0002 |
| CONTACTS_SET / _GET | 00D5 0001 / 00D5 0002 |
| STANDING_REMINDER_SET / _GET | 0060 0001 / 0060 0002 |
| WATER_REMINDER_SET / _GET | 0061 0001 / 0061 0002 |
| TASK_REMINDER_SET / _RET ⚠️ | FFFF 9072 / FFFF A072 |
| الاسم | cmd1,cmd2 |
|---|
| GOALS_SET / _ACK | 005E 0001 / 005E 0003 |
| UNIT_LENGTH / _ACK | FFFF 9067 / FFFF A067 |
| UNIT_TEMPERATURE / _ACK | FFFF 9068 / FFFF A068 |
| TIME_FORMAT / _ACK | 005F 0001 / 005F 0003 |
| WAKE_ON_WRIST_RAISE / _GET / _ACK | 0062 0001 / 0062 0002 / 0062 0003 |
| LANGUAGE_SET / _RET | FFFF 9058 / FFFF A06B |
| HEART_MONITORING_ENABLED_SET / _GET | 009B 0001 / 009B 0002 |
| HEART_MONITORING_ALERTS | FFFF 9059 |
| DO_NOT_DISTURB / _GET | 0099 0001 / 0099 0002 |
| SPORTS_SET / _GET | 00DC 0001 / 00DC 0002 |
| SPORT_LINKAGE_SET / _RET | FFFF 9076 / FFFF A076 |
| SPORT_DATA_SYNC 🔎 (معدل ضربات القلب/سعرات/خطوات مباشر) | FFFF 9078 / FFFF A078 |
| FEMALE_CYCLE_SET / _RET | FFFF 9071 / FFFF A071 |
| SLEEP_CONFIG_SET / _RET (الحد الأدنى المستهدف) | FFFF 9074 / FFFF A074 |
| WORLD_CLOCK_GET | FFFF 906F |
| WORLD_CLOCK_DST_SET / _RET | FFFF 9083 / FFFF A083 |
| VITALITY_GET / _RET | FFFF 9079 / FFFF A079 |
| VITALITY_SW_SET / _RET | FFFF 9070 / FFFF A070 |
| الاسم | cmd1,cmd2 |
|---|
| DIAL_COMMAND_SET / _RET (قائمة/إعادة ترتيب/تحديد) | FFFF 9055 / FFFF A055 |
| DIAL_CONFIG_SET / _RET | FFFF 9075 / FFFF A075 |
| CHANGE_DIAL (⚠️ خامل على 1.0.0.73 — لا تستخدمه) | 009F 0001 |
| QUICK_CARD_SET/GET / _RET (كلاهما على 906D) | FFFF 906D / FFFF A06D |
| الاسم | cmd1,cmd2 |
|---|
| ACTIVITY_FETCH_1 / _2 | FFFF 8005 / FFFF 9057 |
| ACTIVITY_FETCH_ACK_1 / _2 | FFFF 0005 / FFFF A057 |
| ACTIVITY_DATA | 0056 0001 |
| SLEEP_DATA / _GET | 0058 0001 / 0058 0002 |
| SPO2 | 0055 0001 |
| STRESS | 009D 0001 |
| HEART_RATE_MANUAL_AUTO | 0053 0001 |
| HEART_RATE_RESTING | 00DA 0001 |
| HEART_RATE_WORKOUT | 00E0 0001 |
| SKIN_TEMP_HISTORY 🔎 (فارغ على هذا SKU) | 0155 0001 / 0155 0002 |
| WORKOUT_SUMMARY / _V3 | 0057 0001 / 0160 0001 |
| WORKOUT_GPS | FFFF A05A |
| المجال | INIT1 طلب/رد | INIT2 طلب/رد | CHUNK طلب/كتابة | FINISH ack1/ack2 |
|---|
| واجهة الساعة (صورة) | 8052/0052 | 9063/A063 | A064/9064 | A065/9065 |
| واجهة الساعة (منظمة/تبديل) | 8052/0052 | 9075/A075 | A064/9064 | A065/9065 |
| البرنامج الثابت | 9052/A052 | 9040/A040 | A042/9042 | A041/9041 |
| AGPS/EPO | 905E/A05E | — | A05F/905F | A060/9060 |
| الإزاحة | الحجم | الحقل |
|---|
| 0 | 4 | الطابع الزمني (ثواني epoch) |
| 4 | 4 | الخطوات |
| 8 | 4 | المسافة (م) |
| 12 | 4 | السعرات الحرارية |
| 16 | 16 | محجوز (لوحظ 0) |
| الإزاحة | الحجم | الحقل |
|---|
| 0 | 4 | بداية الجلسة (epoch، UTC) |
| 4 | 4 | الاستيقاظ (epoch، UTC) |
| 8 | 2 | إجمالي_العميق_ث |
| 10 | 2 | إجمالي_الأساسي_ث |
| 12 | 2 | إجمالي_REM_ث |
| 14 | 2 | إجمالي_الاستيقاظ_ث |
| 16 | 2 | ⚠️ [غير مؤكد] (معرف الجلسة/الدرجة؟ القيم المرصودة لا تطابق مجموع السجلات) |
ac 49 1f 0100D5 0001) ✅: N × 57 بايت = name(32) ‖ phone(25). تعرض واجهة الساعة حتى 20.0063 0001) ✅ — تصحيح لـ Gadgetbridge (الذي وضع التسمية في النهاية،
مبطنة بـ 0xff — خطأ). 40 بايت لكل منبه، big-endian:
secondsOfDay(i32) ‖ index(u8) ‖ enabled(u8) ‖ repetition-bitmask(u8) ‖ flag(u8) ‖ label[32] UTF-8.
التسمية عند الإزاحة 8 وتظهر على الساعة. repetition = قناع بتات أيام الأسبوع (0 = لمرة واحدة)؛
flag ⚠️ [غير مؤكد] (علامة لمرة واحدة؟). مثال (13:30، idx 2): 0000bdd8 02 01 15 00 "Alarm…".005E 0001) ✅ — يستخدم التطبيق الرسمي والتنفيذ المرجعي
10 بايت، big-endian DailyTargetBean v1: steps(u32 BE) ‖ distance_m(u32 BE) ‖ calories_kcal(u16 BE). (هذه هي صيغة Gadgetbridge؛ التقارير السابقة بأن الساعة "تجاهلتها"
كانت خطأ فك تشفير جلسة قديمة، وليست مشكلة حمولة.) 🔎 هندسة البرنامج الثابت العكسية تُظهر أيضًا
متغيرًا ممتدًا 29 بايت أطول (يضيف sleep_min/exercise_min/stand_h + 6 أعلام تمكين، كلها u32
BE بعد بادئة flag(u16 LE)، مع نطاقات مفروضة: خطوات 2000–30000، مسافة 1000–99000، سعرات
100–5000، نوم 360–720، تمرين 30–90، وقوف 6–16) — ليس المسار الافتراضي للتطبيق؛ فضّل
صيغة 10 بايت إلا إذا كنت بحاجة إلى الأهداف الإضافية.0060/0061 0001) ✅: 11 بايت:
enabled(1) ‖ threshold_min(u16 LE) ‖ dndStart(u32 LE) ‖ dndEnd(u32 LE). لاحظ أن "النافذة النشطة
08:00–22:00" المعروضة في الواجهة هي إعداد افتراضي ثابت في البرنامج الثابت وليست محمولة في الحمولة.00DC 0001) ✅: count(1) = 36 خانة ‖ activityTypeCode[36] (أكواد نشطة ثم حشو 00).
يحدد الرياضات التي تظهر في قائمة التمارين بالساعة.009B 0001) ✅: بايت kind — 01 = HR 24/7، 02 = SpO₂،
04 = الإجهاد (يُقاس كل 30 دقيقة).FFFF 9059) ✅: معطل = 00؛ مفعل =
01 ‖ hrLow ‖ hrHigh ‖ sportHrHigh ‖ spo2Low ‖ 00 00 00 00 (حد 0/255 = "بدون حد").FFFF 9071) ✅: 01 ‖ predictionOpen ‖ notifySwitch ‖ cycleStartSwitch ‖ cycleStartNotifyBefore ‖ ovulationStartSwitch ‖ ovulationStartNotifyBefore ‖ fertileStartSwitch ‖ fertileStartNotifyBefore ‖ period(1) ‖ cyclePeriod(1) ‖ cycleStartDate(u32) ‖ markStart(u32) ‖ markEnd(u32) (مُلتقط: period=5، cyclePeriod=0x1c=28).FFFF 9073) ✅: TLV — count(1) ‖ total(1) ‖ [id(1) ‖ len(u16 LE) ‖ msg-UTF8]…
(7 ردود افتراضية مُلتقطة ومفكوكة التشفير).FFFF 906F) ✅: يرسل معرفات مدن رقمية، وليس أسماء (01 ‖ count ‖ cityId(2 BE)…)؛
تعيّن الساعة المعرفات من جدول داخلي. إعداد DST FFFF 9083 =
count ‖ [id(u16 LE) ‖ dst(u16 LE) ‖ start(u32 LE) ‖ end(u32 LE)]….FFFF 905C، 131 ب) ✅: state(1: 0=لا شيء/1=إيقاف مؤقت/2=تشغيل) ‖ volume(1) ‖ volumeMax(1) ‖ track(64) ‖ artist(64). ترسل الساعة أيضًا MUSIC_BUTTON (A05D) مرة أخرى.FFFF 906B، 199 ب) ✅ — استخدم هذا: 7×9 بايت أيام + 24×2 بايت ساعات +
city(32) + 7×8 بايت شروق/غروب (LE). درجات الحرارة مشفرة كـ (temp_c + 100) & 0xFF.
⚠️ نفس الحمولة المرسلة على WEATHER_SET_2 (0066 0001) لا تحدّث أداة الطقس على
Pro 2 — استخدم دائمًا 906B. (سلسلة المدينة أيضًا ناقل اختطاف بيانات مثبت — انظر §13.)005D 0001) ✅: الحمولة 0x01 → ترن الساعة/تهتز (+ ACK 005D 0003).FFFF 906A) ✅ — big-endian، خط الطول أولاً: 16 بايت
ts(u32 BE) ‖ lon×1e7(i32 BE) ‖ lat×1e7(i32 BE) ‖ 00 00. تم التحقق من موقع حقيقي.FFFF A05A) ✅ — little-endian، خط الطول أولاً: 12 بايت
ts(i32) ‖ lon×1e7(i32) ‖ lat×1e7(i32).FFFF 8004): انظر §7.| cf | bpp | البيانات النقطية (بعد LZ4) | الاستخدام |
|---|
| 4 | 2 | RGB565-LE | خلفية معتمة (FULL/THUMB) |
| 5 | 3 | RGB565-LE (2 بايت) + ألفا (1 بايت) لكل بكسل | صور نقطية مضادة للتعرج (الحروف، العقارب، الأيقونات) |
| 13 (0x0d) | 0.5 | قناع ألفا 4-بت؛ البرنامج الثابت يلوّنه وقت التشغيل | أطلس أرقام الأطلس |
| 24 (0x18) | 4 | RGBA8888 | طبقات كاملة الألوان (بما في ذلك aodImage الدائم التشغيل) |
| 1 | — | JPEG/JFIF (ff d8 ff)، استخرجه بأي مفكك ترميز | إطارات رسوم متحركة نادرة |
0a0x61NotEnvelope0aChildOverflow61 0a 00−18/−160a.bin0x68 مكدسة عند نفس (x,y)،
كل واحدة تُرسم فقط عندما يطابق شرط رؤية — هذا الجزء كان صحيحًا.
لكن قائمة المقاييس لكل فتحة (قائمة 0x1c/0x6a/0x48/0x24/0x19/0x76 في 275) هي بالضبط
قائمة المقاييس، وليست "معرّفات خيار/نمط"؛ مؤشر النشط الافتراضي هو بايت في الملف؛ و
البايت 0x79/0x7a ليس "بايت مثيل" بل المعرّف الذي تُربط عليه البدائل
(0x79 + slotIndex). معاينة ثابتة يمكنها إعادة إنتاج الافتراضي في الملف. انظر §11.13.(446,0)، شوهد على 275/302/325/365/375)
هي فتحات لا يرسمها البرنامج الثابت في العرض الافتراضي — قيمتها لا يمكن حتى أن تتسع قبل
حافة اللوحة. عالجها كمخفية في المعاينة.0x22aod0x22@69,2090x60 img_number مستقل (cnt=10) — المصدر عند −5، منحرف ببايت واحد للأمام. ✅ نفس الانحراف
كما في §11.8 لكن لأرقام غير الساعة: تاريخ "Gradient" كان عند (203,80) أعلى المنتصف مع مصدر
0x17، لكن الفحص الأمامي 82 التقط مؤشر الجار getter الزاوية (0x0a) و
موضع المؤشر → الرقم عُرض في موضع المؤشر بمصدر زائف. الإصلاح: لـ
61 0a 00 img_number في غلاف 0x60، ثق بـ −5/−18/−16 عندما يكون المصدر الأمامي
مستحيلًا لرقم (مصدر-0 أو getter زاوية مؤشر 0x0a/0e/12/70/71/72) و يكون
موضع −18/−16 صالحًا وغير صفري (حارس غير الصفر يتخطى أرقام أطفال المجموعة مع relX=0).0x17 = التاريخ (يوم الشهر)، 0x24 = درجة الحرارة — متميزان. القرص 340 يستخدم كليهما (0x17
"Jun 09" و0x24 درجة حرارة منفصلة)، لذا 0x17 هو التاريخ، وليس درجة الحرارة. القرص الذي تعرض ساعته
درجة حرارة في فتحة 0x17 هو تعقيد مكوَّن من المستخدم (حالة جهاز)، وليس افتراضي الملف.| الوسم | الدور | حاوية؟ | الجسم |
|---|
0x20 | جذر المشهد (غلاف الجسم، ليس عنصرًا قابلًا للرسم) | ✅ | أطفال |
0x21 | الشاشة العادية | ✅ | أطفال |
0x22 | شاشة AOD (§11.9) | ✅ | أطفال |
0x28 | صورة مصغرة معاينة الكتالوج المضمّنة | ✅ | طفل 0x08 واحد |
0x68 | مجموعة / حاوية تخطيط تلقائي | ✅ | إطار 0x48 + أطفال |
0x30 | صورة ثابتة، أو اختيار بالقيمة من N صور | ✅ | 0x01 (+0x02) |
0x60 | قراءة رقمية حية (شريط أرقام) | ✅ | 0x01 + 0x40 (+0x02) |
0x70 | عقرب دوار | ✅ | 0x01 + محور 0x05 |
0x80 | حلقة تقدم، إجرائية | ✅ | 0x01 + 0x5a (§11.15) |
0x81 | حلقة تقدم، مقصوصة بالصورة | ✅ | 0x01 + 0x5b (§11.15) |
0x85 | فتحة تعقيد قابلة للتعيين من المستخدم | ✅ | 0x01 + 0x5f (§11.13) |
0x01 | struct — هندسة + سمات (أدناه) | — | x,y,meta[14] + ذيل مرجع |
0x02 | شرط رؤية (§11.12) | — | قائمة شروط |
0x05 | محور — flag u8, pivotX u16, pivotY u16 | — | 5 بايت |
0x08 | pvStruct — prefix[5] + ذيل مرجع، بدون x/y (معاينة فقط) | — | — |
0x40 | عدد الأرقام / علم الحشو الصفري (§11.10) | — | 1 بايت |
0x48 | إطار — x,y,w,h,gap,align صف/عمود تخطيط تلقائي | — | — |
0x5a / 0x5b | مواصفات قوس لـ 0x80 / 0x81 (§11.15) | — | 19 بايت / 17 بايت |
0x5f | قائمة مقاييس الفتحة لـ 0x85 (§11.13) | — | — |
0x86 | عقدة اسم العرض، دائمًا 64 بايت بالضبط، منتهية بـ NUL، غير مرسومة | — | 64 بايت |
| dial | slot | count | activeIdx | metric ids | → active |
|---|
| 275 SlopeTime | 0 | 6 | 0 | 1c 6a 48 24 19 76 | 0x1c calories |
| 275 SlopeTime | 1 | 6 | 4 | 1c 6a 48 24 19 76 | 0x19 steps |
| 368 Function | 0 | 8 | 0 | 5f 1c 19 48 24 76 1a 8b | 0x5f temperature |
| 368 Function | 1 | 8 | 6 | 5f 1c 19 48 24 76 1a 8b | 0x1a heart rate |
| 273 Activity Mood | 0 | 4 | 0 | 1c 24 48 6a | 0x1c calories |
| 304 Elaborate 2 | 0/1 | 4 | 0 | 1c 48 6a 24 / 24 1c 6a 48 | 0x1c / 0x24 |
| dial | slot | count | activeIdx | metric ids | → active |
|---|
| 275 SlopeTime | 0 | 6 | 0 | 1c 6a 48 24 19 76 | 0x1c calories |
| 275 SlopeTime | 1 | 6 | 4 | 1c 6a 48 24 19 76 | 0x19 steps |
| 368 Function | 0 | 8 | 0 | 5f 1c 19 48 24 76 1a 8b | 0x5f temperature |
| 368 Function | 1 | 8 | 6 | 5f 1c 19 48 24 76 1a 8b | 0x1a heart rate |
| 273 Activity Mood | 0 | 4 | 0 | 1c 24 48 6a | 0x1c calories |
| 304 Elaborate 2 | 0/1 | 4 | 0 | 1c 48 6a 24 / 24 1c 6a 48 | 0x1c / 0x24 |