Tohum

Playbook توسعه محصول IoT از ابتدا تا انتها – مرحله 4: توسعه firmware و نرم‌افزار Cloud

تصویرسازی: میکروکنترلری که با ابری دارای چرخ‌دنده داده مبادله می‌کند

مرحله 4 محصول را به سمت راست V می‌برد: پیاده‌سازی و تست. در V-model کلاسیک توسعه محصول، سمت چپ درباره طراحی است (که در مرحله‌های قبلی با آن مقابله کردیم)؛ اینجا firmware (نرم‌افزاری که روی دستگاه IoTتان اجرا می‌شود) و نرم‌افزار cloud (سرورها، دیتابیس‌ها، APIها، رابط‌های کاربری) را توسعه می‌دهیم که با هم محصولتان را به زندگی می‌آورند. برخلاف توسعه سخت‌افزار که وقتی روی برد مدار چاپ شد نسبتاً غیرمنعطف است، توسعه نرم‌افزار به‌شدت تکرارشونده و چابک است. در واقع، ممکن است از قبل در مرحله 3، حین تست طراحی سخت‌افزارتان، تکه‌هایی از firmware را نوشته باشید. حالا وقت مهمیاب‌کردن آن است. نرم‌افزار مثل گِل است—آسان شکل می‌گیرد و درجا تغییر می‌کند—پس می‌توانیم سریع تکرار کنیم و پیوسته پالایش کنیم. این مرحله با هر کار سخت‌افزاری باقی‌مانده موازی پیش می‌رود و هر دو در صورت نیاز یکدیگر را تغذیه می‌کنند.

مرحله 4 را می‌توان در دو بخش اصلی دید: اول برنامه‌ریزی معماری و طراحی نرم‌افزار (برای گذاشتن بنیانی محکم)، و بعد چرخه جاری اسپرینت‌های توسعه که در آن می‌سازیم، تست می‌کنیم و تکرار می‌کنیم تا نرم‌افزار محکم شود. بیا وارده جزئیات شویم.

4.1 معماری و طراحی نرم‌افزار

قبل از اینکه کسی به کدنویسی بشتابد، عاقلانه است یک طرح بازی برای معماری نرم‌افزار اسکیس شود—هم برای firmware روی دستگاه و هم برای اجزای cloud. آن را مثل برنامه‌ریزی یک سفر جاده‌ای بدانید. بدون نقشه رانندگی را شروع نمی‌کنید؛ مشابهاً، اجزای اصلی و نحوه تعاملشان را قبل از نوشتن هزاران خط کد اسکیس می‌کنیم. این مشابه معماری سیستمی است که در مرحله 2 انجام دادیم، اما حالا در سطح کد. برنامه‌ریزی خوب معماری نرم‌افزار به شما کمک می‌کند بعداً از «کد اسپاگتی» دوری کنید. این‌ها چیزهای کلیدی‌اند که باید در این فاز مشخص شوند:

معماری firmware (رویکرد لایه‌ای و ملاحظات RTOS)

ساختار کد embeddedتان را با لایه‌بندی‌اش تعریف کنید. رویکرد رایج در firmware یک معماری لایه‌ای است که به سازمان‌دهی منطقی کد کمک و نگه‌داری‌اش را آسان‌تر می‌کند. مثلاً در پایین‌ترین سطح ممکن است یک Board Support Package (BSP) یا لایه انتزاع سخت‌افزار داشته باشید—این شامل درایورهای سطح‌پایین خاص میکروکنترلر و پریفرال‌های سخت‌افزاری‌تان است. بالای آن، ممکن است درایورهای دستگاه یا ماژول‌هایی برای سنسورها و عملگرهایتان داشته باشید (مدیریت کارهایی مثل خواندن یک سنسور دما یا راه‌اندازی یک LED). بالای درایورها، می‌تواند یک لایه سرویس‌ها باشد—این ممکن است پروتکل‌های ارتباطی، الگوریتم‌های پردازش داده یا منطق دیگری را پیاده کند که داده خام را پردازش می‌کند. بالاخره، در قله، لایه کاربرد است که مغز دستگاه شماست: همه‌چیز را برای برآورده‌کردن هدف محصول به هم گره می‌زند (مثلاً خواندن مقادیر سنسور، تصمیم‌گیری، فرستادن داده به cloud و واکنش به فرمان‌ها). کشیدن یک دیاگرام ساده (دیاگرام کامپوننت UML یا حتی فقط جعبه‌ها و فلش‌ها روی تخته) برای تصور این لایه‌ها را در نظر بگیرید. این به همه در تیم کمک می‌کند جداسازی دغدغه‌ها را بفهمند—مثل ساختن خانه‌ای با پی، اسکلت و سقف، هر لایه کار خودش را دارد.

یک تصمیم معماری بزرگ دیگر در سمت firmware این است که از RTOS (سیستم‌عامل بلادرنگ) استفاده کنید یا نه. یک RTOS مثل داشتن یک سیستم‌عامل کوچک روی دستگاهتان است که می‌تواند چند تسک را ظاهراً هم‌زمان اجرا کند (از طریق زمان‌بندی). اگر دستگاهتان فقط یک کار را در یک لحظه انجام می‌دهد، شاید به RTOS نیاز نداشته باشید. اما بیشتر گجت‌های IoT چیزهای زیادی هم‌زمان دارند—مثلاً خواندن دوره‌ای سنسورها، گوش‌دادن به پیام‌های ورودی (از cloud یا ورودی کاربر)، به‌روزرسانی یک نمایشگر یا LED و مدیریت ارتباط شبکه. با یک RTOS، می‌توانید firmwareتان را به‌صورت مجموعه‌ای از تسک‌ها یا threadها طراحی کنید. مثلاً، می‌توانید یک تسک اختصاصی برای نمونه‌برداری سنسور داشته باشید، یکی دیگر برای مدیریت ارتباطات (فرستادن داده یا دریافت فرمان‌ها) و یکی برای امور نگه‌داری یا به‌روزرسانی رابط کاربری. به‌عنوان بخشی از طراحی معماری‌تان، این تسک‌ها و نحوه ارتباطشان با یکدیگر را ترسیم کنید—مکانیزم‌های رایج شامل صف‌های پیام، پرچم‌های رویداد یا داده اشتراکی محافظت‌شده با mutex است (برای دوری از تداخل). تصمیم‌گرفتن زودهنگام درباره این، کمک می‌کند firmwareتان به گره‌خورده‌ای از کد درهم‌تنیده که همه‌چیز به همه‌چیز وابسته است (معروف به کد اسپاگتی) تبدیل نشود. در عوض، رابط‌های روشنی خواهید داشت: شاید تسک سنسور خوانش‌های جدید را در یک صف بگذارد و تسک ارتباطات آن‌ها را برای فرستادن به cloud بردارد. اگر این رابط‌ها را حالا شفاف طراحی کنید، کدنویسی بسیار روان‌تر خواهد بود و بعداً وقتی وقت عیب‌یابی یا گسترش firmware رسید، از خودتان تشکر خواهید کرد.

چرا این همه سروصدا برای معماری firmware؟ چون زندگی‌تان را در ادامه راه آسان‌تر می‌کند. با یک طراحی لایه‌ای و ساختار تسک خوب، می‌توانید یک بخش را تغییر دهید (مثلاً یک درایور سنسور را عوض کنید یا یک الگوریتم را تنظیم کنید) بدون شکستن همه‌چیز دیگر. همچنین کارکردن چند توسعه‌دهنده با هم را آسان‌تر می‌کند بدون اینکه زیر پای هم باشند—یک نفر می‌تواند درایور سنسور را توسعه دهد در حالی که دیگری روی کد ارتباط cloud کار می‌کند، تا وقتی روی نحوه رابط آن بخش‌ها توافق کرده‌اید (مثلاً قالب داده یک پیام خوانش سنسور). خلاصه، کمی برنامه‌ریزی اینجا از درد زیاد بعد جلوگیری می‌کند.

معماری cloud (ساختار سرور، دیتابیس و کاربرد)

حالا سمت cloud را در نظر بگیریم. دستگاه IoT در انزوا زندگی نمی‌کند—معمولاً با یک سرویس cloud یا سروری حرف می‌زند. تفکر معماری مشابهی اینجا لازم است: اجزای اصلی نرم‌افزار cloudتان را اسکیس کنید. مثلاً، ممکن است یک سرور API داشته باشید که داده را از دستگاه‌ها دریافت می‌کند و همچنین به درخواست‌های اپلیکیشن‌های کاربر پاسخ می‌دهد. احتمالاً یک دیتابیس خواهد بود که داده دستگاه را در آن ذخیره می‌کنید (خوانش‌های سنسور، وضعیت‌های دستگاه، اطلاعات کاربر و غیره). ممکن است سرویس‌ها یا میکروسرویس‌های اضافی باشد که پردازش داده یا تحلیل می‌کنند—مثلاً تحلیل داده برای ناهنجاری‌ها، فعال‌کردن هشدارها یا اعمال وقتی شرایط خاصی برقرار می‌شود (مثلاً اگر خوانش دما زیادی بالا رفت ایمیل بفرست)، یا انجام تجمیع‌ها برای گزارش‌دهی. علاوه بر این، اپلیکیشن‌های کلاینت را در نظر بگیرید: شاید یک داشبورد وب یا یک اپ موبایل که کاربران با آن تعامل می‌کنند. آن کلاینت‌ها با سرور ارتباط برقرار می‌کنند (اغلب از طریق API) تا داده را نمایش دهند و فرمان‌های کاربر را بفرستند (مثل کاربری که دکمه‌ای را فشار می‌دهد تا لامپ IoTش را از گوشی‌اش روشن کند).

کشیدن دیاگرامی از این اکوسیستم مفید است—مدل C4 (Client-Container-Component-Code) روش محبوبی برای دیاگرام‌کشی معماری نرم‌افزار است. دست‌کم سطح Container را بکشید: دستگاه، سرور، دیتابیس و اپ کلاینت را نشان دهید و اینکه داده چگونه بینشان جریان دارد. مثلاً دیاگرامتان ممکن است فلشی از دستگاه به API ابری داشته باشد («داده سنسور را از طریق HTTP/MQTT می‌فرستد»)، فلشی از Cloud به دیتابیس («داده را ذخیره می‌کند») و فلشی از اپ کلاینت به Cloud («کاربر داده درخواست می‌کند یا فرمان می‌فرستد») و مانند این‌ها. این نقشه بصری تضمین می‌کند همه (توسعه‌دهندگان firmware، مهندسان cloud، توسعه‌دهندگان فرانت‌اند و غیره) مدل ذهنی روشنی از نحوه اتصال چیزها دارند. بحث و گرفتن مسائل طراحی با چنین دیاگرامی در دست بسیار آسان‌تر است. شاید در این مرحله کشف کنید به یک جزء اضافی نیاز دارید—مثلاً یک message broker یا سرویس پلتفرم IoT—برای مدیریت مقیاس به دستگاه‌های زیاد. بهتر است حالا به آن فکر کنید تا وقتی عمیق در کدنویسی هستید.

یک جنبه دیگر: در طراحی cloudتان به مقیاس‌پذیری و قابلیت اطمینان فکر کنید. اگر انتظار دارید سرانجام 100,000 دستگاه وصل شوند، آیا دیتابیس و طراحی سرور انتخابی‌تان از پسش برمی‌آید؟ اگر یک سرور داون شود چه اتفاقی می‌افتد (آیا افزونگی دارید)؟ آن جزئیات ممکن است در مرحله‌ای بعدتر بیشتر پر شوند، اما طراحی سطح‌بالا باید نگرانی‌های بزرگی از این نوع را از ابتدا لحاظ کند. مثلاً ممکن است تصمیم بگیرید به‌جای ساختن همه‌چیز از صفر، از یک پلتفرم IoT ابری (AWS IoT، Azure IoT Hub و غیره) استفاده کنید، که بخشی از تصمیم معماری‌تان می‌شود.

پروتکل‌های ارتباطی و قالب‌های داده (ارتباط دستگاه-Cloud)

یک محصول IoT ذاتاً ارتباط بین دستگاه و cloud را در بر دارد. بخشی از طراحی نرم‌افزار تصمیم‌گرفتن است درباره چگونه با هم حرف می‌زنند و به چه زبانی (پروتکل‌ها و قالب‌های داده). چند انتخاب پروتکل رایج در IoT هست: مثلاً، آیا دستگاهتان از MQTT، HTTP/REST، CoAP یا شاید چیزی مثل WebSockets استفاده می‌کند؟ هر کدام مزایا و معایبی دارد. MQTT یک پروتکل انتشار/اشتراک سبک است که برای IoT عالی است چون بهینه است و روی شبکه‌های غیرقابل‌اعتماد خوب کار می‌کند—بسیاری از سیستم‌های IoT از آن برای پیام‌رسانی دستگاه-به-cloud استفاده می‌کنند. HTTP/REST بسیار رایج و آسان برای تست است (چون می‌توانید با ابزارهای استاندارد وب به آن ضربه بزنید) اما ممکن است کمی سربار بیشتری به ازای هر پیام داشته باشد. CoAP یک پروتکل تخصصی IoT مشابه یک HTTP سبک است که اغلب در شبکه‌های محدود استفاده می‌شود. کلید انتخاب پروتکلی است که به توانایی‌های دستگاهتان و زیرساخت cloudتان بخورد. مثلاً، روی یک دستگاه به‌شدت محدود (میکروکنترلر 8 بیتی با باتری)، MQTT یا CoAP اغلب بهتر از HTTP می‌نشیند. اگر دستگاهتان تواناتر است (لینوکس یا مشابهش را اجرا می‌کند)، HTTP/REST خوب و سرراست‌تر است.

وقتی پروتکل انتخاب شد، قالب داده پیام‌ها را طراحی کنید. بسیاری از پروژه‌ها از JSON استفاده می‌کنند چون برای انسان خوانا و کار کردن با آن آسان است (بیشتر زبان‌ها پارسر JSON دارند). مثلاً یک پیام خوانش دما در JSON ممکن است این‌طور باشد:

{ "device_id": "ABC123", "timestamp": 1692124800, "temperature_c": 22.5 }

این فیلدها را شفاف پهن می‌کند. JSON البته کمی پرحرف است، پس اگر دستگاهتان نیاز دارد داده را خیلی بهینه بفرستد (به هزاران پیام یا پهنای باند خیلی کم فکر کنید)، ممکن است از یک قالب باینری (مثل Protocol Buffers، CBOR یا یک طرح باینری سفارشی) برای صرفه‌جویی در بایت‌ها استفاده کنید. انتخاب طراحی اینجا هم firmware و هم cloud را متاثر می‌کند: هر دو طرف باید روی پروتکل و قالب توافق کنند. کمی شبیه توافق روی یک زبان قبل از شروع گفت‌وگوست—اگر دستگاه MQTT+JSON «صحبت کند» و سرور چیز دیگری انتظار داشته باشد، یکدیگر را نخواهند فهمید.

یک ابزار مفید در این مرحله کشیدن یک دیاگرام توالی یا فقط نوشتن یک توالی نمونه از ارتباط است. مثلاً: «دستگاه یک خوانش سنسور می‌گیرد، یک پی‌لود JSON قالب‌بندی می‌کند، آن را از طریق MQTT به topic X می‌فرستد؛ broker ابری آن را دریافت می‌کند، سرویس ابری آن را پردازش و تأیید می‌کند، بعد در دیتابیس ذخیره می‌کند؛ بعداً کاربر از طریق اپ داده درخواست می‌کند، API ابری از دیتابیس می‌خواند و JSON را به اپ برمی‌گرداند.» نوشتن این جریان تضمین می‌کند همه (مخصوصاً توسعه‌دهنده firmware و توسعه‌دهنده cloud) عیناً درباره نحوه حرکت داده روی یک صفحه‌اند. می‌تواند از سوءتفاهم‌هایی جلوگیری کند مثل «اوه، فکر می‌کردم از timestamp یونیکس به ثانیه استفاده می‌کنیم، نه میلی‌ثانیه» یا «داشتیم داده را فشرده می‌کردیم یا نه؟» آن‌ها را حالا در طراحی بگیرید، در زمان دیباگ بعد صرفه‌جویی می‌کند.

استانداردهای کدنویسی و ابزارهای تیمی (کیفیت از ابتدا)

حالا، درباره اینکه چگونه کد می‌نویسیم و همکاری می‌کنیم حرف بزنیم، چون پیروی کل تیم از شیوه‌های مشترک ضروری است. روی استانداردهای کدنویسی برای هر بخش سیستم توافق کنید. برای توسعه C embedded، بسیاری از تیم‌ها رهنمودهای MISRA-C را می‌پذیرند—مجموعه‌ای از قواعد برای زبان C با هدف دوری از ساختارهای ناامن یا مستعد باگ (اصلاً از صنعت خودرو، اما در هر firmware حیاتی به‌گستردگی مفید). اگر از C++ در firmware استفاده می‌کنید، ممکن است تصمیم بگیرید کدام قابلیت‌های C++ مدرن مجاز یا ممنوع‌اند (تا چیزها برای سیستم‌های embedded امن بماند—مثلاً شاید دوری از تخصیص پویا در زمان اجرا و غیره). برای زبان‌های سطح‌بالاتر در سمت cloud مثل Python، احتمالاً از راهنمای سبک PEP 8 پیروی می‌کنید، و برای JavaScript/TypeScript شاید یک linter با سبک Airbnb یا هر استاندارد دیگری. هدف وسواس‌بازی نیست، تضمین انسجام و گرفتن دام‌های رایج است. وقتی همه با سبکی یکسان می‌نویسند، کدبیس مثل یک محصول واحد به نظر می‌رسد نه یک لکه‌دوز، و خواندن و نگه‌داشتن کد نوشته‌شده توسط کسی دیگر در تیم آسان‌تر است.

علاوه بر سبک کد، ابزارها و زیرساختی را که تیم استفاده خواهد کرد انتخاب کنید. این شامل کنترل نسخه و راه‌اندازی ریپوزیتوری است (تقریباً قطعاً Git—اگر هنوز استفاده نمی‌کنید، قطعاً شروع کنید، چون ستون فقرات کدنویسی مشترک است). روی استراتژی برنچ تصمیم بگیرید: توسعه trunk-based ساده خواهید داشت، یا GitFlow با برنچ‌های قابلیت و غیره؟ همچنین اگر هنوز نکرده‌اید یک سیستم ردیابی issue راه بیندازید (مثل Jira، Trello، Asana یا GitHub Issues) برای مدیریت تسک‌ها و user storyهای اسپرینت‌هایتان. زمان خوبی هم برای راه‌اندازی pipeline یکپارچه‌سازی مستمر (CI) است: یک سرویس CI انتخاب کنید (GitHub Actions، GitLab CI، Jenkins، CircleCI و غیره) و آن را طوری کانفیگ کنید که هر وقت تغییرات push می‌شود کدتان را به‌طور خودکار بیلد و تست کند. در بخش بعدی بیشتر درباره CI حرف می‌زنیم، اما مرحله معماری جایی است که مطمئن می‌شوید همه توسعه‌دهندگان به این ابزارها دسترسی دارند و استفاده ازشان را می‌دانند.

بخش دیگری از «فرهنگ کد» تصمیم‌گرفتن درباره چیزهایی مثل شیوه‌های بازبینی کد است—مثلاً آیا هر pull request به تأیید حداقل یک توسعه‌دهنده دیگر نیاز دارد؟ تعیین زودهنگام این انتظارات سالم است. شاید رسمی به نظر برسد، اما از سناریوهایی جلوگیری می‌کند که کد بازبینی‌نشده یک نفر باگ بدی یا حفره امنیتی معرفی می‌کند که بقیه خیلی بعدتر پیدایش می‌کنند. با توافق بر اینکه «ما همیشه بازبینی کد می‌کنیم، از راهنمای سبک پیروی می‌کنیم و تست‌ها را قبل از مرج اجرا می‌کنیم»، ذهنیت کیفیت را از روز اول نهادینه می‌کنید. توسعه مدرن به همان اندازه که درباره نبوغ کدنویسی است، درباره همکاری تیمی و انضباط هم هست.

بالاخره، به یاد داشته باشید این برنامه‌ریزی مرحله 4.1 درباره کشیدن دیاگرام‌های قشنگی نیست که هیچ‌کس هرگز بهشان نگاه نکند—درباره دادن درک مشترک و نقشه‌راه فنی به تیم است. مثل برنامه‌ریزی مسیر یک سفر جاده‌ای و برداشتن نقشه: می‌دانید پیچ‌های بزرگ کجا‌اند و چگونه مسیریابی می‌کنید، اما هنوز می‌توانید با توقف‌ها یا انحراف‌های جاده انعطاف داشته باشید. زمان معقولی روی این طراحی معماری بگذارید، اما کمال‌طلبی نکنید یا هر تابع آخر را جزئیات ندهید. طراحی به‌اندازه کافی می‌خواهید تا آدم‌ها بتوانند به شکلی هماهنگ شروع به کدنویسی کنند. با این بنیان گذاشته‌شده، آماده‌اید با اعتماد به نفس وارد توسعه شوید.

4.2 اسپرینت‌های توسعه – تکرار کن و فتح کن

با نقشه معماری در دست، ساختن آغاز می‌شود—نوشتن کد، تست‌کردنش و تماشای محصول IoTتان که واقعاً کارها را انجام می‌دهد. ما به این کار در تکرارها نزدیک می‌شویم، معمولاً با استفاده از اسپرینت‌ها همان‌طور که در متدولوژی Agile رایج است. یک اسپرینت چرخه‌ای کوتاه و ثابت‌طول است (معمولاً 2 هفته، اما می‌تواند 1 هفته یا تا 4 هفته بسته به ترجیح تیمتان باشد) که تیم متعهد می‌شود مجموعه‌ای از قابلیت‌ها یا بهبودها را تا انتهای آن تحویل دهد. ایده این است که کار بزرگ «توسعه همه نرم‌افزار» را به تکه‌های قابل‌جویدن تقسیم کنیم تا پیوسته با آن مقابله کنیم و پیشرفت ملموس بسازیم.

این رویکرد تکراری مزایای روشنی دارد: یکپارچه‌سازی مستمر اجزا را اجباری می‌کند (تا firmware و نرم‌افزار cloud مکرر با هم تست شوند)، و فرصت‌های زیادی برای گرفتن بازخورد و تصحیح مسیر در صورت نیاز می‌دهد. بسیار بهتر از 6 ماه کدنویسی و فقط بعدش تلاش برای دیدن اینکه همه‌چیز کار می‌کند یا نه (که دستور پخت غافلگیری‌های ناخوشایند است). ببینیم یک چرخه اسپرینت معمولی برای توسعه محصول IoT ما چه شکلی می‌تواند باشد:

برنامه‌ریزی اسپرینت و user storyها

هر اسپرینت با جلسه برنامه‌ریزی اسپرینت شروع می‌شود. تیم به بک‌لاگ محصول نگاه می‌کند—این فهرست مادر همه قابلیت‌ها، رفع‌ها و تسک‌هایی است که باید انجام شود، که ابتدا از نیازمندی‌هایی که در مرحله 1 جمع کردیم مشتق شده (و با یادگیری بیشتر در مرحله‌های بعد به آن اضافه شده). در برنامه‌ریزی، تیم مجموعه‌ای از user storyها یا تسک‌ها را از بک‌لاگ برای مقابله در اسپرینت انتخاب می‌کند، بر اساس اولویت و ظرفیت تیم.

یک user story توصیف کوچکی از یک قابلیت از دیدگاه کاربر نهایی است، اغلب پیرو قالب «به‌عنوان <نوع کاربر>، می‌خواهم <توانایی‌ای> تا <فایده‌ای>.» مثلاً، یک story می‌تواند این باشد: «به‌عنوان کاربر، می‌خواهم دستگاه IoT هر 10 دقیقه داده دما را به cloud بفرستد تا دمای اتاقم را از راه دور پایش کنم.» از این story، تیم می‌فهمد چه کارهایی در سمت دستگاه و cloud لازم است: شاید در firmware دستگاه، لازم باشد سنسور دما را بخوانیم و هر 600 ثانیه یک پیام MQTT بفرستیم؛ در cloud، به یک API یا message handler برای دریافت آن داده و جایی برای ذخیره‌اش نیاز داریم، و شاید یک رابط ساده برای نمایشش.

حین برنامه‌ریزی اسپرینت، تیم هر story انتخاب‌شده را بحث می‌کند تا معیارهای پذیرش را دقیق کند—در اصل، از کجا می‌فهمیم این story کی «انجام‌شده» است و کار می‌کند؟ برای مثال بالا، معیارها می‌تواند این باشد: «با داشتن دستگاهی با سنسور دما، وقتی 10 دقیقه کار می‌کند، حداقل یک خوانش دما باید در دیتابیس ابری و روی داشبورد کاربر قابل مشاهده باشد.» مشخص‌بودن به تیم کمک می‌کند دقیقاً بفهمد چه بسازد و چگونه راستی‌آزمایی کند. این زمان پرسیدن پرسش‌ها و شفاف‌کردن جزئیات است. آهن‌دزدیدن ابهام‌ها حالا بسیار بهتر از کشف بعداً است که firmwareی‌ها و cloudyها فرضیه‌های متفاوتی داشتند (مثلاً یک توسعه‌دهنده فکر می‌کرد هر 10 دقیقه وقتی فعال است، دیگری فکر می‌کرد همیشه و غیره). وقتی برنامه‌ریزی تمام شد، همه باید بدانند مسئول کدام تکه کار در اسپرینت پیش‌رو هستند.

پیاده‌سازی و یکپارچه‌سازی مستمر (CI)

حالا به پیاده‌سازی می‌رسیم—نوشتن کد برای جان‌دادن به آن user storyها. توسعه‌دهندگان شروع به کدنویسی قابلیت‌های firmware و cloud شناسایی‌شده برای اسپرینت می‌کنند. یک بهترین‌شِيوه کلیدی اینجا نوشتن تست‌های خودکار برای هر قابلیت جدید در حین توسعه‌اش است. برای firmware، این می‌تواند شامل نوشتن تست‌های واحد برای منطقی باشد که روی ماشین میزبان قابل اجراست (مثلاً اگر تابعی دارید که یک خوانش سنسور را پارس می‌کند یا یک state machine برای دستگاه، اغلب می‌توانید تستی مبتنی بر PC برایش بنویسید). برای نرم‌افزار cloud، معمولاً آسان‌تر است: می‌توانید تست‌های واحد برای توابعتان بنویسید و تست‌های یکپارچه‌سازی برای APIهایتان.

همان‌طور که توسعه‌دهندگان کد می‌نویسند، آن را به کنترل نسخه (Git) تحویل می‌دهند. این‌جا همان‌جایی است که راه‌اندازی یکپارچه‌سازی مستمر (CI) ما می‌درخشد. با CI، هر بار که کد push می‌شود یا pull requestی باز می‌شود، یک بیلد خودکار روی سرور CI راه می‌افتد. CI کد firmware را کامپایل می‌کند (شاید در چند کانفیگ در صورت نیاز)، هر تست واحدی که دارید اجرا می‌کند و شاید حتی firmware را به یک شبیه‌ساز یا دستگاه تست مستقر کند. همچنین نرم‌افزار cloud را بیلد می‌کند، همه تست‌هایش را اجرا می‌کند، سبک کدنویسی را بررسی می‌کند (linterها)، ابزارهای تحلیل ایستا را اجرا می‌کند و حتی پوشش کد را می‌سنجد (چقدر از کد توسط تست‌ها تمرین می‌شود). هدف گرفتن هر مشکلی فوراً است. اگر توسعه‌دهنده‌ای به‌تصادف باگی معرفی کند که باعث شکست تستی شود، سیستم CI بلافاصله علامتش می‌دهد—در عرض چند دقیقه از نوشته‌شدن کد—تا تیم قبل از بزرگ‌ترشدنش رفعش کند. این حلقه بازخورد سریع حیاتی است. رفع باگی که یک ساعت پیش معرفی‌اش کرده‌اید بسیار آسان‌تر است (هنوز یادتان است چه می‌کردید) تا باگی از ماه پیش (که تا آن زمان در خیلی تغییرات دیگر تنیده شده).

مثلاً، تصور کنید توسعه‌دهنده‌ای کد خوانش سنسور را تغییر می‌دهد و به‌تصادف باگ سرریز برای مقادیر خیلی بالا معرفی می‌کند. اگر تست واحدی داشته باشید که یک مقدار سنسور را به افراط می‌برد، CI آن شکست را می‌گیرد و تیم را مطلع می‌کند. توسعه‌دهنده بعد می‌تواند فوراً رسیدگی کند. بدون آن تست، باگ ممکن بود کمین کند تا موج گرما بیاید، سنسورهای واقعی دماهای بالا گزارش کنند و دستگاه در میدان کرش کند. CI و تست دقیقاً برای گرفتن این لحظه‌ها وجود دارند—به‌طور خودکار، و در عرض چند دقیقه از نوشته‌شدن کد.

یکپارچه‌سازی مستمر فقط درباره تست‌ها نیست؛ تیم‌ها اغلب ابزارهای تحلیل ایستا را هم در CI ادغام می‌کنند. ابزارهای تحلیل ایستا کد منبع‌تان را برای باگ‌های بالقوه، آسیب‌پذیری‌های امنیتی یا نقض‌های سبک بدون اجراکردنش بررسی می‌کنند. مثلاً می‌توانند چیزهایی مثل «احتمال ارجاع به پوینتر null» یا «متغیرهای استفاده‌نشده» یا بوی کد دیگری را بگیرند. بسیاری از تیم‌های embedded از ابزارهایی مثل PC-Lint یا Cppcheck یا clang-tidy برای C/C++ استفاده می‌کنند، و تیم‌های cloud از linterها (ESLint برای JavaScript، Pylint برای Python و غیره) و شاید اسکنرهای امنیتی. CI را می‌توان طوری کانفیگ کرد که اگر این ابزارها مسائل جدی پیدا کنند بیلد را fail کند. این یک خط کیفیت می‌گذارد—مثلاً «کد firmware باید 0 هشدار جدید داشته باشد و همه تست‌ها باید پاس شوند تا مرج مجاز باشد.» شاید سخت‌گیرانه به نظر برسد، اما کدبیس را سالم نگه می‌دارد.

علاوه بر این، CI می‌تواند پوشش کد را دنبال کند—چه درصدی از کدتان توسط تست‌های خودکار تمرین می‌شود. در حالی که پوشش 100٪ اغلب غیرواقعی است (و همه کد به یک اندازه آسان تست نمی‌شود)، بسیاری از تیم‌ها هدفی تعیین می‌کنند (مثلاً حداقل 80٪ پوشش) و آن را دنبال می‌کنند. اگر یک تحویل کد جدید ناگهان پوشش را از 80٪ به 50٪ برساند، آن یک پرچم قرمز است—شاید تست‌ها رد شده‌اند یا خیلی کد تست‌نشده اضافه شده. با پایش این متریک‌ها اسپرینت‌به‌اسپرینت، کیفیت کد بالا را در سراسر توسعه حفظ می‌کنید، به‌جای تلاش برای «پیچ‌کردنش» بعداً.

خلاصه، پیاده‌سازی در یک بستر تکراری فقط «نوشتن مشتی کد» نیست. این است: کد بنویس، آن کد را پیوسته در یک کدبیس مشترک یکپارچه کن، پیوسته تستش کن و مسائل را در مسیر رفع کن. نتیجه سیستم نرم‌افزاری است که همیشه در حالتی نیمه-کارا است و هر روز کمی بهتر می‌شود.

بیلدهای افزایشی و دموهای پایان اسپرینت

تا پایان هر اسپرینت، هدف دارید یک بیلد افزایشی از کل سیستم داشته باشید که بهبودی ملموس نسبت به قبلی باشد. در حالت ایده‌آل، چیزی است که واقعاً می‌توانید روی یک دستگاه و یک محیط cloud تستی اجرا کنید تا قابلیت‌های جدید را در عمل ببینید. شعار خوبی این است: «همیشه برای دمو آماده باش» در پایان اسپرینت. برای مثال محصول IoT ما، شاید در پایان اسپرینت 1، یک جریان سرتاسری پایه داشته باشیم که کار می‌کند: دستگاه یک مقدار سنسور ساختگی می‌خواند (شاید یک عدد ثابت یا یک مقدار تست تصادفی) و به cloud می‌فرستد، cloud آن را دریافت و ذخیره می‌کند و ما می‌توانیم دستی بررسی کنیم داده رفت (حتی اگر فقط نگاه مستقیم به دیتابیس یا لاگ‌ها باشد). آن از همین حالا یک نقطه عطف معنادار است—ثابت می‌کند کل لوله‌کشی از دستگاه تا cloud کار می‌کند!

تا اسپرینت 2، ممکن است مقدار ساختگی را با یک خوانش سنسور واقعی از سخت‌افزار جایگزین کنیم و یک صفحه وب ساده راه بیندازیم که آخرین خوانش را نمایش می‌دهد. حالا واقعاً می‌توانیم داده زنده ببینیم. تا اسپرینت 3، شاید یک قابلیت هشدار اضافه کنیم: اگر دما از آستانه‌ای بالاتر رفت، دستگاه یا cloud هشداری را فعال می‌کند (شاید یک LED چشمک می‌زند یا ایمیلی فرستاده می‌شود). و الی آخر. هر اسپرینت، کارکرد اضافی را لایه می‌گذاریم: شاید یک اسپرینت پشتیبانی از کنترل از راه دور را اضافه کند (فرستادن فرمان از cloud به دستگاه)، یکی دیگر نمودار UI قشنگی از داده تاریخی اضافه کند و غیره. بعد از تعدادی اسپرینت، سیستمی کامل‌القابلیت خواهید داشت. اما مهم اینکه، در هر مرحله، سیستمی کارا داشتید که به آن می‌افزودید. این یعنی اگر چیزی اشتباه شود، می‌دانید احتمالاً به تازه‌ترین اضافه‌ها مربوط است، که دیباگ را آسان‌تر می‌کند. همچنین یعنی ذی‌نفعان (مثل رئیس شما، یا مدیر محصول، یا حتی یک کاربر تستی) می‌توانند پیشرفت را ببینند و در سراسرش بازخورد بدهند.

دموهای منظم شیوه خوبی‌اند. در پایان هر اسپرینت (یا حتی مکررتر، مثل یک نمایش-و-گفتن سریع هر هفته)، تیم و هر ذی‌نفع علاقه‌مندی را جمع کنید و آنچه جدید است را نشان دهید. می‌تواند به سادگی این باشد: «دستگاه حالا داده سنسور واقعی می‌فرستد—تماشا کنید وقتی سنسور را با دستم گرم می‌کنم، و ببینید عدد روی این داشبورد هم‌زمان بالا می‌رود!» این دموها حس موفقیت و شتاب می‌سازند. همچنین انضباط یکپارچه‌سازی را اجباری می‌کنند: نمی‌توانید چیزی را دمو کنید که واقعاً با هم کار نمی‌کند، پس تیم را هل می‌دهد مسائل یکپارچه‌سازی را در مسیر صاف کند، به‌جای به‌تأخیرانداختن یکپارچه‌سازی تا همان انتها. تماشای جان‌گرفتن محصول تکه‌تکه، اسپرینت پس از اسپرینت، بسیار انگیزه‌بخش‌تر از کارکردن در خلاء برای ماه‌هاست.

تست و معیارهای تعریف انجام‌شده

درباره تست‌های خودکار حین توسعه حرف زدیم، اما ارزش تأکید دارد که چگونه تست در خود تعریف پیشرفت در هر اسپرنت بافته شده است. یک قابلیت یا user story فقط وقتی «انجام‌شده» تلقی می‌شود که معیارهای خاصی فراتر از «کد نوشته شد» را برآورده کند. تیم‌ها اغلب یک چک‌لیست تعریف انجام‌شده (DoD) دارند که می‌تواند شامل مواردی مثل این باشد:

فهرست بلندی است، و در نگاه اول می‌تواند مثل خیلی خانه‌های تیک‌خوردن به نظر برسد. اما این شیوه‌ها از دردسر زیاد بعد جلوگیری می‌کنند. گرفتن و رفع یک باگ یا نقص طراحی حالا، حین اسپرینت جاری، بسیار بهتر از سرخزیدنش به تست نهایی یا production است. تعریف انجام‌شده در اصل یعنی «این قابلیت نیمه‌پخته نیست؛ واقعاً یکپارچه، تست‌شده و آماده است.» وقتی هر بخش سیستمتان در مسیر این معیارها را برآورده می‌کند، فاز یکپارچه‌سازی و تست نهایی (مرحله 5) غافلگیری‌های کمی دارد—شما تمام مدت داشتید راستی‌آزمایی می‌کردید.

مستندسازی مستمر (نقشه‌ها را به‌روز نگه دارید)

احتمالاً در فاز معماری (مرحله 4.1) دیاگرام‌ها و مستنداتی ساختید. حالا، با پیش‌رفتن توسعه، مهم است آن اسناد طراحی را به‌روز نگه دارید. نرم‌افزار روش بامزه‌ای برای تکامل دارد—شاید کشف کنید به تسک جدیدی در RTOS برای یک کارکرد watchdog نیاز دارید، یا معماری cloud تغییر می‌کند تا یک لایه کش برای عملکرد اضافه شود. اگر مستندات معماری را به‌روز نکنید، از واقعیت منحرف می‌شوند و سرانجام هیچ‌کس بهشان اعتماد نمی‌کند. برای دوری از مشکل کلاسیک مستندات کهنه، با مستندات مثل بخش زنده پروژه رفتار کنید. بعضی تیم‌ها حتی دیاگرام‌هایشان را به‌عنوان کد می‌نویسند با ابزارهایی مثل PlantUML یا Mermaid. این‌طوری، «سورس» دیاگرام کنار کد در ریپوزیتوری زندگی می‌کند. وقتی تغییری انجام می‌شود که روی معماری اثر می‌گذارد، تیم کد دیاگرام را به‌روز می‌کند و تصویر را بازتولید می‌کند.

یک گزینه عملی (اگر تیمتان حاضر است) ادغام مستندسازی در فرایند CI هم هست. مثلاً، می‌توانید بررسی‌ای داشته باشید که اگر بعضی فایل‌های هسته تغییر کرد (مثل یک schema پیام یا رابط یک کامپوننت) و فایل مستندات متناظر به‌روز نشده بود، CI به شما یادآوری کند یا بیلد را fail کند. این توسعه‌دهندگان را هل می‌دهد: «هی، نحوه کارکرد سیستم را عوض کردی—لطفاً دیاگرام را به‌روز کن تا همه بدانند.» شاید اغراق به نظر برسد، اما همگام‌نگه‌داشتن مستندات وقتی جواب می‌دهد که توسعه‌دهنده جدیدی می‌پیوندد یا وقتی شش ماه بعد به پروژه برمی‌گردید و سعی می‌کنید یادتان بیاید چیزها چگونه سازمان‌دهی شده‌اند. حتی اگر خودکارش نکنید، دست‌کم هر چند اسپرینت یک بازبینی سریع دیاگرام‌ها را زمان‌بندی کنید تا مطمئن شوید هنوز واقعیت را منعکس می‌کنند.

حرف نهایی: مستندات به‌روز ارزش تلاش را دارد. یعنی هر کسی در تیم (یا بیرون تیم، مثل یک حسابرس انطباق یا یک شریک) می‌تواند به مستندات نگاه کند و بهشان اعتماد کند. از موقعیت‌هایی دوری می‌کنید مثل «ویکی را نخوان، قدیمی است—جو اطلاعات واقعی را در سرش دارد (یا در چند کامنت کد).» در عوض، دانش ثبت و در مسیر به‌روز می‌شود، که نشانه تیمی بالغ و کیفیت‌محور است.

دموهای منظم و حلقه‌های بازخورد

در بستر اسپرینت‌ها به دموها اشاره کردیم، اما بیا ارزش برقراری یک ریتم منظم از نشان‌دادن پیشرفت را تأکید کنیم. در Agile، این اغلب جلسه بازبینی اسپرینت است: در پایان اسپرینت، تیم آنچه ساخته را به ذی‌نفعان نشان می‌دهد (مدیران محصول، تیم‌های دیگر، گاهی حتی کاربران واقعی یا مدیران اجرایی). برای یک پروژه IoT، دمو می‌تواند تحت‌اللفظی باشد—نشان‌دادن دستگاه و سیستم که با هم کار می‌کنند. مثلاً ممکن است داشبورد وب را روی صفحه پروژه کنید و دستگاه فیزیکی روی میز باشد. بعد کاری کنید مثل فعال‌کردن دستگاه (فشاردادن دکمه‌ای یا ساختن شرطی) و نشان دهید cloud هم‌زمان به‌روز می‌شود. این لحظه‌ها واقعاً برای تیم تأییدکننده و برای ذی‌نفعان ارزشمند است. پروژه را واقعی حس می‌کند و همه را درگیر نگه می‌دارد. همچنین فرصتی برای گرفتن بازخورد است: شاید ذی‌نفعی بگوید، «باحاله، ولی می‌شود در نسخه بعدی داده رطوبت را هم نمایش بدهیم؟» یا «هشدار کمی دیر ظاهر شد، می‌توانیم سریع‌ترش کنیم؟» بازخورد زودهنگامی از این نوع مخصوصاً ارزشمند است، چون می‌تواند به اسپرینت‌های بعدی تغذیه شود، خیلی قبل از نهایی‌شدن محصول، وقتی تغییرها بسیار آسان‌ترند.

نگران نباشید اگر در دموهای اولیه کارکرد خیلی پایه است—مورد انتظار است. کلید این است که به‌طور تکراری در حال بهترشدن است. اولین دمو شاید فقط اثبات این باشد که یک دستگاه می‌تواند با یک سرور حرف بزند. دمو بعدی یک UI ساده نشان می‌دهد. دموهای بعدتر صیقل و قابلیت‌های اضافی نشان می‌دهند. اگر چیزی در دمو طبق انتظار کار نکرد، آن هم اشکالی ندارد، چون یک مسئله یکپارچه‌سازی را جلوی همه کشف کردید و حالا می‌توانید رفعش را اولویت بدهید. (در واقع، بعضی تیم‌ها ضرب‌المثلی دارند: «زود دمو کن، زیاد دمو کن—زیاد شکست بخور، سریع‌تر رفع کن!») دمو همچنین کمی انضباط اعمال می‌کند: فشار مثبتی روی تیم می‌گذارد تا کار را هر اسپرینت به حالتی قابل عرضه تمام کند، به‌جای به‌تعویق‌انداختن یکپارچه‌سازی برای بعداً. فهمیدن حالا که JSON دستگاه روی سرور درست پارس نمی‌شود بسیار بهتر از فهمیدنش حین یک فاز تست نهایی بزرگ است. پس با دموها نه فقط به‌عنوان نمایش، بلکه به‌عنوان بخشی از خود فرایند توسعه رفتار کنید.

بالاخره، آن دموها را جشن بگیرید. ساختن یک محصول IoT کار سختی است که قطعات زیادی دارد، و هر قطعه کارا—حتی یک قابلیت کوچک—لحظه‌ای از شناسایی را می‌ارزد. این روحیه را بالا نگه می‌دارد و اعتماد تیم را می‌سازد: انرژی وقتی بالاتر می‌ماند که همه مرتب نتایج ملموس کارشان را می‌بینند.


برای جمع‌بندی مرحله 4: توسعه firmware و نرم‌افزار cloud همه‌اش درباره پیشرفت تکراری و بهبود مستمر است. با سرمایه‌گذاری زمان روی معماری نرم‌افزار از ابتدا، به تیمتان نقشه‌ای محکم می‌دهید (و از گیرافتادن در بن‌بست کدنویسی دوری می‌کنید). با استفاده از اسپرینت‌های توسعه، یکپارچه‌سازی مستمر و تعریف انجام‌شده قوی، تضمین می‌کنید کیفیت در هر قدم ساخته می‌شود—باگ‌ها زود گرفته می‌شوند و نرم‌افزار همیشه یک قدم با محصول کارا فاصله دارد. در پایان مرحله 4 (که احتمالاً چند چرخه اسپرینت را در بر می‌گیرد)، باید firmwareای داشته باشید که کامل‌القابلیت است و روی سخت‌افزار واقعی یا نماینده تست شده، و سیستم cloudی که کامل‌القابلیت است و در محیطی واقع‌گرایانه تست شده. به عبارت دیگر، کل سمت نرم‌افزار محصولتان آماده حرکت است.

به لطف این رویکرد تکراری، توانسته‌اید پیوسته تنظیم و بهبود کنید، به‌جای شرط‌بستن همه‌چیز روی یک انفجار بزرگ در انتها. این ریسک را کم می‌کند و فرایند توسعه را پیش‌بینی‌پذیرتر می‌کند. همچنین صرفاً روش بهتری برای کار است: به‌جای کشش طولانی عدم قطعیت، محصول IoTتان را قدم‌به‌قدم جان گرفتن تماشا کردید، از یک LED چشمک‌زن تا یک دستگاه هوشمند کاملاً متصل و یکپارچه با cloud. کار سنگین سمت نرم‌افزار انجام شده، و با دقت و چابکی انجام شده. به مرحله بعدی، جایی که این سخت‌افزار و نرم‌افزار خوش‌ساخت را می‌گیریم و برای رهاکردنش به دنیا آماده می‌کنیم.