Skip to content

5. TP 2 - التحكم في أجهزة Arduino باستخدام جهاز لوحي يعمل بنظام Android

سنقوم الآن بتعلم كيفية التحكم في لوحة Arduino باستخدام جهاز لوحي. المثال الذي سنتبعه هو مشروع [client-android-skel] من الدورة (انظر الفقرة 2).

5.1. بنية المشروع

ستكون بنية المشروع بأكمله كما يلي:

  • سيتم تزويدكم بالكتلة [1]، خادم الويب / jSON وأردوينو؛
  • سيكون عليكم بناء الكتلة [2]، وبرمجة جهاز Android اللوحي للتواصل مع خادم الويب / jSON.

5.2. المعدات

تتوفر لديك العناصر التالية:

  • جهاز Arduino مزود بامتداد Ethernet ومصباح LED ومستشعر درجة الحرارة؛
  • وحدة miniHub لتقاسمها مع طالب آخر؛
  • كابل USB لتزويد جهاز Arduino بالطاقة؛
  • كابلان شبكيان لتوصيل جهاز Arduino وجهاز PC على نفس الشبكة الخاصة؛
  • جهاز لوحي يعمل بنظام Android؛

5.2.1. جهاز Arduino

فيما يلي كيفية توصيل العناصر المختلفة معًا:

  • افصل كابل الشبكة عن جهاز PC؛
  • قم بتوصيل جهاز PC وجهاز Arduino بواسطة كابل شبكة؛
  • سيكون جهاز أردوينو الذي ستحصل عليه مبرمجًا بالفعل. وسيكون عنوانه IP هو [192.168.2.2]. لكي يرى جهاز PC جهاز Arduino، يجب إعطاؤه عنوان IP على الشبكة [192.168.2]. تمت برمجة أجهزة Arduino للتواصل مع جهاز PC الذي يحمل العنوان IP [192.168.2.1]. وإليك كيفية القيام بذلك:

انتقل إلى [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:

 
  • في [1]، انقر على الرابط [réseau local
  • في [2]، انقر على زر [Propriétés] للشبكة المحلية؛
  • في [3]، انقر على خصائص [IPv4] للخريطة [réseau local
  • في [4]، قم بتعيين العنوان IP [192.168.2.1] وقناع الشبكة الفرعية [255.255.255.0] لهذه البطاقة؛
  • في [5]، انقر على [OK] عدة مرات حسب الحاجة للخروج من المعالج.

5.2.2. الجهاز اللوحي

  • باستخدام مفتاح wifi الخاص بك، قم بتوصيل جهازك بشبكة wifi التي سنحددها لك. افعل الشيء نفسه مع جهازك اللوحي؛
  • تحقق من عنوان شبكة الواي فاي الخاص بجهازك IP عن طريق كتابة [ipconfig] في نافذة DOS. ستجد عنوانًا من النوع [192.168.x.y

dos>ipconfig

Configuration IP de Windows

Carte réseau sans fil Wi-Fi :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : 192.168.1.1
  • تحقق من عنوان IP wifi الخاص بجهازك اللوحي. اسأل مشرفك عن كيفية القيام بذلك إذا كنت لا تعرف. ستجد عنوانًا من النوع [192.168.x.z
  • قم بتعطيل جدار الحماية الخاص بجهازك PC إذا كان نشطًا [Panneau de configuration\Système et sécurité\Pare-feu Windows
  • في نافذة DOS، تحقق من أن جهاز PC والجهاز اللوحي يمكنهما التواصل عن طريق كتابة الأمر [ping 192.168.x.z] حيث [192.168.x.z] هو عنوان جهازك اللوحي. يجب أن يرد الجهاز اللوحي عندئذٍ بما يلي:
dos>ping 192.168.1.26

Envoi d'une requête 'Ping'  192.168.1.26 avec 32 octets de données :
Réponse de 192.168.1.26 : octets=32 temps=102 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=134 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=168 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=208 ms TTL=64

Statistiques Ping pour 192.168.1.26:
    Paquets : envoyés = 4, reçus = 4, perdus = 0 (perte 0%),
Durée approximative des boucles en millisecondes :
    Minimum = 102ms, Maximum = 208ms, Moyenne = 153ms

أصبح إعداد الشبكة لنظامك جاهزًا الآن.

5.2.3. المحاكي [Genymotion]

يحل المحاكي [Genymotion] (انظر الفقرة 6.9) محل الجهاز اللوحي بشكل مثالي. فهو سريع بنفس القدر تقريبًا ولا يتطلب شبكة واي فاي. يُنصح باستخدام هذه الطريقة. يمكنك استخدام الجهاز اللوحي لإجراء الفحص النهائي لتطبيقك.

5.3. برمجة أجهزة Arduino

نحن مهتمون هنا بكتابة كود C لأردوينو:

للقراءة

  • تثبيت بيئة تطوير Arduino (انظر الفقرة 6.1
  • استخدام مكتبات jSON (الملحقات، الفقرة 6.6
  • في بيئة Arduino، اختبار مثال لخادم (مثل خادم الويب) ومثال لعميل (مثل عميل Telnet
  • المرفقات المتعلقة ببيئة برمجة Arduinos في الفقرة 6.1.

Arduino عبارة عن مجموعة من المسامير المتصلة بالأجهزة. هذه المسامير هي مداخل أو مخارج. قيمتها ثنائية أو تناظرية. للتحكم في Arduino، هناك عمليتان أساسيتان:

  • كتابة قيمة ثنائية / تناظرية على دبوس محدد برقمه؛
  • قراءة قيمة ثنائية / تناظرية على دبوس محدد برقمه؛

إلى هاتين العمليتين الأساسيتين، سنضيف عملية ثالثة:

  • إضاءة مصباح LED بشكل متقطع لمدة معينة وبتردد معين. يمكن تنفيذ هذه العملية من خلال تكرار العمليتين الأساسيتين السابقتين. لكننا سنرى في الاختبارات أن التبادلات بين الطبقة [DAO] و Arduino تتم في غضون ثانية واحدة. وبالتالي، لا يمكن جعل مصباح LED يومض كل 100 مللي ثانية على سبيل المثال. لذلك سنقوم بتنفيذ وظيفة الوميض هذه على Arduino نفسه.

سيكون تشغيل Arduino كما يلي:

  • تتم الاتصالات بين الطبقة [DAO] وجهاز Arduino عبر شبكة TCP-IP عن طريق تبادل أسطر نصية بتنسيق jSON (JavaScript Object Notation
  • عند بدء التشغيل، يتصل Arduino بالمنفذ 100 لخادم تسجيل موجود في الطبقة [DAO]. ويرسل إلى الخادم سطر نص واحد:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

وهي سلسلة jSON تميز Arduino الذي يتصل:

  • id: معرف Arduino؛
  • desc: وصف لما يمكن لـ Arduino القيام به. هنا قمنا ببساطة بوضع نوع Arduino؛
  • mac: عنوان Mac الخاص بـ Arduino؛
  • port: رقم المنفذ الذي سينتظر عليه Arduino الأوامر من الطبقة [DAO].

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

  • بمجرد أن يسجل Arduino نفسه لدى خادم التسجيل، يبدأ في الاستماع على المنفذ الذي أشار إليه للخادم (102 أعلاه). وينتظر أوامر jSON بالصيغة التالية:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

هذه سلسلة jSON تحتوي على العناصر التالية:

  • id: معرف الأمر. يمكن أن يكون أي شيء؛
  • ac: إجراء. هناك ثلاثة إجراءات:
  • pw (pin write) لكتابة قيمة على دبوس،
  • pr (قراءة الدبوس) لقراءة قيمة أحد الدبابيس،
  • cl (وميض) لإضاءة مصباح LED بشكل متقطع؛
  • pa: معلمات الإجراء. وهي تعتمد على الإجراء.
  • يرسل Arduino دائمًا استجابة إلى عميله. هذه الاستجابة عبارة عن سلسلة jSON بالشكل التالي:
{"id":"1","er":"0","et":{"pinx":"valx"}}

حيث

  • id: معرف الأمر الذي يتم الرد عليه؛
  • er (خطأ): رمز خطأ في حالة حدوث خطأ، وإلا 0؛
  • و (الحالة): قاموس فارغ دائمًا باستثناء أمر القراءة pr. ويحتوي القاموس عندئذٍ على قيمة الدبوس رقم x المطلوب.

فيما يلي أمثلة لتوضيح المواصفات السابقة:

جعل المصباح رقم 8 يومض 10 مرات بفاصل زمني قدره 100 مللي ثانية:

الأمر
{"id":"1","ac":"cl","pa":{"pin":"8","dur":"100","nb":"10"}}
الرد
{"id":"1","er":"0","et":{}}

معلمات الأمر cl هي: المدة (dur) بالمللي ثانية لكل ومضة، وعدد (nb) الومضات، ورقم (pin) دبوس المصباح.

اكتب القيمة الثنائية 1 على الدبوس رقم 7:

الأمر
{"id":"2","ac":"pw","pa":{"pin":"7","mod":"b","val":"1"}}
الرد
{"id":"2","er":"0","et":{}}

معلمات pa للأمر pw هي: وضع الكتابة mod b (ثنائي) أو a (تناظري)، والقيمة val المراد كتابتها، ورقم الدبوس. للكتابة الثنائية، تكون val هي 0 أو 1. للكتابة التناظرية، تكون val ضمن النطاق [0,255].

كتابة القيمة التناظرية 120 على الدبوس رقم 2:

الأمر
{"id":"3","ac":"pw","pa":{"pin":"2","mod":"a","val":"120"}}
الرد
{"id":"3","er":"0","et":{}}

قراءة القيمة التناظرية من الدبوس رقم 0:

الأمر
{"id":"4","ac":"pr","pa":{"pin":"0","mod":"a"}}
الرد
{"id":"4","er":"0","et":{"pin0":"1023"}}

معلمات الأمر pr هي: وضع القراءة mod b (ثنائي) أو a (تناظري)، ورقم الدبوس. إذا لم يكن هناك خطأ، يضع Arduino قيمة الدبوس المطلوب في القاموس "et" في إجابته. هنا يشير pin0 إلى أن القيمة المطلوبة هي قيمة الدبوس رقم 0 وأن 1023 هي هذه القيمة. عند القراءة، ستكون القيمة التناظرية ضمن النطاق [0, 1024].

لقد قدمنا الأوامر الثلاثة cl و pw و pr. قد يتساءل المرء عن سبب عدم استخدام حقول أكثر وضوحًا في السلاسل jSON، مثل action بدلاً من ac، و pinwrite بدلاً من pw، و parametres بدلاً من pa، ... يتمتع Arduino بذاكرة محدودة للغاية. ومع ذلك، فإن سلاسل jSON التي يتم تبادلها مع Arduino تساهم في شغل الذاكرة. لذلك اخترنا تقصيرها إلى أقصى حد.

لنلقِ نظرة الآن على بعض حالات الأخطاء:

الأمر
xx
الرد
{"id":"","er":"100","et":{}}

تم إرسال أمر لا يتوافق مع التنسيق jSON. أرسل Arduino رمز الخطأ 100.

الأمر
{"id":"4","ac":"pr","pa":{"mod":"a"}}
الرد
{"id":"4","er":"302","et":{}}

تم إرسال أمر pr مع نسيان المعلمة pin. أرسل Arduino رمز الخطأ 302.

الأمر
{"id":"4","ac":"pinread","pa":{"pin":"0","mod":"a"}}
الرد
{"id":"4","er":"104","et":{}}

تم إرسال أمر pinread غير معروف (وهو pr). أرسل Arduino رمز الخطأ 104.

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

يتم توفير كود البرنامج الذي يتم تنفيذه على Arduino في الأمثلة الواردة في هذا المستند:

  

لنقله إلى Arduino:

  • قم بتوصيله بـ PC الخاص بك؛
  • في [1]، افتح الملف [arduino_uno.ino]. سيتم تشغيل Arduino وتحميل الملف؛

ملاحظة: تم إنشاء الكود واختباره في الأصل باستخدام IDE ARDUINO 1.5.x. ومنذ ذلك الحين، تم إصدار إصدارات أخرى من IDE. لم يعمل الكود مع IDE ARDUINO 1.6.x. يبدو أن هناك مشكلة في التوافق مع الإصدارات السابقة بين الإصدارين 1.6 و 1.5.

  • في [2-4]، حدد نوع Arduino المستخدم؛
  • في [5-7]، حدد المنفذ التسلسلي الذي يوجد عليه في PC؛
  • في [8]، قم بتحميل البرنامج [arduino_uno] على Arduino؛

يحتوي كود البرنامج على تعليقات كثيرة. يمكن للقارئ المهتم الرجوع إليها. نكتفي هنا بالإشارة إلى أسطر الكود التي تسمح بتكوين الاتصال الثنائي الاتجاه بين العميل والخادم بين Arduino و PC:


#تضمين <SPI.h>
#تضمين <Ethernet.h>
#تضمين <ajSON.h>

// ---------------------------------- CONFIGURATION DE L'ARDUINO UNO
// عنوان MAC لـ Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// عنوان Arduino IP
IPAddress ipArduino(192,168,2,2);
// معرفه
char * idArduino="cuisine";
// منفذ خادم Arduino
int portArduino=102;
// وصف Arduino
char * descriptionArduino="contrôle domotique";
// سيعمل خادم Arduino على المنفذ 102
EthernetServer server(portArduino);
// IP لخادم التسجيل
IPAddress ipServeurEnregistrement(192,168,2,1); 
// منفذ خادم التسجيل
int portServeurEnregistrement=100;
// عميل Arduino لخادم التسجيل
EthernetClient clientArduino;
// أمر العميل
char commande[100];
// استجابة Arduino
char message[100];

// التهيئة
void setup() {
  // سيسمح جهاز المراقبة التسلسلي بمتابعة التبادلات
  Serial.begin(9600);
  // بدء اتصال إيثرنت
  Ethernet.begin(macArduino,ipArduino);  
  // الذاكرة المتاحة
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}

// حلقة لا نهائية
void loop()
{
  ...
}
  • السطر 8: عنوان Mac الخاص بـ Arduino. لا يكتسي هذا الأمر أهمية كبيرة هنا لأن Arduino سيكون على شبكة خاصة تضم جهاز PC وواحد أو أكثر من أجهزة Arduino. يجب فقط أن يكون عنوان Mac فريدًا على هذه الشبكة الخاصة. عادةً ما تحتوي بطاقة الشبكة الخاصة بـ Arduino على ملصق يوضح عنوان Mac للبطاقة. إذا كان هذا الملصق غير موجود ولم تكن تعرف عنوان Mac للبطاقة، يمكنك إدخال ما تريد في السطر 8 طالما تم الالتزام بقاعدة تفرد عنوان Mac على الشبكة الخاصة؛
  • السطر 11: عنوان IP للبطاقة. مرة أخرى، نضع ما نريد من النوع [192.168.2.x] ونغير x لمختلف أجهزة Arduino في الشبكة الخاصة؛
  • السطر 13: معرف Arduino. يجب أن يكون فريدًا بين معرفات أجهزة Arduino في نفس الشبكة الخاصة؛
  • السطر 15: منفذ خدمة Arduino. يمكن إدخال ما تريد؛
  • السطر 17: وصف وظيفة Arduino. يمكن إدخال ما تريد. انتبه إلى السلاسل الطويلة بسبب الذاكرة المحدودة لـ Arduino؛
  • السطر 21: عنوان IP لخادم تسجيل Arduino على PC. يجب ألا يتم تعديله؛
  • السطر 23: منفذ خدمة التسجيل هذه. لا يجب تعديله؛

5.4. خادم الويب / jSON

5.4.1. التثبيت

Image

يتم تزويدك بالملف الثنائي Java لخادم الويب / jSON:

 

افتح نافذة الأوامر واكتب الأمر التالي:

dos>java -jar arduinos-server-01-all-1.0.jar

إذا لم يكن [java.exe] موجودًا في PATH في نافذة الأوامر، فسيكون من الضروري كتابة المسار الكامل لـ [java.exe] (عادةً C:\Program Files\java\...).

ستفتح نافذة DOS وتعرض السجلات:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v0.5.0.M6)

2014-01-06 11:11:35.550  INFO 8408 --- [           main] arduino.rest.metier.Application          : Starting Application on Gportpers3 with PID 8408 (C:\Users\SergeTahÚ\Desktop\part2\server.jar started by ST)
2014-01-06 11:11:35.587  INFO 8408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6a4ba620: startup date [Mon Jan 06 11:11:35 CET 2014]; root of context hierarchy
2014-01-06 11:11:36.765  INFO 8408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-01-06 11:11:36.766  INFO 8408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.42
2014-01-06 11:11:36.876  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-01-06 11:11:36.877  INFO 8408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1293 ms
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-01-06 11:11:37.184  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.386  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.faireClignoterLed(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/commands/{idArduino}],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.sendCommandesJson(java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.getArduinos(javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.389  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinRead(java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.390  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinWrite(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.463  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] إلى معالج من النوع [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.464  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] معالج من النوع [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.881  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 796 ms
Serveur d'enregistrement lancÚ sur 192.168.2.1:100
2014-01-06 11:11:38.101  INFO 8408 --- [       Thread-4] arduino.dao.Recorder                  : Recorder : [11:11:38:101] : [Serveur d'enregistrement : attente d'un client]
2014-01-06 11:11:38.142  INFO 8408 --- [           main] arduino.rest.metier.Application : Started Application in 3.257 seconds
  • السطر 11: يتم تشغيل خادم Tomcat مدمج؛
  • السطر 15: يتم تحميل وتنفيذ السيرفلت [dispatcherServlet] من Spring MVC؛
  • السطر 18: يتم الكشف عن URL Rest [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}]؛
  • السطر 19: تم الكشف عن URL Rest [/arduinos/commands/{idArduino}]؛
  • السطر 20: تم الكشف عن URL Rest [/arduinos/
  • السطر 21: تم الكشف عن URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}]؛
  • السطر 22: تم الكشف عن URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}]؛
  • السطر 26: تم تشغيل خادم تسجيل أجهزة Arduino؛

قم بتوصيل جهاز Arduino بجهاز PC إذا لم تكن قد قمت بذلك بعد. يجب تعطيل جدار الحماية الخاص بجهاز PC. ثم استخدم متصفحًا للوصول إلى جهاز URL [http://localhost:8080/arduinos]:

يجب أن يظهر معرف Arduino المتصل. إذا لم يظهر شيء، فكر في إعادة ضبط Arduino. يوجد زر ضغط لهذا الغرض.

تم الآن تثبيت خادم الويب / jSON.

5.4.2. URL المعروضة بواسطة خدمة الويب / jSON

للقراءة: مشروع [Exemple-15] (انظر الفقرة 1.16.1

تم تنفيذ خدمة الويب / jSON باستخدام Spring MVC وتعرض ما يلي:


@Controller
public class WebController {

  // طبقة الأعمال
  @Autowired
  private IMetier métier;

  // قائمة أجهزة أردوينو
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }

  // الوميض
  @RequestMapping(value = "/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String faireClignoterLed(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("duree") int duree, @PathVariable("nombre") int nombre) throws JsonProcessingException {
...
  }

  // إرسال الأوامر JSON
  @RequestMapping(value = "/arduinos/commands/{idArduino}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String sendCommandesJson(@PathVariable("idArduino") String idArduino, HttpServletRequest request) throws IOException {
    ...
  }

  // قراءة الدبوس
  @RequestMapping(value = "/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinRead(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode) throws JsonProcessingException {
    ....
  }

  // كتابة دبوس
  @RequestMapping(value = "/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinWrite(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode, @PathVariable("valeur") int valeur) throws JsonProcessingException {
  ...
  }
}

الردود المرسلة من الخادم هي تمثيلات jSON للفئة [Response<T>] التالية:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- الخصائص
    // حالة العملية
    private int status;
    // رسائل الحالة المحتملة
    private List<String> messages;
    // نص الرد
    private T body;

    // المنشئات
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // مُستردات ومُعيّنات
...
}

يرسل URL [/arduinos] استجابة من النوع [Response<List<Arduino>>] حيث [Arduino] هي الفئة التالية:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // البيانات
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// الوصول والضبط
...
}
  • السطر 7: [id] هو معرف أردوينو؛
  • السطر 8: وصفه؛
  • السطر 9: عنوانه MAC؛
  • السطر 10: عنوانه IP؛
  • السطر 11: المنفذ الذي ينتظر عليه الأوامر؛

URL:

  • [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}]؛
  • [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}]؛
  • [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}]؛
  • [/arduinos/commands/{idArduino}]؛

ترسل استجابة من النوع [Response<ArduinoResponse>] حيث تمثل الفئة [ArduinoResponse] الاستجابة القياسية لأردوينو:


public class ArduinoResponse implements Serializable {
  
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;

  // الوصول والتعيين
...
}
  • [json]: السلسلة jSON المرسلة من Arduino والتي تعذر فك تشفيرها (حالة خطأ)، وإلا null؛
  • [id]: معرف الأمر الذي يستجيب له Arduino؛
  • [erreur]: رمز خطأ، 0 إذا كان OK، وإلا شيء آخر؛
  • [etat]: قاموس يحتوي على الرد المحدد للأمر. غالبًا ما يكون فارغًا إلا إذا طلب الأمر قراءة قيمة من Arduino، وفي هذه الحالة سيتم وضعها في هذا القاموس؛

5.4.3. اختبارات خدمة الويب / jSON

تعود على خادم الويب / jSON من خلال اختبار URL التالية:

URL
rôle
http://localhost:8080/arduinos/
rend la liste des Arduinos connectés
http://localhost:8080/arduinos/
blink/1/cuisine/8/100/20/
fait clignoter la led de la pin n° 8
 de l'Arduino identifié par cuisine,
 20 fois toutes les 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
lecture analogique de la pin n° 0 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinRead/1/cuisine/5/b/
lecture binaire de la pin n° 5 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
écriture binaire de la valeur 1 sur la pin n° 8 de l'Arduino identifié par
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/4/a/100/
écriture analogique de la valeur 100 sur la pin n° 4 de l'Arduino identifié
 par cuisine

فيما يلي بعض لقطات الشاشة لما يجب أن تحصل عليه:

الحصول على قائمة بأجهزة Arduino المتصلة:

السلسلة jSON المستلمة من خادم الويب / jSON هي كائن يحتوي على الحقول التالية:

  • [status]: عند 0 يشير إلى عدم وجود خطأ - وإلا فهذا يعني وجود خطأ؛
  • [messages]: قائمة بالرسائل التي تشرح الخطأ في حالة حدوث خطأ:
  • [body]: قائمة بأجهزة Arduino في حالة عدم وجود خطأ. يتم وصف كل جهاز Arduino بواسطة كائن يحتوي على الحقول التالية:
    • [id]: معرف جهاز Arduino. لا يمكن أن يكون لجهازي Arduino نفس المعرف؛
    • [description]: وصف موجز لوظيفة جهاز Arduino؛
    • [mac]: عنوان Mac لـ Arduino؛
    • [ip]: عنوان IP الخاص بـ Arduino؛
    • [port]: المنفذ الذي ينتظر عليه الأوامر؛

جعل مصباح LED الخاص بالدبوس رقم 8 في Arduino المحدد بـ [cuisine] يومض 20 مرة كل 100 مللي ثانية:

 

السلسلة jSON المستلمة من خادم الويب / jSON هي كائن يحتوي على الحقول التالية:

  • [status]: عند 0 يشير إلى عدم وجود خطأ - وإلا فقد حدث خطأ؛
  • [messages]: قائمة بالرسائل التي تشرح الخطأ في حالة حدوث خطأ:
  • [body]: استجابة Arduino في حالة عدم وجود خطأ:
    • [id]: معرف الأمر. هذا المعرف هو الرقم 1 في [/blink/1]. يستخدم Arduino معرف الأمر هذا في ردّه؛
    • [erreur]: رقم خطأ. تشير القيمة المختلفة عن 0 إلى وجود خطأ؛
    • [etat]: يُستخدم فقط لقراءة دبوس. تكون قيمته عندئذ هي قيمة الدبوس؛
    • [json]: يُستخدم فقط في حالة حدوث خطأ بين العميل والخادم. تكون قيمته عندئذ هي السلسلة الخاطئة jSON المرسلة من Arduino؛

قراءة تناظرية للطرف رقم 0 في Arduino المحدد بـ [cuisine]:

 

السلسلة jSON المستلمة من خادم الويب / jSON مشابهة للسلسلة السابقة باستثناء الحقل [etat] الذي يمثل قيمة الدبوس رقم 0.

قراءة ثنائية للرقم 5 من Arduino المحدد بـ [cuisine]:

 

السلسلة jSON المستلمة من خادم الويب / jSON مشابهة للسلسلة السابقة.

كتابة ثنائية للقيمة 1 على الدبوس رقم 8 في Arduino المحدد بـ [cuisine]:

 

السلسلة jSON المستلمة من خادم الويب / jSON مشابهة للسلسلة السابقة.

اختبار URL [http://localhost:8080/arduinos/commands/cuisine] أكثر تعقيدًا. تنتظر طريقة خادم الويب / jSON التي تعالج هذا URL طلب POST لا يمكن محاكاته ببساطة باستخدام متصفح. لاختبار هذا URL، يمكن استخدام متصفح Chrome مع الملحق [Advanced REST Client] (انظر الفقرة 6.13):

 
  • في [1]، URL لطريقة الويب / jSON المراد اختبارها؛
  • إلى [2]، طريقة POST لإرسال الطلب؛
  • في [3-4]، القيمة المرسلة هي jSON؛
  • في [5]، السلسلة jSON المنشورة. يجب ملاحظة الأقواس التي تبدأ القائمة وتنهيها. هنا، في القائمة، لا يوجد سوى أمر jSON الذي يجعل الدبوس رقم 8 يومض 10 مرات كل 100 مللي ثانية؛
  • في [6]، يتم إرسال الطلب؛
 
  • في [7]، يتم إرسال الرد jSON من قبل الخادم. تلقى الكائن كائنًا يحتوي على الحقلين المعتادين [status, messages] وحقل [body] الذي تمثل قيمته قائمة ردود Arduino على كل أمر من أوامر jSON المرسلة.

لنرى ما يحدث عند إرسال أمر jSON غير صحيح من الناحية النحوية بالنسبة لـ Arduino:

عندها نتلقى الرد التالي:

 

نلاحظ أن رقم الخطأ في رد Arduino هو [104]، مما يشير إلى أن الأمر [xx] لم يتم التعرف عليه.

5.5. اختبارات عميل Android

فيما يلي الملف الثنائي القابل للتنفيذ لعميل Android النهائي:

  

باستخدام الماوس، قم بسحب الملف الثنائي [app-debug.apk] أعلاه وإفلاته على محاكي الجهاز اللوحي [GenyMotion]. سيتم بعد ذلك تسجيله ثم تنفيذه. قم أيضًا بتشغيل خادم الويب / jSON إذا لم تكن قد قمت بذلك بالفعل. قم بتوصيل Arduino بـ PC المزود بمصباح LED. يتيح عميل Android إدارة أجهزة Arduino عن بُعد. ويقدم للمستخدم الشاشات التالية.

تتيح علامة التبويب [CONFIG] الاتصال بالخادم واسترداد قائمة أجهزة Arduino المتصلة:

Image

  • في [1]، أدخل العنوان IP [192.168.2.1] المخصص لـ PC الخاص بك (انظر الفقرة 5.2).

تسمح علامة التبويب [PINWRITE] بكتابة قيمة على دبوس في Arduino:

Image

Image

تسمح علامة التبويب [PINREAD] بقراءة قيمة دبوس في Arduino:

Image

تسمح علامة التبويب [BLINK] بإضاءة مصباح LED في Arduino:

Image

تسمح علامة التبويب [COMMAND] بإرسال أمر jSON إلى Arduino:

Image

5.6. عميل Android لخدمة الويب / jSON

ننتقل الآن إلى كتابة عميل Android.

5.6.1. بنية العميل

ستكون بنية عميل Android هي بنية مشروع [Exemple-15] (انظر الفقرة 1.16.2

  • تتواصل طبقة [DAO] مع خادم الويب / jSON؛

يجب أن يكون عميل Android قادرًا على التحكم في عدة أجهزة Arduino في وقت واحد. على سبيل المثال، نريد أن نتمكن من إضاءة مصباحين LED موجودين على جهازي Arduino في نفس الوقت وليس واحدًا تلو الآخر. لذلك سيستخدم عميل Android الخاص بنا مهمة غير متزامنة لكل جهاز Arduino وستُنفذ هذه المهام بالتوازي.

5.6.2. مشروع Android Studio الخاص بالعميل

انسخ المشروع [client-android-skel] (انظر الفقرة 2) إلى المشروع [client-arduinos-01] (إذا لزم الأمر، راجع كيفية نسخ مشروع Gradle في الفقرة 1.15):

Image

5.6.3. العروض الخمسة XML

  

سيكون هناك خمس طرق عرض XML:

  • [blink]: لإضاءة مصباح LED في Arduino. وهي مرتبطة بالجزء [BlinkFragment
  • [commands]: لإرسال أمر jSON إلى Arduino. وهي مرتبطة بالجزء [CommandsFragment
  • [config]: لتكوين URL لخدمة الويب / jSON والحصول على القائمة الأولية لأجهزة Arduino المتصلة. وهي مرتبطة بالجزء [ConfigFragment
  • [pinread]: لقراءة القيمة الثنائية أو التناظرية لدبوس في جهاز Arduino. وهي مرتبطة بالجزء [PinReadFragment
  • [pinwrite]: لكتابة قيمة ثنائية أو تناظرية على دبوس في جهاز Arduino. وهي مرتبطة بالجزء [PinWriteFragment

في الوقت الحالي، ستحتوي هذه العروض الخمسة XML جميعها على نفس المحتوى الفارغ:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
  </RelativeLayout>
</ScrollView>
  • توجد العرضة في حاوية [RelativeLayout] (الأسطر 7-10) التي هي بدورها مضمنة في حاوية [ScrollView] (الأسطر 2-11). وهذا يضمن لنا إمكانية "التمرير" في العرضة إذا تجاوزت حجم شاشة الجهاز اللوحي؛

المهمة: قم بإنشاء العروض الخمسة XML.


5.6.4. قائمة الأجزاء

نعلم أن أجزاء مشروع تم إنشاؤه باستخدام [client-android-skel] يجب أن تكون مرتبطة بقائمة، حتى لو كانت فارغة. هنا، لن تحتوي التطبيق على قائمة. القائمة الفارغة موجودة بالفعل في المشروع؛

  

5.6.5. الأجزاء الخمسة للتطبيق

 

المهمة: قم بنسخ الجزء [DummyFragment] في الأجزاء الخمسة للتطبيق، كما هو موضح في [2].


يحتوي الجزء [ConfigFragment] على الهيكل التالي:


package client.android.fragments.behavior;

import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;

@EFragment
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {

  // الحقول الموروثة من الفئة الأصلية -------------------------------------------------------
...

استبدل السطر 10 بالسطر التالي:


@EFragment(R.layout.config)

المهمة: افعل الشيء نفسه مع الأجزاء الأربعة الأخرى عن طريق تعديل السمة [@EFragment] للفئة.


الجزء
عرض
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. حالات الأجزاء

سيكون لكل جزء حالة.


المهمة: قم بتكرار الفئة [DummyFragmentState] خمس مرات، لإنشاء الحالات الخمس المعروضة في [2].


5.6.7. تخصيص المشروع

 

تحتوي الحزمة [architecture / custom] على العناصر القابلة للتخصيص في بنية التطبيق.

5.6.7.1. الواجهة [IMainActivity]

تحدد واجهة [IMainActivity] ما يمكن أن تطلبه الأجزاء من النشاط بالإضافة إلى ثوابت التطبيق. ستكون هذه الواجهة كما يلي:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // الوصول إلى الجلسة
  ISession getSession();

  // تغيير العرض
  void navigateToView(int position, ISession.Action action);

  // إدارة الانتظار
  void beginWaiting();

  void cancelWaiting();

  // ثوابت التطبيق -------------------------------------

  // وضع التصحيح
  boolean IS_DEBUG_ENABLED = true;

  // الحد الأقصى لوقت انتظار استجابة الخادم
  int TIMEOUT = 1000;

  // فترة الانتظار قبل تنفيذ طلب العميل
  int DELAY = 000;

  // المصادقة الأساسية
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // تجاور الأجزاء
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // شريط علامات التبويب
  boolean ARE_TABS_NEEDED = true;

  // صورة الانتظار
  boolean IS_WAITING_ICON_NEEDED = true;

  // عدد الأجزاء
  int FRAGMENTS_COUNT = 5;

  // عدد المشاهدات
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}
  • الأسطر 25 و28 و31 و40: تكوين الطبقة [DAO]. يستعلم هذا التطبيق عن خادم ويب / jSON؛
  • السطر 37: يحتوي هذا التطبيق على علامات تبويب؛
  • السطر 43: يحتوي هذا التطبيق على خمسة أجزاء؛
  • السطور 46-50: أرقام الأجزاء الخمسة؛
  • السطر 34: تجاور الأجزاء. يمكن للمطور إدخال قيمة هنا في النطاق [1, FRAGMENTS_COUNT-1

5.6.7.2. الفئة [CoreState]

الفئة [CoreState] هي الفئة الأم لحالات الأجزاء:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // ما إذا تمت زيارة الجزء أم لا
  protected boolean hasBeenVisited = false;
  // حالة القائمة المحتملة للجزء
  protected MenuItemState[] menuOptionsState;

  // أدوات الاسترجاع والتعيين
...
}
  • الأسطر 12-16: يجب هنا إعلان فئات حالات الأجزاء الخمسة؛

5.6.8. الفئة [MainActivity]

  

ستكون الفئة [MainActivity] كما يلي:


package client.android.activity;

import android.support.design.widget.TabLayout;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.ArduinoCommand;
import client.android.dao.entities.ArduinoResponse;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.*;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

import java.util.List;
import java.util.Locale;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // الطبقة [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // الجلسة
  private Session session;

  // طرق الفئة الأصلية -----------------------
  @Override
  protected void onCreateActivity() {
    // السجل
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // الجلسة
    this.session = (Session) super.session;
    // إنشاء العلامات الخمس
    for (int i = 0; i < 5; i++) {
      TabLayout.Tab newTab = tabLayout.newTab();
      newTab.setText(getFragmentTitle(i));
      tabLayout.addTab(newTab);
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    Locale l = Locale.getDefault();
    switch (position) {
      case 0:
        return getString(R.string.config_titre).toUpperCase(l);
      case 1:
        return getString(R.string.blink_titre).toUpperCase(l);
      case 2:
        return getString(R.string.pinread_titre).toUpperCase(l);
      case 3:
        return getString(R.string.pinwrite_titre).toUpperCase(l);
      case 4:
        return getString(R.string.commands_titre).toUpperCase(l);
    }
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // عرض الجزء رقم الموضع
    navigateToView(position, ISession.Action.NAVIGATION);
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // التنفيذ IDao -----------------------------------------
}
  • الأسطر 46-50: إنشاء علامات التبويب الخمس للتطبيق؛
  • السطر 48: يتم توفير عناوين علامات التبويب بواسطة الطريقة الواردة في الأسطر 63-79؛
  • يتم إنشاء خمسة أجزاء في السطر 60. وبسبب التعليقات التوضيحية AA، فإن فئات الأجزاء هي تلك التي تم عرضها سابقًا مع إضافة شرطة سفلية في نهايتها؛
  • الأسطر 63-79: يتم تعريف عنوان لكل جزء. سيتم البحث عن هذه العناوين في الملف [res / values / strings.xml]
  

محتوى ملف [strings.xml] هو كما يلي:


<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- اسم التطبيق -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- المقاطع وعلامات التبويب -->
  <string name="config_titre">[Config]</string>
  <string name="blink_titre">[Blink]</string>
  <string name="pinread_titre">[PinRead]</string>
  <string name="pinwrite_titre">[PinWrite]</string>
  <string name="commands_titre">[Commands]</string>

</resources>

المهمة: قم بإنشاء العناصر السابقة وقم بتجميع المشروع. يجب ألا تكون هناك أخطاء.


قم بتشغيل المشروع. يجب أن تحصل على العرض التالي على المحاكي:

Image

افحص السجلات التي رافقت عرض العرض الأول وتابع مسار الخطوات المختلفة التي تم تنفيذها. انتقل من علامة تبويب إلى أخرى واستمر في متابعة السجلات.

5.6.9. الطريقة XML [config]

سيكون العرض XML [config] كما يلي:

يتم الحصول على العرض أعلاه باستخدام الرمز XML التالي:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
      android:id="@+id/txt_TitreConfig"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentTop="true"
      android:layout_centerHorizontal="true"
      android:layout_marginTop="150dp"
      android:text="@string/txt_TitreConfig"
      android:textSize="@dimen/titre"/>

    <TextView
      android:id="@+id/txt_UrlServiceRest"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_TitreConfig"
      android:layout_marginTop="50dp"
      android:text="@string/txt_UrlServiceRest"
      android:textSize="20sp"/>

    <EditText
      android:id="@+id/edt_UrlServiceRest"
      android:layout_width="300dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_UrlServiceRest"
      android:layout_alignBottom="@+id/txt_UrlServiceRest"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_UrlServiceRest"
      android:ems="10"
      android:hint="@string/hint_UrlServiceRest"
      android:inputType="textUri">

      <requestFocus/>
    </EditText>

    <TextView
      android:id="@+id/txt_MsgErreurIpPort"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_UrlServiceRest"
      android:layout_marginTop="20dp"
      android:text="@string/txt_MsgErreurUrlServiceRest"
      android:textColor="@color/red"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_MsgErreurIpPort"
      android:layout_marginTop="40dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp"/>

    <Button
      android:id="@+id/btn_Rafraichir"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_rafraichir"/>

    <Button
      android:id="@+id/btn_Annuler"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_annuler"
      android:visibility="invisible"/>

    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>

  </RelativeLayout>
</ScrollView>

يستخدم العرض سلاسل أحرف (android:text في الأسطر 15 و25 و37 و50 و61 و73) محددة في الملف [res / values / strings]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">android-domotique</string>

    <!-- الأجزاء وعلامات التبويب -->
    <string name="config_titre">[Config]</string>
    <string name="blink_titre">[Blink]</string>
    <string name="pinread_titre">[PinRead]</string>
    <string name="pinwrite_titre">[PinWrite]</string>
    <string name="commands_titre">[Commands]</string>

    <!-- التكوين -->
    <string name="txt_TitreConfig">Se connecter au serveur</string>
    <string name="txt_UrlServiceRest">Url du service web / jSON</string>
    <string name="txt_MsgErreurUrlServiceRest">L\'Url du service doit être entrée sous la forme Ip1.Ip2.Ip3.IP4:Port/contexte</string>
    <string name="hint_UrlServiceRest">ex (192.168.1.120:8080/rest)</string>
    <string name="btn_annuler">Annuler</string>
    <string name="btn_rafraichir">Rafraîchir</string>
    <string name="titre_list_arduinos">Liste des Arduinos connectés</string>
    
</resources>

تستخدم طريقة العرض ألوانًا (android:textColor في الأسطر 51 و62) محددة في الملف [res / values / colors]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <color name="floral_white">#FFFAF0</color>
  <!-- التطبيق -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
</resources>

تستخدم طريقة العرض أبعادًا (android:textSize في السطر 16) محددة في الملف [res / values / dimens]:

  

<resources>
  <!-- هوامش الشاشة الافتراضية، وفقًا لإرشادات تصميم Android. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
  <!-- التطبيق -->
  <dimen name="titre">30dp</dimen>
</resources>

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


المهمة: قم بإنشاء العناصر السابقة.


قم بتشغيل مشروعك مرة أخرى. يجب أن تحصل على العرض التالي:

Image

5.6.10. الجزء [ConfigFragment]

  

لإدارة العرض الجديد [config]، يتطور كود الجزء [ConfigFragment] على النحو التالي:


package client.android.fragments.behavior;

import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {

  // عناصر الواجهة المرئية
  @ViewById(R.id.btn_Rafraichir)
  protected Button btnRafraichir;
  @ViewById(R.id.btn_Annuler)
  protected Button btnAnnuler;
  @ViewById(R.id.edt_UrlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_MsgErreurIpPort)
  protected TextView txtMsgErreurUrlServiceRest;
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  }

  // إدارة دورة حياة الجزء -------------------------------------

  @Override
  public CoreState saveFragment() {
    return new ConfigFragmentState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_CONFIG;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // زيارة أولى؟
    if(previousState==null){
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }

  @Override
  protected void notifyEndOfUpdates() {
    // الأزرار
    initButtons();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }

  // الأساليب الخاصة --------------------------------------------

  private void initButtons() {
    // الزر [Exécuter] يحل محل الزر [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
}
  • الأسطر 23-32: عناصر الواجهة المرئية؛
  • الأسطر 58-60: عند الزيارة الأولى للجزء، يتم إخفاء رسالة الخطأ؛
  • الأسطر 73-76: في كل مرة يتم فيها عرض المقطع، يتم إخفاء الزر [Annuler] (السطر 82) ويتم عرض الزر [Rafraîchir] (الأسطر 86-87). في الواقع، في هذا التطبيق، لا يمكن عرض جزء أثناء إجراء عملية غير متزامنة، وبالتالي يكون الزر [Annuler] مرئيًا؛

المهمة: قم بإنشاء العناصر السابقة.


قم بتشغيل هذه النسخة الجديدة. يجب أن تكون الشاشة الأولى الآن كما يلي:

Image

5.6.10.1. الزر [Rafraîchir]

سنقوم في الوقت الحالي بإدارة النقر على الزر [Rafraîchir] بالطريقة التالية:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // سنقوم بتشغيل مهمة - نستعد للانتظار
    beginWaiting(1);
  }

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // نقوم بإلغاء المهام غير المتزامنة
    cancelRunningTasks();
  }

  protected void beginWaiting(int numberOfRunningTasks) {
    // نقوم بإعداد انتظار المهام
    beginRunningTasks(numberOfRunningTasks);
    // الزر [Annuler] يحل محل الزر [Rafraîchir]
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // إدارة دورة حياة الجزء -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // الأزرار في حالتها الأولية
    initButtons();
  }

  // طرق خاصة --------------------------------------------

  private void initButtons() {
    // الزر [Exécuter] يحل محل الزر [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • الأسطر 1-5: الطريقة التي يتم تنفيذها عند النقر على الزر [Rafraîchir
  • السطر 4: نبدأ الانتظار؛
  • السطر 18: نمرر إلى الفئة الأم عدد المهام غير المتزامنة التي سنقوم بتشغيلها. ستظهر صورة الانتظار؛
  • السطور 20-21: سيؤدي هذا الانتظار إلى ظهور الزر [Annuler]، واختفاء الزر [Rafraîchir]، وظهور صورة الانتظار. لا يحدث أي شيء آخر. ومع ذلك، يمكن للمستخدم النقر على الزر [Annuler]. عندئذٍ سيتم تنفيذ الطريقة الواردة في الأسطر 7-14؛
  • السطر 13: يُطلب من الفئة الأم إلغاء جميع المهام. ستقوم الفئة بذلك وستستدعي بدورها الطريقة الموجودة في الأسطر 25-29 للإشارة إلى أن جميع المهام قد اكتملت. سيكون للمعلمة [runningTasksHaveBeenCanceled] القيمة true للإشارة إلى أنه تم إلغاء المهام؛
  • السطور 35-36: سيختفي الزر [Annuler] بينما سيعود الزر [Rafraîchir] للظهور.

المهمة: قم بإجراء هذه التعديلات ثم قم بتنفيذ المشروع. تحقق من أن الزر [Rafraîchir] يبدأ عملية الانتظار وأن الزر [Annuler] يوقفها. راقب السجلات.


5.6.10.2. التحقق من المدخلات

في الإصدار السابق، لم نكن نتحقق من صحة الإدخال. للتحقق من ذلك، نضيف الكود التالي في [ConfigFragment]:


// القيم المدخلة
  private String urlServiceRest;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // يتم التحقق من الإدخالات
    if (!pageValid()) {
      return;
    }
    // سيتم تشغيل مهمة - يتم التحضير للانتظار
    beginWaiting(1);
  }

  // التحقق من المدخلات
  private boolean pageValid() {
    // في البداية لا توجد رسالة خطأ
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // يتم استرداد عنوان IP ومنفذ الخادم
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // التحقق من صحتها
    try {
      URI uri = new URI(urlServiceRest);
      String host = uri.getHost();
      int port = uri.getPort();
      if (host == null || port == -1) {
        throw new Exception();
      }
    } catch (Exception ex) {
      // عرض رسالة خطأ
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // العودة إلى UI
      return false;
    }
    // تم بنجاح
    return true;
  }
  • السطر 2: الإدخال URL؛
  • الأسطر 7-9: قبل القيام بأي شيء، نتحقق من صحة الإدخالات؛
  • السطر 19: نسترد القيمة المدخلة URL ونضيف إليها البادئة [http://
  • السطر 22: نحاول إنشاء كائن URI (معرف الموارد الموحد) باستخدامه. إذا كان URL المدخل غير صحيح من الناحية النحوية، فسيحدث استثناء؛
  • الأسطر 23-27: يتم إنشاء استثناء إذا كان URI صحيحًا ولكن لدينا مع ذلك [host==null] و [port==-1]. هذه حالة محتملة؛
  • السطر 30: حدثت استثناء. يتم عرض رسالة الخطأ؛
  • السطر 32: يتم إرجاع [false] للإشارة إلى أن الصفحة غير صالحة؛
  • السطر 35: لم تحدث أخطاء. يتم إرجاع [true] للإشارة إلى أن الصفحة صالحة؛

المهمة: قم بإنشاء العناصر السابقة.


اختبر هذه النسخة الجديدة وتأكد من الإبلاغ عن URL غير الصالحة.

5.6.10.3. عرض قائمة أجهزة Arduino

  

ستحتاج العروض المختلفة إلى عرض قائمة أجهزة Arduino المتصلة. وللقيام بذلك، سنقوم بتعريف فئات مختلفة وعرض XML:

  • سيتم تمثيل جهاز Arduino بالفئة [Arduino] [1]؛
  • الفئة [CheckedArduino] [1] موروثة من الفئة [Arduino] التي أضفنا إليها متغيرًا منطقيًا لمعرفة ما إذا تم تحديد جهاز Arduino أم لا في قائمة؛

الفئة [Arduino] هي تلك التي يستخدمها الخادم بالفعل والمقدمة في الفقرة 5.4.2. وهي كما يلي:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // البيانات
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// مُستقبلات ومُعيّنات
...
}
  • السطر 7: [id] هو معرف Arduino؛
  • السطر 8: وصفه؛
  • السطر 9: عنوانه MAC؛
  • السطر 10: عنوانه IP؛
  • السطر 11: المنفذ الذي ينتظر عليه الأوامر؛

تتوافق هذه الفئة مع السلسلة jSON المستلمة من الخادم عند طلب قائمة أجهزة أردوينو المتصلة:

تتوارث الفئة [CheckedArduino] عن الفئة [Arduino]:


package android.arduinos.entities;

public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // يمكن اختيار Arduino
    private boolean isChecked;

    // المنشئ
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // الأصل
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // محلي
        this.isChecked = isChecked;
    }

    // مُستردات ومُعيّنات
    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }

}
  • السطر 3: الفئة [CheckedArduino] ترث الفئة [Arduino
  • السطر 6: نضيف إليه متغيرًا منطقيًا سيُستخدم لمعرفة ما إذا كان قد تم تحديد جهاز Arduino أم لا في قائمة أجهزة Arduino المعروضة؛

في [ConfigFragment]، سنقوم بمحاكاة الحصول على قائمة أجهزة Arduino المتصلة.

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // التحقق من المدخلات
    if (!pageValid()) {
      return;
    }
    // سنقوم بتشغيل مهمة - نستعد للانتظار
    beginWaiting(1);
    // تنظيف قائمة أجهزة Arduino
    clearArduinos();
    // طلب قائمة أجهزة Arduino في مهمة خلفية
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
   ...
  }

  // إعادة تعيين قائمة أجهزة Arduino
  private void clearArduinos() {
    // يتم إنشاء قائمة فارغة
    List<String> strings = new ArrayList<>();
    // عرضها
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • السطر 2: ListView الذي يعرض أجهزة Arduino المتصلة بالخادم؛
  • السطر 5: الطريقة التي تطلب قائمة أجهزة Arduino المتصلة؛
  • السطر 11: يتم إعلام الفئة الأم بأننا سنقوم بتشغيل مهمة غير متزامنة؛
  • السطر 12: نمسح قائمة أجهزة Arduino المعروضة حاليًا؛
  • السطر 15: نطلب في مهمة خلفية قائمة أجهزة Arduino المتصلة؛
  • الأسطر 23-28: الطريقة التي تمسح قائمة أجهزة Arduino المعروضة حاليًا؛

الطريقة [getArduinosInBackground] هي كما يلي:


  private void getArduinosInBackground() {
    // يتم إنشاء قائمة وهمية بأجهزة Arduino
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // محاكاة استجابة من الخادم
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // إلغاء الانتظار
    cancelWaitingTasks();
    // يتم تغيير الأزرار
    initButtons();
    // استهلاك الاستجابة
    consumeArduinosResponse(response);
}
  • الأسطر 3-6: يتم إنشاء قائمة تضم 20 جهاز Arduino؛
  • الأسطر 8-9: يتم إنشاء استجابة من النوع [Response<List<Arduino>>] (الفقرة 5.4.2) التي ستضم قائمة أجهزة Arduino التي تم إنشاؤها؛
  • السطر 11: إلغاء الانتظار؛
  • السطر 13: إعادة الأزرار إلى حالتها الأولية؛
  • السطر 15: يتم استهلاك الرد؛

الطريقة [consumeArduinosResponse] هي كما يلي:


  // عرض الاستجابة
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // خطأ؟
    if (response.getStatus() != 0) {
      // العرض
      showAlert(response.getMessages());
      // العودة إلى واجهة المستخدم
      return;
    }
    // يتم إنشاء قائمة من [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // يتم عرضها
    showArduinos(checkedArduinos);
}
  • الأسطر 4-11: يتم فحص رمز الخطأ في الرد المرسل من الخادم:
  • السطر 4: إذا كان رمز الخطأ مختلفًا عن الصفر؛
  • السطر 6: يتم عرض الرسائل المخزنة من قبل الخادم في الحقل [messages] من الرد؛
  • السطر 8: نعود إلى واجهة المستخدم؛
  • الأسطر 11-16: إذا لم تكن هناك أخطاء، يتم عرض قائمة أجهزة Arduino المستلمة، بعد تحويلها إلى نوع List<CheckedArduino>؛

الطريقة [showArduinos] هي كما يلي:


  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // يتم إنشاء قائمة من String من قائمة أجهزة Arduino
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // يتم عرضها
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

المهمة: قم بإجراء التعديلات السابقة وقم بتشغيل مشروعك.


يجب أن تحصل على العرض التالي عند النقر على الزر [Rafraîchir]:

Image

لا يتم استخدام الإدخال في [1]. لذا يمكنك إدخال أي شيء طالما أنه يتوافق مع التنسيق المتوقع.

5.6.10.4. نموذج لعرض Arduino

في الوقت الحالي، يتم عرض أجهزة Arduino المتصلة في العرض [Config] بالطريقة التالية:

Image

نريد الآن عرضها على النحو التالي:

Image

  • في [1]، مربع اختيار يسمح بتحديد جهاز Arduino. سيتم إخفاء مربع الاختيار هذا عندما نرغب في عرض قائمة بأجهزة Arduino غير قابلة للتحديد؛
  • في [2]، معرف Arduino؛
  • في [3]، وصفه؛

ما يلي يستند إلى المفاهيم التي تم تطويرها في المشاريع [exemple-19] و [exemple-19B] في الفقرة 1.20. راجعها إذا لزم الأمر.

نبدأ أولاً بإنشاء العرض الذي سيعرض عنصرًا من قائمة أجهزة Arduino:

 

رمز العرض [listarduinos_item] أعلاه هو التالي:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat"
    android:orientation="vertical" >

    <CheckBox
        android:id="@+id/checkBoxArduino"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/txt_arduino_description" />

    <TextView
        android:id="@+id/TextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_marginLeft="40dp"
        android:text="@string/txt_arduino_id" />

    <TextView
        android:id="@+id/txt_arduino_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/TextView1"
        android:text="@string/dummy"
        android:textColor="@color/blue" />

    <TextView
        android:id="@+id/TextView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_arduino_id"
        android:text="@string/txt_arduino_description" />

    <TextView
        android:id="@+id/txt_arduino_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignTop="@+id/TextView2"
        android:layout_toRightOf="@+id/TextView2"
        android:text="@string/dummy"
        android:textColor="@color/blue" />

</RelativeLayout>
  • الأسطر 9-15: مربع الاختيار؛
  • الأسطر 17-23: النص [Id : ]؛
  • الأسطر 25-33: سيتم إدخال معرف Arduino هنا؛
  • الأسطر 35-43: النص [Description : ]؛
  • الأسطر 45-53: سيتم إدخال وصف Arduino هنا؛

تستخدم هذه العرض نصوصًا (الأسطر 23 و32 و43) محددة في [res / values / strings.xml]:


    <string name="dummy">XXXXX</string>

    <!--  listarduinos_item -->
    <string name="txt_arduino_id">Id : </string>
<string name="txt_arduino_description">Description : </string>

تستخدم طريقة العرض أيضًا لونًا (الأسطر 33، 53) محددًا في [res / values / colors.xml]:


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>

</resources>

مدير عرض عنصر من قائمة أجهزة Arduino

  

الفئة [ListArduinosAdapter] هي الفئة التي يستدعيها [ListView] لعرض كل عنصر من عناصر قائمة أردوينو. ورمزها هو كما يلي:


package istia.st.android.vues;

import istia.st.android.R;
...

public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {

    // جدول أردوينو
    private List<CheckedArduino> arduinos;
    // سياق التنفيذ
    private Context context;
    // معرف تخطيط عرض سطر من قائمة أردوينو
    private int layoutResourceId;
    // ما إذا كان السطر يحتوي على مربع اختيار أم لا
    private Boolean selectable;

    // المنشئ
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // الأصل
        super(context, layoutResourceId, arduinos);
        // يتم حفظ المعلومات
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • السطر 18: يقبل منشئ الفئة أربعة معلمات: النشاط قيد التنفيذ، ومعرف العرض المراد عرضه لكل عنصر من عناصر مصدر البيانات، ومصدر البيانات الذي يغذي القائمة، وقيمة منطقية تشير إلى ما إذا كان يجب عرض مربع الاختيار المرتبط بكل جهاز Arduino أم لا؛
  • الأسطر 8-15: يتم تخزين هذه المعلومات الأربع محليًا؛

السطر 29، تتولى الطريقة [getView] مهمة إنشاء العرض رقم [position] في [ListView] وإدارة أحداثه. وفيما يلي كودها:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // أردوينو الحالي
        final CheckedArduino arduino = arduinos.get(position);
        // يتم إنشاء السطر الحالي
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // استرداد المراجع على [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // ملء السطر
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // CheckBox ليس مرئيًا دائمًا
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // نقوم بتعيين قيمته
            ck.setChecked(arduino.isChecked());
            // نقوم بإدارة النقر
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // نقوم بعرض السطر
        return row;
    }
  • السطر 2: المعلمة الأولى هي الموضع في [ListView] للسطر المراد إنشاؤه. وهو أيضًا الموضع في قائمة أجهزة Arduino المخزنة محليًا؛
  • السطر 4: يتم استرداد مرجع على Arduino الذي سيتم ربطه بالسطر الذي تم إنشاؤه؛
  • السطر 6: يتم إنشاء السطر الحالي من عرض [listarduinos_item.xml
  • السطران 8-9: يتم استرداد المراجع الخاصة بـ [TextView
  • السطران 11-12: يتم تعيين القيمتين لـ [TextView
  • السطر 14: يتم استرداد مرجع على مربع الاختيار؛
  • السطر 15: يتم جعلها مرئية أو غير مرئية، وفقًا لقيمة [selectable] التي تم تمريرها مبدئيًا إلى المنشئ؛
  • السطر 16: إذا كانت خانة الاختيار موجودة؛
  • السطر 18: يتم تعيين القيمة [isChecked] الخاصة بـ Arduino الحالي لها؛
  • الأسطر 20-26: إدارة النقر على مربع الاختيار؛
  • السطر 23: يتم تخزين قيمة مربع الاختيار في Arduino الحالي؛

إدارة قائمة أجهزة Arduino

يتم إدارة عرض قائمة أجهزة Arduino حاليًا بواسطة طريقتين من فئة [ConfigFragment]:

  • [clearArduinos]: التي تعرض قائمة فارغة؛
  • [showArduinos]: تعرض القائمة التي يرسلها الخادم؛

تتطور هاتان الطريقتان على النحو التالي:


  // إعادة تعيين قائمة أجهزة Arduino
  private void clearArduinos() {
    // يتم عرض قائمة فارغة
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }

  // عرض قائمة أجهزة Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // عرض أجهزة Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

المهمة: قم بإجراء هذه التعديلات واختبر التطبيق الجديد.


Image

5.6.10.5. الجلسة

الجلسة هي المكان الذي نضع فيه المعلومات التي تشاركها الأجزاء والنشاط. تحتاج جميع الأجزاء إلى عرض قائمة أجهزة Arduino المتصلة. لذا، ستكون النسخة الأولى من الجلسة كما يلي:


package client.android.architecture.custom;

import client.android.activity.CheckedArduino;
import client.android.architecture.core.AbstractSession;

import java.util.ArrayList;
import java.util.List;

public class Session extends AbstractSession {
  // البيانات المراد مشاركتها بين الأجزاء نفسها وبين الأجزاء والنشاط
  // العناصر التي لا يمكن تسلسلها في jSON يجب أن تحتوي على التعليق التوضيحي @JsonIgnore
  // لا تنسَ استخدام getters و setters اللازمة للتسلسل / إلغاء التسلسل jSON

  // قائمة أجهزة Arduino
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();

  // الوصول والضبط
...
}

المهمة: قم بإنشاء الفئة [Session] السابقة.


يؤدي إنشاء هذه الجلسة إلى تعديل الكود المكتوب بالفعل على النحو التالي:


  // عرض الرد
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // خطأ؟
    if (response.getStatus() != 0) {
      // عرض
      showAlert(response.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى واجهة المستخدم
      return;
    }
    // يتم إنشاء قائمة من [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // يتم وضعها في الجلسة
    session.setCheckedArduinos(checkedArduinos);
    // عرضها
    showArduinos(checkedArduinos);
    // يتم إلغاء الانتظار
    cancelWaitingTasks();
}
  • السطر 18: يتم وضع قائمة أجهزة Arduino التي تم إنشاؤها في الأسطر السابقة في الجلسة؛

5.6.10.6. إدارة حالة الجزء

عند تدوير الجهاز، يتم عرض المكونات المرئية للطريقة (افتراضيًا) في الحالة التي كانت عليها عند تصميم الطريقة:

  • يحتوي [ListView] على العناصر التي وضعها المصمم فيه؛
  • تكون رسالة الخطأ في الحالة المرئية أو غير المرئية التي وضعها فيها المصمم؛

قد تكون حالات المكونات المرئية عند التصميم مناسبة أو غير مناسبة عند استعادة جزء. ماذا عن هذا؟

  • يجب أن يعرض [ListView] قائمة بأجهزة Arduino المتصلة. وبالتالي، لا يمكن استخدام قيمة [ListView] عند التصميم؛
  • يجب استعادة [TextView] من رسالة الخطأ في الحالة المرئية أو غير المرئية التي كان عليها عند الحفظ. لا يمكن أن تكون قيمته في التصميم مناسبة لهاتين الحالتين؛

لذلك، يتعين علينا حفظ حالة هذين المكونين عند حفظ حالة الجزء:

  • قائمة أجهزة Arduino المتصلة؛
  • رؤية (معروض / مخفي) رسالة الخطأ عند إدخال URL لخدمة الويب / jSON؛

نظرًا لوجود قائمة أجهزة Arduino في الجلسة، فسيتم حفظها تلقائيًا. سيتم حفظ رؤية رسالة الخطأ في الفئة [ConfigFragmentState] التالية:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // إظهار رسالة الخطأ
  private boolean txtMsgErreurUrlServiceRestVisible;

  // المُستردات والمُعيّنات
...
}

المهمة: قم بإنشاء الفئة [ConfigFragmentState] السابقة.


لاستعادة حالات الأجزاء بشكل صحيح، يجب تعديل طريقتيها [getNumView] و [saveFragment]. على سبيل المثال، طريقة الجزء [BlinkFragment] هي حاليًا كما يلي:


  @Override
  public CoreState saveFragment() {
    // يجب حفظ الجزء
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // إذالا يوجد شيء للحفظ، قم بتنفيذ [return new CoreState();] وحذف الفئة [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // يجب إعادة رقم الجزء إلى جدول الأجزاء التي تديرها النشاط (انظر MainActivity)
    return 0;
}

إذا لم يتم اتخاذ أي إجراء، فسيتم حفظ الحالة المعروضة في السطر 6 في العنصر 0 (السطر 13) من المصفوفة CoreState[] coreStates للفئة [AbstractSession] (السطر 5 أدناه):


public class AbstractSession implements ISession {
  ...

  // حالة المشاهدات
  private CoreState[] coreStates = new CoreState[0];
...

ولكن يجب حفظه في العنصر المطابق لرقم الجزء [BlinkFragment] في جدول الأجزاء المحددة في الفئة [MainActivity] (السطر 9 أدناه):


@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  ...

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }


تم تعريف أرقام الأجزاء في الواجهة [IMainActivity]:


public interface IMainActivity extends IDao {

  ...

  // أرقام المشاهدات
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

في النهاية، سيتم إدارة حالة الجزء [BlinkFragment] بشكل صحيح إذا كتبنا:


  @Override
  public CoreState saveFragment() {
    // يجب حفظ الجزء
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // إذالا يوجد شيء للحفظ، قم بتنفيذ [return new CoreState();] وحذف الفئة [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // يجب إعادة رقم الجزء إلى جدول الأجزاء التي تديرها النشاط (انظر MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • السطر 14: يتم إرجاع رقم الجزء [BlinkFragment] في جدول الأجزاء التي تديرها النشاط؛

من ناحية أخرى، فإن الفئة [CoreState] الأم لحالات الأجزاء هي في الوقت الحالي كما يلي (انظر الفقرة 5.6.7.2):


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // جزء تمت زيارته أم لا
  protected boolean hasBeenVisited = false;
  // حالة القائمة المحتملة للجزء
  protected MenuItemState[] menuOptionsState;

  // أدوات الاسترجاع والتعيين
....
}
  • السطور 12-16: لا تظهر الفئة [DummyFragmentState] في قائمة الفئات الفرعية للفئة [CoreState]. ومع ذلك، فإن الطريقة [saveFragment] للفئة [BlinkFragment] تُرجع حاليًا نوعًا [ DummyFragmentState]. إذا تركنا الأمور على حالها، فسوف تفشل عملية التسلسل/إلغاء التسلسل للجلسة ولن يتم استعادة الجلسة، مما يؤدي إلى تعطل التطبيق؛

يجب إعادة كتابة الطريقة [saveFragment] الخاصة بالجزء [BlinkFragment] على النحو التالي:


  @Override
  public CoreState saveFragment() {
    // يجب حفظ الجزء
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // إذالا يوجد شيء للحفظ، قم بتنفيذ [return new CoreState();] وحذف الفئة [DummyFragmentState]
}

المهمة: في كل جزء، قم بتعديل الطريقة [getNumView] بحيث تعرض رقم الجزء، والطريقة [saveFragment] بحيث تعرض مثيلًا لفئة حالة الجزء (كما هو موضح أعلاه).


5.6.10.7. إدارة دورة حياة المقطع

نحن مهتمون هنا بدورة حياة المقطع [ConfigFragment]، ولا سيما الطرق الأربع التالية:

  • [saveFragment]: يجب أن تحفظ حالة الجزء حتى يمكن استعادتها لاحقًا؛
  • [initFragment]: يجب أن تقوم بتهيئة بعض حقول الجزء إذا لزم الأمر. يتم استدعاء هذه الطريقة عند بدء تشغيل التطبيق وفي كل مرة يتم فيها تدوير الجهاز. وبشكل أكثر دقة، يتم استدعاؤها عندما يصبح الجزء مرئيًا بعد أحد الحدثين السابقين؛
  • [initView]: التي يجب أن تقوم بتهيئة بعض مكونات العرض إذا لزم الأمر. يتم استدعاء هذه الطريقة في كل مرة يتم فيها استدعاء [initFragment] وعندما يتعين إعادة إنشاء العرض لأن المقطع قد خرج، في وقت ما، من المنطقة المجاورة للمقطع المعروض. وكما سبق، يتم استدعاؤها عندما يصبح الجزء مرئيًا بعد أحد هذه الأحداث؛
  • [updateOnRestore]: يتم تنفيذها بعد الطريقتين السابقتين عند حدوث دوران للجهاز وكذلك عند حدوث تنقل. وتتمثل مهمتها في استعادة الحالة السابقة للجزء؛

وستكون هذه الطرق كما يلي:


// محول قائمة أجهزة Arduino
  private ListArduinosAdapter adapterListArduinos;

...
  // إدارة دورة حياة المقتطف -------------------------------------

  @Override
  public CoreState saveFragment() {
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // محول listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // ربط قائمة العرض / المحول
    listArduinos.setAdapter(adapterListArduinos);
    // زيارة أولى؟
    if (previousState == null) {
      // ListView فارغ - تم إنشاؤه بواسطة [initFragment]
      // رسالة خطأ مخفية
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // إعادة عرض رسالة الخطأ
      ConfigFragmentState state = (ConfigFragmentState) previousState;
      txtMsgErreurUrlServiceRest.setVisibility(state.isTxtMsgErreurUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }


  @Override
  protected void notifyEndOfUpdates() {
    // أزرار
    initButtons();
}
  • السطر 2: محول ListView لأجهزة Arduino. وهو متغير عام لأنه يُستخدم في طرق مختلفة؛
  • الأسطر 7-12: الطريقة [saveFragment] تحفظ في نوع [ConfigFragmentState] رؤية TextView txtMsgErreurUrlServiceRestVisible (السطر 10)؛
  • الأسطر 14-19: تقوم الطريقة [initFragment] بتهيئة محول السطر 2 بقائمة أجهزة Arduino الموجودة في الجلسة (السطر 17). يُذكر أن دور [initFragment] هو تهيئة حقول الجزء. هنا، يجب إجراء هذه التهيئة في جميع الحالات، سواء كانت الزيارة الأولى (previousState==null) أم لا؛
  • السطر 17: نرى أن المحول مرتبط بمصدر البيانات [session.getCheckedArduinos]. يجب ألا يكون لهذا المصدر القيمة null. لهذا السبب، يتم تهيئة الحقل [session.checkedArduinos] بقائمة فارغة في الجلسة:

  // قائمة أجهزة Arduino
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • الأسطر 21-35: تتمثل وظيفة الطريقة [initView] في تهيئة بعض مكونات الواجهة المرئية، ولا سيما تلك التي لا يتم الاحتفاظ بقيمتها عند تدوير الجهاز؛
  • السطر 24: يتم ربط ListView الخاص بأجهزة Arduino بمحول السطر 2؛
  • الأسطر 28-32: يتم تمييز الزيارة الأولى عن الزيارات الأخرى؛
  • السطر 29: عند الزيارة الأولى، يجب عرض [ListView] فارغًا. وهذا هو الحال لأن محول [ListView] قد تم ربطه بقائمة فارغة (السطر 17) عند الزيارة الأولى؛
  • السطر 31: يتم إخفاء رسالة الخطأ؛
  • الأسطر 32-36: الحالة التي لا تكون فيها هذه هي الزيارة الأولى؛
  • [ListView] في حالة جيدة بالفعل منذ السطر 24. لا يوجد شيء آخر يمكن القيام به؛
  • السطور 34-35: يتم استعادة رسالة الخطأ إلى الحالة التي كانت عليها عند آخر حفظ للجزء؛
  • الأسطر 31-36: يجب أن تعيد الطريقة [updateOnRestore] الجزء إلى حالته الأولية. يمكن الوصول إلى الطريقة [updateOnRestore] بطريقتين:
    • إما بسبب دوران الجهاز. في هذه الحالة، تم إجراء جميع عمليات التهيئة في [initView
    • إما بسبب الانتقال من علامة تبويب إلى علامة التبويب [Config]. إذا خرج الجزء [Config] من منطقة الجوار للأجزاء المعروضة منذ مغادرته، فإن الطريقة [initView] قد تم تنفيذها بالفعل ويكون الجزء في الحالة المطلوبة. إذا لم يخرج الجزء [Config] من نطاق الأجزاء المعروضة منذ مغادرته، فإن مكوناته المرئية لم تتغير حالتها ولا يوجد ما يجب فعله؛

نرى أن الطريقة [updateOnRestore] ليس لديها ما تفعله. هذا هو الحال أحيانًا، وأحيانًا لا. يأتي الاختلاف من الطريقة [updateOnSubmit]: إذا قامت هذه الطريقة بعمل شيء يجعل بعض عمليات التهيئة التي تمت في [initView] غير ضرورية، فيجب إجراء هذه التهيئة في الطريقة [updateOnRestore]. لنأخذ مثالاً على زر اختيار ذي ثلاث قيم: V1، V2، V3. ربما في حالة التنقل المرتبط بإجراء [SUBMIT]، يجب أن يكون زر الاختيار المحدد دائمًا هو الذي يحمل القيمة V1. في هذه الحالة، لا فائدة من استعادة قيمة زر الاختيار في الطريقة [initView]، لأنه في حالة [SUBMIT]، سيتم استبدال هذه القيمة بالقيمة التي تحددها الطريقة [updateOnSubmit]. لذلك، من الأفضل نقل عملية الاستعادة هذه إلى الطريقة [updateOnRestore] لتجنب إجراء عملية غير ضرورية في بعض الأحيان.

  • الأسطر 48-52: يتم تنفيذ الطريقة [notifyEndOfUpdates] بعد جميع الطرق السابقة؛
  • السطر 51: يتم إعادة الأزرار إلى حالتها الأولية: زر [Rafraîchir] معروض، زر [Annuler] مخفي:

المهمة: أضف الكود السابق إلى [ConfigFragment] ثم قم بتشغيل التطبيق. لاحظ أنه عند تدوير الجهاز، تحتفظ علامة التبويب [Config] بحالتها (رسالة الخطأ، قائمة أجهزة Arduino). تأكد من أن الأمر نفسه يحدث عند التنقل ببساطة من علامة التبويب [config] --> علامة التبويب [Commands] --> علامة التبويب [Config]. في الحالة الأخيرة، إذا احتفظت في [IMainActivity] بجوار شظايا 1، فسيتم إتلاف عرض الجزء [ConfigFragment] عند الانتقال إلى علامة التبويب [Commands] ثم إعادة إنشائه عند العودة إلى علامة التبويب [Config]. أثناء الاختبارات، راجع السجلات.


5.6.10.8. تحسين الكود

يمكن تحسين كود الجزء [ConfigFragment]. على سبيل المثال، كتبنا:


// محول قائمة أجهزة Arduino
  private ListArduinosAdapter adapterListArduinos;

...

  // عرض قائمة أجهزة Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // عرض أجهزة Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }

  // مسح قائمة أجهزة Arduino
  private void clearArduinos() {
    // عرض قائمة فارغة
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • نلاحظ في السطرين 9 و16 استخدام متغير محلي غير مرتبط بحقل السطر 2، في حين أننا نريد معالجة الكيان نفسه؛

نقوم بتطوير الكود بالطريقة التالية:


  // محول قائمة أجهزة Arduino
  private ListArduinosAdapter adapterListArduinos;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  ...
  }

  private void getArduinosInBackground() {
 ...
    // استهلاكها
    consumeArduinosResponse(response);
  }

  // عرض الرد
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // خطأ؟
    if (response.getStatus() != 0) {
      // العرض
      showAlert(response.getMessages());
      // إلغاء
      doAnnuler();
      // العودة إلى واجهة المستخدم
      return;
    }
    // يتم إنشاء قائمة من [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // عرضها
    adapterListArduinos.notifyDataSetChanged();
    // إلغاء الانتظار
    cancelWaitingTasks();
}
  
  @Override
  protected void initFragment(CoreState previousState) {
    // محول listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // ربط قائمة العرض / المحول
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • عند تنفيذ الطريقة في السطر 5، تم تنفيذ دورة حياة الجزء. وبالتالي:
    • تم ربط محول السطر 2 بمصدر البيانات الخاص به (السطر 41)؛
    • تم ربط [ListView] الخاص بأجهزة Arduino المتصلة بهذا المحول (السطر 48)؛

عندما نريد تغيير عرض [ListView]، يجب القيام بأمرين:

  • تغيير محتوى مصدر البيانات [session.checkedArduinos
  • الإبلاغ عن هذا التغيير للمحول بواسطة الأمر [adapterListArduinos.notifyDataSetChanged()]؛

يتعلق الأمر بتغيير محتوى مصدر البيانات وليس مصدر البيانات نفسه. إذا قمنا بتغيير مصدر البيانات نفسه، فستستمر العملية [adapterListArduinos.notifyDataSetChanged()] في عرض مصدر البيانات القديم. وسيكون من الضروري عندئذ ربط المحول بمصدر البيانات الجديد.

الرمز هو كما يلي:

  • السطر 27: نسترد مصدر البيانات؛
  • السطر 28: نقوم بإفراغها. ولهذا السبب، قمنا بحذف الطريقة [clearArduinos
  • الأسطر 29-31: في هذه القائمة التي أصبحت فارغة، نضيف عناصر جديدة؛
  • السطر 33: يُطلب من المُحول أن يُحدَّث. سيؤدي ذلك إلى تحديث عرض [ListView] المرتبط؛

المهمة: قم بإجراء هذه التعديلات وتحقق من أن تطبيقك لا يزال يعمل.


5.6.11. التواصل بين العروض

للتحقق من الاتصال بين العروض، سنقوم بعرض قائمة أجهزة Arduino التي تم الحصول عليها من خلال العرض [Config] على جميع العروض الأخرى. لنبدأ بالعرض [blink.xml]. في حين أنه لم يكن يعرض أي شيء، فإنه سيعرض الآن قائمة أجهزة Arduino المتصلة:

Image

 

سيكون الرمز XML للطريقة [blink.xml] كما يلي:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <?xml version="1.0" encoding="utf-8"?>
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:id="@+id/RelativeLayout1"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">

    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_marginTop="150dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp" />

    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>

  </RelativeLayout>
</ScrollView>

تم أخذ هذا الرمز مباشرة من العرض [config.xml]. تم فقط تعديل الهامش العلوي للسطر 19.


المهمة: قم بنسخ هذا الرمز في العرض [commands.xml, pinread.xml, pinwrite.xml].


كما يتغير رمز الجزء [BlinkFragment] المرتبط بالعرض [blink.xml]:

  

  // المكونات المرئية
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  // محول قائمة أجهزة Arduino
  private ListArduinosAdapter adapterListArduinos;
...

  // الأساليب التي تفرضها الفئة الأم -------------------------------------------------------

...
  @Override
  protected void initFragment(CoreState previousState) {
    // محول listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);

  }

  @Override
  protected void initView(CoreState previousState) {
    // الربط بين قائمة العرض والمحول
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • السطران 2-3: المكون [ListView] لأجهزة Arduino المتصلة؛
  • السطر 6: محول هذا [ListView
  • السطور 12-23: كود طريقتي [initFragment] و [initView] هو نفسه المستخدم بالفعل في الجزء [ConfigFragment
  • السطر 15: عندما يتعين إعادة تعيين الجزء، يتم إعادة تعيين محول السطر 2 من خلال ربطه بقائمة أجهزة Arduino المخزنة في الجلسة. المعلمة الأخيرة [true] للمُنشئ [ListArduinosAdapter] تعني أننا نريد رؤية مربع اختيار بجانب كل جهاز Arduino؛
  • السطر 22: عندما يتعين إعادة تعيين عرض المقطع، نربط [ListView] لأجهزة Arduino المتصلة بمحول السطر 6؛

المهمة: قم بنسخ هذا الرمز في الأجزاء الأخرى [CommandsFragment, PinReadFragment, PinWriteFragment]. قم بتشغيل التطبيق ولاحظ الآن أن كل علامة تبويب تعرض قائمة بأجهزة Arduino المتصلة. لاحظ أيضًا أنه إذا قمت بتحديد أجهزة Arduino في علامة تبويب ما وانتقلت إلى علامة تبويب أخرى، فستجدها محددة في هذه الأخيرة.


ملاحظة: فيما يلي تفسير استمرار تحديد أجهزة Arduino. تم عرض الفئة [ListArduinosAdapter] في الفقرة 5.6.10.4. الرمز المرتبط بمربع الاختيار هو التالي:


        // أردوينو الحالي
        final CheckedArduino arduino = arduinos.get(position);
...
        // CheckBox ليس مرئيًا دائمًا
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // يتم تعيين قيمته
            ck.setChecked(arduino.isChecked());
            // إدارة النقر
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • الأسطر 11-15: إذا تم تحديد خانة في علامة التبويب X، يتم تعيين خاصية [checked] الخاصة بـ Arduino في السطر 2 إلى true (السطر 14)؛
  • عند الانتقال إلى علامة التبويب Y، يتم عرض [ListView] لأجهزة Arduino في هذه العلامة. في السطر 9، نرى أنه إذا تم تعيين خاصية [checked] الخاصة بـ Arduino في السطر 2 إلى true، فسيتم تحديد المربع [ck] في السطر 5؛

5.6.12. الطبقة [DAO]

ملاحظة: بالنسبة لهذا الجزء، راجع تنفيذ الطبقة [DAO] في المشروع [exemple-16B] (انظر الفقرة 2.8.3).

في الوقت الحالي، قمنا بإنشاء قائمة بأجهزة Arduino المتصلة يدويًا. سنقوم الآن بطلبها من خادم الويب / jSON. للقيام بذلك، سنقوم بإنشاء الطبقة [DAO]:

  

5.6.12.1. واجهة IDao

ستكون واجهة [IDao] للطبقة [DAO] كما يلي:


package client.android.dao.service;

import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import rx.Observable;

import java.util.List;

public interface IDao {
  // عنوان URL لخدمة الويب
  void setUrlServiceWebJson(String url);

  // المستخدم
  void setUser(String user, String mdp);

  // مهلة انتظار العميل
  void setTimeout(int timeout);

  // المصادقة الأساسية
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // وضع التصحيح
  void setDebugMode(boolean isDebugEnabled);

  // مهلة انتظار العميل بالمللي ثانية قبل الطلب
  void setDelay(int delay);

  // خاص ----------------------------------------
  // قائمة أجهزة أردوينو
  Observable<Response<List<Arduino>>> getArduinos();
}
  • الأسطر 11-26: هذه الأسطر موجودة بالفعل في واجهة [IDao] للمشروع النموذجي [client-android-skel
  • السطر 30: تسمح الطريقة [getArduinos] بالحصول على قائمة أجهزة Arduino المتصلة في شكل متغير قابل للمراقبة من النوع Observable<[Response<List<Arduino>>>]؛

يُذكر أن [Response<T>] هو نوع جميع الردود المرسلة من الخادم في شكل سلسلة jSON:


package client.android.dao.entities;

import java.util.List;

public class Response<T> {

    // ----------------- الخصائص
    // حالة العملية
    private int status;
    // رسائل الخطأ المحتملة
    private List<String> messages;
    // نص الرد
    private T body;

    // المنشئون
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // مُستردات ومُعيّنات
...
}

5.6.12.2. الواجهة [WebClient]

  

الواجهة [WebClient] هي واجهة توفر مكتبة AA تنفيذًا لها. ستكون هذه الواجهة كما يلي:


package client.android.dao.service;

import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // محدد --------------------------------------
  // قائمة أجهزة أردوينو
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • الأسطر 15-19: هذه الأسطر موجودة بشكل أساسي في واجهة [WebClient] للمشروع النموذجي [client-android-skel
  • السطر 23: URL للخادم الذي يسمح بالحصول على قائمة أجهزة Arduino باستخدام عملية GET. يُذكر أن هذا URL يُقاس بالنسبة إلى URL الجذر [RestClientRootUrl] في السطر 16؛
  • السطر 24: يعيد الخادم السلسلة jSON من النوع [Response<List<Arduino>>]. يتم تحويل هذه السلسلة jSON تلقائيًا إلى النوع [Response<List<Arduino>>] بفضل المحول jSON [MappingJackson2HttpMessageConverter] في السطر 15؛

5.6.12.3. الفئة [Dao]

تنفذ الفئة [Dao] الواجهة [IDao] بالطريقة التالية:


package client.android.dao.service;

import android.util.Log;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // عميل خدمة الويب
  @RestService
  protected WebClient webClient;
  // الأمان
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // مصنع RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // سجل
    Log.d(className, "afterInject");
    // يتم إنشاء restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // يتم تثبيت المحول jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // يتم تعيين restTemplate لعميل الويب
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // يتم تعيين URL لخدمة الويب
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // يتم تسجيل المستخدم في المعترض
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // تكوين المصنع
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // معترض المصادقة؟
    if (isBasicAuthentificationNeeded) {
      // يتم إضافة معترض المصادقة
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // طرق خاصة -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // تنفيذ IDao محدد -----------------------------------------------

  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // تشغيل عميل الويب
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • الأسطر 19-87: هذه الأسطر أساسية في الفئة [Dao] للمشروع [client-android-skel
  • الأسطر 91-100: تنفيذ الطريقة [getArduinos
  • السطر 94: يتم استدعاء الطريقة [getResponse] من الفئة الأصلية. المعلمة الوحيدة لهذه الطريقة هي مثيل للواجهة [IRequest<T>]؛
  • الأسطر 95-99: الطريقة الوحيدة للواجهة [IRequest<T>] هي الطريقة [T getResponse()]؛
  • السطر 94: يجب أن يكون النوع T لـ [IRequest<T>] هو النوع T لنتيجة Observable<T> للطريقة في السطر 92، وبالتالي هنا، نوع [Response<List<Arduino>>]؛
  • السطر 97: تفوض الطريقة [IRequest.getResponse()] المهمة إلى الطريقة [webClient.getArduinos()] التي قدمناها. يتم إنشاء مثيل [webClient]، المحدد في السطر 24، بواسطة المكتبة AA وهو مثيل للواجهة [WebClient] التي عرضناها؛

5.6.13. النشاط [MainActivity]

  

لقد سبق أن عرضنا النشاط [MainActivity] في الفقرة 5.6.8. وهي توسع نطاق الفئة [AbstractActivity]، وبهذا الصفة، فإنها تنفذ الواجهة [IMainActivity] التي توسع بدورها نطاق الواجهة [IDao]. في كل مرة يتم فيها إضافة طريقة إلى واجهة [IDao]، يجب تنفيذها في الفئة [MainActivity]. سيتم تنفيذ الطريقة [IDao.getArduinos] المضافة إلى الواجهة [IDao] بالطريقة التالية في [MainActivity]:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // طبقة [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // جلسة
  private Session session;

...

  // تنفيذ IDao -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • الأسطر 15-18: يتم تنفيذ الطريقة [getArduinos] عن طريق تفويض المهمة إلى الفئة [Dao] التي تم عرضها للتو والتي توجد لها مرجع في السطر 8؛

5.6.14. إعادة النظر في الجزء [ConfigFragment]

في الفئة [ConfigFragment]، الرمز الذي يتم تنفيذه عند النقر على الزر [Rafraîchir] هو في الوقت الحالي كما يلي:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // يتم طلب قائمة أجهزة Arduino في الخلفية
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
    // يتم إنشاء قائمة وهمية بأجهزة أردوينو
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // محاكاة استجابة من الخادم
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // يتم استهلاكها
    consumeArduinosResponse(response);
  }

  // عرض الرد
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

علينا إعادة كتابة الأسطر 10-16 التي كانت تولد استجابة ثابتة من النوع [Response<List<Arduino>>]. علينا الآن طلب هذه القائمة من الطبقة [DAO] عبر النشاط. يصبح الكود كما يلي:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // التحقق من المدخلات
    if (!pageValid()) {
      return;
    }
    // يتم حفظ الإدخال
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // يتم التحضير للانتظار
    beginWaiting(1);
    // تنفيذ المهمة غير المتزامنة
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {

      @Override
      public void call(Response<List<Arduino>> response) {
        // استهلاك الرد
        consumeArduinosResponse(response);
      }
    });
}
  • السطر 8: تم تمرير URL، جذر خدمة الويب / jSON الذي أدخله المستخدم، إلى الطبقة [DAO] عبر النشاط. وستكون هذه هي جذر واجهة [WebClient] (انظر الفقرة 5.6.12.2
  • السطر 10: يتم إخطار الفئة الأم بأننا سنقوم بتشغيل مهمة غير متزامنة؛
  • الأسطر 12-19: تشغيل المهمة غير المتزامنة التي ستعيد قائمة أجهزة Arduino المتصلة بالخادم؛
  • السطر 12: استدعاء الطريقة [executeInBackground] من الفئة الأصلية. تتوقع هذه الطريقة معلمتين:
    • السطر 12: العملية المراد مراقبة. يتم توفير هذه العملية هنا بواسطة الطريقة [mainActivity.getArduinos()]؛
    • الأسطر 12-19: مثيل لواجهة [Action1<T>]، حيث النوع T هو النوع المقدم من قبل العملية، وهو هنا نوع [Response<List<Arduino>>]؛
  • الأسطر 14-18: الطريقة التي يتم استدعاؤها عندما تعرض المهمة غير المتزامنة نتيجتها من النوع [Response<List<Arduino>>]؛
  • السطر 17: يتم تمرير الرد المستلم إلى الطريقة [consumeArduinosResponse] المكتوبة مسبقًا؛

المهمة: قم بتشغيل الخادم كما هو موضح في الفقرة 5.4. قم بتوصيل جهاز Arduino واحد أو أكثر بـ PC الذي تم تشغيل الخادم عليه. ثم قم بتشغيل عميل Android وتحقق من أنه يمكنك الحصول على قائمة بأجهزة Arduino المتصلة. راقب السجلات.


Image

  • اكتب URL المشار إليه في [1]. هذا هو أحد عناوين IP لخادمك؛
  • انقر على الزر [2]؛
  • يجب أن تحصل على قائمة بأجهزة Arduino المتصلة في [3]؛

تأكد من ظهور هذه القائمة أيضًا في علامات التبويب الأخرى.

5.7. المهمة المطلوبة


بالطريقة نفسها التي اتبعتها للتو مع العرض [Config]، قم بتنفيذ ثم اختبار العروض الأربعة الأخرى للتطبيق بالتتابع: [Blink]، [PinRead]، [PinWrite] و [Commands].


تم عرض طرق العرض المطلوب تنفيذها في الفقرة 5.5.

بالنسبة لكل عرض، يجب:

  • رسم المنظر XML (انظر الفقرة 5.6.9
  • بناء الجزء المرتبط (انظر الفقرة 5.6.10
  • إضافة طريقة إلى الواجهة [WebClient] (انظر الفقرة 5.6.12.2
  • إضافة طريقة إلى الواجهة [IDao] (انظر الفقرة 5.6.12.2
  • إضافة طريقة إلى الفئة [Dao] (انظر الفقرة 5.6.12.3
  • إضافة طريقة إلى النشاط [MainActivity] (انظر الفقرة 5.6.13
  • كتابة معالجات أحداث الجزء (انظر الفقرة 5.6.14
  • اختبار ومراقبة السجلات؛

ملاحظة 1: المثال الذي يجب اتباعه هو مشروع [Exemple-16B] من الدورة (انظر الفقرة 2.8.3).

ملاحظة 2: تم عرض URL المطلوب استجوابها ونوع إجاباتها في الفقرة 5.4.2.

ملاحظة 3:

ترسل الفئة [CommandsFragment] قائمة تحتوي على أمر واحد لتنفيذه بواسطة واحد أو أكثر من أجهزة Arduino. سيتم تغليف هذا الأمر في الفئة [ArduinoCommand] التالية:


package android.arduinos.dao;

import java.util.Map;

public class ArduinoCommand {

  // البيانات
  private String id;
  private String ac;
  private Map<String, Object> pa;

  // المنشئات
  public ArduinoCommand() {

  }

  public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
    this.id = id;
    this.ac = ac;
    this.pa = pa;
  }

  // المُستردات والمُعيّنات
...
}

في واجهة [WebClient]، ستكون طريقة تنفيذ قائمة الأوامر هذه كما يلي:


  // إرسال الأوامر JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • السطر 2: يتم طلب URL بأمر HTTP POST؛
  • السطر 3: يجب أن تحمل القيمة المرسلة التعليق [@Body

ملاحظة 4: يُنصح بالقيام بهذا العمل بالطريقة التالية:

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