Теперь, когда мы разобрались с интерфейсом и коммуникацией на стороне C#, пора уделить внимание программированию самой платы Arduino. Тут важно понимать, что код для микроконтроллера отличается от привычного C# не только синтаксисом, но и подходом к организации программы. Вместо событийной модели у нас циклическая структура, а ресурсы сильно ограничены.
Все части статьи:
Управление Arduino из Windows Forms приложения C#. Подключение Arduino и создание приложения
Управление Arduino из Windows Forms приложения C#. Программирование Arduino и отправка команд, датчики
Управление Arduino из Windows Forms приложения C#. Примеры применения
Структура скетча Arduino
Для тех, кто ещё не знаком с базовой структурой программы для Arduino (которая называется "скетч"), напомню, что она состоит из двух основных функций:
| C++ | 1
2
3
4
5
6
7
| void setup() {
// Инициализация происходит один раз при запуске
}
void loop() {
// Этот код выполняется циклически, пока устройство включено
} |
|
Функция setup() выполняется однократно при включении питания или перезагрузке платы. Здесь обычно размещают код инициализации: настройку пинов, инициализацию последовательного порта, подготовку датчиков и т.д. Функция loop() выполняется снова и снова в бесконечном цикле, пока устройство работает.
Обмен данными через последовательный порт
Для связи с C# приложением нам понадобится инициализировать последовательный порт. Делается это в функции setup():
| C++ | 1
2
3
4
5
6
7
8
| void setup() {
// Инициализация последовательного порта на скорости 9600 бод
Serial.begin(9600);
// Настройка пинов для управления устройствами
pinMode(13, OUTPUT); // Встроенный светодиод
pinMode(7, INPUT); // Кнопка
} |
|
Скорость передачи данных (baudrate) должна совпадать с той, что указана в C# приложении. Я обычно использую 9600 для начала, но если вам нужно передавать большие объемы данных, можно увеличить скорость до 115200. Правда учитывайте, что с увеличением скорости растет и вероятность ошибок при передаче.
Чтение команд из последовательного порта
В функции loop() нам нужно постоянно проверять наличие новых данных в буфере последовательного порта и обрабатывать их. Вот простейший пример:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| void loop() {
// Проверяем, есть ли данные в буфере
if (Serial.available() > 0) {
// Читаем символ из буфера
char command = Serial.read();
// Обрабатываем команду
switch(command) {
case 'H':
digitalWrite(13, HIGH); // Включаем светодиод
Serial.println("LED ON");
break;
case 'L':
digitalWrite(13, LOW); // Выключаем светодиод
Serial.println("LED OFF");
break;
default:
Serial.println("Unknown command");
break;
}
}
// Остальной код loop
} |
|
Этот код проверяет наличие данных в буфере с помощью функции Serial.available(), которая возвращает количество байт, доступных для чтения. Если данные есть, мы читаем один символ с помощью Serial.read() и обрабатываем его в конструкции switch-case.
Но в реальных проектах такой простой подход редко бывает достаточным. Возникает несколько проблем:
1. Данные часто приходят по частям, а не целыми командами.
2. Может возникнуть потеря данных, если буфер переполнится.
3. Нужен более гибкий формат команд, чем один символ.
Буферизация и парсинг команд
Для решения этих проблем я обычно реализую простой протокол, где команды имеют формат "КОМАНДА:ПАРАМЕТР1,ПАРАМЕТР2;". Конец команды обозначается символом точки с запятой. Вот как это можно реализовать:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| String inputBuffer = "";
boolean commandComplete = false;
void loop() {
// Читаем данные из порта
while (Serial.available() > 0) {
char inChar = (char)Serial.read();
// Если это символ конца команды, устанавливаем флаг завершенности
if (inChar == ';') {
commandComplete = true;
break;
}
// Иначе добавляем символ в буфер
inputBuffer += inChar;
}
// Если команда получена полностью, обрабатываем ее
if (commandComplete) {
processCommand(inputBuffer);
inputBuffer = "";
commandComplete = false;
}
// Остальной код loop
}
void processCommand(String command) {
// Ищем разделитель между командой и параметрами
int colonIndex = command.indexOf(':');
if (colonIndex != -1) {
String cmd = command.substring(0, colonIndex);
String params = command.substring(colonIndex + 1);
if (cmd.equals("LED")) {
// Ищем разделитель между параметрами
int commaIndex = params.indexOf(',');
if (commaIndex != -1) {
// Выделяем номер пина и состояние
int pin = params.substring(0, commaIndex).toInt();
int state = params.substring(commaIndex + 1).toInt();
// Устанавливаем состояние пина
digitalWrite(pin, state);
// Отправляем подтверждение
Serial.println("LED_ACK");
}
}
else if (cmd.equals("READ")) {
// Чтение состояния пина
int pin = params.toInt();
int state = digitalRead(pin);
// Отправляем результат
Serial.print("READ_ACK:");
Serial.println(state);
}
// Добавьте обработку других команд
}
} |
|
Этот код накапливает символы в строке inputBuffer, пока не встретит разделитель команды (точка с запятой). Затем он вызывает функцию processCommand(), которая разбирает команду на части и выполняет соответствующие действия.
Контроль целостности данных
В идеальном мире все данные передавались бы без ошибок, но в реальности помехи и сбои неизбежны. Для обеспечения целостности данных можно использовать контрольные суммы:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| boolean validateChecksum(String command) {
// Разделяем команду и контрольную сумму
int lastSemicolonIndex = command.lastIndexOf(';');
if (lastSemicolonIndex == -1) return false;
String actualCommand = command.substring(0, lastSemicolonIndex);
String checksumStr = command.substring(lastSemicolonIndex + 1);
// Преобразуем строку контрольной суммы в число
int receivedChecksum = checksumStr.toInt();
// Вычисляем контрольную сумму команды
int calculatedChecksum = 0;
for (int i = 0; i < actualCommand.length(); i++) {
calculatedChecksum += actualCommand.charAt(i);
}
calculatedChecksum %= 256; // Ограничиваем одним байтом
// Сравниваем
return (calculatedChecksum == receivedChecksum);
}
[/CSHARP]
Теперь мы можем модифицировать код обработки команд, чтобы проверять контрольную сумму перед выполнением:
[/CSHARP]cpp
if (commandComplete) {
if (validateChecksum(inputBuffer)) {
// Удаляем контрольную сумму из команды
int lastSemicolonIndex = inputBuffer.lastIndexOf(';');
String actualCommand = inputBuffer.substring(0, lastSemicolonIndex);
processCommand(actualCommand);
} else {
// Отправляем сообщение об ошибке
Serial.println("ERROR:CHECKSUM");
}
inputBuffer = "";
commandComplete = false;
} |
|
Реализация Watchdog-таймера
Одна из частых проблем в работе микроконтроллеров - зависания. Причины могут быть разные: от программных ошибок до внешних помех. Для защиты от таких ситуаций в Arduino есть встроенный watchdog-таймер, который перезагружает плату, если основной код перестает отвечать.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <avr/wdt.h>
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
// Включаем watchdog с таймаутом 8 секунд
wdt_enable(WDTO_8S);
}
void loop() {
// Сбрасываем watchdog в начале каждого цикла
wdt_reset();
// Обработка команд и другой код
// ...
// Если здесь произойдет зависание, watchdog перезагрузит Arduino через 8 секунд
} |
|
Важно: не все платы Arduino поддерживают watchdog. Перед использованием убедитесь, что ваша модель его поддерживает.
Оптимизация использования памяти
Работая с Arduino, всегда нужно помнить об ограничениях памяти. На Arduino Uno всего 2 КБ оперативной памяти и 32 КБ флеш-памяти для программы - это очень мало по современным меркам.
Вот несколько советов для оптимизации использования памяти:
1. Используйте тип char[] вместо String - это менее удобно, но более эффективно по памяти.
2. Не используйте динамическое выделение памяти (new, malloc) без острой необходимости.
3. Размещайте константные строки в PROGMEM, чтобы они хранились во флеш-памяти, а не в RAM.
| C++ | 1
2
3
4
5
| // Вместо
String errorMessage = "Error: Command not recognized";
// Используйте
const char errorMessage[] PROGMEM = "Error: Command not recognized"; |
|
При правильном подходе к программированию Arduino даже с такими ограничениями можно создавать довольно сложные и функциональные устройства. В следующих частях мы рассмотрим более продвинутые техники работы с датчиками и актуаторами, а также взаимодействие с другими библиотеками.
Расширенные методы работы с данными
Когда я начал углублятся в разработку реальных проектов, то понял, что простой обмен текстовыми командами не всегда достаточен. Особенно когда речь идет о высоких скоростях передачи или работе с аналоговыми данными. В таких случаях бинарный протокол может быть гораздо эффективнее.
Вот пример скетча Arduino для работы с бинарным протоколом:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #define CMD_READ_ANALOG 0x01
#define CMD_SET_DIGITAL 0x02
#define CMD_READ_DIGITAL 0x03
void loop() {
if (Serial.available() >= 3) { // Каждая команда содержит минимум 3 байта
byte cmd = Serial.read();
byte pin = Serial.read();
byte value = Serial.read();
switch (cmd) {
case CMD_READ_ANALOG: {
int analogValue = analogRead(pin);
// Отправляем 2 байта (аналоговое значение 0-1023)
Serial.write(analogValue & 0xFF); // Младший байт
Serial.write((analogValue >> 8) & 0xFF); // Старший байт
break;
}
case CMD_SET_DIGITAL:
digitalWrite(pin, value ? HIGH : LOW);
Serial.write(0x01); // Подтверждение
break;
case CMD_READ_DIGITAL:
Serial.write(digitalRead(pin) ? 0x01 : 0x00);
break;
}
}
} |
|
Тут важно обратить внимание на использование Serial.write() вместо Serial.print() - это принципиальное отличие. Метод write отправляет сырые байты, тогда как print преобразует данные в ASCII.
Организация кода в виде конечного автомата
Для более сложных проектов я использую подход на основе конечного автомата (state machine). Это позволяет создавать многозадачные приложения без использования прерываний и сложного планирования.
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| enum State {
STATE_IDLE,
STATE_WAITING_COMMAND,
STATE_PROCESSING_COMMAND,
STATE_SENDING_RESPONSE
};
State currentState = STATE_IDLE;
unsigned long lastStateChangeTime = 0;
String commandBuffer = "";
void loop() {
unsigned long currentTime = millis();
switch (currentState) {
case STATE_IDLE:
// Проверяем, есть ли данные
if (Serial.available() > 0) {
currentState = STATE_WAITING_COMMAND;
lastStateChangeTime = currentTime;
}
// Между проверками можно делать другие действия
updateSensors();
break;
case STATE_WAITING_COMMAND:
// Собираем команду из порта
while (Serial.available() > 0) {
char c = Serial.read();
if (c == ';') {
currentState = STATE_PROCESSING_COMMAND;
break;
}
commandBuffer += c;
}
// Проверка таймаута
if (currentTime - lastStateChangeTime > 5000) {
// Слишком долго ждем команду, возвращаемся в IDLE
commandBuffer = "";
currentState = STATE_IDLE;
}
break;
case STATE_PROCESSING_COMMAND:
processCommand(commandBuffer);
commandBuffer = "";
currentState = STATE_SENDING_RESPONSE;
break;
case STATE_SENDING_RESPONSE:
// Здесь может быть логика для отправки длинных ответов частями
currentState = STATE_IDLE;
break;
}
} |
|
Этот подход позволяет избежать блокировки основного цикла при ожидании данных. Вы можете добавлять новые состояния по мере усложнения логики приложения.
Периодическая отправка данных
Часто возникает необходимость периодически отправлять данные в C# приложение, например, показания датчиков. Вот как я обычно реализую это:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| unsigned long lastSensorUpdateTime = 0;
const unsigned long SENSOR_UPDATE_INTERVAL = 1000; // Интервал в миллисекундах
void loop() {
// Основной код обработки команд
// ...
// Отправка данных с датчиков с заданной периодичностью
unsigned long currentTime = millis();
if (currentTime - lastSensorUpdateTime >= SENSOR_UPDATE_INTERVAL) {
sendSensorData();
lastSensorUpdateTime = currentTime;
}
}
void sendSensorData() {
// Считываем значения с датчиков
int temperatureValue = readTemperature();
int humidityValue = readHumidity();
// Отправляем данные в формате "SENSOR:TYPE,VALUE;"
Serial.print("SENSOR:TEMP,");
Serial.print(temperatureValue);
Serial.println(";");
Serial.print("SENSOR:HUM,");
Serial.print(humidityValue);
Serial.println(";");
} |
|
Функция millis() возвращает количество миллисекунд с момента запуска Arduino, и я использую её для создания неблокирующих таймеров. Но нужно учитывать, что эта функция переполняется примерно каждые 50 дней работы, поэтому для долгосрочных проектов стоит предусмотреть обработку переполнения.
Защита от случайной перезагрузки
Иногда при интенсивном обмене данными или из-за помех на линии Arduino может получать случайные перезагрузки. Вот трюк, который я применяю, чтобы определить перезагрузку и восстановить состояние:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| bool wasReset = true;
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
// Признак перезагрузки - отправляем уведомление C# приложению
Serial.println("SYSTEM:RESET;");
// Код инициализации...
}
void loop() {
// Если это первый проход после перезагрузки
if (wasReset) {
// Запрашиваем текущее состояние от C# приложения
Serial.println("SYSTEM:GET_STATE;");
wasReset = false;
}
// Основной код
} |
|
C# приложение должно обрабатывать сообщение "SYSTEM:RESET;" и отправлять в ответ текущее состояние всех управляемых устройств, чтобы Arduino могло восстановить своё состояние.
Использование прерываний для внешних событий
Для обработки внешних событий, таких как нажатие кнопки, я предпочитаю использовать прерывания, а не постоянный опрос в основном цикле:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| const byte buttonPin = 2; // Пин с поддержкой прерываний (на Arduino Uno это пины 2 и 3)
volatile bool buttonPressed = false;
void setup() {
Serial.begin(9600);
pinMode(buttonPin, INPUT_PULLUP);
// Регистрируем функцию обработчик прерывания
attachInterrupt(digitalPinToInterrupt(buttonPin), buttonInterruptHandler, FALLING);
}
void loop() {
// Проверяем флаг, установленный в прерывании
if (buttonPressed) {
// Отправляем сообщение в C# приложение
Serial.println("EVENT:BUTTON_PRESSED;");
buttonPressed = false;
}
// Основной код
}
// Обработчик прерывания (должен быть максимально коротким!)
void buttonInterruptHandler() {
buttonPressed = true;
} |
|
Важно понимать, что в обработчике прерывания нельзя использовать операции, которые могут занять много времени, например, Serial.println(). Вместо этого мы устанавливаем флаг, который затем проверяется в основном цикле.
Отправка команд из C#
Теперь, когда мы разобрались с программированием Arduino и созданием пользовательского интерфейса, пора связать эти две части воедино и наладить непосредственную отправку команд из нашего C# приложения на микроконтроллер. На первый взгляд это может показаться простой задачей - ведь мы уже настроили SerialPort, но на практике тут скрывается множество подводных камней.
Базовые принципы отправки команд
Начнем с базовой операции отправки команды. Класс SerialPort предоставляет несколько методов для этого:
| C# | 1
2
3
4
5
6
7
8
9
| // Отправка строки с добавлением символа новой строки
serialPort.WriteLine("COMMAND");
// Отправка строки без символа новой строки
serialPort.Write("COMMAND");
// Отправка массива байтов
byte[] data = { 0x01, 0x02, 0x03 };
serialPort.Write(data, 0, data.Length); |
|
На первый взгляд все просто, но на практике я неоднократно сталкивался с тем, что данные не доходят до Arduino или приходят в искаженном виде. Одна из причин - символы окончания строки. В Windows обычно используется комбинация \r\n, а Arduino ожидает только \n. Поэтому я предпочитаю явно контролировать окончания строк:
| C# | 1
2
3
4
5
6
7
8
9
10
11
| public void SendCommand(string command)
{
if (!serialPort.IsOpen)
throw new InvalidOperationException("Port is not open");
// Добавляем символ новой строки, если его нет
if (!command.EndsWith("\n"))
command += "\n";
serialPort.Write(command);
} |
|
Обработка исключений и таймауты
При работе с внешними устройствами исключения - это норма, а не исключение (прости за каламбур). Устройство может быть отключено, кабель поврежден, в линии связи могут возникнуть помехи. Надежное приложение должно корректно обрабатывать все эти ситуации.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| public bool TrySendCommand(string command, out string errorMessage)
{
errorMessage = null;
try
{
// Попытка переоткрыть порт, если он закрыт
if (!serialPort.IsOpen)
{
try
{
serialPort.Open();
// Даем Arduino время на сброс
Thread.Sleep(1500);
}
catch (Exception ex)
{
errorMessage = $"Не удалось открыть порт: {ex.Message}";
return false;
}
}
// Установка таймаута для операции записи
serialPort.WriteTimeout = 1000;
// Отправка команды
if (!command.EndsWith("\n"))
command += "\n";
serialPort.Write(command);
return true;
}
catch (TimeoutException)
{
errorMessage = "Превышено время ожидания при отправке команды";
return false;
}
catch (InvalidOperationException ex)
{
errorMessage = $"Недопустимая операция: {ex.Message}";
return false;
}
catch (Exception ex)
{
errorMessage = $"Ошибка при отправке команды: {ex.Message}";
// Если порт в непригодном состоянии, закрываем его
try
{
if (serialPort.IsOpen)
serialPort.Close();
}
catch { /* Игнорируем ошибки закрытия */ }
return false;
}
} |
|
Такой подход с возвращением булевого значения успеха и выводом сообщения об ошибке через out-параметр я нахожу более удобным для обработки вызывающим кодом, чем просто выбрасывание исключений.
Ожидание ответа от устройства
В большинстве случаев после отправки команды нам нужно дождаться ответа от Arduino. Простейший способ - использовать синхронное чтение с таймаутом:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public string SendCommandAndWaitForResponse(string command, int timeoutMs = 2000)
{
try
{
// Очищаем буфер перед отправкой
serialPort.DiscardInBuffer();
// Отправляем команду
if (!command.EndsWith("\n"))
command += "\n";
serialPort.Write(command);
// Устанавливаем таймаут для чтения
serialPort.ReadTimeout = timeoutMs;
// Читаем ответ
return serialPort.ReadLine();
}
catch (TimeoutException)
{
return "ERROR:TIMEOUT";
}
catch (Exception ex)
{
return $"ERROR:{ex.Message}";
}
} |
|
Однако этот подход имеет серьезный недостаток - он блокирует поток выполнения до получения ответа или истечения таймаута. Для пользовательского интерфейса это может привести к "зависанию". Поэтому лучше использовать асинхронный подход:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| public async Task<string> SendCommandAndWaitForResponseAsync(string command, int timeoutMs = 2000)
{
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
// Обработчик события получения данных
SerialDataReceivedEventHandler handler = null;
handler = (s, e) =>
{
try
{
string response = serialPort.ReadLine();
// Отписываемся от события
serialPort.DataReceived -= handler;
// Устанавливаем результат
tcs.TrySetResult(response);
}
catch (Exception ex)
{
// В случае ошибки также отписываемся и устанавливаем исключение
serialPort.DataReceived -= handler;
tcs.TrySetException(ex);
}
};
try
{
// Очищаем буфер
serialPort.DiscardInBuffer();
// Подписываемся на событие
serialPort.DataReceived += handler;
// Отправляем команду
if (!command.EndsWith("\n"))
command += "\n";
serialPort.Write(command);
// Создаем задачу с таймаутом
Task<string> resultTask = tcs.Task;
Task timeoutTask = Task.Delay(timeoutMs);
// Ожидаем либо результат, либо таймаут
Task completedTask = await Task.WhenAny(resultTask, timeoutTask);
if (completedTask == timeoutTask)
{
// Таймаут - отписываемся от события
serialPort.DataReceived -= handler;
return "ERROR:TIMEOUT";
}
// Получен результат
return await resultTask;
}
catch (Exception ex)
{
// Обеспечиваем отписку от события в случае ошибки
serialPort.DataReceived -= handler;
return $"ERROR:{ex.Message}";
}
} |
|
Очередь команд и приоритизация
В сложных проектах часто возникает необходимость отправлять множество команд подряд, причем некоторые из них могут быть более срочными, чем другие. Например, команда аварийной остановки должна выполняться немедленно, независимо от того, какие другие команды находятся в очереди. Для решения этой задачи я реализую систему очереди команд с приоритетами:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| public class CommandQueue
{
// Очередь команд с их приоритетами
private SortedList<int, Queue<CommandItem>> _priorityQueues = new SortedList<int, Queue<CommandItem>>();
// Блокировка для потокобезопасного доступа
private object _lock = new object();
// Класс, представляющий команду
public class CommandItem
{
public string Command { get; }
public TaskCompletionSource<string> ResponseTask { get; }
public CommandItem(string command, TaskCompletionSource<string> responseTask)
{
Command = command;
ResponseTask = responseTask;
}
}
// Добавление команды в очередь
public Task<string> EnqueueCommand(string command, int priority = 0)
{
var tcs = new TaskCompletionSource<string>();
lock (_lock)
{
// Создаем очередь для приоритета, если её еще нет
if (!_priorityQueues.ContainsKey(priority))
_priorityQueues[priority] = new Queue<CommandItem>();
// Добавляем команду в соответствующую очередь
_priorityQueues[priority].Enqueue(new CommandItem(command, tcs));
}
// Возвращаем задачу, которая будет завершена, когда придет ответ
return tcs.Task;
}
// Получение следующей команды из очереди
public bool TryDequeueCommand(out CommandItem command)
{
lock (_lock)
{
command = null;
// Перебираем очереди по приоритетам (от высокого к низкому)
foreach (var queue in _priorityQueues.Values.Reverse())
{
if (queue.Count > 0)
{
command = queue.Dequeue();
return true;
}
}
return false;
}
}
} |
|
А теперь объединим очередь команд с отправкой и получением ответов:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
| public class ArduinoCommunicator : IDisposable
{
private SerialPort _serialPort;
private CommandQueue _commandQueue = new CommandQueue();
private bool _isProcessingQueue = false;
private object _processingLock = new object();
// Конструктор и методы инициализации порта опущены для краткости
// Метод для отправки команды через очередь
public Task<string> SendCommandAsync(string command, int priority = 0)
{
var resultTask = _commandQueue.EnqueueCommand(command, priority);
// Запускаем обработку очереди, если она еще не запущена
StartQueueProcessing();
return resultTask;
}
private void StartQueueProcessing()
{
lock (_processingLock)
{
if (_isProcessingQueue)
return;
_isProcessingQueue = true;
// Запускаем обработку в отдельном потоке
Task.Run(() => ProcessCommandQueue());
}
}
private async Task ProcessCommandQueue()
{
try
{
while (true)
{
// Получаем следующую команду из очереди
if (!_commandQueue.TryDequeueCommand(out var commandItem))
{
// Очередь пуста - завершаем обработку
lock (_processingLock)
{
_isProcessingQueue = false;
}
break;
}
try
{
// Отправляем команду и ждем ответа
string response = await SendCommandAndWaitForResponseAsync(commandItem.Command);
// Устанавливаем результат
commandItem.ResponseTask.TrySetResult(response);
}
catch (Exception ex)
{
// В случае ошибки устанавливаем исключение
commandItem.ResponseTask.TrySetException(ex);
}
}
}
catch (Exception)
{
// Обработка общих ошибок
lock (_processingLock)
{
_isProcessingQueue = false;
}
}
}
// Реализация отправки команды и ожидания ответа
private async Task<string> SendCommandAndWaitForResponseAsync(string command)
{
// Реализация аналогична показанной выше
}
public void Dispose()
{
if (_serialPort != null)
{
if (_serialPort.IsOpen)
_serialPort.Close();
_serialPort.Dispose();
_serialPort = null;
}
}
} |
|
Паттерн Command для инкапсуляции операций
Для еще более гибкого и расширяемого кода я рекомендую использовать паттерн Command. Этот паттерн позволяет инкапсулировать запрос как объект, что дает возможность параметризовать клиентов разными запросами, ставить запросы в очередь, логировать их и поддерживать отмену операций. Давайте реализуем паттерн Command для нашего проекта взаимодействия с Arduino:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // Базовый интерфейс команды
public interface IArduinoCommand
{
Task<string> ExecuteAsync(IArduinoController controller);
string GetCommandString();
int Priority { get; }
}
// Базовая реализация команды
public abstract class ArduinoCommandBase : IArduinoCommand
{
public virtual int Priority => 0; // Обычный приоритет по умолчанию
public abstract string GetCommandString();
public async Task<string> ExecuteAsync(IArduinoController controller)
{
return await controller.SendCommandAsync(GetCommandString(), Priority);
}
}
// Пример конкретной команды - установка состояния светодиода
public class SetLedCommand : ArduinoCommandBase
{
private readonly int _pin;
private readonly bool _state;
public SetLedCommand(int pin, bool state)
{
_pin = pin;
_state = state;
}
public override string GetCommandString()
{
return $"LED:{_pin},{(_state ? "1" : "0")};";
}
}
// Команда экстренной остановки с высоким приоритетом
public class EmergencyStopCommand : ArduinoCommandBase
{
public override int Priority => 10; // Высокий приоритет
public override string GetCommandString()
{
return "STOP;";
}
} |
|
Теперь я могу использовать эти команды в коде интерфейса:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| private async void buttonStop_Click(object sender, EventArgs e)
{
try
{
IArduinoCommand command = new EmergencyStopCommand();
string response = await command.ExecuteAsync(_arduinoController);
LogResponse(response);
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка при отправке команды: {ex.Message}");
}
} |
|
Одно из преимуществ паттерна Command – возможность логирования и аудита действий. Расширим нашу реализацию:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| public class CommandExecutionLogger
{
private readonly string _logPath;
public CommandExecutionLogger(string logPath)
{
_logPath = logPath;
Directory.CreateDirectory(Path.GetDirectoryName(logPath));
}
public void LogCommand(IArduinoCommand command, string response, bool success)
{
try
{
string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | " +
$"Command: {command.GetCommandString()} | " +
$"Response: {response} | " +
$"Success: {success}";
File.AppendAllText(_logPath, logEntry + Environment.NewLine);
}
catch
{
// Логирование не должно прерывать основной процесс
}
}
} |
|
И используем логер в нашем контроллере:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class ArduinoController : IArduinoController
{
private readonly CommandExecutionLogger _logger;
public ArduinoController()
{
_logger = new CommandExecutionLogger("arduino_commands.log");
}
public async Task<string> ExecuteCommandAsync(IArduinoCommand command)
{
string response = string.Empty;
bool success = false;
try
{
response = await command.ExecuteAsync(this);
success = !response.StartsWith("ERROR:");
}
catch (Exception ex)
{
response = $"ERROR:{ex.Message}";
}
finally
{
_logger.LogCommand(command, response, success);
}
return response;
}
} |
|
Для отмены операций мы можем использовать токены отмены:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public async Task<string> SendCommandWithCancellationAsync(
IArduinoCommand command,
CancellationToken cancellationToken)
{
// Регистрируем задачу в очереди
var resultTask = _commandQueue.EnqueueCommand(command.GetCommandString(), command.Priority);
// Запускаем обработку очереди
StartQueueProcessing();
// Ожидаем завершения или отмены
await Task.WhenAny(resultTask, Task.Delay(-1, cancellationToken));
if (cancellationToken.IsCancellationRequested)
{
// Можно добавить логику для уведомления Arduino об отмене операции
return "CANCELLED";
}
return await resultTask;
} |
|
А вот пример использования отмены в UI:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| private CancellationTokenSource _cts;
private async void buttonStartOperation_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
buttonCancel.Enabled = true;
try
{
var command = new StartOperationCommand();
string result = await _arduinoController.SendCommandWithCancellationAsync(
command, _cts.Token);
if (result == "CANCELLED")
LogMessage("Операция отменена пользователем");
else
LogMessage($"Результат: {result}");
}
catch (Exception ex)
{
LogMessage($"Ошибка: {ex.Message}");
}
finally
{
buttonCancel.Enabled = false;
}
}
private void buttonCancel_Click(object sender, EventArgs e)
{
_cts?.Cancel();
} |
|
Такой подход позволяет создать гибкую, расширяемую и поддерживаемую систему управления Arduino из C# приложения, обеспечивая при этом надежную обработку ошибок, логирование действий и возможность отмены операций.
Arduino и датчики температуры Хотелось бы реализовать такую программу, которая получает значения с 3-х датчиков температуры,... Как подключать датчики Arduino Sensor Kit к Nucleo Имеется набор Arduino Sensor Kit, использоватьсобираюсь совместно с Nucleo STM32F072RB. Вопрос... Управление 12 битным сигналом с ЦАП опираясь на датчики угла поворота (ADuCM360/ADuCM361) В вкратце, с помощью 12 битного аналогового сигнала должен управлять габаритным мотором. Сигнал... Как элементы Windows Forms использовать для грамотного расположения двух таблиц Windows Forms? Как элементы Windows Forms использовать для грамотного расположения двух таблиц Windows Forms?
...
Получение данных с датчиков
Работа с датчиками - одна из самых интересных и полезных возможностей Arduino. Я помню, как впервые подключил датчик температуры и с детским восторгом наблюдал, как в консоли меняются цифры при нагревании сенсора зажигалкой. Но настоящая магия происходит, когда данные попадают в полноценное Windows-приложение, где их можно анализировать, визуализировать и сохранять.
Типы датчиков и особенности работы с ними
Мир сенсоров для Arduino поражает разнообразием. Вот несколько основных типов, с которыми мне приходилось работать:
Аналоговые датчики (температуры, освещенности, влажности почвы) - выдают непрерывный сигнал, который считывается через аналоговые входы Arduino функцией analogRead().
Цифровые датчики с простым интерфейсом (кнопки, герконы, датчики движения) - выдают бинарный сигнал (0 или 1), который считывается через digitalRead().
Цифровые датчики с собственным протоколом (DHT11/22, DS18B20, BMP280) - требуют использования специальных библиотек.
Датчики с интерфейсом I2C или SPI (MPU6050, BMP280, LCD-дисплеи) - подключаются к специальным выводам Arduino и требуют соответствующих библиотек.
Выбор типа датчика существенно влияет на способ получения данных. Например, для простого аналогового сенсора температуры LM35 код будет выглядеть так:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| int sensorPin = A0;
float temperature;
void setup() {
Serial.begin(9600);
}
void loop() {
int sensorValue = analogRead(sensorPin);
// Преобразуем значение АЦП в напряжение
float voltage = sensorValue * (5.0 / 1023.0);
// Преобразуем напряжение в температуру (для LM35)
temperature = voltage * 100.0;
// Отправляем данные в формате "TEMP:значение;"
Serial.print("TEMP:");
Serial.print(temperature);
Serial.println(";");
delay(1000); // Обновляем раз в секунду
} |
|
А для цифрового датчика DHT22 код существенно отличается:
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| #include <DHT.h>
#define DHTPIN 2 // Пин подключения датчика
#define DHTTYPE DHT22 // Тип датчика
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(9600);
dht.begin();
}
void loop() {
// Небольшая задержка между измерениями (DHT22 медленный)
delay(2000);
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
// Проверка на ошибки чтения
if (isnan(humidity) || isnan(temperature)) {
Serial.println("ERROR:DHT;");
return;
}
// Отправляем данные о температуре
Serial.print("TEMP:");
Serial.print(temperature);
Serial.println(";");
// Отправляем данные о влажности
Serial.print("HUM:");
Serial.print(humidity);
Serial.println(";");
} |
|
Чтение данных с датчиков в C# приложении
На стороне C# нам нужно обработать входящие данные и преобразовать их в нужный формат. Обычно я реализую отдельный класс для каждого типа датчика, который умеет парсить соответствующий формат данных:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| public class TemperatureSensor
{
private float _lastValue;
private DateTime _lastReadTime;
private List<SensorReading> _readings = new List<SensorReading>();
public float CurrentValue => _lastValue;
public DateTime LastReadTime => _lastReadTime;
public IReadOnlyList<SensorReading> Readings => _readings;
// Структура для хранения показания с временной меткой
public struct SensorReading
{
public DateTime Timestamp { get; }
public float Value { get; }
public SensorReading(DateTime timestamp, float value)
{
Timestamp = timestamp;
Value = value;
}
}
// Метод для обработки данных от Arduino
public bool TryProcessData(string data)
{
// Проверяем, что данные предназначены для этого датчика
if (!data.StartsWith("TEMP:"))
return false;
// Извлекаем значение
string valueString = data.Substring(5).TrimEnd(';');
if (!float.TryParse(valueString, out float value))
return false;
// Обновляем текущее значение и время считывания
_lastValue = value;
_lastReadTime = DateTime.Now;
// Добавляем в историю
_readings.Add(new SensorReading(_lastReadTime, value));
// Ограничиваем размер истории
if (_readings.Count > 1000)
_readings.RemoveAt(0);
return true;
}
} |
|
Для контроля нескольких датчиков я обычно создаю менеджер сенсоров:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class SensorManager
{
private Dictionary<string, ISensor> _sensors = new Dictionary<string, ISensor>();
public void RegisterSensor(string id, ISensor sensor)
{
_sensors[id] = sensor;
}
public void ProcessData(string data)
{
foreach (var sensor in _sensors.Values)
{
if (sensor.TryProcessData(data))
break; // Данные обработаны
}
}
public T GetSensor<T>(string id) where T : ISensor
{
if (_sensors.TryGetValue(id, out ISensor sensor) && sensor is T typedSensor)
return typedSensor;
return default;
}
} |
|
Этот менеджер можно использовать в обработчике получения данных от Arduino:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = _serialPort.ReadLine().Trim();
// Обрабатываем данные в менеджере датчиков
this.Invoke((MethodInvoker)delegate {
_sensorManager.ProcessData(data);
UpdateSensorDisplay();
});
}
catch (Exception ex)
{
Debug.WriteLine($"Error reading data: {ex.Message}");
}
} |
|
Визуализация данных с датчиков
Получение данных - это только полдела. Не менее важно правильно их отобразить. В Windows Forms для этого есть несколько подходов:
1. Простое текстовое представление - отображение текущего значения в Label.
2. Графическое представление - использование стандартного компонента Chart или сторонней библиотеки вроде LiveCharts.
3. Индикаторы и шкалы - создание кастомных элементов для аналоговых шкал, спидометров и т.д.
Для создания простой линейной диаграммы я обычно использую стандартный Chart:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| private void InitializeTemperatureChart()
{
// Настройка серии данных
chartTemperature.Series.Clear();
var series = new Series("Temperature");
series.ChartType = SeriesChartType.Line;
series.XValueType = ChartValueType.DateTime;
chartTemperature.Series.Add(series);
// Настройка осей
chartTemperature.ChartAreas[0].AxisX.LabelStyle.Format = "HH:mm:ss";
chartTemperature.ChartAreas[0].AxisX.Title = "Время";
chartTemperature.ChartAreas[0].AxisY.Title = "Температура (°C)";
// Начальные границы
chartTemperature.ChartAreas[0].AxisY.Minimum = 0;
chartTemperature.ChartAreas[0].AxisY.Maximum = 50;
// Авто-масштабирование по оси X
chartTemperature.ChartAreas[0].AxisX.ScrollBar.Enabled = true;
chartTemperature.ChartAreas[0].CursorX.AutoScroll = true;
}
private void UpdateTemperatureChart()
{
var tempSensor = _sensorManager.GetSensor<TemperatureSensor>("temp1");
if (tempSensor == null)
return;
var series = chartTemperature.Series[0];
series.Points.Clear();
foreach (var reading in tempSensor.Readings)
{
series.Points.AddXY(reading.Timestamp, reading.Value);
}
// Обновляем границы по Y, если нужно
double min = tempSensor.Readings.Count > 0 ?
tempSensor.Readings.Min(r => r.Value) : 0;
double max = tempSensor.Readings.Count > 0 ?
tempSensor.Readings.Max(r => r.Value) : 50;
// Добавляем небольшой запас к границам
chartTemperature.ChartAreas[0].AxisY.Minimum = Math.Max(0, min - 5);
chartTemperature.ChartAreas[0].AxisY.Maximum = max + 5;
// Фокусируемся на последних данных
if (series.Points.Count > 0)
{
chartTemperature.ChartAreas[0].AxisX.ScaleView.Zoom(
DateTime.Now.AddMinutes(-5), DateTime.Now);
}
} |
|
Калибровка датчиков через интерфейс приложения
Почти всегда датчики требуют калибровки для повышения точности измерений. Я добавляю в приложение возможность настройки коэффициентов калибровки для каждого датчика:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| public class CalibratableSensor : ISensor
{
private float _offset = 0.0f; // Сдвиг (аддитивная составляющая)
private float _scale = 1.0f; // Масштаб (мультипликативная составляющая)
public float Offset
{
get => _offset;
set => _offset = value;
}
public float Scale
{
get => _scale;
set => _scale = value != 0 ? value : 1.0f; // Предотвращаем деление на 0
}
// Применяем калибровку к сырому значению
protected float ApplyCalibration(float rawValue)
{
return rawValue * _scale + _offset;
}
// Сохранение калибровочных коэффициентов
public void SaveCalibration(string sensorId)
{
Properties.Settings.Default[$"{sensorId}_Offset"] = _offset;
Properties.Settings.Default[$"{sensorId}_Scale"] = _scale;
Properties.Settings.Default.Save();
}
// Загрузка калибровочных коэффициентов
public void LoadCalibration(string sensorId)
{
if (Properties.Settings.Default[$"{sensorId}_Offset"] != null)
_offset = (float)Properties.Settings.Default[$"{sensorId}_Offset"];
if (Properties.Settings.Default[$"{sensorId}_Scale"] != null)
_scale = (float)Properties.Settings.Default[$"{sensorId}_Scale"];
}
} |
|
А в интерфейсе можно реализовать форму калибровки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| private void buttonCalibrate_Click(object sender, EventArgs e)
{
var sensor = _sensorManager.GetSensor<TemperatureSensor>("temp1");
if (sensor == null)
return;
using (var calibrationForm = new CalibrationForm(sensor))
{
if (calibrationForm.ShowDialog() == DialogResult.OK)
{
// Калибровка применена в форме
UpdateSensorDisplay();
}
}
} |
|
Фильтрация шумов и математическая обработка
В реальном мире датчики подвержены шумам и помехам. Для получения более стабильных показаний я применяю различные алгоритмы фильтрации. Самый простой - скользящее среднее:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public class MovingAverageFilter
{
private Queue<float> _values = new Queue<float>();
private int _windowSize;
private float _sum = 0;
public MovingAverageFilter(int windowSize = 10)
{
_windowSize = windowSize > 0 ? windowSize : 1;
}
public float ProcessValue(float newValue)
{
// Добавляем новое значение
_values.Enqueue(newValue);
_sum += newValue;
// Удаляем старое значение, если превышен размер окна
if (_values.Count > _windowSize)
{
float oldValue = _values.Dequeue();
_sum -= oldValue;
}
// Возвращаем среднее
return _sum / _values.Count;
}
} |
|
Более продвинутый вариант - фильтр Калмана, который отлично справляется с случайными шумами, сохраняя при этом способность быстро реагировать на реальные изменения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class KalmanFilter
{
private float _estimate; // Текущая оценка
private float _errorEst; // Ошибка оценки
private float _errorMeas; // Ошибка измерения
private float _q; // Шум процесса
public KalmanFilter(float initialValue = 0, float initialErrorEstimate = 1,
float measurementError = 1, float processNoise = 0.01f)
{
_estimate = initialValue;
_errorEst = initialErrorEstimate;
_errorMeas = measurementError;
_q = processNoise;
}
public float ProcessValue(float measurement)
{
// Предсказание
float errorPred = _errorEst + _q;
// Обновление
float gain = errorPred / (errorPred + _errorMeas);
_estimate += gain * (measurement - _estimate);
_errorEst = (1 - gain) * errorPred;
return _estimate;
}
} |
|
Для датчиков с известными характеристиками я также реализую специфическую математическую обработку. Например, для датчика расстояния Sharp GP2Y0A21YK, выходной сигнал которого нелинеен, применяю такую формулу:
| C# | 1
2
3
4
5
6
7
8
9
10
| // Преобразование аналогового значения в расстояние для Sharp GP2Y0A21YK
private float ConvertToDistance(int adcValue)
{
// Предотвращение деления на 0
if (adcValue <= 0)
return 500; // Максимальное значение в мм
// Эмпирическая формула преобразования
return 27.86f * (float)Math.Pow(adcValue, -1.15f) * 100;
} |
|
Обработка нескольких датчиков одновременно
Нередко в моих проектах приходится работать одновременно с несколькими датчиками разных типов. Это создаёт дополнительные сложности, но и открывает новые возможности для анализа данных. Я обычно группирую датчики по функциональности и создаю абстракцию, которая позволяет работать с ними единообразно.
Вот пример абстрактного класса для датчика, который можно расширять для конкретных типов:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public abstract class SensorBase : ISensor
{
protected string _id;
protected string _name;
protected string _units;
protected float _warningThresholdLow;
protected float _warningThresholdHigh;
protected bool _isWarningActive;
// Обработчик события предупреждений
public event EventHandler<SensorWarningEventArgs> WarningTriggered;
public string Id => _id;
public string Name => _name;
public string Units => _units;
public abstract float CurrentValue { get; }
public bool IsWarningActive => _isWarningActive;
// Метод проверки предупреждений
protected void CheckWarningThresholds(float value)
{
bool wasWarningActive = _isWarningActive;
_isWarningActive = value < _warningThresholdLow || value > _warningThresholdHigh;
// Если состояние изменилось, генерируем событие
if (_isWarningActive != wasWarningActive)
{
OnWarningTriggered(new SensorWarningEventArgs(
_id, _name, value, _isWarningActive,
value < _warningThresholdLow ? SensorWarningType.Low : SensorWarningType.High));
}
}
protected virtual void OnWarningTriggered(SensorWarningEventArgs e)
{
WarningTriggered?.Invoke(this, e);
}
public abstract bool TryProcessData(string data);
} |
|
Сохранение данных для дальнейшего анализа
Одна из самых полезных функций в моих проектах мониторинга - возможность сохранять данные с датчиков для последующего анализа. Я использую несколько подходов:
1. Запись в CSV-файл - простой и универсальный формат;
2. Сохранение в базу данных - для больших объемов и сложных запросов;
3. Экспорт в Excel - для удобства анализа пользователем;
Вот пример реализации записи в CSV:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| public class DataLogger
{
private string _filePath;
private SensorManager _sensorManager;
private Timer _loggingTimer;
private bool _isHeaderWritten = false;
public DataLogger(string filePath, SensorManager sensorManager, int loggingIntervalMs = 5000)
{
_filePath = filePath;
_sensorManager = sensorManager;
_loggingTimer = new Timer(loggingIntervalMs);
_loggingTimer.Elapsed += LoggingTimer_Elapsed;
}
public void Start()
{
_loggingTimer.Start();
}
public void Stop()
{
_loggingTimer.Stop();
}
private void LoggingTimer_Elapsed(object sender, ElapsedEventArgs e)
{
try
{
WriteDataToFile();
}
catch (Exception ex)
{
Debug.WriteLine($"Error writing to log file: {ex.Message}");
}
}
private void WriteDataToFile()
{
// Получаем данные со всех датчиков
var sensors = _sensorManager.GetAllSensors();
// Если это первая запись, добавляем заголовок
if (!_isHeaderWritten || !File.Exists(_filePath))
{
string header = "Timestamp," + string.Join(",", sensors.Select(s => $"{s.Name} ({s.Units})"));
File.WriteAllText(_filePath, header + Environment.NewLine);
_isHeaderWritten = true;
}
// Формируем строку с данными
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string dataLine = timestamp + "," + string.Join(",", sensors.Select(s => s.CurrentValue.ToString("F2")));
// Добавляем в файл
File.AppendAllText(_filePath, dataLine + Environment.NewLine);
}
} |
|
Обнаружение аномалий и предупреждения
Иногда просто собирать и визуализировать данные недостаточно - нужно реагировать на аномальные показания. Я реализую в своих приложениях систему предупреждений с разными уровнями важности.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public enum SensorWarningType
{
Low,
High,
RateOfChange,
Disconnected
}
public class SensorWarningEventArgs : EventArgs
{
public string SensorId { get; }
public string SensorName { get; }
public float Value { get; }
public bool IsActive { get; }
public SensorWarningType WarningType { get; }
public SensorWarningEventArgs(string id, string name, float value,
bool isActive, SensorWarningType type)
{
SensorId = id;
SensorName = name;
Value = value;
IsActive = isActive;
WarningType = type;
}
} |
|
А вот пример обработчика таких предупреждений в пользовательском интерфейсе:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| private void InitializeWarningSystem()
{
foreach (var sensor in _sensorManager.GetAllSensors())
{
sensor.WarningTriggered += Sensor_WarningTriggered;
}
}
private void Sensor_WarningTriggered(object sender, SensorWarningEventArgs e)
{
// Выводим сообщение пользователю
string message = $"Предупреждение: {e.SensorName} - ";
switch (e.WarningType)
{
case SensorWarningType.Low:
message += $"значение слишком низкое ({e.Value})";
break;
case SensorWarningType.High:
message += $"значение слишком высокое ({e.Value})";
break;
case SensorWarningType.RateOfChange:
message += $"слишком быстрое изменение";
break;
case SensorWarningType.Disconnected:
message += "потеряна связь с датчиком";
break;
}
// Обновляем UI в соответствии с состоянием предупреждения
if (e.IsActive)
{
logTextBox.AppendText($"[ПРЕДУПРЕЖДЕНИЕ] {message}{Environment.NewLine}");
// Находим индикатор датчика и меняем его цвет на красный
UpdateSensorIndicator(e.SensorId, Color.Red);
// Воспроизводим звуковое оповещение
SystemSounds.Exclamation.Play();
}
else
{
logTextBox.AppendText($"[ИНФО] Предупреждение для {e.SensorName} снято{Environment.NewLine}");
// Возвращаем обычный цвет индикатора
UpdateSensorIndicator(e.SensorId, Color.Green);
}
} |
|
Создание виртуальных датчиков
Иногда бывает полезно создать "виртуальный" датчик, который не связан напрямую с физическим устройством, а выводит значение на основе других датчиков или расчетов. Например, индекс теплового комфорта на основе температуры и влажности:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| public class HeatIndexSensor : SensorBase
{
private TemperatureSensor _temperatureSensor;
private HumiditySensor _humiditySensor;
private float _lastCalculatedValue;
public HeatIndexSensor(string id, TemperatureSensor tempSensor, HumiditySensor humSensor)
{
_id = id;
_name = "Тепловой индекс";
_units = "°C";
_temperatureSensor = tempSensor;
_humiditySensor = humSensor;
// Устанавливаем пороговые значения
_warningThresholdLow = 0;
_warningThresholdHigh = 40; // Опасно высокий тепловой индекс
// Рассчитываем начальное значение
UpdateValue();
}
public override float CurrentValue => _lastCalculatedValue;
public void UpdateValue()
{
float t = _temperatureSensor.CurrentValue;
float h = _humiditySensor.CurrentValue;
// Формула для расчета теплового индекса
_lastCalculatedValue = CalculateHeatIndex(t, h);
// Проверяем, не превышены ли пороговые значения
CheckWarningThresholds(_lastCalculatedValue);
}
// Не требуется обработка данных напрямую, так как этот датчик виртуальный
public override bool TryProcessData(string data)
{
return false;
}
private float CalculateHeatIndex(float temperature, float humidity)
{
// Упрощенная формула расчета теплового индекса
if (temperature < 27)
return temperature; // При низкой температуре индекс равен температуре
// Коэффициенты формулы
float c1 = -8.784695f;
float c2 = 1.61139411f;
float c3 = 2.338549f;
float c4 = -0.14611605f;
float c5 = -0.01230809f;
float c6 = -0.01642482f;
float c7 = 0.00221173f;
float c8 = 0.00072546f;
float c9 = -0.00000358f;
return c1 + c2 * temperature + c3 * humidity +
c4 * temperature * humidity + c5 * temperature * temperature +
c6 * humidity * humidity + c7 * temperature * temperature * humidity +
c8 * temperature * humidity * humidity +
c9 * temperature * temperature * humidity * humidity;
}
} |
|
Нужен перевод кода с С# Windows Forms в C++ Windows Forms Нужно конвертировать(перевести код) в С++ Windows Forms
using System;
using... Конверт Virtual-Key Codes в System.Windows.Forms.Keys и System.Windows.Forms.Keys в Virtual-Key Codes Есть Virtual-Key Codes https://docs.microsoft.com/ru-ru/windows/desktop/inputdev/virtual-key-codes... Нужен таймер. Не удаётся преобразовать преобразовать из "System.Windows.Forms.Timer" в "System.Windows.Forms.Control" private static void TimeLable(Form current)
{
int i;
int tk;
... Запуск консольных команд (CMD) в окне Windows Forms Добрый день!
В есть bat файл, который я хочу запустить в приложении Windows Forms.
На самом... Временно отменить добавление команд в очередь команд диалогового окна Добрый день, уважаемые программисты!
Начнём по порядку...
Я пишу оконное приложение на WinApi,... Изучение работы команд пересылки mov и арифметических команд процессора add и sub Реализовать вычисление указанных формул в виде ассемблерных вставок в текст программы на С++.... C++ отправка команд на ардуино через com порт посредством библиотеки windows.h Добрый день!
Возникла проблема, с которой сам разобраться не могу.
Хотел сделать консольное... Windows Forms и Arduino Uno Добрый вечер!
Имеется приложение Windows Forms и простая программа на Arduino. Суть программы... Код для Windows Forms не работает в Web Forms? В том году я делал лабораторки по Winforms. Естественно, они все у меня сохранились, и я полез в их... Будут ли работать приложения написанные на Windows Forms на Windows 8 Будут ли работать приложения написанные на Windows Forms на Windows 8?
И собсно еще вопрос, вы... Создание приложения Windows Forms на C++/CLI в Windows 8 Добрый день! Нужно создать windows приложение на Си++, стоит 8, на 2013 много заморочек с созданием... Перенос приложения Windows Forms в Windows Market Как с минимальными затратами времени перенести десктопное приложение на магазин Windows ? Есть ли...
|