Калитка на сканере отпечатков пальцев CROW R503
- Войдите на сайт для отправки комментариев
Заняться этой проблемой меня сподвигло 2 не связанных друг с другом событий, во первых на работе при решение весьма конкретных задач всерьез обсуждали некую автоматизацию с использованием биометрического распознавания, вариантов было много, но все или слишком дорогие или нам не подходили.
Ну а второе событие - меня обокрали :) сперли с участка велосипед, и хоть злодеев нашли и велосипед вернули но я стал укреплять передний край обороны от злодеев (датчики, камеры и т.д.).
И вот я добрался до калитки, стал думать как сделать и надежно и безопасно, сначала хотел купить датчик "таблетка", но потом понял, что это не удобно, и потерять можно и забыть дома (дети постоянно бегают то туда то сюда).
От сюда возникло желание сделать так, что бы нельзя было ключ украсть или потерять. Но по сколько мне было интересно разобраться и я решить сразу и вопрос который возник на работе, то я заказал датчик без платы управления.
Начал я с библиотеки Adafruit_Fingerprint, но очень быстро понял, что она во первых сильно кривая, а во вторых в ней банально нет поддержки части нужных команд датчика R503.
В результате я просто частично ее использовал в виде вставок в код.
На момент публикации у меня все работает в домашних условия (платы спаяны, код написан). Но поставить на улицу пока не могу (погода плохая), буду ждать выходных.
теперь ТТХ того, что получилось:
1. После обучения, распознает палец почти со 100% вероятностью, не срабатывает примерно 1 раз на 50 попыток. Чужие пальцы ни разу не привели к открытию (проверял на всей семье).
2. Время распознавания около 1 сек
3. Обучение на 1 палец занимает 2...3 минуты (необходимо создать 3 модели)
В 1 посте выложу код и схему (что бы можно было править)
схема
файл R503.ino
файл A_Config.h
файл GROW_R503.cpp
/*************************************************** Объект для работы со сканером отпечатка пальцев GROW R503 Создан на основе библиотеки Adafruit_Fingerprint ****************************************************/ #include "GROW_R503.h" #ifdef __AVR__ #include #include #endif //----------------------------------------------------------- //#define DEBUG_R503 // вывод сообщений модуля R503, в продакшене закоментить //#define DEBUG_R503_READ_ECHO // вывод данных получаемых от модуля R503, в продакшене закоментить #ifdef DEBUG_R503_READ_ECHO uint8_t Debug_Read_Echo_Packet[] = {0x05,0x06,0x1f,0x0D}; // номера пакетов которые нужно выводить #endif // ----------------------------------------------------------- #ifdef __AVR__ Finger_R503::Finger_R503(int wPin, int pPin, SoftwareSerial *ss) { thePassword = 0; theAddress = 0xFFFFFFFF; hwSerial = NULL; swSerial = ss; mySerial = swSerial; wakerPin = wPin; powerPin = pPin; } #endif Finger_R503::Finger_R503(int wPin, int pPin, HardwareSerial *ss) { thePassword = 0; theAddress = 0xFFFFFFFF; #ifdef __AVR__ swSerial = NULL; #endif hwSerial = ss; mySerial = hwSerial; wakerPin = wPin; powerPin = pPin; } void Finger_R503::RunStep(unsigned long time_loop ) { boolean TecFirstStep = FirstStep; FirstStep = false; if (stepMode == 0) { if (mode == FINGER_MODE_SLEEP) { set_stepMode (0); } else if (mode == FINGER_MODE_SCAN) { set_stepMode (1); } else if (mode == FINGER_MODE_ADD) { set_stepMode (20); } else if (mode == FINGER_MODE_ADD_A) { set_stepMode (10); } else if (mode == FINGER_MODE_CLEAR) { set_stepMode (90); } else if (mode == FINGER_MODE_CLEAR_A) { set_stepMode (90); } else { set_stepMode (0); } return; } // ------------------------------------------------------------------------------------ if (stepMode == 1) { if (verifyPassword()) { if (TecFirstStep) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_BLUE, 0); if (getImage() == FINGER_OK) set_stepMode (2); } else FirstStep = TecFirstStep; return; } if (stepMode == 2) { if (verifyPassword()) { if (image2Tz(1) == FINGER_OK) set_stepMode (3); else set_stepMode (1); time_loop_step = time_loop; } return; } if (stepMode == 3) { if (verifyPassword()) { if (Search(0x01) == FINGER_OK) set_stepMode (100); else set_stepMode (101); time_loop_step = time_loop; } return; } if (stepMode == 100) { // есть выход по тайму // это событие "скан сработал" if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_PURLE, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_stepMode (0); time_loop_step = time_loop; } return; } if (stepMode == 101) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_RED, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_stepMode (0); time_loop_step = time_loop; } return; } // ------------------------------------------------------------------------------------ if (stepMode == 10) { if (verifyPassword()) { leteNum = GetFreeIndex(0); if (leteNum == 0xff) {} // ошибка, пробуем еще else if (leteNum >= 199) {} // нет места в библиотеке, никуда не уходим, ждем смены режима на сон или на очистку else if (leteNum == 0) { // отпечатков в базе нет, переходим к добавлению set_stepMode (20); Num = 1; time_loop_step = time_loop; } else if (mode == FINGER_MODE_ADD) { // отпечаток был предоставлен ранее set_stepMode (20); Num = 1; time_loop_step = time_loop; } else { // есть отпечатки, требуется подтверждение set_stepMode (12); time_loop_step = time_loop; } } return; } if (stepMode == 12) { if (verifyPassword()) { if (TecFirstStep) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_BLUE, 0); if (getImage() == FINGER_OK) { set_stepMode (13); time_loop_step = time_loop; } } else FirstStep = TecFirstStep; return; } if (stepMode == 13) { if (verifyPassword()) { if (image2Tz(1) == FINGER_OK) set_stepMode (14); else set_stepMode (11); time_loop_step = time_loop; } return; } if (stepMode == 14) { if (verifyPassword()) { if (Search(0x01) == FINGER_OK) { set_stepMode (102); Num = 1; } else set_stepMode (103); time_loop_step = time_loop; } return; } if (stepMode == 102) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_PURLE, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_mode(FINGER_MODE_ADD); Num = 1; time_loop_step = time_loop; } return; } if (stepMode == 103) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_RED, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_stepMode (10); time_loop_step = time_loop; } return; } // ------------------------------------------------------------------------------------ if (stepMode == 20) { if (verifyPassword()) { if (TecFirstStep) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_PURLE, 0); if (getImage() == FINGER_OK) { set_stepMode (21); time_loop_step = time_loop; } } else { FirstStep = TecFirstStep; } return; } if (stepMode == 21) { if (verifyPassword()) { if (image2Tz(Num) == FINGER_OK) set_stepMode (22); else set_stepMode (20); time_loop_step = time_loop; } return; } if (stepMode == 22) { if (verifyPassword()) { if (Search(Num) != FINGER_OK) set_stepMode (104); else set_stepMode (105); time_loop_step = time_loop; } return; } if (stepMode == 104) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) { LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_BLUE, 0); time_loop_step = time_loop; Num = Num + 1; } else FirstStep = TecFirstStep; } if (getDelayTime(time_loop_step, time_loop) > 2000) { if (Num > 2) set_stepMode (60); else set_stepMode (20); time_loop_step = time_loop; } return; } if (stepMode == 105) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_RED, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_stepMode (20); Num = 1; time_loop_step = time_loop; } return; } // ------------------------------------------------------------------------------------ if (stepMode == 60) { if (verifyPassword()) { if (TecFirstStep) { LedConfig(FINGER_LED_1, 0, FINGER_COLOR_PURLE, 0); if (RegModel() == FINGER_OK) set_stepMode (61); else set_stepMode (106); time_loop_step = time_loop; } else FirstStep = TecFirstStep; } return; } if (stepMode == 61) { if (verifyPassword()) { if (store(leteNum) == FINGER_OK) { set_mode(FINGER_MODE_ADD); Num = 1; set_stepMode (10); #ifdef DEBUG_R503 Serial.println("ADD SCAN BASE"); #endif } else set_stepMode (106); time_loop_step = time_loop; } return; } if (stepMode == 106) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_RED, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_mode(FINGER_MODE_ADD); set_stepMode (10); Num = 1; time_loop_step = time_loop; } return; } //---------------------------------------------------------------------------------------------- if (stepMode == 90) { if (verifyPassword()) { if (TecFirstStep) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_BLUE, 0); if (getImage() == FINGER_OK) { set_stepMode (91); time_loop_step = time_loop; } } else FirstStep = TecFirstStep; return; } if (stepMode == 91) { if (verifyPassword()) { if (image2Tz(1) == FINGER_OK) set_stepMode (92); else set_stepMode (90); time_loop_step = time_loop; } return; } if (stepMode == 92) { if (verifyPassword()) { if (Search(0x01) == FINGER_OK) set_stepMode (93); else set_stepMode (109); time_loop_step = time_loop; } return; } if (stepMode == 93) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_PURLE, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_stepMode (94); time_loop_step = time_loop; } return; } if (stepMode == 94) { if (verifyPassword()) { if (TecFirstStep) LedConfig(FINGER_LED_1, 0, FINGER_COLOR_RED, 0); if (getImage() == FINGER_OK) { set_stepMode (95); time_loop_step = time_loop; } } else FirstStep = TecFirstStep; return; } if (stepMode == 95) { if (verifyPassword()) { if (image2Tz(1) == FINGER_OK) set_stepMode (96); else set_stepMode (94); time_loop_step = time_loop; } return; } if (stepMode == 96) { if (verifyPassword()) { if (Search(0x01) == FINGER_OK) { emptyDatabase(); set_mode(FINGER_MODE_SLEEP); } else set_stepMode (109); time_loop_step = time_loop; } return; } if (stepMode == 109) { // есть выход по тайму if (TecFirstStep) { if (verifyPassword()) LedConfig(FINGER_LED_ON, 0, FINGER_COLOR_RED, 0); else FirstStep = TecFirstStep; time_loop_step = time_loop; } if (getDelayTime(time_loop_step, time_loop) > 2000) { set_stepMode (90); time_loop_step = time_loop; } return; } } // ----------------------------------------------------------- void Finger_R503::poll(unsigned long time_loop) { boolean waker = !digitalRead(wakerPin); if (waker) { if (mode == FINGER_MODE_SLEEP ) { set_mode (FINGER_MODE_SCAN); } time_loop_mode = time_loop; } else { if (mode != FINGER_MODE_SLEEP ) { if (getDelayTime(time_loop_mode, time_loop) > TIME_RUN) { set_mode (FINGER_MODE_SLEEP); } } } RunStep(time_loop ); } // ----------------------------------------------------------- boolean Finger_R503::LedConfig(uint8_t Ctrl, uint8_t Speed, uint8_t Color, uint8_t Count) { uint8_t packet[] = {0x35, Ctrl, Speed, Color, Count }; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len == 1) && (packetR[0] == FINGER_ACKPACKET) && (packetR[1] == FINGER_OK)) return true; return false; } // ----------------------------------------------------------- boolean Finger_R503::HandShake(void) { uint8_t packet[] = {0x40}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len == 1) && (packetR[0] == FINGER_ACKPACKET) && (packetR[1] == FINGER_OK)) return true; return false; } // ----------------------------------------------------------- uint8_t Finger_R503::TempleteNum(void) { uint8_t packet[] = {0x1d}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len != 1) && (packetR[0] != FINGER_ACKPACKET)) { return 0; } return packetR[1]; } // ----------------------------------------------------------- uint8_t Finger_R503::GetFreeIndex(uint8_t page = 0) { uint8_t packet[] = {0x1f, page}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[35]; uint8_t len = getReply(packetR, packet[0]); if ((len != 33) || (packetR[0] != FINGER_ACKPACKET) || (packetR[1] != FINGER_OK)) { return 0xff; } uint8_t r = 0; uint8_t ri = 0; for (int i = 2; i <= 33; i++) { ri = packetR[i]; for (int ii = 0; ii <= 7; ii++) { if (bitRead(ri, ii) == 0) { return r; } r++; } } return 0; } // ----------------------------------------------------------- uint8_t Finger_R503::getImage(void) { uint8_t packet[] = {0x01}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len != 1) || (packetR[0] != FINGER_ACKPACKET)) { return 100; } return packetR[1]; } // ----------------------------------------------------------- uint8_t Finger_R503::image2Tz(uint8_t slot) { // нумерация 1...6 uint8_t packet[] = {0x02, slot}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len != 1) || (packetR[0] != FINGER_ACKPACKET)) { return 100; } return packetR[1]; } // ----------------------------------------------------------- uint16_t Finger_R503::Search(uint8_t slot) { uint16_t StartPage = 0x0000; uint16_t PageNum = 0x00c8; uint8_t packet[] = {0x04, slot, (StartPage >> 8), StartPage, (PageNum >> 8), PageNum }; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[6]; uint8_t len = getReply(packetR, packet[0]); if ((len != 5) || (packetR[0] != FINGER_ACKPACKET)) { return -1; } // с 0 по 5 uint16_t fingerID = 0xFFFF; fingerID = packetR[2]; fingerID <<= 8; fingerID |= packetR[3]; return packetR[1]; } // ----------------------------------------------------------- uint8_t Finger_R503::RegModel(void) { uint8_t packet[] = {0x05}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len != 1) || (packetR[0] != FINGER_ACKPACKET)) return -1; return packetR[1]; } // ----------------------------------------------------------- uint8_t Finger_R503::store(uint16_t id) { uint8_t packet[] = {0x06, 0x01, id >> 8, id & 0xFF}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len != 1) || (packetR[0] != FINGER_ACKPACKET)) return -1; return packetR[1]; } // ----------------------------------------------------------- boolean Finger_R503::verifyPassword(void) { uint8_t packet[] = {FINGER_VERIFYPASSWORD, (thePassword >> 24), (thePassword >> 16), (thePassword >> 8), thePassword }; writePacket(theAddress, FINGER_COMMANDPACKET, 7, packet); uint8_t packetR[2]; uint8_t len = getReply(packetR, packet[0]); if ((len == 1) && (packetR[0] == FINGER_ACKPACKET) && (packetR[1] == FINGER_OK)) return true; return false; } // ----------------------------------------------------------- uint8_t Finger_R503::emptyDatabase(void) { uint8_t packet[] = {0x0D}; writePacket(theAddress, FINGER_COMMANDPACKET, sizeof(packet) + 2, packet); uint8_t len = getReply(packet, packet[0]); if ((len != 1) || (packet[0] != FINGER_ACKPACKET)) return -1; return packet[1]; } void Finger_R503::writePacket(uint32_t addr, uint8_t packettype, uint16_t len, uint8_t *packet) { while (mySerial->available()) { mySerial->read(); } #if ARDUINO >= 100 mySerial->write((uint8_t)(FINGER_STARTCODE >> 8)); mySerial->write((uint8_t)FINGER_STARTCODE); mySerial->write((uint8_t)(addr >> 24)); mySerial->write((uint8_t)(addr >> 16)); mySerial->write((uint8_t)(addr >> 8)); mySerial->write((uint8_t)(addr)); mySerial->write((uint8_t)packettype); mySerial->write((uint8_t)(len >> 8)); mySerial->write((uint8_t)(len)); #else mySerial->print((uint8_t)(FINGER_STARTCODE >> 8), BYTE); mySerial->print((uint8_t)FINGER_STARTCODE, BYTE); mySerial->print((uint8_t)(addr >> 24), BYTE); mySerial->print((uint8_t)(addr >> 16), BYTE); mySerial->print((uint8_t)(addr >> 8), BYTE); mySerial->print((uint8_t)(addr), BYTE); mySerial->print((uint8_t)packettype, BYTE); mySerial->print((uint8_t)(len >> 8), BYTE); mySerial->print((uint8_t)(len), BYTE); #endif uint16_t sum = (len >> 8) + (len & 0xFF) + packettype; for (uint8_t i = 0; i < len - 2; i++) { #if ARDUINO >= 100 mySerial->write((uint8_t)(packet[i])); #else mySerial->print((uint8_t)(packet[i]), BYTE); #endif sum += packet[i]; } #if ARDUINO >= 100 mySerial->write((uint8_t)(sum >> 8)); mySerial->write((uint8_t)sum); #else mySerial->print((uint8_t)(sum >> 8), BYTE); mySerial->print((uint8_t)sum, BYTE); #endif } uint8_t Finger_R503::getReply(uint8_t packet[], uint8_t id_packet, uint16_t timeout) { uint8_t reply[58], idx; // uint8_t reply[42], idx; uint16_t timer = 0; idx = 0; #ifdef DEBUG_R503_READ_ECHO boolean Echo_ON = false; for (uint8_t i1 = 0; i1 < sizeof(Debug_Read_Echo_Packet); i1++) { if (Debug_Read_Echo_Packet[i1] == id_packet) {Echo_ON = true;} } if (Echo_ON) {Serial.print(" 0x"); Serial.print(String(id_packet, HEX));Serial.print(":<--- ");} #endif while (true) { if (idx >= 58) { #ifdef DEBUG_R503_READ_ECHO if (Echo_ON) {Serial.print(" Bad length-"); Serial.println(idx);} #endif return FINGER_BADPACKET; } while (!mySerial->available()) { delay(1); timer++; if (timer >= timeout) return FINGER_TIMEOUT; } // something to read! reply[idx] = mySerial->read(); #ifdef DEBUG_R503_READ_ECHO if (Echo_ON) {Serial.print(" 0x"); Serial.print(reply[idx], HEX);} #endif if ((idx == 0) && (reply[0] != (FINGER_STARTCODE >> 8))) continue; idx++; // check packet! if (idx >= 9) { if ((reply[0] != (FINGER_STARTCODE >> 8)) || (reply[1] != (FINGER_STARTCODE & 0xFF))) return FINGER_BADPACKET; uint8_t packettype = reply[6]; //Serial.print("Packet type"); Serial.println(packettype); uint16_t len = reply[7]; len <<= 8; len |= reply[8]; len -= 2; //Serial.print("Packet len"); Serial.println(len); if (idx <= (len + 10)) continue; packet[0] = packettype; for (uint8_t i = 0; i < len; i++) { packet[1 + i] = reply[9 + i]; } #ifdef DEBUG_R503_READ_ECHO if (Echo_ON) {Serial.println();} #endif return len; } } } // ----------------------------------------------------------- void Finger_R503::powerOn (void) { digitalWrite(powerPin, HIGH); #ifdef DEBUG_R503 Serial.println("Power - On"); #endif begin(); if (!verifyPassword()) { #ifdef DEBUG_R503 Serial.println("Not sensor"); #endif powerOff(); // при неудачной попытке включения питания переходим в спящий режим } else { } } // ----------------------------------------------------------- void Finger_R503::powerOff (void) { set_mode (FINGER_MODE_SLEEP); // при выключении питания в любом случае переходим в спящий режим if (hwSerial) hwSerial->end(); #ifdef __AVR__ if (swSerial) swSerial->end(); #endif digitalWrite(powerPin, LOW); #ifdef DEBUG_R503 Serial.print(powerPin); Serial.print(">"); Serial.println("Power - Off"); #endif } // ----------------------------------------------------------- void Finger_R503::begin(void) { delay(TIME_PAUSE_P); // delay to let the sensor 'boot up' if (hwSerial) hwSerial->begin(baudrate, SERIAL_8N2); #ifdef __AVR__ if (swSerial) swSerial->begin(baudrate); #endif } // ----------------------------------------------------------- uint8_t Finger_R503::get_mode (void) { return mode; } // ----------------------------------------------------------- void Finger_R503::set_stepMode (int new_stepMode) { if (stepMode != new_stepMode) { FirstStep = true; #ifdef DEBUG_R503 Serial.print("step: "); Serial.print(stepMode); Serial.print(" > "); Serial.println(new_stepMode); #endif } stepMode = new_stepMode; } // ----------------------------------------------------------- int Finger_R503::get_stepMode (void){ return stepMode; } // ----------------------------------------------------------- void Finger_R503::set_mode (uint8_t newMode) { if (newMode == FINGER_MODE_SLEEP) { if (mode != FINGER_MODE_SLEEP) { mode = newMode; set_stepMode (0); FirstStep = true; #ifdef DEBUG_R503 Serial.println("SLEEP"); #endif powerOff(); } } else if (newMode == FINGER_MODE_ADD) { if (mode != FINGER_MODE_ADD) { if (mode == FINGER_MODE_SLEEP) powerOn(); mode = newMode; Num = 1; set_stepMode (0); FirstStep = true; time_loop_mode = millis(); #ifdef DEBUG_R503 Serial.println("ADD"); #endif } } else if (newMode == FINGER_MODE_ADD_A) { if (mode != FINGER_MODE_ADD_A) { if (mode == FINGER_MODE_SLEEP) powerOn(); mode = newMode; Num = 1; set_stepMode (0); FirstStep = true; time_loop_mode = millis(); #ifdef DEBUG_R503 Serial.println("ADD+"); #endif } } else if (newMode == FINGER_MODE_CLEAR) { if (mode != FINGER_MODE_CLEAR) { if (mode == FINGER_MODE_SLEEP) powerOn(); mode = newMode; set_stepMode (0); FirstStep = true; time_loop_mode = millis(); #ifdef DEBUG_R503 Serial.println("CLEAR"); #endif } } else if (newMode == FINGER_MODE_CLEAR_A) { if (mode != FINGER_MODE_CLEAR_A) { if (mode == FINGER_MODE_SLEEP) powerOn(); mode = newMode; set_stepMode (0); FirstStep = true; time_loop_mode = millis(); #ifdef DEBUG_R503 Serial.println("CLEAR+"); #endif } } else { if (mode != FINGER_MODE_SCAN) { if (mode == FINGER_MODE_SLEEP) powerOn(); mode = FINGER_MODE_SCAN; set_stepMode (0); FirstStep = true; time_loop_mode = millis(); #ifdef DEBUG_R503 Serial.println("SCAN"); #endif } } } // ----------------------------------------------------------- void Finger_R503::init (void) { pinMode(powerPin, OUTPUT); digitalWrite(powerPin, LOW); pinMode(wakerPin, INPUT); digitalWrite(powerPin, HIGH); }файл GROW_R503.h
/*************************************************** Объект для работы со сканером отпечатка пальцев GROW R503 Создан на основе библиотеки Adafruit_Fingerprint Общий алгоритм работы stepMode=0 - ничего не делается, состояние инициализации 1. Режим SCAN stepMode=1 - LedConfig, включить синий цвет, означает "готов" getImage, сканируем палец stepMode=2 - image2Tz, обрабатываем скан пальца stepMode=3 - Search, ищем отпечаток в базе stepMode=100 - LedConfig, включить фиолетовый цвет, означает "палец распознан" pause, ожидание для открытия замка, этот статус должен обрабатыватся в основном цикле stepMode=101 - LedConfig, включить красный цвет, означает "ошибка" pause, ожидание, ничего не происходит просто показываем красную подсветку 2. Режим ADD подтверждение режима (библиотека не пустая) stepMode=10 - TempleteNum, найдем номер в библиотеке stepMode=11 - LedConfig, включить моргание фиолетовым цветом, означает "готов" getImage, сканируем палец дяя подтверждения возможности добавления (только если библиотека не пустая) stepMode=12 - image2Tz, обрабатываем скан пальца stepMode=13 - Search, ищем отпечаток в базе stepMode=102 - LedConfig, включить красный цвет, означает "ошибка подтверждения" pause, ожидание, ничего не происходит просто показываем красную подсветку 3. Режим ADD ввод новых сканов в библиотеку // 1 stepMode=20 - LedConfig, включить фиолетовый цвет, означает "готов" getImage, сканируем палец stepMode=21 - image2Tz, обрабатываем скан пальца stepMode=22 - Search, ищем отпечаток в базе stepMode=103 - LedConfig, включить синий цвет, означает "вариант пальца принят" pause, ожидание, ничего не происходит просто показываем синюю подсветку stepMode=104 - LedConfig, включить красный цвет, означает "ошибка или палец уже в базе" pause, ожидание, ничего не происходит просто показываем красную подсветку // 2 stepMode=30 - LedConfig, включить фиолетовый цвет, означает "готов" getImage, сканируем палец stepMode=31 - image2Tz, обрабатываем скан пальца stepMode=32 - Search, ищем отпечаток в базе // 3 stepMode=40 - LedConfig, включить фиолетовый цвет, означает "готов" getImage, сканируем палец stepMode=41 - image2Tz, обрабатываем скан пальца stepMode=42 - Search, ищем отпечаток в базе // 4 stepMode=50 - LedConfig, включить фиолетовый цвет, означает "готов" getImage, сканируем палец stepMode=51 - image2Tz, обрабатываем скан пальца stepMode=52 - Search, ищем отпечаток в базе // 5 stepMode=50 - LedConfig, включить фиолетовый цвет, означает "готов" getImage, сканируем палец stepMode=51 - image2Tz, обрабатываем скан пальца stepMode=52 - Search, ищем отпечаток в базе // ---- stepMode=60 - LedConfig, моргание фиолетовым цветом, означает "пальцы сосканированы идет расчет" RegModel, создаем модель отпечатка stepMode=61 - Store, сохраняем модель отпечатка в библиотеке stepMode=105 - LedConfig, включить красный цвет, означает "ошибка сборки модели" pause, ожидание, ничего не происходит просто показываем красную подсветку 4. Режим CLEAR подтверждение очистки stepMode=90 - LedConfig, включить моргание красным цветом, означает "готов" getImage, сканируем палец дяя подтверждения возможности добавления (только если библиотека не пустая) stepMode=91 - image2Tz, обрабатываем скан пальца stepMode=92 - Search, ищем отпечаток в базе stepMode=106 - LedConfig, включить красный цвет, означает "ошибка подтверждения" pause, ожидание, ничего не происходит просто показываем красную подсветку stepMode=107 - LedConfig, включить фиолетовый цвет, означает "очистка завершена" pause, ожидание, ничего не происходит просто показываем фиолетовую подсветку ****************************************************/ #ifndef GROW_R503_H #define GROW_R503_H #include "Run.h" #include "Arduino.h" #ifdef __AVR__ #include #endif #define TIME_PAUSE_W 50 // пауза включения waker, для избежания ложных срабатываний, #define TIME_PAUSE_P 500 // пауза включения порта (до начала приема команд), минимально по документации 200 #define TIME_RUN 30000 // время работы по сигналу waker, потом идет выключение #define FINGER_LED_1 0x01 // мерцающий #define FINGER_LED_2 0x02 // мигающий #define FINGER_LED_ON 0x03 // включить #define FINGER_LED_OFF 0x04 // выключить #define FINGER_LED_ON_L 0x05 // включить плавно #define FINGER_LED_OFF_L 0x06 // выключить плавно #define FINGER_COLOR_RED 0x01 // #define FINGER_COLOR_BLUE 0x02 // #define FINGER_COLOR_PURLE 0x03 // #define FINGER_MODE_SLEEP 0x00 // режим сна #define FINGER_MODE_SCAN 0x01 // режим скана #define FINGER_MODE_ADD 0x02 // режим добавления отпечатков #define FINGER_MODE_ADD_A 0x03 // режим авторизации добавления отпечатков #define FINGER_MODE_CLEAR 0x04 // режим сброса всех отпечатков #define FINGER_MODE_CLEAR_A 0x05// режим авторизации для сброса всех отпечатков #define FINGER_STARTCODE 0xEF01 #define FINGER_COMMANDPACKET 0x1 #define FINGER_DATAPACKET 0x2 #define FINGER_ACKPACKET 0x7 #define FINGER_ENDDATAPACKET 0x8 #define FINGER_OK 0x00 #define FINGER_PACKETRECIEVEERR 0x01 #define FINGER_NOFINGER 0x02 #define FINGER_IMAGEFAIL 0x03 #define FINGER_IMAGEMESS 0x06 #define FINGER_FEATUREFAIL 0x07 #define FINGER_NOMATCH 0x08 #define FINGER_NOTFOUND 0x09 #define FINGER_ENROLLMISMATCH 0x0A #define FINGER_BADLOCATION 0x0B #define FINGER_DBRANGEFAIL 0x0C #define FINGER_UPLOADFEATUREFAIL 0x0D #define FINGER_PACKETRESPONSEFAIL 0x0E #define FINGER_UPLOADFAIL 0x0F #define FINGER_DELETEFAIL 0x10 #define FINGER_DBCLEARFAIL 0x11 #define FINGER_PASSFAIL 0x13 #define FINGER_INVALIDIMAGE 0x15 #define FINGER_FLASHERR 0x18 #define FINGER_INVALIDREG 0x1A #define FINGER_ADDRCODE 0x20 #define FINGER_PASSVERIFY 0x21 #define FINGER_TIMEOUT 0xFF #define FINGER_BADPACKET 0xFE #define FINGER_REGMODEL 0x05 #define FINGER_STORE 0x06 #define FINGER_LOAD 0x07 #define FINGER_UPLOAD 0x08 #define FINGER_DELETE 0x0C #define FINGER_EMPTY 0x0D #define FINGER_VERIFYPASSWORD 0x13 #define FINGER_TEMPLATECOUNT 0x1D #define DEFAULTTIMEOUT 5000 // milliseconds class Finger_R503 { public: uint16_t fingerID, confidence, templateCount; #ifdef __AVR__ Finger_R503(int wPin,int pPin,SoftwareSerial *); #endif Finger_R503(int wPin,int pPin,HardwareSerial *); uint8_t get_mode (void); // возвращает текущий режим void set_mode (uint8_t newMode); void set_stepMode (int new_stepMode); int get_stepMode (void); void init (void); // должна вызыватся из setup, устанавливает параметры ввода вывода контроллера void poll (unsigned long time_loop);// должна вызыватся из loop, отрабатывает сигнал WAKEUP void RunStep (unsigned long time_loop);// должна вызыватся из loop, отрабатывает состояния Step void begin (void); boolean LedConfig (uint8_t Ctrl, uint8_t Speed, uint8_t Color, uint8_t Count); boolean HandShake (void); uint8_t getImage (void); uint8_t TempleteNum (void); uint8_t image2Tz (uint8_t slot = 1); uint16_t Search (uint8_t slot); uint8_t GetFreeIndex (uint8_t page = 0); boolean verifyPassword (void); uint8_t RegModel (void); uint8_t store (uint16_t id); uint8_t emptyDatabase (void); void writePacket (uint32_t addr, uint8_t packettype, uint16_t len, uint8_t *packet); uint8_t getReply (uint8_t packet[], uint8_t id_packet, uint16_t timeout=DEFAULTTIMEOUT); private: int wakerPin = 0; int powerPin = 0; int Num = 1; boolean FirstStep = false; uint16_t baudrate = 57600; unsigned long time_loop_mode = 0; // время начала работы во временном режиме unsigned long time_loop_step = 0; // время начала паузы по степу uint8_t mode = FINGER_MODE_SCAN; // текущий режим, int stepMode = 0; // текущий шаг для режима (-1 для ошибок) uint8_t leteNum = 0; // пустой индекс библиотеки отпечатков uint32_t thePassword; uint32_t theAddress; Stream *mySerial; #ifdef __AVR__ SoftwareSerial *swSerial; #endif HardwareSerial *hwSerial; void powerOn (void); void powerOff (void); }; #endifфайл Run.cpp
// ************************************************************************************************* // процедура сравнивает два времени и возвращает разницу в виде числа, учитывает переход времени через 0 // start_time - начальное время // end_time - конечное время // // !!!! процедура чуствительна к разрядности исполняемого кода !!!! // !!!! процедура может работать неправильно при двойном переходе времени через 0 !!!! // ************************************************************************************************* unsigned long getDelayTime(unsigned long start_time, unsigned long end_time){ unsigned long result; if (start_time <= end_time) { result = end_time - start_time; } else { result = 4294967295 - end_time + start_time; } return result; }файл Run.h
файл other.ino
// --------------------------------------------------------- // Дополнительные процедуры // // --------------------------------------------------------- // ----------------------------------------------------------- void poll(unsigned long time_loop) { boolean buttonState = digitalRead(pinKey); if (!buttonState) { // кнопка отжата, обновим время и статус time_loop_key = time_loop; status_key = false; } else if (!status_key) { // кнопка нажата, но это первый цикл time_loop_key = time_loop; status_key = true; } else if (getDelayTime(time_loop_key, time_loop) > pauseKey) { // кнопка зажата давно, изменим режим и начнем считать заново time_loop_key = time_loop; status_key = false; #ifdef GROW_R503 if (finger.get_mode() == FINGER_MODE_SLEEP) { finger.set_mode(FINGER_MODE_ADD_A); } else if (finger.get_mode() == FINGER_MODE_SCAN) { finger.set_mode(FINGER_MODE_ADD_A); } else if (finger.get_mode() == FINGER_MODE_ADD) { finger.set_mode(FINGER_MODE_CLEAR_A); } else if (finger.get_mode() == FINGER_MODE_ADD_A) { finger.set_mode(FINGER_MODE_CLEAR_A); } else { finger.set_mode(FINGER_MODE_SLEEP); } #endif } #ifdef GROW_R503 if (finger.get_stepMode() == 100) { if (!status_open) { digitalWrite(pinOpen, HIGH); status_open = true; Serial.println("=== DOOR OPEN ==="); } } else { if (status_open) { digitalWrite(pinOpen, LOW); status_open = false; Serial.println("=== DOOR CLOSE ==="); } } #endif }У меня на мобиле только чистые пальцы распознаются. Стоит только мелким ремонтом (с наличием пыли) позаниматься - все, начинаются капризы. Мол пальцы не те...
Вот и я ж про то же. А на улице пальцы то замёрзли, то увлажнились, то ещё чего. Потому и спрашиваю про плохую погоду.
Сканер отпечатков в условиях улицы - ИМХО это баловство. Либо надо его весить в сухом и теплом месте.
У нас в офисе я ставил заводской сканер отпечатков на входной двери. Работает нормально, если руки сухие. Чуть только немного влажные - все, не открывает. Ну и плюс нужно ручное дублирование открывания замка. Пару раз замок глючил, если бы он с ключа не открывался - куча народа оказалось бы запертыми в офисе.
В детском саду у нас около года на калитках (на улице) стоят примерно вот такие замки
Я такой замок тоже настраивал и устанавливал. У него есть возможность привязать брелок (как у домофона), код менять можно хоть каждый день. Единственный минус - от частого использования некоторые цифры на кнопках "облезли". И на одной панели перегорела подсветка одной из клавиш. Настроить можно различные типы замков, и еще есть куча настроек. Цена замка в районе 1-2 тыс. руб. и выглядит вполне прилично. Если прикинуть, что его каждый день открывают по несколько сотен (если не тысяч) раз - вполне достойный вариант. И ключи таскать не надо.
Есть еще замки в антивандальном исполнении, типа таких


они на вид попрочнее, программируются тоже легко. Только вроде брелки к ним не привязываются.
Если решите все-таки ставить отпечаток пальцев - отпишитесь о полевых испытаниях.
ставить на улицу буду в выходные (если дождя не будет),
пока провел следующие испытания
1. сильно потер пальцы (до красноты), эмитация физ нагрузки - все ОК
2. подложил под палец 2 нитки, эмитация мусора (пробовал под разными положениями) - все ОК
3. втер в палец цемент, эмитация грязных рук - все ОК
4. сильно намочил палец - НЕ работает
5. сухой палец и влажный датчик (из пульеверизатора, аналог росы) если не слегка влажно - все ОК, если прямо каплями - НЕ работает
6. через тонкий слой пленки - вообще не срабатывает датчик касания
Поставил на улицу, правда не до конца (но об этом чуть ниже)
На улице 0 градусов, влажно и что-то моросит, короче мерзость, провозился около 3х часов сам замерз, но точно могу сказать датчик то-же замерз :)
Итого - сам сканер работает почти так-же как дома, чуть хуже стал работать датчик наличия пальца (контакт WakeUp), но распознавание и поиск работаю хорошо.
Сами пальцы от мороза то же "потеряли форму", и стали давать примерно 20% ошибок, но дополнительное "дообучение" (повторил обучение и он добавил по 1 модели на палец), в результате правильное распознование более примерно 95%
теперь о том что я не доделал, и тут нужна помощь сообщества
Короче у меня есть явная проблема в электрической схеме, я не зря запаял джампер "test", без него вся схема работает нормально, но само собой замок не открывается.
Когда я его ставлю у меня проседает напряжение на датчике и он перестает работать,
Средний провод разьема отмеченный как "Key" идет из вызывной панели и на нем постоянные +4,5v, это провод должен идти на внутренний выключатель "Открыть замок".
То есть если замкнуть "Key" на землю, то замок открывается.
Как правильно реализовать такое замыкание? понятно, что можно поставить реле, но очень не хочется...
Средний провод разьема отмеченный как "Key" идет из вызывной панели и на нем постоянные +4,5v, это провод должен идти на внутренний выключатель "Открыть замок".
То есть если замкнуть "Key" на землю, то замок открывается.
Вы точно уверены, что логика именно такова? У меня вот всё по другому. Открыванием занимается сама вызывная панель по сигналу с домофона. Она (панель) замыкает питание на соленоид замка. А когда она ничего не замыкает, то мультиметр показывает на проводе те же 12В, что и должны быть при открывании за счёт тока утечки ключа.
Средний провод разьема отмеченный как "Key" идет из вызывной панели и на нем постоянные +4,5v, это провод должен идти на внутренний выключатель "Открыть замок".
То есть если замкнуть "Key" на землю, то замок открывается.
Вы точно уверены, что логика именно такова? У меня вот всё по другому. Открыванием занимается сама вызывная панель по сигналу с домофона. Она (панель) замыкает питание на соленоид замка. А когда она ничего не замыкает, то мультиметр показывает на проводе те же 12В, что и должны быть при открывании за счёт тока утечки ключа.
Уверен, вот схема
кроме того если я замыкаю этот контакт на землю действительно замок открывается
Вот как установил сам датчик, по идее он закрыт от ветра и дождя (с верху над калиткой навес)
Все теперь у меня работает. Была проблема с оптроном, он банально не рабочий был.
заодно поменял резистр на 10к (можно и меньше, но учитывая, что мне не требуется яркость и тот факт, что этот идет с чужого устройства решил оставить 10к), и поставил светодиод для визуального контроля.
Исправил несколько багов в программе.
схему и программу в 1 посте обновил до актуального состояния.
Теперь, что касается субьективного ощущения (на улице -2 и мелкий снег)
1. на разьем 3.3vt лучше подавать более высокое напряжение чем 3.3, от этого улучшается "отзывчивость", по той документации которая у меня есть, допустимо до 6 вольт, у меня сейчас не смотря на стабилитрон (на 3.3) реально 4.3 вольта, по чему - не понимаю, когда питание было от USB было ровно 3.3, а сейчас 4.3, но я включал и напрямую на +5, отзывчивость была выше.
2. есть эффект "разогрева", возможно при -25 будут проблемы, будем ждать морозов
3. Обучение надо периодически повторять, при этом добавляются модели на "скукоженные" пальцы
4. ощущается некая "тормознутость" по сравнению например с телефоном, причина в том, что питание на датчик подается только после обнаружения касания, то есть расходуется время на определение касания+запуск подуля+сканирование+распознавание+поиск в базе. На холодную это все примерно 2...3 сек, при повторном скане 1..2 сек
Прошла неделя, за это время был и снегопад и ледяной дождь. В целом все работает хорошо. Для датчика единственно чего ему мешает - это когда он мокрый, все остальное работает. Проверка проходит на 4х членах семьи, возраст от 8 до 50 лет, полет хороший.
сегодня наконец запихнул платы во временную коробочку, когда будет тепло надо будет переделать с макеток на нормальную плату и сделать коробочку по размеру побольше.
у меня сейчас такая https://www.stroyportal.ru/catalog/section-korobki-montazhnye-2668/korobka-raspayachnaya-80h80h25-otkrytoy-provodki-b-684306313/ но висит на видном месте, по этому надежную и страшную не хочется.
сейчас из бутылки феном сделал сверху колпак, то есть сейчас ничего не промокнет, но во первых нужно отверстие для кнопки обучения, наверно нужно светодиод вывести.
Короче надо будет весной посмотреть, сейчас все равно этим заниматься холодно, к весне может 3д принтер куплю и напечатаю или подберу чего...
Ну, для видного места я сам на принтере напечатал такую, что сверху у неё вообще никаких щелей нет от слова совсем, т.е. никакой дождь и снег ей не страшны. Там у меня профильная труба 40х25, вот я и сделал коробку для клемм в аккурат шириной 25мм, чтобы как родная стала. Могу показать, если интересно.
Всем кого интересовали полевые испытания:
Пришлось третий раз проводить обучение, видимо пальцы меняют геометрию от множества параметров. При чем у меня сильнее хуже чем у остальных принимаются, у детей вообще замечательо у меня иногда с десятого раза... После дообучения все хорошо, но не надолго, примерно на месяц хватает.
наверно буду проект дорабатывать, поставлю sd карту и заложу программу самообучение, на основе принятых отпечатков буду строить варианты с теми которые не прошли, по идее те которые совсем чужие не смогут объединится в пару.
Короче если до НГ придет блок питания для сверлильного станка (3 посылки зависли на почте) доделаю его, если не придет займусь этой доработкой...
Прошла зима.
Разобрал макетку, вытравил плату, перепаял, залачил цапон лаком, на плату нанес технические надписи. Внес небольшие изменения, в основном в схему питания.
Исправил в коде 1 серьезную ошибку (переполнение буфера) и немного улучшил стабильность и скорость.
Напечатал нормальный корпус.
Все работает, ничего переделывать больше не планирую.
РЕЗЮМЕ: в целом данный датчик вполне подходит для средней полосы для работы на улице без прямого попадания на него осадков (под козырьком)
зы
в 1м посту обновил и схему и код
Скажите пожалуйста как вы это все прошивали? через Atom? ардуино IDE ругается...
Скажите пожалуйста как вы это все прошивали? через Atom? ардуино IDE ругается...
шил через IDE
я в коментах всегда пишу версию на чем шил - версия среды Arduino 1.8.14
А не могли бы Вы скинуть все файлы в архиве? Мб я что-то не так перенес из кода.
какую ошибку выдает?
Ну можно ж сделать намного проще, штош вы.
Звоните своей ардуине со своего номера. Ардуина сверяет входящий номер. Если он true, система сбрасывает звонок и открывает/снимает с охраны. Никакого внешнего контакта, никаких чужих.
Не забываем про watchdog
Была открыта IDE от Flprog. Поставил отдельно IDE, установил библиотеку и все равно такая же ошибка...
Была открыта IDE от Flprog. Поставил отдельно IDE, установил библиотеку и все равно такая же ошибка...
это проблема не скеча а установки IDE, заведи новую тему (в соответствующем разделе) с описанием ошибки, народ поможет, я спать ушел :)
Звоните своей ардуине со своего номера. Ардуина сверяет входящий номер. Если он true, система сбрасывает звонок и открывает/снимает с охраны. Никакого внешнего контакта, никаких чужих.
Не забываем про watchdog
Не понимаю, чем Ваш вариант проще:
1. Нужно достать телефон, набрать номер, дождаться ответа. Это всё секунд 10-15 минимум. Отпечаток пальца всегда под рукой в прямом и переносном смысле слова.
2. Существуют сервисы подмены номеров, любой может открыть дверь, зная Ваш номер. Способов узнать номер можно придумать кучу.
3. Телефон абонента может быть временно недоступен. При отсутствии движения по счету месяца через три оператор блокирует симку.
1. Нужно достать телефон, набрать номер, дождаться ответа. Это всё секунд 10-15 минимум. Отпечаток пальца всегда под рукой в прямом и переносном смысле слова.
2. Существуют сервисы подмены номеров, любой может открыть дверь, зная Ваш номер. Способов узнать номер можно придумать кучу.
3. Телефон абонента может быть временно недоступен. При отсутствии движения по счету месяца через три оператор блокирует симку.
Да, мой вариант дольше, но безопаснее и без контакта с погодными условиями.
2.От того, что вы подмените номер, вы совершенно не можете знать, на каком номере висит охранка. Куда будете звонит с подменного номера-то?
С телефоном, когда то делал следующий доступ и он до сих пор работает (используя то, что у всех операторов связи по их закону, 3 секунды соединения всегда бесплатны для людей) .
1. Подойдя к двери, человек звонит на номер для открытия 2. Контроллер, определив номер, кладет трубку. 3. Найдя номер в базе доступа, перезванивает на номер человека. 4. Если действительно человек хочет пройти в дверь, он поднимает трубку. 4. контроллер видя, что подняли трубку, ее кладет и открывает дверь. 5. Если человек дает отбой (сам кладет трубку), то означает, что это не он хочет пройти. Да, вот этот алгоритм занимал примерно 5-7 секунд. Но он был надежный и работает годами бесплатно на безлимитных тарифах опсосов. Потому как они блокируют симку, если нет исходящих/входящих звонков а не движения по счету. Может уже что то поменялось у них но раньше было так.
И в данном случае, ни какой подмены номеров не прокатит, потому как звонок контроллера будет прямым. Ну и я думаю безопастникам будет повод задуматься о возможной попытке проникновения.
С телефоном, когда то делал следующий доступ и он до сих пор работает (используя то, что у всех операторов связи по их закону, 3 секунды соединения всегда бесплатны для людей) .
Все варианты с телефоном требуют наличие этого самого телефона, живя в деревне Вы бы знали, что наличие телефона не всегда возможно, а при наличии детей тем более.
Кроме того если уж привязыватся к физическому устройству, то не к телефона а к таблетке/брелку это однозначно и проще и быстрее и надежнее.
Лично я за биометрию, если Вы хотите привязку к внешнему устройству - Вам никто не запрещает...
может проще архивом скетч выложить?
может проще архивом скетч выложить?
мне тут zip не дает сохранять в своих файлах, а выкладывать на левые ресурсы не хорошо, он сдохнет через какое-то время
мне тут zip не дает сохранять в своих файлах, а выкладывать на левые ресурсы не хорошо, он сдохнет через какое-то время
скинь на мыло ua6em собака orviss точка ru
и мне бы на мыло не могли бы скинуть? kesha.ncuxxx@gmail.com
скинул на 3 ящика.
в папке "схемы" 3d модели корпуса, плата, схема, и официальная pdf по датчику
Спасибо огромное, вот теперь скетч компилится без ошибок.
Спасибо огромное, вот теперь скетч компилится без ошибок.
да в первом посту строка 31 (файл R503.ino) обрезалась, должно быть
строки 11,12 (файл GROW_R503.cpp) обрезалась, должно быть
строка 88 (файл GROW_R503.h) обрезалась, должно быть
короче обрезались символы больше, меньше.