Темы



DIY AR монитор в промышленности

Привет,  Хабр!

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

Ну что ж, я ценю ваше мнение, поэтому данная статья будет посвящена реализации простого и недорого AR решения для отображения параметров системы сбора данных. Если стало интересно, то добро пожаловать под кат!

❯ Начало

Некоторые элементы данной статьи будут пересекаться с контекстом предыдущей, поэтому убедительно прошу ознакомиться с ней, чтобы иметь полное понимание происходящего. Спасибо!

Итак, как я уже говорил ранее, рассматриваемое в статье устройство является частью «экосистемы» программного комплекса, который занимается сбором технологических данных предприятия. Ниже я попытаюсь описать конструкцию устройства и программную реализацию обмена данными между устройством и мобильным приложением.

❯ Корпус устройства

Корпус устройства не обладает каким-либо сложным конструктивом и выглядит следующим образом:

3D модель корпуса AR монитора

3D модель корпуса AR монитора

Чтобы сэкономить время и не мучиться с подгонкой оптической системы, я использовал в качестве базы готовое решение, в которое внес свои доработки. Благо, после презентации гарнитуры Google Glass в 2015 году, индийские товарищи «наплодили» DIY вариантов подобных корпусов. Ниже представлено более подробное описание элементов устройства:

3D модель корпуса AR монитора

3D модель корпуса AR монитора

Как вы можете видеть, оптическая система состоит из следующих элементов:

  1. Проекционное стекло;

  2. Фокусирующая линза;

  3. Зеркало.

Проекционное стекло — один из важных элементов устройства, от которого зависит качество проекции. Ниже представлено фото используемого проекционного стекла:

Проекционное стекло

Проекционное стекло

В качестве проекционного стекла необходимо применять специализированные стекла с металлическим напылением, чтобы обеспечить эффективное отражение света проецируемого изображения. Чтобы сделать подобное стекло в домашних условиях, достаточно всего лишь расслоить DVD-диск, так как в компакт-дисках используется специальное напыление для эффективного отражения лазерного луча со считываемой поверхности. Наличие покрытия на стекле можно определить по металлическому отблеску при вращении стекла.

Фокусирующая линза — здесь все просто, данная линза необходима для формирования фокусного расстояния проецируемого изображения, чтобы правильно совместить картинку реальности и проекции. Данная линза взята из дешевого VR бокса и была подпилина под габариты выходного «окна».

Линза и зеркало

Линза и зеркало

Зеркало — здесь не все так просто, как показала практика, в качестве зеркала нельзя использовать обычное стеклянное зеркало (как использовали индусы). При использовании обычного зеркала наблюдается большое расслоение проекции из-за двойного отражения. Наилучшим решением является использование металлического зеркала, которое применяется в лазерных системах. Зачастую данные зеркала достаточно дорогие, но для DIY решения вполне подойдет металлическое зеркало изготовленное из алюминиевого «блина» HDD диска, что я и применил на практике.

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

❯ Электроника

Принципиальная схема устройства не сложная, в качестве «мозгов» я выбрал модуль ESP32, по большей мере из за наличия Bluetooth интерфейса, а в качестве дисплея был выбран недорогой 0,66 дюймовый OLED модуль с разрешением 64×48. Почему именно данный модуль? — он компактнее LCD и обладает большей яркостью пикселя. Ниже приведена принципиальная схема устройства:

Принципиальная схема  AR монитора

Принципиальная схема AR монитора

Для обеспечения питания в данном прототипе, применялся Li-on аккумулятор емкостью 250 mAh, а в качестве модуля зарядки использовалась популярная плата на базе TP4056. Ниже вы можете видеть компоновку элементов электроники в корпусе устройства:

Компоновка электроники

Компоновка электроники

Вид спереди:

Вид спереди

Вид спереди

❯ Программное обеспечение

Функционал ПО обеспечивает обмен данными в формате JSON между смартфоном наладчика и AR монитором с помощью мобильного приложения системы сбора данных.

Микро ПО AR монитора разрабатывалось в среде Arduino IDE и не отличается какой-то сложностью. Ниже представлен код устройства:

Main

#include 
#include 
#include 
#include 
#include 
#include  
#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 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 году

Испытание первого прототипа в 2020

Испытание первого прототипа в 2020

PS: Данное решение не обошло стороной и моё хобби: я давно катаюсь на моноколесе и решил применить данный AR монитор для отображения телеметрии, предварительно добавив в приложение WheelLog пару классов для работы с устройством, результат мне очень понравился.

Ссылки к статье:

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

Перейти ↩

© Habrahabr.ru