Не могу разобраться с Modbus RTU и 485 интерфейсом ...

vasya00
Offline
Зарегистрирован: 30.05.2016

Спасибо! Очень помогло в разборе ответа от термостата. В принципе с вашей помощью справился с задачей, поднял web сервер через который можно отправлять и принимать команды термостатам. Правда он возвращает ответ со странным опозданием на одну перезагрузку страницы. Отправляет вовремя, а выводит ответ только после перезагрузки, хотя все должно обрабатываться последовательно.. Если кто-нибудь ткнет где проблема, буду очень благодарен. Сразу извиняюсь за качество кода.. Учучь потихоньку.

#include <SPI.h>
#include <UIPEthernet.h>

// определяем конфигурацию сети
byte mac[] = {0xAE, 0xB2, 0x26, 0xE4, 0x4A, 0x5C}; // MAC-адрес
byte ip[] = {192, 168, 1, 10}; // IP-адрес

EthernetServer server(80); // создаем сервер, порт 80
EthernetClient client; // объект клиент

boolean flagEmptyLine = true; // признак строка пустая
char tempChar;
char urnFromRequest[51]; // строка URN из запроса
boolean urnReceived = false; // признак URN принят
unsigned int indUrn; // адрес в строке URN
int pind, valued, pulsed = 0, digit, TelLen;;
typedef struct {
  char a;
  char b;
} Btype;
byte SendData[8], SerialData[16], req[8];
int getNumbers(char *str, char *value);
String getRequest (char *str, char *value);

void setup() {
  Ethernet.begin(mac, ip); // инициализация контроллера
  server.begin(); // включаем ожидание входящих соединений
  Serial.begin(4800);
  Serial2.begin(4800);
  Serial.print("Server address:");
  Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
  Serial.print("");
  pinMode(5, OUTPUT);
}

void loop() {
  
  client = server.available(); // ожидаем объект клиент
  if (client) {
    flagEmptyLine = true;
    flagEmptyLine = true;
    urnReceived = false;
    indUrn = 0xffff;
    Serial.println("New request from client:");

    while (client.connected()) {
      if (client.available()) {
        tempChar = client.read();
        Serial.write(tempChar);
        if ( urnReceived == false ) {
          if ( indUrn == 0xffff ) {

            if ( tempChar == '/' ) indUrn = 0;
          }
          else {
            // запись строки
            if ( tempChar == ' ' ) {
              // URN закончен
              urnFromRequest[indUrn] = 0;
              urnReceived = true;
            }
            else {
              // загрузка символа URN в строку
              urnFromRequest[indUrn] = tempChar;
              indUrn++;
              if ( indUrn > 49 ) {
                // переполнение
                urnFromRequest[50] = 0;
                urnReceived = true;
              }
            }
          }
        }

        if (tempChar == '\n' && flagEmptyLine) {
          // пустая строка, ответ клиенту
          client.println(F("HTTP/1.1 200 OK"));
          client.println(F("Content-Type: text/html; charset=utf-8")); 
          //client.println(F("Refresh: 2"));  // обновить страницу автоматически
          client.println(F("Connection: close")); 
          client.println();
          client.println(F("<!DOCTYPE HTML>")); 
          client.println(F("<html>"));
          client.println(F("<br>"));
          client.println(urnFromRequest);
          char *str = urnFromRequest;
          char pin[3], value[2], pulse[5], modbus[17];
          while (*str != '\r' && *str != '\0')
          {
            switch (*str)
            {
              case 'P':
                pind = getNumbers(str, pin);
                break;

              case 'S':
                valued = getNumbers(str, value);
                break;

              case 'T':
                pulsed = getNumbers(str, pulse);
                break;

              case 'R':
                getRequest(str, modbus);
                digitalWrite(7, HIGH);
                for (byte i = 0; i < 8; i++)
                  Serial2.write(SendData[i]);
                digitalWrite(7, LOW);
                delay(9);
                TelLen = 0;
                SerialData[TelLen] = (char)Serial2.read();
                if (SerialData[TelLen] != 0) {
                  TelLen++;
                }
                while (Serial2.available()) {
                  SerialData[TelLen] = (char)Serial2.read();
                  TelLen++;
                  delay(1);
                }
                delay(500);
                client.println(F("<br>"));
                client.println(F("ModBus RTU ответ: "));
                for (byte i = 0; i < TelLen; i++) {
                  client.println(SerialData[i], HEX);
                  Serial.println(SerialData[i]);
                  req[i] = SerialData[i];
                }
                delay(500);
                break;
            }
            str++;
          }
          Serial.println("__________");
          if (valued > 1)
            Serial.println("ошибка или уже делали");
          else {
            digitalWrite(pind, valued);
            if (pulsed) {
              delay(pulsed);
              digitalWrite(pind, !valued);
            }
            pulsed = 0;
            valued = 2;
          }
          client.println(F("<br>"));
          for (int i = 0; i <= 53; i++) {
            client.println(("PIN "));
            client.println(i);
            client.println((digitalRead(i)));
            client.println(F("<br>"));
          }
          for (int i = 54; i <= 69; i++) {
            client.println(("PIN "));
            client.println(i);
            client.println((analogRead(i)));
            client.println(F("<br>"));
          }
          client.println(F("<form action=\"http://192.168.1.10\" method=\"get\" name=\"form\">"));
          client.println(F("PIN"));
          client.println(F("<input type=\"number\" name=\"P\">"));
          client.println(F("значение"));
          client.println(F("<input type=\"number\" name=\"S\">"));
          client.println(F("время"));
          client.println(F("<input type=\"number\" name=\"T\">"));
          client.println(F("ModBus RTU запрос"));
          client.println(F("<input type=\"text\" name=\"R\">"));
          client.print(F("<input type=\"submit\" text=\"передать\">"));
          client.print( (float)millis() / 1000. );
          client.println(F("<br>"));
          for (byte i = 0; i < 8; i++) {
            client.println(req[i], HEX);
          }
          client.println(F("<br>"));
          for (byte i = 0; i < 8; i++) {
            client.println(SerialData[i], HEX);
          }
          client.println(F("</html>"));
          break;
        }
        if (tempChar == '\n') {
          // новая строка
          flagEmptyLine = true;
        }
        else if (tempChar != '\r') {
          // в строке хотя бы один символ
          flagEmptyLine = false;
        }
      }
    }
    delay(1);
    client.stop();
    Serial.println("Break");
                    for (byte i = 0; i < TelLen; i++) {

                  Serial.println(SerialData[i]);
                  req[i] = SerialData[i];
                }

  }
}
int getNumbers(char *str, char *value)
{
  int i = 0;
  str = str + 2;
  digit = 0;
  while ('0' <= *str && *str <= '9')
  {

    value[i] = *str;
    digit = digit * 10 + value[i] - 48;
    i++;
    str++;
  }
  value[i] = '\0';
  return (digit);
}

String getRequest(char *str, char *value)
{
  int i = 0;
  str = str + 2;
  for (i = 0; i < 17; i++)
  {

    value[i] = *str;
    str++;
  }
  value[i] = '\0';
  convert(value, SendData);
  return (value);
}

int convert(char *str, unsigned char *byte)
{
  Btype *chp;
  chp = (Btype*)str;
  int i = 0;
  unsigned char c1, b1;
  while (chp[i].a) {
    c1 = (chp[i].a >= 65) ? 55 : 48;
    b1 = (chp[i].a - c1) << 4;
    c1 = (chp[i].b >= 65) ? 55 : 48;
    byte[i] = b1 | (chp[i].b - c1); i++;
  }
  return i;
}

 

vasya00
Offline
Зарегистрирован: 30.05.2016

Спасибо ещё раз за помощь в разборе отчета термостата, застрял немного, спалил выход шилда MAX485, и никак не мог понять почему ничего не принимает) Пришлось подпаяться напрямую к Rx Tx термостата на входе в его MAX485, так завелось..

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Я тут уже сто раз рассказывал о проблемах китайских шилдов. У вас оно так  будет гореть. Схема:

https://media2.24aul.ru/imgs/5320f704e1bf2714b47e5453/nabor-dlya-sborki-preobrazovatelya-interfeysov-v-rs485-1-3830949.jpg

Обязательно добавьте два стабилитрона.

Что то у вас работа с модбас шиной мутная какая то, попробуйте строки 106-121 заменить вот на это:

uint16_t buffSize=0xFFFF;
while(buffSize!=Serial2.availableForWrite()){
   buffSize=Serial2.availableForWrite();
   delay(10);    
}
digitalWrite(7, HIGH);
for (byte i = 0; i < 8; i++){
   Serial2.write(SendData[i]);
}
while(buffSize!=Serial2.availableForWrite()){}
digitalWrite(7, LOW);
delay(500);
TelLen = 0;
while (Serial2.available()) {
   if(TelLen<sizeof(SerialData)-1){
      SerialData[TelLen] = Serial2.read(); 
   } else {
      Serial2.read(); 
   }
}

Кстати, а для чего задержки аж по полсекунды ?

vasya00
Offline
Зарегистрирован: 30.05.2016

Спасибо, стабилитроны добавлю, не думал что они с чего то так горят. Задержки были нужны для более удобной интерпретации отладочной информации в Serial0, они там временно. По поводу строчек:

while (Serial2.available()) {
  if (TelLen < sizeof(SerialData) - 1) {
    SerialData[TelLen] = Serial2.read();
  }
  else {
    Serial2.read();
  }

Не понимаю мы сравниваем длину SerialData которая по сути всегда равна 8? С TelLen который мы не меняем в этом цикле? Эта часть у меня не заработала, и я её не понял. Остальное, спасибо, исправил по вашему совету, так оно гораздо лучше выглядит. Но с чтением и выводом SerialData остается проблемка, оно отстает на один цикл.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

У вас размер буфера задан жестко и он равен 16, что будет если ваше устройство "случайно" пошлет подряд больше 16 байт ?

У вас тут конечно нет проверки контрольной суммы, это важно, а то вы можете таких странных данных напихать в веб интерфейс , что человек будет долго удивляться.

485 - интерфейс токовой петли, при превышении напряжения между двумя устройствами (например разные блоки питания), этот самый ток выжигает входные ноги микросхемы... Вот такой косяк, а может китайская подделка. 

vasya00
Offline
Зарегистрирован: 30.05.2016

Точно, спасибо! Тогда там нужно ещё добавить TelLen++; и все завелось.. Вот только все равно, после отправки команды на термостат выводяться нули, и только со второго запроса получаем предыдущий ответ.. До проверки контрольной суммы обязательно дойду, тем более вы уже подсказали как с ней быть.. Единственный нюанс есть еще термостат с привычным ModBus RTU, и он считает контрольные суммы иначе.. Про разные потенциалы я интуитивно догадывался, и отрубал блок питания от ноутбука при соединении, но не спасло..

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Да, пропустил. Не проверял. 

Нужно понять, что означают нули. У вас же Serial2 - хардварный ? Попробуйте в 3 строке моего кода поставить печать buffSize для отладки. Возможно у вас не реализован метод availableForWrite.

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

Дальше нужно дождаться ответа от термостата. Нули могут читаться из порта если вы читаете его не убедившись в наличии данных (Serial.available()), либо из-за помех происходящих в момент переключения.

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

vasya00
Offline
Зарегистрирован: 30.05.2016

Получается что после отправки он верно получает ответ, каждый раз, например мы запросили состояние, и судя по всему получили ответ, но он не ушёл в сформированную страничку в браузер, затем мы может отправить термостату что угодно, например FF, и только после этого браузер нарисует верный ответ на предыдущий запрос. То есть с отправкой и получением данных проблем нет, проблема с отрисовкой их в браузере с опозданием, как будто мы обрабатываем этот ответ после отрисовки страницы, и выводим уже соответственно при следующей перезагрузке. Но это не так. Не могу понять почему так происходит. К тому же в Serial0 выводится так же 0 сразу после получения, и следующий раз верный ответ но на предыдущий запрос.

По идее катастрофы не будет если мы получим 7 байт, то восьмой мы не заменим на новый, и он останется от предыдущего запроса, тут помог бы подсчет контрольной суммы.

Опрашивать Serial2 постоянно имеет смысл? Ведь slave отправляет данные только в ответ на запрос?

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Да нет вы вместо отправки страницы клиенту будете доооолго ждать восьмого байта.

vasya00
Offline
Зарегистрирован: 30.05.2016

Но мы ведь проверяем Serial2.available() это как раз и говорит что там еще есть данные для чтения? Когда они кончаться, пусть даже на 7, условия цикла нарушиться и мы пойдем дальше. Разве не так?

trembo
trembo аватар
Offline
Зарегистрирован: 08.04.2011

brokly пишет:

У вас размер буфера задан жестко и он равен 16, что будет если ваше устройство "случайно" пошлет подряд больше 16 байт ?

У вас тут конечно нет проверки контрольной суммы, это важно, а то вы можете таких странных данных напихать в веб интерфейс , что человек будет долго удивляться.

485 - интерфейс токовой петли, при превышении напряжения между двумя устройствами (например разные блоки питания), этот самый ток выжигает входные ноги микросхемы... Вот такой косяк, а может китайская подделка. 


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

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

trembo пишет:
Вообще-то не токовой петли, а дифференциальная линия.

Не путайте мягкое с теплым. А может это я путаю. Пойду даташит посмотрю.

То что диф, это очевидно, а вот с токовой петлей это я что то из другой оперы... Это мне dali навеяло, там что то типа цифровой токовой петли.

nik182
Offline
Зарегистрирован: 04.05.2015

Токовая не получится. Они в параллель сажаются, а токовая последовательности требует.

vasya00
Offline
Зарегистрирован: 30.05.2016

Проблема с чтением ответа, разрешилась с помощью небольшой задержки после отправки команды и перед чтением, эта задержка должна быть более 30 мс

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

vasya00 пишет:

Проблема с чтением ответа, разрешилась с помощью небольшой задержки после отправки команды и перед чтением, эта задержка должна быть более 30 мс

Собственно, задержка - это костыль. По хорошему надо на лету анализировать ответ и дожидаться (с учетом таймаута), когда ответ придет полностью.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Должны быть две задачи, одна беседует с контролером, а вторая с веб клиентом. К моменту когда клиент только собрался спросить у сервера как там температура, сервер уже должен спросить об этом контролер и получить ответ.

vasya00
Offline
Зарегистрирован: 30.05.2016

Да, наверно это в идеале должно быть так. Хотя, с вашей помощью, получилось реализовать вполне рабочий вариант. Но нет предела совершенству) Намекните направление для реализации двух задач? Ведь что именно, в какой момент и у какого устройства запросит клиент заранее не известно. Получается формировать запрос и ждать ответ, все равно, придется после получения get запроса в теле веб сервера?

sadman41
Offline
Зарегистрирован: 19.10.2016

Периодический опрос слейвов, хранение значений метрик в ОЗУ и отдача их по запросу спасут отца русской демократии.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Я бы сделал как Cадман. От того как часто вы опрашиваете термостат зависит только свежесть данных у вас на сервере, которые вы и выдаете клиенту.

Например не взирая на то есть ли подключение веб клиента или нет, вы постоянно , раз в секунду (например) опрашиваете термостат, складываете полученные данные к себе в буфер. Если клиент подключился - отдаете ему данные из буфера. Вы же за хранение в озу данных никому не платите ? Каждый байт данных с термостата у вас не тарифицируется ? Бесплатно же ;) Терять то нечего.  

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Что то я не увидел тут ModBus'а.
Это ж протокол, со своими правилами построения пакета и контрольной суммой.
У вас ни первого ни второго.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Пакет тут построен по правилам мод баса. А вот контрольная сумма нет. Но это уже было выяснено в первых обсуждениях.