В интернете можно найти достаточно готовых Arduino-проектов устройств для копирования домофонных ключей DS1990, в народе так же известных как ibutton, или просто «таблетки». Но практически всё проекты, что я нашёл, оказались несколько сыроваты. Поэтому решено было, используя уже имеющиеся наработки, сделать свой проект, полностью законченный и достаточно функциональный. За основу была взята эта статья, а так же, некоторые идеи были почерпнуты отсюда, поэтому на авторство сильно не претендую.
Аппаратная часть
Данное устройство я собрал на Arduino Nano (его китайском аналоге) в корпусе от нерабочего USB-хаба. Получилось достаточно компактно и удобно:
USB-кабель используется как для питания устройства, так и для взаимодействия с COM-терминалом. На корпусе установлена нефиксируемая кнопка для переключения режимов чтение/запись/восстановление, светодиод для индикации текущего режима, и контактная площадка (считыватель) для ключей.
Схема устройства простейшая. Линия данных 1wire (ibutton) обязательно подтягивается к питанию (+5В) через резистор 2,2 кОм (скорее всего, будет нормально работать и с другими номиналами порядка 1…4,7 кОм). Светодиод подключаем через ограничительный резистор подходящего номинала. В скетче используем вход кнопки (D2) со встроенным подтягивающим (PULLUP) резистором, соответственно кнопку сажаем на GND.
Програмная часть
В прошивке практически без изменений оставил только часть кода, отвечающую непосредственно за чтение/запись данных в ключи по протоколу 1-wire и, собственно, принцип работы устройства (дабы не изобретать велосипеды). Однако, остальная программная часть была фактически полностью переписана и при этом дополнена разными полезными функциями. В скетче используется стандартная Ардуино-библиотека «OneWire». Актуальную версию скетча смотрите в конце статьи.
Возможности устройства:
- Чтение ID ключа с последующей записью в перезаписываемую «болванку» (RW1990);
- Запись «универсального» ID, заранее заданного в скетче (в данном случае используется ID: 01:FF:FF:FF:FF:FF:FF:2F );
- Защита от случайной записи некорректного значения ID;
- Восстановление нечитаемых ключей, случайно испорченных при неудачной записи;
- Ввод ID вручную в терминале;
- Возможность работы без COM-терминала (при наличии источника питания постоянного тока на 5В с USB разъёмом).
Работа с устройством
Устройство можно использовать как с COM-терминалом (предпочтительно), так и без него. Для работы с терминалом на компьютере должны быть установлены драйверы для платы Arduino (FTDI, CH341 или др., в зависимости от того, какой чип установлен на Вашей плате). Если плата удачно программируется в среде разработки Arduino-IDE, значит нужный драйвер в системе уже установлен. Терминал COM-порта можно использовать любой, какой больше нравится (например, монитор порта среды Arduino-IDE или Гипертерминал. Лично я использую PuTTY). В настройках терминала нужно выбрать виртуальный COM-порт, под которым определилась наша плата, и скорость обмена, выставленную в скетче (в моём случае — 115200).
Итак, подключаем устройство к компьютеру и запускаем терминал COM-порта (Arduino при этом автоматически перезагружается). Светодиод несколько раз мигает в процессе загрузки. Через пару секунд устройство готово к работе, светодиод при этом гаснет, а в терминале выводится сообщение о готовности системы.
Список доступных команд через терминал:
- d — загрузка в буфер «универсального» ключа (в данном случае: 01:FF:FF:FF:FF:FF:FF:2F);
- w — переключение режима чтение/запись;
- m — переход в режим ручного ввода ID;
- r — переход в режим восстановления нечитаемых ключей;
- h — показать справку по командам.
Чтобы считать ID ключа, прикладываем ключ к контактной площадке. Светодиод при этом начинает часто моргать, а в терминале отображается считанный ID, который сохраняется в буфере (переменной oldID ) до тех пор, пока в неё не будет загружен другой ID. Идентификатор состоит из восьми бит, которые отображаются в шестнадцатиричном виде: 01 XX XX XX XX XX XX YY. Здесь первый бит — это Family code, для ключей ibutton он всегда будет равен 1. Если считанный Family code будет отличаться от 1, в терминал будет выведено соответствующее предупреждение и записывать данный ID устройство откажется.
Следующие шесть бит — это, собственно, уникальный идентификатор ключа. А восьмой бит — это, так называемый, «избыточный код» CRC или, другими словами, контрольная сумма, вычисляемая по специальному алгоритму из предыдущих семи бит. Контрольная сумма проверяется, и если её значение не верно, то, опять же, в терминал выводится предупреждение об этом и запись такого ID будет невозможна.
Чтобы записать ID в перезаписываемый ключ, в терминале нужно отправить символ «w», либо нажать кнопку на устройстве. При этом зажигается светодиод, что говорит о готовности устройства к записи. Прикладываем записываемый ключ к контактной площадке: светодиод при этом гаснет, а примерно через секунду начинает часто моргать, что говорит о завершении процесса записи и переключении устойства обратно в режим чтения. Если в терминале мы видим, что считывается только что записанный ID, без каких либо предупреждений, значит всё прошло успешно. С таким же успехом можно копировать ключи и без использования терминала, наблюдая только за светодиодом.
Если записываемый ID был некорректный (с неверным Family code, или CRC), в терминал выведется соответствующее сообщение и запись будет отменена. Таким образом, устройство предохраняняет ключ от записи в него некорректных данных. Тем не менее, всё же может случиться так, что данные запишутся с ошибками. Такое может произойти, например, если ключ будет недостаточно плотно приложен к контактной площадке при записи данных. Более того, ключ в этом случае может вовсе перестать читаться некоторыми устройствами (включая данный дубликатор), если в первый бит будет записано нулевое значение. А при эксперементах с ключами такое частенько бывает, особенно по неопытности! Так что, незнавши можно и подумать, что ключик умер.
Для восстановления нечитаемого ключа необходимо перевести устройство в соответствующий режим. Для этого в терминале нужно отправить символ «r», либо включить устройство, удерживая кнопку нажатой. Светодиод начнёт часто моргать, независимо от того, приложен ключ, или нет (для выхода из режима восстановления отправьте символ «r» снова, или переподключите устройство).
Прикладываем ключ к контактной площадке. Если он читаемый, то его ID считается так же, как и в обычном режиме чтения. Если же ключ не читается, соответственно ничего не произойдёт. Удерживая ключ приложенным, нажимаем кнопку. При этом в ключ принудительно будет записан «универсальный» ID, прописанный в скетче (у меня: 01:FF:FF:FF:FF:FF:FF:2F) либо другой, ранее загруженный в буфер. По окончанию записи устройство вернётся в обычный режим чтения.
Загрузить в буфер универсальный ID можно, отправив в терминале символ «d», либо просто перезагрузив устройство (он используется по умолчанию, если не был введён какой-либо другой ID).
Кроме всего прочего, в устройстве предусмотрена возможность сделать дубликат ключа, даже не имея под рукой оригинала! Достаточно лишь знать его идентификатор.
Переводим устройство в режим ручного ввода ID. Для этого в терминале нужно отправить символ «m». После этого появляется приглашение ввести ID, либо выйти из ручного режима (по нажатию Esc). После этого можно ввести любой ID (в шестнадцатиричном виде). При этом первый бит (Family code) всегда должен быть равным 1 и программа подставляет его автоматически, а так же, автоматически вычисляет CRC и подставляет в восьмой бит. Таким образом, нам нет необходимости вводить Family code и вычислять контрольную сумму CRC, достаточно ввести только значения битов со второго по седьмой.
В итоге мы имеем вполне функциональное, полезное в хозяйстве устройство за смешные дениги. Теперь скопировать на скорую руку ключик на дежурную болванку можно где угодно, при наличии источника питания на 5В постоянного тока с USB разъёмом!
#include <OneWire.h>
const int switchPin = 2; // Будем использовать аппаратное прерывание INT0, поэтому кнопка должна быть подключена к 2-му пину
const int dataPin = 12; // Data пин считывателя ключей
const int ledPin = 13; // Пин контрольного светодиода
volatile boolean writeMode = false; // Режим записи: 1 - включен; 0 - выключен (режим чтения)
boolean recoveryMode = false; // Режим восстановления нечитаемых ключей (с записанным по ошибке нулевым первым байтом)
byte oldID[8]; // Считанный ID ключа
byte newID[8]; // Записываемый ID ключа
const byte defaultID[8] = { 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F }; // По умолчанию прошивается "Универсальный" ID: 01:FF:FF:FF:FF:FF:FF:2F
byte crc; // Контрольная сумма CRC
OneWire ibutton (dataPin);
void setup() {
Serial.begin(115200);
loadDefaultID();
pinMode(ledPin, OUTPUT);
pinMode(switchPin, INPUT_PULLUP);
// При включении устройства, удерживая кнопку нажатой, активируется режим восстановления
if (digitalRead(switchPin) == LOW) recoveryMode = true;
attachInterrupt(0, int0, LOW); // при нажитии кнопки срабатывает 0-е прерывание, обработчик прерывания (ISR) - функция int0()
Serial.println("Device is ready. Send 'h' for help.");
}
// Загрузка дефолтного "универсального" ID
void loadDefaultID() {
for (byte x = 0; x < 8; x++) oldID[x] = defaultID[x];
}
// Переключение режима: Чтение/Запись
void changeMode () {
// Перестраховка от записи некорректного ID
if (!writeMode) {
crc = ibutton.crc8(newID, 7); // Вычисление контрольной суммы записываемого ID
if (newID[0] != 1 || newID[7] != crc) {
Serial.println(F("ID is incorrect! Writing is not permitted."));
writeModeOff();
return;
}
}
writeMode = !writeMode;
digitalWrite(ledPin, writeMode);
if (writeMode) {
Serial.print(F("Waiting for the key to WRITE the new ID: "));
for (byte x = 0; x < 8; x++) {
Serial.print(newID[x], HEX);
Serial.print(' ');
}
Serial.println(" ...");
}
else {
writeModeOff();
}
}
// Автоматическое отключение режима восстановления после записи и вывод приглашения считать новый ключ
void writeModeOff() {
if (recoveryMode) {
recoveryMode = false;
Serial.println(F("Recovery mode disabled."));
}
Serial.println(F("Waiting for the key to READ the ID..."));
}
// Обработчик прерывания по нажатию кнопки: переключает режим: Чтение/Запись (отфильтровывая дребезг контактов)
void int0() {
static unsigned long millis_prev;
if ( millis() - millis_prev > 100 ) changeMode();
millis_prev = millis();
}
// Вывод считанного ID в терминал
void printID() {
for (byte x = 0; x < 8; x++) {
Serial.print(oldID[x], HEX);
Serial.print(" ");
}
crc = ibutton.crc8(oldID, 7); // Вычисление контрольной суммы считанного ID
Serial.print(" CRC: ");
Serial.print(crc, HEX);
if (oldID[0] != 0x01) Serial.print(F(" Family code is not valid!"));
if (crc != oldID[7]) Serial.print(F(" CRC is not valid!"));
Serial.println();
}
void loop() {
// Обработка команд, посылаемых через терминал COM-порта
if (Serial.available() > 0) {
byte com; // Команда, отправляемая через терминал COM-порта
com = Serial.read();
switch ( com ) {
case 'h': {
Serial.println(F("Help:"));
Serial.println(F("d - load default ID"));
Serial.println(F("w - switch read/write mode"));
Serial.println(F("m - enter ID manually"));
Serial.println(F("r - enable recovery mode (send 'r' again to disable)"));
Serial.println(F("h - show this help"));
break;
}
case 'd': {
if (writeMode) {
writeMode = false;
digitalWrite(ledPin, LOW);
}
loadDefaultID();
Serial.println(F("Default ID is loaded."));
printID();
break;
}
case 'w': {
changeMode();
break;
}
case 'r': {
writeMode = false;
recoveryMode = !recoveryMode;
Serial.println(recoveryMode ? F("Recovery mode enabled.") : F("Recovery mode disabled."));
break;
}
case 'm': {
byte inputID[8]; // Введённый вручную ID ключа
char inputChar; // Код введённого символа
char inputNum = 2; // Порядковый номер вводимого сивмола (от 0 до 15). Начинаем вводить со 2-го символа, т.к. 0-ой и 1-ый - фиксированные.
char charEncode; // 16-ричное число (от 0 до F), в которое преобразуется каждый вводимый ASCII символ
boolean even = 0; // Признак чётности порядкового номера вводимого символа
Serial.println(F("Enter the new ID, or press 'Esc' to cancel."));
inputID[0] = 1;
Serial.print(F("The new ID is: 01 "));
while (inputNum < 14) {
if (Serial.available() > 0) {
inputChar = Serial.read();
if (inputChar == 27) {
Serial.flush();
Serial.println();
Serial.print(F("Canceled..."));
break;
}
else {
if ( inputChar >= 48 && inputChar <= 57 ) charEncode = inputChar - 48;
else if ( inputChar >= 65 && inputChar <= 70 ) charEncode = inputChar - 55;
else if ( inputChar >= 97 && inputChar <= 102 ) charEncode = inputChar - 87;
else inputNum = -1;
if ( inputNum != -1 ) {
Serial.write(inputChar);
if (!even) inputID[inputNum/2] = charEncode << 4;
else {
inputID[inputNum/2] = inputID[inputNum/2] + charEncode;
Serial.print(' ');
}
even = !even;
inputNum++;
}
}
}
}
if (inputNum == 14) {
inputID[7] = ibutton.crc8(inputID, 7); // Автоматическое вычисление контрольной суммы введённого ID
for (byte i=0; i<8; i++) oldID[i] = inputID[i];
}
Serial.println(oldID[7], HEX);
printID();
break;
}
}
}
for (byte x = 0; x < 8; x++) newID[x] = oldID[x];
// Проверяем, приложен ли ключ
if (!ibutton.search (oldID)) {
ibutton.reset_search();
delay(50);
if (!recoveryMode) return;
}
// Режим чтения
if (!writeMode) {
digitalWrite(ledPin, HIGH);
delay(50);
printID();
digitalWrite(ledPin, LOW);
}
// Режим записи
if (writeMode) {
delay(200);
digitalWrite(ledPin, LOW);
ibuttonCommand(0x33, 1, 1);
Serial.print("Old ID: ");
for (byte x = 0; x < 8; x++) {
Serial.print(ibutton.read(), HEX);
Serial.print(' ');
}
ibuttonCommand(0xD1, 1, 1);
// устанавливаем на линии логический 0
digitalWrite(dataPin, LOW); pinMode(dataPin, OUTPUT); delayMicroseconds(60);
pinMode(dataPin, INPUT); digitalWrite(dataPin, HIGH); delay(10);
Serial.print(" New ID: ");
for (byte x = 0; x < 8; x++) {
Serial.print(newID[x], HEX);
Serial.print(' ');
}
ibuttonCommand(0xD5, 1, 1);
Serial.print("Writing... ");
for (byte x = 0; x < 8; x++) {
writeByte(newID[x]);
Serial.print('*');
}
Serial.println(F(" OK!"));
ibuttonCommand(0xD1, 0, 1);
// устанавливаем на линии логическую 1
digitalWrite(dataPin, LOW); pinMode(dataPin, OUTPUT); delayMicroseconds(10);
pinMode(dataPin, INPUT); digitalWrite(dataPin, HIGH); delay(10);
changeMode();
}
}
// Отправка команды iButton
void ibuttonCommand(uint8_t command, boolean sk, boolean rs) {
if (sk) ibutton.skip();
if (rs) ibutton.reset();
ibutton.write(command);
}
// Побайтовая запись нового ID
void writeByte(byte data) {
for (int data_bit = 0; data_bit < 8; data_bit++) {
digitalWrite(dataPin, LOW); pinMode(dataPin, OUTPUT);
if (data & 1) delayMicroseconds(60);
pinMode(dataPin, INPUT); digitalWrite(dataPin, HIGH);
delay(10);
data = data >> 1;
}
}
Возможно, что прошивка устройства будет и дальше дорабатываться. Если Вы заметили какие-то баги и недоработки в программе, конструктивная критика всегда приветствуется!
Сергей говорит:
Использовал нонейм uno r3. С болванками с гравировкой RW1990 никаких проблем. Болванки без всяких надписей читает, но не пишет. Уменьшил резистор до 1к, не помогло, уменьшил до 470 Ом и все болванки успешно прописались.