[Перевод] Делаем аппаратную кнопку выключения звука в Zoom

Недавно Instagram показал мне рекламу специальной кнопки для выключения звука в Zoom, которая оказалась для меня уж очень актуальной.

acdauzxubg_j-1m071wtmv5q4ow.jpeg

Да, я каждый день на созвонах в Zoom. Да, у меня есть ребёнок, который часто вмешивается в эти звонки. Алгоритм выдачи рекламы Instagram, ты победил, мне нужна эта кнопка.

Но у меня есть предубеждения. В Instagram рекламируется проект с Kickstarter. К тому же, я не хочу делать свой вклад в доходы от рекламы Facebook, даже нажимая на этот ролик. Выражение Дженни Оделл «Бесплатных часов не бывает» полностью описывает мою точку зрения на качество продуктов в рекламе Instagram. Кроме того, мой лимит на финансирование проектов с Kickstarter практически исчерпался.

Я поддерживал множество проектов на Kickstarter, и иногда результат этих проектов напоминал мне гамбургер, который Майкл Дуглас получил в фильме «С меня хватит!».


Так что давайте соберём такую кнопку сами.

Первое, о чём стоит задуматься: какую кнопку мне будет приятно нажимать?

Я люблю клавиатуры с переключателями Cherry MX. Существует три типа механических переключателей: линейные, тактильные и кликающие. Линейный — простейший тип переключателя, перемещающийся вверх-вниз почти без обратной связи. У тактильных переключателей в середине хода есть выступ, позволяющий почувствовать, когда произошло нажатие клавиши. А кликающие переключатели имеют более сильную тактильную обратную связь И производят слышимый клик при нажатии.

В обычной ситуации мы бы купили тестер переключателей, чтобы разобраться, какой тип нам отзывается, а ещё опросили бы своих коллег, какой звук должна издавать клавиатура, чтобы они нас не убили. Но мы посередине пандемии COVID, поэтому коллег рядом нет! Выберем переключатель Cherry MX Blue с удобной тактильной обратной связью, который при этом чрезвычайно громкий. На сайте Cherry MX этот переключатель называют «кликающим и заметным», но это ещё очень слабо сказано.

rnce29_rq99ak3lpzoysiv6lnmc.jpeg

Выглядит красиво, но можно придумать и кое-что получше. Если мне приятно нажимать переключатель Cherry MX Blue, то не будет ли ещё приятнее нажимать комически большой Cherry MX Blue?

И это Novelkeys Big Switch.

jpbcpjmjkdrnqvv5doz3bapkb84.jpeg

Он в 4 раза больше по каждой из размерностей и в 64 раза больше по объёму, чем обычный переключатель. У него даже есть огромный колпачок!
nzgd-vtp1g7-lz9h2v5p8piz_e0.jpeg

К сожалению Большой Переключатель не продаётся в корпусе, поэтому мне пришлось воспользоваться 3D-печатью. Красивый корпус нашёлся на Thingiverse: NovelKeys Big Switch Case. Всегда стоит поискать ремиксы на случай, если кто-нибудь решил усовершенствовать исходный дизайн. В данном случае нашёлся ремикс, в который добавлен отсек для Pro Micro, а переключатель устанавливается более плотно, поэтому я напечатал его.
Теперь, когда у нас есть корпус, нужна плата, которую мы вставим в него и прикрепим к переключателю.

У Pro Micro есть чип ATmega32U4, позволяющий эмулировать устройство USB HID, например, USB-клавиатуру. К тому же, эта плата имеет маленький размер.

2b6ait3gisev9ro1nllcolzo9gk.jpeg

В нижней части Большого Переключателя есть два металлических контакта.
hrji6z_ntcxctdzkvxz5acdsmww.jpeg

При нажатии клавиши внутри переключателя происходит замыкание цепи между этими двумя контактами.
g-t8fvqila1watxxtii8cph4xmk.gif

Взглянем на расположение контактов Pro Micro:
wrobkgot5immzc7l-cgliaerpie.jpeg

Можно подключить GND к одному металлическому контакту, а Pin 2 — ко второму. Pin 2 — это контакт цифрового ввода-вывода, который считывает HIGH, когда клавиша нажата, и LOW, когда нет.

Было бы ещё здорово иметь какой-нибудь наглядный индикатор состояния Mute, поэтому можно добавить светодиод.

Я заказал светодиод размером 10 мм:

b61b0wyk0cn6elqgq1so3a7rlvw.jpeg

И резистор на 220 ом:
mjn9svcvuf5fauk1_7nxsy9zfau.jpeg

Длинная нога светодиодов подключается к PWR, а короткая — к GND. Мы вставим резистор между длинной ногой и другим контактом, чтобы снизить величину тока. Я выбрал Pin 9 в нижней части платы. Короткую ногу я соединил с GND. Мне показалась полезной эта страница о светодиодах и резисторах.

Между платой и переключателем я припаял такой провод 20 AWG:

93-r4_3533tgkdnz_qlsr7smzrc.jpeg

В результате получилась вот такая конструкция:
fvcivjbrwiudf_5kkjo8f07niz8.jpeg

Просто запихнём всё это в наш напечатанный корпус:
yyd9tsewbj9ea7kajo-o87zfatc.jpeg

Теперь нужно написать код.

Я начал с кода, который Sparkfun написала для создания огромной Кнопки Сохранения, и немного его изменил.

Принцип заключается в следующем: при нажатии клавиши она посылает сочетание горячих клавиш Zoom включения и отключения звука (на Mac это Cmd-Shift-A). Нужно будет изменить настройки Zoom, чтобы это сочетание клавиш распознавалось, даже когда Zoom находится не в фокусе. Поставим флажок Enable Global Shortcut:

49sze68gqqrkrbeadvwodase0hm.png

Также мы хотим включать или выключать светодиод после каждого нажатия на клавишу. Я решил, что светодиод будет аналогом лампочки «В эфире» — когда синий светодиод горит, звук у меня включен и люди слышат, что я говорю.

Но если просто включать-выключать светодиод при каждом нажатии на клавишу, то как сохранять синхронизацию с состоянием Mute в самом Zoom?

Удобство Pro Micro заключается в том, что она имеет и последовательное подключение. Обычно оно используется для печати отладочной информации в Arduino IDE, но мы можем использовать его для обеспечения синхронизации с состоянием включения звука в Zoom.

Вот код, который мы загружаем в саму Pro Micro:

#include "Keyboard.h"

// OS parameters
typedef enum {
  LINUX,
  WINDOWS,
  MAC
} os_types;

// Change this to your operating system
const os_types OS = MAC;

// Pins
const int btn_pin = 2;
const int led_pin = 9;

// Constants
const int debounce_delay = 50;              // ms

// Globals
int btn_state = HIGH;
int btn_prev = HIGH;
unsigned long last_debounce_time = 0;
int os_ctrl;
int led_state = LOW;

void setup() {

  Serial.begin(57600); // opens serial port, sets data rate to 57600 bps
  
  // Set up LED and button pins
  pinMode(btn_pin, INPUT_PULLUP);  // Set the button as an input
  pinMode(led_pin, OUTPUT);
  digitalWrite(led_pin, led_state);

  // Begin keyboard
  Keyboard.begin();

  // Switch to correct control/command key
  switch(OS){
    case LINUX:
    case WINDOWS:
      os_ctrl = KEY_LEFT_CTRL;
      break;
    case MAC:
      os_ctrl = KEY_LEFT_GUI;
      break;
    default:
      os_ctrl = KEY_LEFT_CTRL;
      break;
  }

  // Get initial timestamp

  Serial.println("started");
  
}

void loop() {

  // Read current state of the button
  int btn_read = digitalRead(btn_pin);

  // Remember when the button changed states
  if ( btn_read != btn_prev ) {
    last_debounce_time = millis();
  }

  // Wait before checking the state of the button again
  if ( millis() > (last_debounce_time + debounce_delay) ) {
    if ( btn_read != btn_state ) {
      btn_state = btn_read;
      if ( btn_state == LOW ) {

        // Send cmd+shift+a
        Keyboard.press(KEY_LEFT_SHIFT);
        Keyboard.press(os_ctrl);
        Keyboard.press('a');
        delay(100);
        Keyboard.releaseAll();

        Serial.println("pressed");

        if (led_state == LOW) {
          led_state = HIGH;
        } else {
          led_state = LOW;
        }
        digitalWrite(led_pin, led_state);

      }
    }
  }

  // Remember the previous button position for next loop()
  btn_prev = btn_read;

  if (Serial.available() > 0) {

    String incomingString = Serial.readStringUntil('\n');

    if (incomingString == "muted") {
      led_state = LOW;
    } else if (incomingString == "unmuted") {
      led_state = HIGH;      
    }

    digitalWrite(led_pin, led_state);
    
  }
  
}

Дальше мы можем добавить Applescript, сообщающий о текущем состоянии Zoom. Я нашёл Zoom-плагин для устройства Streamdeck, содержавший исходный Applescript, и изменил его так, чтобы он сообщал о том, открыт ли Zoom, и каково состояние его звука. Также я модифицировал скрипт, чтобы он выводил JSON.
set zoomStatus to "closed"
set muteStatus to "disabled"
tell application "System Events"
	if exists (window 1 of process "zoom.us") then
		set zoomStatus to "open"
		tell application process "zoom.us"
			if exists (menu bar item "Meeting" of menu bar 1) then
				set zoomStatus to "call"
				if exists (menu item "Mute audio" of menu 1 of menu bar item "Meeting" of menu bar 1) then
					set muteStatus to "unmuted"
				else
					set muteStatus to "muted"
				end if
			end if
		end tell
	end if
end tell

copy "{\"mute\":\"" & (muteStatus as text) & "\",\"status\":\"" & (zoomStatus as text) & "\"}" to stdout

Если теперь мы запустим его во время звонка в Zoom, то он будет выводить примерно такое:
$ osascript get-zoom-status.scpt
{"mute":"muted","status":"call"}

Затем я написал небольшое приложение на Node, используемое в качестве посредника между Pro Micro и этим скриптом:
const { exec } = require('child_process');

const SerialPort = require('serialport');
const Readline = require('@serialport/parser-readline');
const port = new SerialPort('/dev/tty.usbmodemHIDPC1', {
    baudRate: 57600
});

var checkStatus = function() {
    console.log('Checking status...');
    exec('osascript get-zoom-status.scpt', (error, stdout, stderr) => {

        if (error) {
            console.error(`exec error: ${error}`);
            return;
        }

        var status = JSON.parse(stdout);
        if (status.mute == 'unmuted') {
            port.write('unmuted');
        } else {
            port.write('muted');
        }

    });
}

const parser = port.pipe(new Readline({ delimiter: '\r\n' }))
parser.on('data', function (data) {
    if (data == "pressed") {
        console.log('Button pressed.');
        checkStatus();
    }
})

checkStatus();
setInterval(checkStatus, 30000);

Этот скрипт выполняет две задачи. При нажатии кнопки он отправляет плате Pro Micro через последовательный порт команду «нажато», при этом запускается Applescript для определения текущего состояния звука в Zoom. Затем он отправляет плате Pro Micro команду «звук выключен» или «звук включен», благодаря чему светодиод переключается в соответствующее состояние. Также я создал таймер, запускающий скрипт через каждые 30 секунд на случай, если я случайно отключу или включу звук через интерфейс Zoom, а не через кнопку, в противном случае состояние будет обновляться только при нажатии кнопки.

Вот как выглядит кнопка при использовании в звонке через Zoom:


Пожалуйста, поддержите мой Kickstarter — шучу, у меня нет никакого Kickstarter, но, надеюсь, теперь вы сможете создать такую кнопку сами.

На правах рекламы


Закажите сервер и сразу начинайте работать! Создание VDS любой конфигурации в течение минуты. Эпичненько :)

8p3vz47nluspfyc0axlkx88gdua.png

© Habrahabr.ru