Как и обещал в предыдущей статье, пишу о нашем опыте работы с контактными ключами Metacom и Cyfral.

Про русские народные протоколы контактных ключей

Эти ключи построены на микросхемах 1233KT1 и 1233KT2, которые не сильно друг от друга отличаются и имеют очень схожий принцип работы.

При подаче питания ключ просто выдает свой id. При этом никакие команды ключ не принимает и не посылает, а проверка правильности считывания ключа производится путем повторного считывания. Первым, для определения начала передачи, всегда идет стартовое слово. В отличие от ключей Dallas, они работают не по напряжению, а по току. Это менее распространенные и более дорогие ключи. Таким образом, логические уровни определяются сопротивлением ключа (около 400 Ом и 800 Ом). А значение бита определяется длительностью удержания низкого и высокого значения потребления тока.

Разберем эти ключи по отдельности.

Про метаком

Этот ключ построен на микросхеме 1233KT2 (хотя у метаком есть и ключи dallas) .

В начале передается синхронизирующий сигнал. Передача синхронизирующего бита представляет собой удержание потребляемого тока на высоком уровне в течение целого периода передачи одного бита. При этом период передачи одного бита может быть от 50 до 230 мкс. За ним передаётся трёхразрядное стартовое слово, которое содержит порядковый номер разработки – 210=0102 без контроля на чётность.

Далее передаются сами биты ключа: передача каждого представляет собой последовательное удержание потребляемого тока сначала на низком, а затем на высоком уровне. При этом, при передаче логической «1» около 2/3 периода удерживается низкий уровень сигнала и остальные 1/3 высокий.  При передаче логического «0», соответственно, наоборот. Ключ Metakom посылает 4 байта, где каждый байт заканчивается битом четности.

Кстати, все это есть в datasheet. Будем считать, что с теорией всё понятно. Осталось понять, как прочитать этот ключ микроконтроллером.  Напомню, на данный момент мы используем ESP8266 и программируем его в среде Arduino IDE.
Для начала надо понять как определять изменения потребления тока. Какого-либо датчика тока ни у Arduino ни у ESP8266 нет, но зато есть АЦП и закон ома! При увеличении тока в цепи, падает напряжение, а его мы как раз можем измерить.

Новая схема ключа
Новая схема ключа

Тут нас ждет сразу несколько проблем:
1. Точно неизвестно, относительно какого уровня напряжения определять низкий уровень потребления тока и высокий.

2. Период передачи одного бита может быть от 50 до 230 мкс, а время работы функции analogRead у ESP8266 около 90мкс, у ардуино вообще 112 мкс (знал бы я это сразу, было бы попроще).

3. Точно не известен период передачи 1 бита (от 50 мкс до 230 мкс это не очень точно).

Ну, пойдем по очереди. На самом деле, очень помог проект от Alex Malov EasyKeyDublicator, правда пришлось почти все переписать.

Определить граничную величину напряжения довольно просто, это средне-медианное напряжение при взаимодействии с ключем. То есть, просто вызываем analogRead 256 раз, складываем результаты и делим на 256. В итоге напряжение при высоком уровне потребления тока будет немного ниже медианного, а при низком – немного выше. У меня это делает функция calcAverage():

 int calcAverageU() {
    unsigned long sum = 512;
    for (byte i = 0; i < 255; i++) {
      delayMicroseconds(10);
      sum += analogRead(A0);
    }
    sum = sum >> 8;
    return sum;
  }

Ускоряем AnalogRead

Теперь надо разобраться с АЦП. На ардуино проблем нет, достаточно просто изменить делитель, при этом, правда, снизится точность, но с этим проблем не возникает. Или можно вообще использовать функцию analogReadFast от AlexGyver. Так же можно ещё читать напрямую из регистров, но это уже лишнее.

void setup()
{
  Serial.begin(9600);
  ADCSRA &= B11111000; // очистить три младших бита
  ADCSRA |= B00000100; // установить в них комбинацию 100 что дает делитель 16 и тактовую частоту АЦП 1МГц
}

// Функция analogReadFast от AlexGyver
// ВНИМАНИЕ! Нужное опорное установлено DEFAULT, можно изменить на своё
uint16_t analogReadFast(uint8_t pin) {
  pin = ((pin < 8) ? pin : pin - 14);    // analogRead(2) = analogRead(A2)
  ADMUX = (DEFAULT<< 6) | pin;    // Set analog MUX & reference
  bitSet(ADCSRA, ADSC);            // Start 
  while (ADCSRA & (1 << ADSC));        // Wait
  return ADC;                // Return result
}

void loop()
{
  Serial.println(analogRead(0));
  Serial.println(analogReadFast(0));
}

С ESP все немного сложнее, прямого доступа ни к регистрам, ни к функции analogRead нет. Казалось бы, ничего не поделаешь, но нашлась встроенная функция system_adc_read_fast, которая работает намного быстрее.

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

int analogReadFast(byte pin) //pin просто для совместимости
{
  uint16_t adc_addr[1]; // значения которые считываются ацп, может быть [1, 65535]
  system_adc_read_fast(adc_addr,1,10); //костыльное решение проблемы со временем, но работает за 17мкс, а не 80 (знаяения, колво, adc_clk_div)
  return adc_addr[0];
}

Читаем ключ

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

unsigned long  GetLowPulseDuration(int Average = 510, unsigned long timeOut = 1500)  
  {
    bool AcompState;
    unsigned long tEnd = micros() + timeOut;
    do {
      //Если напряжение выше среднего, значит это начало низкого уровня потребления тока
      if (analogReadFast(A0) < Average) { 
        tEnd = micros() + timeOut;
        do {
          //Если напряжение ниже среднего, значит это конец возвращаем время
          if (analogReadFast(A0); > Average) return (unsigned long)(micros() + timeOut - tEnd);  
        } while (micros() < tEnd);
        return 0;  //таймаут, импульс не вернуся обратно
      }            
    } while (micros() < tEnd);
    return 0;
  }

 Функция возвращает длительность низкого уровня потребления тока, а далее по этой длительности мы можем определить, один это или ноль. Проще всего оказалось высчитать половину периода передачи и сравниваться с ней. Если длительность больше полупериода, то это 1, если меньше – 0.

Осталось определить половину периода. Для этого просто считаем среднее для нескольких вызовов функции GetLowPulseDuration.

int calcAverageP(int aver = 510) { // aver – среднее значение из calcAverage
    unsigned long tSt = 0;
    for (int i = 0; i < 20; i++) {
      tSt += GetLowPulseDuration(aver);
    }
    return tSt / 20;
  }

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

byte readBit(int aver, int halfT) {
    int ti = GetLowPulseDuration(aver);
    if ((ti == 0) || (ti > 400)) return 2; // ошибка чтения
    if (ti >= halfT * 2) return 3;  // Сигнал синхронизации метаком
    if (ti < halfT) return 1;
    else return 0;
  }

Попробуем прочитать весь ключ:

bool read_metacom(byte* buf) {
    int aver = calcAverageU();
    int halfT = calcAverageP(aver);
    unsigned long tEnd = millis() + 30;

    byte b1 = 1; //4 бита стартового слова
    byte b2 = 1;
    byte b3 = 1;
    byte b4 = 1;

    do {

      b1 = b2;
      b2 = b3;
      b3 = b4;
      b4 = readBit(aver,halfT);
      if (b4 == 2) continue;
      
      if (b1 == 3 && b2 == 0 && b3 == 1 && b4 == 0) { //Ожидаем стартовое слово
        buf[0] = 0b00100000;  //добавляем кодовое слово
      } else  continue;

      for (int byteInd = 1; byteInd < 5; byteInd++) {  //читаем 4 байта по 8 бит
        int k = 0;                                     //для подсчета четности
        for (int bitInd = 0; bitInd < 8; bitInd++) {
          byte bit = readBit(aver,halfT);
          if (bit == 2) return false;
          if (bit == 1) {
            bitSet(buf[byteInd], 7 - bitInd);
            k++;
          }
        }
        if (k % 2 == 1)  //Проверка четность
        {
          //Serial.print("Fiasko");
          if (byteInd == 4) buf[byteInd]++;  //Последний бит хрен считаешь, поэтому костылим
          else return false;
        }
      }
      return true;
    } while ((millis() < tEnd));
    return false;
  }

Для универсальности, ключ мы, как и dallas, записываем в 8-байтный массив. Первый байт – это код семейства, у метакома мы запишем 0b00100000, т.е. стартовое слово, и потом сам код. Для надежности можно считать пару раз. Считать получилось, осталось попробовать эмулировать.

Эмуляция Metacom

На самом деле, это оказалось самым простым. Правда возникла пара нюансов)

Для начала функция эмуляции одного бита:

void SendBitMetacom(bool Bit) {
    //берем период за 100 мкс
    digitalWrite(EmulatePort, 1);  //низкий уровень тока
    if (Bit) delayMicroseconds(70);  //1 это удержание 2/3 периода
    else delayMicroseconds(30);
  
    digitalWrite(EmulatePort, 0);
    if (Bit) delayMicroseconds(30);
    else delayMicroseconds(70);
  }

То есть, мы просто подаем на порт эмуляции HIGH, потом ждем 2/3 периода если хотим передать 1 или 1/3 периода если 0, далее подаем LOW и ждем оставшуюся часть периода.

Сам ключ отсылается тоже не сложно:

 void SendKey(byte Key[8]) {
    //Бит синхронизации и стартовое слово
    digitalWrite(EmulatePort, 0);
    delayMicroseconds(90); // с учетом выключения ждать надо меньше
    SendBitMetacom(0);
    SendBitMetacom(1);
    SendBitMetacom(0);

    byte bInd = 0;
    for (int i = 1; i < 5; i++) { //4 байта
      for (int bInd = 0; bInd < 8; bInd++) { // по 8 бит
        bool bit = (((Key[i]) & (0x1 << (7 - bInd))) != 0);  //получаем бит
        SendBitMetacom(bit); // отправляем бит
      }
    }
  }

Я долго возился с подбором резисторов, чтобы уровень сигнала у эмулятора совпадал с настоящим ключем. Но оказалось, что домофону глубоко фиолетово на уровень сигнала :), и достаточно подключить порт напрямую (как и в схеме с dallas). Но не фиолетово на период между передачами ключа. Нужно, чтобы ключ хотя бы несколько раз передавался непрерывно. Я просто решил вызывать функцию эмуляции в цикле.

Про Цифрал

Микросхема в этом ключе практически такая же, как и в метаком. Даже называются похоже – 1233KT2. Но есть несколько отличий.

Cyfral циклично отправляет 9 нибблов (1 ниббл = 4 бита): 1 стартовый и 8 ID. Ниббл может иметь всего четыре значения для ID и одно значение для стартового слова.

Остальные комбинации запрещены, что может быть использовано для контроля достоверности считывания кода. В отличие от метаком, сигнала синхронизации нет, передается сразу стартовое слово 00012, и далее 8 ниблов по 4 бита. То есть, всего в сумме 36 бит, как и у метакома.

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

  void SendBitCyfral(bool Bit) {
    digitalWrite(EmulatePort, 1);  //0 на выходе транзисторного ключа

    if (Bit) delayMicroseconds(79);  //Иначе, если 1 - задержка 79,2мкс (>0,6Tп=113us)
    else delayMicroseconds(39);      //Если передаётся 0 задержка 39,6мкс (<0,4Tп=107us)

    digitalWrite(EmulatePort, 0);    //1 на выходе транзисторного ключа
    if (Bit) delayMicroseconds(33);  //Иначе, если 1 - задержка 33,2мкс (<0,4Tп=113us)
    else delayMicroseconds(67);      //Если передаётся 0 задержка 67,2мкс (>0,6Tп=107us)
  }

  void SendKey(byte Key[8]) {
    //стартовое слово
    SendBitCyfral(0);
    SendBitCyfral(0);
    SendBitCyfral(0);
    SendBitCyfral(1);

    byte bInd = 0;
    for (int i = 1; i < 5; i++) {
      for (int bInd = 0; bInd < 8; bInd++) {
        bool bit = (((Key[i]) & (0x1 << (7 - bInd))) != 0);  //получаем бит
        SendBitCyfral(bit); //отправляем бит
      }
    }
  }

  void Emulate(byte Key[8]) {
    pinMode(EmulatePort, OUTPUT);
    for (int i = 0; i < 10; i++) {
      SendKey(Key); //просто 10 раз посылаем ключ
    }
  }

Но доступный мне для проверки домофон Cyfral CCD 2094.1 на эту эмуляцию так и не реагирует :(

Еще, из интересного, нашлись способы перекодирования этих ключей в формат dallas. У метакома все просто (у меня вышло почти с первого раза): ключ переписывается в прямом или обратном порядке (зависит от домофона) и считывается контрольная сумма. У Cyfral несколько вариантов перекодировок (по сути, различная запись ниблов, иногда с ошибками): три я реализовал, но проверить ещё не удалось, поэтому про эту тему в следующий раз.

P.S. Тем временем в устройстве появилось многостраничное меню, вывод напряжения батарей(скоро будет просто индикатор заряда) и меню для ввода ключей вручную.

Уже меньше брелка от машины, но будет ещё компактнее :)
Уже меньше брелка от машины, но будет ещё компактнее :)

Комментарии (14)


  1. Redduck119
    24.04.2024 10:04
    +1

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


    1. EnvalidGamer Автор
      24.04.2024 10:04

      Сильно компактнее наверное не выйдет, дальше упираемся в размер RFID антенны.


      1. GoooodBoy
        24.04.2024 10:04

        По размеру уже приемлемо. Жду релиз или краудфандинг. С удовольствием куплю такую штуку. Надоело таскать с собой ворох ключей и вспоминать какой от чего)


      1. VirtualVoid
        24.04.2024 10:04

        Возможно антенну можно сделать чуть более компактной, проводом поменьше?
        А так, я очень жду вашу статейку о реализации LF RFID.
        p.s Запись на T55xx не реализовывали?


        1. EnvalidGamer Автор
          24.04.2024 10:04

          Запись на T5557 есть, но пока не всегда стабильно работает.
          Пробовали взять антенну с ключа (как раз с T5557), работало не стабильно. Но ещё в эту сторону экспериментируем.
          Эмуляция, кстати, хорошо работает)


    1. gmastrum
      24.04.2024 10:04

      Если это "брелок" то размеры норм. Можно даже больше его сделать. Если только чуть тоньше, тут вы правы. Ну и естественно избавиться от острых граней на корпусе.

      Если это "прибор" то волне до сигаретной пачки можно увеличивать.

      Антену на mifare делать как часть печатной платы, антена em-marin в разных устройствах как круглая катушка 27мм диаметром. Куда уж меньше


  1. app-z
    24.04.2024 10:04

    Шикарно!


  1. sdore
    24.04.2024 10:04
    +2

    Конечно, лучше было так:

    uint16_t analogReadFast() {
      uint16_t res;
      system_adc_read_fast(&res, 1, 10);
      return res;
    }
    

    и ничего костыльного здесь нет.


  1. squeezy1332
    24.04.2024 10:04
    +1

    Я в последнее время тоже занимался этими ключами, нужно было сниффер сделать при чем максимально быстрый, чтобы считывать ключ сразу, а не вычислять периоды и тд. Темболее по крайне мере на моих униках периоды 0 и 1 отличались на 20мкс, а разница напряжений там 0.8-1в так что можно просто использовать компаратор при чем я делал еще фильтр антидребезга получалось все очень четко даже когда уровень не стабильный в начале


    1. EnvalidGamer Автор
      24.04.2024 10:04

      Обычно на компараторе это и реализовывают, но при наличии АЦП мне показалось логичнее решить это при помощи него, вместо отдельной микросхемы (у esp8266 встроенного компаратора нет)
      Насколько быстрый сниффер нужно было сделать?
      Просто вычисление периода происходит один раз перед считыванием, и почти не занимает времени. Зато можно читать ключи при разном, или не стабильном напряжении :)


  1. Savagesin
    24.04.2024 10:04

    Отлично! Я сотрудник охранной организации и с интересом слежу за подобного рода разработками.

    Мне как сервиснику интересно именно эмуляция всего спектра ТМ. Крайне полезен как ручной ввод кода, так и считывание с распознаванием типа (dal, meta, cyfr, mif и тд). Неплохо бы иметь возможность метить образы ключей произвольным словом (хоть цифры, хоть циробуквы), но если нет, то и ладно - порядковый номер и норм. Если все это будет хранится на sd карте шик-блеск. И мне как раз глубоко сиренево на размер. Я уже вижу, что на вряд ли устройство будет больше пачки сигарет.


    1. EnvalidGamer Автор
      24.04.2024 10:04

      Пока планируем хранить ключи во flash памяти esp32, и реализовать подключение к ней, как к flash накопителю. Но если не выйдет, будет sd карта.
      Метить ключи можно будет либо в файле с ключами, либо в самом устройстве. Но пока не понятно, как сделать удобный ввод текста через 3 кнопки :)
      Пока просто порядковые номера


      1. gmastrum
        24.04.2024 10:04

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

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

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


  1. gmastrum
    24.04.2024 10:04

    Просто оставлю это тут.

    Китайский клон хамелеона, 2.5к на озоне

    Читает и эмулирует карты нч. Mifare, вч em-marine. Пишет mifare zero, mifare.

    Рулиться со смартфона через голобозуб, имеет 8 слотов под ключи. В слот позволяет пихнуть одновременно вч и нч метку что очень удобно (к примеру ключ дом ру, который бевард, на карте мифаре 1к и карту от работы которая em-marin) . В автономном режиме умеет переключать слоты кнопочками, включаться автоматически при поднесении к считывателю и отдавать последний используемый слот.

    Зарядка тупец, акум на полгода.