محاكاة للأجهزة الحقيقية. يوفر هذا المشروع خادمًا قادرًا على إنتاج أجهزة محاكاة متعددة وخدمة الطلبات بشكل متزامن.
يوفر هذا المشروع فقط البنية التحتية المطلوبة لتشغيل الخادم من ملف التكوين (YAML أو TOML أو json) ووسيلة لتسجيل المكونات الإضافية لجهاز الطرف الثالث من خلال آلية نقطة دخول python.
حتى الآن، يوفر المشروع وسائل نقل لـ TCP وUDP والخط التسلسلي. يتم تنفيذ دعم وسائل النقل الجديدة (على سبيل المثال: USB أو GPIB أو SPI) على أساس الحاجة.
العلاقات العامة هي موضع ترحيب!
( TL;DR : pip install sinstruments[all]
)
من داخل بيئة بايثون المفضلة لديك:
$ pip install sinstruments
بالإضافة إلى ذلك، إذا كنت تريد كتابة ملفات تكوين YAML في YAML:
$ pip install sinstruments[yaml]
...أو للتكوين المستند إلى TOML:
$ pip install sinstruments[toml]
بمجرد التثبيت يمكن تشغيل الخادم باستخدام:
$ sinstruments-server -c <config file name>
يصف ملف التكوين الأجهزة التي يجب على الخادم إنشاء مثيل لها بالإضافة إلى سلسلة من الخيارات مثل وسائل النقل التي يستمع إليها كل جهاز للطلبات.
تخيل أنك بحاجة إلى محاكاة جهازي GE Pace 5000 يمكن الوصول إليهما عبر منفذ TCP وCryoCon 24C يمكن الوصول إليهما من خلال الخط التسلسلي.
أولاً، تأكد من تثبيت التبعيات مع:
$ pip install gepace[simulator] cryoncon[simulator]
يمكننا الآن إعداد إعدادات YAML تسمى simulator.yml
:
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
- class : Pace
name : pace-2
transports :
- type : tcp
url : :5001
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
نحن الآن جاهزون لإطلاق الخادم:
$ sinstruments-server -c simulator.yml
هذا كل شيء! يجب أن تكون الآن قادرًا على الاتصال بأي من أجهزة Pace عبر TCP أو CryoCon باستخدام الخط التسلسلي المحلي الذي تمت محاكاته.
دعونا نحاول الاتصال بـ Pace الأول باستخدام أداة سطر أوامر Linux nc (المعروفة أيضًا باسم netcat) ونطلب اسم النطاق *IDN?
أمر SCPI:
$ nc localhost 5000
*IDN?
GE,Pace5000,204683,1.01A
هذا ملخص لمكتبات أدوات الطرف الثالث المعروفة التي توفر أجهزة المحاكاة الخاصة بها.
إذا كتبت جهازًا متاحًا للعامة، فلا تتردد في إكمال القائمة أعلاه عن طريق إنشاء PR.
تلميح : يعرض sinstruments-server ls
قائمة بالمكونات الإضافية المتاحة.
يمكن أن يكون ملف التكوين عبارة عن ملف YAML أو TOML أو JSON طالما أنه يُترجم إلى قاموس بالوصف الوارد أدناه.
في هذا الفصل سوف نستخدم YAML كمثال مرجعي.
يجب أن يحتوي الملف على الأقل على مفتاح عالي المستوى يسمى devices
. يجب أن تكون القيمة عبارة عن قائمة بأوصاف الجهاز:
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
يجب أن يحتوي كل وصف للجهاز على:
tcp
و udp
و serial
)يتم تمرير أي خيارات أخرى تُعطى لكل جهاز مباشرةً إلى كائن البرنامج الإضافي المحدد في وقت التشغيل. يجب أن يصف كل مكون إضافي الخيارات الإضافية التي يدعمها وكيفية استخدامها.
بالنسبة لعمليات نقل TCP وUDP، يكون عنوان URL بالتنسيق <host>:<port>
.
يتم تفسير المضيف الفارغ (كما في المثال أعلاه) على أنه 0.0.0.0
(مما يعني الاستماع على جميع واجهات الشبكة). إذا كان المضيف هو 127.0.0.1
أو localhost
فلن يمكن الوصول إلى الجهاز إلا من الجهاز الذي يتم تشغيل جهاز المحاكاة عليه.
تعني قيمة المنفذ 0 أن تطلب من نظام التشغيل تعيين منفذ مجاني (مفيد لتشغيل مجموعة اختبار). وبخلاف ذلك، يجب أن يكون منفذ TCP أو UDP صالحًا.
يمثل عنوان url ملفًا خاصًا تم إنشاؤه بواسطة جهاز المحاكاة لمحاكاة خط تسلسلي يمكن الوصول إليه مثل ملف السطر التسلسلي linux /dev/ttyS0
.
هذه الميزة متاحة فقط في نظام التشغيل Linux والأنظمة التي يتم فيها تنفيذ pseudo Terminal pty
في لغة python.
عنوان URL اختياري. سيقوم جهاز المحاكاة دائمًا بإنشاء اسم غير محدد مثل /dev/pts/4
وسيقوم بتسجيل هذه المعلومات في حالة احتياجك للوصول إليها. تكون هذه الميزة مفيدة للغاية عند تشغيل مجموعة اختبار.
أنت حر في اختيار أي مسار عنوان URL تريده (على سبيل المثال: /dev/ttyRP10
) طالما أنك متأكد من أن جهاز المحاكاة لديه أذونات لإنشاء الملف الرمزي.
بالنسبة لأي من وسائل النقل (TCP، UDP والخط التسلسلي) من الممكن إجراء محاكاة أساسية لسرعة قناة الاتصال من خلال توفير معلمة baudrate
إضافية للتكوين. مثال:
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
baudrate : 9600
يوفر جهاز المحاكاة وحدة تحكم python للباب الخلفي والتي يمكنك تنشيطها إذا كنت تريد الوصول إلى عملية محاكاة جارية عن بعد. لتنشيط هذه الميزة، ما عليك سوى إضافة ما يلي إلى المستوى الأعلى للتكوين:
backdoor : ["localhost": 10001]
devices :
- ...
أنت حر في اختيار أي منفذ TCP آخر وعنوان الربط. انتبه إلى أن هذا الباب الخلفي لا يوفر أي مصادقة ولا يحاول تقييد ما يمكن للمستخدمين البعيدين القيام به. يمكن لأي شخص يمكنه الوصول إلى الخادم اتخاذ أي إجراء يمكن أن تقوم به عملية بايثون الجارية. وبالتالي، بينما يمكنك الارتباط بأي واجهة، فمن المستحسن، لأغراض أمنية، الارتباط بواجهة يمكن الوصول إليها فقط من خلال الجهاز المحلي، على سبيل المثال، 127.0.0.1/localhost.
الاستخدام
بمجرد تكوين الباب الخلفي وتشغيل الخادم، في محطة أخرى، اتصل بـ:
$ nc 127.0.0.1 10001
Welcome to Simulator server console.
You can access me through the 'server()' function. Have fun!
>>> print(server())
...
كتابة جهاز جديد أمر بسيط. لنتخيل أنك تريد محاكاة راسم الذبذبات SCPI. الشيء الوحيد الذي عليك القيام به هو كتابة فئة ترث من BaseDevice وتنفيذ handle_message(self, message)
حيث يجب عليك التعامل مع الأوامر المختلفة التي يدعمها جهازك:
# myproject/simulator.py
from sinstruments . simulator import BaseDevice
class Oscilloscope ( BaseDevice ):
def handle_message ( self , message ):
self . _log . info ( "received request %r" , message )
message = message . strip (). decode ()
if message == "*IDN?" :
return b"ACME Inc,O-3000,23l032,3.5A"
elif message == "*RST" :
self . _log . info ( "Resetting myself!" )
...
لا تنسَ إرجاع bytes
دائمًا! لا يقوم جهاز المحاكاة بأي تخمينات حول كيفية تشفير str
بافتراض أن ملف simulator.py
هذا هو جزء من حزمة python تسمى myproject
، فإن الشيء الثاني الذي يجب عليك فعله هو تسجيل المكون الإضافي للمحاكي في ملف setup.py الخاص بك:
setup (
...
entry_points = {
"sinstruments.device" : [
"Oscilloscope=myproject.simulator:Oscilloscope"
]
}
)
يجب أن تكون الآن قادرًا على تشغيل جهاز المحاكاة الخاص بك عن طريق كتابة ملف التكوين:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
transports :
- type : tcp
url : :5000
الآن قم بتشغيل الخادم باستخدام
$ sinstruments-server -c oscillo.yml
ويجب أن تكون قادرًا على التواصل مع:
$ nc localhost 5000
*IDN?
ACME Inc,O-3000,23l032,3.5A
افتراضيًا، يتم تعيين eol
على n
. يمكنك تغييره إلى أي شخصية باستخدام:
class Oscilloscope ( BaseDevice ):
newline = b" r "
إذا كان جهازك يطبق بروتوكولًا يجيب بإجابات متعددة (من المحتمل أن تكون متأخرة) لطلب واحد، فيمكنك دعم ذلك عن طريق تحويل handle_message()
إلى مولد:
class Oscilloscope ( BaseDevice ):
def handle_message ( self , message ):
self . _log . info ( "received request %r" , message )
message = message . strip (). decode ()
if message == "*IDN?" :
yield b"ACME Inc,O-3000,23l032,3.5A"
elif message == "*RST" :
self . _log . info ( "Resetting myself!" )
elif message == "GIVE:ME 10" :
for i in range ( 1 , 11 ):
yield f"Here's { i } n " . encode ()
...
لا تنس أن تنتج bytes
دائمًا! لا يقوم جهاز المحاكاة بأي تخمينات حول كيفية تشفير str
إذا كان جهازك الذي تمت محاكاته يتطلب تكوينًا إضافيًا، فيمكن توفيره من خلال نفس ملف YAML.
لنفترض أنك تريد أن تكون قادرًا على التهيئة إذا كان جهازك في وضع CONTROL
عند بدء التشغيل. بالإضافة إلى ذلك، إذا لم يتم تكوين أي قيمة أولية، فيجب أن تكون القيمة الافتراضية "OFF".
دعونا أولاً نضيف هذا إلى مثال التكوين الخاص بنا:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
control : ON
transports :
- type : tcp
url : :5000
بعد ذلك، نعيد تنفيذ راسم الذبذبات __init__()
لاعتراض هذه المعلمة الجديدة ونتعامل معها في handle_message()
:
class Oscilloscope ( BaseDevice ):
def __init__ ( self , name , ** opts ):
self . _control = opts . pop ( "control" , "OFF" ). upper ()
super (). __init__ ( name , ** opts )
def handle_message ( self , message ):
...
elif message == "CONTROL" :
return f"CONTROL { self . _control } n " . encode ()
...
أنت حر في إضافة أي عدد تريده من الخيارات طالما أنها لا تتعارض مع name
المفاتيح المحجوزة class
ووسائل transports
.
تنفذ بعض الأدوات بروتوكولات لا تتم إدارتها بشكل مناسب بواسطة بروتوكول الرسائل المستند إلى موسوعة الحياة.
يسمح لك جهاز المحاكاة بكتابة بروتوكول الرسائل الخاص بك. هنا مثال:
from sinstruments . simulator import MessageProtocol
class FixSizeProtocol ( MessageProtocol ):
Size = 32
def read_messages ( self ):
transport = self . transport
buff = b''
while True :
buff += transport . read ( self . channel , size = 4096 )
if not buff :
return
for i in range ( 0 , len ( buff ), self . Size ):
message = buff [ i : i + self . Size ]
if len ( message ) < self . Size :
buff = message
break
yield message
class Oscilloscope ( BaseDevice ):
protocol = FixSizeProtocol
...
إذا كنت تقوم بتطوير مكتبة بايثون توفر الوصول إلى أداة يمكن الوصول إليها من خلال المقبس أو الخط التسلسلي وقمت بكتابة محاكي لها، فقد تكون مهتمًا باختبار مكتبتك مقابل جهاز المحاكاة.
توفر Sinstruments زوجًا من مساعدي pytest الذين ينتجون جهاز محاكاة في سلسلة رسائل منفصلة.
server_context
الاستخدام الأول هو ببساطة استخدام مساعد server_context
. في الواقع، لا يوجد شيء محدد في هذا المساعد، لذا يمكنك تخيل استخدامه في سيناريوهات أخرى أيضًا.
هنا مثال:
import pytest
from sinstruments . pytest import server_context
cfg = {
"devices" : [{
"name" : "oscillo-1" ,
"class" : "Oscilloscope" ,
"transports" : [
{ "type" : "tcp" , "url" : "localhost:0" }
]
}]
}
def test_oscilloscope_id ():
with server_context ( cfg ) as server :
# put here code to perform your tests that need to communicate with
# the simulator. In this example an oscilloscope client
addr = server . devices [ "oscillo-1" ]. transports [ 0 ]. address
oscillo = Oscilloscope ( addr )
assert oscillo . idn (). startswith ( "ACME Inc,O-3000" )
قد تلاحظ أنه في التكوين نستخدم المنفذ 0
. هذا يخبر المحاكي بالاستماع على أي منفذ مجاني يوفره نظام التشغيل.
يسترد الاختبار الفعلي العنوان الحالي الذي تم تعيينه بواسطة نظام التشغيل ويستخدمه في الاختبار.
كما ترون، لا تعتمد الاختبارات على توفر منفذ واحد محدد مما يجعلها محمولة.
فيما يلي اقتراح حول كيفية كتابة التركيبات الخاصة بك باستخدام مساعد server_context
. كان الهدف هو تقليل كمية التعليمات البرمجية النمطية التي تحتاجها لكتابة الاختبار الخاص بك:
@ pytest . fixture
def oscillo_server ():
with server_context ( config ) as server :
server . oscillo1 = server . devices [ "oscillo-1" ]
server . oscillo1 . addr = server . oscillo1 . transports [ 0 ]. address
yield server
def test_oscilloscope_current ( oscillo_server ):
oscillo = Oscilloscope ( oscillo_server . oscillo1 . addr )
assert .05 < oscillo . current () < 0.01
server
المساعد الثاني هو تركيبات server
. تعتمد هذه المباراة على ميزة config
الموجودة التي يجب أن تكون موجودة في الوحدة النمطية الخاصة بك. فيما يلي مثال يتبع الكود السابق:
from sinstruments . pytest import server
@ pytest . fixture
def config ()
yield cfg
def test_oscilloscope_voltage ( server ):
addr = server . devices [ "oscillo-1" ]. transports [ 0 ]. address
oscillo = Oscilloscope ( addr )
assert 5 < oscillo . voltage () < 10