Датчик расстояния в умном доме

в 14:11, , рубрики: автоматизация, датчик расстояния

Для чего применить

Одна из задач при проектировании «автоматизированного» дома — получить информацию о том что человек где‑то появился. Казалось бы — есть датчики движения (PIR‑сенсоры)? Да, есть, они простые и дешевые. И для задачи «включить свет в коридоре когда там кто‑то появился» подходят. Но вот если чуть усложнить задачу, добавив в нее домашних животных — то становится гораздо интересней. Идеально иметь возможность отличать животных и людей.

Довольно часто вижу жаркие холивары про способы определить «присутствие». То есть пока человек двигается — PIR сенсор будет движение фиксировать. Но стоит замереть... Особенно актуально для туалета.

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

Вот так выглядит собранное. Экран тут не используется. Хотя можно.

Вот так выглядит собранное. Экран тут не используется. Хотя можно.

Почему DIY?

Не нашел подходящих с требуемым функционалом:

  • Работа по RS-485 (Modbus) или по Ethernet (MQTT)

  • Значение в виде “количества движения” а не бинарного есть/нет

  • Возможность анализа как по интенсивности так и зоне движения

Ну и просто интересно.

Выбор комплектующих

Микроконтроллером — пусть будет Ардуинка. Да, попсово — но почему бы нет?

Работать будет по RS-485, используя Modbus RTU. Просто потому что эта шина — дешевле и удобнее.

WI‑FI использовать не хочу, Ethernet — требует отдельного порта на коммутаторе (ну, это ладно) и отдельного UTP к каждому устройству. Ну и избыточен.

Значит нужен трансмиттер для 485. У китайцев полно:

Пример трансмиттера отдельной платкой.

Пример трансмиттера отдельной платкой.

Главное чтобы без «автоопределения передачи» всякого — оно нормально работает только в узком диапазоне скоростей. Ну и часть стартового импульса может «откусить». Мой выбор — честный DI и DE для управления.

Ну и сам датчик. Использую VL53L1X c али за $4. А на чипдипе — цена больше 2К Наверно «настоящие»!

Оффтопик: Если б кто‑нибудь мне рассказал лет н‑цадьт назед что появятся TOF сенсоры в таком размере — ну, посмеялся бы.

Параметры датчика по документации:

Угол обзора

Угол обзора

Датчик способен вернуть расстояние до каждой группы 4×4 чувствительных элементов. Да, можно и до каждого, хоть в даташите не рекомендуют. Но тут бюджет времени сильно растет.

Использовать буду 16 зон, получив матрицу 4×4 расстояний.
При выделенном на одно измерение бюджете в 50мс обновление всех займет ~0,8с, обычно меньше.

Если расположить датчик на потолке на высоте 2,5м получится что на уровне пола квадрат ~1200 мм. Одна зона получается 300×300мм.

Схема

Когда развожу платки для разных прототипов - стараюсь (если место есть) добавить например i2c разъем. Ну и всякое-разное, типа i2c расширителя с выводами на PLS.
Вот такую платку использую:

Схема

Схема

Цепочка R13-R14 образует делитель напряжения, чтобы можно было измерить величину питающего. J4, для шин 1-wire и полевичок управления их питанием тоже в этом проекте не используются.

dc-dc совершенно типовой, D2 можно вообще выкинуть.

Питание

Питание

Можно собрать и на макетке.
Подключаем трансмиттер к ардуинке, DI -> TX, DO -> RX, RE и DE вместе - к "D13".

Ну и датчик расстояния - к i2c.

Вот так выглядит

В сборе

В сборе

Не стал снимать то что установлено в потолке (оно страшное, собрано на скорую руку), да и датчик там вклеен. Так что собрал еще один экземпляр.

Код

Я не настоящий программист, поэтому использовал для работы с Modbus и с VL53L1X библиотечки. Особо радует Modbus. В версию 4.1.0 заехала пользовательская обработка фреймов, так что можно замахнуться и на Быстрый Modbus(c) WirenBoard.

/*
 * 
  WB some standard registers add
  v1.1
    Тут добавлены coil 0..5 включающие gpio. Перечислены ниже как "Выходы для Coil 00-05"  
  v1.2
    Добавил регистры внешнего напряжения и температуру МК
  v1.3
    Добавил регистры дальномера
  v1.4
    Добавил регистры настройки чтения ROI
*/

//Для дальномера
#include <VL53L1X.h>
VL53L1X rangeSensor; //Объект дальномера

//Для работы с EEPROM
#include <EEPROM.h>

//https://github.com/emelianov/modbus-esp8266
#include <ModbusRTU.h>

//Объект md
ModbusRTU mb;

#define RXTX_PIN 13 //GPIO, 1 - передача по RS-485. 0 - прием
#define LD4 4 //LED on board
#define LD5 5 //LED on board
#define LD6 6 //LED on board

#define V_in A7 //Input voltage divider (10+91)/(91/10)=11,099

#define OW_POWER 10 //OUTPUT +5V enable. 0-enable, 1-disable
#define OW_1 A0 //1Wire 1
#define OW_2 A1 //1Wire 2




//#define LED_BUILTIN 13

//Адреса в EEPROM для хранения регистров
#define h80eeprom 0 //Modbus address
#define h6Eeeprom 1 //110 speed port
#define h6Feeprom 2 //111 parity bit
#define h70eeprom 3 //112 stop bit

#define h800eeprom 10 //800 holding - 16 штук. Если 1 -то 



//Выходы для Coil 00-05
#define Coil00  A4
#define Coil01  A3
#define Coil02  A2
#define Coil03  LD4
#define Coil04  LD5
#define Coil05  LD6

//Массив для хранения выходов, связанных с coil
static uint8_t coilOutList[] = {Coil00, Coil01, Coil02, Coil03, Coil04, Coil05};

//тут описываем прототипы функций. Чтобы при создании структуры уже были.
void h80setup(void);
uint16_t h80set(TRegister* reg, uint16_t val);

void h6Esetup(void); //скорость.
uint16_t h6Eset(TRegister* reg, uint16_t val);

void h6Fsetup(void); //четность
uint16_t h6Fset(TRegister* reg, uint16_t val);

void h70setup(void); // стопбиты
uint16_t h70set(TRegister* reg, uint16_t val);

void i68setup(void); // время работы
uint16_t i68set(TRegister* reg, uint16_t val);

void i79setup(void); // Текущее напряжение питания. Также надо запускать из таймера преобразование, оно занимает ~100мкС - ждать нет смысла.
void i7csetup(void); // Температура МК

//User function
void c00setup(void); // coil relay output
uint16_t c00set(TRegister* reg, uint16_t val);
uint16_t c00get(TRegister* reg, uint16_t val);

void hc8setup(void); // просто регистр
//void h111setup(void);
//void h112setup(void);
void rangeRegSetup(void); // Регистры для дальномера
void rangeRegSetupReading(void); // Регистры для дальномера


typedef void (* funcPtr) (); 
// Теперь создадим массив длиной три 
// и сложем в него указатели на функции.
// Массив имеет тип, который мы только что создали
//FuncPtr funcArray[2] = {h80setup, pf104};

//Все эти функции будут вызваны при запуске
//Сюда дописываем и свои тоже
const funcPtr funcArr[] = {
  h80setup, //Modbus address
  h6Esetup,
  h6Fsetup,
  h70setup,
  i68setup,
  i79setup,
  i7csetup,
  //User function
  c00setup,
  hc8setup,
  rangeRegSetup, //Регистры для дальности:
  rangeRegSetupReading //регистры для насстройки чтения ROI
  };

byte modbusNeedStart = 0; //флаг необходимости перезапуска Modbus

//volatile uint8_t ADCDest[]={121, 7,  124};
volatile uint16_t regForValueADC=0;
volatile uint8_t valTimer;
volatile uint8_t rangeSensorFlag = 1; //Флаг для сенсора расстояния
volatile uint8_t rangeSensorCurrentROI = 0; //Текущий ROI сенсора
const uint8_t rangeSensorROIs[] = {10, 42, 74, 82, 106,    14, 46, 78, 110,      245, 213, 181, 149, 241, 209, 177, 145};    


// the setup function runs once when you press reset or power the board
void setup() {

  //Включаю подтяжку для RX. Без нее почему-то плохо работает трансмиттер. 2do: поправить на следующей версии платы
  digitalWrite(0, 1);


  Serial.begin(9600);
  //Serial.println("Start!");
  
  Wire.begin(); //i2c запускаем тут
  Wire.setClock(400000); // use 400 kHz I2C

  //переключаем GPIO в выходы 2do - в цикле надо
    for (byte i=0; i < (sizeof(coilOutList)/sizeof(coilOutList[0])); i++){
    pinMode(coilOutList[i], OUTPUT);
  }
  
  //Считаем количество элементов массива.
  //тут в цикле пройдем по всем элементам funcArr и запустим функцию.
  for (byte i=0; i < (sizeof(funcArr)/sizeof(funcArr[0])); i++){
    //Serial.print(i);
    //Serial.println("start");
    delay(40);
    funcArr[i]();
    //Serial.println("startED");
  }


  //Настройка канала A таймера 0. сам таймер не трогаем.
  OCR0A = 128; //Устанавливаем регистр совпадения
  TIMSK0 |= (1 << OCIE0A);  // включение прерываний по совпадению для 0 таймера, канал A
}


void loop() {
  if (modbusNeedStart){
    modbusStart();//Запускаем Modbus
    modbusNeedStart = 0;
    //Serial.print("mb.Hreg(128) "); Serial.println(mb.Hreg(128));
  }
  mb.task();

  //Проверяем, если измерение прошло и данные есть - то читаем их.
  if ((1==rangeSensorFlag) && rangeSensor.dataReady()){
    //Serial.println("dataReady");
    rangeSensor.read();
    rangeSensorFlag = 0;
    //digitalWrite(LD4, 0);
    mb.Ireg(0x800 + rangeSensorCurrentROI, rangeSensor.ranging_data.range_mm); //Значение - в регистр
    //digitalWrite(LD6, 0);
  }

  if (rangeSensorFlag == 0){
    rangeSensorCurrentROI++;
    if(rangeSensorCurrentROI >15){rangeSensorCurrentROI=0;}
    if(mb.Hreg(0x800+rangeSensorCurrentROI) == 1){ //Если этот ROI включен - запускаем для него измерение. Если нет - пропускаем.
      rangeSensorFlag = 1;
      rangeSensor.setROICenter(rangeSensorROIs[rangeSensorCurrentROI]); //Установим центр ROI.
      rangeSensor.readSingle(false); //Запустим не блокирующее (без ожидания) измерение
    }
  }

  delayMicroseconds(10);     // Ну ардуинка ж. Как тут без задержек? ;)

}





// А вот и описания функций.
//
//Адрес 128 **************
void h80setup(){
  //Надо прочитать из EEPROM байт.
  byte readByte = eeRead(h80eeprom, 1);
  if (0 == readByte || 247<readByte){//Если прочитанное равно нулю или больше 247 (новая плата)
    eeWrite(h80eeprom, 1, 1); //Записываем в EEprom 1
    readByte = 1;
  };
  //Serial.print("address=");Serial.println(readByte);
  mb.addHreg(128); //Создаем holding регистр с адресом 128
  mb.Hreg(128, readByte);//Устанавливаем значение
  mb.onSetHreg(128, h80set); // Add callback on Hreg 128 value set
  modbusNeedStart=1;//нужно перезапустить Modbus
}
uint16_t h80set(TRegister* reg, uint16_t val){
  //Serial.println("enter h80set");
  //Надо прочитать из EEPROM байт.
  byte readByte = eeRead(h80eeprom, 1);
  if (0!=val || 248>val){//Если прочитанное НЕ равно новому, не ноль и в диапазоне адресов
    eeWrite(h80eeprom, 1, val); //Записываем в EEprom 1
    modbusNeedStart=1;//нужно перезапустить Modbus
    return val;
  }
  else{
    return readByte; //Если значение неверно - просто оставляем старое
  }
}


void h6Esetup(){//Скорость 110 **************
  //Serial.println("enter h6Esetup");
  //Надо прочитать из EEPROM байт.
  byte readByte = eeRead(h6Eeeprom, 1);
  //Serial.print("readByte");Serial.println(readByte);
  mb.addHreg(110); //Создаем holding регистр с адресом 110
  uint16_t speedReg = 0;
  switch(readByte){
    case 1:
      speedReg = 12;
      break;
    case 2:
      speedReg = 24;
      break;
    case 4:
      speedReg = 48;
      break;
    case 9:
      speedReg = 96;
      break;
    case 19:
      speedReg = 192;
      break;
    case 57:
      speedReg = 576;
      break;
    case 115:
      speedReg = 1152;
      break;
  }
  if (0 == speedReg){//Если прочитанное ни с чем не совпало
    eeWrite(h6Eeeprom, 1, 9); //Записываем в EEprom 9 (9600)
    speedReg = 96;
  }
  mb.Hreg(110, speedReg);//Устанавливаем значение
  mb.onSetHreg(110, h6Eset); // Add callback on Hreg 128 value set
  modbusNeedStart=1;//нужно перезапустить Modbus
}
uint16_t h6Eset(TRegister* reg, uint16_t val){
  //Serial.println("enter h6Eset");
  uint8_t toeeprom = 0;
  //Serial.print("val=");Serial.println(val);
  switch(val){
    case 12:
      toeeprom = 1;
      break;
    case 24:
      toeeprom = 2;
      break;
    case 48:
      toeeprom = 4;
      break;
    case 96:
      toeeprom = 9;
      break;
    case 192:
      toeeprom = 19;
      break;
    case 576:
      toeeprom = 57;
      break;
    case 1152:
      toeeprom = 115;
      break;
  }
  if (0 == toeeprom){//Если записанное не совпало со списком скоростей - то ой.
    eeWrite(h6Eeeprom, 1, toeeprom); //Записываем в EEprom 1
    modbusNeedStart=1;//нужно перезапустить Modbus
    return val;
  }
  else{
    return mb.Hreg(110); //Если значение неверно - просто оставляем старое
  }
}


void h6Fsetup(){//Четность 111 **************
  //Serial.println("enter h6Esetup (111)");
  //Надо прочитать из EEPROM байт.
  byte readByte = eeRead(h6Feeprom, 1);
  //Serial.print("readByte h6Feeprom");Serial.println(readByte);
  //Serial.print("h6Feeprom=");Serial.println(h6Feeprom);
  //Serial.print("readByte 0 ");Serial.println(eeRead(0, 1));
  //Serial.print("readByte 1 ");Serial.println(eeRead(1, 1));
  //Serial.print("readByte 2 ");Serial.println(eeRead(2, 1));
  //Serial.print("readByte 3 ");Serial.println(eeRead(3, 1));
  
  mb.addHreg(111); //Создаем holding регистр с адресом 111
  uint8_t temp = 4;
  switch(readByte){
    case 1:
      temp = 0;
      break;
    case 2:
      temp = 1;
      break;
    case 3:
      temp = 2;
      break;
  }
  if (4 == temp){//Если прочитанное ни с чем не совпало
    eeWrite(h6Feeprom, 1, 0); //Записываем в EEprom 0 (нет бита чётности (none))
    temp = 0;
  }
   /* 0 — нет бита чётности (none),
    1 — нечетный (odd),
    2 — четный (even) */
  //Serial.print("mb.Hreg(111, temp) ");Serial.println(temp);
  mb.Hreg(111, temp);//Устанавливаем значение четности
  mb.onSetHreg(111, h6Fset); // Add callback on Hreg 111 value set
  modbusNeedStart=1;//нужно перезапустить Modbus
}

uint16_t h6Fset(TRegister* reg, uint16_t val){
  //Serial.println("enter h6Fset (111)");
  uint8_t toeeprom = 0;
  switch(val){
    case 0:
      toeeprom = 1;
      break;
    case 1:
      toeeprom = 2;
      break;
    case 2:
      toeeprom = 3;
      break;
  }
  if (0!=toeeprom){//Если записанное не совпало со списком четностей
    eeWrite(h6Feeprom, 1, toeeprom); //Записываем в EEprom 1
    modbusNeedStart=1;//нужно перезапустить Modbus
    return val;
  }
  else{
    return mb.Hreg(111); //Если значение неверно - просто оставляем старое
  }
}

void h70setup(){
  //Serial.println("enter h70setup (112)");
  //Надо прочитать из EEPROM байт.
  byte readByte = eeRead(h70eeprom, 1);
  //Serial.print("readByte");Serial.println(readByte);
  mb.addHreg(112); //Создаем holding регистр с адресом 112
  uint8_t temp = 0;
  switch(readByte){
    case 1:
      temp = 1;
      break;
    case 2:
      temp = 2;
      break;
  }
  if (0 == temp){//Если прочитанное ни с чем не совпало
    eeWrite(h70eeprom, 1, 2); //Записываем в EEprom 9 (9600)
    temp = 2;
  }
  /*1 — 1 стопбит
    2 — 2 стопбит*/
  mb.Hreg(112, temp);//Устанавливаем значение четности
  mb.onSetHreg(112, h70set); // Add callback on Hreg 112 value set
  modbusNeedStart=1;//нужно перезапустить Modbus
}

uint16_t h70set(TRegister* reg, uint16_t val){
  //Serial.println("enter h70set (112)");
  uint8_t toeeprom = 0;
  //Serial.print("val=");Serial.println(val);
  switch(val){
    case 1:
      toeeprom = 1;
      break;
    case 2:
      toeeprom = 2;
      break;
  }
  if (0 != toeeprom){//Если записанное не совпало со списком четностей
    eeWrite(h70eeprom, 1, toeeprom); //Записываем в EEprom 1
    modbusNeedStart=1;//нужно перезапустить Modbus
    return val;
  }
  else{
    return mb.Hreg(112); //Если значение неверно - просто оставляем старое
  }
}


void i68setup(){
  //Serial.println("enter i68setup (104)");
  mb.addIreg(104, 0, 2); //Создаем input регистрЫ с адресом 104-105 записывая в них "0"
  mb.onGetIreg(104, i68get, 2);  // Add single callback for multiple Inputs. It will be called for each of these inputs value get
}

uint16_t i68get(TRegister* reg, uint16_t val){
  //Serial.println("enter h68get (104)");
  //Serial.print(reg->address.address);
  //
  //mb.Ireg(104, TCCR0A);//Устанавливаем значение счетчика. Только надо делать это не тут.
  //так, надо взять ulong значение millis(), разделить его на 1000.
  uint32_t tempSecunds = millis()/1000;
  //Получим ulong секунд. Старшую часть записываем в 104 а младшую в 105
  //просто берем указатель16-разрядный на старшую часть, для начала.
  uint16_t* ptrTemp = (uint16_t*)&tempSecunds;
  if(reg->address.address == 104)
    //return OCR0A;
    return *(ptrTemp+1);
  if(reg->address.address == 105)
    return *(ptrTemp);
  return 256;
}

void i79setup(){//Напряжение питания модуля (внешнее, через делители)
  //Serial.println("enter i79setup (121)");
  mb.addIreg(121, 0, 1); //Создаем input регистр с адресом 121 записывая в него "0" 
  ADCSRA = 0;             // Сбрасываем регистр ADCSRA
  ADCSRB = 0;             // Сбрасываем регистр ADCSRB
  ADCSRA = (1<<ADEN) | (0<<ADIF)  | (0<<ADATE); //Запуск ADC, сброс флага ADIF, отключение автоматического запуска (ADATE)
  //ADMUX |= (1<<REFS0) | (0<<ADLAR) | (0<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0);//Установка ref as vcc, Вход - 7
  ADCSRA |= (1<<ADIE) | (1<<ADPS2) | (0<<ADPS1) | (1<<ADPS0);//Включение прерывания ADC (ADIE) & частота/64
  //DIDR0 = (0<<ADC0D); 
  sei();
  //digitalWrite(LD5, 1);
}

void i7csetup(){//Температура МК
  //Serial.println("enter i7c_setup (124)");
  mb.addIreg(124, 0, 1); //Создаем input регистр с адресом 124 записывая в него "0" 
}

ISR(ADC_vect){ //Прерывания ADC
  // Если нужны все 10 бит (полная 10-битная точность), как и установлено ADLAR=0: значение типа uint16 = ADCL | (ADCH << 8)
  if (ADMUX == ((1<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (0<<MUX2) | (0<<MUX1) | (0<<MUX0)) ){//проверка - что читали
    mb.Ireg(124, ADCL | (ADCH << 8));//Устанавливаем значение вольта на единицу АЦП
    //digitalWrite(LD4, 0);
  }
  if (ADMUX == ((1<<REFS0) | (0<<ADLAR) | (0<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0)) ){//проверка - что читали
    mb.Ireg(121, (ADCL | (ADCH << 8))*0.482); //Устанавливаем значение   //10,90 ->226. 0,0482 вольта на единицу АЦП
    //digitalWrite(LD5, 0);
    //digitalWrite(LD6, 1);
  }
}

ISR(TIMER0_COMPA_vect){
  valTimer++;
  if (valTimer>20){ //запускаем раз в 20мс
    valTimer = 0;
    //digitalWrite(13, !digitalRead(13));
  }
  if (valTimer==1){ //подготовим преобразование температуры MUX[3:0]	1111
    ADMUX =  (1<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (0<<MUX2) | (0<<MUX1) | (0<<MUX0);// Вход - температурный
  }

  if (valTimer==6){ //запустим преобразование температуры 
    ADCSRA |= (1<<ADSC);//Запуск преобразования
  }

  if (valTimer==8){ //подготовим преобразование напряжения MUX[3:0]	0111, 7 канал
    ADMUX =  (1<<REFS0) | (0<<ADLAR) | (0<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0);//Установка ref as vcc, Вход - Напряжение
  }
  
  if (valTimer==14){ //запустим преобразование напряжения
    ADCSRA |= (1<<ADSC);//Запуск преобразования
  }
  
  /**
  if ((valTimer==17) && (rangeSensorFlag==0)){ //запустим измерение расстояния если флаг опущен
    digitalWrite(LD4, 1);
    rangeSensorFlag = 1; //Поднимем флаг
    //rangeSensor.readSingle(false); //Запустим не блокирующее (без ожидания) измерение
  }
  */
}


void modbusStart(){
  //Serial.begin(9600, SERIAL_8N2); //четность и стопбиты - менять!
  //mb.begin(&Serial, RXTX_PIN); //запускаем на указанном порту Modbus
  
  uint32_t speedReg = 0;
  switch(mb.Hreg(110)){
    case 12:
      speedReg = 1200;
      break;
    case 24:
      speedReg = 2400;
      break;
    case 48:
      speedReg = 4800;
      break;
    case 96:
      speedReg = 9600;
      break;
    case 192:
      speedReg = 19200;
      break;
    case 576:
      speedReg = 57600;
      break;
    case 1152:
      speedReg = 115200;
      break;
  }

  // SERIAL_8N2 - это дефайн, смотреть можно на 
  // https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/HardwareSerial.h
  // с 68 строки
  // SERIAL_8N1 0x06
  // SERIAL_8N2 0x0E
  // SERIAL_8E1 0x26
  // SERIAL_8E2 0x2E
  // SERIAL_8O1 0x36
  // SERIAL_8O2 0x3E
  // видно что получается суммой четности 
  // N - 0x00
  // E - 0x20
  // O - 0x30
  // и стопбитов
  // 1 - 0x06
  // 2 - 0x0E
  uint8_t serialParam=0;
  switch(mb.Hreg(111)){
    case 0:
      serialParam = 0x0;
      break;
    case 1:
      serialParam = 0x30;
      break;
    case 2:
      serialParam = 0x20;
      break;
  }
    switch(mb.Hreg(112)){
    case 1:
      serialParam += 0x06;
      break;
    case 2:
      serialParam += 0x0E;
      break;
  }
  
  Serial.begin(speedReg, serialParam); //четность и стопбиты - менять!
  //Serial.begin(speedReg, SERIAL_8N2);
  mb.begin(&Serial, RXTX_PIN); //запускаем на указанном порту Modbus
  
  //Serial.print("Modbus speed ");Serial.println(mb.Hreg(110));
  mb.setBaudrate(speedReg);// Это не скорость порта, это для задержек, смотри в исходник библиотеки.
  mb.slave(mb.Hreg(128));
  //mb.slave(1); // for debug
  //Serial.print("Modbus speed ");Serial.print(mb.Hreg(110));Serial.print(" speed ");Serial.println(speedReg);
  //Serial.print("mb.Hreg(111) parity:");Serial.println(mb.Hreg(111));
  //Serial.print("mb.Hreg(112) stopbit");Serial.println(mb.Hreg(112));
  //Serial.print("Modbus serial parameter ");Serial.println(serialParam);
  //Serial.print("Modbus parameter SERIAL_8N2:");Serial.println(SERIAL_8N2);
  //Serial.print("Modbus started with address ");Serial.println(mb.Hreg(128));
}


uint32_t eeRead (int addr, byte lenght){
  //Возвращает считанное из EEPROM значение
  //Первый (младший) байт читается с addr, количество считывемых байт [1..2] задается в lenght
  uint32_t retVal = 0;
  byte *ptrb = (byte*)&retVal; //Указатель приведен к byte, чтобы обращаться к байтам int отдельно
    for (uint8_t i = 0; i<lenght; i++){
      *(ptrb+i) = EEPROM.read(addr);
    }
  return *ptrb;
}

void eeWrite (uint32_t addr, byte lenght, uint32_t val){
  //Пишет в EEPROM значение
  //Первый (младший) байт пишется в addr, количество записываемых байт [1..2] задается в lenght
  byte *ptrb = (byte*)&val; //Указатель приведен к byte, чтобы обращаться к байтам int отдельно
  for (byte i = 0; i<lenght; i++){
    EEPROM.update(addr, *(ptrb+i));
  }
}


//User register:

void c00setup(){
  //Serial.println("enter c00setup (00)");
  //
  mb.addCoil(0, 0, 6); //Создаем coil регистрЫ с адресом 00-05 записывая в них "0"
  mb.onGetCoil(0, c00get, 6);  // Add single callback for multiple coils. It will be called for each of these coils value get
  mb.onSetCoil(0, c00set, 6);  // Add single callback for multiple coils. It will be called for each of these coils value SET
  //modbusNeedStart=1;//нужно перезапустить Modbus
  //Serial.println("eXIT c00setup (00)");
}

uint16_t c00get(TRegister* reg, uint16_t val){
  //if(reg->address.address == 0)
  //  return COIL_VAL(1);
  //  //return *(ptrTemp+1);
  //if(reg->address.address == 1)
  //  //return *(ptrTemp);
  //  return COIL_VAL(1);
  return val;
}

uint16_t c00set(TRegister* reg, uint16_t val) {
  //Serial.print("enter c00set (00)");
  //Serial.print(reg->address.address);
  //Serial.println(COIL_BOOL(val));
  //mb.Hreg(0, mb.Hreg(0)+1);
  digitalWrite(coilOutList[reg->address.address], COIL_BOOL(val));
  //mb.Hreg(1)+=1;
  return val;
}

void hc8setup(){
  //Serial.println("enter hс8setup (200)");
  mb.addHreg(200); //Создаем holding регистр с адресом 200
  mb.addHreg(0, 0, 6);//Создаем 6 holding регистров с адреса 0
}

void rangeRegSetup(){
  mb.addHreg(0x7ff); //Создаем holding регистр с адресом 2047
  mb.addIreg(0x800, 4096, 16);//Создаем 16 input регистров с адреса 0x800
  delay(1);
  rangeSensor.setTimeout(500);  
  if (!rangeSensor.init()) {
    mb.Hreg(0x7ff, 3);
    //Serial.println("rangeRegSetup init error");
  } else {
    mb.Hreg(0x7ff, 2); //Примем значение "2" как успешную инициализацию.
    rangeSensor.setROISize(4, 4); //Задаем ROI, 4 по ширине и 4 по высоте
    //Serial.println("rangeRegSetup init good");
    //delay(150);
    rangeSensorFlag = 0; //Скидываем флаг, для того чтобы пошело процесс измерения
  }
}

void rangeRegSetupReading(){
  mb.addHreg(0x800, 0, 16);//Создаем 16 holding регистров с адреса 0x800
  for (uint8_t i=0; i<16; i++){//Читаем из EEPROM значения
    uint8_t readByte = eeRead(h800eeprom+i, 1);
    if (255==readByte){//Если прочитанное равно нулю или равно (новая плата)
      eeWrite(h800eeprom+i, 1, 1); //Записываем в EEprom 1
      readByte = 1;
    };
    mb.Hreg(0x800+i, readByte);
  }
  mb.onSetHreg(0x800, h800set, 16);
}

uint16_t h800set(TRegister* reg, uint16_t val){
  //Serial.println("enter h800set");
  uint8_t toeeprom = 0;
  if (0 == val || 1 == val){//Если записанное 1  или 0
    //Serial.print("reg->address.address");
    //Serial.println(reg->address.address);
    eeWrite(h800eeprom + reg->address.address - 0x800, 1, val); //Записываем в EEprom
    return val;
  }
  else{
    return mb.Hreg(reg->address.address); //Если значение неверно - просто оставляем старое
  }
}

Вот таблица регистров

Адрес

Тип

Значение

0x80

holding

Modbus ID 1-247 По-умолчанию 1. Хранится в EEPROM

0x6e

holding

RS-485 скорость. Значение скорости деленное на 100. То есть 9600->96, 19200->192, .. 115200->1152 Хранится в EEPROM

0x6f

holding

RS-485 Четность 0-none 1-odd 2-even Хранится в EEPROM

0x70

holding

RS-485 Стопбиты 1 или 2 Хранится в EEPROM

0x82

holding

Выключение стстусного светодиода, не реализовано.

0

coil

Управление выходом A4

1

coil

Управление выходом A3

2

coil

Управление выходом A2

3

coil

Управление выходом D4

4

coil

Управление выходом D5

5

coil

Управление выходом D6

0-5

holding

Просто отладочные

0x7ff

holding

Статус инициализации сенсора. 2 - удачно 3 - неудачно

0x79

input

Напряжение питания с коэффициентом 10 то есть 195 - 19,5В

0x7с

input

Температура микроконтроллера (надо отлаживать, что-то даташит невнятен)

0x68, 0x69

input

Uptime в секундах. 0x68 - старшая часть.

0x800-0x80f

holding

Регистры конфигурации опроса ROI Если 1 - ROI опрашивается. 0 - нет. Хранится в EEPROM

0x800-0x80f

input

Значения дальности в миллиметрах для ROI

вот так выглядит:

Интеграция

Теперь - самое интересное. У нас есть устройство которое отдает значения. Как их получить и применить для решения насущных задач?
Да, мне нравится Home Assistant, но реализация в нем Modbus RTU - ну, весьма-весьма так себе.
Поэтому опрашивать устройство будет контроллер Wiren Board. У меня версии 8.5 - но это по большему счету влияет только на быстродействие и объем памяти. Вот работу с Modbus в его шататном ПО можно настроить как угодно, до тонкостей.

То есть план такой:

  • контроллер опрашивает "дальномер", результаты публикуются в MQTT

  • Результаты используются и в самом контроллере и в HA

Что ж, открывем документацию, вполне годную и подробную, с примерами и пишем шаблон

{
    "title": "rangeSensor_title",
    "device_type": "rangeSensor_title",
    "group": "g-diy",
    "device": {
        "name": "rangeSensor_title",
        "id": "RangeSensor",
        "min_read_registers": 1,
        "max_read_registers": 10,
        "max_reg_hole": 10,
        "max_bit_hole": 10,
        "response_timeout_ms": 60,
        "frame_timeout_ms": 10,
        "device_max_fail_cycles": 5,
        "guard_interval_us": 500,
        "groups": [
            {
                "title": "General",
                "id": "general"
            },
            {
                "title": "ROI poll setup",
                "id": "ROI_poll"
            },
            {
                "title": "HW Info",
                "id": "g_hw_info"
            },
            {
                "title": "Debug",
                "id": "debug"
            }
        ],

        "parameters": {
            "baud_rate": {
                "title": "Baud rate",
                "description": "baud_rate_description",
                "address": 110,
                "reg_type": "holding",
                "enum": [96, 192, 384, 576, 1152],
                "default": 96,
                "enum_titles": [
                    "9600",
                    "19200",
                    "38400",
                    "57600",
                    "115200"
                ],
                "group": "general",
                "order": 1
            },
            "disable_indication": {
                "title": "Status LED",
                "address": 130,
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 0,
                "group": "general",
                "order": 3
            },
             
            "distance_0_0": {
                "title": "distance_0_0",
                "address": "0x800",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 1
            },
            "distance_0_1": {
                "title": "distance_0_1",
                "address": "0x801",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 2
            },
            "distance_0_2": {
                "title": "distance_0_2",
                "address": "0x802",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 3
            },
            "distance_0_3": {
                "title": "distance_0_3",
                "address": "0x803",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 4
            },
             
            "distance_1_0": {
                "title": "distance_1_0",
                "address": "0x804",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 5
            },
            "distance_1_1": {
                "title": "distance_1_1",
                "address": "0x805",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 6
            },
            "distance_1_2": {
                "title": "distance_1_2",
                "address": "0x806",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 7
            },
            "distance_1_3": {
                "title": "distance_1_3",
                "address": "0x807",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 8
            },
             
            "distance_2_0": {
                "title": "distance_2_0",
                "address": "0x808",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 9
            },
            "distance_2_1": {
                "title": "distance_2_1",
                "address": "0x809",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 10
            },
            "distance_2_2": {
                "title": "distance_2_2",
                "address": "0x80a",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 11
            },
            "distance_2_3": {
                "title": "distance_2_3",
                "address": "0x80b",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 12
            },
             
            "distance_3_0": {
                "title": "distance_3_0",
                "address": "0x80c",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 13
            },
            "distance_3_1": {
                "title": "distance_3_1",
                "address": "0x80d",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 14
            },
            "distance_3_2": {
                "title": "distance_3_2",
                "address": "0x80e",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 15
            },
            "distance_3_3": {
                "title": "distance_3_3",
                "address": "0x80f",
                "reg_type": "holding",
                "enum": [0, 1],
                "enum_titles": ["Enabled", "Disabled"],
                "default": 1,
                "group": "ROI_poll",
                "order": 16
            }






        },

        "channels": [
            {
                "name": "testreg",
                "reg_type": "holding",
                "address": 200,
                "scale": 1,
                "max": 255,
                "type": "value",
                "format": "u16",
                "group": "general"
            },

            {
                "name": "testreg0",
                "reg_type": "holding",
                "address": "0",
                "scale": 1,
                "max": 255,
                "type": "value",
                "format": "u16",
                "group": "general"
            },            

            {
                "name": "testreg1",
                "reg_type": "holding",
                "address": "1",
                "scale": 1,
                "max": 255,
                "type": "value",
                "format": "u16",
                "group": "general"
            },
            {
                "name": "testreg2",
                "reg_type": "holding",
                "address": "2",
                "scale": 1,
                "max": 255,
                "type": "value",
                "format": "u16",
                "group": "general"
            },
            {
                "name": "testreg3",
                "reg_type": "holding",
                "address": "3",
                "scale": 1,
                "max": 255,
                "type": "value",
                "format": "u16",
                "group": "general"
            },
            {
                "name": "testreg4",
                "reg_type": "holding",
                "address": "4",
                "scale": 1,
                "max": 255,
                "type": "value",
                "format": "u16",
                "group": "general"
            },


            {
                "name": "testreg5",
                "reg_type": "holding",
                "address": "5",
                "scale": 1,
                "type": "value",
                "format": "u16",
                "group": "general"
            },


            {
                "name": "sensorStatus",
                "reg_type": "holding",
                "enum": [ 1, 2, 3 ],
                "enum_titles": [ "zero", "work", "notwork" ],
                "readonly": false,
                "address": "0x7ff",
                "scale": 1,
                "max": 255,
                "format": "u16",
                "group": "general"
            },


            {
                "name": "Out0",
                "reg_type": "coil",
                "address": 0,
                "type": "switch",
                "group": "general"
            },
            {
                "name": "Out1",
                "reg_type": "coil",
                "address": 1,
                "type": "switch",
                "group": "general"
            },
            {
                "name": "Out2",
                "reg_type": "coil",
                "address": 2,
                "type": "switch",
                "group": "general"
            },
            {
                "name": "LD4",
                "reg_type": "coil",
                "address": 3,
                "type": "switch",
                "group": "general"
            },
            {
                "name": "LD5",
                "reg_type": "coil",
                "address": 4,
                "type": "switch",
                "group": "general"
            },
            {
                "name": "LD6",
                "reg_type": "coil",
                "address": 5,
                "type": "switch",
                "group": "general"
            },

            {
                "name": "distance-0-0",
                "reg_type": "input",
                "address": "0x800",
                "type": "value",
                "group": "general"
            },
            
            
            {
                "name": "distance-0-1",
                "reg_type": "input",
                "address": "0x801",
                "type": "value",
                "group": "general"
            },
            
                        {
                "name": "distance-0-2",
                "reg_type": "input",
                "address": "0x802",
                "type": "value",
                "group": "general"
            },
            
            
            {
                "name": "distance-0-3",
                "reg_type": "input",
                "address": "0x803",
                "type": "value",
                "group": "general"
            },
            
            
            {
                "name": "distance-1-0",
                "reg_type": "input",
                "address": "0x804",
                "type": "value",
                "group": "general"
            },
            {
                "name": "distance-1-1",
                "reg_type": "input",
                "address": "0x805",
                "type": "value",
                "group": "general"
            },
            
            {
                "name": "distance-1-2",
                "reg_type": "input",
                "address": "0x806",
                "type": "value",
                "group": "general"
            },
            {
                "name": "distance-1-3",
                "reg_type": "input",
                "address": "0x807",
                "type": "value",
                "group": "general"
            },
            

            
            {
                "name": "distance-2-0",
                "reg_type": "input",
                "address": "0x808",
                "type": "value",
                "group": "general"
            },
            {
                "name": "distance-2-1",
                "reg_type": "input",
                "address": "0x809",
                "type": "value",
                "group": "general"
            },
            
            {
                "name": "distance-2-2",
                "reg_type": "input",
                "address": "0x80a",
                "type": "value",
                "group": "general"
            },
            {
                "name": "distance-2-3",
                "reg_type": "input",
                "address": "0x80b",
                "type": "value",
                "group": "general"
            },
            
            
            
            {
                "name": "distance-3-0",
                "reg_type": "input",
                "address": "0x80c",
                "type": "value",
                "group": "general"
            },
            {
                "name": "distance-3-1",
                "reg_type": "input",
                "address": "0x80d",
                "type": "value",
                "group": "general"
            },
            
            {
                "name": "distance-3-2",
                "reg_type": "input",
                "address": "0x80e",
                "type": "value",
                "group": "general"
            },
            {
                "name": "distance-3-3",
                "reg_type": "input",
                "address": "0x80f",
                "type": "value",
                "group": "general"
            },
            
            
            
            

            {
                "name": "Supply Voltage",
                "reg_type": "input",
                "address": 121,
                "scale": 0.1,
                "type": "voltage",
                "readonly": true,
                "enabled": false,
                "group": "g_hw_info"
            },
            {
                "name": "MCU Temperature",
                "reg_type": "input",
                "address": 124,
                "type": "temperature",
                "format": "s16",
                "scale": 0.1,
                "enabled": false,
                "group": "g_hw_info"
            },
            {
                "name": "Uptime",
                "reg_type": "input",
                "address": 104,
                "type": "text",
                "format": "u32",
                "enabled": false,
                "group": "g_hw_info"
            }
        ],
        "translations": {
            "en": {
                "rangeSensor_title": "Modbus range array sensor",
                "Current": "Load current"
        },
            "ru": {
                "rangeSensor_title": "Дальномер",
                "General": "Общее",
                "HW Info": "Данные модуля",
                "Debug": "Диагностика",
                "ROI poll setup": "Включение опроса ROI",
                "no": "нет",
                "yes": "да",
                "Disabled": "Отключен",
                "testreg": "Тестовый",
                "Supply Voltage": "Напряжение питания",
                "zero": "Ноль",
                "work": "Работает",
                "notwork": "Не работает",
            }
        }
    }
}

Отправляем на контроллер шаблон и перезапускаем сервис

scp config-rangeSensor.json  root@main1:/etc/wb-mqtt-serial.conf.d/templates/ && ssh root@main1 systemctl restart wb-mqtt-serial

После создания устройства с шаблоном получаем результат

Топики в интерфейсе

Топики в интерфейсе

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

Но это уже весной как потеплеет.

Автор: garageman

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js