أفضل الممارسات للتحقّق من حالة الأجهزة الخارجيّة وعرضها - تصميم لا يكتفي بعبارة «متّصل»

· آخر تحديث: · · Windows, أجهزة خارجية, تكامل الأجهزة, إدارة الحالة, UI/UX, مراقبة

سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240873)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621437)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). أفضل الممارسات للتحقّق من حالة الأجهزة الخارجيّة وعرضها - تصميم لا يكتفي بعبارة «متّصل». شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621437 https://comcomponent.com/ar/blog/2026/03/20/002-external-device-state-check-display-best-practices/

DOI (أحدث نسخة)
10.5281/zenodo.21621437
DOI (هذه النسخة)
10.5281/zenodo.22279870

كاميرات صناعيّة، قارئات باركود، PLC، أجهزة قياس، طابعات، أجهزة تسلسليّة، أجهزة USB. في تطبيقات Windows التي تتّصل بأجهزة خارجيّة، يقع الحادث في كثير من الأحيان قبل العطل الفعلي نفسه، لأن عرض الحالة على الشاشة ينحرف عن الواقع.

حالات من هذا النوع:

  • نظام التشغيل يراه، لكن عمليّة أخرى تمسك به فلا يمكن استخدامه
  • نجح open، لكن لم تكتمل بعد العودة إلى الأصل أو الإحماء أو المصادقة
  • الجهاز معلّق في مكانه، لكن الردّ توقّف
  • خيط الاكتساب مات، وبقيت القيمة الأخيرة على الشاشة
  • الوحدة أو الـ firmware غير متوقّعين، ومع ذلك تعرض الشاشة مجرّد «متّصل»

ما نريد معرفته هنا ليس هل هو متّصل فحسب. بل ما الذي يجوز فعله بأمان الآن.

ما نريد معرفته هو جواز العمليّةمخطّط يبيّن أن ما نريد معرفته حقّاً في تطبيق ربط بأجهزة خارجيّة ليس الاتّصال وحده، بل ما الذي يجوز فعله بأمان الآن، وأن الحوادث تقع لأن عرض الحالة على الشاشة ينحرف عن الواقع قبل العطل الفعلي.«هل هو متّصل»جزء فقط ممّا نريد معرفته«ما الذي يجوز فعله بأمان الآن»ما نريد معرفته حقّاًانحراف العرض عن الواقع يُسقط العمليّة أوّلاً

الشكل 1: غاية عرض الحالة ليست الإبلاغ عن الاتّصال، بل إبلاغ المشغّل بالعمليّات الآمنة الآن.

جمهور هذه المقالة وافتراضاتها

البند المحتوى
الجمهور من يصمّم أو ينفّذ تطبيقات Windows تتّصل بأجهزة خارجيّة. نفترض من يريد تقليل استفسارات من نوع «الشاشة تقول متّصل لكنّه لا يعمل» في تطبيق قائم
المعرفة المفترضة القدرة على كتابة تطبيق بلغة ما. لا نفترض معرفة SDK معيّن أو برنامج تشغيل جهاز
البيئة المفترضة تطبيقات سطح مكتب Windows. مع ذلك، طريقة تقسيم الحالة ومبادئ العرض نفسها لا تعتمد على نظام التشغيل
خارج النطاق كيفيّة استدعاء واجهات SDK لمورّد معيّن، وتنفيذ جانب برنامج التشغيل

مصطلحات هذه المقالة

نرتّب أوّلاً، بسطر واحد لكلّ مصطلح، الألفاظ التي تظهر بالإنجليزيّة دون شرح.

المصطلح المعنى بسطر واحد
PLC Programmable Logic Controller. متحكّم صناعي يُستخدم للتحكّم في معدّات الإنتاج
firmware برمجيّات مضمَّنة في الجهاز. قد يختلف الإصدار من وحدة إلى أخرى حتى مع نفس رقم الطراز
heartbeat استعلام أو إشعار خفيف يُتبادَل دوريّاً للتأكّد من أن الطرف حيّ
poll / event poll يعني أن نسأل نحن دوريّاً، وevent يعني أن ننتظر إشعاراً من الطرف الآخر
stale حالة القيمة قديمة. أمكن الحصول عليها سابقاً، لكن القيمة الظاهرة الآن لا يمكن عدّها جديدة
freshness budget سقف يُقرَّر مسبقاً: إذا تجاوزته القيمة فلا تُعدّ جديدة
flapping تذبذب الحالة ذهاباً وإياباً في وقت قصير. يحدث بسبب تماس رديء أو انقطاع لحظي
reconcile المقارنة ثم إعادة مواءمة الحالة الداخليّة مع الواقع
interlock آلية توقف الحركة لأسباب سلامة. ما دامت مفتوحة لا تتحرّك المعدّة
PnP Plug and Play. آلية يكتشف بها نظام التشغيل توصيل الجهاز وفصله ويكيّف التكوين
RTT round-trip time. الزمن من إرسال الاستعلام حتى عودة الردّ

1. الخلاصة أوّلاً

أكثر ما ينفع في التحقّق من حالة الجهاز الخارجي وعرضها هو ألّا تُطوى الحالة في boolean واحد.

على الأقل، نريد الاحتفاظ بهذه المحاور منفصلة.

  • الوجود: هل يراه نظام التشغيل
  • إقامة الجلسة: هل أكمل التطبيق نفسه open / login / initialize
  • الاستجابة: هل يردّ على heartbeat أو status query
  • جاهزية الوظيفة: هل يقبل العمليّة الفعليّة الآن
  • حداثة البيانات: هل قيمة الشاشة جديدة
  • تطابق التكوين: هل هي الوحدة والطراز وfirmware المتوقّعة
  • سلامة المراقبة: هل مسار المراقبة نفسه حيّ

بعبارة تقريبيّة:

التحقّق من الوجود من جهة نظام التشغيل، وقابلية الاستخدام من جهة التطبيق، وحكم الحداثة من جهة الشاشة.

عدم خلط هذه الثلاثة وحدها يجعل عرض الحالة أكثر استقراراً.

ما تجيب عنه الشاشةما لا يجيب عنه إلا التطبيقما يستطيع نظام التشغيل الإجابة عنهإذا انحرف هذا المحورفلا يُستخدم حتى لو اكتملت البقيّةإذا توقّفصارت كل الأحكام قديمةحداثة البياناتهل القيمة المعروضة جديدةسلامة المراقبةهل مسار المراقبة نفسه حيّالجلسةهل اكتمل open / login / initializeالاستجابةهل يردّ على استعلام خفيف ضمن المهلةجاهزية الوظيفةهل يقبل العمليّة الآنتطابق التكوينهل هي الوحدة والطراز وfirmware المتوقّعةالوجودهل واجهة الهدف ظاهرة

الشكل 2: لا تُطوى الحالة في boolean واحد، بل تُقسَم حسب من يستطيع الإجابة عن السؤال. اكتمال الطبقة العليا لا يعني اكتمال الطبقة التي تحتها

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 31، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. لماذا عبارة «متّصل» خطرة

عبارة «متّصل» تحمّل نفسها، دون أن يُلاحَظ ذلك، معاني متعدّدة في لفظ واحد.

في الواقع تختلط فيها الأسئلة التالية على الأقل.

  1. هل يرى نظام التشغيل واجهة الجهاز المستهدف
  2. هل تمكّن التطبيق نفسه من open / login / initialize لذلك الجهاز
  3. هل يردّ على استعلام خفيف ضمن المهلة
  4. هل يمكن تنفيذ العمليّة المطلوبة الآن بأمان
  5. هل القيمة الظاهرة على الشاشة جديدة
  6. هل هي الوحدة والطراز وfirmware المتوقّعة

معنى «قابل للاستخدام» يتغيّر بحسب أيّ من هذه الستّة متحقّق.

الأربع التالية كلّها مختلفة.

  • غير متّصل نظام التشغيل لم يجد الواجهة المستهدفة أصلاً
  • متّصل / قيد التحقّق ظاهر فيزيائيّاً، لكن التهيئة أو المصادقة لم تكتمل
  • متّصل / غير قابل للاستخدام هناك ردّ، لكن لا يمكن التشغيل بسبب warming up أو busy أو interlock أو غياب الوسيط
  • القيمة قديمة أمكن الحصول عليها سابقاً، لكن قيمة الشاشة تجاوزت freshness budget

إذا طُويت كلّها في «متّصل»، لا يستطيع المشغّل أن يقرّر ماذا يفعل.

الأسئلة التي تحمّلها عبارة «متّصل»مخطّط يبيّن أن لفظ «متّصل» الواحد يحمّل نفسه أسئلة متعدّدة: هل يراه نظام التشغيل، وهل يردّ، وهل يمكن تشغيله، وهل القيمة جديدة، وهل هي الوحدة المتوقّعة، وأن طيّها جميعاً يمنع المشغّل من تحديد الخطوة التالية.هل يراه نظام التشغيلتُطوى كلّها في «متّصل»هل يردّ وهل يمكن تشغيلههل القيمة جديدة وهل هي الوحدة المتوقّعةالمشغّل لا يستطيع تحديد الخطوة التالية

الشكل 3: لفظ «متّصل» الواحد يحمّل نفسه أسئلة تختلف إجاباتها.

3. الحالات التي ينبغي فصلها أوّلاً

التوصية هي الاحتفاظ بالحالة الداخليّة على محاور متعدّدة، ثم تلخيصها في الـ UI عند الحاجة.

3.1 محاور الحالة التي نريد فصلها داخليّاً

المحور ماذا يعني طريقة التحقّق النموذجيّة مثال لما نريد إظهاره في الـ UI
الوجود هل يرى نظام التشغيل الواجهة المستهدفة العدّ عند بدء التشغيل، إشعارات arrival / removal غير متّصل / متّصل
الجلسة هل أكمل التطبيق نفسه open / login / initialize نتيجة تهيئة handle / SDK قيد التحقّق / قيد التهيئة
الاستجابة هل يردّ على status query أو heartbeat استعلام خفيف مع timeout يردّ / الردّ متأخّر / لا ردّ
جاهزية الوظيفة هل العمليّة الفعليّة ممكنة الآن status خاص بالجهاز قابل للاستخدام / busy / warming up
حداثة البيانات هل القيمة المعروضة جديدة timestamp / sequence أحدث قيمة / القيمة قديمة
تطابق التكوين هل يطابق الجهاز المتوقّع model / serial / firmware / profile الجهاز المستهدف / جهاز غير متوقّع
سلامة المراقبة هل مسار مراقبة التطبيق حيّ heartbeat للعامل / تأخّر الحلقة قيد المراقبة / توقّفت المراقبة

المهم هنا فصل حالة الجهاز السيّئة عن حالة لم يتمكّن التطبيق من رصدها.

3.2 لا يلزم أن يعرض الـ UI كل المحاور على مستوى واحد

الاحتفاظ بمحاور متعدّدة داخليّاً يبدو وكأنّه سيُضجّ الشاشة. لكن الـ UI لا يحتاج إلى إخراج كل شيء بالوزن نفسه.

التوصية ثلاث طبقات.

  • في الأعلى حالة ملخّصة
  • تحتها السبب
  • وعند الحاجة لوحة تفاصيل

مثلاً:

  • الملخّص: متّصل / غير قابل للاستخدام
  • السبب: قيد الإحماء يتبقّى نحو 18 ثانية
  • التفاصيل: model serial firmware last heartbeat last frame time

هذا التقسيم يبقي القراءة سهلة حتى مع زيادة كمّ المعلومات.

هيكل الشاشة يكون بهذا الترتيب.

+-- كاميرا المرحلة السابقة ---------------------------------+
|
| [ملخّص]  ! متّصل / غير قابل للاستخدام
| [سبب]     قيد الإحماء - يتبقّى نحو 18 ثانية
|
| [تفاصيل]  v توسيع                       (مطويّة افتراضيّاً)
|           model           ACME-CAM-2000
|           serial          A1B2C3
|           firmware        2.4.1
|           last heartbeat  10:23:41.512   (قبل 0.5 ثانية)
|           last frame      10:23:41.402   (قبل 0.6 ثانية)
|
+-----------------------------------------------------------+

النقطة هي أن عدد القرّاء يجوز أن ينقص كلّما نزلنا. الملخّص يقرأه الجميع في ثانية، والسبب يقرأه من يريد معرفة لماذا توقّف، والتفاصيل يفتحها فقط من يقوم بالتشخيص. بهذا الافتراض يمكن تكثيف التفاصيل دون إزعاج الشاشة.

العكس، أي عرض model وserial دائماً بنفس حجم الملخّص، يدفن السطر الأهم.

ثلاث طبقات: ملخّص وسبب وتفاصيلمخطّط يبيّن أن الـ UI يُقسَم إلى ثلاث طبقات، حالة ملخّصة في الأعلى ثم السبب ثم لوحة تفاصيل عند الحاجة، وأن افتراض تناقص عدد القرّاء نزولاً يبقي الشاشة مقروءة حتى مع زيادة المعلومات.الملخّص: يقرأه الجميع في ثانيةالسبب: من يريد معرفة لماذا توقّفالتفاصيل: يفتحها فقط من يقوم بالتشخيصتكثيف التفاصيل لا يُضجّ الشاشة

الشكل 4: التقسيم إلى ثلاث طبقات يقوم على أن عدد القرّاء يجوز أن ينقص في الطبقات السفلى.

4. أفضل الممارسات للتحقّق من الحالة

4.1 العدّ عند بدء التشغيل وإشعارات الوصول / الإزالة

الأساس عند التعامل مع أجهزة خارجيّة على Windows هو عدّ الأجهزة الموجودة عند بدء التشغيل، ثم استقبال إشعارات arrival / removal بعد ذلك.

ثلاث نقاط تستحق التثبيت.

  • الإشعارات وحدها لا تلتقط الأجهزة الموجودة مسبقاً
  • في اتّصال وقت التشغيل تكون interface class أطبيعيّة من setup class
  • ترتيب ظهور إشعار الإزالة وخطأ I/O قد يتقدّم أو يتأخّر

قاعدة الممارسة بسيطة.

  1. العدّ عند بدء التشغيل
  2. الاشتراك في الإشعارات
  3. عند وصول إشعار، إعادة العدّ وreconcile للحالة الداخليّة
قاعدة الممارسة للعدّ والإشعارمخطّط يبيّن قاعدة الممارسة: لأن الإشعارات وحدها لا تلتقط الأجهزة الموجودة مسبقاً، يُعَدّ عند بدء التشغيل، ثم يُشترَك في إشعارات الوصول والإزالة، ثم يُعاد العدّ عند الإشعار لـ reconcile الحالة الداخليّة.1. العدّ عند بدء التشغيل2. الاشتراك في الإشعارات3. عند الإشعار إعادة العدّ وreconcileالإشعارات وحدها لا تلتقط الأجهزة الموجودة مسبقاً

الشكل 5: إدراك الوجود يتمّ بثلاثيّة العدّ والإشعار وreconcile.

4.2 فصل «موجود» و«يمكن فتحه» و«يردّ» و«قابل للاستخدام»

حوادث الأجهزة الخارجيّة تزداد عندما تُعالَج هذه معاً.

  • موجود نظام التشغيل يرى الواجهة
  • يمكن فتحه يمكن امتلاك handle / session دون تعارض عمليّة أخرى أو مشكلة صلاحيات
  • يردّ يردّ على استعلام خفيف ضمن timeout
  • قابل للاستخدام يقبل العمليّة الفعليّة

هذه الأربعة ليست الشيء نفسه.

الوجود والفتح والردّ وقابلية الاستخدام أمور مختلفةمخطّط يبيّن أن الوجود (ظهور الواجهة لنظام التشغيل)، وإمكانية الفتح (امتلاك جلسة دون تعارض أو مشكلة صلاحيات)، والردّ (الردّ على استعلام خفيف ضمن timeout)، وقابلية الاستخدام (قبول العمليّة الفعليّة) ليست الشيء نفسه.موجوديمكن فتحهيردّقابل للاستخدامالأربعة ليست الشيء نفسه

الشكل 6: من «موجود» إلى «قابل للاستخدام» أربع مراحل للتحقّق.

4.3 مزج event وpoll

أسهل في الممارسة من الانحياز كليّاً إلى الأحداث أو كليّاً إلى الاستعلام الدوري أن يكون الكشف بالأحداث وفحص السلامة بالاستعلام الدوري.

  • arrival / removal أحداث
  • heartbeat / status query استعلام دوري
  • حكم freshness بـ timestamp / sequence

هذا التقسيم يسهّل فصل اكتشاف الاتّصال عن قابلية الاستخدام الفعليّة.

الكشف بالأحداث والسلامة بالاستعلام الدوريمخطّط يبيّن أن التقسيم الأسهل هو استقبال كشف الوصول والإزالة كأحداث، وفحص السلامة عبر heartbeat وstatus query باستعلام دوري، وحكم الحداثة بـ timestamp أو sequence، بدل الانحياز إلى أحد الأسلوبين فقط.كشف الوصول والإزالةeventheartbeat وstatus querypollحكم الحداثةtimestamp / sequence

الشكل 7: لا يُجعَل event وpoll في خصومة، بل يُستخدَم كلّ منهما لغايته.

4.4 فصل مسار المراقبة عن الـ UI

تشغيل open / read / status query مباشرة على خيط الـ UI يخلط بسهولة مقتضيات العرض بمقتضيات المراقبة.

التوصية هي الشكل التالي:

  • عامل المراقبة يحدّث state store
  • الـ UI يشترك في state store ويرسم
  • عمليّات الـ UI تُمرَّر كأوامر إلى طبقة المراقبة

بهذا يسهل التعامل مع توقّف المراقبة وتوقّف الجهاز كلّ على حدة.

فصل المراقبة والـ UI عبر state storeمخطّط يبيّن أن جعل عامل المراقبة يحدّث state store، والـ UI يشترك فيه للرسم، وعمليّات الـ UI تُمرَّر كأوامر إلى طبقة المراقبة في اتّجاه واحد، يسهّل فصل توقّف المراقبة عن توقّف الجهاز.يحدّثيشترك ويرسمالعمليّات تُمرَّر كأوامرعامل المراقبةstate storeUIمن يكتب هو العامل فقط، في اتّجاه واحد

الشكل 8: تُفصَل المراقبة عن الـ UI عبر state store، وفي اتّجاه واحد.

الهيكل بـ C# بهذا الحجم تقريباً (افتراض .NET 8 / C# 12). الحيلة هي الاتّجاه الواحد: عامل المراقبة وحده يكتب، والـ UI يقرأ ويرسم فقط.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

public enum DeviceAvailability
{
    Unknown,        // まだ一度も観測できていない
    Absent,         // OS から interface が見えない
    Initializing,   // open / login / initialize の途中
    Ready,          // 操作を受け付けられる
    Unavailable,    // 応答はあるが busy / warming up などで使えない
    NotResponding,  // heartbeat が返らない
    Mismatched,     // 想定外の個体 / firmware
}

// UI へ渡す不変スナップショット。record にして値等価性で差分を判定する
public sealed record DeviceSnapshot(
    string DeviceKey,                 // serial number などのぶれにくいキー
    string DisplayName,
    DeviceAvailability Availability,
    string Reason,                    // 「ウォームアップ中」などの理由
    DateTimeOffset? LastSuccessAt,    // 最後に観測へ成功した時刻
    long Sequence,                    // 機器側が付ける連番
    string FirmwareVersion);

public sealed class DeviceStateStore
{
    private readonly ConcurrentDictionary<string, DeviceSnapshot> _snapshots = new();

    public event Action<DeviceSnapshot>? Changed;

    // 呼ぶのは監視ワーカーだけ
    public void Publish(DeviceSnapshot snapshot)
    {
        _snapshots.TryGetValue(snapshot.DeviceKey, out var previous);
        _snapshots[snapshot.DeviceKey] = snapshot;

        // 値が変わったときだけ通知する。毎 poll ごとに通知すると UI が無駄に再描画される
        if (previous != snapshot)
        {
            Changed?.Invoke(snapshot);
        }
    }

    public IReadOnlyList<DeviceSnapshot> Current() => _snapshots.Values.ToList();
}

جانب الـ UI يحصر الاشتراك وإعادة التنفيذ إلى خيط الـ UI في موضع واحد.

using System;
using System.Windows.Forms;

public sealed class DeviceStatusPresenter
{
    private readonly DeviceStateStore _store;
    private readonly Control _uiContext;   // UI スレッドへ戻すための足場
    private readonly Label _summary;
    private readonly Label _reason;

    public DeviceStatusPresenter(DeviceStateStore store, Control uiContext, Label summary, Label reason)
    {
        _store = store;
        _uiContext = uiContext;
        _summary = summary;
        _reason = reason;
        // اربط الاشتراك بعمر الشاشة. نسيان Dispose يتحوّل مباشرة
        // إلى «الاستمرار في رمي تحديثات إلى نموذج مغلق»
        _uiContext.Disposed += (_, _) => Dispose();
        _store.Changed += OnChanged;      // إن نسيت الاشتراك، لن تتغيّر الشاشة عند التحديث
    }

    public void Dispose() => _store.Changed -= OnChanged;

    private void OnChanged(DeviceSnapshot snapshot)
    {
        // عامل المراقبة يبقى يعمل أثناء إغلاق الشاشة.
        // BeginInvoke بعد تدمير المقبض يرمي استثناء، وذلك الاستثناء
        // يخرج عبر Changed?.Invoke إلى عامل المراقبة.
        // فيصير العطب: «أُغلقت الشاشة فتوقفَت المراقبة»
        if (_uiContext.IsDisposed || _uiContext.Disposing || !_uiContext.IsHandleCreated)
        {
            return;
        }

        try
        {
            if (_uiContext.InvokeRequired)
            {
                _uiContext.BeginInvoke(() => Render(snapshot));
                return;
            }

            Render(snapshot);
        }
        catch (ObjectDisposedException)
        {
            // أُغلق بعد اجتياز الفحص أعلاه. هذه الفجوة لا تُمحى من حيث المبدأ،
            // فاستقبلها بالتخلّي عن العرض. لا توقف المراقبة
        }
        catch (InvalidOperationException)
        {
            // المقبض غير مُنشأ أو مدمَّر. كما سبق
        }
    }

    private void Render(DeviceSnapshot snapshot)
    {
        _summary.Text = snapshot.Availability switch
        {
            DeviceAvailability.Absent => "غير متّصل",
            DeviceAvailability.Initializing => "متّصل / قيد التحقّق",
            DeviceAvailability.Ready => "قابل للاستخدام",
            DeviceAvailability.Unavailable => "متّصل / غير قابل للاستخدام",
            DeviceAvailability.NotResponding => "لا استجابة",
            DeviceAvailability.Mismatched => "جهاز غير متوقَّع",
            _ => "قيد التحقّق",
        };
        _reason.Text = snapshot.Reason;
    }
}

لحظة إغلاق الشاشة هي أضعف نقطة في هذا الشكل. Changed?.Invoke(snapshot) يستدعي المعالج تزامنيّاً من خيط عامل المراقبة. استدعاء BeginInvoke بعد إغلاق النموذج وتدمير مقبض التحكّم يرمي استثناء، وذلك الاستثناء يخرج عبر Publish إلى عامل المراقبة. تنظيف الشاشة يسقط المراقبة نفسها. والعَرَض هو «يسقط أحياناً عند الإنهاء»، فيصعب الإمساك بشروط إعادة الإنتاج.

إلغاء الاشتراك في Dispose لا يكفي. إذا كان العامل قد بدأ Invoke أثناء الإلغاء، فلا يمكن إيقاف ذلك الاستدعاء. نقاط التثبيت ثلاث.

  • اربط الاشتراك بعمر الشاشة. ألغِ الاشتراك تلقائيّاً عبر Control.Disposed حتى لا يتحوّل نسيان استدعاء Dispose إلى «مواصلة الرمي إلى نموذج مغلق»
  • لا ترمِ إلى تحكّم قيد التدمير أو مدمَّر. انظر IsDisposed / Disposing / IsHandleCreated وتخلَّ عن التحديث في المكان
  • الفجوة المتبقّية تُستقبَل بالاستثناء. إمكان الإغلاق بين الحكم وBeginInvoke لا يُمحى من حيث المبدأ. اقبض هنا ObjectDisposedException وInvalidOperationException، وتخلَّ عن العرض وحده. إن لم تقبضهما، تنجرف المراقبة مع الشاشة

النقطة هي أن تقرّر مسبقاً أن «تحديث العرض عند الإغلاق يجوز التخلّي عنه». لا قيمة لذلك الرسم الواحد، لكن لبقاء عامل المراقبة حيّاً قيمة.

الانجراف معه في لحظة إغلاق الشاشةمخطّط يبيّن أن Changed يستدعي المعالج تزامنيّاً من خيط المراقبة، فينتقل استثناء BeginInvoke بعد تدمير المقبض عبر Publish إلى عامل المراقبة ويسقطه، لذا يُربَط الاشتراك بعمر الشاشة وتُقبَض الفجوة المتبقّية بالتخلّي عن العرض وحده.للمنعإغلاق الشاشةBeginInvoke بعد التدمير يرمي استثناءالاستثناء يخرج إلى عامل المراقبة«أغلقت الشاشة فتوقفّت المراقبة»ربط الاشتراك بعمر الشاشةقبض الفجوة المتبقّية والتخلّي عن العرض وحده

الشكل 9: تخلَّ عن الرسم الواحد في لحظة الإغلاق، واحمِ بقاء عامل المراقبة.

4.5 حكم freshness يفصل «هل وصل» عن «هل تقدّم المحتوى»

حكم حداثة البيانات لا يكفي بالنظر إلى زمن الاستقبال وحده. هناك نمط عطل يستمرّ فيه callback من SDK بينما يتوقّف timestamp أو sequence للقيمة.

لذا نفصل حداثة الاستقبال عن حداثة المحتوى.

using System;

public sealed record Reading(
    long Sequence,                  // 機器側が付ける連番
    DateTimeOffset ValueTimestamp,  // 機器側が値に付けた時刻
    DateTimeOffset ReceivedAt,      // アプリが受け取った時刻。画面に出す用
    long ReceivedTicks);            // 同じ受信の単調増加タイムスタンプ。判定用

public enum Freshness
{
    Fresh,
    Stale,
    Unknown,
}

public static class FreshnessPolicy
{
    // freshness budget: これを超えたら live の顔で見せない
    public static readonly TimeSpan Budget = TimeSpan.FromSeconds(5);

    /// <param name="lastAdvancedTicks">連番が最後に進んだときの、単調増加タイムスタンプ</param>
    /// <param name="nowTicks">判定時点の単調増加タイムスタンプ</param>
    public static Freshness Evaluate(
        Reading? previous, Reading? current,
        long lastAdvancedTicks, long nowTicks, TimeProvider clock)
    {
        if (current is null)
        {
            return Freshness.Unknown;   // 一度も取れていない
        }

        if (clock.GetElapsedTime(current.ReceivedTicks, nowTicks) > Budget)
        {
            return Freshness.Stale;     // そもそも届いていない
        }

        if (previous is null)
        {
            return Freshness.Fresh;     // 初回は比較対象がないので受信時刻だけで判断する
        }

        if (current.Sequence < previous.Sequence)
        {
            // 連番が戻った。機器の再起動、別個体への差し替え、SDK 再初期化を疑う
            return Freshness.Unknown;
        }

        if (current.Sequence == previous.Sequence &&
            clock.GetElapsedTime(lastAdvancedTicks, nowTicks) > Budget)
        {
            // 受信は続いているのに中身が更新されていない
            return Freshness.Stale;
        }

        return Freshness.Fresh;
    }
}

يُحفَظ lastAdvancedTicks بساعتنا نحن كالتالي.

public sealed class FreshnessTracker(TimeProvider clock)
{
    private readonly object _gate = new();
    private Reading? _previous;
    private long _lastAdvancedTicks;

    // 受信のたびに呼ぶ。ReceivedAt と ReceivedTicks は同じ受信の記録
    public Reading Capture(long sequence, DateTimeOffset valueTimestamp) =>
        new(sequence, valueTimestamp, clock.GetLocalNow(), clock.GetTimestamp());

    public Freshness Observe(Reading current)
    {
        lock (_gate)
        {
            if (_previous is null || current.Sequence > _previous.Sequence)
            {
                _lastAdvancedTicks = current.ReceivedTicks;
            }

            var result = FreshnessPolicy.Evaluate(
                _previous, current, _lastAdvancedTicks, clock.GetTimestamp(), clock);
            _previous = current;
            return result;
        }
    }

    // 受信が完全に途絶えると Observe は二度と呼ばれない。
    // タイマーから定期的にこれを呼び、最後に受け取った値の「古さ」を測り直す。
    // 状態は更新しないので、何度呼んでも安全
    public Freshness Reevaluate()
    {
        lock (_gate)
        {
            return FreshnessPolicy.Evaluate(
                _previous, _previous, _lastAdvancedTicks, clock.GetTimestamp(), clock);
        }
    }
}

Observe وحده لا يكتشف صمت الجهاز. Observe يُستدعى فقط عند الاستقبال، وReading الممرَّر حينها أُنشئ للتو. الفرق بين ReceivedTicks والآن يكاد يكون صفراً، لذا الطريق الوحيد إلى Stale عبر هذا المسار هو «الاستقبال مستمرّ لكن الرقم التسلسلي لا يتقدّم». عندما يتوقّف callback من SDK تماماً ── وهو نمط العطل الذي نريد معرفته أكثر من غيره ── لا يُستدعى Observe، وتتجمّد الشاشة على آخر Fresh حُسِب.

لذا نوفّر منفذاً يعيد الحكم بدورة مستقلّة عن الاستقبال. ذلك هو Reevaluate أعلاه، ويُستدعى من مؤقّت. اجعل الدورة أقصر من budget (إن كان budget 5 ثوانٍ فكل ثانية تقريباً). إن ساويت الطولين فقد لا تلاحظ العطل في أسوأ الحالات حتى نحو ضعف budget.

Observe وحده لا يكتشف الصمتمخطّط يبيّن أن Observe يُستدعى فقط عند الاستقبال، فإذا توقّف callback من SDK تماماً لم يُستدعَ Observe وتجمّدت الشاشة على آخر Fresh، لذا يُستدعى Reevaluate من مؤقّت بدورة مستقلّة عن الاستقبال لإعادة قياس قِدَم آخر قيمة.المعالجةيتوقّف callback تماماًيتوقّف استدعاء Observeتتجمّد الشاشة على آخر Freshاستدعاء Reevaluate من مؤقّتالدورة أقصر من budget

الشكل 10: «الصمت التام» الذي نريد معرفته أكثر من غيره لا يُرى بحكم مدفوع بالاستقبال.

// System.Threading.Timer。受信が無くても判定は進める。
// RenderFreshness は、4.4 の presenter と同じ経路で画面へ反映する自前のメソッド
_freshnessTimer = new Timer(
    _ => RenderFreshness(_tracker.Reevaluate()),
    null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

Observe وReevaluate يأتيان في الوقت نفسه من خيطين مختلفين، لذا تُحمى داخلية FreshnessTracker بـ lock. إن أُسقط ذلك اختلط استبدال _previous بقراءته، فيظهر عطل يصعب تتبّعه: حكم أقدم بجيل واحد أحياناً.

النقطة هنا ألّا نأخذ الفرق مع ValueTimestamp. ValueTimestamp قيمة وضعتها ساعة الجهاز، ولا ضمان أنّها تطابق ساعتنا. طرحهما يجعل القيمة الواصلة للتو stale إذا تأخّرت ساعة الجهاز، ويبقي القيمة المتوقّفة fresh إلى ما لا نهاية إذا تقدّمت. اجعل budget دائماً على زمن منقضٍ قِيس بساعتك أنت. اترك ValueTimestamp لعرض «ما الزمن الذي ينسبه الجهاز إلى القيمة» على الشاشة، أو لاستخدامه مع الرقم التسلسلي كمؤشّر على توقّف جانب الجهاز.

و«ساعتك أنت» أيضاً لا يكفي طرح DateTimeOffset. هذه ساعة حائط، وتقفز بمزامنة NTP أو الضبط اليدوي أو التحويل إلى التوقيت الصيفي. إن رجعت الساعة صار الزمن المنقضي سالباً، وبقي الجهاز Fresh رغم انقطاعه. إن تقدّمت صارت القيمة الواصلة للتو Stale في اللحظة نفسها. كلّما طال تشغيل الشاشة 24 ساعة زاد الدوس على هذا الفخ.

لذا يُستخدم لحكم budget طابع زمني متزايد رتيب. TimeProvider.GetTimestamp() يعيد قيمة عالية الدقّة قائمة على Stopwatch، وGetElapsedTime(بداية, نهاية) يعطي الزمن المنقضي بين نقطتين (راجع مراجع الفصل 10. متاح من .NET 8 فما فوق). اترك ReceivedAt بساعة الحائط لعرض «استُقبل في 10:15:03» على الشاشة فقط، ولا تدعه يشارك في حكم «كم ثانية مضت». إمكان استبدال ساعة الاختبار مزيّة أخرى للتوسّط عبر TimeProvider.

حكم الحداثة بساعة متزايدة رتيبةمخطّط يبيّن أن ساعة الحائط تقفز بمزامنة NTP أو الضبط اليدوي، فطرحها لحكم الحداثة يبقي الجهاز Fresh رغم انقطاعه أو يجعل القيمة الواصلة للتو Stale، لذا يُحكَم budget بطابع زمني متزايد رتيب ويُفرَد زمن الاستقبال بساعة الحائط للعرض فقط.لذلكساعة الحائط تقفز بـ NTP أو الضبط اليدويالزمن المنقضي يصبح سالباً أو مبالغاً فيهحكم الحداثة يخطئالحكم بطابع زمني متزايد رتيبزمن الاستقبال بساعة الحائط للعرض فقط

الشكل 11: أساس حكم الحداثة هو ألّا تُستخدم ساعة الحائط لسؤال «كم ثانية مضت».

بهذا الشكل يستطيع جانب الـ UI، عند استلام Freshness.Stale، تطبيق سياسة 5.3 كما هي: «إظهار العمر بجانب القيمة» و«إخراجها من حكم قابلية التشغيل».

الاحتفاظ بـ Unknown منفصلاً عن Stale هو حتى لا يُخلَط ما لم يُعرَف بعد مع ما هو قديم. الأوّل قد يُحلّ بالانتظار، والثاني لا يُصلَح بالانتظار.

4.6 تثبيت تعريف الوحدة

تتبّع الحالة بمعرّف ظاهري مثل الاسم الودّي أو COM3 وحده يسهّل الخلط بين الوحدات.

الأأمن داخليّاً هو الاحتفاظ بمفتاح قليل التذبذب مثل:

  • serial number
  • logical device id
  • stable device path
  • معرّف الوحدة من جانب الجهاز

5. أفضل الممارسات للعرض

5.1 جدول حكم في صفحة واحدة

الحالة الفعليّة ملخّص الـ UI عرض تكميلي
لا واجهة غير متّصل افحص الكابل والطاقة واتّصال USB
واجهة موجودة، قيد التهيئة متّصل / قيد التحقّق قيد التهيئة، قيد المصادقة، قيد الإحماء
يردّ، شروط التشغيل غير متحقّقة متّصل / غير قابل للاستخدام busy، لا وسيط، interlock مفتوح
يردّ، القيمة قديمة متّصل / القيمة قديمة آخر تحديث قبل 12 ثانية
لا ردّ لا ردّ قيد إعادة الاتّصال، timeout الاتّصال
وحدة غير متوقّعة جهاز غير متوقّع عدم تطابق model / serial / firmware
توقّف مسار المراقبة خلل في المراقبة توقّف عامل المراقبة، يلزم إعادة التشغيل

5.2 الصياغة: «الحالة + السبب + الإجراء التالي»

خطأ أو خلل وحدهما ضعيفان كشاشة. تقريب الرسالة إلى العناصر الثلاثة التالية يقلّل حيرة المشغّل.

  • الحالة: ماذا يحدث
  • السبب: لماذا حُكم بذلك
  • الإجراء التالي: ماذا ينبغي فعله

مثلاً:

  • متّصل / غير قابل للاستخدام - قيد الإحماء - انتظر نحو 18 ثانية
  • لا ردّ - heartbeat timeout - افحص الكابل والطاقة
  • جهاز غير متوقّع - يلزم Firmware 2.1.0 - افحص الجهاز المستهدف

بالمقارنة مع صياغات شائعة في الميدان يتّضح ما ينقص.

صياغة مرفوضة شائعة ما ينقص مثال إعادة كتابة
خطأ لا حالة ولا سبب ولا إجراء تالٍ لا ردّ - heartbeat timeout - افحص الكابل والطاقة
متّصل الحالة غامضة. لا يُعرَف إن كان قابلاً للاستخدام الآن متّصل / غير قابل للاستخدام - قيد الإحماء - انتظر نحو 18 ثانية
الجهاز غير موجود لا سبب ولا إجراء تالٍ غير متّصل - لم تُعَدّ الواجهة المستهدفة - افحص الكابل والطاقة
حدث 0x80070005 لا حالة مقروءة ولا إجراء غير قابل للاستخدام - تعذّر open للمنفذ. 0x80070005 رُفض الوصول - افحص إن كان تطبيق آخر يستخدم المنفذ نفسه
جارٍ إعادة المحاولة... لا يُعرَف حتى متى ولا كم مرّة ولا ماذا بعد ذلك إعادة الاتّصال المرّة 3 / الحدّ 10 - 8 ثوانٍ حتى المحاولة التالية - يمكن إعادة الاتّصال يدوياً أيضاً
طبيعي لا يُعرَف في أيّ لحظة كان طبيعيّاً قابل للاستخدام - آخر تحديث قبل 0.5 ثانية

المشترك في أمثلة إعادة الكتابة هو هل يستطيع المشغّل أن يقرّر الخطوة التالية من تلك الشاشة وحدها. إن لم يمكن كتابة شيء سوى «اتصل بالدعم»، فالمشكلة ليست في الصياغة بل في نقص تصميم الحالة.

الصياغة حالة + سبب + إجراء تالٍمخطّط يبيّن أن خطأ أو خلل وحدهما ضعيفان، وأن تقريب الرسالة إلى ثلاثة عناصر، ماذا يحدث (الحالة) ولماذا حُكم بذلك (السبب) وماذا ينبغي فعله (الإجراء التالي)، يمكّن المشغّل من تقرير الخطوة التالية من الشاشة وحدها.الحالة: ماذا يحدثصياغة لا تحيّر المشغّلالسبب: لماذا حُكم بذلكالإجراء التالي: ماذا ينبغي فعلههل تقرَّر الخطوة التالية من الشاشة وحدها

الشكل 12: جودة الرسالة تُحكَم باكتمال العناصر الثلاثة.

5.3 لا تُخفَ بيانات stale

آخر قيمة معروفة مفيدة. لكن عرضها بوجه قيمة live أقلّ أماناً.

التوصية:

  • timestamp بجانب القيمة
  • عرض عمر القيمة
  • تغيير اللون أو الوسم عند stale
  • إخراجها من حكم قابلية التشغيل إذا تجاوزت زمناً معيّناً

5.4 غيّر موضع العرض بحسب الأهمّيّة

شريط الحالة مفيد، لكن يسهل تفويته. تجنّب وضع خلل حرج في زاوية شريط الحالة وحدها.

تقسيم طبيعي:

  • تغيّر حالة طفيف: شريط الحالة
  • تنبيه يمكن مواصلة العمل معه: إشعار ضمن السطر
  • خلل يتطلّب إيقاف العمليّة: المنطقة الرئيسيّة، حوار، لافتة

5.5 في عرض عدّة أجهزة افصل الملخّص عن التفاصيل

في شاشة تتعامل مع عدّة أجهزة يصبح العرض صعباً إذا أُخرجت تفاصيل الجميع دائماً.

ثلاث طبقات تسهّل الجمع بين النظرة الكلّيّة والتشخيص الفردي:

  • في الأعلى ملخّص الكلّ
  • في الأسفل صف لكلّ جهاز
  • عند الاختيار جزء تفاصيل

هيكل الشاشة كالتالي.

+-- قائمة المعدّات -----------------------------------------+
|
| [ملخّص الكلّ]  قابل للاستخدام 6 / 8      تنبيه 1      خلل 1
|
| [صف لكلّ جهاز]
|     الحالة        الاسم المعروض        السبب               آخر تحديث
|     --------      ---------------      ----------------    --------
|     قابل للاستخدام كاميرا المرحلة السابقة -                    قبل 0.5 ثانية
|     قابل للاستخدام طابعة الملصقات      -                    قبل 1.2 ثانية
|   > غير قابل للاستخدام كاميرا الفحص    قيد الإحماء          قبل 0.6 ثانية   <- محدّد
|     لا ردّ        قارئ الباركود        heartbeat timeout   قبل 48 ثانية
|
| [جزء التفاصيل]  كاميرا الفحص
|     serial A1B2C3 / firmware 2.4.1 / يتبقّى نحو 18 ثانية
|     [ إعادة اتّصال ]   [ فتح السجلّ ]
|
+------------------------------------------------------------+

بهذا التكوين الثلاثي تلبي الشاشة نفسها في الوقت عينه من ينظر إلى الملخّص الكلّي (هل يجوز تشغيل الخطّ اليوم)، ومن ينظر إلى الصفوف (أيّ جهاز توقّف)، ومن ينظر إلى جزء التفاصيل (ماذا يفعل ليُصلَح).

ترتيب الصفوف أسهل إذا رُفعت حالات الخلل إلى الأعلى. لكن تبديل الترتيب كل ثانية يسبّب ضغطاً خاطئاً، لذا تُعاد الترتيب على الحالة المثبّتة بعد تنعيم flapping (راجع 6.2).

التكوين الثلاثي يلبّي ثلاثة أنواع من القرّاءمخطّط يبيّن أن شاشة عدّة الأجهزة، إذا قُسِمت إلى ملخّص كلّي في الأعلى وصفوف لكلّ جهاز في الأسفل وجزء تفاصيل عند الاختيار، تلبي في الوقت عينه من يسأل إن جاز تشغيل الخطّ، ومن يسأل أيّ جهاز توقّف، ومن يسأل ماذا يفعل ليُصلَح.ملخّص الكلّهل يجوز تشغيل الخطّ اليومصف لكلّ جهازأيّ جهاز توقّفجزء التفاصيلماذا يفعل ليُصلَح

الشكل 13: شاشة عدّة الأجهزة تُبنى على ثلاث طبقات لقرّاء مختلفين.

6. أفضل الممارسات لإعادة الاتّصال والتشغيل

6.1 إعادة الاتّصال مع backoff

عند توقّف الردّ، أأمن ألّا تُطرَق إعادة الاتّصال بأقصر حلقة.

لأن ذلك:

  • يحمّل device / driver / SDK
  • يغرق السجلّات
  • يفاقم عدم الاستقرار المؤقّت
  • يجعل الـ UI يتذبذب بعنف

الشكل الواقعي هو:

  • إعادة المحاولة فوراً في المرّة الأولى
  • إن فشلت، إطالة الفترة تدريجيّاً
  • وضع حدّ أعلى
  • توفير إعادة اتّصال يدوي أيضاً

رسم الحالة وشروط الانتقال يُظهر أين يعمل backoff وحدّ إعادة الاتّصال.

لم يُعثَر عليه حتى بعد العدّالعدّ عند بدء التشغيل / إشعار الوصولإشعار الوصولإشعار الإزالة (من أيّ حالة)UnknownAbsentPresentopen / login / initializeنجحت التهيئة والتحقّق من التكوينفشلت التهيئة (ما زال هناك مجال لإعادة المحاولة)وحدة أو طراز أو firmware غير متوقّعwarming up / قيد التنفيذ / interlockيقبل العمليّةخطأ I/Oخطأ I/O / تثبّت انعدام الردّوضع backoffانقضت مدّة الانتظاربُلِغ حدّ إعادة الاتّصالإعادة اتّصال يدويّةإعادة اتّصال يدويّة (لا عودة تلقائيّة)إعادة اتّصال يدويّة بعد إصلاح التكوينDetectedOpeningReadyFaultMismatchBusyReconnectingRetryExhausted

الشكل 14: هذا المخطّط يعالج عمر الاتّصال والجلسة فقط، ولا يُدخل حداثة البيانات. الحداثة محور مستقلّ عن جاهزية الوظيفة (الشكل 1)، ويمكن أن تقدُم القيمة في Ready كما في Busy بالقدر نفسه، فخلطها كحالة يولّد خلطاً من نوع «انقطعت القيمة أثناء التنفيذ، وعند العودة صارت Ready». على الشاشة يُحتفَظ بحالة هذا المخطّط وبحكم الحداثة في 4.5 كلّ على حدة ثم يُدمَجان. حتى عند بلوغ حدّ إعادة المحاولة لا تُسقَط الحالة إلى Absent (غير متّصل)، بل تُترَك في RetryExhausted التي لا تعود تلقائيّاً ── فقد يبقى الجهاز ظاهراً وهو معطوب، وعرضه كغير متّصل يضلّل عمليّة الاستعادة. عدم تطابق التكوين أيضاً لا يُصلَح بإعادة المحاولة، لذا أُخرج من حلقة إعادة الاتّصال التلقائيّة

6.2 تنعيم flapping

في تماس USB رديء أو انقطاع شبكي لحظي تذهب الحالة وتعود في وقت قصير. إخراج الأحداث الخام كما هي إلى الـ UI يجعل القراءة صعبة جدّاً.

لذا يسهل التعامل مع التقسيم التالي:

  • السجلّ الداخلي يحتفظ بالأحداث الخام
  • الـ UI ينتظر فترة تأكيد قصيرة ثم يثبّت العرض
  • لكن الخلل الحرج يُعرَض فوراً
تنعيم flapping ثم تثبيت العرضمخطّط يبيّن أن flapping، أي تذبذب الحالة في وقت قصير بسبب تماس رديء أو انقطاع لحظي، يُعالَج بالإبقاء على الأحداث الخام في السجلّ الداخلي، وتثبيت عرض الـ UI بعد فترة تأكيد قصيرة، مع عرض الخلل الحرج فوراً.الحالة تذهب وتعود في وقت قصيرالأحداث الخام تبقى في السجلّ كما هيالـ UI يثبت العرض بعد فترة تأكيدالخلل الحرج وحده يُعرَض فوراً

الشكل 15: الأحداث الخام للسجلّ، والحالة المثبّتة المنعَّمة للـ UI.

6.3 الحدّ الأدنى من السجلّات التي ينبغي إبقاؤها

تحسين عرض الحالة يأتي تقريباً مع تصميم السجلّ.

البند مثال
timestamp 2026-03-20T10:23:41.512+09:00
stable device key camera:A1B2C3
الاسم المعروض كاميرا المرحلة السابقة
الحالة القديمة -> الحالة الجديدة Ready -> Stale
السبب heartbeat timeout firmware mismatch
رمز الخطأ HRESULT Win32 SDK code
last success 2026-03-20T10:23:36.011+09:00
age / RTT 5.5s 320ms
retry count 3
إصدار التطبيق / firmware App 1.8.2 / FW 2.4.1

الأهم خصوصاً هو سجلّ انتقال الحالة.

6.4 لا تخلط توقّف المراقبة بتوقّف الجهاز

  • ماتت حلقة الاستعلام الدوري باستثناء
  • توقّف callback من SDK
  • دخل عامل الاكتساب في deadlock
  • توقّف تحديث state store وحده

في هذه الحالات قد يكون الجهاز حيّاً بينما التطبيق لا يرصد. عرض ذلك بـ غير متّصل أو لا ردّ وحدهما يجعله يبدو مشكلة في الجهاز.

لذا أأمن الاحتفاظ بسلامة مسار المراقبة كمحور مستقلّ.

لا تُظهر توقّف المراقبة كتوقّف للجهازمخطّط يبيّن أن موت حلقة الاستعلام الدوري أو توقّف callback يعني أن التطبيق لا يرصد حتى إن كان الجهاز حيّاً، وعرض ذلك كغير متّصل أو لا ردّ يجعله يبدو مشكلة في الجهاز، لذا تُحفَظ سلامة مسار المراقبة كمحور مستقلّ.لذلكتموت حلقة الاستعلام الدوري باستثناءالجهاز حيّ لكن لا رصديتوقّف callback أو العاملعرض «غير متّصل» يبدو مشكلة في الجهازسلامة المراقبة كمحور مستقلّ

الشكل 16: لا تخلط «لم نرصد» مع «الجهاز سيّئ».

7. نقاط يسهل تفويتها حسب نوع الجهاز

7.1 أجهزة USB / PnP

  • الإشعارات وحدها لا تلتقط existing device
  • في وقت التشغيل تكون interface class أطبيعيّة من setup class
  • الجهاز المركّب قد يُخرج عدّة واجهات
  • ترتيب ظهور إشعار الإزالة وخطأ I/O قد يتقدّم أو يتأخّر

7.2 الأجهزة التسلسليّة

ظهور COMx وحده لا يكفي للاطمئنان.

  • المنفذ موجود لكن الجهاز المستهدف غير معلّق
  • عمليّة أخرى فتحت المنفذ
  • الردّ توقّف أصلاً
  • read / write يتجمّدان بـ timeout

في التسلسلي أأمن خصوصاً فصل الوجود عن الردّ عن قابل للاستخدام.

7.3 الأجهزة الشبكيّة

لا تساوِ بين نجاح ping وقابلية استخدام التطبيق.

هناك مراحل:

  • هل يُحَلّ الاسم
  • هل يمكن اتّصال TCP
  • هل يمكن handshake طبقة التطبيق
  • هل status جاهز
  • هل القيمة fresh
مراحل نجاح ping مقابل قابلية الاستخداممخطّط يبيّن أن الأجهزة الشبكيّة لها مراحل: هل يُحَلّ الاسم، وهل يمكن اتّصال TCP، وهل يمكن handshake طبقة التطبيق، وهل status جاهز، وهل القيمة fresh، فلا يُساوى نجاح ping بقابلية الاستخدام.يُحَلّ الاسميمكن اتّصال TCPيمكن handshake طبقة التطبيقstatus جاهزالقيمة freshنجاح ping لا يساوي قابلية الاستخدام

الشكل 17: حتى «قابل للاستخدام» في الجهاز الشبكي خمس مراحل.

7.4 الكاميرات / أجهزة القياس المعتمدة على SDK

لا تحكم بأنّها live لمجرّد وصول callback من SDK.

  • خيط callback نفسه يتوقّف
  • الإطار يصل لكن timestamp لا يتقدّم
  • دفق الصورة يصل لكن قناة التحكّم ميتة
  • إعادة تطبيق الإعدادات بعد إعادة الاتّصال لم تكتمل

لذلك يطمئن الاحتفاظ أيضاً بسلامة تُرى من خارج SDK.

8. ما لا ينبغي فعله

  • طيّ الحالة في ثلاثة: متّصل / غير متّصل / خطأ
  • الاعتقاد أن الإشعارات وحدها تلتقط existing device أيضاً
  • عدّ نجاح open مباشرة قابلاً للاستخدام
  • عرض آخر قيمة معروفة بوجه fresh
  • عدم عرض timestamp
  • تشغيل open / read / status query على خيط الـ UI
  • تدوير إعادة المحاولة بأقصر حلقة
  • إخراج الخلل الحرج في شريط الحالة وحده
  • خلط غير متّصل مع توقّف المراقبة
  • تعريف الوحدة بالاسم الودّي أو COM3 وحدهما

9. الخلاصة

ما يهمّ حقّاً في تطبيق الربط مع أجهزة خارجيّة هو تقرير ماذا يحقّ لك أن تقول، بعد أيّ تحقّق.

هذا التقسيم ينفع خصوصاً.

موجود التطبيق نفسه يستطيع فتحه يردّ تلك العمليّة ممكنة الآن قيمة الشاشة جديدة

افصل هذه الخمسة.

بعد ذلك تكون الإرشادات العمليّة تقريباً كالتالي.

  • عند بدء التشغيل العدّ، وبعده الإشعارات
  • قابلية الاستخدام تُقرَّر بـ heartbeat وstatus خاص بالجهاز
  • القيمة المعروضة تحمل timestamp وعمر القيمة
  • الخلل الحرج يُخرَج في موضع يصعب تفويته
  • لا تُظهر خلل مسار المراقبة كخلل في الجهاز

في الممارسة، أهمّ من إمكان عرض «متّصل» هو كم يصعب على ذلك العرض أن ينحرف عن الواقع.

روابط مرجعية

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

تطوير تطبيقات ويندوز

في تطبيقات التكامل مع الأجهزة الخارجيّة، لا ترتبط جودة التشغيل بمعالجة الاتّصال وحدها، بل أيضاً بتوافق إدارة الحالة مع ما تعرضه واجهة المستخدم، لذا فإنّ ترتيب ذلك في مرحلة التصميم يقلّل الحوادث.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

لماذا لا يكفي عرض حالة الجهاز بعبارة «متّصل»؟
لأن عبارة «متّصل» تطوي أسئلة متعدّدة في سؤال واحد: هل يراه نظام التشغيل، وهل نجح التطبيق في open، وهل يردّ، وهل يمكن تشغيله الآن، وهل القيم على الشاشة جديدة، وهل هي الوحدة المتوقّعة. «غير متّصل» و«متّصل / قيد التحقّق» و«متّصل / غير قابل للاستخدام» و«القيمة قديمة» حالات مختلفة. إذا طُويت كلّها في «متّصل»، لا يستطيع المشغّل أن يقرّر ماذا يفعل.
كيف ينبغي الاحتفاظ بحالة الجهاز الخارجي داخليّاً؟
تُفصَل المحاور: الوجود (هل يراه نظام التشغيل)، وإقامة الجلسة (هل اكتمل open / login / initialize)، والاستجابة (هل يردّ على heartbeat)، وجاهزية الوظيفة (هل يقبل العملية الآن)، وحداثة البيانات (هل قيم الشاشة جديدة)، وتطابق التكوين (هل هي الوحدة وfirmware المتوقّعان)، وسلامة المراقبة (هل مسار المراقبة نفسه حيّ). بعبارة تقريبيّة: التحقّق من الوجود من جهة نظام التشغيل، وقابلية الاستخدام من جهة التطبيق، وحكم الحداثة من جهة الشاشة. يصبح الـ UI أوضح إذا عُرض على ثلاث طبقات: ملخّص وسبب وتفاصيل.
هل يُكتشَف الجهاز وتُفحَص سلامته بالأحداث أم بالاستعلام الدوري؟
لا تُختزل المسألة إلى أحد الخيارين. في الممارسة يسهل التعامل مع الكشف كأحداث ومع فحص السلامة كاستعلام دوري. وصول الجهاز وإزالته تُستقبَل كإشعارات، وheartbeat وstatus query يُنفَّذان باستعلام دوري، وحكم الحداثة يعتمد على timestamp أو sequence. الإشعارات وحدها لا تلتقط الأجهزة الموجودة مسبقاً، لذا القاعدة العمليّة هي العدّ عند بدء التشغيل، ثم الاشتراك في الإشعارات، ثم إعادة العدّ عند كل إشعار لمواءمة الحالة الداخليّة.
كيف تُنفَّذ إعادة الاتّصال بجهاز توقّف عن الردّ؟
لا تُطرَق بأقصر حلقة ممكنة، بل تُرفَق بـ backoff. الشكل الواقعي هو إعادة المحاولة فوراً في المرّة الأولى، ثم إطالة الفترة تدريجيّاً إن فشلت، مع وضع حدّ أعلى وتوفير زرّ «إعادة اتّصال» يدوي. الحلقة القصيرة تحمّل الجهاز وSDK، وتغرق السجلّات، وتفاقم عدم الاستقرار المؤقّت. أمّا flapping، أي تذبذب الحالة في نافذة قصيرة بسبب تماس USB رديء مثلاً، فيفيد فيه أن ينتظر الـ UI فترة تأكيد قصيرة قبل تثبيت العرض.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة