Создание своей эргономичной клавиатуры, часть 3: оживляем её

в 19:19, , рубрики: diy или сделай сам, qmk, гаджеты, клавиатура, Компьютерное железо, механические клавиатуры, пайка, пайка взрывом, периферия, печатная плата, Производство и разработка электроники, прошивка, сборка трезвым, язь

В предыдущей части были заказаны для клавиатуры платы и все её детальки. Они, конечно же, все приехали и настало время смешать всё вместе и вдохнуть жизнь с помощью свежепротранслированной прошивки. Если вы пропустили о чем речь — загляните в первую часть.

Сборка клавиатуры

Теперь все детали в наличии, пришла пора собрать всё вместе — что потребует по крайней мере паяльника, флюса и припоя. Я рекомендую иметь под рукой так же какой-либо тестер/мультиметр (прозвонка линий или омметр для проверки качества пайки, тестер диодов для проверки полярности после пайки). Так как я заказал SMD диоды, мне ещё понадобится лупа. Для удобства еще нужна "Третья рука" и дополнительное освещение.

Так что я заказал ещё инструментов:

  • FixPoint 51226: "третья рука" с лупой, подсветкой и подставкой для паяльника. База оказалась конечно тяжелая, но недостаточно для сборки клавиатуры из-за её размеров, так что использовал стаканчики как подпорки под плату. Крокодилы третьей руки не покрыты ничем прорезиненным, так что они впиваются и царапают плату. Учитывая размер плат, я не попробовал подставку под паяльник — использовал отдельную чтобы держать его в стороне.

  • JCD 908U Kit: набор всё-в-одном для пайки поделок: не самый плохой паяльник (100 ватт, очень быстро греется, набор разных голов — да, не самых лучших, но для валяния в подвале годами — сойдут), мультиметр, подставка, твердый флюс, флюс-паста, пинцет, инструмент для отпайки и всё вместе в небольшом складном чехле. Сам паяльник мне показался достаточно неплохим для эпизодической пайки, а вот к самому набору есть претензии: инструмент для отпайки отсутствовал (вместо него лежит отпаечная оплётка), флюс-паста протекла из коробки замазав всё вокруг (легко убралось тряпочкой и 70% спиртом). В общем, чувства остались смешанные: по цене самого дешевого паяльника что я могу купить рядом с домом я получил паяльник мощнее и качеством выше, да еще в довесок разных полезных вещей для пайки — так что спор на Али я не открывал.

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

Пришлось заказать/сходить до магазином за припоем и флюсом:

  • CFH Electronic solder EL 324: удобный и простой в использовании припой. Немного толстоват (1мм) но со флюсом внутри, что позволяет легко им паять. Я еще заказал Goobay Professional lead-free solder ради его 0.35мм диаметра, не так и не попробовал — мой страх что пайка будет плохая из-за толстого припоя оказался беспочвенным. Пайка была плохая из-за моих плохих навыков пайки :)

  • CFH Flux FM 342: самый обычный флюс-паста, единственный что я нашел в супермаркете рядом.

Рабочий коврик уже был дома, а потому можно приступать к работе! Подготавливаем рабочее место: в центре рабочее пространство с ковриком, 3я рука и свет, все детали слева, паяльник и пинцеты справа. Как-то вот так:

рабочий уголок к работе готов
рабочий уголок к работе готов

И да я растерял все остатки моих паяльных навыков :( Качество результата пропайки мне не понравилось, я даже не припаял один из диодов (просто повесив каплю ему на бок) и не заметил этого даже на тестовой прозвонке линий. Я не пришел к выводу что же лучше: сперва припаять hotswap сокеты или диоды первыми...

используем сверхсекретные технологии
используем сверхсекретные технологии

Важно: сокеты немного асимметричны, одна сторона квадратная а другая восьмиугольник который позволяет легче размещать там диод. Впрочем, диод влезает всё равно. И обязательно прозвонить все линии и все диоды после сборки и исправить непропай или полярность сразу же, пока еще паяльник горячий и все на столе.

пайка взрывом
пайка взрывом

После сборки и прозвонки матрицы переворачиваем плату и напаиваем процессорный модуль. Осталось вставить свичи:

сборка трезвым
сборка трезвым

После сборки основы надо прикрепить крышки и стабилизаторы…

... вот тут-то я и выяснил что у стабилизаторов есть два способа их размещения, и крышки заказанные не совпадают с размещением стабилизатора на плате. По сравнению со Stab элементом их надо бы сдвинуть на почти 3мм.

... но это неважно, поскольку высоты стабилизаторов недостаточно, они не подходят для случая когда свичи размещены на печатной плате! Они заточены для размещения свичей В плате. Впрочем, возможно они подойдут для ультра-тонких бабочек... В общем, со стабилизаторами разберёмся в последней части, а на пока я решил "пусть будет так" и пошел разбираться с прошивкой.

первый блин комом, зато второй будет EXEшником
первый блин комом, зато второй будет EXEшником

Сборка прошивки

Просто собрать матрицу и подключить её к процессору недостаточно, чтобы получить клавиатуру. Она еще должна быть видна в системе как клавиатура и работать как клавиатура. Что ж, ей нужна прошивка.

В поисках софта для создания клавиатур на базе RP2040 я наткнулся на пару своеобразных проектов:

У меня есть опыт работы низкоуровневым программистом и идея использовать интерпретируемый язык на микроконтроллере просто для запуска бесконечного цикла сканирования матрицы вызывает чувство глубокого внутреннего противоречия. Да, так можно. Нет, идея мне не нравится :) Так что приходим к следующему варианту:

Проект QMK меня порадовал: множество готовых фич но после сборки остаётся только то, что потребовалось. Эта прошивка явно будет более экономична по питанию а потому меньше беспокоить меня. Одна проблема: не поддерживает RP2040. Пока ещё.

Поскольку модуль очень дешевый и многоногий, он прекрасен для создания клавиатур (особенно со светодиодами, экранчиками и проч, когда надо много ног) фич-реквест уже открыт. И @KarlK90 уже подготовил пулл-реквест с поддержкой его.

А значит, уже можно воспользоваться!

Устанавливаем QMK

Чтобы поставить QMK под Windows, простейший способ это поставить подготовленную сборку MSYS с сайта https://msys.qmk.fm/. После установки запускаем "QMK MSYS" — все команды указанные ниже запускались в ней..

Сперва, нам понадобится сам QMK с поддержкой RP2040. Поэтому установку запускаем из соответствующего форка (когда пулл-реквест будет слит, из команды следует убрать всё начиная с “-b”). Поправьте путь для директории куда ставить под себя.

$ qmk setup -H D:/Keyboard/qmk_firmware 
      -b feature/raspberry-pi-rp2040-support KarlK90/qmk_firmware.git
...
Would you like to set D:/Keyboard/qmk_firmware as your QMK home? [y/n] y
Ψ QMK Doctor is checking your environment.
Ψ CLI version: 1.0.0
Ψ QMK home: D:/Keyboard/qmk_firmware
Ψ Detected Windows 10 (10.0.19044).
Ψ Git branch: feature/raspberry-pi-rp2040-support
Ψ Repo version: 0.9.43
Ψ All dependencies are installed.
Ψ Found ...
Ψ Submodules are up to date.
Ψ QMK is ready to go

Надо набраться терпения, скачивание репозитория и всех сабмодулей потребует времени. После пробуем посмотреть как создать новую клавиатуру:

$ qmk new-keyboard -h

... и выясняем что нужны еще пакеты “wheel” и “Pillow”. Их можно доставить запустив:

$ pacman -S mingw-w64-x86_64-openjpeg2 
$ pacman -S mingw-w64-x86_64-zlib
$ pacman -S mingw-w64-x86_64-freetype 
$ pacman -S mingw-w64-x86_64-libimagequant 
$ pacman -S mingw-w64-x86_64-libraqm
$ pip3 install —upgrade wheel
$ pip3 install —upgrade Pillow

Всё, все зависимости стоят, qmk работает, приступаем к сборке.

Создание прошивки для одной половины

Самая пора выбрать название для клавиатуры. Выдавать названия это самое сложное в программировании вообще. Я назвал её “dacobokb” как удобное, легко запоминаемое и краткое.

Клавиатура состоит из двух зависимых половин и самое простое чтобы заставить всё работать в сборе это идти по нарастающей. Сперва надо создать прошивку для одной половины, убедиться что всё железо работает, и уже потом думать о том как заставить половины работать вместе. К тому же, у меня пока только одна половина собрана.

Итак, создаём заготовку для прошивки. Так как я раскладку рисовал начиная с полноразмерной, для прошивки тоже беру “fullsize_ansi” как основу.

$ qmk new-keyboard -u datacompboy -n "Anton Fedorov" -l fullsize_ansi 
    -t RP2040 -kb dacobokb_l

В директории `D:Keyboardqmk_firmwarekeyboardsdacobokb_l` были созданы необходимые файлы, причешем их. Открываем `D:Keyboardqmk_firmwarekeyboardsdacobokb_l` как директорию проекта в Notepad++ (или любом другом редакторе на свой вкус).

(1) Правим файл `rules.mk`. Вот всё нужное содержимое:

MCU = RP2040

(2) Правим `info.json`. Чтобы заработало, надо выставить процессор в "RP2040", загрузчик в "custom" и задать ножки к которым подведена была матрица плате. (А если распаяно вручную — вы же записали на распечатке матрицы к каким ногам её подвели, да?)

{
	…
	"bootloader": "custom",
	"processor": "RP2040",
	"matrix_pins": {
		"cols": ["GP0", "GP1", "GP2", "GP3", "GP4", "GP5", "GP6", "GP7"],
		"rows": ["GP10", "GP11", "GP12", "GP13", "GP14", "GP15"]
	},
	…
}

Раздел раскладок "layouts" можно просто стереть — а можно на https://qmk.fm/converter/ вставить содержиме влкадки "raw data" с KLE в поле "Input" — и из поля "output" скопировать строку "layout": [...] и заменить ею в `info.json`.

В целом можно использовать любые pid/vid, но лучше избегать конфликтов, так что на странице https://pid.codes/1209/ есть список зарезервированных PIDов внутри VID 1209 выделенного для опенсорса. Там же размещена инструкция как зарезервировать один для своего проекта, если планируется его распространение. Если нет, то просто взять что-нибудь свободное для себя и всё.

(3) Создаём файл ".h" для макроса перевода раскладки — файл называем как клавиатуру. То есть я создал файл `dacobokb_l.h`. Вот именно сейчас потребуется сгенерированный ранее #define. Копипастим его как нам выдали:

#pragma once
#include "quantum.h"

// Copy-paste the #define macro that was earlier generated at the matrix PCB generation.
#define LAYOUT( 
	K00, K01, K02, K03, K04, K05, K06, K07, 
	K10, K11, K12, K13, K14, K15, K16, K17, 
	K20,      K22, K23, K24, K25, K26, K27, 
	K30,      K32, K33, K34, K35, K36, K37, 
	K40,      K42, K43, K44, K45, K46, K47, 
	K50,      K52, K53, K54,      K56  
) { 
	{ K00,   K01,   K02,   K03,   K04,   K05,   K06,   K07 }, 
	{ K10,   K11,   K12,   K13,   K14,   K15,   K16,   K17 }, 
	{ K20,   KC_NO, K22,   K23,   K24,   K25,   K26,   K27 }, 
	{ K30,   KC_NO, K32,   K33,   K34,   K35,   K36,   K37 }, 
	{ K40,   KC_NO, K42,   K43,   K44,   K45,   K46,   K47 }, 
	{ K50,   KC_NO, K52,   K53,   K54,   KC_NO, K56,   KC_NO }  
}

(4) Редактируем раскладку в `keymaps/default/keymap.c`, чтобы она соответствовала желаемой. Мне показалось удобным для читаемости переименовать слои, так что я добавил пару констант для базы и для Fn слоёв.

#define _BL 0
#define _FL 1

Затем поправил `[0] = LAYOUT...` на `[_BL] = LAYOUT(` и отредактировал назначенные клавиши для текущей половины. Поскольку раскладка базируется на полноразмере, в основном просто надо было удалить лишние клавиши и добавить Fn. Чтобы определить Fn клавишу воспользовался MO(_FL) (`MO`дификатор на `Fn-Layer`). Следом определил слой для Fn клавиш, добавив еще один элемент массива, где все Fn функции внесены, а остальные места заполнены `_______`, что означает "использовать значение со слоя ниже". Все слои требуют определять все клавиши.

[_FL] = LAYOUT(
	KC_CALCULATOR, KC_AUDIO_MUTE, KC_AUDIO_VOL_DOWN, KC_AUDIO_VOL_UP, KC_MEDIA_PREV_TRACK, KC_MEDIA_PLAY_PAUSE, KC_MEDIA_NEXT_TRACK, _______,
	_______, _______, _______, _______, _______, _______, _______, _______,
	…
),

Получившиеся файлы для половины лежат у меня на github.

После редактирования, компилируем прошивку:

$ qmk compile -kb dacobokb_l -km default
Ψ Compiling keymap with make —jobs=1 dacobokb_l:default


QMK Firmware 0.9.43
Making dacobokb_l with keymap default
...
Generating: ...
Compiling: ...
Assembling: ...
and finally:
Linking: ...
Linking: .build/dacobokb_l_default.elf                              [OK]
Creating load file for flashing: .build/dacobokb_l_default.hex      [OK]
Creating UF2 file for deployment: .build/dacobokb_l_default.uf2     [OK]
Copying dacobokb_l_default.uf2 to qmk_firmware folder               [OK]

Всё, прошивка готова. Зажимаем маленькую кнопочку на RP2040 модуле, подключаем USB провод, отпускаем кнопочку – в системе появляется новый диск (у меня E:). Чтобы залить прошивку просто копируем её туда:

$ cp D:/Keyboard/qmk_firmware/dacobokb_l_default.uf2 E:/

Файл копируется практически моментально, процессор перезагружается в режим клавиатуры и определяется в системе. Проверяем все клавиши в поисках тех, что не хотят работать. (Ага, у меня оказалась одна на левой — непропай диода и позднее одна на правой — непропай сокета). Поскольку всё работает — собираем вторую половину клавиатуры (и железо и прошивку).

К слову: протестировать Fn слой на левой половине можно напрямую (кнопка Fn там расположена), а вот на правой нужно либо временно переопределить в прошивке что-нибудь как `MO(_FL)` или дождаться когда будет готова прошивка для полной клавиатуры.

Кстати да, пора уже объединить половины!

Собираем прошивку для распиленной полной клавиатуры

QMK поддерживает работу распиленной клавиатуры когда любая из половин подключена по USB а другая соединена с ней по отдельной последовательной линии и передаёт ей результат опроса своей матрицы. На самом деле там чуть больше, чем только опрос матрицы (передача информации о модификаторах, управление подсветкой и проч) но для моей клавиатуры ничто больше не требуется — только чтобы Fn с левой половины работал на обеих.

У проекта есть некоторая документация о поддержке распиленых клавиатур: https://docs.qmk.fm/#/feature_split_keyboard, но бОльшую часть можно выяснить читая исходники. Как я понял (поправьте если я ошибаюсь!), у QMK есть следующие предположения:

  • Определение какая половина основная какая дополнительная идет либо по USB питанию либо по USB подключению.

  • Обе половины соединяются через одни и те же RX/TX пины / UARTы / I2C линии.

  • У обеих половин одинаковое количество строк и столбцов, но они могут быть подключены к разным ногам.

  • Прошивка определяет левая/правая половина на основании одного из:

    • Состояния определённой ножки при запуске;

    • Состояния USB при запуске;

    • По чтению состояния замкнутости (впаянный диод) на одном из пересечений матрицы.

Поскольку я рисовал платы еще не зная ничего из этого, мои платы:

  • левая и правая половины используют разные ноги для связи и они взяты с разных UARTов,

  • на левой и правой половине разное количество столбцов,

  • ряды и столбцы обеих половин заведены на разные ноги процессора так, что не осталось ни одной неиспользованной ноги и ни одна нога не пересекается между половинами.

Может, конечно, и можно найти общую дырку в матрице, чтобы на одной из половин её замкнуть диодом, но это не повлияет на факт того, что всё остальное всё равно уникально для половин, а потому нет смысла даже искать. Надо найти способ собрать уникальную прошивку для каждой половины так, чтобы они могли работать вместе. Я подумаю как сделать лучше в другой раз.

Настройки которые я привожу тут предполагают что будет использоваться аппаратный UART (драйвер "usart" из rules.mk и "SIO" из chibios).

(0) Копируем всю папку одной из прошивок для половины как базу для общей (я скопировал `dacobokb_l` ⇒ `dacobokb`).

(1) Правим `rules.mk`, чтобы добавить еще две строчки для включения режима распила и UART драйвер "usart"

MCU = RP2040
SPLIT_KEYBOARD = yes
SERIAL_DRIVER = usart

(2) Редактируем `info.json`, чтобы убрать оттуда “matrix_pins”: я не нашел способа задать разные ноги для половин через него. Еще можно удалить “layouts” или добавить от обеих половин.

(3) В `config.h` самые важные правки. Поскольку “matrix_pins” из `info.json` убрали, теперь все ноги задаются здесь, плюс нужно выбрать правильный драйвер для использования. в общем, добавляем важных дефайнов:

#pragma once
#include "config_common.h"

// Left half should have two fake columns to make left & right matrix equals
#define MATRIX_COL_PINS       { GP0,  GP1,  GP2,  GP3,  GP4,  GP5,  GP6,  GP7,  GP7,  GP7 }
#define MATRIX_ROW_PINS       { GP10, GP11, GP12, GP13, GP14, GP15 }

#define MATRIX_COL_PINS_RIGHT { GP16, GP3,  GP2,  GP17, GP18, GP20, GP21, GP22, GP27, GP28 }
#define MATRIX_ROW_PINS_RIGHT { GP26, GP19, GP4,  GP13, GP14, GP15 }

#define SPLIT_USB_DETECT
#define SPLIT_LAYER_STATE_ENABLE
#define SERIAL_USART_FULL_DUPLEX
#define HAL_USE_SIO  TRUE

Внимательный читатель заметит, что последний столбец для левой повторен трижды — это выравнивает количество столбцов на левой и правой половинах, но любые нажатия там будут подавлены через KC_NO, так что всё будет работать как надо.

Последние строки включают определение первичной клавиатуры по наличию USB соединения, обмен состоянием активного слоя между половинами (надо только для индикации, так как обработка для передач на компьютер идёт всегда на головной половине) и задают протокол обмена UART full-duplex через драйвер ChibiOS "SIO".

(4) Переименовываем заголовочный файл клавиатуры в новое имя (`dacobokb_l.h` ⇒ `dacobokb.h`) и меняем дефайн в нём так, чтобы задавал раскладку работал для полной клавиатуры из двух половин. Это делается потому, что основная половина занимается переводом нажатия в сигналы клавиш для обеих половин разом, а значит должна знать всю раскладку разом.

Я просмотрел имеющиеся раскладки и в основном там задается макрос когда все клавиши для половин заданы в ряд:

#define LAYOUT( 
	L01, L02, L03, L04,    R01, R02, R03, R04, 
	L11, L12, L13, L14,    R11, R12, R13, R14 
) { 
	{ L01, L02, L03, L04 }, 
	{ L11, L12, L13, L14 }, 
	{ R01, R02, R03, R04 }, 
	{ R11, R12, R13, R14 } 
}

но у меня половины имеют разное число столбцов, плюс у меня уже есть два отдельных макроса и готовые раскладки, поэтому я пошел другим путём — я просто склеил их последовательно, тем более, что внутри QMK хочет как раз такой формат. Чтобы выровнять размеры матриц для левой добавил по две KC_NO в каждой строке. Мне еще повезло что генератор дефайна для левой половины из-за того что столбцов всего 8, использовал одно знакоместо под номер столбца (Kxx) а для правой, где 10 столбцов, он использовал два знакоместа (Kxxx), так что никаких конфликтов в аргументах макро не было. Если бы были — поменял бы Kxx на левой на Lxx и на Rxx на правой.

То есть идея в том, чтобы поучить макро такого вида:

#define LAYOUT( 
	L01, L02, L03, 
	L11, L12, L13, 
	R01, R02, R03, R04, 
	R11, R12, R13, R14 
) { 
	{ L01, L02, L03, KC_NO }, 
	{ L11, L12, L13, KC_NO }, 
	{ R01, R02, R03, R04 }, 
	{ R11, R12, R13, R14 } 
}

Важно не забыть добавить запятую между левой и правой (впрочем, компилятор напомнит и громко ругнётся там).

Добавляем в файл еще прототип функции, которяа будет переопределять левая/правая половина:

bool is_keyboard_left(void);

(5) Создаём файл `${keyboard}.c` в которой определяем саму функцию определения половины. То есть в `dacobokb.c` вписываем:

#include "dacobokb.h"
#include "config.h"

bool is_keyboard_left(void)
{
#ifdef LEFT
#pragma message "kb.c: Compiled left version"
	return true;
#else
#pragma message "kb.c: Compiled right version"
	return false;
#endif
}

Это хардкодит определение половины на момент компиляции (Можно закомментировать #pragma, чтобы меньше было сообщений при компиляции).

(6) Еще надо задать настройки железа, выбрать ноги и сами UARTы в зависимости от половины. Создаем `mcuconf.h`, в который складываем

#pragma once
#include_next <mcuconf.h>
#include "config.h"

#undef  RP_SIO_USE_UART0
#undef  RP_SIO_USE_UART1
#ifdef LEFT
// LEFT: UART0 on 21/22 GP16/GP17
#  define SERIAL_USART_TX_PIN		GP16
#  define SERIAL_USART_RX_PIN		GP17
#  define PLATFORM_SIO_USE_SIO0     TRUE
#  define RP_SIO_USE_UART0          TRUE
#  define SERIAL_USART_DRIVER       SIOD0
#else
// RIGHT: UART1 on 11/12 GP8/GP9
#  define SERIAL_USART_TX_PIN		GP8
#  define SERIAL_USART_RX_PIN		GP9
#  define PLATFORM_SIO_USE_SIO1     TRUE
#  define RP_SIO_USE_UART1          TRUE
#  define SERIAL_USART_DRIVER       SIOD1
#endif

Это задает настройки на основании наличия `#define LEFT` в соответствующем `config.h`.

(7) Следующим шагом надо объединить раскладки вместе. Редактируем `keymaps/default/keymap.c` файл, складывая слои для обеих половин подряд. Опять же — не забываем добавить запятую между левой и правой :)

(9) Чтобы создать прошивки для левой/правой половин создаём внутри keymaps/ еще две директории: “left” и “right”.

(9) Для левой еще создаём `left/config.h` вписываем:

#pragma once
#include_next "config.h"
#define LEFT

Для правой половины такой файл не нужен, так как правую определяем по отсутствию дефайна.

(10) И последний шаг — раскладки для половин. Можно сделать копию, можно симлинк а можно просто создать `keymap.c` в обеих директориях  `keymaps/left/` и `keymaps/right/` с простым инклудом внутри:

#include "../default/keymap.c"

Все файлы клавиатуры лежат у меня на github.

Наконец всё готово, можно компилять!

$ qmk compile -kb dacobokb -km left
$ qmk compile -kb dacobokb -km right

Пришла пора залить прошивки. Берём левую половину, зажимаем на ней кнопочку на процессоре, подключаем USB, копируем прошивку:

$ cp D:/Keyboard/qmk_firmware/dacobokb_left.uf2 E:/

Пробегаемся по клавишам и проверяем что они работают.

Отключаем левую, берём правую половину, зажимаем кнопочку, подключаем половину, копируем:

$ cp D:/Keyboard/qmk_firmware/dacobokb_right.uf2 E:/

И проверяем клавиши правой половины. Они тоже всё еще должны работать.

Отключаем USB и, наконец, соединяем половины между собой по кабелю и подключаем USB к одной из половин. Тестируем всю клавиатуру. Тестируем наконец Fn+кнопки между половинами. Пробуем как она работает если USB подключить к другой половине. Повторяем тест. Наслаждаемся! :)

Клавиатура ожила! Самое время попользоваться её немного чтобы понять насколько она удобна, подходит ли раскладка, надо ли поменять назначение каких-либо клавиш. И всё ли хорошо с расположением клавиш под рукой. Как всегда, чем раньше обнаружена ошибка тем дешевле её исправление: если становится понятно что полученная раскладка ну точно не подходит, лучше не тратить ни время ни деньги на изготовление корпуса для неё — а просто начать с начала, поправить раскладку и так далее. Если же всё достаточно хорошо, продолжаем еще немного ею пользоваться и переходим к следующей части.

Продолжение следует

Следующая часть расскажет как завернуть полученную плату в корпус, а в последней статье цикла я расскажу всё, что узнал о создании своих кейкапов.

Эта часть также доступна на английском на Medium и LinkedIn.

Автор: Anton Fedorov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js