LED controller: прошивка

После долгого перерыва, вызванного устранением аппаратных проблем, продолжаем пилить LED controller. На этот раз будет алгоритм работы и исходники прошивки.

Disclaimer

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

Логика работы устройства

Напомню, наш контроллер предназначен для управления освещением в помещении (в моём случае — в санузле). Для этого у него есть драйверы и различные чувствительные элементы:

  • выключатель (2 шт.);
  • кнопка управления яркостью (2 шт.);
  • датчик движения;
  • датчик освещенности;

Один выключатель используется для управления светом, второй — для управления внешней нагрузкой через встроенное реле (в моём случае нагрузкой является вытяжной вентилятор). Кнопки управления яркостью DIM+/DIM- увеличивают/уменьшают скважность управляющих импульсов для драйверов. Датчик движения нужен для определения присутствия пользователя. Датчик освещенности пока не используется (в моей конфигурации он не нужен). Контроллер предназначен для работы в автоматическом режиме.

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

Кнопки управления яркостью меняют максимально допустимую яркость. То есть, если мы вошли в помещение, свет загорелся на яркости 999 попугаев, мы уменьшили до 600 попугаев — то при следующем включении света яркость будет 600 попугаев.

Ещё один способ применения уставки допустимой яркости является переключение в ночной режим. Так как наш контроллер имеет интерфейс Ethernet и умеет по нему общаться, то и команды мы ему может выдавать не только органами местного управления (кнопками и выключателями), но и дистанционно с какого-нибудь сервера. Например, сервера автоматизации жилища. Наш сервер в заданное время сам передает новое значение максимально допустимой яркости в контроллер. Ночью это может быть 200 попугаев, например (наиболее комфортные значения будут выяснены позже в процессе опытной эксплуатации). Это чрезвычайно удобно для ночного похода в санузел — вместо яркого света, бьющего по глазам, мы получаем мягкую подсветку.

Но что делать, если в ночное время нам всё-таки понадобился яркий свет? А вот на этот случай нам как раз и нужен выключатель. Будучи включенным, он однозначно говорит системе, что сейчас нам не нужны все эти автоматические приколы — нам нужен просто свет. Выключатель включает свет на абсолютный максимум яркости вне зависимости от любых настроек. Это своего рода переключатель между режимами (выключен — «АВТО», включен — «РУЧНОЙ ВКЛ»).

Организация программы

Программа для нашего МК (а там стоит STM32F107) будет использовать FreeRTOS. Это очень печально, но без неё обрабатывать TCP соединения очень неудобно. Поэтому мы будем работать с задачами, их у нас будет три:

  • defaultTask
  • networkTask
  • networkReceiveTask

defaultTask — эта задача опрашивает кнопки и датчики, а также управляет выработкой ШИМ сигналов на драйвера по вышеприведенному алгоритму. Например, опрашиваем выключатель света:


/* Check SWITCH input */
paramStruct.switchIn = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9);
/* If SWITCH is ON - force light ON at max brightness*/
if(paramStruct.switchIn == 0)
{
  paramStruct.pwm1 = ABSOLUTE_MAX_PWM;
  paramStruct.pwm2 = ABSOLUTE_MAX_PWM;
  paramStruct.pwm3 = ABSOLUTE_MAX_PWM;
  paramStruct.pwm4 = ABSOLUTE_MAX_PWM;
  TIM3->CCR1 = paramStruct.pwm1;
  TIM3->CCR2 = paramStruct.pwm2;
  TIM3->CCR3 = paramStruct.pwm3;
  TIM3->CCR4 = paramStruct.pwm4;

  if(switchInPrev == 1)
  {
    memset(mqttTopicName, 0, PARAMFIELD_LENGTH);
    sprintf(mqttTopicName, "pwm");
    memset(mqttValue, 0, PARAMFIELD_LENGTH);
    sprintf(mqttValue, "%d", paramStruct.pwm1);
    osSemaphoreRelease(ethTxSemaphoreId);
    tickCount = 0;
  }
}

Здесь показана только одна ветвь условного оператора (когда выключатель включен), т.к. ветвь else довольно большая (в ней идут остальные проверки). Здесь же мы включаем ШИМ на максимум и если произошло включение — записываем в буфер название и значение изменившейся переменной (в данном случае это ШИМ в попугаях) и семафорим второму потоку, что данные можно (нужно?) отсылать. Проверки остальных органов управления проходят по той же схеме.

networkTask — эта задача занимается общением по Ethernet’у и всем с этим связанным. В ней инициализируется Ethernet, присваивается MAC адрес, происходит получение IP адреса от DHCP сервера. После того, как адрес получен, задача пытается установить TCP соединение с сервером, которому мы будем отдавать данные. Как только это получается — задача переходит в режим ожидания семафора для отправки данных.


if(connState == CONNSTATE_MQTT_READY)
    {
        if(osSemaphoreWait(ethTxSemaphoreId, 1000) == 0)
        {
          error = MqttPublish(sock, mqttClientId, mqttTopicName, mqttValue, 0);
          if(error < 0)
          {
              connState = CONNSTATE_TCP_NOCONNECT;
              shutdown(sock, 2);
          }
        }
    }

Ещё одна функция этой задачи — поддерживать TCP соединение. Состояние соединения описывается машиной состояний. Для успешного соединения мы должны последовательно пройти все стадии:


#define CONNSTATE_IF_NOTREADY       0
#define CONNSTATE_IF_READY      1
#define CONNSTATE_SOCKET_READY      2
#define CONNSTATE_TCP_NOCONNECT     3
#define CONNSTATE_TCP_CONNECTED     4
#define CONNSTATE_MQTT_CONNECTED    5
#define CONNSTATE_MQTT_READY        6

Если по каким-то причинам отправка не удалась — скидываем состояние в CONNSTATE_TCP_NOCONNECT. И на следующей итерации цикла создаем соединение заново.

networkReceiveTask — эта задача нужна для приёма данных от сервера. Так как наше общение с сервером проходит в асинхронном режиме, то принимать данные в том же потоке не получится. Наверное все-таки можно, если использовать неблокирующий recv и callback‘и, но если мы уж всё равно нагородили тут RTOS, то почему бы просто не вытащить это в ещё одну задачу. Для того, чтобы принимать асинхронные сообщения от сервера, высланные в это соединение мы должны использовать для приема тот же сокет. Я не уверен в том, что использование одного и того же сокета в разных потоках это хорошая идея, но в нашем случае запись производится в одном, а чтение в другом. Так что, может быть, всё будет нормально 🙂


void NetworkReceiveTaskFunction(void const * argument)
{
    int nbytes, nrecv = 0, err;
    unsigned char buffer[200];
    char cmdBuf[50], valBuf[20];
    while(1)
    {
        while(connState < CONNSTATE_TCP_CONNECTED)
        {
            osDelay(10);
        }
        nbytes = recv(sock, buffer, sizeof(buffer),0);
        if(nbytes > 0)
        {
            //nrecv++;
            err = MqttParse(buffer, mqttClientId, "cmd", cmdBuf, valBuf);
            if(err == 100) connState = CONNSTATE_MQTT_CONNECTED;
            if(err == 101) ParseCommand(cmdBuf, valBuf);
            memset(buffer, 0, 200);
            memset(cmdBuf, 0, 50);
            memset(valBuf, 0, 20);
        }
    }
}

Это, собственно, весь код этой задачи. Ждем пока установится соединение с сервером. Принимаем сообщения от сервера и парсим их.

MQTT

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

MQTT протокол

MQTT работает следующим образом: есть сервер (брокер) и клиенты, клиенты могут посылать данные на сервер в топики (topics) и получать данные обратно от сервера. Для последнего нужно быть подписанным на соответствующий топик. Тогда при изменении значения этого параметра сообщения об этом будут разосланы сервером всем подписчикам.

Для начала нужно раздобыть где-то спецификацию протокола. Благо эту спецификацию никто не скрывает и не требует членства в какой-то межгалактической ассоциации любителей MQTT (в отличие от некоторых других интерфейсов!). Лежит она прямо вот тут. Я использовал версию протокола 3.1.1, потому что она у меня уже давно лежала загруженная 🙂

Процесс общения с MQTT сервером начинается с команды CONNECT, которую клиент должен отправить серверу. В этом пакете кроме стандартных заголовков должен быть только ID клиента (в самом простом случае). На что сервер должен ответить пакетом CONNACK с кодом ошибки внутри. Если код ошибки равен нулю — то всё OK, соединение принято.


int MqttConnect(int connSocket, char *clientId)
{
  int err = 0;
  //char *clientId = "led_ctrl_020701";
  unsigned char message[29];
  message[0] = MQTT_MESSAGE_TYPE_CONNECT;
  message[1] = 27;
  /* Variable header */
  /* Field: protocol name */
  message[2] = 0;  /* MSB field length*/
  message[3] = 4;  /* LSB field length*/
  message[4] = 'M';
  message[5] = 'Q';
  message[6] = 'T';
  message[7] = 'T';
  /* Field: protocol level */
  message[8] = 4; /* 4 by default*/
  /* Field: connect flags */
  message[9] = MQTT_CONNECTFLAGS_CLEANSESSION;
  /* Field: keep alive */
  message[10] = 0;
  message[11] = 0;
  /* Payload */
  /* Field length */
  message[12] = 0;
  message[13] = 15;
  /* Client ID field */
  sprintf(message + 14, "%s", clientId);

  /* send packet */
  err = send(connSocket, message, sizeof(message), 0);
  return err;
}

После получения подтверждения от сервера можно засылать туда свои данные. Этим занимается команда PUBLISH.


int MqttPublish(int connSocket, char *clientId, char *topicName, char *value, int packetId)
{
    int err = 0;
    int index, length;
    //char *clientId = "led_ctrl_020701";
    unsigned char message[50];

    memset(message, 0, 50);
    /* -- Fixed header -- */
    /* Field: message type */
    message[0] = MQTT_MESSAGE_TYPE_PUBLISH;
    /* Field: remaining length - init as 0xff */
    message[1] = 0xff;
    /* -- Variable header -- */
    /* Field: Length MSB & LSB - init as 0xff */
    message[2] = 0xff;
    message[3] = 0xff;
    /* Field: Topic Name */
    sprintf(message + 4, "%s\/%s", clientId, topicName);
    index = strlen(message);
    /* Field: Packet ID */
    /* not used with QoS = 0 */

    /* -- Payload -- */
    sprintf(message + index, "%s", value);
    /* Fill in remaining length field */
    message[1] = strlen(message) - 2;
    /* Fill in variable header length field */
    length = index - 4;
    message[2] = (unsigned char)((length & 0x0000ff00) >> 8);
    message[3] = (unsigned char)(length & 0x000000ff);

    /* send packet */
    err = send(connSocket, message, (int)(message[1] + 2), 0);
    return err;
}

А чтобы получать от сервера интересующие нас параметры мы должны на них подписаться командой SUBSCRIBE. При этом допускается использование wildcard, которым служит символ ‘#‘ (решетка). Т.е. подписавшись на топик led_ctrl_020701/cmd/# мы будем получать сообщения об изменении параметров led_ctrl_020701/cmd/pwm, led_ctrl_020701/cmd/auxCtrl, led_ctrl_020701/cmd/maxPwm и т.д.


int MqttSubscribe(int connSocket, char *clientId, char *topicName, int packetId)
{
    int err = 0;
    int index, length;
    unsigned char message[50];

    memset(message, 0, 50);
    /* -- Fixed header -- */
    /* Field: message type */
    message[0] = MQTT_MESSAGE_TYPE_SUBSCRIBE | 0x02;
    /* Field: remaining length - init as 0xff */
    message[1] = 0xff;
    /* -- Variable header -- */
    /* Field: PacketId MSB & LSB - init as 0xff */
    message[2] = (unsigned char)((packetId & 0x0000ff00) >> 8);
    message[3] = (unsigned char)(packetId & 0x000000ff);
    /* Payload */
    /* Field: Length MSB & LSB - init as 0xff */
    message[4] = 0xff;
    message[5] = 0xff;
    /* Field: Topic name*/
    sprintf(message + 6, "%s\/%s", clientId, topicName);
    /* Field: Max QoS - set as 0*/
    index = 6 + strlen(clientId) + 1 + strlen(topicName);
    message[index] = 0;
    length = strlen(clientId) + 1 + strlen(topicName);
    message[1] = index - 1;
    message[4] = (unsigned char)((length & 0x0000ff00) >> 8);
    message[5] = (unsigned char)(length & 0x000000ff);

    /* send packet */
    err = send(connSocket, message, (int)(message[1] + 2), 0);
    return err;
}

Для отладки взаимодействия по протоколу MQTT понадобятся некоторые инструменты. Сам MQTT сервер mosquitto (можно взять тут), к нему же в комплекте прилагаются две утилиты: mosquitto_pub и mosquitto_sub, которые отправляют на сервер команды PUBLISH и SUBSCRIBE соответственно. Подробнее об их использовании можно прочитать тут. Очень мощным инструментом для отладки является старый добрый wireshark, который умеет парсить MQTT пакеты.

Wireshark — MQTT пакеты

Полный текст программы можно, как обычно, найти на Github.

About the Author: admin

Добавить комментарий

Ваш адрес email не будет опубликован.