Порівнюємо код pet-проєктів, що перевіряють наявність світла

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Усім привіт! Ще нещодавно в Україні була актуальна проблема зі світлом. Багато розробників замислювалися над тим, щоб створити застосунок, який буде сповіщати про наявність світла вдома. Найпростіші реалізації зводилися до того, щоб пінгувати статичну адресу роутера, і в разі відсутності / появи світла сповіщати про це через месенджер. Проблема може знову стати актуальною взимку, отже розглянемо ці реалізації. Вони цікаві тим, що ми можемо порівняти код застосунків на різних мовах, що вирішують майже одну й ту саму просту задачу.

У цій статті я хотів би порівняти два застосунки:

Оскільки технології, що використовувались для створення єСвітло зрозумілі та описані в статті, спробуємо розібратися з другою «темною конячкою» — Fractal. Маємо приблизно таке порівняння:

Спробуємо розібрати код проєкту, так би мовити, на атоми.

Database

База даних Electricity має документоорієнтований вигляд і складається з кількох колекцій.

Dashboard

Dashboard — це колекція, що виконує роль source для основної сторінки.

Колекція містить основний документ в JSON-форматі, що описує дані для трьох контролів: Location, NewLocation та Map.

{
   "Locations": "Locations",
   "NewLocation": "New Location",
   "Map": {
      "Key": "",
      "Title": "Electricity points",
      "Zoom": 15,
      "Center": {
         "Lat": "50.4329658690059",
         "Lng": "30.57958332067064"
      },
      "Points": [
         {
            "Lat": "50.4329658690059",
            "Lng": "30.57958332067064"
         },
         {
            "Lat": "49.84395251649464",
            "Lng": "24.026309322098676"
         }
      ]
   }
}

Типи цих контролів описані в UI Dimension.

{
  "Style": "Save:false;Cancel:false",
  "Map": {
    "ControlType": "Map"
  },
  "NewLocation": {
    "ControlType": "Button"
  },
  "Locations": {
    "ControlType": "Button"
  }
}

Locations

Locations — це колекція, що зберігає points (або точки на карті), де ми можем моніторити наявність світла. Типовий документ для тесту виглядає так:

{
   "Address": "Kyiv",
   "Description": "",
   "HasElectricity": true,
   "IPAddress": "127.0.0.1",
   "LastPingTime": "09/27/2023 15:12:04",
   "Lat": "50.4329658690059",
   "Lng": "30.57958332067064",
   "TelegramUserID": "5018512422",
   "TextMessages": [
      {
         "IsSent": true,
         "Message": "Electricity available",
         "Provider": "Telegram",
         "Receiver": 5018512422
      }
   ]
}

Окрім уже знайомого нам UI Dimension, який може налаштовувати відображення певних JSON-атрибутів у світі UI, у нас також тут є TextMessages Dimension.

Його роль дуже проста: він спостерігає за документом, та якщо в документ було додано новий обʼєкт в масив TextMessages, він, використовуючи свою конфігурацію, намагається надіслати повідомлення через сконфігурований провайдер. Надіславши, переводить прапорець IsSent в true, або ж логує помилку, а також вказує причину, через яку не зміг відправити, і намагається це зробити знову за деякий час. У такому випадку цей Dimension сконфігурований так, щоб відправляти повідомлення через телеграм.

Ця колекція також має Timer Dimension, який свідчить, що документ повинен оброблятися за таймером кожні 300 секунд. Але як це працює — ми глянемо згодом, розібравши код в евенті OnTimerDimension.

NewLocation

NewLocation — це колекція, що зберігає шаблон для створення нового документу, який буде додано до Locations колекції.

{
  "Address": "",
  "IPAddress": "",
  "Lat": "",
  "Lng": "",
  "TelegramUserID": 0,
  "Description": "",
  "HasElectricity": true,
  "LastPingTime": ""
}

Крім уже знайомого UI Dimension, ця колекція має Validation Dimension, що інформує нас про те, як треба провалідувати поля перед записом на сервер.

{
  "Address": {
    "IsRequired": true,
    "MinLen": 3,
    "MaxLen": 256
  },
  "IPAddress": {
    "IsRequired": true,
    "MinLen": 7,
    "MaxLen": 12
  },
  "Lat": {
    "IsRequired": true,
    "Type": "float"
  },
  "Lng": {
    "IsRequired": true,
    "Type": "float"
  },
  "TelegramUserID": {
    "Type": "number"
  }
}

Тож підсумуємо. Ми маємо в базі три прості колекції. Dashboard відповідає за нашу основну сторінку. Locations зберігає точки для моніторингу світла, а NewLocation зберігає шаблон для створення нової точки моніторингу світла.

Application

Тепер усе, що нам потрібно, це написати трохи C# коду в функціональному стилі, щоб звʼязати всю бізнес-логіку до купи.

Ping host

Передусім нам потрібна функція, що буде перевіряти доступність хосту через пінг.

        private bool PingHost(string host)
        {
            try
            {
                var ping = new Ping();
                var pingReply = ping.Send(host, 1000);
                return pingReply.Status == IPStatus.Success;
            }
            catch
            {
                return false;
            }
        }

OnStart

Цей код викликатиметься завжди, коли наш застосунок стартує. Для цього ми звертаємося до нашої колекції Dashboard, отримуємо перший документ через GetFirstDoc(), та викликаємо OpenForm()

        public override void OnStart()
        {
            Client.SetDefaultCollection("Dashboard")
                  .GetFirstDoc()
                  .OpenForm();
        }

У самій Dashboard колекції вже описано, як саме відображати цей документ.

OnEventDimension

Далі в нас у формі є дві кнопки: Locations та New Location, тож потрібно подивитися, що саме повино трапитися після їх натискання.

        public override bool OnEventDimension(EventInfo eventInfo)
        {
            switch (eventInfo.Action)
            {
                case "NewLocation":
                    Client.SetDefaultCollection("NewLocation")
                          .WantCreateNewDocumentFor("Locations")
                          .OpenForm(result =>
                          {
                              if (result.Result)
                              {
                                  var gps = result.Collection
                                                  .GetFirstDoc()
                                                  .Values("{'Lat':$,'Lng':$}");
                                  Client.SetDefaultCollection("Dashboard")
                                        .GetFirstDoc()
                                        .Update("{'Map':{'Points':[Add,{'Lat':@Lat,'Lng':@Lng}]}}", gps[0], gps[1]);
                                  result.NeedReloadData = true;
                              }
                          });
                    return true;
                case "Locations":
                    Client.SetDefaultCollection("Locations")
                          .GetAll()
                          .OpenForm();
                    return true;
                default:
                    return base.OnEventDimension(eventInfo);
            }
        }

Для Locations-кнопки код дуже простий. Ми знову звертаємося до колекції Locations, через GetAll() отримуємо звідти всі документи та відкриваємо форму через OpenForm().

Для NewLocation код трішки складніший, але схожий на загальний шаблон виразів. Ми звертаємося до колекції NewLocation, далі кажемо через WantCreateNewDocumentFor("Locations"), що ми хочемо створити новий документ для колекції Locations, а в лямді в кінці перевіряємо через result.Result, чи натиснув користувач кнопку Create (Save). Якщо так, ми отримуємо координати через вираз:

var gps = result.Collection
                .GetFirstDoc()
                .Values("{'Lat':$,'Lng':$}");

Та додаємо ці координати в наш Dashboard як нову точку на мапі:

Client.SetDefaultCollection("Dashboard")
      .GetFirstDoc()
      .Update("{'Map':{'Points':[Add,{'Lat':@Lat,'Lng':@Lng}]}}", gps[0], gps[1]);

OnTimerDimension

В нас лишилась основна бізнес-логіка цього застосунку, яка періодично моніторить доступність хостів з колекції Locations.

        public override bool OnTimerDimension(TimerInfo timerInfo)
        {
            var locations = Client.SetDefaultCollection("Locations")
                                  .GetAll()
                                  .Select<Location>();
            foreach (var location in locations)
            {
                if (location.HasElectricity != PingHost(location.IPAddress) && location.TelegramUserID > 0)
                {
                    location.HasElectricity = !location.HasElectricity;
                    Client.SetDefaultCollection("Locations")
                          .GetWhere("{'Address':@Address}", location.Address)
                          .Update(@"{'HasElectricity':@HasElectricity,
                                     'TextMessages':[Add,{'Provider':'Telegram',
                                                          'Receiver':@Receiver,
                                                          'Message':@Message,
                                                          'IsSent':false}]}",
                                    location.HasElectricity,
                                    location.TelegramUserID,
                                    location.HasElectricity ? $"{location.Address} HAS electricity" : $"{location.Address} HAS NO electricity");
                }
                Client.SetDefaultCollection("Locations")
                      .GetWhere("{'Address':@Address}", location.Address)
                      .Update("{'LastPingTime':@Now}");
            }
            return true;
        }

Розберемо її покроково. OnTimerDimension-функція викликається кожні 300 секунд (5 хвилин). Перше, що вона зробить: видаляє з бази даних усі документи з колекції Locatons:

var locations = Client.SetDefaultCollection("Locations")
                      .GetAll()
                      .Select<Location>();

та десеріалізує їх в це dto:

    public class Location
    {
        public string Address { get; set; }
        public string IPAddress { get; set; }
        public long TelegramUserID { get; set; }
        public bool HasElectricity { get; set; }
    }

Далі ми перевіряємо, чи не змінився статус хосту, порівнявши прапорець HasElectricity з поточним станом доступності хосту. Якщо стан змінився, ми інвертуємо цей правопрець:

location.HasElectricity = !location.HasElectricity;

Далі ми звертаємося до колекції Locations, шукаємо документ з потрібною адресою, оновлюємо прапорець HasElectricity та вставляємо в масив TextMessages новий обʼєкт з прапорцем IsSent=false. Як ми вже говорили, спеціальний SendMessages dimension моніторить цей масив і надсилає звідти повідомлення.

Client.SetDefaultCollection("Locations")
      .GetWhere("{'Address':@Address}", location.Address)
      .Update(@"{'HasElectricity':@HasElectricity,
                 'TextMessages':[Add,{'Provider':'Telegram',
                                      'Receiver':@Receiver,
                                      'Message':@Message,
                                      'IsSent':false}]}",
                                    location.HasElectricity,
                                    location.TelegramUserID,
                                    location.HasElectricity ? $"{location.Address} HAS electricity" : $"{location.Address} HAS NO electricity");

І останній вираз оновлює LastPingTime атрибут в документі, незалежно від того, чи статус з світлом змінився. Це нам допомогає просто переглядати у вебформі, коли останній раз пінгували наш Location:

Client.SetDefaultCollection("Locations")
      .GetWhere("{'Address':@Address}", location.Address)
      .Update("{'LastPingTime':@Now}");

Висновки

Якщо в великих проєктах ми загалом обмежені у виборі технологій, то в малих проєктах, стартапах, а також в pet-проєктах ми можемо вибирати ті технології, що в десять та більше разів скорочують час розробки, роблять код більш простим, функціональним, гнучким і зрозумілим.

Також я хочу лишити тут маленьку інструкцію того, що потрібно зробити, аби задеплоїти свій перший проєкт для експериментів.

Для цього створено пʼять пісочниць (Sandbox1, Sandbox2, Sandbox3, Sandbox4, Sandbox5), ви можете використовувати будь-яку вільну з них.

Для цього потрібно:

  • Установити Visual Studio Community.
  • Установити .NET Core 3.1.
  • Клонувати репозиторій.
  • Установити в конфізі потрібний проєкт для деплою.Наприклад, якщо ви хочете деплоїти свій код в Sandbox3, то змінити цей рядок на:
    "AppNames": ["Sandbox3"]
  • Запустити FractalPlatform.Deployment проєкт на виконання. Він миттєво задеплоїть усі необхідні файли з Sandbox-проєкту в інфраструктуру Fractal і відкриє в браузері задеплоєний проєкт з потрібним url.

    Також я створив телеграм-групу @FractalPlatform, ви можете долучатися та ставити будь-які питання. Також чекаю на ваші думки тут в коментарях.

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Я себе делал с использованием LilyGo-T-Call-SIM800, полностью автономное решение, проверяет есть ли интернет по вафле и работает от сети или баты, уведомления приходили по смс. Решение в итоге на голову выше чем всякие там лябмы в облаках с пингом и сриптики на роутерах, так как свет может быть, а интернета домашнего нет.

Я собі зробив флаттер додаток на пару функцій, вмикав телефон в розетку і коли світло з’являлося то дзвонив аларм, таким чином я прокидався (якщо спав під час вимкнення)

@override
void initState() {
super.initState();
print("initState()");

_battery.batteryState.then(_updateBatteryState);
_batteryStateSubscription =
_battery.onBatteryStateChanged.listen(_updateBatteryState);
}

void _updateBatteryState(BatteryState state) {
if (_batteryState == state) return;
if (state == BatteryState.charging) {
print("Battery are charging now");
_scheduleOneShotAlarm();
}
setState(() {
_batteryState = state;
});
}

Це зовсім простий кейс використання. Для нього підійде в розетку любе радіо, тв, або просто ввімкнути світло в кімнаті.
Як зявиться світло — розбудить.
Вище додатки покривають складніший кейс використання, умовно ти сидиш десь в сусідів, чи в офісі, чи просто гуляєш по вулиці, та хочеш дізнатися коли слід повертатися додому, ліфт працює і тд.

0 рядків коду, статична ip-адреса + готове рішення від uptime robot. працювало без проблем. є пуши з додатку, є можливість налаштувати сповіщення в телеграм.

Це настільки круто, що я не полінився витратити 10 хвилин свого часу, та створити і задеплоїти ще одну веб апку, щоб ви змогли додати свої результати до загальної таблички результатів пет проєктів:

fraplat.com/jupiter/Sandbox5

Код самої веб апки

Client.SetDefaultCollection("ElectricityScore")
          .GetFirstDoc()
          .OpenForm(result => {
                      Client.SetDefaultCollection("NewElectricityScore")
                            .WantCreateNewDocumentForArray("ElectricityScore", "{'Apps':[$]}")
                            .OpenForm(result => OnStart());
          });

Дуже насторожує використання .NET Core 3.1, який вже майже рік як не підтримується.

Дякую, мабуть треба буде зробити міграцію на новішу версію.

«Проект» на 1000 рядків коду? І про це ціла стаття? Куди котиться цей світ...

Ця стаття більше не про код, а про технології, як писати набагато меньше коду.
Якщо в ентерпрайз суцільний консерватизм, то для пет проєкту можна вибрати щось більш зручне, що дасть змогу написати весь код за один вечір (100 рядків коду, не тисяча).

Висновок з коду в тому, що там суцільна магія.
Проблема з магією в тому, що щоб зрозуміти, як її написати самому, треба на 2 порядки більше часу сидіти за документацією та експериментувати, ніж коли пишеш щось simple&stupid.

Висновок з коду в тому, що там суцільна магія

"Будь-яка достатньо розвинута технологія, не відрізняєтся від чаклунства"© Артур Кларк, Три закони Кларка

encrypted-tbn0.gstatic.com/...​hR4ztfa4eiTQmRFA&usqp=CAU

=)

simple&stupid.

Все пізнається в порівнянні
github.com/p1v2/eSvitlo

Давайте зробимо експеримент, виберемо будь який не зрозумілий блок коду, та спитаємо GPT, що він робить.

Виберемо з лямди, та виберемо з Fractal.
У Пайтона фора, в інтернеті гігабайти коду для машинного навчання. На Фрактал майже нічого немає, лише дуже прості принципи побудови додатків.

Згода, вибираємо самий не зрозумілий блок коду вище?

Ви пропонуєте зворотню задачу.
Пряма задача — написати щось на Вашій технології. Для цього треба оцю технологію розуміти. Для цього треба перечитати купу її доків, і треба, щоб її інтерфейс був зворотньо-сумісним.
В результаті пересічному програмісту буде швидше щось навелосипедити на Пітоні, як оцей приклад, хай навіть в 10 разів більше рядків коду, ніж вчити нову магію.
ЗІ саме через це люди люблять С і не люблять С++. Бо в С нема магії.

Бо в С нема магії.

На Сі повно магії. А ось на Brainfuck вже зовсім все просто.
З однієї сторінки вікі можна повністю вивчити мову, яка складається лише з кількох операторів.
uk.wikipedia.org/wiki/Brainfuck

Тому, краще писати на брейнфак, бо краще написати

навіть в 10 разів більше рядків коду, ніж вчити нову магію.

=)

А якщо серйозно, гарний інструмент має економити час, навіть якщо спочатку дещо є просадка на навчанні.

Питання, чи колись навчання окупиться, чи до того часу усе забудеться. Це залежить від розміру інструмента, організації його інтерфейсів, та якості документації.
Приклад 1: можна рік вчитись керувати літаком, щоб один раз політати. Неефективно.
Приклад 2: в Пітона дуже якісно зроблено документацію, тому його ядро вчиться за 3 дні, а в бібліотеці швидко знаходити лише ті модулі, що тобі потрібні саме зараз. Ефективно.

Частково згоден. Але як на мене найкраща документація в brainfuck, взагалі не лишилось не описаних чи не зрозумілих аспектів роботи інтерпретатора =)

Що насправді важливо розуміти, якщо різниця в х10 разів в ефективності, то вже пайтон відіграє роль brainfuck. Тому що навіть на одному проєкті це економія, умовно 1 вечір з вивченням на Фрактал, проти 5 вечорів на Пайтоні який добре знаєш.
А на наступному проєкті то вже 1 вечір буде проти 10 вечорів.

Чи вчиться весь Фрактал за 1 вечір з нуля?
Чи одразу зрозуміло, які його частини необхідно вивчити, щоб зробити потрібний тобі проект? Чи ці частини не потребують розуміння інших його компонентів?
І чи знайдеться отой наступний проект настільки швидко, щоб не забути вивчене?

Базові речі вчаться навіть швидше чим за вечір. От як конвертувати будь-який json в повноцінний CRUD веб сайт.
fraplat.com/jupiter/JsonToWebApp

А це код цього нескладного апп.
github.com/...​sonToWebAppApplication.cs

Тепер уяви, в тебе є бд з схемою табличок і тобі потрібно просто круд зробити на Пайтоні ...

А Фрактал робить це просто отримуючи на вхід джисон. Підправив джисон і фактично добавив філди в базу, контроли на юай. Ніде не помилився.

Приклад дуже простий, але він показує що вхідний квиток хочаб для таких простих крудів, майже нульовий.

То не базові речі, а якісь випадкові фрагменти магії.
Ось як пояснюють базові речі docs.python.org/...​utorial/introduction.html

Ок, наприклад я студент.
Хочу написати hello world для веба, тобто ToDo list.
Відкриваю цю класну документацію, та читаю щось типу:

word[:2] # character from the beginning to position 2 (excluded)
’Py’
word[4:] # characters from position 4 (included) to the end
’on’
word[-2:] # characters from the second-last (included) to the end
’on’

Це якось мене наблизило до мети? Ну чесно, сумніваюсь.
При цьому, я йду на Fractal, дивлюся відео довжиною 2 хв та 45 секунд.
www.youtube.com/watch?v=eVC2M_c8574

Бачу код бази данних в джисоні

{
  "Title": "My Todo List",
  "Location": "Home",
  "ToDos": [
    {
      "Completed": false,
      "Task": "Buy food"
    },
    {
      "Completed": false,
      "Task": "Clean room"
    }
  ]
}

Та код в шарпі, прочитати джисон, показати на його основі форму

Client.SetDefaultCollection("ToDoList")
         .GetFirstDoc()
         .WantModifyExistingDocuments()
         .OpenForm();

Та отримую свій ToDo лист за 2 хвилини.
Код просто фантастично простий.

Це просто дуже різні інструменти.
Фрактал просто не морочить голову низькорівневим програмуванням як Пайтон, та дає змогу писати в х10-100 разів меньше коду.

Я розумію, що ви бачите магію, нічого не зрозумієте.
Але потрібно цю магію вчити, бо вона проста, та дає змогу збирати бекенд для сайтів за 3-5 доби. Не якусь лямду на авс що пінгує хост, а сайти ось такого рівня з купою скрінів та адмінкою:
fraplat.com/...​843a4982f73510b50179d.jpg

Фрактал фактично з бекендом робить те, що Figma зробила з дизайном сайтів. Фігма просто лишила без роботи всіх любітелів набирати html в блокноті. І абсолютно всеодно, як добре документація по html була описана до того в інтернеті, бо дизайн систем пришвидчився в х10 разів, а рівень того дизайну, як і дизайн бекенду на Фрактал, просто улетів в космос.

Всі хто зараз вивчає Пайтон, то потенційні робітники за 5 долл на годину. Саме така продуктивність праці. А Фрактал, та всі хто зміг вивчити що таке Json та Dimensions то потенційні робітники з ставкою в 100-200 долл на годину.
Just business, nothing personal.

А як щодо Django? Воно на Пітоні, і створювалось як раз для швидкої розробки сайтів з адмінкою та базою django-book-new.readthedocs.io/en/latest/chapter01.html

А як щодо Django? Воно на Пітоні, і створювалось як раз для швидкої розробки сайтів з адмінкою та базою django-book-new.readthedocs.io/en/latest/chapter01.html

Код ToDo list на Django в студію.
Бажано який було написано з нуля за 2 хвилини.
І ще за 2 хвилини задеплоєно в світ.

Та я ж ембедер. Мені цей веб нафіг не треба.

Підписатись на коментарі