
Не знаю, как вам, но мне в течении дня приходится часто отходить от рабочего места и блокировать мак. Чтобы не совершать несколько кликов мышкой, блокировку своего мака я «повесил» на клавиши «shift + cmd + l», но по приходу к рабочему месту опять же приходилось вводить пароль (который в силу моей параноидальности не так-то прост). И вот, ошибившись в спешке в очередной раз при его вводе, задумался автоматизировать процесс блокировки/разблокировки. Так как все двери нашего офиса открываются по карте, решил повесить на RFID-метку (всё равно всё время болтается на шее) и эту функцию. Итак, задача на словах выглядела так: авторизовавшись единожды в начале рабочего дня иметь возможность блокировки/разблокировки мака по RFID-метке, при этом все функции проверки валидности метки и т.п. должны происходить на стороне мака.
Начало — уже половина дела, да и как раз под рукой освободился стенд на базе Arduino UNO.
В процессе работы решил дополнить функционал: считывание метки будет происходить только при нажатой кнопке жёлтого цвета на фото выше (уж не знаю, зачем такие усложнения — видимо, опять параноя сказывается). Итак, общий процесс должен будет выглядеть следующим образом:
- Функциональная часть вся будет на стороне мака, а Arduino будет только передавать код метки и «мигать светодиодами»;
- Зажимаем кнопку — загорается жёлтый светодиод готовности;
- Если прикладываем некорректную метку — загорается красный светодиод;
- Прикладываем правильную метку — загорается зелёный светодиод и происходит блокировка/разблокировка мака.
Докупив модуль RFID на 125 кГц, собрал на макетной плате прототип устройства.
Скетч и код для Arduino

#include <SoftwareSerial.h> // "Распиновка" int buttonPin = 2; int ledGreenPin = 13; int ledYellowPin = 12; int ledRedPin = 11; // Модуль RFID и переменны для него SoftwareSerial RFID(6, 7); String inputString = ""; int rfidData; String rfidNumber = ""; String rfidNumberLast = ""; boolean startPressButton = false; void setup() { Serial.begin(115200); RFID.begin(9600); pinMode(buttonPin, INPUT); pinMode(ledGreenPin, OUTPUT); pinMode(ledYellowPin, OUTPUT); pinMode(ledRedPin, OUTPUT); digitalWrite(ledGreenPin, LOW); digitalWrite(ledYellowPin, LOW); digitalWrite(ledRedPin, LOW); } void loop() { listenButton(); } /* Слушаем кнопку. Если нажата - слушаем RFID */ void listenButton() { if (digitalRead(buttonPin) == HIGH) { if (!startPressButton) { startPressButton = true; clearRFID(); } digitalWrite(ledYellowPin, HIGH); listenRFID(); } else { startPressButton = false; digitalWrite(ledYellowPin, LOW); } } /* Слушаем RFID. Если получен номер метки - кидаем его в поток */ void listenRFID() { if (RFID.available()) { delay(100); rfidNumber = ""; for (int i = 0; i < 14; i++) { rfidData = RFID.read(); if (rfidData < 16) rfidNumber += '0'; rfidNumber += rfidData; } RFID.flush(); sendRDIFNumber(); } } /* Кидаем номер метки в поток */ void sendRDIFNumber() { if (rfidNumber != "" and rfidNumberLast != rfidNumber) { Serial.print("S"); Serial.print(rfidNumber); Serial.print("E"); rfidNumberLast = rfidNumber; rfidNumber = ""; } } /* Слушаем поток на предмет комманд для Arduino */ void serialEvent() { while (Serial.available()) { char inChar = (char)Serial.read(); inputString += inChar; if (inputString == "M1F") { Serial.flush(); inputString = ""; logInOutProcess(); } if (inputString == "M0F") { Serial.flush(); inputString = ""; logInOutFail(); } } } /* Проверка на стороне мака прошла успешно - зелёный цвет */ void logInOutProcess() { clearRFID(); digitalWrite(ledGreenPin, HIGH); digitalWrite(ledYellowPin, LOW); digitalWrite(ledRedPin, LOW); delay(1000); digitalWrite(ledGreenPin, LOW); digitalWrite(ledYellowPin, LOW); digitalWrite(ledRedPin, LOW); } /* Проверка на стороне мака не прошла - красный цвет */ void logInOutFail() { clearRFID(); digitalWrite(ledGreenPin, LOW); digitalWrite(ledYellowPin, LOW); digitalWrite(ledRedPin, HIGH); delay(1000); digitalWrite(ledGreenPin, LOW); digitalWrite(ledYellowPin, LOW); digitalWrite(ledRedPin, LOW); } /* Чистка выдачи RFID-модуля */ void clearRFID() { RFID.flush(); rfidNumberLast = ""; rfidNumber = ""; }
Самое интересное, на мой взгляд, происходит не на стороне Adruino, а на стороне мака. Итак, общаться со стендом будет Node.js с модулем SerialPort. Но для начала хотелось бы решить вопрос с хранением пароля разблокировки (очень уж не хотелось держать его открытым в теле скрипта, хоть и FileVault по-умолчанию включён). Для этого решил воспользоваться стандартной «ключницей» OS X — Keychain Access.
Как добавить пароль в ключницу?
Вызываем Keychain Access (Spotlight Search Вам в помощь)

Добавляем новый пароль...

В поле Account Name прописываем адекватное имя — позже к нему будем обращаться из скрипта
Не забываем получить доступ к ключу:

Подтверждаем доступ к ключу для консольной программы security

Добавляем новый пароль...

В поле Account Name прописываем адекватное имя — позже к нему будем обращаться из скрипта
Не забываем получить доступ к ключу:
security find-generic-password -ga my password

Подтверждаем доступ к ключу для консольной программы security
Ну вот, можно приступить к самому скрипту на Node.js. Для этого на рабочем столе создаём папку «RFIDUnLock», сам скрипт будет именоваться как «rfid.js»:
var inputString = ""; var serialport = require('serialport'); var SerialPort = serialport.SerialPort; var sp = new SerialPort('/dev/tty.usbmodem20331', { // подсмотреть "путь" девайса можно в "Tools/Serial Port" программы Arduino baudrate: 115200 }); var exec = require('child_process').exec; sp.on('open', function() { /* Читаем поток */ sp.on('data', function(data) { inputString += data.toString("utf8"); /* Берём из потока нужные данные по "маркерам" */ var cardCode = inputString.match(/S([0-9]+)E/i); if (cardCode && cardCode[1] != 'undefined') { checkCardNumber(cardCode[1]); inputString = ''; } }); }); function checkCardNumber(cardCode) { sp.flush(function() { /* Если метка та, что нужно... */ if (cardCode == '0211111111111111111111111103') { /* ...отправляем команду Arduino "мигнуть зелёным" */ sp.write('M1F'); /* проверяем: запущен ли "скрин сейвер"? */ exec('ps aux | grep -c ScreenSaverEngine.app | grep -v grep', function (error, stdout, stderr) { /* если запущен - берём пароль из Kaychain и "печатаем" в поле ввода пароля */ if (parseInt(stdout) > 2) { exec("security 2>&1 >/dev/null find-generic-password -ga mypassword | ruby -e 'print $1 if STDIN.gets =~ /^password: \"(.*)\"$/'", function (error, stdout, stderr) { if (error !== null) return; var appleScript = 'osascript -e \'tell application "System Events"\' -e \'key code 56\' -e \'delay 0.5\' -e \'keystroke "' + stdout + '"\' -e \'key code 36\' -e \'end tell\''; exec(appleScript); }); /* ...если "скрин север" не запущен - запускаем */ } else { exec('open -a /System/Library/Frameworks/ScreenSaver.framework/Versions/Current/Resources/ScreenSaverEngine.app'); } }); /* Метка не корректна - отправляем команду Arduino "мигнуть красным" */ } else { sp.write('M0F'); } }); }
Далее сохраняем как программу (с помощью Script Editor) код вызова Node.js скрипта:
do shell script "/usr/local/bin/node ~/Desktop/RFIDUnLock/rfid.js"
Подробнее, если можно...
Вызываем Script Editor (Spotlight Search Вам в помощь)

Прописываем код...

Экспортируем...

Сохраняем как Application
Можно так же добавить ключ, сообщающий о запуске программы в background-режиме. Для этого в файле «info.plist» (доступен при просмоте содержимого папки программы: ctrl + click на файле и выбор «Show Package Contents») необходимо дописать перед закрывающими тегами "</dict></plist>":

Прописываем код...

Экспортируем...

Сохраняем как Application
Можно так же добавить ключ, сообщающий о запуске программы в background-режиме. Для этого в файле «info.plist» (доступен при просмоте содержимого папки программы: ctrl + click на файле и выбор «Show Package Contents») необходимо дописать перед закрывающими тегами "</dict></plist>":
<key>LSBackgroundOnly</key> </true>
...И добавляем запуск нашей программы при загрузке системы

P.S.: Поигравшись пару дней с прототипом, решил, что он «имеет право жить» — осталось спаять его в корпус, с использованием меньшего брата Arduino Nano.
N.B.: В XXI веке будет много завещаний, содержащих пароли.
