[Из песочницы] По ту сторону квеста
«Сегодня вам предстоит переместиться в начало 20 века, чтобы изучить работы великого учёного Николы Теслы. Но технология перемещения в пространстве и времени пока несовершенна, и у вас будет ровно 1 час, чтобы вернуться в настоящее. Если не успеете — растворитесь в пространстве и времени...»
Примерно такую предысторию вы услышите на одном из квестов Клаустрофобии.
В каждой из комнат вам предстоит решать загадки, выполнять физические эксперименты, открывать замки. Где-то через час вы выйдете, но абсолютно ничего не узнаете про работу квеста «изнутри», про то как игра устроена. Интересно?
В этой статье я расскажу про внутреннее устройство «Загадки Теслы», покажу какие технические решения были приняты, какие доработки мы внесли в программы и управляющие устройства за полгода работы.
Если хотите пройти этот квест сами — дальше не читайте.
Что такое «Клаустрофобия»? О чём эта статья?
Есть такая организация, которая проводит «квесты в реальности». Это чем-то напоминает телеигру «Форт Боярд». Вас запирают в комнате, из которой вы должны выбраться в течении часа. Внутри загадки, которые нужно разгадать. Например, нажать правильную комбинацию кнопок, поместить спрятанный предмет на место, найти числа для кодового замка. В этой статье я расскажу, как работают устройства, как они определят, что условие выполнено, и как всё это работает в единой системе, контролируемой из операторской.
Устройство «Кнопочная панель»
Нужно открыть выдвижную полку шкафа, когда нажата правильная комбинация кнопок. Устройство управления простое как топор. Провода и выключатели, ничего больше.
Что видит игрок. Кнопочную панель из 49 кнопок (7х7), закреплённую на шкафу и «чертёж» на столе. По чертежу нужно догадаться, какие кнопки нажимать, чтобы что-то произошло (чтобы открылся ящик с мешком).
Как это работает. На панели есть «правильные» кнопки и «неправильные». Устройство должно сработать, если нажаты все «правильные». Соединияем их последовательно. Но замок не должен открыться, если нажата хотя бы одна «неправильная» кнопка. Значит, соединяем их параллельно. В итоге получили две пары контактов — одна замыкается когда «все правильные», другая — когда «есть неправильные».
Что с ними делать? Замком управляет электромагнитное реле, а у него только один вход. Здесь решение было такое:
Внешне устройство задумывалось как панель из квадратных металлических кнопок:
Каждая кнопка представляла собой обычную тактовую кнопку, на которую сверху был наклеен металлический квадрат 2х2 см:
Внешний вид утвердили, и первая версия устройства была собрана:
Несколько месяцев спустя стало понятно, что такая конструкция не годится. Игроки стали снимать кнопки с панели, а собрать всё в исходное состояние занимало достаточно много времени. Поэтому металлические кнопки заменили неразборными пластиковыми. Управляющую схему оставили той же самой. Она стабильно работает до сих пор.
Устройство «Звонок»
Нужно выдвинуть ящик стола, когда игрок позвонит в звонок.
Что видит игрок. Обычный звонок, после нажатия на который открывается ящик (на ящике этот звонок нарисован, чтобы проще было догадаться).
Как это работает. Сам звонок переделан. В момент ударара по звонку замыкается контакт (на долю секунды), и этот момент должна обнаружить управляющая схема. А управляющих схем было сделано несколько.
Первую из сделали на двух транзисторах, повторив схему RS-триггера. Предполагалось, что ящик должен выдвигаться электромотором, который надо включить в момент нажатия на звонок (вход S триггера), и выключить в момент полного открытия ящика (вход R). В то время ТЗ никто из нас не писал, а «оформление» и прочая бутафория делалась после электронной начинки.
Так, закончив сборку и тестирование на макете, стало известно, что ящик выдвигается с помощью пневматического поршня, а открытие происходит автомобильным двигателем замка дверей. Переделать управляющую схему не составило труда.
Но и на этом всё не закончилось. Установив оборудование и включив питание мы увидели, что замок не открывается. Питание подаётся, но ничего не происходит. Как оказалось, провода на замок идут слишком тонкие, и падение напряжения на них не позволяет сдвинуть защёлку с места.
Долбить стены и прокладывать новые провода конечно же никто не согласился, поэтому было решено сделать пусковую схему для этого двигателя. Получилось вполне компактное устройство:
Основная идея — зарядить конденсатор большой ёмкости до 12В через тонкие провода, а потом по команде от управляющей схемы подключить его к двигателю. Всего на полсекунды. Этого достаточно, чтобы открыть замок.
Последняя версия (я надеюсь, теперь уже последняя) построена на микроконтроллере ATmega8L. Загрузчик взяли от Arduino.
Этот контроллер отсчитывает количество звонков. Ящик открывается, если позвонили ровно три раза. Если игрок позвонил большее число раз, или если паузы между звонками были слишком длинные, то ящик не откроется.
Устройство «Голова Горгоны»
Задача считается решённой, если на голову Горгоны натянут тряпочный мешок.
Что видит игрок.
Как это работает. На поверхности головы установлено четыре SMD фототранзистора (которые очень хорошо замаскированы и невооружённым глазом не видны). Сверху, на стене, установлена достаточно яркая лампа. Фототранзистор работает так: он проводит ток, когда освещён, и не проводит, когда в темноте. Проверкой условия занимается ATmega8L, перепрошитая на Arduino.
Уровни «светлого» и «тёмного» состояний прописаны в константах. Так что в главном цикле остаётся проверить напряжение на фототранзисторах через ADC:
void loop() {
...
outputValue = 1; // assume that all right (all covered)
for (int i=0; i<5; ++i) {
sensorValue = analogRead(photoInPins[i]);
if (sensorValue < inputThresholds[i]) { // this sensor not covered ("light")
outputValue = 0;
digitalWrite(stateOutPins[i], LOW);
}
else // this sensor covered ("dark") or phototransistor not connected
digitalWrite(stateOutPins[i], HIGH);
}
...
}
Само устройство (вместе с аудиомодулем RS012, о котором я ещё расскажу) выглядит так:
Как только все фототранзисторы накрыты, схема выдаёт сигнал «готовность» и воспроизводит звуковой файл (через динамик, спрятанный под столом). По светодиодам оператор может проверить работоспособность фототранзисторов.
Подъёмник ключа
Нужно установить элекнты схемы в правильные позиции, после чего по стержню поднимется ключ от замка.
Что видит игрок. Стол, в который вмонтированы шесть двухконтактных розеток, и шесть кубиков с обозначениями элементов схемы (к этому моменту игрок нашёл уже все шесть).
Если все кубики установлены правильно, вдоль стержня начинает подниматься ключ.
Как это работает. Стержень — это тонкая пластиковая труба, внутри которой поднимается магнит. Этот магнит поднимает ключ снаружи трубы. Сам магнит зафиксирован на резиновом ремне, а ремень приводится в движение шаговым двигателем.
Первая версия этого устройства выглядела так:
Кубики были деревянные, а всей этой штукой управлял STM32F0. Вскоре стало понятно, что такие кубики использовать нельзя (они не отвечали требованиям вандалоустойчивости), и устройство было полностью переделано. В последней версии мы решили использовать Arduino.
Как вы уже догадались, внутри кубиков спрятаны резисторы. Схема включения полностю совпадает со схемой для Горгоны, только вместо фототранзисторов подключены розетки. Чем больше сопротивление установлено в кубике, тем большее напряжение на нём падает. Считываем это значение через analogRead() и сравниваем с эталонным. Таким образом можно однозначно определить, какой кубик вставлен в какую розетку.
Панель переключателей
Поворачивая выключатели, нужно установить правильную комбинацию включенных и выключенных.
Что видит игрок. Большой ящик, в котором расположено несколько рядов переключателей. Игрок должен догадаться, какие из них надо включить, а какие оставить выключенными.
Как это работает. Точно так же, как устройство «Кнопочная панель». Устройство управления повторено один-в-один. Это устройство практически не изменилось с момента проектирования. Вот такая модель мы сделали:
Она же в разрезе:
И в разобранном виде:
А вот так устройство выглядело после сборки, до передачи декораторам:
Затем выключатели соединили, всё это убрали в ящик, и повесили на стену. Схему соединения иногда меняют, чтобы невозможно было угадать правильную комбинацию по царапинам и потёртостям на краске.
Приборная панель
Чтобы выполнилось условие, все три стрелки прибора должны быть установлены в правильное положение. Стрелки устанавливаются с помощью ручек на передней панели.
Что видит игрок.
Как это работает. За ручками стоят относительные энкодеры, за стрелками — шаговые двигатели. Управляет всем этим STM32F0. Выбрали STM потому, что у него есть такой режим таймеров TIM1, TIM2 и TIM3, в котором эти таймеры считают импульсы от энкодера.
При вращении энкодера по часовой стрелке счётчик увеличивается, против часовой стрелки — уменьшается. Это работает даже при быстром вращении всех трёх одновременно.
Первоначально устройство проектировалось с переменным резистором вместо энкодера, и сервоприводами вместо шаговых двигателей.
От резисторов отказались, т.к. со временем они будут работать хуже из-за износа, а от сервоприводов отказались из-за их шумности (экспериментировали с TowerPro) и ограничения в 180 градусов на максимальный угол поворота.
Графитовая установка
Идея состоит в том, что вы крутите ручку генератора, заряжая батарею. Между электродами при этом проскакивают искры, а показания прибора увеличиваются от нулевого до максимального. Затем вы устанавливаете толстый графитовый стержень между клеммами, он сгорает (выделяя много дыма и света). После чего открывается заслонка проектора, и вы можете смотреть видео, вращая ручку кинопроектора.
Что видит игрок.
Как это работает. Разумеется, никакого генератора там нет, и прибор показывает вовсе не заряд батареи. Да и батареи там тоже никакой нет, а графит сгорает за счёт тока, подаваемого через трансформатор из сети 220В. Но давайте обо всём по порядку.
Внутри «генератора» (самый левый ящик) за ручкой стоит энкодер. Микроконтроллер считает обороты ручки. Стрелочный прибор — это микроамперметр. Он подключен к PWM-выходу микроконтроллера через ограничительный резистор. Графитовая установка — совершенно независимый прибор, имеющий вход «пуск» и выход «готов». Когда игроки сделали требуемое количество оборотов ручки, подаётся сигнал «пуск» и графит загорается. Когда графит полностью прогорел, установка выдаёт сигнал «готов», открывающий заслонку проектора.
Кинопроектор
Нужно крутить ручку для просмотра видеозаписи.
Что видит игрок.
На картинке это самый правый ящик. Когда заслонка открыта, на стену проецируется изображение.
Как это работает. Вот здесь начинается тяжёлая артиллерия.
Кинопроектор — это одноплатный компьютер Raspberry Pi и портативный презентационный проектор DELL. Ручка проектора крутит энкодер.
На RPi установили Raspbian, в котором порт UART ('/dev/ttyAMA0') был отключен от терминала (программно) и подключен к микроконтроллеру (аппаратно). Когда ручку вращали, по UART от контроллера приходил байт 'p' (play), когда переставали вращать — 's' (stop).
Затем мы полностью переписали управляющие скрипты, а микроконтроллер выкинули, подключив энкодер напрямую к GPIO на RPi. Сейчас за состоянием энкодера следит Python скрипт, использующий модуль RPIO с настроенными прерываниями. Он даёт команды плееру omxplayer через stdin.
В новой версии omxplayer появилась возможность воспроизводить видео зацикленно (--loop), поэтому про способы перезапуска видеоролика рассказывать не буду.
Машина времени
Она состоит из двух частей. То, что внутри кабины — видно игрокам — это дисплей для отображения даты, ручки установки дня и месяца, кнопки запуска. То, что хорошо спрятано за декоративными панелями — это схемы управления светом, звуком, дымом и дверьми.Консоль машины времени и дисплей
На панели установлено 8 газоразрядных ламп ИН-4 (называемых NIXIE), отображающих «дату назначения». Внизу три ручки: первые две предназначены для установки дня и месяца назначения, третяя заблокирована на 2015 году (чтобы игроки быстрее сообразили, что нужно установить первыми двумя ручками). На стенах машины установлены 4 кнопки, которые подсвечиваются зелёным, когда выставлена правильная дата.
Что видит игрок.
Цифры, разумеется, другие. Это отладочный режим. В рабочем режиме там будет что-то вроде «25 10 2015».
Как это работает. Для газоразрядных ламп требуется высокое напряжение, порядка 200В. Поэтому первым делом был собран блок повышения напряжения от 12В до требуемого значения, и схема коммутации ламп. Она основана на схеме из проекта Электронные часы-будильник, поэтому повторять описание электрической части не буду.
Программная часть — это прошивка для Arduino, принимающая по UART информацию о том, какую цифру отобразить на какой лампе. Кода там всего две сотни строк.
В цикле выполняется «развёртка» — по очереди включаем анод каждой лампы на 1 миллисекунду:
void loop() {
...
/* Scan */
for (int i = 0; i < N_lamps; ++i) {
selectDigit(dispDigits[i]);
selectAnode(i);
delay(1);
selectAnode(NO_LAMP);
delay(1);
}
...
}
А в прерывании от UART сохраняем информацию об отображаемых цифрах (разбивая пришедший байт на два полубайта):
/* ...
To control displayed values send packed data to UART.
Format: [ 7 6 5 4 | 3 2 1 0 ]
[ LAMP_ID | DIGIT ]
Each received byte describes one digit only.
... */
/* New digit(s) from UART */
void serialEvent() {
/* Read all bytes from serial port */
while (Serial.available()) {
/* Unpack data */
char inChar = (char)Serial.read();
char lamp = (inChar & 0xF0) >> 4;
char digit = (inChar & 0x0F);
/* Verify */
if (lamp > N_lamps || digit > 9)
continue;
/* Store */
dispDigits[lamp] = digit;
}
}
Всё вместе:
/* Tesla NIXIE controller
Device schematics copied from http://cxem.net/mc/mc206.php
There are 8 lamps. Each lamp can display digits 0..9
To control displayed values send packed data to UART.
Format: [ 7 6 5 4 | 3 2 1 0 ]
[ LAMP_ID | DIGIT ]
Each received byte describes one digit only.
Incorrect bytes silently drops.
NIXIE controller pin mapping
+--------------------------------+
| |
N/C | TXO +----------------+ RAW | N/C
| | | |
FTDI ====> | RXI | | GND | GND
| | | |
N/C | RST | | RST | N/C
| | | |
GND | GND | | VCC | VCC
| | | |
CATHODE_ADDR_BIT_2 | 2 | | A3 |
| | | A5 |
CATHODE_ADDR_BIT_1 | 3 | | A2 |
| | | A4 |
CATHODE_ADDR_BIT_0 | 4 | Pro Mini | A1 |
| | | |
ANODE_0 | 5 | | A0 |
| | | |
ANODE_1 | 6 | | 13 | CATHODE_ADDR_BIT_3
| | | |
ANODE_2 | 7 | | 12 | ANODE_7
| | | |
ANODE_3 | 8 | | 11 | ANODE_6
| +----------------+ |
ANODE_4 | 9 10 | ANODE_5
| GND A6 A7 |
+--------------------------------+
*/
/* NIXIE anodes count: */
const int N_lamps = 8;
/* Constant for invalid lamp ID */
const int NO_LAMP = N_lamps;
/* NIXIE cathode address width: */
const int W_addr = 4;
/* Anodes: */
const int anodeEnablePin[N_lamps] = {5, 6, 7, 8, 9, 10, 11, 12};
/* Cathodes: */
const int cathodeAddressPin[W_addr] = {4, 3, 2, 13};
/* Map of digits 0-9 to addreses: 0 1 2 3 4 5 6 7 8 9 */
const int digitToAddressMap[10] = {5, 9, 8, 7, 1, 0, 2, 6, 4, 3};
/* Display values (digits to show): */
int dispDigits[] = {0, 0, 0, 0, 0, 0, 0, 0};
void setup() {
Serial.begin(9600);
/* Prepare MPSA42 (anode) controller: */
for (int i = 0; i < N_lamps; ++i)
pinMode(anodeEnablePin[i], OUTPUT);
/* Prepare K155ID1 (cathode) controller: */
for (int i = 0; i < W_addr; ++i)
pinMode(cathodeAddressPin[i], OUTPUT);
}
/* Select lamp */
void selectAnode(int id) {
/* Switch-off all lamps */
for (int i = 0; i < N_lamps; ++i)
digitalWrite(anodeEnablePin[i], LOW);
/* Check lamp ID */
if (0 > id || id > N_lamps)
return;
/* Switch-on selected lamp */
digitalWrite(anodeEnablePin[id], HIGH);
}
/* Set address on K155ID1 */
void selectCathode(int addr) {
/* Check limits: */
if (0 > addr || addr > 9)
return;
/* Write address */
for (int i = 0; i < W_addr; ++i)
digitalWrite(cathodeAddressPin[i], (addr & (1 << i)) ? HIGH : LOW);
}
/* Set digit on NIXIE */
void selectDigit(int digit) {
/* Check limits: */
if (0 > digit || digit > 9)
return;
/* Set address using digit-to-address map */
selectCathode(digitToAddressMap[digit]);
}
void loop() {
/* Scan */
for (int i = 0; i < N_lamps; ++i) {
selectDigit(dispDigits[i]);
selectAnode(i);
delay(1);
selectAnode(NO_LAMP);
delay(1);
}
}
/* New digit(s) from UART */
void serialEvent() {
/* Read all bytes from serial port */
while (Serial.available()) {
/* Unpack data */
char inChar = (char)Serial.read();
char lamp = (inChar & 0xF0) >> 4;
char digit = (inChar & 0x0F);
/* Verify */
if (lamp > N_lamps || digit > 9)
continue;
/* Store */
dispDigits[lamp] = digit;
}
}
Вот так выглядел наш тестовый стенд до монтажа ламп в декоративную панель:
Блок управления машиной времени
Что видит игрок. Когда в «лаборатории» все загадки решены, открывается вход в машину времени. Но не сразу, а через некоторое время. Игроки заходят, устанавливают правильную дату, нажимают зелёные кнопки и начинается светозвуковая анимация.
Как это работает. Работа начинается при получении сигнала о прохождении комнаты «лаборатория». Включается генератор дыма, затем вентиляторы этот дым разгоняют. Через 10 секунд свет включается на полную мощность (светодиодные ленты установлены в стенах). Входные двери открываются, игроки заходят внутрь машины времени.
Cветовыми модулями управление идёт через DMX512. Всей силовой электрикой и вентиляторами — через реле.
Центральный блок управления
Вот он:
Точнее, так он выглядел сразу после сборки, когда на оборудование писалась документация, когда предполагалось, что «всё заработает с первого раза», что «все требования окончательные и изменениям не подлежат» и что «мы больше ничего нового доделывать не будем». Но так не бывает, и сейчас плата выглядит следующим образом:
Эта плата отвечает за работу всего квеста, на неё приходит информация практически со всего оборудования, и практически любое устройство можно «активировать» удалённо (если в игровой комнате что-то пошло не так).
Основной элемент — STM32F0Discovery (да-да, туда втыкается именно отладочная макетная плата). Как мы потом поняли, это было очень удачным решением. Во-первых, там встроенный программатор. Если нужно срочно обновить прошивку — нужен только ноутбук с установленным ST-LINK. Во-вторых, если сгорит микроконтроллер (такое было однажды, статического электричества на этом квесте более чем достаточно), то не нужно будет всю ночь переделывать весь блок управления. Вытащили сгоревшую плату — вставили новую.
Для связи с компьютером используется интерфейс UART. На плате центрального контроллера стоит преобразователь UART-USB (FT232RL), распаянный на USB разъём. Через него подключается компьютер оператора для управления квестом.
На микроконтроллере запущена FreeRTOS. Когда квест стартовал, запускается задача «кабинет». В этой задаче отслеживается, накрыта ли голова Горгоны мешком, и повёрнуты ли ключи. Если всё сделано, то открывается дверь в лабораторию, там же включается свет, а в кабинете свет диммируется. На этом задача «кабинет» завершается, установив флаги для «лаборатории». Затем то же самое происходит для «машины времени».
На компьютере оператора запущена программа, написанная на Qt. Для работы с виртуальным COM-портом мы взяли QtSerialPort. А интерфейс просто нарисовали в Qt Designer. Получилось как-то так:
На микроконтроллере каждой задаче (FreeRTOS) соответствует какой-то конкретный блок квеста. Эту часть я расскажу чуть подробнее, потому что где-то 90% всей логики квеста находися именно на этом микроконтроллере.
Например, посмотрим на самый короткий блок — «Кабинет», в котором мы проверяем всего два устройства. Создаётся задача так (добавляется в планировщик):
xTaskCreate( prvCabinetWatcherTask,
"Cabinet_Watcher",
configMINIMAL_STACK_SIZE,
NULL,
mainWATCHER_TASK_PRIORITY,
NULL );
Планировщик запустится после вызова:
vTaskStartScheduler();
И после этого запустится наша задача:
void prvCabinetWatcherTask( void *pvParameters )
{
/* Don't start while Cabinet is not enabled */
xEventGroupWaitBits(xOpenedLocks, ebBIT_QUEST_STARTED, pdFALSE, pdFALSE, portMAX_DELAY );
/* Wait for hardware to be ready */
vTaskDelay( mainWAIT_HARDWARE_READY_MS );
for( ;; ) {
EventBits_t uxCabinetConditions = xEventGroupGetBits(xCabinetEvents);
/* === Check that the head is covered (4 transistors are "opened") === */
if ( условие, при котором голова Горгоны считается накрытой )
xEventGroupSetBits(xCabinetEvents, ebBIT_HEAD_COVERED);
else
xEventGroupClearBits(xCabinetEvents, ebBIT_HEAD_COVERED);
/* === Check that the key was rotated === */
if (STM_TESLA_INPUTGetState(INPUT_KEYHOLE) == (uint8_t)Bit_RESET)
xEventGroupSetBits(xCabinetEvents, ebBIT_KEY_ROTATED);
else
xEventGroupClearBits(xCabinetEvents, ebBIT_KEY_ROTATED);
/* === Check ALL conditions for exit === */
if ((uxCabinetConditions & ebCABINET_IN_CONDITIONS) == ebCABINET_IN_CONDITIONS) {
STM_TESLA_RELAYOn(RELAY_CAB_LIGHT_DIM);
vTaskDelay( mainWAIT_HARDWARE_READY_MS );
STM_TESLA_RELAYOff(RELAY_CAB_LOCK_OUT);
STM_TESLA_RELAYOff(RELAY_LAB_LIGHT_OFF);
xEventGroupSetBits(xOpenedLocks, ebBIT_CABINET_DONE);
/* Suspend ourselves. */
vTaskSuspend( NULL );
}
/* Wait before next checking cycle. */
vTaskDelay( mainMIN_CHECKING_DELAY_MS );
}
}
В которой первым же делом мы «зависнем» на строке:
/* Don't start while Cabinet is not enabled */
xEventGroupWaitBits(xOpenedLocks, ebBIT_QUEST_STARTED, pdFALSE, pdFALSE, portMAX_DELAY );
И будем висеть бесконечно (portMAX_DELAY), пока не будет установлен бит ebBIT_QUEST_STARTED в битовой группе xOpenedLocks. То есть это такое условие начала работы квеста.
Забегая вперёд, скажу, что в другой задаче этот бит будет установлен автоматически через 3 секунды после включения, и система начнёт работу. Это нужно для отладочного режима (который может быть активирован именно в эти 3 секунды), в котором всё оборудование управляется только с компьютера, а автоматика полностью выключена.
Далее идёт бесконечный цикл, в котором создаётся битовая группа uxCabinetConditions в которой всего два бита — ebBIT_HEAD_COVERED и ebBIT_KEY_ROTATED.
EventBits_t uxCabinetConditions = xEventGroupGetBits(xCabinetEvents);
Когда выполнилось условие «голова Горгоны накрыта», устанавливается первый бит:
xEventGroupSetBits(xCabinetEvents, ebBIT_HEAD_COVERED);
А когда выполнилось условие «ключи в замках повёрнуты», устанавливается второй бит:
xEventGroupSetBits(xCabinetEvents, ebBIT_KEY_ROTATED);
Затем проверяем, что оба бита установлены:
if ((uxCabinetConditions & ebCABINET_IN_CONDITIONS) == ebCABINET_IN_CONDITIONS) { ... }
И если так, то:
— включаем в кабинете диммер,
— ждём 1 секунду, чтобы яркость ламп успела упасть,
— открываем (размагничиваем) замок из кабинета в лабораторию,
— включаем свет в лаборатории,
— устанавливаем бит «кабинет пройден»,
— засыпаем.
STM_TESLA_RELAYOn(RELAY_CAB_LIGHT_DIM);
vTaskDelay( mainWAIT_HARDWARE_READY_MS );
STM_TESLA_RELAYOff(RELAY_CAB_LOCK_OUT);
STM_TESLA_RELAYOff(RELAY_LAB_LIGHT_OFF);
xEventGroupSetBits(xOpenedLocks, ebBIT_CABINET_DONE);
vTaskSuspend( NULL );
Всё. Эта задача больше планировщиком не ставится. А та задача, которая висела в ожидании ebBIT_CABINET_DONE начинает свою работу.
Те задачи, которые отвечают за интерфейсы, устроены несколько сложнее. Там используются семафоры, на которых мы висим и ждём какого-то события из прерывания, а в прерываниях полученные данные записываются в очереди.
Компьютерная программа на Qt, с которой работает оператор, достаточно тривиальна. Протокол обмена данными по UART был спроектирован так, чтобы можно было через PuTTY командами типа '>?' проверить состояние системы (ответ '<OK'), командой '>!' перезагрузить микроконтроллер, командой '>R61' включить (1) шестое реле, а командой '>R60' выключить (0) его же. Программа была написана только для того, чтобы не вводить эти команды вручную.
Вот и всё. Это была «Тесла 1.0».
Что дальше?
После удачного запуска квеста на Курской мне предложили продублировать всё оборудование. Исходники лежали в репозиториях, схемы устройств тоже сохранились. Почему бы нет, я согласился. Так началась Тесла 2.0.
Раз всё делаем по-новой, было решено децентрализовать управление. Хотелось упростить самую сложную часть — STM32 с FreeRTOS. Вся логика квеста была на ней. Поддерживать систему становилось всё сложнее, да и количество свободных GPIO на STM32F0Discovery подходило к концу.
Поэтому все блоки управления были сделаны аппаратно, каждый в отдельной коробке:
Некоторые блоки управления были установлены в сами устройства.
По новым требованиям был ещё режим «полностью ручного управления». Встал выбор — или делать ещё одну коробку с ~20 кнопками и тумблерами, или писать программу, которая могла бы контролировать состояние всего этого железа. По-моему как раз в этот момент пришла идея о веб-интерфейсе.
Новая концепция
Веб-интерфейс — штука хорошая, только его надо было подружить с железом. Надо было сделать так, чтобы из браузера включалось любое оборудование и решалась любая задача.
Web-интерфейс решили запустить на RPi. Чтобы обмениваться данными с STM32 был выбран интерфейс SPI (как наиболее быстрый и надёжный в данных условиях). Для STM32 у нас было уже довольно много наработок, оставалось только добавить модуль для SPI. Для работы с SPI на Raspberry Pi взяли py-spidev. Из веб-серверов на Python выбор пал на Flask (потому что он простой, а в сложном разбираться времени уже не было). Для построения интерфейса оператора отлично подошёл jQuery.
Таким образом, на RPi у нас крутился веб-сервер, который через SPI общался с STM32.
Уже на этом этапе мы могли считывать состояние всех входов и выходов. То есть как угодно настраивать GPIO.
Новые требования
По прежнему стоял открытым вопрос с реализацией экрана «приветствия», добавленного в Теслу 2.0. Приветствие должно было демонстрироваться на мониторе в начале игры. Видеоролик должен был быть на четырёх языках. Язык выбирает оператор, который привёл игроков.
Управляющее устройство было готово заранее. С него можно было кнопкой запустить один из 4 видеороликов. Можно было даже специальной комбинацией перезагрузить или выключить Raspberry Pi, который был подключен к экрану. Но всё это не решало главной задачи — как сразу после окончания видео начать работу квеста (включить свет в кабинете), и как запустить видео удалённо, не нажимая эти кнопки вообще.
К тому времени ещё добавилась задача по аудиосистеме. Нужно было менять фоновый звук (который вы слышите в течение всей игры) при переходе из одной комнаты в другую.
Делать ещё кучу управляющих устройств для каждого нового требования совсем не хотелось. Поэтому мы соединили все Raspberry Pi в единую сеть, а данные между ними стали передавать через TCP сокеты. Туда вошла RPi из проектора, из «приветствия» и собственно сам веб-сервер.
Для работы с аудио купили внешнюю звуковую карту USB Audio.
После настройки Raspbian через неё удалось вывести нужный файл mp3.
Из «центральной» RPi мы смогли управлять всем остальным: видео на «Проекторе», на «Приветствии», фоновым звуком.
Новый интерфейс
Сейчас он выглядит так:
Все команды выполняются асинхронно. Обновление состояния происходит автоматически каждую секунду.
Если что-то пойдёт не так, оператор сразу увидит сообщение с описанием ошибки и меткой времени.
Правой колонки (с «автоподсказкой») операторы не увидят, т.к. «автоподсказку» выпилили из ТЗ. Аппаратную часть для неё тоже никто не делал. Там идея была такая: много ячеек, в каждой лежит карточка с подсказкой. Каждая ячейка может открываться независимо от других. Когда игроки хотят подсказку, им отвечает не оператор, а вываливается карточка.
Новые устройства
В Тесле 2.0 кроме системы управления поменялись некоторые управляющие устройства. Например, в консоли машины времени переменные резисторы заменили абсолютными энкодерами (стало работать намного стабильнее, особенно в крайних положениях; всё-таки чтение одного из 128 положений энкодера — это не измерение сопротивления через ADC), в проекторе для заслонки сделали электрический привод (по аналогии с автомобильным стеклоподъёмником), озвучили «Горгону» (чтобы она шипела, если её накрывают) и добавили звуки на все события в лаборатории. В этом нам помогли модули RS012.
Новая аудиосистема
С выбором аудиосистемы всё было сложнее. Задача была — озвучка событий, т.е. когда игроки правильно выставили стрелки на приборах — включить один звук, когда замкнули рубильник — другой звук, когда включили все тумблеры — третий звук. Когда выполнили вообще все условия — четвёртый звук, одновременно с открытием двери в машину времени.
Очевидно, что самое простое решение с Arduino MP3 Shield здесь не подходит. Любое событие может произойти одновременно с любым другим, то есть звук должен наложиться один на другой. Следующим возможным решением было поставить ещё одну Raspberry Pi, отслеживать входы, и запускать aplay на каждое событие. Можно запустить сколько угодно экземпляров, и задача вроде как решена. Но мы нашли ещё более простое и дешевое решение.
RS012, вот он:
Учитывая стоимость модуля и microSD на 4GB, ничто не мешает поставить пять таких модулей — по одному на каждый звук.
Отдельно надо сказать про управление этими модулями. На них стоят микросхемы со стёртой маркировкой. Как удалось выяснить, на 10-й ноге самой большой микросхемы во время воспроизведения высокий уровень. Во время паузы — низкий. Считывая через ADC её состояние, узнаём текущий режим. Кнопками play/pause и next управляем через GPIO микроконтроллера. Когда на карте памяти файл всего один, то нажатие кнопки next начинает воспроизведение того же самого файла с начала, даже после паузы.
Получился вот такой бутерброд из этих модулей и одной Arduino Pro Mini:
Ещё один был в самом начале этой статьи, где я рассказывал про устройство управления Горгоной.
Итоги
Здесь я просто оставлю структурную схему Теслы 2.0:
Она не претендует ни на точность, ни на полноту, но понять что к чему поможет.
Ещё в этом квесте есть устройства, которые создают «физические эффекты», такие как Лестница Якова, катушка Теслы (на квесте он чуть меньше, чем на картинке), пушка Гаусса, некое подобие дуговой лампы, и много чего ещё. Эти устройства разрабатывались специалистами в области физики.
И на каждом квесте конечно же есть операторы. Это люди, которые готовят квест перед игрой, следят за прохождением, подсказывают игрокам, и проявляют чудеса актёрского мастерства, если с электроникой возникают какие-то проблемы. Но это совсем другая история.