در قسمت هجدهم از آموزش آردوینو به بررسی دریافت سریال دیتا در آردوینو و همچنین، ارسال چندین فیلد متنی از آردوینو در یک پیام، پرداختیم. در این قسمت قصد داریم درباره دریافت چندین فیلد متنی در یک پیام و همچنین، ارسال دادههای باینری از آردوینو، صحبت کنیم.
دریافت چندین فیلد متنی در یک پیام
در ادامه این مطلب ابتدا درباره چگونگی دریافت پیامی که حاوی بیش از یک فیلد باشد، توضیح میدهیم. بهعنوانمثال، پیام شما ممکن است حاوی یک شناسه برای نشاندادن یک دستگاه خاص (مانند یک موتور یا سایر محرکها) و مقدار (value) (مانند سرعت) برای تنظیم آن باشد.
کد زیر پیامی با یک کاراکتر H بهعنوان هدر دریافت میکند که دارای سه فیلد عددی است که با کاما از هم جدا شدهاند و در نهایت، با کاراکتر خط جدید (newline) خاتمه مییابد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | *This code expects a message in the format : H, 12, 345, 678 * This code requires a newline character to indicate the end of the data *Set the serial monitor to send newline characters * / const int NUMBER_OF_FIELDS = 3; // how many comma separated fields we expect int // values[NUMBER_OF_FIELDS]; // array holding values for all the // fields void setup() { Serial.begin(9600); // Initialize // serial port to send and receive at 9600 baud } void loop() { // if ( Serial.available()) { if (Serial.read() == 'H') { // // Read the values for (int i = 0; i < NUMBER_OF_FIELDS; i++) { // values[i] = Serial.parseInt(); } // Display the values in // comma-separated format Serial.print(values[0]); // First // value // Print the rest of the values with a leading comma // for (int i = 1; i < NUMBER_OF_FIELDS; i++) { // Serial.print(","); Serial.print(values[i]); } // Serial.println(); } } } |
این اسکچ از متد parseInt استفاده میکند که استخراج مقادیر عددی را از استریمهای سریال و وب آسان میکند. این نمونهای از نحوه استفاده از این قابلیت است. شما میتوانید این اسکچ را با باز کردن Serial Monitor و ارسال یک پیام جدا شده با کاما مانند H1,2,3 تست کنید. parseInt تمامی موارد بهغیراز یک علامت منفی و یک رقم را نادیده میگیرد؛ بنابراین لازم نیست که با کاما، جدا شود. شما میتوانید از هر جداکننده دیگری مانند H1/2/3 استفاده کنید.این اسکچ، اعداد را در یک آرایه ذخیره میکند و سپس آنها را با کاما از هم جدا میکند.
زمان انتظارِ توابعِ stream-parsing برای یک کاراکتر بهصورت پیشفرض، یک ثانیه است. اگر هیچرقمی دریافت نشده باشد و زمانهای parseInt تمام شود، مقدار 0 برگردانده می شود. شما می توانید پارامتر تایم اوت (timeout) را از طریق فراخوانی تابع Stream.setTimeout(timeoutPeriod) تغییر دهید. پارامتر تایم اوت یک عدد صحیح long است که تعداد میلی ثانیه را نشان می دهد؛ بنابراین محدوده زمانی میتواند از 1 میلی ثانیه تا 2,147,483,647 میلی ثانیه باشد. که
Stream.setTimeout(2147483647);
فاصله زمانی را به حدود کمتر از 25 روز تغییر می دهد.
توابع Stream
Stream کلاس پایه برای کاراکترها و جریان های مبتنی بر باینری است. این کلاس مستقیماً فراخوانی نمیشود، اما هر زمان که از تابعی استفاده میکنید که به آن متکی است، فراخوانی می شود. این کلاس به هر عملکردی که شامل خواندن مقادیر باشد مربوط است.
توابع پردازشجریان (stream) که توسط پلتفرم آردوینو پشتیبانی میشوند، شامل موارد زیر میباشند:
- bool find(char *target);
تابع bool find از جریان (stream) شروع و تا زمانی که رشته موردنظر یافت شود، ادامه میدهد. اگر رشتهی هدف یافت شود، مقدار true برگردانده میشود و اگر دادهها در هیچ نقطهای از جریان یافت نشود و دیگر دادهای در دسترس نباشد، مقدار false برگردانده میشود. توجه کنید که تجزیهی جریان (stream) یکبار از ابتدا تا انتها انجام میشود و امکان بازگشت به عقب برای جستجو یا دریافت چیز دیگری وجود ندارد.
- bool findUntil(char *target, char *terminate);
مشابه روش Find است، با این تفاوت که در این تابع، اگر رشته (string) پایان یابد، جستجو متوقف خواهد شد. فقط در صورتیافتن هدف (تارگت)، مقدار true برگردانده میشود. بهعنوانمثال:
1 | finder.findUntil("target", "\n"); |
این تابع سعی میکند رشته “value” را جستجو کند، اما روی یک کاراکتر کد جدید متوقف میشود تا اگر هدف پیدا نشد، اسکچ بتواند کار دیگری انجام دهد.
- long parseInt();
این تابع اولین مقدار عددی long را برمیگرداند. اگر عددی در رشته وجود داشته باشد، parseInt() اولین عدد صحیح long را برمیگرداند و اگر عددی در رشته وجود نداشته باشد، مقدار 0 برگردانده میشود.
- long parseInt(char skipChar);
همانند تابع parseInt میباشد، با این تفاوت که در اینجا skipChar داده شده در مقدار عددی نادیده گرفته میشود. این موضوع میتواند هنگام تجزیه یک مقدار عددی منفرد که بین بلوکهای اعداد از کاما استفاده میکند مفید باشد. اما بهخاطر داشته باشید که مقادیر متن فرمت شده با کاما را نمیتوان بهعنوان یکرشته جدا شده با کاما تجزیه کرد.
- float parseFloat();
این تابع، نسخه شناور (float) تابع parseInt است که در آن، همه کاراکترها به جز ارقام، نقطهی اعشار و علامت منفی (ای که قبل از عدد باشد) را حذف میکند.
- size_t readBytes(char *buffer, size_t length);
این تابع کاراکترهای دریافتی را تا زمانی که کاراکترهای وقفه یا طول خوانده شوند در بافر داده شده قرار میدهد و در نهایت، تعداد کاراکترهای قرار داده شده در بافر را برمیگرداند.
- size_t readBytesUntil(char terminator, char *buf, size_t length);
تابع readBytesUntil، کاراکترهای ورودی را در بافر (buffer) مشخص شده میگذارد تا زمانی که کاراکتر پایاندهنده (terminator character) شناسایی شود. اگر طول رشته ورودی بیشتر از طول مشخص شده باشد، تابع آن رشته را کوتاه میکند تا در بافر جا بیفتد و در نهایت، تابع تعداد کاراکترهای قرار گرفته شده در بافر را برمیگرداند.
ارسال دادههای باینری از آردوینو
شما باید دادهها را در قالب باینری ارسال کنید؛ زیرا میخواهید اطلاعات را با کمترین تعداد بایت ارسال کنید. همچنین، دلیل دیگر آن، این است که برنامهای که به آن متصل میشوید فقط دادههای باینری را ساپورت میکند.
در این اسکچ، ابتدا یک هدر (header) ارسال میشود و سپس دو مقدار صحیح (دو بایتی) بهعنوان دادههای باینری ارسال میشوند. در این اسکچ، از نوع دادهای short استفاده شده است، زیرا بدون درنظرگرفتن اینکه برد شما 8 بیتی یا 32 بیتی است، این نوع داده همواره دو بایت خواهد بود. مقادیر تولید شده با استفاده از تابع random آردوینو ایجاد میشوند. اگرچه تابع random مقداری از نوع long برمیگرداند، اما با توجه به آرگومان 599، هیچگاه مقداری بیش تر از این عدد برنگردانده نخواهد شد. این مقدار کوچک برای جایگذاری در نوع دادهای short مناسب است.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /* * SendBinary sketch * Sends a header followed by two random integer values as * binary data. */ short intValue; // short integer value (two bytes on all boards) void setup() { // Serial.begin(9600); } void loop() { Serial.print('H'); // // send a header character // send a random integer intValue = // random(599); // generate a random number between 0 and 599 // // send the two bytes that comprise that integer // Serial.write(lowByte(intValue)); // send the low byte // Serial.write(highByte(intValue)); // send the high byte // // send another random integer intValue = random(599); // // generate a random number between 0 and 599 // send the two // bytes that comprise that integer // Serial.write(lowByte(intValue)); // send the low byte // Serial.write(highByte(intValue)); // send the high byte // delay(1000); } |
ارسال دادههای باینری نیاز به برنامهریزی دقیق دارد؛ زیرا در غیر این صورت ممکن است شما پیامهای نامفهومی دریافت کنید. برخلاف دادههای متنی که با حضور کاراکتر پایاندهنده (مثل کاراکتر بازگشت خط یا کاراکتر منحصربهفرد دیگری که انتخاب میکنید) مشخص میشود که پیام کجا تمام میشود، در دادههای باینری ممکن است نتوانید بهتنهایی از دادهها بفهمید که پیام کجا شروع میشود یا تمام میشود. این دادهها میتوانند هر مقداری داشته باشند؛ به عنوان مثال، میتوانند مقدار یک کاراکتر هدر یا پایاندهنده را داشته باشند.
شما میتوانید بهراحتی این مشکل را حل کنید با طراحی پیامهای خود بهنحویکه طرفهای ارسالکننده و گیرنده دقیقاً بدانند چند بایت انتظار دارند. پایان یک پیام توسط تعداد بایتهای ارسالی تعیین میشود و نیازی به تشخیص یک کاراکتر خاص نیست. این میتواند با ارسال یک مقدار اولیه برای اعلام تعداد بایتهایی که پیگیری میشوند، پیادهسازی شود.
همچنین، میتوانید اندازه پیام را طوری تنظیم کنید تا اندازهای کافی برای نگهداری دادههایی که میخواهید ارسال کنید، داشته باشد. انجام هر دوی این روشها همیشه آسان نیست؛ زیرا پلتفرمها و زبانهای مختلف ممکن است از اندازههای متفاوتی برای انواع دادههای باینری استفاده کنند. هم تعداد بایتها و هم ترتیب آنها ممکن است با آردوینو متفاوت باشد.
در ارتباطات سریال، طرف دریافتکننده تشخیص میدهد که مقدار به طور کامل توسط کاراکتر بازگشت خط (carriage return) یا کاراکتر دیگری که نمایانگر پایان پیام است، دریافت شده است یا خیر. انتقالهای باینری در صورتی میتوانند در مورد ترکیب یک پیام اطلاعاتی مطلع شوند که یا این ترکیب بهصورت پیشفرض تعریف یا در پیام مشخص شده باشد.
انجام این کار نیاز به درک انواع دادهها در پلتفرمهای ارسال و دریافت و همچنین، برنامهریزی دقیق دارد.
ارسال تک بایت آسان است. برای این کار شما میتوانید از Serial.write (byteVal) استفاده کنید. برای ارسال یک عدد صحیح از آردوینو، باید بایتهای کم (low byte) و زیاد (high byte) که عدد صحیح را تشکیل میدهند ارسال کنید. این کار را میتوانید با استفاده از توابع lowByte و highByte انجام دهید:
1 2 | Serial.write(lowByte(intValue)); Serial.write(highByte(intValue)); |
برای ارسال یک عدد صحیح بلند (long) در Arduino، شما باید چهار بایت که عدد را تشکیل میدهند، به دو بخش 16 بیتی تقسیم کنید. سپس، باید هر یک از این دو بخش را با استفاده از روش ارسال عدد صحیح (integer) که قبلاً توضیح داده شد، ارسال کنید:
1 2 | long longValue = 2147483648; int intValue; |
ابتدا مقدار صحیح 16 بیتی پایین تر (lower) را ارسال کنید:
1 2 3 | intValue = longValue & 0xFFFF; // get the value of the lower 16 bits Serial.write(lowByte(intValue)); Serial.write(highByte(intValue)); |
سپس باید مقدار عدد صحیح 16 بیتی بالاتر (higher) را ارسال کنید:
1 2 3 | intValue = longValue >> 16; // get the value of the higher 16 bits Serial.write(lowByte(intValue)); Serial.write(highByte(intValue)); |
شما میتوانید برای ارسال دادهها توابعی ایجاد کنید. در ادامه، یک تابع نمونه را برای ارسال یک عدد صحیح 16 بیتی به پورت سریال آردوینو مشاهده میکنید:
1 2 3 4 | // function to send the given integer value to the serial port void // sendBinary(int value) { // send the two bytes that comprise a two-byte // (16-bit) integer Serial.write(lowByte(value)); // send the low byte // Serial.write(highByte(value)); // send the high byte } |
تابع زیر مقدار یک عدد صحیح long (چهار بایت) را ابتدا با ارسال دو بایت پایینتر (راستترین) و سپس بایتهای بالاتر (سمت چپ) ارسال میکند:
1 2 3 4 5 | // function to send the given long integer value to the serial port void // sendBinary(long value) { // first send the low 16-bit integer value int temp // = value & 0xFFFF; // get the value of the lower 16 bits sendBinary(temp); // // then send the higher 16-bit integer value: temp = value >> 16; // get the // value of the higher 16 bits sendBinary(temp); } |
این توابع برای ارسال مقادیر عددی بهصورت باینری (binary) دارای نام یکسانی هستند: sendBinary. کامپایلر، این توابع را بر اساس نوع مقداری که بهعنوان پارامتر استفاده میشود، تشخیص میدهد. اگر کد شما تابع sendBinary را با یک مقدار دو بایتی فراخوانی کند، نسخهای که بهعنوان void sendBinary (int value) تعریف شده است، فراخوانی میشود. اگر پارامتر یک مقدار عددی بلند (long) باشد، نسخهای که بهعنوان void sendBinary(long value) تعریف شده است، فراخوانی میشود. این رفتار بهعنوان “تابعهای چندگانه” (function overloading) شناخته میشود.
شما میتوانید از ساختارها (structures) برای ارسال دادههای باینری (binary data) استفاده کنید. ساختارها وقتی مفید هستند که شما نیاز به خواندن یا نوشتن دادههای باینری با یک ساختار خاص دارید. در زیر نمونهای از ارسال بایتها در یک ساختار به پورت سریال بهعنوان دادههای باینری آمده است. این نمونه شامل کاراکتر هدر (header) در ساختار است، بنابراین پیامهای مشابهی با راهحل دیگر ارسال میشود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | * SendBinaryStruct sketch Sends a struct as binary data. */ typedef struct { char padding; // ensure same alignment on 8-bit and 32-bit char header; short intValue1; short intValue2; } shortMsg; void setup() { Serial.begin(9600); } void loop() { shortMsg myStruct = { 0, 'H', (short) random(599), (short) random(599) }; sendStructure((byte *)&myStruct, sizeof(myStruct)); delay(1000); } void sendStructure(byte *structurePointer, int structureLength) { int i; for (i = 0 ; i < structureLength ; i++) { Serial.write(structurePointer[i]); } } |
اگر ساختار shortMsg را بدون عضو پدینگ (padding) تعریف کنید، ممکن است در یک برد، طول ساختار پنج بایت و در برد دیگر، شش بایت باشد. این موضوع به این دلیل است که کامپایلر یک معماری ممکن است بهراحتی یک ساختار پنج بایتی را قبول کند، اما معماری دیگر ممکن است یک یا چند بایت اضافی را درج کند تا اندازه ساختار چندبرابر اندازه دادههای طبیعی برد باشد. با قراردادن پدینگ در جلو، شما اطمینان حاصل میکنید که کاراکتر char در مرز زوجی قرار میگیرد (دومین بایت)، بنابراین کامپایلر احتمالاً پدینگ را بین مقادیر char و short درج نمیکند. اما این ترفند همیشه تضمینی نیست، بنابراین ممکن است نیاز به آزمایش داشته باشید.
ارسال دادهها بهصورت بایتهای باینری مقرونبهصرفهتر از ارسال آنها بهصورت متنی است، اما تنها در صورتی قابلاطمینان عمل میکند که طرفهای فرستنده و گیرنده دقیقاً در مورد ترکیب دادهها موافقت کرده باشند. در زیر خلاصهای از نکات مهمی که باید در هنگام نوشتن کد خود بررسی کنید، آمده است:
اندازه متغیر (Variable size)
اطمینان حاصل کنید که اندازه دادههای ارسال شده در هر دو طرف یکسان است. یک عدد صحیح در آردوینو Uno و سایر بردهای 8 بیتی دو بایت و در بردهای 32 بیتی و اکثر پلتفرمهای دیگر چهار بایت است. هیچ مشکلی برای دریافت یک عدد صحیح دو بایتی آردوینو به عنوان یک عدد صحیح چهار بایتی در پردازش وجود ندارد، تا زمانی که پردازش انتظار دارد فقط دو بایت دریافت کند. اما مطمئن باشید که طرف ارسال کننده از مقادیری استفاده نمی کند که نوع استفاده شده توسط طرف گیرنده را سرریز (اورفلو یا overflow) کند.
ترتیب بایتها (Byte order)
مطمئن شوید که بایتها در داخل یک عدد صحیح (int) یا عدد صحیح بلند (long) به ترتیبی که گیرنده انتظار دارد، ارسال میشوند. راهحل ساده است از همان ترتیب بایتی استفاده کنید که بردهای آردوینو از آن استفاده میکنند، این ترتیب little endian نامیده میشود. این به ترتیبی اشاره دارد، بکه بایت کمارزشتر (least significant byte) ابتدا ظاهر میشود.
به طور فنی، بردهای آردوینو با معماری ARM دارای دو ترتیب بایت (bi-endian) هستند، به این معنا که میتوانند به حالت big-endian یا little-endian تنظیم شوند، اما در عمل، بهاحتمال زیاد با برد آردوینویی که little endian نیست، مواجه نخواهید شد.
وقتی از توابع lowByte و highByte برای تجزیه یک عدد صحیح استفاده میکنید، شما در کنترل ترتیب ارسال بایتها قرار دارید. اما وقتی یک ساختار (struct) را بهصورت باینری ارسال میکنید، از نمایش داخلی ساختار استفاده خواهد شد.
همگامسازی (Synchronization)
برای همگامسازی در انتقال دادههای باینری، ارسال یک توالی بایت که در بدنه پیام وجود نخواهد داشت، میتواند مفید باشد. بهعنوانمثال، اگر دادههای باینری را از سنسورها میخوانید و این دادهها در بازهی ۰ تا ۱۰۲۳ قرار دارند، میتوانید از دو بایت ابتدایی با مقدار ۴ (یا هر مقدار بزرگتر از ۳) برای نشاندادن شروع یا پایان پیام استفاده کنید. این روش به شما امکان میدهد تا دریافت کامل و صحیح دادهها را تضمین کنید.
کنترل جریان (Flow control)
در انتقال دادهها، میتوانید سرعت انتقال را به نحوی انتخاب کنید که اطمینان حاصل شود که طرف دریافتکننده قادر به پیگیری سرعت ارسال باشد، یا از نوعی کنترل جریان استفاده کنید. کنترل جریان (Flow control) به فرستنده اطلاع میدهد که دریافتکننده آماده دریافت دادههای بیشتر است.