Для чего применить
Одна из задач при проектировании «автоматизированного» дома — получить информацию о том что человек где‑то появился. Казалось бы — есть датчики движения (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
Что ж, открывем документацию, вполне годную и подробную, с примерами и пишем шаблон
config-rangeSensor.json
{ "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
После создания устройства с шаблоном получаем результат

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