Привет, Хабр!
В предыдущей своей статье я упомянул о реализации устройства, которое разрабатывалось для облегчения процесса настройки датчиков обслуживающим персоналом, а прикрепленный опрос показал, что вам интересна тема реализации данного устройства.
Ну что ж, я ценю ваше мнение, поэтому данная статья будет посвящена реализации простого и недорого AR решения для отображения параметров системы сбора данных. Если стало интересно, то добро пожаловать под кат!
❯ Начало
Некоторые элементы данной статьи будут пересекаться с контекстом предыдущей, поэтому убедительно прошу ознакомиться с ней, чтобы иметь полное понимание происходящего. Спасибо!
Итак, как я уже говорил ранее, рассматриваемое в статье устройство является частью «экосистемы» программного комплекса, который занимается сбором технологических данных предприятия. Ниже я попытаюсь описать конструкцию устройства и программную реализацию обмена данными между устройством и мобильным приложением.
❯ Корпус устройства
Корпус устройства не обладает каким-либо сложным конструктивом и выглядит следующим образом:
Чтобы сэкономить время и не мучиться с подгонкой оптической системы, я использовал в качестве базы готовое решение, в которое внес свои доработки. Благо, после презентации гарнитуры Google Glass в 2015 году, индийские товарищи «наплодили» DIY вариантов подобных корпусов. Ниже представлено более подробное описание элементов устройства:
Как вы можете видеть, оптическая система состоит из следующих элементов:
-
Проекционное стекло;
-
Фокусирующая линза;
-
Зеркало.
Проекционное стекло — один из важных элементов устройства, от которого зависит качество проекции. Ниже представлено фото используемого проекционного стекла:
В качестве проекционного стекла необходимо применять специализированные стекла с металлическим напылением, чтобы обеспечить эффективное отражение света проецируемого изображения. Чтобы сделать подобное стекло в домашних условиях, достаточно всего лишь расслоить DVD-диск, так как в компакт-дисках используется специальное напыление для эффективного отражения лазерного луча со считываемой поверхности. Наличие покрытия на стекле можно определить по металлическому отблеску при вращении стекла.
Фокусирующая линза — здесь все просто, данная линза необходима для формирования фокусного расстояния проецируемого изображения, чтобы правильно совместить картинку реальности и проекции. Данная линза взята из дешевого VR бокса и была подпилина под габариты выходного «окна».
Зеркало — здесь не все так просто, как показала практика, в качестве зеркала нельзя использовать обычное стеклянное зеркало (как использовали индусы). При использовании обычного зеркала наблюдается большое расслоение проекции из-за двойного отражения. Наилучшим решением является использование металлического зеркала, которое применяется в лазерных системах. Зачастую данные зеркала достаточно дорогие, но для DIY решения вполне подойдет металлическое зеркало изготовленное из алюминиевого «блина» HDD диска, что я и применил на практике.
Как вы можете видеть выше на изображении, на корпусе присутствуют небольшие заслоняющие элементы, которые выглядят как «гармошка». Данные элементы я внес в конструкцию для борьбы с эффектом ореола в проекции, так как свет исходящий от дисплея отражался от стенок корпуса.
❯ Электроника
Принципиальная схема устройства не сложная, в качестве «мозгов» я выбрал модуль ESP32, по большей мере из за наличия Bluetooth интерфейса, а в качестве дисплея был выбран недорогой 0,66 дюймовый OLED модуль с разрешением 64х48. Почему именно данный модуль? — он компактнее LCD и обладает большей яркостью пикселя. Ниже приведена принципиальная схема устройства:
Для обеспечения питания в данном прототипе, применялся Li-on аккумулятор емкостью 250 mAh, а в качестве модуля зарядки использовалась популярная плата на базе TP4056. Ниже вы можете видеть компоновку элементов электроники в корпусе устройства:
Вид спереди:
❯ Программное обеспечение
Функционал ПО обеспечивает обмен данными в формате JSON между смартфоном наладчика и AR монитором с помощью мобильного приложения системы сбора данных.
Микро ПО AR монитора разрабатывалось в среде Arduino IDE и не отличается какой-то сложностью. Ниже представлен код устройства:
Main
#include <BLEDevice.h> #include <BLEServer.h> #include <BLEUtils.h> #include <BLE2902.h> #include <ArduinoJson.h> #include <Wire.h> #include "SSD1306Wire.h" // Использую русифицированную библиотеку дисплея SSD1306Wire display(0x3c, 4, 5); #define batPin 34 #define DURATION 10000 int battery = 0; float bat = 0; int count = 0; float data = 0; char *unit = ""; char *leg = ""; long timeSinceLastModeSwitch = 0; BLEServer *pServer = NULL; BLECharacteristic *pCharacteristic = NULL; bool deviceConnected = false; uint8_t txValue = 0; #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" void send_json(String json){ dsjson(json); } class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; } void onDisconnect(BLEServer* pServer) { deviceConnected = false; } }; class MyCharacteristicCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string rxValue = pCharacteristic->getValue(); if (!rxValue.empty()) { send_json(rxValue.c_str()); // Обработка полученных данных } } }; void setup() { display.init(); display.flipScreenVertically(); display.setFont(ArialMT_Plain_10); BLEDevice::init("AR Monitor"); pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); BLEService *pService = pServer->createService(BLEUUID(SERVICE_UUID)); pCharacteristic = pService->createCharacteristic( BLEUUID(CHARACTERISTIC_UUID), BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE ); pCharacteristic->addDescriptor(new BLE2902()); pCharacteristic->setCallbacks(new MyCharacteristicCallbacks()); pService->start(); BLEAdvertising *pAdvertising = pServer->getAdvertising(); pAdvertising->start(); } void loop() { process(); }
DataProcess
void process(){ if (millis() - timeSinceLastModeSwitch > DURATION) { int anbat = map(analogRead(batPin), 0, 4095, 0, 420); bat = anbat*0.01; int bata = bat*100; battery = map(bata, 257, 419, 0, 100); timeSinceLastModeSwitch = millis(); } if (!deviceConnected) { display_text(18,"УСТРОЙСТВО ГОТОВО К ПОДКЛЮЧЕНИЮ","CYBRX","tech", battery); }else{ display_text(18, leg, String(data), unit, battery); } delay(10); } void dsjson(String json){ StaticJsonDocument<200> doc; deserializeJson(doc, json); leg = doc["legend"]; // Имя отображаемого параметра data = doc["data"]; // Значение параметра unit = doc["unit"]; // Единица измерения }
DisplayProcess
void display_text(int posY, String texts, String data_bt, String unit_bt, int bat_2){ int co = texts.length()*6; int point; int positionLine; if(co > 120){ count++; point = co - count; positionLine = point; if(count > co+60){ count = 0; } }else { positionLine = 64; } display.clear(); display.setTextAlignment(TEXT_ALIGN_CENTER); display.setFont(Font5x7); display.drawString(positionLine, posY, texts); display.setFont(ArialMT_Plain_16); display.drawString(64, 32, data_bt); display.setFont(Font5x7); display.setTextAlignment(TEXT_ALIGN_RIGHT); display.drawString(96, 56, unit_bt); if(bat != 0){ display.drawProgressBar(32, 56, 20, 6, bat_2); } display.display(); }
➤ Обмен в мобильном приложении
В приложении реализована следующая логика: Пользователю нет необходимости в ручном добавлении AR устройства для отображения данных, в приложении реализован поиск и автоматическое подключение AR монитора. Данная функция реализована в следующем классе:
Класс поиска BLE устройства
public class BleScanner { private BluetoothAdapter bluetoothAdapter; private BluetoothLeScanner bluetoothLeScanner; private boolean scanning; private ScanCallback scanCallback; private Handler handler; private ScanResultListener scanResultListener; public BleScanner() { bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); scanning = false; handler = new Handler(Looper.getMainLooper()); setupScanCallback(); } public void setScanResultListener(ScanResultListener listener) { this.scanResultListener = listener; } private void setupScanCallback() { scanCallback = new ScanCallback() { @SuppressLint("MissingPermission") @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); BluetoothDevice device = result.getDevice(); if (device.getName() != null) { // Пропускаем отправку в слушетель устройства не с именем AR Monitor String dev; try { dev = convert_to_utf_8(device.getName()); // Проверяем на кириллицу } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } if (dev.equals("AR Monitor")) { // Если нашли наше устройство, то отправляем его в слушатель scanResultListener.onDeviceFound(device); stopScan(); } } } @Override public void onBatchScanResults(List<ScanResult> results) { super.onBatchScanResults(results); // Handle batch scan results if needed } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); // Handle scan failure scanResultListener.onScanFailed(errorCode); } }; } @SuppressLint("MissingPermission") public void startScan() { if (!scanning && bluetoothLeScanner != null) { scanning = true; bluetoothLeScanner.startScan(scanCallback); handler.postDelayed(this::stopScan, 10000); // Останавливаем сканирование после 10 сек } } @SuppressLint("MissingPermission") public void stopScan() { if (scanning && bluetoothLeScanner != null) { scanning = false; bluetoothLeScanner.stopScan(scanCallback); } } public interface ScanResultListener { void onDeviceFound(BluetoothDevice device); void onScanFailed(int errorCode); } private String convert_to_utf_8(String data) throws UnsupportedEncodingException { String return_data = ""; if(data !=null) { byte[] ptext = data.getBytes(getEncoding(data)); return_data = new String(ptext, StandardCharsets.UTF_8);; } return return_data; } public static String getEncoding(String str) { String encode = "GB2312"; try { if (str.equals(new String(str.getBytes(encode), encode))) { return encode; } } catch (Exception ignored) {} encode = "ISO-8859-1"; try { if (str.equals(new String(str.getBytes(encode), encode))) { return encode; } } catch (Exception ignored) {} encode = "UTF-8"; try { if (str.equals(new String(str.getBytes(encode), encode))) { return encode; } } catch (Exception ignored) {} encode = "GBK"; try { if (str.equals(new String(str.getBytes(encode), encode))) { return encode; } } catch (Exception ignored) {} return ""; } }
Поиск устройства запускается с помощью метода startScan() и, в случае наличия нашего AR монитора поблизости, возвращает MAC адрес нашего устройства для инициализации подключения. Далее полученный MAC адрес сохраняется в памяти приложения. Для работы с BLE подключением, реализован следующий класс:
Класс управления BLE подключением
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; public BLEManager(Context context) { this.context = context; BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager != null) { bluetoothAdapter = bluetoothManager.getAdapter(); } } @SuppressLint("MissingPermission") public void connectToDevice() { if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { return; } // Адрес ESP32 String DEVICE_ADDRESS = new MySharedPreferences(context).getString("VrMAC", "00:00:00:00:00"); if(!Objects.equals(DEVICE_ADDRESS, "00:00:00:00:00")) { BluetoothDevice device = bluetoothAdapter.getRemoteDevice(DEVICE_ADDRESS); bluetoothGatt = device.connectGatt(context, false, gattCallback); } } private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { @SuppressLint("MissingPermission") @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); if (newState == BluetoothGatt.STATE_CONNECTED) { connected = true; gatt.discoverServices(); } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { connected = false; } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); if (status == BluetoothGatt.GATT_SUCCESS) { BluetoothGattService service = gatt.getService(SERVICE_UUID); characteristic = service.getCharacteristic(CHARACTERISTIC_UUID); } } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); if (CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { byte[] data = characteristic.getValue(); String dataStr = new String(data); // Здесь можно обработать полученные данные } } }; @SuppressLint("MissingPermission") public void sendData(String data) { if (bluetoothGatt != null && characteristic != null) { characteristic.setValue(data.getBytes()); bluetoothGatt.writeCharacteristic(characteristic); } } @SuppressLint("MissingPermission") public void disconnect() { if (bluetoothGatt != null) { bluetoothGatt.disconnect(); bluetoothGatt.close(); } } public boolean isConnected() { return connected; } }
Для подключения к устройству используется метод connectToDevice(), а для передачи данных на устройство используется метод sendData(), где в качестве аргумента передается строка в формате JSON. Ниже представлена функция передачи для данных на устройство:
Функция передачи данных на AR монитор
private void sendToAr(String legend, float data, String unit){ if(bleManager.isConnected) { JSONObject json = new JSONObject(); json.put("legend", legend); json.put("data", data); json.put("unit", unit); bleManager.sendData(json.toString()); // Отправка JSON по BLE } }
Данная функция реализована в Foreground Service в котором выполняется циклический запрос требуемого параметра из системы сбора данных, а полученные данные передаются в устройство с помощью выше описанной функции. Активация Foreground сервиса в приложении выполняется с помощью элемента «переключатель» «Трансляция данных в AR устройство».
❯ Итоги
В данной статье я постарался упрощенно рассказать как реализовано данное устройство и программное обеспечение для его работы. Как вы можете видеть, устройство не обладает какими-то сложными решениями и вполне доступно для реализации. Ниже представлен список затрат на реализацию аппаратной части:
-
Микроконтроллер ESP-32S — $2,26;
-
Дисплейный модуль SSD1306 — $2,06;
-
Аккумулятор Li-po 250mAh — $2,04;
-
Модуль заряда TP4056 — $1,12 (за 5 шт);
-
Остальные компоненты — $1;
Итоговая стоимость компонентов: ~ $7,6.
Спасибо всем, кто нашел время для прочтения данной статьи и если у вас возникли вопросы, то добро пожаловать в комментарии! Всем добра, успехов и интересных проектов!
Испытание первого прототипа в 2020 году
PS: Данное решение не обошло стороной и моё хобби: я давно катаюсь на моноколесе и решил применить данный AR монитор для отображения телеметрии, предварительно добавив в приложение WheelLog пару классов для работы с устройством, результат мне очень понравился.
Ссылки к статье:
-
➤ Как я делал систему сбора данных на провинциальном заводе и что из этого вышло;
-
➤ DIY на заводе или как сделать измерительный комплекс для оценки износа опорных роликов;
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
ссылка на оригинал статьи https://habr.com/ru/articles/852388/
Добавить комментарий