معماری IoT تحت فشار: ترموستات هوشمند ، یک مثال (قسمت 5)

در یک پست قبلی ، ما بررسی کردیم که چرا طراحی یک دستگاه هوشمند می تواند پیچیده تر (و پرهزینه تر) از آنچه انتظار می رود ، هم برای مشاغل و هم برای مصرف کنندگان باشد. سپس ما یک روش جایگزین را معرفی کردیم که امکان جمع آوری منابع و خدمات مشترک را فراهم می کند و به ما امکان می دهد تا روی تجارت اصلی خود تمرکز کنیم. سرانجام ، ما استفاده از WebAssembly را برای پرداختن به چالش های کلیدی در این مدل پیشنهاد کردیم.
اکنون ، وقت آن است که تئوری را عملی کنیم ، بیایید به کد شیرجه بزنیم!
ما در حال اجرای یک ترموستات حداقل هستیم. در حالی که بسیاری از ویژگی های اضافی وجود دارد که ما می توانیم و الگوریتم های پیشرفته تری برای بهینه سازی عملکرد در آن قرار دهیم ، این مثال صرفاً برای تصویربشر مشخصات واقعی به جزئیات بسیار بیشتری نیاز دارد ، هدف من در اینجا ارائه یک نمای کلی این به سرعت اهداف طراحی ما را نشان می دهد.
کارکرد اصلی
هدف ما ایجاد یک ترموستات هوشمند به سیستم HVAC را کنترل کنید، با شروع فقط گرمایش (بدون پشتیبانی AC در نسخه اول).
ملاحظات سخت افزاری
- در حال حاضر ، ویژگی های سیستم یک سنسور دمای داخلیبشر
- پشتیبانی آینده برای سنسورها و مناطق متعدد برنامه ریزی شده است (به عنوان مثال ، تنظیم درجه حرارت در مناطق خواب یا ادغام یک سنسور در فضای باز برای گرمایش بهینه).
- مکانیسم کنترل ساده است رله 24 ولت برای فعال کردن (سوئیچ روشن/خاموش) واحد اما برنامه هایی برای آینده وجود دارد تا بتواند از کنترل PWM پشتیبانی کند.
ویژگی های اساسی
- کاربران می توانند تنظیم کنند دو درجه حرارت هدفبا
T1
وتT2
، با یک برنامه این در فواصل 30 دقیقه ای بین آنها تغییر می کند. - این سیستم می تواند شامل چندین باشد از پیش تعیین شده حالت ها برای غلبه بر برنامه اما ما قصد داریم با این موارد شروع کنیم:
- دور – سیستم خاموش است مگر اینکه دما در زیر کاهش یابد
TMin
بشر - مهمانی – سیستم همچنان ادامه دارد
T1
به طور مداوم - عادی – سیستم برنامه ریزی شده را دنبال می کند
T1
وتT2
دما
- دور – سیستم خاموش است مگر اینکه دما در زیر کاهش یابد
ویژگی های هوشمند
- کنترل دما و برنامه از طریق UI Hub ، از جمله مدیریت می شود برنامه همراه دسترسی برای کنترل از راه دور.
- پیش بینی هوا از داده ها (در صورت وجود) برای تعیین استراتژی های گرمایشی مقرون به صرفه استفاده می شود.
- سیستم داده ها را جمع می کند برای تجزیه و تحلیل کارآیی HVAC ، عادات کاربر و توسعه بهینه سازی صرفه جویی در مصرف انرژی در آینده.
ملاحظات توسعه
- توسعه سخت افزار هنوز شروع نشده است، و مشخصات نهایی هیئت مدیره تعریف نشده است.
- در اتصال به هاب TBD است: این می تواند از I2C ، CAN ، USB یا بلوتوث استفاده کند.
- با وجود کمبود سخت افزار ، ما می خواهیم توسعه نرم افزار را بلافاصله شروع کنید، اطمینان از یک بنیاد قوی قبل از ادغام.
- هدف ما این است که این دستگاه را با همان قیمت همتای “سنتی” خود بفروشیم. ما نمی خواهیم هزینه های اضافی را تکرار کنیم مگر اینکه ROI مشخصی وجود داشته باشد (به عنوان مثال ذخیره داده).
از آنجا که ما هنوز یک هیئت کار نداریم ، ما به راهی نیاز خواهیم داشت شبیه سازی کردن آن یک شبیه سازی مبتنی بر نرم افزار که به عنوان یک سرویس جداگانه در قطب ما اجرا می شود می تواند به این منظور خدمت کند. ما باید به طور آزمایشی با تیم سخت افزار در مورد پروتکل ارتباطی، در حال حاضر (همچنین با توجه به تحولات مورد انتظار آینده):
- هاب و دستگاه با استفاده از ارتباط خط به خط رشته های رمزگذاری شده ASCII ارتباط برقرار می کنند.
- توپی همیشه ارتباطات را آغاز می کند.
- دستورات حساس به مورد هستند و فضاهای پیشرو/دنباله دار دور ریخته نمی شوند.
- هاب دستور “دمای خوانده شده” را ارسال می کند
r
که دستگاه با یک عدد عدد صحیح که درجه حرارت در درجه سانتیگراد است پاسخ می دهد. این می تواند به صورت اختیاری یک علامت اصلی داشته باشد+
یا-
بشر - هاب می تواند دستور “روشن کردن گرمایش” را ارسال کند
1
یا دستور “خاموش کردن گرمایش”0
بشر دستگاه طبق دستورالعمل بدون پاسخ دادن انجام می شود.
در حالی که یک شبیه سازی در مراحل اولیه کمک می کند ، محدودیت هایی دارد. برای ایجاد شکاف ، ما به سرعت یک دستگاه را با استفاده از یک توسعه خواهیم داد سر و صدا در حالی که منتظر نهایی کردن تیم سخت افزار هستند. در همین حال ، شبیه سازی نرم افزار همچنین به عنوان ابزاری مهم برای تست های ادغام، اطمینان از هدر رفتن هیچ وقت.
برای شروع ، ما می توانیم چیزی را به سادگی به همین ترتیب پیاده سازی کنیم (به قسمت 3 این سری مراجعه کنید ، ما در این مثال دوباره از AssemblyScript استفاده می کنیم):
import { Context } from "firmwareless/hosting"
import { IoStream } from "firmwareless/lib"
const TEMPERATURE = "temperature";
const FURNACE = "furnace";
const READ_TEMPERATURE_COMMAND = "r";
const FURNACE_ON_COMMAND = "1";
const FURNACE_OFF_COMMAND = "0";
let channel: IoStream | null = null;
export function setup(context: Context) {
// This is the temperature we report when queried. To help
// debugging we can edit this value manually in the UI to
// observe the effects.
context.status.register<i8>({
id: TEMPERATURE,
type: "environment/temperature",
unit: "Celsius",
editable: true,
range: { nullable: true, minimum: 0, maximum: 40, step: 1 }
});
// This is the relay we control, we can observe this in the UI
// to determine if everything is working as expected.
context.status.register<u8>({
id: FURNACE,
type: "status/boolean",
});
// To simulate a physical device we open a stream for character
// device. The hosted firmware with our logic is agnostic
// of the transport mechanism!
channel = context.communication.stream.open({
mode: "read-write",
encoding: "ascii",
onWrite: handleWrite
});
}
export function teardown(context: Context) {
channel?.close();
}
function handleWrite(context: Context, stream: IoStream, data: string) {
if (data === READ_TEMPERATURE_COMMAND) {
stream.writeLine(context.status.get<i8>(TEMPERATURE).toString());
} else if (data === FURNACE_ON_COMMAND) {
context.status.set<u8>(FURNACE, 1);
} else if (data === FURNACE_OFF_COMMAND) {
context.status.set<u8>(FURNACE, 0);
}
}
این حداقل است ، فاقد ورود به سیستم و حتی بررسی خطای اساسی است ، اما کاربردی است و ما را شروع می کند.
در همین حال ، یکی دیگر از اعضای تیم به سرعت با استفاده از یک دستگاه فیزیکی مونتاژ می کند سر و صدا ما در یک کشو داشتیم. این یک است بیش از حد– قدرتمندتر (و گران تر) از حد لازم – اما این کار را در 30 دقیقه انجام می دهد ، و این همان چیزی است که فعلاً اهمیت دارد. در یک پست آینده ، ما شرح خواهیم داد واقعی طراحی سخت افزار (شاید با استفاده از میکروکنترلر 50 سنت Attiny85).
ما قصد داریم از پردازنده LED استفاده کنیم تا در هنگام روشن شدن کوره بررسی کنیم. در این مثال ما از Arduino Nano و یک سنسور دما آنالوگ LM35 استفاده کردیم ، لطفاً برای پیکربندی و کد مناسب به برگه های داده های محصولات مراجعه کنید. به عنوان مثال ، در برنامه های واقعی شما واقعاً به چند مؤلفه منفعل نیاز دارید تا خوانش های تمیز داشته باشید (یا در بعضی موارد اصلاً خواندن). از برگه داده LM35:
در موارد ساده (مطمئناً اگر از تخته نان استفاده می کنید) می توانید از یک دمپر RC استفاده کنید:
حالا بیایید کد را بنویسیم:
#include "Arduino.h"
#include
#define NUMBER_OF_READINGS_PER_MEASURE 4
#define TEMPERATURE_SENSOR_APIN A0
#define FURNACE_STATUS_INDICATOR LED_BUILTIN
#define READ_TEMPERATURE_COMMAND 'r'
#define FURNACE_ON_COMMAND '1'
#define FURNACE_OFF_COMMAND '0'
int8_t readTemperature();
void setFurnaceStatus(bool active);
char tryReadCommandFromMaster();
void setup() {
pinMode(FURNACE_STATUS_INDICATOR, OUTPUT);
Serial.begin(9600);
}
// We keep waiting for a command from the serial port, inputs
// are always commands (we ignore what we do not know) and the
// only output is the temperature (when we're asked to).
// Note that this is not what you would do in a real application
// but it mimics (more or less) how it could work with I2C.
void loop() {
switch (tryReadCommandFromMaster()) {
case READ_TEMPERATURE_COMMAND:
Serial.println(readTemperature());
break;
case FURNACE_ON_COMMAND:
setFurnaceStatus(true);
break;
case FURNACE_OFF_COMMAND:
setFurnaceStatus(false);
break;
}
}
// The readings are fairly noisy, for this example it's
// enough to calculate a simple average.
int8_t readTemperature() {
int16_t value = 0;
for (int8_t i=0; i < NUMBER_OF_READINGS_PER_MEASURE; ++i) {
value += analogRead(TEMPERATURE_SENSOR_APIN);
}
return (int8_t)((5 * value * 100.0) / 1024 / NUMBER_OF_READINGS_PER_MEASURE);
}
// This is a development board, we use a LED instead of
// turning on/off the heating.
void setFurnaceStatus(bool active) {
digitalWrite(FURNACE_STATUS_INDICATOR, active);
}
// We read one byte (instead of a full line) because currently
// the supported commands are one byte only and we ignore what
// we do not know how to process (for example spaces and new lines).
int16_t tryReadCommandFromMaster() {
if (Serial.available()) {
return Serial.read();
}
}
این تمام چیزی است که ما برای شروع توسعه خود نیاز داریم سیستم عامل میزبانبشر
در این مثال ما نمی خواهیم توصیف کردن تنظیمات ما با کد ، سیستم عامل ما با توصیف کننده JSON همراه است. توجه داشته باشید که تمام دما در درجه سانتیگراد است ، UI مقادیر را با استفاده از واحد مورد نظر خود (به عنوان مثال ° F) به کاربر ارائه می دهد.
{
"id": "76a38d89-d756-4412-ac87-604ff3cf84d0",
"vendor": "Acme",
"name": "Smart Thermostat Mod. 1",
"version": "1.0.0",
"compatibility": "1.0+",
"channel": {
"initiator": "host"
},
"config": {
"monitoringInterval": "15 minutes",
"schedulingInterval": "30 minutes",
"minimumTemperature": "8 °C",
"maximumTemperature": "30 °C"
},
"variables": [
{
"name": "furnace",
"storage": "uint8",
"type": "boolean"
},
{
"name": "current_temp",
"storage": "int8",
"type": "measure/temperature",
},
{
"name": "target_temp",
"storage": "int8",
"type": "measure/temperature",
},
{
"name": "desired_temp_1",
"storage": "int8",
"type": "measure/temperature",
"default": "16 °C"
},
{
"name": "desired_temp_2",
"storage": "int8",
"type": "measure/temperature",
"default": "18 °C"
},
{
"name": "schedule",
"label": "Schedule",
"type": "system/schedule",
"editable": true,
"editorOptions": {
"interval": "week",
"granularity": "{{ config.schedulingInterval }}",
"selection": "list",
"default": "0",
"listItems": [
{ "key", "0", "label", "Off" },
{ "key", "1", "label", "{{ variables.desired_temp_1 }}" },
{ "key", "2", "label", "{{ variables.desired_temp_2 }}" }
]
}
}
]
}
این کافی است برای شروع MVP سریع ، ما بعداً می خواهیم بیت های گمشده را اضافه کنیم. جزئیات کلیدی که باید توجه داشته باشید initiator
در channel
بخش – تعیین می کند چه کسی ارتباط را آغاز می کند. در این حالت ، سیستم عامل میزبان ما رهبری می کند. با این حال ، برای دستگاه های باتری، نقش ها را می توان معکوس کرد: به جای شروع برقراری ارتباط ، سیستم عامل داده های بافر شده و دستورات صف را که در طول اتصال بعدی اجرا می شود ، بهینه سازی مصرف برق می خواند.
سیستم عامل ما نقطه شروع اجرای یک مکانیسم کنترل روشن/خاموش که از برنامه تعریف شده توسط کاربر پیروی می کند:
import { Context, Scheduling } from "firmwareless/hosting"
import { IoStream, Interval, Temperature } from "firmwareless/lib"
const READ_TEMPERATURE_COMMAND = "r";
const FURNACE_ON_COMMAND = "1";
const FURNACE_OFF_COMMAND = "0";
let channel: IoStream | null = null;
export function setup(context: Context) {
// This is how often we are going to check for the temperature.
context.schedule(Interval.parse(context.config.get("monitoringInterval")), main);
// When these variables change value we need to recalculate
// our status because they represent the desired temperatures
// and our scheduling.
context.variables.onChange(["desired_temp_1", "desired_temp_2", "schedule"], main);
}
export function teardown(context: Context) {
// If we are going down then we want to be sure we are not
// leaving the heater on!
const stream = IoStream.Open(context.associatedDeviceId);
try {
stream.writeByte(FURNACE_OFF_COMMAND);
}
finally {
stream.close();
}
}
function main(context: Context) {
const stream = IoStream.Open(context.associatedDeviceId);
try {
applyFurnaceStatus(context, stream);
}
finally {
stream.close();
}
}
function applyFurnaceStatus(context: Context, stream: IoStream) {
const desiredTemperature = resolveDesiredTemperature(context);
context.variables.set("target_temp", desiredTemperature);
if (desiredTemperature === null) {
stream.writeByte(FURNACE_OFF_COMMAND);
return;
}
stream.writeByte(READ_TEMPERATURE_COMMAND);
stream.flush();
const temperature = Temperature.parse(stream.readLine(), "°C");
const status = temperature < desiredTemperature;
stream.writeByte(status ? FURNACE_ON_COMMAND : FURNACE_OFF_COMMAND);
context.variables.set("furnace", status);
}
function resolveDesiredTemperature(context: Context) {
// To "resolve" the desired temperature we need to read the list
// of scheduled values from the "schedule" variable and pick the
// selected one for the current date and time.
// It's so common that we have an helper function for that.
const temperatureId = Scheduling.resolve("schedule");
if (temperatureId === "1") {
return context.variables.get("desired_temp_1");
}
if (temperatureId === "2") {
return context.variables.get("desired_temp_2");
}
// A value of "0" or an unknown key means "off".
return null;
}
لطفاً توجه داشته باشید که این کد حداقل است ، فقط با تمرکز بر تجارت اصلی ما (ترموستات های ساختمان) که هدف اصلی ما بود.
مرحله آخر: UI
برای اینکه این رویکرد واقعاً مؤثر باشد ، UI باید به یک فناوری خاص گره خورده نباشید یا چارچوب (مانند React یا حتی HTML ساده). درعوض ، ما به یک نمایشگاه فناوری-آگنوستیک از صفحه نیاز داریم-به قطب های مختلف اجازه می دهیم تا آن را به روش های مختلف ارائه دهند.
- یکی از قطب ها ممکن است از HTML استفاده کند ، دیگری می تواند QT را اهرم کند ، در حالی که برنامه های تلفن همراه ممکن است موتورهای رندر کاملاً متفاوتی داشته باشند.
- هدف انعطاف پذیری است: اطمینان از سازگاری یکپارچه در سکوها بدون مجبور کردن یک الگوی UI واحد.
بیایید یک بخش جدید اضافه کنیم ui
به پرونده JSON ما (توجه داشته باشید که JSON ممکن است بهترین راه برای نمایش UI نباشد ، این کد را فقط در نظر بگیرید):
{
...
"ui": [
{
"type": "page",
"title": "Thermostat",
"content": {
"control": "ring",
"label": "Temperature",
"minimum": "{{ config.minimumTemperature }}",
"maximum": "{{ config.maximumTemperature }}",
"value": "{{ variables.current_temp}}",
"steps": [ "{{ variables.target_temp }}" ],
"text": true,
"actions": [
{
"type": "edit",
"target": "schedule",
"label": "Schedule"
},
{
"type": "edit",
"target": "desired_temp_1",
"label": "Temperature 1"
},
{
"type": "edit",
"target": "desired_temp_2",
"label": "Temperature 2"
}
]
}
}
]
}
اکنون ، ما می توانیم برنامه خود را ویرایش کنیم ، دما را رصد کنیم و اطمینان حاصل کنیم که همه چیز به راحتی در حال اجرا است.
در پست بعدی ، برخی از آنها را اضافه خواهیم کرد ویژگی های هوشمند و الگوریتم را اصلاح کنید تا آن را کارآمدتر و کمتری تر کند.