เครื่องจำลองสำหรับฮาร์ดแวร์จริง โปรเจ็กต์นี้มอบเซิร์ฟเวอร์ที่สามารถวางไข่อุปกรณ์จำลองหลายตัวและให้บริการคำขอพร้อมกัน
โปรเจ็กต์นี้จัดเตรียมเฉพาะโครงสร้างพื้นฐานที่จำเป็นในการเปิดใช้เซิร์ฟเวอร์จากไฟล์การกำหนดค่า (YAML, TOML หรือ json) และวิธีการลงทะเบียนปลั๊กอินของอุปกรณ์บุคคลที่สามผ่านกลไกจุดเข้าใช้งานของ Python
จนถึงตอนนี้ โครงการได้จัดเตรียมการขนส่งสำหรับ TCP, UDP และซีเรียลไลน์ การสนับสนุนการขนส่งใหม่ (เช่น USB, GPIB หรือ SPI) กำลังดำเนินการตามความต้องการ
PR ยินดีต้อนรับ!
( 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 จำนวน 2 เครื่องที่สามารถเข้าถึงได้ผ่านพอร์ต 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 (aka 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 แสดงถึงไฟล์พิเศษที่สร้างขึ้นโดยเครื่องจำลองเพื่อจำลองบรรทัดซีเรียลที่สามารถเข้าถึงได้ เช่น ไฟล์บรรทัดซีเรียล /dev/ttyS0
linux
คุณลักษณะนี้มีเฉพาะใน 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 ประตูหลัง gevent ซึ่งคุณสามารถเปิดใช้งานได้หากคุณต้องการเข้าถึงกระบวนการจำลองที่ทำงานจากระยะไกล หากต้องการเปิดใช้งานคุณสมบัตินี้ เพียงเพิ่มการกำหนดค่าต่อไปนี้ไปที่ระดับบนสุด:
backdoor : ["localhost": 10001]
devices :
- ...
คุณมีอิสระที่จะเลือกพอร์ต TCP และที่อยู่การเชื่อมโยงอื่น ๆ โปรดทราบว่าแบ็คดอร์นี้ไม่มีการรับรองความถูกต้อง และไม่พยายามที่จะจำกัดสิ่งที่ผู้ใช้ระยะไกลสามารถทำได้ ทุกคนที่สามารถเข้าถึงเซิร์ฟเวอร์สามารถดำเนินการใดๆ ก็ตามที่กระบวนการ python ที่ทำงานอยู่สามารถทำได้ ดังนั้น แม้ว่าคุณอาจผูกเข้ากับอินเทอร์เฟซใดก็ได้ แต่เพื่อความปลอดภัย ขอแนะนำให้คุณผูกเข้ากับอินเทอร์เฟซเดียวที่สามารถเข้าถึงได้โดยเครื่องท้องถิ่น เช่น 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
เมื่อเริ่มต้นระบบ นอกจากนี้ หากไม่มีการกำหนดค่าเริ่มต้น ควรตั้งค่าเริ่มต้นเป็น 'ปิด'
ขั้นแรกให้เพิ่มสิ่งนี้ลงในตัวอย่างการกำหนดค่าของเรา:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
control : ON
transports :
- type : tcp
url : :5000
จากนั้น เราปรับใช้ Oscilloscope ของเรา __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
เครื่องมือบางอย่างใช้โปรโตคอลที่ไม่ได้รับการจัดการอย่างเหมาะสมโดยโปรโตคอลข้อความแบบ EOL
เครื่องจำลองช่วยให้คุณสามารถเขียนโปรโตคอลข้อความของคุณเองได้ นี่คือตัวอย่าง:
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
...
หากคุณกำลังพัฒนาไลบรารี Python ที่ให้การเข้าถึงเครื่องมือที่สามารถเข้าถึงได้ผ่านซ็อกเก็ตหรือซีเรียลไลน์ และคุณเขียนโปรแกรมจำลองสำหรับเครื่องมือนั้น คุณอาจสนใจที่จะทดสอบไลบรารีของคุณกับโปรแกรมจำลอง
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