در قسمت هفدهم از آموزش آردوینو به بررسی ارسال اطلاعات از آردوینو به کامپیوتر و ارسال متن فرمت شده و دادههای عددی از آردوینو، پرداختیم. در این قسمت قصد داریم درباره دریافت سریال دیتا در آردوینو و همچنین، ارسال چندین فیلد متنی از آردوینو در یک پیام صحبت کنیم.
دریافت سریال دیتا
در ادامه این مطلب، چگونگی دریافت دیتاهای آردوینو از کامپیوتر یا دستگاه سریال دیگری را بررسی خواهیم کرد.
این اسکچ یک رقم (تک کاراکتری از 0 تا 9) دریافت می کند و سپس سرعت چشمک زدن LED، متناسب با این رقم دریافتی تغییر می کند.
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 28 29 30 31 | /* * SerialReceive sketch * Blink the LED at a rate proportional to the received digit value */ int blinkDelay = 0; // blink delay stored in this variable void setup() { Serial.begin(9600); // Initialize serial port to send and receive at 9600 baud pinMode(LED_BUILTIN, OUTPUT); // set this pin as output } void loop() { if (Serial.available()) // Check to see if at least one character is available { char ch = (char) Serial.read(); if( isDigit(ch) ) // is this an ASCII digit between 0 and 9? { blinkDelay = (ch - '0'); // ASCII value converted to numeric value blinkDelay = blinkDelay * 100; // actual delay is 100 ms *" received digit } } blink(); } // blink the LED with the on and off times determined by blinkDelay void blink() { digitalWrite(LED_BUILTIN, HIGH); delay(blinkDelay); // delay depends on blinkDelay value digitalWrite(LED_BUILTIN, LOW); delay(blinkDelay); } |
ابتدا اسکچ را جایگذاری و آپلود کنید و سپس با استفاده از سریال مانیتور پیام ارسال کنید. سریال مانیتور را با کلیککردن روی نماد مانیتور (بالا سمت راست)، باز کنید و یک رقم را در کادر متنی بالای پنجره نمایشگر سریال تایپ کنید. با کلیک بر روی دکمه ارسال، کاراکتر تایپ شده در کادر متن، ارسال میشود. اگر رقمی را تایپ کنید، نرخ چشمکزدن (blink rate) را باید مشاهده کنید.
تابع Serial.read مقدار int را برمیگرداند؛ بنابراین برای مقایسههای بعدی باید آن را به یک char ارسال کنید. روش تبدیل به کارکتر به صورت زیر است.
1 | char ch = (char) Serial.read(); |
حال نیاز است که مقادیر ASCII به عداد تبدیل شود اگر با روشی که ASCII، کاراکترها را نشان میدهد آشنا نباشید، احتمالاً تبدیل کاراکترهای ASCII به مقادیر عددی برای شما کمی مشکل به نظر برسد. کد زیر کاراکتر ch را به مقدار عددی آن تبدیل میکند:
1 | blinkDelay = (ch - '0'); // ASCII value converted to numeric value |
کاراکترهای ”0 تا ‘9’ ASCII دارای مقدار 48 تا 57 هستند. تبدیل ‘1’ به مقدار عددی 1 با تفریق ‘0’ انجام می شود؛ زیرا ‘1’ دارای مقدار ASCII، 49 است؛ بنابراین باید مقدار 48 (ASCII ‘0’) از آن کم شود تا این عدد به عدد 1 تبدیل شود. برای مثال، اگر ch نشان دهنده کاراکتر 1 است، مقدار ASCII آن 49 می باشد. عبارت ‘0’ -49 همان 48 – 49 است. این مقدار برابر با 1 است که مقدار عددی کاراکتر 1 میباشد.
- بهعبارتدیگر، عبارت (ch -‘0’) همان (ch- 48) است. این مقدار ASCII متغیر ch را به یک مقدار عددی تبدیل میکند.
- شما با استفاده از روشهای parseInt و parse Float که استخراج مقادیر عددی از سریال را ساده میکنند، میتوانید اعدادی با بیش از یک رقم دریافت کنید. (همچنین، با استفاده از Ethernet و سایر آبجکتهای کلاس Stream نیز این کار امکانپذیر است.)
- ()Serial.parseInt و ()Serial.parseFloat کاراکترهای سریال را میخوانند و نمایش عددی آنها را برمیگردانند. کاراکترهای غیرعددی قبل از عدد نادیده گرفته میشوند و عدد با اولین کاراکتر که رقم عددی نیست (یا “.” در صورت استفاده از parseFloat) به پایان میرسد.
اگر هیچ کاراکتر عددی در ورودی وجود نداشته باشد، توابع 0 را برمیگردانند؛ بنابراین باید مقادیر صفر را بررسی و به درستی مدیریت کنید.
اگر سریال مانیتور (Serial Monitor) را طوری کانفینگ کردهاید که وقتی روی گزینه Send کلیک میکنید، یک کاراکتر جدید یا یک کاراکتر CR (Carriage Return) یا هر دو را ارسال می کند (همان منوی کشویی کنار نرخ ارتباط (baud rate))، parseInt یا parseFloat، این کاراکتر جدید یا کاراکتر CR را به عنوان یک عدد تفسیر می کند و در نتیجه، صفر برمیگرداند. این کار باعث می شود که blinkDe lay بلافاصله پس از تنظیم مقدار موردنظر خود روی صفر تنظیم شود که منجر به چشمک نزدن LED می شود:
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 28 29 | /* * SerialParsing sketch * Blink the LED at a rate proportional to the received digit value */ int blinkDelay = 0; void setup() { Serial.begin(9600); // Initialize serial port to send and receive at 9600 baud pinMode(LED_BUILTIN, OUTPUT); // set this pin as output } void loop() { if ( Serial.available()) // Check to see if at least one character is available { int i = Serial.parseInt(); if (i != 0) { blinkDelay = i; } } blink(); } // blink the LED with the on and off times determined by blinkDelay void blink() { digitalWrite(LED_BUILTIN, HIGH); delay(blinkDelay); // delay depends on blinkDelay value digitalWrite(LED_BUILTIN, LOW); delay(blinkDelay); } |
روش دیگری برای تبدیل رشته (string) های متنی که اعداد را نشان میدهند، استفاده از تابع تبدیل زبان C به نام atoi (برای متغیرهای int) یا atol (برای متغیرهای long) میباشد. این توابع با نام نامشخص یک رشته را به اعداد صحیح یا اعداد صحیح long تبدیل میکنند. برای استفاده از این توابع، قبل از اینکه تابع تبدیل را فراخوانی کنید، باید کل رشته (string) را در یک آرایه کاراکتری دریافت و ذخیره کنید.
این کد، ارقام ورودی را در هر کاراکتری که یک رقم نیست (یا هنگامی که بافر پر است) پایان میدهد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const int maxChars = 5; // an int string contains up to 5 digits and // is terminated by a 0 to indicate end of string char strValue[maxChars+1]; // must be big enough for digits and terminating null int idx = 0; // index into the array storing the received digits void loop() { if( Serial.available()) { char ch = (char) Serial.read(); if( idx < maxChars && isDigit(ch) ){ strValue[idx++] = ch; // add the ASCII character to the string; } else { // here when buffer full or on the first nondigit strValue[idx] = 0; // terminate the string with a 0 blinkDelay = atoi(strValue); // use atoi to convert the string to an int idx = 0; } } blink(); } |
strValue یک رشته عددی است که از کاراکترهای دریافت شده از پورت سریال ساخته شده است.
atoi (مخفف ASCII to integer) تابعی است که رشته کاراکتری را به عدد صحیح تبدیل میکند (atol به عدد صحیح long تبدیل میشود).
همچنین، آردوینو از تابع serialEvent پشتیبانی میکند و شما میتوانید از آن برای مدیریت کاراکترهای سریال ورودی استفاده کنید. اگر در اسکچ خود، کدی در تابع serialEvent دارید، این کد هر بار از طریق تابع loop فراخوانی میشود. اسکچ زیر همان کار اولین اسکچ این مطلب را انجام میدهد، با این تفاوت که از serialEvent برای مدیریت کاراکترهای ورودی استفاده میکند:
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 28 29 30 31 32 33 34 35 | /* * SerialEvent Receive sketch * Blink the LED at a rate proportional to the received digit value */ int blinkDelay = 0; // blink delay stored in this variable void setup() { Serial.begin(9600); // Initialize serial port to send and receive at 9600 baud pinMode(LED_BUILTIN, OUTPUT); // set this pin as output } void loop() { blink(); } void serialEvent() { while(Serial.available()) { char ch = (char) Serial.read(); Serial.write(ch); if( isDigit(ch) ) // is this an ASCII digit between 0 and 9? { blinkDelay = (ch - '0'); // ASCII value converted to numeric value blinkDelay = blinkDelay * 100; // actual delay is 100 ms times digit } } } // blink the LED with the on and off times determined by blinkDelay void blink() { digitalWrite(LED_BUILTIN, HIGH); delay(blinkDelay); // delay depends on blinkDelay value digitalWrite(LED_BUILTIN, LOW); delay(blinkDelay); } |
برای کسب اطلاعات بیشتر درباره “atoi” و “atol” به لینک زیر مراجعه کنید:
ارسال چندین فیلد متنی در یک پیام
در ادامه این مطلب درباره چگونگی ارسال پیامی که حاوی اطلاعاتی بیش از یک فیلد در هر پیام است، صحبت خواهیم کرد. بهعنوانمثال، پیامی که دارای مقادیری از دو یا چند سنسور باشد. شما میتوانید از این مقادیر در برنامههایی مانند پردازش، در زمان اجرا بر روی کامپیوتر یا دستگاهی مثل Raspberry Pi استفاده کنید.
یک راه آسان برای انجام این کار، ارسال یک رشته متنی است که در آن تمام فیلدها با یک کاراکتر جداکننده (separating) مانند کاما از هم جدا شدهاند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // CommaDelimitedOutput sketch void setup() { Serial.begin(9600); } void loop() { int value1 = 10; // some hardcoded values to send int value2 = 100; int value3 = 1000; Serial.print('H'); // unique header to identify start of message Serial.print(","); Serial.print(value1,DEC); Serial.print(","); Serial.print(value2,DEC); Serial.print(","); Serial.print(value3,DEC); Serial.println(); // send a carriage return and line feed delay(100); } |
در اینجا کد پایتون پردازش برای برد رزبری پای آورده شده است؛ این کد دیتا را از پورت سریال میخواند:
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 28 29 30 31 32 33 | // Processing Sketch to read comma delimited serial // expects format: H,1,2,3 import processing.serial.*; Serial myPort; // Create object from Serial class char HEADER = 'H'; // character to identify the start of a message short LF = 10; // ASCII linefeed // WARNING! // If necessary change the definition below to the correct port short portIndex = 0; // select the com port, 0 is the first port void setup() { size(200, 200); println( (Object[]) Serial.list()); println(" Connecting to -> " + Serial.list()[portIndex]); myPort = new Serial(this, Serial.list()[portIndex], 9600); } void draw() { if (myPort.available() > 0) { String message = myPort.readStringUntil(LF); // read serial data if (message != null) { message = message.trim(); // Remove whitespace from start/end of string println(message); String [] data = message.split(","); // Split the comma-separated message if (data[0].charAt(0) == HEADER && data.length == 4) // check validity { for (int i = 1; i < data.length; i++) // skip header (start at 1, not 0) { println("Value " + i + " = " + data[i]); // Print the field values } println(); } } } |
در اینجا کد آردوینو، رشته متنی زیر را به پورت سریال میفرستد (r\ نشاندهنده کاراکتر CR یا همان Carriage Return و n\ نشاندهنده کاراکتر LF همان Line Feed میباشد):
1 | H,10,100,1000\r\n |
دراینخصوص، شما باید یک کاراکتر جداکننده (separating) انتخاب کنید که هرگز در دیتاهای واقعی وجود نداشته باشد. بهعنوانمثال، اگر دیتاهای شما فقط از مقادیر عددی تشکیل شده است، کاما انتخاب خوبی برای جداکننده است.
هر زمان که آردوینو با استفاده از تابع ()println پرینت میکند، کاراکترهای CR (Carriage Return) و LF (Line Feed) ارسال میشوند که این کار به طرف گیرنده (receiving side) کمک میکند تا بفهمد که آیا رشته پیام (message string) کامل دریافت شده است یا خیر.
کد پردازش، پیام را بهصورت رشتهای میخواند و از متد () split جاوا برای ایجاد یک آرایه از فیلدهای جدا شده با کاما استفاده میکند.
✅نکته
خواندن پردازش برای نمایش مقادیر سنسور میتواند به شما در تجسم دیتاها کمک کند و در نتیجه، باعث صرفه جویی در زمان دیباگ شود. اگرچه CSV یک فرمت رایج و مفید است، اما JSON (Java Script Object Notation) گویاتر و برای انسان قابل خواندن است. JSON یک فرمت رایج تبادل داده است که برای تبادل پیام در یک شبکه استفاده میشود.
اسکچ زیر شتابسنج را از Arduino WiFi Rev 2 یا Arduino Nano 33 BLE Sense میخواند و آن را با استفاده از JSON به پورت سریال میفرستد (بهعنوانمثال: {‘x’: 0.66, ‘y’: 0.59, ‘z’: -0.49, }):
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 28 | /* AccelerometerToJSON. Sends JSON-formatted representation of accelerometer readings. */ #include <Arduino_LSM6DS3.h> // Arduino WiFi R2 //#include <Arduino_LSM9DS1.h> // Arduino BLE Sense void setup() { Serial.begin(9600); while (!Serial); if (!IMU.begin()) { while (1) { Serial.println("Error: Failed to initialize IMU"); delay(3000); } } } void loop() { float x, y, z; if (IMU.accelerationAvailable()) { IMU.readAcceleration(x, y, z); Serial.print("{"); Serial.print("'x': "); Serial.print(x); Serial.print(", "); Serial.print("'y': "); Serial.print(y); Serial.print(", "); Serial.print("'z': "); Serial.print(z); Serial.print(", "); Serial.println("}"); delay(200); } } |
برنامه پردازش برد رزبری پای زیر، نمایش بصری real-time تا مقدار 12 از آردوینو را اضافه می کند. این اسکچ، مقادیر ممیز شناور (floating-point) را در محدوده 5- تا 5+ نمایش می دهد:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | /* * ShowSensorData. * * Displays bar graph of JSON sensor data ranging from -127 to 127 * expects format as: "{'label1': value, 'label2': value,}\n" * for example: * {'x': 1.0, 'y': -1.0, 'z': 2.1,} */ import processing.serial.*; import java.util.Set; Serial myPort; // Create object from Serial class PFont fontA; // font to display text int fontSize = 12; short LF = 10; // ASCII linefeed int rectMargin = 40; int windowWidth = 600; int maxLabelCount = 12; // Increase this if you need to support more labels int windowHeight = rectMargin + (maxLabelCount + 1) * (fontSize *2); int rectWidth = windowWidth - rectMargin*2; int rectHeight = windowHeight - rectMargin; int rectCenter = rectMargin + rectWidth / 2; int origin = rectCenter; int minValue = -5; int maxValue = 5; float scale = float(rectWidth) / (maxValue - minValue); // WARNING! // If necessary change the definition below to the correct port short portIndex = 0; // select the com port, 0 is the first port void settings() { size(windowWidth, windowHeight); } void setup() { println( (Object[]) Serial.list()); println(" Connecting to -> " + Serial.list()[portIndex]); myPort = new Serial(this, Serial.list()[portIndex], 9600); fontA = createFont("Arial.normal", fontSize); textFont(fontA); } void draw() { if (myPort.available () > 0) { String message = myPort.readStringUntil(LF); if (message != null) { // Load the JSON data from the message JSONObject json = new JSONObject(); try { json = parseJSONObject(message); } catch(Exception e) { println("Could not parse [" + message + "]"); } // Copy the JSON labels and values into separate arrays. ArrayList<String> labels = new ArrayList<String>(); ArrayList<Float> values = new ArrayList<Float>(); for (String key : (Set<String>) json.keys()) { labels.add(key); values.add(json.getFloat(key)); } // Draw the grid and chart the values background(255); drawGrid(labels); fill(204); for (int i = 0; i < values.size(); i++) { drawBar(i, values.get(i)); } } } } // Draw a bar to represent the current sensor reading void drawBar(int yIndex, float value) { rect(origin, yPos(yIndex)-fontSize, value * scale, fontSize); } void drawGrid(ArrayList<String> sensorLabels) { fill(0); // Draw the minimum value label and a line for it text(minValue, xPos(minValue), rectMargin-fontSize); line(xPos(minValue), rectMargin, xPos(minValue), rectHeight + fontSize); // Draw the center value label and a line for it text((minValue+maxValue)/2, rectCenter, rectMargin-fontSize); line(rectCenter, rectMargin, rectCenter, rectHeight + fontSize); // Draw the maximum value label and a line for it text(maxValue, xPos(maxValue), rectMargin-fontSize); line( xPos(maxValue), rectMargin, xPos(maxValue), rectHeight + fontSize); // Print each sensor label for (int i=0; i < sensorLabels.size(); i++) { text(sensorLabels.get(i), fontSize, yPos(i)); text(sensorLabels.get(i), xPos(maxValue) + fontSize, yPos(i)); } } // Calculate a y position, taking into account margins and font sizes int yPos(int index) { return rectMargin + fontSize + (index * fontSize*2); } // Calculate a y position, taking into account the scale and origin int xPos(int value) { return origin + int(scale * value); } |
شکل 1 نشان می دهد که چگونه مقادیر شتاب سنج (x، y، z) نمایش داده می شوند. هنگامی که دستگاه را تکان دهید، نوارها ظاهر می شوند.
در صورت نیاز، میتوان محدوده مقادیر و مبدأ نمودار را بهراحتی تغییر داد. بهعنوانمثال، برای نمایش نوارهایی از محور چپ با مقادیر 0 تا 1024، می توانید از موارد زیر استفاده کنید:
1 2 3 | int origin = rectMargin; // rectMargin is the left edge of the graphing area int minValue = 0; int maxValue = 1024; |
اگر شتابسنج ندارید، میتوانید با اسکچ ساده زیر مقادیری ایجاد کنید که مقادیر ورودی آنالوگ را نمایش دهد و اگر هیچ سنسوری برای اتصال ندارید، با زدن انگشتان خود در پایین پینهای آنالوگ سطوحی ایجاد میشود که در اسکچ پردازش قابلمشاهده هستند. این مقادیر از 0 تا 1023 متغیر است؛ بنابراین میتوانید مبدأ و مقادیر حداقل و حداکثر را در اسکچ پردازش تغییر دهید، همانطور که در پاراگراف قبلی توضیح داده شد:
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 | /* AnalogToJSON. Sends JSON-formatted representation of analog readings. */ void setup() { Serial.begin(9600); while (!Serial); } void loop() { erial.print("{"); Serial.print("'A0': "); Serial.print(analogRead(A0)); Serial.print(", "); Serial.print("'A1': "); Serial.print(analogRead(A1)); Serial.print(", "); Serial.print("'A2': "); Serial.print(analogRead(A2)); Serial.print(", "); Serial.print("'A3': "); Serial.print(analogRead(A3)); Serial.print(", "); Serial.print("'A4': "); Serial.print(analogRead(A4)); Serial.print(", "); Serial.print("'A5': "); Serial.print(analogRead(A5)); Serial.print(", "); Serial.println("}"); |