Fiddle เป็นโมดูลที่ไม่ค่อยมีใครรู้จักซึ่งถูกเพิ่มลงในไลบรารีมาตรฐานของ Ruby ใน 1.9.x อนุญาตให้คุณโต้ตอบโดยตรงกับไลบรารี C จาก Ruby มันยังช่วยให้คุณตรวจสอบและแก้ไขตัวแปลทับทิมขณะทำงานได้อีกด้วย
ทำงานโดยปิด libffi ซึ่งเป็นไลบรารี C ยอดนิยมที่อนุญาตให้เขียนโค้ดในภาษาหนึ่งเพื่อเรียกเมธอดที่เขียนในอีกภาษาหนึ่งได้ ในกรณีที่คุณไม่เคยได้ยินมาก่อน "ffi" ย่อมาจาก "foreign function interface" และคุณไม่ได้จำกัดอยู่แค่ C เท่านั้น เมื่อคุณเรียนรู้ Fiddle แล้ว คุณสามารถใช้ไลบรารี่ที่เขียนด้วย Rust และภาษาอื่นๆ ที่รองรับได้
มาดูฟิดเดิ้ลกัน เราจะเริ่มด้วยตัวอย่างง่ายๆ แล้วปิดท้ายด้วยการเข้าถึง Arduino ผ่านพอร์ตอนุกรม คุณไม่จำเป็นต้องรู้มากขนาดนั้น C. ฉันสัญญา :)
ตัวอย่างง่ายๆ
ใน Ruby แบบเก่า คุณต้องกำหนดวิธีการเสมอก่อนจึงจะเรียกมันได้ มันเหมือนกันกับซอ คุณไม่สามารถเรียกฟังก์ชัน C ได้โดยตรง คุณต้องสร้าง wrapper สำหรับฟังก์ชัน C แทน จากนั้นเรียกใช้ wrapper
ในตัวอย่างด้านล่าง เรากำลังปิดฟังก์ชันลอการิทึมของ C โดยพื้นฐานแล้วเรากำลังทำซ้ำ Math.log
ของ Ruby .
require 'fiddle'
# We're going to "open" a library, so we have to tell Fiddle where
# it's located on disk. This example works on OSX Yosemite.
libm = Fiddle.dlopen('/usr/lib/libSystem.dylib')
# Create a wrapper for the c function "log".
log = Fiddle::Function.new(
libm['log'], # Get the function from the math library we opened
[Fiddle::TYPE_DOUBLE], # It has one argument, a double, which is similar to ruby's Float
Fiddle::TYPE_DOUBLE # It returns a double
)
# call the c function via our wrapper
puts log.call(3.14159)
ทำให้สวยขึ้น
ตัวอย่างก่อนหน้านี้ใช้งานได้ แต่มันค่อนข้างละเอียด ฉันแน่ใจว่าคุณสามารถจินตนาการได้ว่าของรกๆ จะเป็นอย่างไร ถ้าคุณต้องห่อฟังก์ชัน 100 อย่าง นั่นเป็นเหตุผลที่ Fiddle ให้ DSL ที่ดี มันถูกเปิดเผยผ่าน Fiddle::Importer
มิกซ์อิน
การใช้มิกซ์อินนี้ทำให้การสร้างโมดูลที่เต็มไปด้วยฟังก์ชันภายนอกใช้เวลาไม่นาน ในตัวอย่างด้านล่าง เรากำลังสร้างโมดูลที่มีวิธีลอการิทึมหลายวิธี
require 'fiddle'
require 'fiddle/import'
module Logs
extend Fiddle::Importer
dlload '/usr/lib/libSystem.dylib'
extern 'double log(double)'
extern 'double log10(double)'
extern 'double log2(double)'
end
# We can call the external functions as if they were ruby methods!
puts Logs.log(10) # 2.302585092994046
puts Logs.log10(10) # 1.0
puts Logs.log2(10) # 3.321928094887362
การควบคุมพอร์ตอนุกรม
ตกลง ในที่สุดคุณก็ซื้อ Arduinos ตัวหนึ่งที่คุณคิดจะซื้อมาหลายปีแล้ว คุณได้ส่งข้อความ "สวัสดีชาวโลก" ไปยังคอมพิวเตอร์ของคุณหนึ่งครั้งต่อวินาทีโดยใช้พอร์ตอนุกรม
ตอนนี้ คงจะดีมากถ้าคุณสามารถอ่านข้อมูลนั้นโดยใช้ Ruby และคุณสามารถอ่านจากพอร์ตอนุกรมได้จริงโดยใช้วิธี IO มาตรฐานของ Ruby แต่วิธีการ IO เหล่านี้ไม่อนุญาตให้เรากำหนดค่าอินพุตซีเรียลด้วยระดับความละเอียดที่เราต้องการ หากเราต้องการเป็นแฮ็กเกอร์ฮาร์ดแวร์ที่ไม่ยอมใครง่ายๆ
โชคดีที่ไลบรารีมาตรฐาน C มีฟังก์ชันทั้งหมดที่เราต้องการเพื่อใช้งานกับพอร์ตอนุกรม และด้วยการใช้ Fiddle เราสามารถเข้าถึงฟังก์ชัน C เหล่านี้ใน Ruby ได้
แน่นอนว่ามีอัญมณีที่ทำสิ่งนี้ให้คุณอยู่แล้ว เมื่อฉันทำงานกับโค้ดนี้ ฉันได้รับแรงบันดาลใจเล็กน้อยจากอัญมณีทับทิม แต่เป้าหมายของเราคือการเรียนรู้วิธีทำสิ่งนี้ด้วยตัวเราเอง
กำลังตรวจสอบ termios.h
ใน C ชื่อไฟล์ใดๆ ที่ลงท้ายด้วย .h จะเป็นไฟล์ส่วนหัว ประกอบด้วยรายการฟังก์ชันและค่าคงที่ทั้งหมดที่ไลบรารีทำให้พร้อมใช้งานสำหรับโค้ดของบุคคลที่สาม (โค้ดของเรา) ขั้นตอนแรกในการปิดไลบรารี C ใดๆ ก็คือ ให้ค้นหาไฟล์นี้ เปิดไฟล์แล้วมองไปรอบๆ
ห้องสมุดที่เราใช้เรียกว่า termios ใน OSX Yosemite ไฟล์ส่วนหัวจะอยู่ที่ /usr/include/sys/termios.h มันจะอยู่ที่อื่นบน linux
นี่คือหน้าตาของ termios.h ฉันย่อมันเล็กน้อยเพื่อความชัดเจน
typedef unsigned long tcflag_t;
typedef unsigned char cc_t;
typedef unsigned long speed_t;
struct termios {
tcflag_t c_iflag; /* input flags */
tcflag_t c_oflag; /* output flags */
tcflag_t c_cflag; /* control flags */
tcflag_t c_lflag; /* local flags */
cc_t c_cc[NCCS]; /* control chars */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
int tcgetattr(int, struct termios *);
int tcsetattr(int, int, const struct termios *);
int tcflush(int, int);
มีสิ่งสำคัญสามประการที่ควรสังเกตเกี่ยวกับรหัสนี้ อันดับแรกเรามี typedefs บางตัว จากนั้นเราก็มีโครงสร้างข้อมูลที่ใช้เก็บข้อมูลการกำหนดค่าสำหรับการเชื่อมต่อ ในที่สุดเราก็มีวิธีการที่เราจะใช้กัน
ด้วยการใช้ตัวนำเข้าของซอ เราเกือบจะสามารถคัดลอกส่วนต่างๆ เหล่านี้แบบต่อคำลงในโค้ด Ruby ของเราได้ มาจัดการทีละคน
พิมพ์นามแฝงและ typedefs
ใน C เป็นเรื่องปกติที่จะสร้างนามแฝงสำหรับประเภทข้อมูล ตัวอย่างเช่น ไฟล์ termios.h สร้างประเภทใหม่ที่เรียกว่า speed_t ซึ่งเป็นจำนวนเต็มแบบยาว ผู้นำเข้า Fiddle สามารถจัดการสิ่งเหล่านี้ผ่านคุณสมบัติ typealias
# equivalent to `typedef unsigned long tcflag_t;`
typealias "tcflag_t", "unsigned long"
โครงสร้าง
C ไม่มีคลาสหรือโมดูล หากคุณต้องการจัดกลุ่มตัวแปรหลายๆ ตัวเข้าด้วยกัน ให้ใช้ struct Fiddle เป็นกลไกที่ดีในการทำงานกับ struct ใน ruby
ขั้นตอนแรกคือการกำหนดโครงสร้างในตัวนำเข้าซอ อย่างที่คุณเห็น เราเกือบจะสามารถคัดลอกและวางจากไฟล์ส่วนหัวได้แล้ว
Termios = struct [
'tcflag_t c_iflag',
'tcflag_t c_oflag',
'tcflag_t c_cflag',
'tcflag_t c_lflag',
'cc_t c_cc[20]',
'speed_t c_ispeed',
'speed_t c_ospeed',
]
ตอนนี้ เราสามารถสร้าง "ตัวอย่าง" ของ struct โดยใช้เมธอด malloc เราสามารถตั้งค่าและดึงค่าได้มาก แบบเดียวกับที่ทำกับอินสแตนซ์คลาส Ruby ปกติ
s = Termios.malloc
s.c_iflag = 12345
รวมทุกอย่างเข้าด้วยกัน
หากคุณรวมสิ่งที่เราเพิ่งเรียนรู้เกี่ยวกับ typedefs และ struct เข้ากับสิ่งที่เราได้แสดงให้เห็นแล้วด้วยฟังก์ชัน เราสามารถรวบรวม wrapper ที่ใช้งานได้ของไลบรารี termios
เสื้อคลุมของเรามีวิธีการที่เราต้องกำหนดค่าพอร์ตอนุกรม เราจะใช้ทับทิมเก่าธรรมดาสำหรับส่วนที่เหลือ
require 'fiddle'
require 'fiddle/import'
# Everything in this module was pretty much copied directly from
# termios.h. In Yosemite it's at /usr/include/sys/termios.h
module Serial
extend Fiddle::Importer
dlload '/usr/lib/libSystem.dylib'
# Type definitions
typealias "tcflag_t", "unsigned long"
typealias "speed_t", "unsigned long"
typealias "cc_t", "char"
# A structure which will hold configuratin data. Instantiate like
# so: `Serial::Termios.malloc()`
Termios = struct [
'tcflag_t c_iflag',
'tcflag_t c_oflag',
'tcflag_t c_cflag',
'tcflag_t c_lflag',
'cc_t c_cc[20]',
'speed_t c_ispeed',
'speed_t c_ospeed',
]
# Functions for working with a serial device
extern 'int tcgetattr(int, struct termios*)' # get the config for a serial device
extern 'int tcsetattr(int, int, struct termios*)' # set the config for a serial device
extern 'int tcflush(int, int)' # flush all buffers in the device
end
การใช้ห้องสมุด
ในตัวอย่างด้านล่าง เราเปิดอุปกรณ์ซีเรียลของเราใน Ruby จากนั้นรับ file descriptor เราใช้ฟังก์ชันเทอร์มิโอของเราเพื่อกำหนดค่าอุปกรณ์สำหรับการอ่าน แล้วเราค่อยอ่าน
ฉันกำลังใช้ตัวเลขมหัศจรรย์บางอย่างที่นี่ สิ่งเหล่านี้เป็นผลมาจากการรวมตัวเลือกการกำหนดค่าระดับบิตที่จำเป็นสำหรับการสื่อสารกับ Arduino หากคุณสนใจว่าตัวเลขมหัศจรรย์มาจากไหน ลองดูโพสต์ในบล็อกนี้
file = open("/dev/cu.wchusbserial1450", 'r')
fd = file.to_i
# Create a new instance of our config structure
config = Serial::Termios.malloc
# Load the default config options into our struct
Serial.tcgetattr(fd, config);
# Set config options important to the arduino.
# I'm sorry for the magic numbers.
config.c_ispeed = 9600;
config.c_ospeed = 9600;
config.c_cflag = 51968;
config.c_iflag = 0;
config.c_oflag = 0;
# wait for 12 characters to come in before read returns.
config.c_cc[17] = 12;
# no minimum time to wait before read returns
config.c_cc[16] = 0;
# Save our configuration
Serial.tcsetattr(fd, 0, config);
# Wait 1 second for the arduino to reboot
sleep(1)
# Remove any existing serial input waiting to be read
Serial.tcflush(fd, 1)
buffer = file.read(12)
puts "#{ buffer.size } bytes: #{ buffer }"
file.close
การทำงานของโปรแกรมอ่านซีเรียล
หากตอนนี้ฉันโหลด Arduino ด้วยโปรแกรมที่พิมพ์สวัสดีชาวโลกอย่างต่อเนื่อง ฉันสามารถใช้สคริปต์ทับทิมเพื่ออ่านมันได้
โปรแกรม Arduino นี้เพียงแค่เขียน "hello world" ลงในซีเรียลครั้งแล้วครั้งเล่า
และนี่คือสิ่งที่ดูเหมือนเมื่อฉันเรียกใช้มอนิเตอร์แบบอนุกรม