Привет, Хабр!
В предыдущей статье я уже описывал свой опыт реализации программного (и немного аппаратного) комплекса «Системы сбора технологических данных» в рамках промышленного предприятия. А в этой статье я хочу поделиться опытом реализации DIY-проекта для нужд завода, речь пойдет об измерительном комплексе для оценки износа опорных роликов. Если стало интересно, то добро пожаловать под кат!
❚ Начало
Однажды, в обычный скучный будничный рабочий день, ко мне в кабинет заглянул в гости коллега из соседнего подразделения, который работал инженером-механиком. Недолго ходя «вокруг да около», он предложил рассмотреть вопрос о разработке устройства для измерения диаметра роликовых опор. На мой вопрос: «А почему не купить готовое решение?», он ответил: «Ты видел, сколько оно стоит?». После обсуждения возможной концепции устройства, я задал вопрос: «Компания готова оплатить реализацию данного устройства в плане материалов, аппаратной и программной разработки?» Ответ был утвердительным, все комплектующие покупает компания, а оплата разработчику будет выплачена в виде премии. Ок, сказал я, давай попробуем реализовать.
Для общего понимания, ниже на изображении вы можете увидеть, что из себя представляет опорный ролик:
❚ Концепция устройства
По сути, для оценки износа поверхности опорного ролика достаточно инструмента под названием «Мерное колесо», с помощью которого можно измерить длину окружности ролика и рассчитать его диаметр. Сравнивая полученное измерение с прошлыми замерами, мы можем определить степень деградации поверхности ролика. Другими словами, мне предстояло разработать более современный аналог «Мерного колеса» с некоторыми «плюшками» цифрового мира.
В процессе обсуждения концепции, «заказчиком» было высказано несколько пожеланий, будем их называть «техническим заданием»:
- Устройство должно иметь автономное питание;
- Устройство должно иметь функцию автоматического замера по датчику;
- Устройство должно иметь возможность сохранения замеров;
- И от себя я добавил функцию экспорта журнала замеров в файл;
- Устройство должно иметь удобный интерфейс.
Что касается последнего пункта, то в качестве «удобного интерфейса» я решил разработать мобильное приложение, не хотелось мне возиться с кнопочками и экраном на физическом устройстве.
❚ Ключевой элемент устройства
В качестве ключевого элемента системы будет выступать измерительный энкодер. Для текущего проекта был выбран энкодер от компании Autonics марки E50S8-5000-6-L-5. Инкрементальный энкодер E50S8-5000-6-L-5 точно замеряет угол перемещения объекта и преобразует его в электрический импульс. Он имеет сплошной вал диаметром 8 мм. В основе работы датчика лежит инкрементальный принцип. Электрическое соединение выполняется через фланцевый разъем, тип — осевое. Данный энкодер имеет разрешение 5000 шагов на оборот, чего вполне достаточно для наших целей. Энкодер имеет три выходных сигнала TTL: фазы A, B и Z, а напряжение питания составляет 5В.
Внешний вид энкодера:
Энкодер с выходами A, B и Z — это инкрементальный энкодер с тремя основными сигналами, каждый из которых выполняет свою функцию:
A (канал A) – основной инкрементальный выходной сигнал. Это импульсы, которые генерируются при вращении вала энкодера. Каждый импульс соответствует определённому углу поворота. С помощью этого сигнала можно отслеживать количество шагов, пройденных энкодером, что позволяет определить угол или положение вращения.
B (канал B) – второй инкрементальный выход, сдвинутый по фазе относительно канала A. Благодаря этому фазовому сдвигу можно определить направление вращения. Если сигнал A опережает сигнал B, это одно направление вращения, а если B опережает A — противоположное направление. Это позволяет системе различать, вращается ли вал энкодера по часовой или против часовой стрелки.
Z (канал Z, или индексный импульс) – это сигнал, который появляется только один раз за полный оборот энкодера. Этот импульс служит для точной синхронизации или калибровки системы. Он часто используется для того, чтобы определить начальную точку отсчёта или «нулевое» положение, откуда начинается отслеживание вращения.
Таким образом, сигналы A и B позволяют отслеживать положение и направление вращения, а Z используется для сброса или синхронизации на определённой позиции.
Ниже представлены формы сигналов выходных фаз энкодера:
И реальная осциллограмма выходных сигналов:
Ниже приведены габаритные характеристики энкодера:
❚ Крепление энкодера
Для удобного выполнения процедуры измерения, предстояла задача разработки крепления энкодера с возможностью подстройки положения при жесткой фиксации платформы. Немного подумав, я пришел к следующему варианту конструкции.
Модель фиксирующей конструкции энкодера:
Данная модель разрабатывалась в САПР FreeCAD, а далее элементы конструкции распечатывались на рабочем 3D принтере.
Как можно видеть ниже на фото, приводная конструкция энкодера реализована с применением трех шкивов, которые соединены осью и двумя приводными ремнями (типа пасик), которые были изготовлены из полиуританового филамента для 3D принтера. А для лучшего сцепления измерительного колеса с измеряемой поверхностью, были предусмотрены уплотнительные кольца из того же полиуританового филамента.
Фиксирующая конструкции энкодера после сборки:
❚ Контроллер энкодера
Для обработки сигналов энкодера, как ни крути, необходим контроллер. В качестве микроконтроллера для данного модуля я выбрал ESP32, прежде всего из-за наличия встроенного модуля Bluetooth. И так как в планах реализовать питание от встроенного аккумулятора, то на плате контроллера реализован повышающий преобразователь на 5В, в котором применен ШИМ контроллер MAX1771. Как можно догадаться, на плате контроллера реализована не сложная схема, ознакомиться с которой вы можете ниже.
Принципиальная схема контроллера:
Трассировка дорожек печатной платы:
Модель печатной платы:
Далее было заказано изготовление плат на одном популярном китайском сервисе и, после недолгого ожидания и получения заказа, был выполнен монтаж компонентов и небольшая отладка повышающего преобразователя.
Сборка печатной платы:
❚ Корпус блока электроники
Также как и крепление энкодера, модель корпуса разрабатывалась в САПР FreeCAD. В корпусе был предусмотрен отсек для размещения аккумулятора и модуля зарядки для Li-ion аккумулятора на базе контроллера TP4056.
Модель корпуса блока электроники:
Установка электроники в корпус:
Kорпус блока электроники после печати и сборки:
Для отображения индикатора процесса зарядки, был распечатан небольшой элемент из белого пластика и вставлен в корпус.
Kорпус блока электроники в сборе:
Выше на фото вы можете увидеть два светодиодных индикатора: красный светодиод отвечает за индикацию срабатывания датчика оборотов, а синий светодиод отвечает за индикацию BLE подключения к устройству. Мигание светодиода означает готовность к подключению или отсутствие подключения, а постоянное свечение сигнализирует об успешном подключении.
Ниже на изображении вы можете видеть датчик оборотов, в качестве которого используется геркон, а для активации датчика используется небольшой неодимовый магнит, который крепится на измеряемый валик. Данный датчик используется в функции автоматического замера длины окружности валика опоры.
Датчик для автоматического режима:
❚ Программное обеспечение
Микро ПО контроллера
Прошивка контроллера разрабатывалась в среде Arduino IDE и не содержит какой-либо сложной логики. Наша задача — обработать сигналы энкодера, выполнить несложные вычисления и отдать результат в формате JSON по BLE-каналу в мобильное приложение.
На этапе разработки, когда у меня еще не было в наличии энкодера, мне пришлось написать его эмулятор и использовать его в процессе отладки. Эмулятор запускался на дешевой плате Arduino NANO и подключался к отладочной плате с микроконтроллером ESP32. Ниже представлен код эмулятора сигналов энкодера:
// Пины для имитации выходов энкодера
#define pinA 8
#define pinB 9
#define pinZ 10
#define pinR 11 // Канал для реверса направления
const int pulsesPerRevolution = 5000; // Количество импульсов на один оборот (например, 5000 импульсов)
int pulseCount = 0; // Счётчик импульсов для канала Z
void setup() {
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);
pinMode(pinZ, OUTPUT);
digitalWrite(pinA, LOW);
digitalWrite(pinB, LOW);
digitalWrite(pinZ, LOW);
}
void loop() {
generateEncoderSignals(); // Генерация импульсов на каналах A и B
if (pulseCount >= pulsesPerRevolution) {
pulseCount = 0; // Сброс после полного оборота
generateZSignal(); // Генерация сигнала Z
}
delay(55); // Настроить для регулировки скорости
}
void generateEncoderSignals() {
int delays = 55;
if (digitalRead(pinR)) { // Вращение по часовой стрелке (A опережает B)
digitalWrite(pinA, HIGH);
delay(delays); // Половина периода сигнала
digitalWrite(pinB, HIGH);
delay(delays);
digitalWrite(pinA, LOW);
delay(delays);
digitalWrite(pinB, LOW);
} else { // Вращение против часовой стрелки (B опережает A)
digitalWrite(pinB, HIGH);
delay(delays); // Половина периода сигнала
digitalWrite(pinA, HIGH);
delay(delays);
digitalWrite(pinB, LOW);
delay(delays);
digitalWrite(pinA, LOW);
}
pulseCount++; // Увеличение счётчика импульсов
}
void generateZSignal() { // Имитация индекса (Z) — один импульс на оборот
digitalWrite(pinZ, HIGH);
delay(10);
digitalWrite(pinZ, LOW);
}
Что касается самого контроллера, обработка сигналов энкодера выполняется с помощью аппаратного прерывания:
attachInterrupt(encoder0PinA, doEncoderA, CHANGE); // Обработка внешнего прерывания фазы А
attachInterrupt(encoder0PinZ, doEncoderZ, RISING); // Обработка внешнего прерывания фазы Z
Ниже представлена функция обработки прерывания на фазе А:
void IRAM_ATTR doEncoderA() { // Обработчик прерывания для канала A
if(auto_measuring && auto_start or !auto_measuring){
if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) { // Определяем направление вращения по состоянию канала B
direction = -1; // Вращение против часовой стрелки
} else {
direction = 1; // Вращение по часовой стрелке
}
encoder0Pos += direction;
}
}
Функция обработки прерывания на фазе Z:
void IRAM_ATTR doEncoderZ(){ // Обработчик прерывания для канала Z
if(auto_measuring && auto_start or !auto_measuring){
if(digitalRead(encoder0PinZ)){
encoder0Rotations++;
}
}
}
Как видите, здесь ничего сложного. Полный код прошивки контроллера я приложу в конце статьи.
Мобильное приложение
Моя самая любимая часть. Несмотря на то, что код мобильного приложения будет посерьезнее микро ПО, вся логика вычислений реализована исключительно в контроллере энкодера. В данном случае, приложение выступает в роли пользовательского терминала и выполняет обмен с устройством по BLE соединению. Ниже представлен класс, в котором содержатся методы работы с BLE соединением.
package vgc.cyberex.encoder_app;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.UUID;
import vgc.cyberex.encoder_app.service.DataCallback;
public class BLEManager {
private static final UUID SERVICE_UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
private static final UUID CHARACTERISTIC_UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361b26a8");
private final Context context;
private BluetoothAdapter bluetoothAdapter;
private BluetoothGatt bluetoothGatt;
private BluetoothGattCharacteristic characteristic;
private boolean connected;
private boolean presend;
private final DataCallback callback;
public BLEManager(Context context, DataCallback callback) {
this.context = context;
this.callback = callback;
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager != null) {
bluetoothAdapter = bluetoothManager.getAdapter();
}
}
public void connectToDevice(String mac) {
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
return;
}
if (!Objects.equals(mac, "00:00:00:00:00")) {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(mac);
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
bluetoothGatt = device.connectGatt(context, false, gattCallback);
}
}
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothGatt.STATE_CONNECTED) {
connected = true;
callback.connectStatusBT(connected);
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
gatt.discoverServices();
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
connected = false;
callback.connectStatusBT(connected);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
BluetoothGattService service = gatt.getService(SERVICE_UUID);
if (service != null) {
characteristic = service.getCharacteristic(CHARACTERISTIC_UUID);
if (characteristic != null) {
//readCharacteristic(characteristic);
//setCharacteristicNotification(characteristic,true);
process_time(characteristic);
}
}
}
}
@SuppressLint("MissingPermission")
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
byte[] data = characteristic.getValue();
String dataString = new String(data, StandardCharsets.UTF_8);
// Обработка данных
try {
callback.onRData(dataString);
} catch (Exception e) {
// throw new RuntimeException(e);
}
// Log.d("BLEDataRead", "Received: " + dataString);
}
}
private void readCharacteristic(BluetoothGattCharacteristic characteristic) {
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
bluetoothGatt.readCharacteristic(characteristic);
}
};
private void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
descriptor.setValue(enabled ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
}
@SuppressLint("MissingPermission")
public void sendData(String data) {
//setCharacteristicNotification(characteristic,false);
if (bluetoothGatt != null && characteristic != null) {
characteristic.setValue(data.getBytes());
bluetoothGatt.writeCharacteristic(characteristic);
this.presend = false;
process_time(characteristic);
}
}
@SuppressLint("MissingPermission")
public void disconnect() {
if (bluetoothGatt != null) {
bluetoothGatt.disconnect();
bluetoothGatt.close();
}
}
public boolean isConnected() {
return connected;
}
public void precends(boolean presends, String data ){
this.presend = presends;
long time = System.currentTimeMillis();
while (System.currentTimeMillis() - time < 800 ){}
sendData(data);
}
private void process_time(BluetoothGattCharacteristic characteristics){
@SuppressLint("MissingPermission") Runnable runnable = () -> {
long time = System.currentTimeMillis();
while (!presend){
if(System.currentTimeMillis() - time >= 1 ){
bluetoothGatt.readCharacteristic(characteristics);
time = System.currentTimeMillis();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
Подключение к устройству выполняется с помощью метода connectToDevice(), в качестве аргумента в данном методе передается mac адрес устройства. Обмен данными с устройством начинается после подключения и реализуется с помощью функции process_time(), где запускается процесс чтения характеристики устройства, а метод onCharacteristicRead() возвращает прочитанные данные, и с помощью метода callback.onRData() отправляет их в широковещательную рассылку. Для отправки данных в контроллер энкодера, используется метод sendData(), куда в качестве аргумента передается строка в формате JSON. Да, код это, наверное, скучно, поэтому давайте перейдем к описанию интерфейса и функционала приложения.
Ниже представлены скриншоты экранов мобильного приложения.
Скриншоты экранов мобильного приложения:
Системное уведомление:
Функционал мобильного приложения
- Одна из основных функций — это отображение данных контроллера энкодера и рассчитываемых им параметров.
- Функция автоматического замера, точнее, функция управления автоматическим замером, так как сам автоматический замер реализован в прошивке контроллера энкодера. Автоматический замер активируется с помощью одноименного переключателя, и в процессе активации данные измерения сбрасываются. В режиме автоматического замера запуск измерения начинается по сигналу датчика и останавливается при повторной активации датчика. Данные замера фиксируются, и последующий автоматический замер можно будет выполнить только при повторной активации функции. О завершении автоматического замера сообщит всплывающее окно. Индикатором данного режима является зеленая иконка с буквой «A», а индикатором активации датчика служит иконка двойной красной стрелки.
- Функция сохранения замера: данная функция активируется при нажатии на кнопку «Сохранить замер». Далее, данные текущего замера сохраняются во внутренний архив приложения. При активации данной функции необходимо указать описание замера в окне ввода.
- Журнал замеров – данная функция активируется по нажатию одноименной кнопки. Журнал замеров представляет собой отдельный экран приложения, где отображен список сохраненных измерений. Каждый элемент списка может быть удален при долгом нажатии на элемент списка.
- Сброс замера — данная функция активируется по нажатию кнопки «Сбросить замер», при активации данной функции, выполняется сброс внутренних счетчиков контроллера энкодера и рассчитываемых параметров.
- Функция ввода коэффициента шага энкодера — данная функция активируется при долгом нажатии на значение текущего коэффициента шага. При активации данной функции, открывается форма ввода, где необходимо указать новый коэффициент шага.
- Функция экспорта архива замеров в файл формата CSV — данная функция активируется при нажатии на синюю иконку со стрелкой вниз в верхнем правом углу на экране архива измерений. После удачного экспорта файла, пользователю будет выведено сообщение с указанием пути сохранения и именем экспортного файла.
- Функция отображения данных энкодера в режиме блокировки смартфона — данная функция позволяет наблюдать значения энкодера в режиме блокировки в виде системного уведомления, так же данное уведомление может быть отображено (в режиме блокировки смартфона) на подключенных смарт-часах пользователя.
Что касается коэффициента шага, здесь я хотел бы добавить небольшое пояснение:
Коэффициент шага энкодера — это соотношение между количеством импульсов (или шагов) на один оборот энкодера и расстоянием, которое проходит измерительное колесо, но так как у нас реализована система передачи, то необходимо учитывать и передаточный коэффициент. Ниже представлен пример расчета коэффициента шага для нашей конструкции:
- Приводной шкив энкодера (Dэ): ∅ 50 мм.
- Шкив на оси (Do): ∅ 15 мм.
- Измерительное колесо (Dи): ∅ 50 мм.
- Количество импульсов на оборот (PPR): 5000.
Расчет:
1. Передаточное отношение между приводного шкива энкодера и шкивом на оси: Передаточное отношение ременной передачи можно рассчитать как отношение диаметров двух шкивов:
iрп = Dо / Dэ = 15 / 50 = 3,333
Это означает, что один полный оборот приводного шкива энкодера приводит к 3,333 оборотам шкива на оси.
2. Обороты измерительного колеса: Поскольку шкив на оси и измерительное колесо жестко связаны через ось (без передаточного механизма), они будут вращаться с одинаковой скоростью. То есть за 3,333 оборота шкива на оси измерительное колесо тоже сделает 3,333 оборота.
3. Окружность измерительного колеса: Для измерительного колеса с ∅ 50 мм окружность вычисляется так:
Cи = π × Dи = 3,1416 × 50 мм = 157,08 мм
Cи — это длина, которую пройдет измерительное колесо за один оборот.
4. Путь, пройденный измерительным колесом за один оборот приводного шкива: Так как за один оборот приводного шкива измерительное колесо делает 3,333 оборота, общее расстояние, которое проходит измерительное колесо за один оборот приводного шкива, равно:
Pопш = iрп × Cи = 3,333 × 157,08 = 523,547 мм
5. Коэффициент шага будет равен:
Kш = Pопш / PPR = 523,547 / 5000 = 0,1047 мм*имп
Ниже изображен пример просмотра экспортного файла CSV с архивом измерений в приложении Numbers.
Просмотр таблицы измерений:
❚ Итоги
Не смотря на то, что я так и не получил* обещанную премию за данный проект, это был достаточно интересный опыт реализации DIY проекта в условиях предприятия. Надеюсь статья вас не утомила. Большое спасибо за Ваше внимание! Всем добра и интересных проектов!
* — по заводской традиции, обещанного три года ждут, а я покинул компанию через год.
➤ Ссылки к статье:
Автор: CyberexTech