- PVSM.RU - https://www.pvsm.ru -
Привет, Хабр!
В предыдущей статье [1] я уже описывал свой опыт реализации программного (и немного аппаратного) комплекса «Системы сбора технологических данных» в рамках промышленного предприятия. А в этой статье я хочу поделиться опытом реализации 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. Да, код это, наверное, скучно, поэтому давайте перейдем к описанию интерфейса и функционала приложения.
Ниже представлены скриншоты экранов мобильного приложения.
Скриншоты экранов мобильного приложения:
Системное уведомление:
Что касается коэффициента шага, здесь я хотел бы добавить небольшое пояснение:
Коэффициент шага энкодера — это соотношение между количеством импульсов (или шагов) на один оборот энкодера и расстоянием, которое проходит измерительное колесо, но так как у нас реализована система передачи, то необходимо учитывать и передаточный коэффициент. Ниже представлен пример расчета коэффициента шага для нашей конструкции:
Расчет:
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
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android-development/399543
Ссылки в тексте:
[1] предыдущей статье: https://habr.com/ru/articles/844552/
[2] Исходники микро ПО измерительного контроллера: https://github.com/VGCH/encoder/tree/main/encoder2
[3] 3D модели корпуса и крепления: https://github.com/VGCH/encoder/tree/main/3D_models
[4] Проект печатной платы измерительного контроллера: https://easyeda.com/editor#project_id=895f3cdfb29d4ca4a76959076661df18
[5] Мобильное приложение: https://github.com/VGCH/encoder/tree/main/android_app
[6] Источник: https://habr.com/ru/articles/849558/?utm_campaign=849558&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.