Dooya DT82 (Onviz MR-2234F)

Мотор для раздвижных штор Dooya DT82 (Onviz MR-2234F)
Подключен на отдельную линию RS485 кабелем 1м по инструкции поставщика.
Фото подключения:


Кнопки “открыть”, “закрыть”, “остановить” работают. Свитч “инверсия” в постоянном переключении, соответственно нажав “открыть”, “стоп”, потом снова “открыть” получим что мотор вращается в разные стороны:

В логах активно идут сообщения Serial protocol error: request timed out [slave_id is dooya:0x0101]:

Порт настроен по инструкции с вики:

Да, можно отключить опрос параметра “инверсия”, но это не избавит от ошибок в лог.
Остальные темы по этому мотору видел, но там или нет решения или дело в физическом подключении.
В чем может быть проблема и как её устранить?

Добрый день.
А указанный привод - точно имеет тот же (поддерживаемый) протокол?
Судя по симптомам - он не полностью совместим.
Насколько помню для onviz поддерживаются только рулонные привода: Использование рулонных штор Onviz с контроллером Wiren Board — Wiren Board

Onviz не является производителем моторов. Так же не в их силах написать прошивку для мотора. Это OEM с шильдиком. Да, я лишь предполагаю что внутри Dooya 82, потому как мотор, размещение портов, логика настройки совпадает, шаблон подходит.
В любом случае хочется заставить это работать

Для проверки - отправьте в привод команду вручную, например прочитайте статус.

Судя по поведению - протокол все же не соответствует, но, возможно у привода другие настройки связи. Можно попробовать перебором.

Не совсем понял про что вы.
мотором я могу управлять как из интерфейса wirenboard, так и через serial_tool отправкой команд:
image
image

Поставщиком была преоставлена карта регистров, если это может помочь вопросу:
Настройки_команд_протокола_RS485_для_2234F_и_2234FM_4.pdf (317,8 КБ)

Благодарю за документ.
В нем, например указано для статуса направления:
Screenshot_20240709_155957
Для Dooya Dt82 - тоже wb-mqtt-serial/templates/config-dooya82.json at a15fd59cf7847f63195a1d9c99c69c5f127e53c1 · wirenboard/wb-mqtt-serial · GitHub адрес третий
А вот позиция: в шаблоне dooya это нулевой адрес: wb-mqtt-serial/templates/config-dooya82.json at a15fd59cf7847f63195a1d9c99c69c5f127e53c1 · wirenboard/wb-mqtt-serial · GitHub
А в документе второй:
Screenshot_20240709_161229

Так что да, команды (одинаковые) в протоколах есть. Но далеко не все.
Рекомендую описать само устройство так: Шаблон для электрокарниза - #6 от пользователя BrainRoot отредактировав под используемые команды и статусы.

1 Like

Попробуем по этой инструкции. Не закрывайте тему, отпишусь о результатах для истории

  1. Попробовали еще раз адаптировать шаблоны от Dooya и Akko. Проблемы все те же: самопроизвольно включение инверсии, если включен её опрос и постоянные ошибки протокола в лог.
  2. Я правильно понял ваше предложение: адаптировать предложенный шаблон под мотор Onviz и загрузить этот скрипт в движок правил WB? Если так, то с этим пока не удалось разобраться. Попробуем позже
  3. В Вашей статье на хабре описано прямым текстом что моторы Onviz MR-2234F работают по протоколу dooya и нативный шаблон от Dooya 82 подходит. Это было одним из фактором принятия решения об использовании этих моторов в проекте.
  4. Три дня назад появилась тема с аналогичной проблемой - не корректно работает инверсия (горит красным) Некорректная работа моторов Onviz с шаблоном Dooya - #6 от пользователя BrainRoot

Делаю вывод что проблема не единичная, статья на хабре вводит в заблуждение. Возможно это новая партия моторов. Будут ли приняты какие-то меры с вашей стороны? Со своей стороны попробуем разобраться в предложенном коде, но именно в этом направлении пока пусто и не хватает знаний.

Возможно те приводы что использовались - поддерживают полностью протокол.
У нас в Устройства, протоколы и программы, с которыми может работать контроллер Wiren Board — Wiren Board приводы Onviz не перечислены - мы не тестировали их.

В том случае если будем включать эти приводы в поддержку - да, протестируем и проверим.

Поможете с редактированием?

//Путь к RPC
var pathRPC = "/rpc/v1/wb-mqtt-serial/port/Load/";

 
function createBlind(NameBlind, portBlind, speedPortBlind, pariryBlid, stopBitBlind, blindID, blindCh_L, blindCh_H){
  //Создаем виртуальное устройство
  makeNewVirtualControl(NameBlind, "position", {type: "range", value: 0, min: 0, max: 200, readonly: false});
  makeNewVirtualControl(NameBlind, "position_in", {type: "range", value: 0, min: 0, max: 200, readonly: false});
  // кнопка "Открыть" (работает)
  makeNewVirtualControl(NameBlind, "open", {type: "pushbutton", readonly: false});
  // кнопка "Стоп"(Работает)
  makeNewVirtualControl(NameBlind, "Stop", {type: "pushbutton", readonly: false});
  // кнопка "Закрыть" (работает)
  makeNewVirtualControl(NameBlind, "close", {type: "pushbutton", readonly: false});
  // Активность мотора (1 - вращается)
  makeNewVirtualControl(NameBlind, "MotorRun", {type: "switch", readonly: true});
  // временный для отладки
  makeNewVirtualControl(NameBlind, "reply", {type: "text", value: "", readonly: false});
  
  //open: 
  defineRule(NameBlind+"_rule_open",{
  whenChanged: NameBlind+"/open",
    then: function () {
      log.info(NameBlind+"_open")
      //Формируем запрос 0a dd 
      var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x01);
      log.info("open=", req)
      //Вызовем запрос
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 0 );
    }
  });
  //Stop: 
  defineRule(NameBlind+"_rule_Stop",{
  whenChanged: NameBlind+"/Stop",
    then: function () {
      //Формируем запрос 0a 0d 
      var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x03);
      log.info("req_Stop=", req)
      //Вызовем запрос, не ожидая ответ. На команды ответа нет.
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 0 );
    }
  });
  //close: 
  defineRule(NameBlind+"_rule_close",{
  whenChanged: NameBlind+"/close",
    then: function () {
      //Формируем запрос 0a ee 
      var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x02);
      //log.info("close=", req)
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 0 );
    }
  });
  //Сделаем таймер, который будет опрашивать позицию, возвращая ее в контрол.
  startTicker(NameBlind+"_timer", 6000);
  //И правило, работающее по срабатыванию таймера
  defineRule(NameBlind+"_rule",{
  when: function () { return timers[NameBlind+"_timer"].firing; },
    then: function () {
      var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x04);
      //log.info("req_timer "+NameBlind+"=", req)
      //Вызовем запрос, ожидая 9 байт в ответ
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 9 );
    }
  });
  trackMqtt(pathRPC+NameBlind+"/reply", function(message){
    log.info("from "+NameBlind+" name: {}, value: {}".format(message.topic, message.value))
    var replyObj = JSON.parse(message.value);
    //log.info("from "+NameBlind+" replyObj.error: ", replyObj.error)
    log.info("from "+NameBlind+" replyObj.result.response: {}".format(replyObj.result.response))
    dev[NameBlind]["reply"] = replyObj.result.response;
  });
  
  return 0
}

function requestRPC(modbusPort, modbusSpeed, modbusParity, reqStopbit, clientID, requiestID, messageType, message, responseSize){
  //Формируем JSON запрос. Должно быть что-то такое:
  //{"params": {"response_size": 8, "format": "HEX", "path": "/dev/ttyRS485-1", "baud_rate": 9600, "parity" : "N", "data_bits" : 8, "stop_bits" : 1, "msg": "0A03008000018499"}, "id" : 1}
  var strJson = JSON.stringify({params: {response_size: responseSize, format: messageType, path: modbusPort, "baud_rate": modbusSpeed, parity: modbusParity, "data_bits" : 8, "stop_bits" : reqStopbit, "msg": message}, "id" : requiestID})
  publish(pathRPC+clientID, strJson, 2, false);
}

function blindRequestMsg(blindID, blindCh_L, blindCh_H, commandBlind, dataBlind){
  //Сформируем набор байт команды. Первый байт всегда 55.
  //Второй - blindID
  //третий - blindCh_L
  //четвертый - blindCh_H
  //пятый - команда
  //шестой - данные команды
  //седьмой - crc
  //0A03008000018499
  //на входе ожидаем целые без знака.

  //Вычисляем CRC прямо тут, без вызова функции. Она все равно нигде больше не применяется.
  var verify = blindID ^ blindCh_L ^ blindCh_L ^ commandBlind ^ dataBlind;
  //Готовим строку
  return "55"+toHexStr(blindID)+toHexStr(blindCh_L)+toHexStr(blindCh_H)+toHexStr(commandBlind)+toHexStr(dataBlind)+toHexStr(verify);
}

function toHexStr(inArg){
  //На входе int, на выходе hex строка дополненные нулями спереди
  var tmp = inArg.toString(16);
  if (tmp.length < 2) {tmp = "0"+tmp};
  return tmp;
}

function makeNewVirtualControl(vdName, nameControl, typeControl){
  //log.info("GetDevice", getDevice("vdName"))
  if (getDevice(vdName) === undefined) {
    log.info("Define new")
    defineVirtualDevice(vdName, {
      title: vdName,
      cells: {},
    })
  }
     //Тут проверим есть ли уже контрол и если нет - создадим.
    if (!getDevice(vdName).isControlExists(nameControl)) {
      log.debug("Контрола "+nameControl+" нет, создаем.")
      getDevice(vdName).addControl(nameControl, typeControl);
    }
}

//Создаем виртуальное устройство, с контролами для шторы. По строчке на одну штору.
//                имя               порт          bod parity stopbit  ID
createBlind("ONVIZ MR2234F", "/dev/ttyRS485-1", 9600, "N", 1, 0, 0, 0)

Данный код при запуске выдает:


2024-07-16 11:29:51Error in getting device: Device with given ID doesn't exist

2024-07-16 11:29:51Define new

2024-07-16 11:29:51Контрола position нет, создаем.

2024-07-16 11:29:51Контрола position_in нет, создаем.

2024-07-16 11:29:51Контрола open нет, создаем.

2024-07-16 11:29:51Контрола Stop нет, создаем.

2024-07-16 11:29:51Контрола close нет, создаем.

2024-07-16 11:29:51Контрола MotorRun нет, создаем.

2024-07-16 11:29:51Контрола reply нет, создаем.

2024-07-16 11:29:51defineRule: ONVIZ MR2234F_rule_open

2024-07-16 11:29:51defineRule: ONVIZ MR2234F_rule_Stop

2024-07-16 11:29:51defineRule: ONVIZ MR2234F_rule_close

2024-07-16 11:29:51defineRule: ONVIZ MR2234F_rule

2024-07-16 11:29:58from ONVIZ MR2234F name: /rpc/v1/wb-mqtt-serial/port/Load/ONVIZ MR2234F/reply, value: {"error":{"code":-32000,"data":"Port IO error: Serial protocol error: request timed out","message":"Server error"},"id":1}

2024-07-16 11:29:58ECMAScript error: TypeError: invalid base value
duk_hobject_props.c:2000
anon /etc/wb-rules/sasa1.js:69 preventsyield

Давайте попробуем.
Для начала целесообразно получать текущую позицию привода.
Покажите пожалуйста запрос с ответом, например через serial_tool, возвращающим ее.
Как раз можно будет проверить, верно ли реализован CRC расчет тут.

Если я вас правильно понял, то это команда чтения позиции в процентах - 0x02. В моём случае запрос 550101010201A142. Такой запрос возможен, если у мотора выставлены крайние положения. Для этого необходимо установить мотор в карниз и запустить процесс авто-калибровки. Сделать это не возможно. Карниз смонтирован на объекте, мотор в мастерской.

Через serial_tool с мотором могу общаться. На запросы “открыть” и “стоп” ответы оприходят:

root@wirenboard-AWUXTPDN:~# serial_tool -b 9600 -p N -d 8 -s 1 -t 1 /dev/ttyRS485-1
serial_tool on /dev/ttyRS485-1: 9600 8N1.0
Enter your commands below in HEX form. 
All characters but 0-9,a-f including spaces are ignored.
Press Control-D or Control-C to leave the application.
Press [Enter] to print received data
>> 5501010102F861
>> 550101010201A142
>> 5501010301B900
<< 55 01 01 03 01 B9 00
>> 550101030338C1
<< 55 01 01 03 03 38 C1

В текущем состоянии

2024-07-16 16:25:46 open= 55010101030103
2024-07-16 16:25:47 req_Stop= 55010101030301

Думаю что корректного open и close
Перепишу. Но что-то не пойму как именно вычисляется crc16
UPD: понял, обычный moddbus CRC16, но в нем участвует и стартовый 0x55

Проверьте:

//Путь к RPC
var pathRPC = "/rpc/v1/wb-mqtt-serial/port/Load/";

 
function createBlind(NameBlind, portBlind, speedPortBlind, pariryBlid, stopBitBlind, blindCh_L, blindCh_H){
  //Создаем виртуальное устройство
  makeNewVirtualControl(NameBlind, "position", {type: "range", value: 0, min: 0, max: 200, readonly: false});
  makeNewVirtualControl(NameBlind, "position_in", {type: "range", value: 0, min: 0, max: 200, readonly: false});
  // кнопка "Открыть" (работает)
  makeNewVirtualControl(NameBlind, "open", {type: "pushbutton", readonly: false});
  // кнопка "Стоп"(Работает)
  makeNewVirtualControl(NameBlind, "Stop", {type: "pushbutton", readonly: false});
  // кнопка "Закрыть" (работает)
  makeNewVirtualControl(NameBlind, "close", {type: "pushbutton", readonly: false});
  // Активность мотора (1 - вращается)
  makeNewVirtualControl(NameBlind, "MotorRun", {type: "switch", readonly: true});
  // временный для отладки
  makeNewVirtualControl(NameBlind, "reply", {type: "text", value: "", readonly: false});
  
  //open: 
  defineRule(NameBlind+"_rule_open",{
  whenChanged: NameBlind+"/open",
    then: function () {
      log.info(NameBlind+"_open")
      //Формируем запрос 0a dd 
      var req = blindRequestMsg(blindCh_L, blindCh_H, 0x03, 0x01);
      log.info("open=", req)
      //Вызовем запрос
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 0 );
    }
  });
  //Stop: 
  defineRule(NameBlind+"_rule_Stop",{
  whenChanged: NameBlind+"/Stop",
    then: function () {
      //Формируем запрос 0a 0d 
      var req = blindRequestMsg(blindCh_L, blindCh_H, 0x03, 0x03);
      log.info("req_Stop=", req)
      //Вызовем запрос, не ожидая ответ. На команды ответа нет.
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 0 );
    }
  });
  //close: 
  defineRule(NameBlind+"_rule_close",{
  whenChanged: NameBlind+"/close",
    then: function () {
      //Формируем запрос 0a ee 
      var req = blindRequestMsg(blindCh_L, blindCh_H, 0x03, 0x02);
      //log.info("close=", req)
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 0 );
    }
  });
  //Сделаем таймер, который будет опрашивать позицию, возвращая ее в контрол.
/*  startTicker(NameBlind+"_timer", 6000);
  //И правило, работающее по срабатыванию таймера
  defineRule(NameBlind+"_rule",{
  when: function () { return timers[NameBlind+"_timer"].firing; },
    then: function () {
      var req = blindRequestMsg(blindCh_L, blindCh_H, 0x03, 0x04);
      //log.info("req_timer "+NameBlind+"=", req)
      //Вызовем запрос, ожидая 9 байт в ответ
      requestRPC(portBlind, speedPortBlind, pariryBlid, stopBitBlind, NameBlind, 1, "HEX", req, 9 );
    }
  });
*/
  
  trackMqtt(pathRPC+NameBlind+"/reply", function(message){
    log.info("from "+NameBlind+" name: {}, value: {}".format(message.topic, message.value))
    var replyObj = JSON.parse(message.value);
    //log.info("from "+NameBlind+" replyObj.error: ", replyObj.error)
    //log.info("from "+NameBlind+" replyObj.result.response: {}".format(replyObj.result.response))
    dev[NameBlind]["reply"] = replyObj.result.response;
  });
  
  return 0
}

function requestRPC(modbusPort, modbusSpeed, modbusParity, reqStopbit, clientID, requiestID, messageType, message, responseSize){
  //Формируем JSON запрос. Должно быть что-то такое:
  //{"params": {"response_size": 8, "format": "HEX", "path": "/dev/ttyRS485-1", "baud_rate": 9600, "parity" : "N", "data_bits" : 8, "stop_bits" : 1, "msg": "0A03008000018499"}, "id" : 1}
  var strJson = JSON.stringify({params: {response_size: responseSize, format: messageType, path: modbusPort, "baud_rate": modbusSpeed, parity: modbusParity, "data_bits" : 8, "stop_bits" : reqStopbit, "msg": message}, "id" : requiestID})
  publish(pathRPC+clientID, strJson, 2, false);
}

function blindRequestMsg(blindCh_L, blindCh_H, commandBlind, dataBlind){
  //Сформируем набор байт команды. Первый байт всегда 55.
  //отсутствует для текущего - blindID
  //Второй - blindCh_L
  //третий - blindCh_H
  //четыертый - функция
  //пятый - данные функции
  //шестой - crc16_l
  //седьмой - crc16_h
    //Для "открыть", проверочный  55 fe fe 03 01 AD 8A
  //на входе ожидаем целые без знака.

  //Вычисляем CRC.
  var verify = crc16modbus([0x55,toHexStr(blindCh_L),toHexStr(blindCh_H),toHexStr(commandBlind),toHexStr(dataBlind)])
  // CRC16 возвращается, но он может быть короче двух байт (меньше 256) и перым надо отправлять его младший байт.
  verify=toHexStr(verify)
  //log.info("verifyHEX=", verify)
  verify_L = "0x"+verify & 0x00FF
  //log.info("verifyHEX_L=", "0x"+verify & 0x00FF)
  verify_H = "0x"+verify & 0xFF00
  //log.info("verifyHEX_H=", "0x"+verify & 0xFF00)
  
  //Готовим строку
  return "55"+toHexStr(blindCh_L)+toHexStr(blindCh_H)+toHexStr(commandBlind)+toHexStr(dataBlind)+toHexStr(verify_L)+toHexStr(verify_H);
}

function crc16modbus(buffer) {
    var crc = 0xFFFF;
    var odd;

    for (var i = 0; i < buffer.length; i++) {
        crc = crc ^ buffer[i];

        for (var j = 0; j < 8; j++) {
            odd = crc & 0x0001;
            crc = crc >> 1;
            if (odd) {
                crc = crc ^ 0xA001;
            }
        }
    }
    return crc;
};

function toHexStr(inArg){
  //На входе int, на выходе hex строка дополненные нулями спереди
  var tmp = inArg.toString(16);
  if (tmp.length < 2) {tmp = "0"+tmp};
  return tmp;
}

function makeNewVirtualControl(vdName, nameControl, typeControl){
  //log.info("GetDevice", getDevice("vdName"))
  if (getDevice(vdName) === undefined) {
    log.info("Define new")
    defineVirtualDevice(vdName, {
      title: vdName,
      cells: {},
    })
  }
     //Тут проверим есть ли уже контрол и если нет - создадим.
    if (!getDevice(vdName).isControlExists(nameControl)) {
      log.debug("Контрола "+nameControl+" нет, создаем.")
      getDevice(vdName).addControl(nameControl, typeControl);
    }
}

//Создаем виртуальное устройство, с контролами для шторы. По строчке на одну штору.
//                имя               порт        bod   parity   stopbit  ID_L   ID_H
createBlind("ONVIZ MR2234F", "/dev/ttyRS485-1", 9600, "N",      1,      1,     1,    1)

Кнопка “open” выдает

open= 5501010301b900

Почти:

  1. :white_check_mark: Устройство появилось:
    image

  2. :white_check_mark: Кнопка “открыть” работает

  3. :white_check_mark: Кнопка “стоп” работает

  4. :warning: Бегунка позиции - два

  5. :x: Кнопка “закрыть” не работает. Информация из отладки, нажал эти три кнопки по очереди:

2024-07-17 10:47:04ONVIZ MR2234F_open

2024-07-17 10:47:04open= 5501010301b900

2024-07-17 10:47:04from ONVIZ MR2234F name: /rpc/v1/wb-mqtt-serial/port/Load/ONVIZ MR2234F/reply, value: {"error":null,"id":1,"result":{"response":""}}

2024-07-17 10:47:14req_Stop= 550101030338c100

2024-07-17 10:47:14from ONVIZ MR2234F name: /rpc/v1/wb-mqtt-serial/port/Load/ONVIZ MR2234F/reply, value: {"error":null,"id":1,"result":{"response":""}}

2024-07-17 10:47:18from ONVIZ MR2234F name: /rpc/v1/wb-mqtt-serial/port/Load/ONVIZ MR2234F/reply, value: {"error":{"code":-32000,"data":"Hex message has odd char count","message":"Server error"},"id":1}

2024-07-17 10:47:18ECMAScript error: TypeError: invalid base value
duk_hobject_props.c:2000
anon /etc/wb-rules/onviz-2234F.js:72 preventsyield

Вопрос:
Как потом это затащить в спрут?
Сейчас через шаблон от Dooya прокидывается
dooya-dt82tv.json (2,5 КБ)

Да, я не редактировал их, как и получение позиции. Я добился правильного формирования пакета с корректным CRC. Далее - доработайте уже в соответствии с задачами/требованиями.

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