Привет, Хабр!
В предыдущей своей статье я упомянул о реализации устройства, которое разрабатывалось для облегчения процесса настройки датчиков обслуживающим персоналом, а прикрепленный опрос показал, что вам интересна тема реализации данного устройства.
Ну что ж, я ценю ваше мнение, поэтому данная статья будет посвящена реализации простого и недорого 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 пару классов для работы с устройством, результат мне очень понравился.
Ссылки к статье:
Автор: CyberexTech