الفرس DIY

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


بيان المشكلة


وتنقسم المهمة ، بوضوح ، إلى قسمين:


  • أجهزة التحكم الدقيقة ، و
  • برنامج يتم تشغيله على جهاز كمبيوتر ويتحكم في محتوياته.

بما أننا نعمل مع AVR ، فلماذا لا اردوينو؟


نحن نطرح المشكلة.
منصة الأجهزة:
HW1. تتم الإدارة عن طريق الأزرار دون تثبيت ؛
HW2. نحن نقدم 3 أزرار (بشكل عام ، كم عدد لا يمانعون) ؛
HW3. يعتبر الضغط مع الاستمرار في الضغط على الزر لمدة 100 مللي ثانية على الأقل ؛
HW4. يتم تجاهل المطابع الأطول. لا يتم تنفيذ أكثر من زر واحد في المرة الواحدة ؛
HW5. عند الضغط على زر ، يتم تشغيل إجراء معين على الكمبيوتر ؛
HW6. توفير واجهة اتصال مع جهاز كمبيوتر من خلال محول تسلسلي / USB مدمج ؛
منصة البرمجيات:
SW1. توفير واجهة اتصال مع جهاز كمبيوتر من خلال منفذ تسلسلي قابل للتحديد ؛
SW2. قم بتحويل الأوامر القادمة من خلال واجهة الاتصال إلى أحداث نظام التشغيل التي يتم تسليمها إلى مشغل الصوت المطلوب.
SW3. وقفة معالجة الأمر. بما في ذلك أمر من جهاز التحكم عن بعد.


حسنًا ، هناك متطلب إضافي: إذا لم يوفر هذا استثمارًا جادًا للوقت ، فاجعل الحلول عالمية قدر الإمكان.


التصميم والحل


Hw1


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


Hw2


هناك العديد من الأزرار ، لذلك نحتاج إلى كمية معينة من السجلات الموحدة حول كيفية استطلاع الأزرار وماذا نفعل إذا تم الضغط عليه. نحن نتطلع نحو التغليف ونجعل فئة Button ، والتي تحتوي على رقم الدبوس الذي يتم إجراء المسح منه (ويقوم بتهيئته بنفسه) ، والأمر الذي يجب إرساله إلى المنفذ. سنتعامل مع ما سيكون عليه الفريق لاحقًا.


ستبدو فئة الزر كما يلي:


رمز فئة الزر
 class Button { public: Button(uint8_t pin, ::Command command) : pin(pin), command(command) {} void Begin() { pinMode(pin, INPUT); digitalWrite(pin, 1); } bool IsPressed() { return !digitalRead(pin); } ::Command Command() const { return command; } private: uint8_t pin; ::Command command; }; 

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


ضع الأزرار معًا وقم بتعيين دبابيس لهم:


 Button buttons[] = { Button(A0, Command::Previous), Button(A1, Command::PauseResume), Button(A2, Command::Next), }; 

تتم تهيئة جميع الأزرار عن طريق استدعاء طريقة Begin() لكل زر:


 for (auto &button : buttons) { button.Begin(); } 

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


GetPressed ()
 int GetPressed() { int index = PressedNothing; for (byte i = 0; i < ButtonsCount; ++i) { if (buttons[i].IsPressed()) { if (index == PressedNothing) { index = i; } else { return PressedMultiple; } } } return index; } 

Hw3


سيتم استطلاع الأزرار بفترة معينة (على سبيل المثال ، 10 مللي ثانية) ، وسوف نفترض أن الضغط قد حدث إذا تم الاحتفاظ بنفس الزر (وزر واحد بالضبط) لعدد معين من دورات الاقتراع. اقسم وقت التثبيت (100 مللي ثانية) على فترة الاستطلاع (10 مللي ثانية) ، نحصل على 10.
سنبدأ عداد تناقص ، نكتب فيه 10 عند التثبيت الأول للضغط ، ونقص في كل فترة. بمجرد أن تنتقل من 1 إلى 0 ، نبدأ المعالجة (انظر HW5)


Hw4


إذا كان العداد بالفعل 0 ، لا يتم اتخاذ أي إجراء.


Hw5


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


في هذه المرحلة ، يمكنك تنفيذ استراتيجية لوحة المفاتيح.


تنفيذ الحلقة الرئيسية
 void HandleButtons() { static int CurrentButton = PressedNothing; static byte counter; int button = GetPressed(); if (button == PressedMultiple || button == PressedNothing) { CurrentButton = button; counter = -1; return; } if (button == CurrentButton) { if (counter > 0) { if (--counter == 0) { InvokeCommand(buttons[button]); return; } } } else { CurrentButton = button; counter = PressInterval / TickPeriod; } } void loop() { HandleButtons(); delay(TickPeriod); } 

الأب 6


يجب أن تكون واجهة الاتصال واضحة لكل من المرسل والمستلم. نظرًا لأن الواجهة التسلسلية تحتوي على وحدة نقل بيانات 1 بايت ولديها مزامنة البايت ، فمن المنطقي أن تسيطر على شيء معقد ونقتصر على إرسال بايت واحد لكل أمر. لتسهيل عملية التصحيح ، سننقل حرف ASCII واحد لكل أمر.


تنفيذ اردوينو


الآن نجمع. يظهر رمز التنفيذ الكامل أدناه تحت المفسد. لتوسيعه ، يكفي تحديد رمز ASCII للأمر الجديد وإرفاق زر به.
بالطبع ، سيكون من الممكن فقط الإشارة بوضوح إلى رمز رمز لكل زر ، لكننا لن نفعل ذلك: سيكون تسمية الأمر مفيدًا لنا عند تنفيذ عميل لجهاز كمبيوتر.


التنفيذ الكامل
 const int TickPeriod = 10; //ms const int PressInterval = 100; //ms enum class Command : char { None = 0, Previous = 'P', Next = 'N', PauseResume = 'C', SuspendResumeCommands = '/', }; class Button { public: Button(uint8_t pin, Command command) : pin(pin), command(command) {} void Begin() { pinMode(pin, INPUT); digitalWrite(pin, 1); } bool IsPressed() { return !digitalRead(pin); } Command GetCommand() const { return command; } private: uint8_t pin; Command command; }; Button buttons[] = { Button(A0, Command::Previous), Button(A1, Command::PauseResume), Button(A2, Command::Next), Button(12, Command::SuspendResumeCommands), }; const byte ButtonsCount = sizeof(buttons) / sizeof(buttons[0]); void setup() { for (auto &button : buttons) { button.Begin(); } Serial.begin(9600); } enum { PressedNothing = -1, PressedMultiple = -2, }; int GetPressed() { int index = PressedNothing; for (byte i = 0; i < ButtonsCount; ++i) { if (buttons[i].IsPressed()) { if (index == PressedNothing) { index = i; } else { return PressedMultiple; } } } return index; } void InvokeCommand(const class Button& button) { Serial.write((char)button.GetCommand()); } void HandleButtons() { static int CurrentButton = PressedNothing; static byte counter; int button = GetPressed(); if (button == PressedMultiple || button == PressedNothing) { CurrentButton = button; counter = -1; return; } if (button == CurrentButton) { if (counter > 0) { if (--counter == 0) { InvokeCommand(buttons[button]); return; } } } else { CurrentButton = button; counter = PressInterval / TickPeriod; } } void loop() { HandleButtons(); delay(TickPeriod); } 

ونعم ، قمت بعمل زر آخر لأتمكن من إيقاف نقل الأوامر إلى العميل مؤقتًا.


عميل للكمبيوتر الشخصي


ننتقل إلى الجزء الثاني.
نظرًا لأننا لا نحتاج إلى واجهة معقدة وملزمة لنظام التشغيل Windows ، يمكننا الذهاب بطرق مختلفة ، كما تريد: WinAPI أو MFC أو Delphi أو .NET (Windows Forms أو WPF وما إلى ذلك) أو وحدات التحكم على نفس الأنظمة الأساسية ( حسنا ، باستثناء MFC).


SW1


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


SW2


ربما رأى الجميع لوحات المفاتيح مع مفاتيح الوسائط المتعددة. كل مفتاح على لوحة المفاتيح ، بما في ذلك الوسائط المتعددة ، له رمزه الخاص. إن أبسط حل لمشكلتنا هو محاكاة ضغطات مفاتيح الوسائط المتعددة على لوحة المفاتيح. يمكن العثور على رموز المفاتيح في المصدر الأصلي - MSDN . يبقى لتعلم كيفية إرسالها إلى النظام. هذا ليس صعبًا أيضًا: توجد وظيفة SendInput في WinAPI.
كل ضغطة مفتاح هي حدثان: الضغط والإطلاق.
إذا استخدمنا C / C ++ ، فيمكننا ببساطة تضمين ملفات الرأس. في لغات أخرى ، يجب أن تتم إعادة توجيه المكالمات. لذلك ، على سبيل المثال ، عند التطوير على .NET ، سيتعين عليك استيراد الوظيفة المحددة ووصف الوسيطات. اخترت .NET من أجل راحة تطوير واجهة.
لقد اخترت من المشروع الجزء الموضوعي فقط ، والذي يتلخص في فصل واحد: Internals .
هنا هو رمزه:


الداخلية رمز الفصل
  internal class Internals { [StructLayout(LayoutKind.Sequential)] [DebuggerDisplay("{Type} {Data}")] private struct INPUT { public uint Type; public KEYBDINPUT Data; public const uint Keyboard = 1; public static readonly int Size = Marshal.SizeOf(typeof(INPUT)); } [StructLayout(LayoutKind.Sequential)] [DebuggerDisplay("Vk={Vk} Scan={Scan} Flags={Flags} Time={Time} ExtraInfo={ExtraInfo}")] private struct KEYBDINPUT { public ushort Vk; public ushort Scan; public uint Flags; public uint Time; public IntPtr ExtraInfo; private long spare; } [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure); private static INPUT[] inputs = { new INPUT { Type = INPUT.Keyboard, Data = { Flags = 0 // Push } }, new INPUT { Type = INPUT.Keyboard, Data = { Flags = 2 // Release } } }; public static void SendKey(Keys key) { inputs[0].Data.Vk = (ushort) key; inputs[1].Data.Vk = (ushort) key; SendInput(2, inputs, INPUT.Size); } } 

أولاً ، يصف هياكل البيانات (يتم قطع ما يتعلق بإدخال لوحة المفاتيح فقط ، حيث نقوم بمحاكاة ذلك) ، واستيراد SendInput .
حقل inputs عبارة عن مصفوفة من عنصرين سيتم استخدامهما لإنشاء أحداث لوحة المفاتيح. لا معنى SendKey ديناميكيًا إذا SendKey بنية التطبيق أن SendKey لن يتم SendKey في SendKey متعددة.
في الواقع ، الأمر الفني أبعد من ذلك: نملأ حقول البنية المقابلة برمز المفتاح الظاهري ونرسله إلى قائمة انتظار إدخال نظام التشغيل.


SW3


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


بدلا من الاستنتاج


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


شكرا لكم على اهتمامكم.

Source: https://habr.com/ru/post/ar422203/


All Articles