Паттерн внедрение зависимостей в .NET nanoFramework для микроконтроллеров

Сегодня сломаем привычный мир инженеров и разработчиков встраиваемых систем на микроконтроллерах. В .NET существует замечательный паттерн программирования, как внедрение зависимостей (Dependency injection, DI). Суть паттерна заключается в предоставление механизма, который позволяет сделать взаимодействующие в приложение объекты слабосвязанными. Эти объекты будут связаны между собой через абстракции, например, через интерфейсы, что делает всю систему более гибкой, более адаптируемой и расширяемой. Но когда ведется разработка для микроконтроллеров, все зависимости обычно жестко завязаны на используемых устройствах, и замена датчика иногда приводит к существенному переписыванию программного кода. Напишем приложение на .NET nanoFramework для микроконтроллера ESP32, используя паттерн DI с возможностью легкой замены датчиков и LCD экрана.

Данный материал был подготовлен ​​при финансовой поддержке компании Timeweb Cloud в рамках публикации в блоге компании на Хабре. Timeweb Cloud — это облачная инфраструктура для бизнеса и разработки. Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud в Telegram-канале.

Паттерн внедрение зависимостей

Паттерн внедрение зависимостей в основном используют для разработки Веб-приложений на ASP.NET. На самой концепцией паттерна не будем останавливаться, многие .NET разработчики хорошо знакомы с данным паттерном, более подробно можно почитать статью Сервисы и Dependency Injection на metanit.

Библиотека DI для nanoFramework предоставляется в виде nuget-пакета nanoFramework.DependencyInjection. Контейнер DI автоматизирует многие задачи, связывает объекты, управляет жизненным циклом приложения. API библиотеки максимально приближен к официальному .NET Dependency Injection. Исключения в основном возникают из-за отсутствия поддержки дженериков в .NET nanoFramework.

Для создания контейнера DI необходимо три основных компонента:

  • Композиция объектов (Object Composition) — композиция объектов, определяющий набор объектов для создания и сопряжения;
  • Регистрация сервисов (Registering Services) — необходимо определить экземпляр  ServiceCollection  и зарегистрировать в нем объекты с определенным временем жизни;
  • Поставщик услуг (Service Provider) — создание поставщика услуг для извлечения объекта.

DI был бы неполным без  Generic Host  (общий хост). В nanoFramework доступен в виде nuget-пакета nanoFramework.Hosting.  Generic Host  конфигурирует контейнер приложения DI, а также предоставляет доступ к сервисам в контейнере DI и управляет жизненным циклом. Когда запускается  Host , то вызывается  Start()  для каждой реализации  IHostedService , которые зарегистрированы в коллекции сервисов хоста. В контейнере приложения для всех объектов  IHostedService , таких как  BackgroundService  или  SchedulerService , вызывается метод  ExecuteAsync() . API библиотеки максимально приближен к официальному .NET Generic Host. Рассмотрим на практике применение паттерна DI.

Архитектура

В качестве примера применения DI, разработаем небольшое устройство метеостанцию. Принцип работы устройства достаточно прост. Пользователь взаимодействует с устройством путем нажатия на кнопки переключения состояний экрана. Всего доступно три состояния экрана для отображения данных:

  • Текущая дата и время;
  • Температура и влажность;
  • Давление.

Таким образом, пользователь может узнать текущее время и температуру окружающей среды. Время на устройстве автоматически синхронизируется с NTP-сервером в сети Интернет.

Метеостанция состоит из следующих компонентов:

Работа устройства:

Схема подключения

Все датчики подключаются по шине I2C. Единственно, был использован LCD SSD1306 c 7-pin контактами для которого требуется дополнительно подключать контакт GPIO для инициализации. В случае использования 4-pin дисплея этого не требуется делать.

Устройства для подключения:

  • Датчик BME280, шина I2C, контакты: 21-pin DATA, 22-pin CLOCK;
  • Экран SSD1306 OLED с 7-pin I2C/SPI, шина I2C, контакты: 21-pin DATA, 22-pin CLOCK, 18-pin RES для инициализации;
  • Емкостная панель клавиатуры на базе датчика MPR121, шина I2C, контакты: 21-pin DATA, 22-pin.

Итоговая схема будет выглядеть следующим образом:

.NET nanoFramework Weatherstation
Принципиальная схема подключения устройств к ESP32 DevKit v1 (fzz)

Приложение

Взаимодействие с датчиками реализовано посредством интерфейсов, которые позволяют легко заменить физический датчик на любой другой, включая виртуальный. Виртуальные датчики очень удобны для процедуры тестирования приложения, когда нет доступа к физическим датчикам. Основная логика приложения максимально абстрактна от механизма работы датчиков. Так в приложение реализуются следующие интерфейсы:

  • ISensorsService — получение данных о состоянии окружающей среды;
  • IKeyboardService — получение номера кнопки;
  • IDisplayService — отображение информации.

Интерфейсы

ISensorsService

В интерфейсе  ISensorsService  декларирована только одна функция получения данных с датчика.

public interface ISensorsService
{
public SensorsResult GetSensorsResult();
}
public class SensorsResult
{
public SensorsResult() { }
public SensorsResult(double temperature, double pressure, double humidity)
{
Temperature = temperature;
Pressure = pressure;
Humidity = humidity;
}
public double Temperature { get; }
public double Pressure { get; }
public double Humidity { get; }
}
public interface ISensorsService { public SensorsResult GetSensorsResult(); } public class SensorsResult { public SensorsResult() { } public SensorsResult(double temperature, double pressure, double humidity) { Temperature = temperature; Pressure = pressure; Humidity = humidity; } public double Temperature { get; } public double Pressure { get; } public double Humidity { get; } }
public interface ISensorsService
{
  public SensorsResult GetSensorsResult();
}

public class SensorsResult
{
  public SensorsResult() { }
  public SensorsResult(double temperature, double pressure, double humidity)
  {
    Temperature = temperature;
    Pressure = pressure;
    Humidity = humidity;
  }
  public double Temperature { get; }
  public double Pressure { get; }
  public double Humidity { get; }
}

Как видно из примера, отсутствует жесткая привязка к внутренней реализации работы датчика.

IKeyboardService

Задача интерфейса  IKeyboardService  заключается в получение кода кнопки. Каким образом будет реализована клавиатура абсолютно неважно, это может быть и обычная кнопочная клавиатура, подключаемая к аналоговому контакту, например клавиатура Analog ADKeyboard Module.

.NET nanoFramework Weatherstation
Модуль Analog ADKeyboard Module

Просто считываем код кнопки, который привязан к вариантам состояния экрана.

public interface IKeyboardService
{
public int ReadKey();
}
public interface IKeyboardService { public int ReadKey(); }
public interface IKeyboardService
{
  public int ReadKey();
}

IDisplayService
Для интерфейса дисплея  IDisplayService  передается тип экрана и объект содержащий данные для отображения.

public interface IDisplayService
{
public void Show(Screen screen, object obj);
}
public enum Screen
{
Clear_0,
DateTime_1,
TempHum_2,
Pressure_3,
}
public interface IDisplayService { public void Show(Screen screen, object obj); } public enum Screen { Clear_0, DateTime_1, TempHum_2, Pressure_3, }
public interface IDisplayService
{
  public void Show(Screen screen, object obj);
}

public enum Screen
{
  Clear_0,
  DateTime_1,
  TempHum_2,
  Pressure_3,
}

Датчики

Рассмотрим клавиатуру на базе датчика Mpr121. Класс  KeyboardSingleton  наследуется от интерфейсов  IKeyboardService  и  IDisposable . Содержит функцию инициализации и функцию  ReadKey()  которая объявлена в интерфейсе  IKeyboardService .

Файл KeyboardSingleton.cs:

internal class KeyboardSingleton : IKeyboardService, IDisposable
{
private const int busId = 1; // bus id on the MCU
private I2cDevice _i2cDevice;
private Mpr121 _mpr121;
public KeyboardSingleton()
{
this.InitMpr121();
}
private void InitMpr121()
{
Debug.WriteLine("Init InitMpr121!");
I2cConnectionSettings i2cSettings = new(busId, Mpr121.DefaultI2cAddress);
_i2cDevice = I2cDevice.Create(i2cSettings);
_mpr121 = new Mpr121(_i2cDevice);
}
public int ReadKey()
{
bool[] channelStatuses = _mpr121.ReadChannelStatuses();
int key = -1;
for (int i = 0; i < channelStatuses.Length; i++)
{
if (channelStatuses[i])
{
key = i;
break;
}
}
return key;
}
}
internal class KeyboardSingleton : IKeyboardService, IDisposable { private const int busId = 1; // bus id on the MCU private I2cDevice _i2cDevice; private Mpr121 _mpr121; public KeyboardSingleton() { this.InitMpr121(); } private void InitMpr121() { Debug.WriteLine("Init InitMpr121!"); I2cConnectionSettings i2cSettings = new(busId, Mpr121.DefaultI2cAddress); _i2cDevice = I2cDevice.Create(i2cSettings); _mpr121 = new Mpr121(_i2cDevice); } public int ReadKey() { bool[] channelStatuses = _mpr121.ReadChannelStatuses(); int key = -1; for (int i = 0; i < channelStatuses.Length; i++) { if (channelStatuses[i]) { key = i; break; } } return key; } }
internal class KeyboardSingleton : IKeyboardService, IDisposable
{
  private const int busId = 1; // bus id on the MCU
  private I2cDevice _i2cDevice;
  private Mpr121 _mpr121;
  
  public KeyboardSingleton()
  {
    this.InitMpr121();
  }
  
  private void InitMpr121()
  {
    Debug.WriteLine("Init InitMpr121!");
    I2cConnectionSettings i2cSettings = new(busId, Mpr121.DefaultI2cAddress);
    _i2cDevice = I2cDevice.Create(i2cSettings);
    _mpr121 = new Mpr121(_i2cDevice);
  }

  public int ReadKey()
  {
    bool[] channelStatuses = _mpr121.ReadChannelStatuses();
    int key = -1;
    for (int i = 0; i < channelStatuses.Length; i++)
    {
      if (channelStatuses[i])
      {
        key = i;
        break;
      }
    }
    return key;
  }
}

Работа с другими устройствами, такими как клавиатура и дисплей организованна так же.

Сервисы IHostedService

Сервисы являются основными самостоятельными единицами приложения. Все сервисы добавляются в коллекцию сервисов  ServiceCollection() .  Host builder  вызывает методы  Start()  и  Stop()  для соответствующих сервисов. Можно создать несколько реализаций  IHostedService  и зарегистрировать с помощью метода  ConfigureService()  в контейнере DI.

Пример класса сервиса:

public class CustomService : IHostedService
{
public void Start() { }
public void Stop() { }
}
public class CustomService : IHostedService { public void Start() { } public void Stop() { } }
public class CustomService : IHostedService
{
  public void Start() { }

  public void Stop() { }
}

Сервис MonitorService

Вся основная логика приложения размещается в сервисе  MonitorService . Рассмотрим объявленные переменные в классе и конструктор класса.

Файл MonitorService.cs:

internal class MonitorService : IHostedService
{
private ISensorsService _sensorsService { get; }
private IDisplayService _displayService { get; set; }
private IKeyboardService _keyboardService { get; }
private Thread _handlerThread;
private CancellationTokenSource _cs;
public MonitorService(ISensorsService sensorsService, IDisplayService displayService, IKeyboardService keyboardService)
{
_sensorsService = sensorsService;
_displayService = displayService;
_keyboardService = keyboardService;
}
internal class MonitorService : IHostedService { private ISensorsService _sensorsService { get; } private IDisplayService _displayService { get; set; } private IKeyboardService _keyboardService { get; } private Thread _handlerThread; private CancellationTokenSource _cs; public MonitorService(ISensorsService sensorsService, IDisplayService displayService, IKeyboardService keyboardService) { _sensorsService = sensorsService; _displayService = displayService; _keyboardService = keyboardService; }
internal class MonitorService : IHostedService
{
  private ISensorsService _sensorsService { get; }
  private IDisplayService _displayService { get; set; }
  private IKeyboardService _keyboardService { get; }
  private Thread _handlerThread;
  private CancellationTokenSource _cs;

  public MonitorService(ISensorsService sensorsService, IDisplayService displayService, IKeyboardService keyboardService)
  {
    _sensorsService = sensorsService;
    _displayService = displayService;
    _keyboardService = keyboardService;
  }

В конструкторе класса присутствует композиция объектов, с которыми доступно взаимодействие. Как видим, вместо класса  KeyboardSingleton  присутствует интерфейс  IKeyboardService . Сам класс  MonitorService  не завязан на реализации конечных используемых датчиков, таким образом, реализуется концепция слабосвязанного приложения. Подобным образом строится взаимодействие с датчиками интерфейс  ISensorsService , и LCD дисплеем интерфейс  IDisplayService .

Далее метод  Start()  запускает поток, в задачу которого входит считывание состояния клавиатуры, показаний датчиков и отправка данных на LCD дисплей.

Файл MonitorService.cs
public void Start()
{
...
_handlerThread = new Thread(() =>
{
while (!csToken.IsCancellationRequested)
{
//sensors
sensorsResult = _sensorsService.GetSensorsResult();
//key
key = _keyboardService.ReadKey();
switch (key)
{
case 8:
currentScreen = Screen.DateTime_1;
break;
...
}
//screen
switch (currentScreen)
{
case Screen.DateTime_1:
DateTime currentDateTime = DateTime.UtcNow + TimeSpan.FromHours(3); // +3 GMT
_displayService.Show(Screen.DateTime_1, currentDateTime);
break;
case Screen.TempHum_2:
_displayService.Show(Screen.TempHum_2, sensorsResult);
break;
...
}
Thread.Sleep(500);
}
});
_handlerThread.Start();
}
public void Start() { ... _handlerThread = new Thread(() => { while (!csToken.IsCancellationRequested) { //sensors sensorsResult = _sensorsService.GetSensorsResult(); //key key = _keyboardService.ReadKey(); switch (key) { case 8: currentScreen = Screen.DateTime_1; break; ... } //screen switch (currentScreen) { case Screen.DateTime_1: DateTime currentDateTime = DateTime.UtcNow + TimeSpan.FromHours(3); // +3 GMT _displayService.Show(Screen.DateTime_1, currentDateTime); break; case Screen.TempHum_2: _displayService.Show(Screen.TempHum_2, sensorsResult); break; ... } Thread.Sleep(500); } }); _handlerThread.Start(); }
public void Start()
{
  ...
  _handlerThread = new Thread(() =>
  {
    while (!csToken.IsCancellationRequested)
    {
      //sensors
      sensorsResult = _sensorsService.GetSensorsResult();      
      //key
      key = _keyboardService.ReadKey();
      switch (key)
      {
        case 8:
          currentScreen = Screen.DateTime_1;
          break;
        ...
      }
      //screen
      switch (currentScreen)
      {
        case Screen.DateTime_1:
          DateTime currentDateTime = DateTime.UtcNow + TimeSpan.FromHours(3); // +3 GMT
          _displayService.Show(Screen.DateTime_1, currentDateTime);
          break;
        case Screen.TempHum_2:
          _displayService.Show(Screen.TempHum_2, sensorsResult);
          break;
        ...
      }
      Thread.Sleep(500);
    }
  });
  _handlerThread.Start();
}
Используя интерфейс, получаем показания датчиков:

sensorsResult = _sensorsService.GetSensorsResult();
sensorsResult = _sensorsService.GetSensorsResult();
sensorsResult = _sensorsService.GetSensorsResult();  

Далее, считываем код кнопки:

key = _keyboardService.ReadKey();
key = _keyboardService.ReadKey();
key = _keyboardService.ReadKey();

В зависимости от нажатой кнопки отправляем на интерфейс дисплея необходимые данные для отображения. Где  Screen.DateTime_1  и  Screen.TempHum_2  тип экрана для отображения, второй параметр тип  object  данные для отображения.

switch (currentScreen)
{
case Screen.DateTime_1:
DateTime currentDateTime = DateTime.UtcNow + TimeSpan.FromHours(3); // +3 GMT
_displayService.Show(Screen.DateTime_1, currentDateTime);
break;
case Screen.TempHum_2:
_displayService.Show(Screen.TempHum_2, sensorsResult);
break;
...
switch (currentScreen) { case Screen.DateTime_1: DateTime currentDateTime = DateTime.UtcNow + TimeSpan.FromHours(3); // +3 GMT _displayService.Show(Screen.DateTime_1, currentDateTime); break; case Screen.TempHum_2: _displayService.Show(Screen.TempHum_2, sensorsResult); break; ...
switch (currentScreen)
      {
        case Screen.DateTime_1:
          DateTime currentDateTime = DateTime.UtcNow + TimeSpan.FromHours(3); // +3 GMT
          _displayService.Show(Screen.DateTime_1, currentDateTime);
          break;
        case Screen.TempHum_2:
          _displayService.Show(Screen.TempHum_2, sensorsResult);
          break;
...

Для завершения работы сервиса вызывается метод  Stop() .

public void Stop()
{
Debug.WriteLine("MonitorService stopped");
_cs.Cancel();
Thread.Sleep(2000);
if (_handlerThread.ThreadState == ThreadState.Running) _handlerThread.Abort();
}
public void Stop() { Debug.WriteLine("MonitorService stopped"); _cs.Cancel(); Thread.Sleep(2000); if (_handlerThread.ThreadState == ThreadState.Running) _handlerThread.Abort(); }
public void Stop()
{
  Debug.WriteLine("MonitorService stopped");
  _cs.Cancel();
  Thread.Sleep(2000);
  if (_handlerThread.ThreadState == ThreadState.Running) _handlerThread.Abort();
}

Теперь перейдем к основному  host builder .

Generic Host

Для создания хоста запускается построитель хоста (host builder), в задачу которого входит создание контейнера сервисов, т.е. создается  ServiceProvider  содержащий коллекцию сервисов.

Основная функция  Main() , файл Program.cs:

public static void Main()
{
//////////////////////////////////////////////////////////////////////
// when connecting to an ESP32 device, need to configure the I2C GPIOs
Configuration.SetPinFunction(21, DeviceFunction.I2C1_DATA);
Configuration.SetPinFunction(22, DeviceFunction.I2C1_CLOCK);
//////////////////////////////////////////////////////////////////////
IHost host = CreateHostBuilder().Build();
// starts application and blocks the main calling thread
host.Run();
}
public static void Main() { ////////////////////////////////////////////////////////////////////// // when connecting to an ESP32 device, need to configure the I2C GPIOs Configuration.SetPinFunction(21, DeviceFunction.I2C1_DATA); Configuration.SetPinFunction(22, DeviceFunction.I2C1_CLOCK); ////////////////////////////////////////////////////////////////////// IHost host = CreateHostBuilder().Build(); // starts application and blocks the main calling thread host.Run(); }
public static void Main()
{
  //////////////////////////////////////////////////////////////////////
  // when connecting to an ESP32 device, need to configure the I2C GPIOs
  Configuration.SetPinFunction(21, DeviceFunction.I2C1_DATA);
  Configuration.SetPinFunction(22, DeviceFunction.I2C1_CLOCK);
  //////////////////////////////////////////////////////////////////////            
  IHost host = CreateHostBuilder().Build();
  // starts application and blocks the main calling thread 
  host.Run();
}

До использования датчиков на шине I2C необходимо объявить соответствующие контакты 21-pin и 22-pin.

Затем построитель хоста создает  host  и затем его запускаем методом  Run() .

Регистрация сервисов выполняется в отдельной функции  CreateHostBuilder() :

public static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
//Receiving data from sensors
services.AddSingleton(typeof(ISensorsService), typeof(SensorsSingleton));
//Data output to the display
services.AddSingleton(typeof(IDisplayService), typeof(DisplaySingleton));
//Keyboard
services.AddSingleton(typeof(IKeyboardService), typeof(KeyboardSingleton));
//MonitorService
services.AddHostedService(typeof(MonitorService));
//Connecting to WiFi and time synchronization
services.AddHostedService(typeof(ConnectionService));
});
public static IHostBuilder CreateHostBuilder() => Host.CreateDefaultBuilder() .ConfigureServices(services => { //Receiving data from sensors services.AddSingleton(typeof(ISensorsService), typeof(SensorsSingleton)); //Data output to the display services.AddSingleton(typeof(IDisplayService), typeof(DisplaySingleton)); //Keyboard services.AddSingleton(typeof(IKeyboardService), typeof(KeyboardSingleton)); //MonitorService services.AddHostedService(typeof(MonitorService)); //Connecting to WiFi and time synchronization services.AddHostedService(typeof(ConnectionService)); });
public static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
  .ConfigureServices(services =>
  {
    //Receiving data from sensors
    services.AddSingleton(typeof(ISensorsService), typeof(SensorsSingleton));
    //Data output to the display
    services.AddSingleton(typeof(IDisplayService), typeof(DisplaySingleton));
    //Keyboard
    services.AddSingleton(typeof(IKeyboardService), typeof(KeyboardSingleton));
    //MonitorService
    services.AddHostedService(typeof(MonitorService));
    //Connecting to WiFi and time synchronization
    services.AddHostedService(typeof(ConnectionService));
  });

Необходимо обратить внимание, что жизненный цикл  Singleton  начинается только тогда, когда они вызывются из  HostedService .

Последним из сервисов вызывается сервис подключения по беспроводному соединению Wi-Fi к сети Интернет для синхронизации времени. Рассмотрим его подробнее.

Классы BackgroundService и SchedulerService

Дополнительно в библиотеке есть классы  SchedulerService  и  BackgroundService , образованные от  IHostedService .

Класс SchedulerService

Данных класс предназначен для выполнения повторяющихся действий с заданным интервалом времени. Класс содержит объект Timer и запускает в указанное время с заданным интервалом асинхронный метод  ExecuteAsync() . Таймер выключается вызовом метода  Stop() .

Пример сервиса на базе класса SchedulerService:

public class DisplayService : SchedulerService
{
// represents a timer control that involks ExecuteAsync at a
// specified interval of time repeatedly
public DisplayService() : base(TimeSpan.FromSeconds(1)) {}
protected override void ExecuteAsync(object state)
{
}
}
public class DisplayService : SchedulerService { // represents a timer control that involks ExecuteAsync at a // specified interval of time repeatedly public DisplayService() : base(TimeSpan.FromSeconds(1)) {} protected override void ExecuteAsync(object state) { } }
public class DisplayService : SchedulerService
{
  // represents a timer control that involks ExecuteAsync at a 
  // specified interval of time repeatedly
  public DisplayService() : base(TimeSpan.FromSeconds(1)) {}

  protected override void ExecuteAsync(object state)
  {   
  }
}

Класс BackgroundService

Класс предназначен для выполнения долгоработающей фоновой задачи. Для запуска сервиса вызывается асинхронный метод  ExecuteAsync() . Работа  ExecuteAsync()  должна завершиться сразу после вызова  CancellationRequested  для корректного завершения работы сервиса.

Пример сервиса на базе класса BackgroundService:

public class SensorService : BackgroundService
{
protected override void ExecuteAsync()
{
while (!CancellationRequested)
{
// to allow other threads time to process include
// at least one millsecond sleep in loop
Thread.Sleep(1);
}
}
}
public class SensorService : BackgroundService { protected override void ExecuteAsync() { while (!CancellationRequested) { // to allow other threads time to process include // at least one millsecond sleep in loop Thread.Sleep(1); } } }
public class SensorService : BackgroundService
{
  protected override void ExecuteAsync()
  {
    while (!CancellationRequested)
    {
      // to allow other threads time to process include 
      // at least one millsecond sleep in loop
      Thread.Sleep(1);
    }
  }
}

На основе класса  BackgroundService  реазизована задача подключения к беспроводной сети Wi-Fi.

Сервис ConnectionService

В задачу сервиса входит подключение к беспроводной сети с последующей синхроизацией времени. После подключение к сети отправляется запрос точного времени на сервер «0.fr.pool.ntp.org», адрес NTP-сервера времени можно выставить любой.

Файл ConnectionService.cs
protected override void ExecuteAsync()
{
//connecting to WiFi
const string Ssid = "ssid";
const string Password = "password";
// Give 60 seconds to the wifi join to happen
CancellationTokenSource cs = new(60000);
bool flag = false;
while (!flag)
{
var success = WifiNetworkHelper.ConnectDhcp(Ssid, Password, System.Device.Wifi.WifiReconnectionKind.Manual, requiresDateTime: false, token: cs.Token);
if (!success)
{
// Something went wrong, you can get details with the ConnectionError property:
Debug.WriteLine($"Can't connect to the network, error: {WifiNetworkHelper.Status}");
if (WifiNetworkHelper.HelperException != null)
Debug.WriteLine($"ex: {WifiNetworkHelper.HelperException}");
}
else
{
Debug.WriteLine($"success");
flag = true;
}
}
//time synchronization
Sntp.Server1 = "0.fr.pool.ntp.org";
Sntp.UpdateNow();
Debug.WriteLine($"Now: {DateTime.UtcNow}");
}
protected override void ExecuteAsync() { //connecting to WiFi const string Ssid = "ssid"; const string Password = "password"; // Give 60 seconds to the wifi join to happen CancellationTokenSource cs = new(60000); bool flag = false; while (!flag) { var success = WifiNetworkHelper.ConnectDhcp(Ssid, Password, System.Device.Wifi.WifiReconnectionKind.Manual, requiresDateTime: false, token: cs.Token); if (!success) { // Something went wrong, you can get details with the ConnectionError property: Debug.WriteLine($"Can't connect to the network, error: {WifiNetworkHelper.Status}"); if (WifiNetworkHelper.HelperException != null) Debug.WriteLine($"ex: {WifiNetworkHelper.HelperException}"); } else { Debug.WriteLine($"success"); flag = true; } } //time synchronization Sntp.Server1 = "0.fr.pool.ntp.org"; Sntp.UpdateNow(); Debug.WriteLine($"Now: {DateTime.UtcNow}"); }
protected override void ExecuteAsync()
{
  //connecting to WiFi
  const string Ssid = "ssid";
  const string Password = "password";
  // Give 60 seconds to the wifi join to happen
  CancellationTokenSource cs = new(60000);
  bool flag = false;
  while (!flag)
  {
    var success = WifiNetworkHelper.ConnectDhcp(Ssid, Password, System.Device.Wifi.WifiReconnectionKind.Manual, requiresDateTime: false, token: cs.Token);
    if (!success)
    {
      // Something went wrong, you can get details with the ConnectionError property:
      Debug.WriteLine($"Can't connect to the network, error: {WifiNetworkHelper.Status}");
      if (WifiNetworkHelper.HelperException != null)
        Debug.WriteLine($"ex: {WifiNetworkHelper.HelperException}");
    }
    else
    {
      Debug.WriteLine($"success");
      flag = true;
    }
  }
  //time synchronization           
  Sntp.Server1 = "0.fr.pool.ntp.org";
  Sntp.UpdateNow();
  Debug.WriteLine($"Now: {DateTime.UtcNow}");
}

 

Исходный код приложения: GitHub — nanoframework-esp32-di-weatherstation

Доработка библиотеки для дисплея SSD1306 OLED

В прошлый раз библиотека nanoFramework.Iot.Device.Ssd13xx была доработана для поддержки 7-pin контатного варианта дисплея, Pull requests #550. Но на этом работа с дисплеем оказалась не закончена. Во время написания приложения обнаружился неприятный эффект в виде большой паузы во время перерисовки экрана при выводе времени. Перерисовка экрана выполнялась 1 раз в секунду. Проблема заключалась в неверном подходе работы с дисплеем. Рассмотрим текущий алгоритм работы с дисплеем:

using Ssd1306 device = new Ssd1306(I2cDevice.Create(new I2cConnectionSettings(1, Ssd1306.SecondaryI2cAddress)), Ssd13xx.DisplayResolution.OLED128x64, 18);
device.ClearScreen();
device.Font = new BasicFont();
device.DrawString(2, 2, "nF IOT!", 2);//large size 2 font
device.DrawString(2, 32, "nanoFramework", 1, true);//centered text
device.Display();
using Ssd1306 device = new Ssd1306(I2cDevice.Create(new I2cConnectionSettings(1, Ssd1306.SecondaryI2cAddress)), Ssd13xx.DisplayResolution.OLED128x64, 18); device.ClearScreen(); device.Font = new BasicFont(); device.DrawString(2, 2, "nF IOT!", 2);//large size 2 font device.DrawString(2, 32, "nanoFramework", 1, true);//centered text device.Display();
using Ssd1306 device = new Ssd1306(I2cDevice.Create(new I2cConnectionSettings(1, Ssd1306.SecondaryI2cAddress)), Ssd13xx.DisplayResolution.OLED128x64, 18);

device.ClearScreen();
device.Font = new BasicFont();
device.DrawString(2, 2, "nF IOT!", 2);//large size 2 font
device.DrawString(2, 32, "nanoFramework", 1, true);//centered text
device.Display();

Метод  ClearScreen()  очищает экран от изображения. Далее методами  DrawString()  выполняется формирование изображения в буфере дисплея. Метод  Display()  формирует изображение на дисплее исходя из матрицы данных в буфере. Таким образом, видна очевидная проблема при повторной отрисовки изображения, которая заключается в том, что в интервал времени между вызовами методов  ClearScreen()  и  Display()  дисплей заполнен черным цветом.

Для решения этой проблемы в библиотеку необходимо добавить новый метод очистки буфера дисплея. В результате после вызова метода очистки буфера дисплея при формирования нового изображения, на дисплее будет отображаться текущее изображение. Далее при вызове  Display()  изображения отрисуется минуя стадию заполнения дисплея черным цветом, и мигание при отрисовке со стороны пользователя не будет видно.

Для упрощения реализации, в методе  ClearScreen() , просто была закомментирована строка вызова метода  Display()  для перерисовки экрана дисплея.

Файл Ssd13xx.cs, метод  ClearScreen() :

public void ClearScreen()
{
Array.Clear(_genericBuffer, 0, _genericBuffer.Length);
//---> Display();
}
public void ClearScreen() { Array.Clear(_genericBuffer, 0, _genericBuffer.Length); //---> Display(); }
public void ClearScreen()
{
  Array.Clear(_genericBuffer, 0, _genericBuffer.Length);

  //---> Display();
}

Обсуждение на Habr.com

Ресурсы

Вам также может понравиться

About the Author: Anton

Programistik