====== Электронный тайник с IMU-сенсором ======
{{ :projects:magicbook:title.jpg?nolink |}}
* Платформы: Iskra Mini
* Языки программирования: Arduino (C++)
* Тэги: шкатулка с секретом, Himitsu-Bako, углы Эйлера, загадка, квест
===== Что это? =====
Любите интересные загадки, создаёте квесты или просто хотите устроить надёжный и необычный тайник? Соберите магическую шкатулку, замок который невозможно взломать отмычкой или открыть на слух.
Открытая шкатулка считывает свою ориентацию в пространстве в 3 позициях, запоминает их и закрывается. Теперь открыть её можно только повторив комбинацию положений ещё раз.
===== Что нам понадобится? =====
{{ :projects:magicbook:all-in.jpg?nolink |}}
- [[amp>product/iskra-mini-headless?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Iskra Mini (без ног)]]
- [[amp>product/arduino-uno?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Arduino Uno]] или [[amp>product/usb-serial-converter?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | USB-Serial адаптер]] для программирования Iskra Mini.
- [[amp>product/troyka-imu-10-dof?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | IMU-сенсор на 10 степеней свободы (Troyka-модуль)]]
- [[amp>product/servo-fs90?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Микросервопривод FS90]]
- [[amp>product/troyka-led-module?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Светодиод «Пиранья» Зеленый (Troyka-модуль)]] (2 шт.)
- [[amp>product/troyka-buzzer?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Зуммер (Troyka-модуль)]]
- [[amp>product/button_for_boxes_17mm?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Корпусная кнопка]]
- [[amp>product/3-wire-cable-digital-troyka?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | 3-проводной шлейф «мама-мама»]]
- [[amp>product/krona-21mm-cable?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Кабель питания от батарейки Крона]]
- [[amp>product/nimh-krona-battery?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | NiMH-аккумулятор «Крона»]]
- [[amp>product/battery-holder-3aa?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Батарейный отсек]]
- [[amp>product/wire-mm?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Соединительные провода «папа-папа»]]
- [[amp>product/structor-troyka?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Крепления Troyka (#Структор)]]
- [[amp>product/structor-wide-rail?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Широкие рейки (#Структор)]]
- [[amp>product/structor-mega?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Пластина мега (#Структор)]]
- [[amp>product/nylon-screw-m3x8?utm_source=proj&utm_campaign=magicbook&utm_medium=wiki | Нейлоновые винты М3×8]] (8 шт.)
- Шкатулка-фолиант
===== Как собрать? =====
- Закрепите светодиод «Пиранья» (Troyka-модуль) и IMU-сенсор на 10 степеней свободы на панели крепления двух Troyka-модулей (#Структор) нейлоновыми винтами. Аналогично закрепите второй светодиод и зумер. {{ :projects:magicbook:how_to_1.jpg?nolink |}}
- Подключите два светодиода «Пиранья» (Troyka-модуль) к ''5'' и ''6'' цифровому пину платы Iskra Mini следующим образом:
- возьмите два трёхпроводных шлейфа «мама-мама» и откусите с одной стороны разъём для подключения Troyka-модулей;
- скрутите между собой красные и чёрные провода;
- припаяйте красные провода к питанию, чёрные — к земле, жёлтые к ''5'' и ''6'' пину;
- подключите трёхпроводные шлейфы к светодиодам «Пиранья».
Остальные модули и сенсоры подключаются аналогично.
{{ :projects:magicbook:how_to_2.jpg?nolink |}}
- Используйте два трёхпроводных шлейфа для подключения IMU-сенсора к Iskra Mini:
* **Контакты питания:**
- Земля (G) — чёрный провод. Соедините с пином GND.
- Питание (V) — красный провод. Соедините с пином VCC.
- Не используется.
* **Контакты шины I²C:**
- Сигнальный (D) — жёлтый провод. Подключите к пину ''A4'' (SDA).
- Сигнальный (С) — красный провод. Подключите к пину ''A5'' (SCL).
- Не используется.{{ :projects:magicbook:how_to_3.jpg?nolink |}}
- Подключите зуммер (Troyka-модуль) через трёхпроводной шлейф к ''12'' цифровому пину Iskra Mini.{{ :projects:magicbook:how_to_4.jpg?nolink |}}
- Припаяйте трёхпроводной шлейф к ''11'' цифровому пину. В будущем через него подключим корпусную кнопку. {{ :projects:magicbook:how_to_5.jpg?nolink |}}
- Подключите микросервопривод к ''10'' цифровому пину Iskra Mini. {{ :projects:magicbook:how_to_6.jpg?nolink |}}
- Соедините кабель питания от батарейки «кроны» через выключатель к Iskra Mini:
- контакт ''+'' — на ''Vin'' платы Iskra Mini;
- контакт ''−'' — на ''GND'' платы Iskra Mini.{{ :projects:magicbook:how_to_7.jpg?nolink |}}
- Сделайте «домики» из структора для установки Troyka-модулей. {{ :projects:magicbook:how_to_8.jpg?nolink |}}и установите всю электронику в шкатулку.
- В боковых частях шкатулки закрепите собранные «домики» из Troyka модулей. {{ :projects:magicbook:how_to_9.jpg?nolink |}}
- Установите в боковую грань шкатулки кнопку и выключатель в заранее просверленные отверстия. {{ :projects:magicbook:how_to_10.jpg?nolink |}}
- Припаяйте корпусную кнопку трёхпроводным шлейфом к Iskra Mini используя подтягивающий резистор. {{ :projects:magicbook:how_to_11.jpg?nolink |}}В итоге должна получиться схема:{{ :projects:magicbook:magicbook_scheme.png |}}
- Установите плату Iskra Mini и батарейку «крона» в шкатулку с помощью #Структора и двухсторонней клейкой ленты. {{ :projects:magicbook:how_to_12.jpg?nolink |}}
- [[:продукты:iskra-mini#Подключение_и_настройка|Прошейте плату Iskra Mini.]]
- Закрепите сервопривод на передней панели шкатулки. {{ :projects:magicbook:how_to_14.jpg?nolink |}}
- С помощью #Структора сделайте запорную скобу для качельки сервопривода. {{ :projects:magicbook:how_to_15.jpg?nolink |}}
- Разломайте батарейный отсек, извлеките пружинки и установите их на боковые грани шкатулки. {{ :projects:magicbook:how_to_16.jpg?nolink |}}
===== Алгоритм =====
* После подачи питания открываем шкатулку.
* Проверяем состояние кнопки.
* Если кнопка не нажата проверяем снова и снова.
* Устанавливаем новый пароль.
* Подтверждаем новый пароль.
* Если пароли не совпадают, повторяем установку нового пароля.
* Закрываем шкатулку.
* Проверяем состояние кнопки.
* Если кнопка не нажата проверяем снова и снова.
* Считываем введённый пароль.
* Проверяем введённый пароль с паролем установленным в шкатулке.
* Если пароли не совпадают, возвращаемся в состояние закрытой шкатулки и ожидания нажатия кнопки.
* Проверяем в какую сторону направлена шкатулка.
* Если в течении 10 секунд направление шкатулки не будет в сторону севера, возвращаемся в состояние закрытой шкатулки и ожидания нажатия кнопки.
* Открываем шкатулку.
{{ :projects:magicbook:book.jpg?nolink |}}
===== Исходный код =====
Для работы ниже приведённого скетча вам понадобиться скачать и подключить библиотеку [[https://github.com/amperka/Troyka-IMU|Troyka-IMU]].
// библиотека для работы I²C
#include
// библиотека для работы с модулями IMU
#include
// библиотека для работы с сервоприводами
#include
// создаём объект для работы с сервоприводом
Servo myServo;
// даём разумное имя для пинов, к которым подключены светодиоды
#define INDICATOR_PIN_1 5
#define INDICATOR_PIN_2 6
// даём разумное имя для пина, к которому подключена кнопка
#define SERVO_PIN 10
// даём разумное имя для пина, к которому подключена кнопка
#define BUTTOM_PIN 11
// даём разумное имя для пина, к которому подключена пищалка
#define BUZZER_PIN 12
#define N 3
// множитель фильтра
#define BETA 0.22
// дрифт углов Эйлера
#define DRIFT_LIMIT 0.3
// компенсация точного местоположения объекта в пространстве
#define DRIFT_COMPRASION 10
// создаём объект для фильтра Madgwick
Madgwick filter;
// создаём объект для работы с акселерометром
Accelerometer accel;
// создаём объект для работы с гироскопом
Gyroscope gyro;
// создаём объект для работы с компасом
Compass compass;
// переменные для данных с гироскопа, акселерометра и компаса
float gx, gy, gz, ax, ay, az, mx, my, mz;
// получаемые углы ориентации (Эйлера)
float yaw, pitch, roll;
float prevYaw, prevPitch, prevRoll;
// переменная для хранения частоты выборок фильтра
float fps = 100;
unsigned long prevMillis;
unsigned long currMillis;
// создаём структуру
struct vector
{
float x;
float y;
float z;
};
// переменная состояния
int flag = 0;
// калибровочные значения компаса
// полученные в калибровочной матрице из примера «compassCalibrateMatrixx»
const double compassCalibrationBias[3] = {
524.21,
3352.214,
-1402.236
};
const double compassCalibrationMatrix[3][3] = {
{1.757, 0.04, -0.028},
{0.008, 1.767, -0.016},
{-0.018, 0.077, 1.782}
};
// состояния шкатулки:
// открыта, закрыта, ввод нового пароля, подтверждение нового пароля
enum State
{
OPENED,
CLOSED,
SETPWD,
CNFPWD,
};
// массив для хранения кода шкатулки
vector codeBox[3];
// массив для хранения кода подтверждения
vector confirmCodeBox[3];
// массив для хранения кода, при попытке открыть шкатулку
vector codeOpenBox[3];
// состояние шкатулки
State state;
void setup()
{
// открываем последовательный порт
Serial.begin(115200);
Serial.println("Begin init...");
pinMode(INDICATOR_PIN_1, OUTPUT);
pinMode(INDICATOR_PIN_2, OUTPUT);
// подключаем сервопривод
myServo.attach(SERVO_PIN);
// инициализация акселерометра
accel.begin();
// инициализация гироскопа
gyro.begin();
// инициализация компаса
compass.begin();
// задаем начальное ненулевое значение в переменную
prevMillis = millis();
// калибровка компаса
compass.calibrateMatrix(compassCalibrationMatrix, compassCalibrationBias);
// даём время стабилизироваться объекту в пространстве
while(!stayIMU(12000)) {
}
// выводим сообщение об удачной инициализации
Serial.println("Initialization completed");
// выбираем текущее состояние шкатулки
state = OPENED;
}
void loop()
{
// проверяем состояние шкатулки
switch (state) {
// шкатулка открыта
case OPENED: {
// открываем шкатулку
open();
// пока кнопка не нажата
while (digitalRead(BUTTOM_PIN) == HIGH) {
// ничего не делаем
}
// переходим в состоянии установки нового пароля
state = SETPWD;
break;
}
// шкатулка закрыта
case CLOSED: {
// закрываем шкаиулку
close();
// пока кнопка не нажата
while (digitalRead(BUTTOM_PIN ) == HIGH){
// следим за ориентацией объекта в пространстве
readYawPitchRoll();
}
// считываем код, для открытия шкатулки
readOpenCode();
// проверяем правильный ли код
if(checkOpenCode() == true) {
// если код правильный
Serial.println("Password accepted");
for(int i = 1; i < 15; i++) {
tone(BUZZER_PIN, 2000, 1000/i);
if(readAzimut() == true) {
Serial.println("Box is opened");
open();
return;
}
delay(2000/i);
}
Serial.println("Box is not opened");
} else {
Serial.println("Password not accepted");
tone(BUZZER_PIN, 500, 2000);
}
break;
}
// установка нового кода
case SETPWD: {
// устанавливаем новый код
setCode();
// уходим на подтверждение
state = CNFPWD;
break;
}
// подтверждаем кодовую последовательность
case CNFPWD: {
// повторяем ввод нового кода
setConfirmCode();
if(checkConfirmCode() == true) {
Serial.println("Code confirm accepted");
tone(BUZZER_PIN, 2000, 300);
close();
}else {
Serial.println("Code confirm not accepted");
tone(BUZZER_PIN, 500, 2000);
// повторяем ввод кода
state = SETPWD;
}
break;
}
}
}
// считывание углов Эйлера
void readYawPitchRoll() {
// считываем данные с акселерометра в единицах G
accel.readGXYZ(&ax, &ay, &az);
// считываем данные с гироскопа в радианах в секунду
gyro.readRadPerSecXYZ(&gx, &gy, &gz);
// считываем данные с компаса в Гауссах
compass.readCalibrateGaussXYZ(&mx, &my, &mz);
// устанавливаем коэффициенты фильтра
filter.setKoeff(fps, BETA);
// обновляем входные данные в фильтр
filter.update(gx, gy, gz, ax, ay, az, mx, my, mz);
// получение углов yaw, pitch и roll из фильтра
yaw = filter.getYawDeg();
pitch = filter.getPitchDeg();
roll = filter.getRollDeg();
}
// двигается ли шкатулка
bool stayIMU() {
// запоминаем предыдущие значения углов для сглаживания мелкого дрифта
float prevYaw = yaw;
float prevPitch = pitch;
float prevRoll = roll;
// считываем углы Эйлера
readYawPitchRoll();
// проверяем дрифт углов
if ((prevYaw - yaw < DRIFT_LIMIT && prevYaw - yaw > -DRIFT_LIMIT) &&
(prevPitch - pitch < DRIFT_LIMIT && prevPitch - pitch > -DRIFT_LIMIT) &&
(prevRoll - roll < DRIFT_LIMIT && prevRoll - roll > -DRIFT_LIMIT)) {
// шкатулка остановилась
return true;
}
else
// катулка движется в пространстве
return false;
}
// двигается ли шкатулка с проверкой на время
bool stayIMU(int interval) {
// текущее состояние шкатулки
if (stayIMU() == false) {
flag = 0;
return false;
}
if (flag == 0) {
// запоминаем текущее время
prevMillis = millis();
flag = 1;
return false;
}
// ожидаем выдержку по времени
if(flag == 1 && millis() - prevMillis > interval) {
flag = 0;
prevMillis = millis();
return true;
} else {
return false;
}
prevMillis = millis();
return false;
}
// установка кода
void setCode() {
Serial.println("Set code:");
for(int i = 0; i < N; i++) {
while(!stayIMU(3000)) {
}
codeBox[i].x = yaw;
codeBox[i].y = pitch;
codeBox[i].z = roll;
tone(BUZZER_PIN, 1000, 100);
Serial.print("Saving set ");
Serial.println(i+1);
}
delay(200);
tone(BUZZER_PIN, 1000, 100);
delay(200);
tone(BUZZER_PIN, 1000, 100);
}
// подтверждение кода
void setConfirmCode() {
Serial.println("Confirm code:");
for(int i = 0; i < N; i++) {
while(!stayIMU(3000)) {
}
confirmCodeBox[i].x = yaw;
confirmCodeBox[i].y = pitch;
confirmCodeBox[i].z = roll;
tone(BUZZER_PIN, 1000, 100);
Serial.print("Saving confirm ");
Serial.println(i+1);
}
delay(200);
tone(BUZZER_PIN, 1000, 100);
delay(200);
tone(BUZZER_PIN, 1000, 100);
}
// проверка кода
bool checkConfirmCode() {
bool checkCode = true;
for(int i = 0; i < N && checkCode == true; i++) {
if ((abs(codeBox[i].x) - abs(confirmCodeBox[i].x) < DRIFT_COMPRASION && abs(codeBox[i].x) - abs(confirmCodeBox[i].x) > -DRIFT_COMPRASION) &&
(abs(codeBox[i].y) - abs(confirmCodeBox[i].y) < DRIFT_COMPRASION && abs(codeBox[i].y) - abs(confirmCodeBox[i].y) > -DRIFT_COMPRASION) &&
(abs(codeBox[i].z) - abs(confirmCodeBox[i].z) < DRIFT_COMPRASION && abs(codeBox[i].z) - abs(confirmCodeBox[i].z) > -DRIFT_COMPRASION)) {
checkCode = true;
} else {
checkCode = false;
}
}
return checkCode;
}
// считывание пароля для открытия шкатулки
void readOpenCode() {
for(int i = 0; i < N; i++) {
while(!stayIMU(3000)) {
}
codeOpenBox[i].x = yaw;
codeOpenBox[i].y = pitch;
codeOpenBox[i].z = roll;
tone(BUZZER_PIN, 1000, 100);
delay(200);
Serial.print("Save ");
Serial.println(i+1);
}
}
// проверка введённого пароля с паролем шкатулки
bool checkOpenCode() {
bool checkPass = true;
for(int i = 0; i < N && checkPass == true; i++) {
if ((abs(codeOpenBox[i].x) - abs(codeBox[i].x) < DRIFT_COMPRASION && abs(codeOpenBox[i].x) - abs(codeBox[i].x) > -DRIFT_COMPRASION) &&
(abs(codeOpenBox[i].y) - abs(codeBox[i].y) < DRIFT_COMPRASION && abs(codeOpenBox[i].y) - abs(codeBox[i].y) > -DRIFT_COMPRASION) &&
(abs(codeOpenBox[i].z) - abs(codeBox[i].z) < DRIFT_COMPRASION && abs(codeOpenBox[i].z) - abs(codeBox[i].z) > -DRIFT_COMPRASION)) {
checkPass = true;
} else {
checkPass = false;
}
}
return checkPass;
}
// проверка напраление шкатулки относительно севера
bool readAzimut() {
bool checkAzimut = true;
float x = compass.readAzimut();
Serial.println(x);
if (x < DRIFT_COMPRASION && x > -DRIFT_COMPRASION){
checkAzimut = true;
} else {
checkAzimut = false;
}
return checkAzimut;
}
// вывод углов Эйлера в serial-порт
void printYawPitchRoll() {
Serial.print("yaw: ");
Serial.print(yaw);
Serial.print("\t\t");
Serial.print("pitch: ");
Serial.print(pitch);
Serial.print("\t\t");
Serial.print("roll: ");
Serial.println(roll);
}
// Функция отпирает замок
void open(void) {
digitalWrite(INDICATOR_PIN_1, HIGH);
digitalWrite(INDICATOR_PIN_2, HIGH);
myServo.write(0);
state = OPENED;
}
// функция запирает замок
void close(void) {
digitalWrite(INDICATOR_PIN_1, LOW);
digitalWrite(INDICATOR_PIN_2, LOW);
myServo.write(70);
state = CLOSED;
}
===== Демонстрация работы устройства =====
{{youtube>wFbXeHyEQjs?large}}
===== Что дальше? =====
Сделайте настоящую Himitsu-Bako без электронных компонентов снаружи. Замените [[amp>product/button_for_boxes_17mm?utm_source=proj&utm_campaign=duel&utm_medium=wiki | корпусную кнопку]] на [[amp>product/troyka-sound-loudness-sensor?utm_source=proj&utm_campaign=duel&utm_medium=wiki | датчик шума (Troyka-модуль)]], и вместо нажатия на кнопку — стучите по шкатулке.