Потроганное ИИ фото результата
Потроганное ИИ фото результата

Дисклеймер: весь код проекта написан Claude в режиме диалога. Данная статья процентов на 20 написана им же - по результатам работы над проектом. Не пугайтесь и не плачьте, увидев типичное для ИИ форматирование и слог - если они остались, значит меня устроили его формулировки.

Предыстория и постановка задачи

Однажды жизнь сложилась так, что количество выкуриваемых за день сигарет выпало за грани разумного. Ну то есть совсем за грани - 2+ пачки в день. Поскольку здоровье не казенное, решил немножко сбавить пыл и вернуться к вменяемому количеству. Так я познакомился с трекерами сигарет. Кто не в курсе - это такой класс программ для смартов, который позволяет:

  1. посмотреть как давно ты курил

  2. посмотреть сколько на сегодня уже скурил

  3. отметить очередную сигарету. Ну или не отметить, если тебе не нравятся предыдущие два пункта.

Надо сказать, штука оказалась эффективной. Не чтобы бросить, конечно, хотя авторы программ их обычно так и позиционируют - но чтобы не закуривать "раньше чем через 40-50-60-70 минут от прошлой сигареты" - вполне. В результате загнал количество выкуриваемого в устраивающий меня диапазон и там и держусь.

Практически с первых месяцев использования я осознал недостатки примерное всех программ на рынке (перебрал с десяток). Главное - скорость и необходимость доступа к телефону. Ни одна не поддерживала виджетов, для всех надо как минимум достать телефон, разблокировать, найти программу, запустить, найти кнопку, найти время, кликнуть. Была (да и осталась) мысль сделать навык Сири, но мы же не ищем легких путей.

Поэтому я начал искать альтернативы. Часы - нет, не умеют. По крайней мере бюджетные не умеют. Думал отреверсить что-нибудь, но мои навыки хака и программирования кажется не очень. Следующими по списку были тасбих-счетчики, они же электронные четки. На рынке есть симпатичные, у них работает кликер, но они предназначены только для количества и не регистрируют интервалы. Мультитаймеры для спортсменов - я не нашел с нормальной эргономикой, и у них тоже так себе с интервалами.

И вот пришло начало июня. И на меня внезапно выпал из ящичка зачем-то когда-то купленный nice!nano nrf52840. Из соседнего ящичка выпал OLED-дисплей 0,91", ну и я вспомнил что у меня давно валяется аккум 161040. 2 недели до ДР любимой жены, которая тоже пользуется трекером сигарет. А почему бы и не да - подумал я. И понеслось

Концепция

Итак, что должно получиться.

Устройство. Компактный брелок — чтобы лежал в кармане рядом с ключами. Одна кнопка, маленький OLED-экран, батарея на несколько недель автономной работы. Симпатичное и тактильно приятное - жена будет пользоваться. Стимпанк/постапок приветствуетеся, но не обязателен.

Экран. Включается по нажатию и гаснет через несколько секунд — чтобы не жечь батарею. Показывает две цифры: сколько сигарет сегодня и сколько времени прошло с последней. Шрифт логотипный моноширинный, похожий на терминалы из "Матрицы".

По отдельному жесту — экран часов, крупные цифры на всю высоту. За рамками функционала, но раз уж есть RTC - надо использовать.

Отдельная история — треугольники. Рядом с каждой цифрой стоит маленький ▲ или ▼. Аналитика в зачаточном состоянии: треугольник у количества сигарет показывает сравнение с вчерашним днём в аналогичный момент времени: ▼ — сегодня курю меньше чем вчера к этому часу, ▲ — больше. Треугольник у таймера — сравнение текущего перерыва с предыдущим: ▼ — перерыв короче чем прошлый, ▲ — длиннее.

В таком виде считывать именно цифры необязательно - для "подсознательной аналитики" принятия решения - курить иль не курить, достаточно треугольника.

Справа — индикатор заряда батареи, шесть горизонтальных полосок снизу вверх.

Управление одной кнопкой. Вся логика строится на трёх типах нажатий и состоянии экрана (вкл/выкл):

Жест

Экран выкл

Экран вкл

Тап

Показать статистику

Отметить сигарету

Даблтап

Показать статистику

Отменить последнюю

Длинное

Показать часы

Включить BLE

Состояние экрана фиксируется в момент нажатия — чтобы не было путаницы если экран успел погаснуть пока держишь кнопку.

Синхронизация. По BLE (Nordic UART Service) брелок отдаёт лог — список unix-timestamp'ов, по одному на сигарету. Приложение на ноутбуке или телефоне забирает лог и отправляет на сервер. Заодно синхронизирует время на брелоке — RTC держит время пока есть питание, но при разряде сбрасывается.

Бэкенд и PWA. Простой REST API на FastAPI, SQLite, пара эндпоинтов. PWA с Web Bluetooth — открываешь в Chrome, подключаешь брелок, синхронизируешь. Статистика по дням, график, дельта к предыдущему дню.

Статистика в PWA. Ещё одна вещь которую я привык видеть в телефонных трекерах — статистика. График по дням, сколько в среднем, динамика по неделям. Зачем это надо - ну я не очень понимаю, возможно при постановке целей уровня "за три месяца увеличить промежутки на N минут или снижать количество сигарет на 1 каждые 2 недели", но я этим не пользуюсь и не планирую. В PWA добавил на всякий случай аналитику: переключатель сегодня/неделя/месяц, столбчатый график (по часам для текущего дня, по дням для периода), список с дельтой к предыдущему дню.

Железо

Микроконтроллер. Nice!nano — клон с nRF52840, купленный давно и не очень понятно почём. BLE из коробки, встроенный контроллер заряда LiPo, компактный форм-фактор а-ля Pro Micro. Умение глубоко и крепко спать. Плюс Adafruit делает для nRF52840 нормальный Arduino BSP, что для меня было важно.

Дисплей. OLED 0.91", 128×32 пикселя, I2C. Тоже из ящика.

Аккумулятор. LiPo 101640 от электронной сигареты — плоский, ~200 мАч. Тоже из ящика. Зарядка через USB-C платы, контроллер заряда встроенный.

Кнопка. Тактовая, припаяна к свободному пину. Плюс отдельная кнопка на RST — надоело тыкать пинцетом при прошивке. В корпусе она доступна через отверстие под скрепку.

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

Детектив с железом.

Первый nice!nano из ящика оказался нерабочим. Симптомы: USB-диск не появляется, двойной ресет не помогает, плата просто мигает. Попытался реанимировать через SWD — взял у знакомого на погонять J-Link клон, подпаял, установил nRF Connect Programmer, OpenOCD, пытался прошивать драйверы через Zadig. Выяснилось что чип живой, но напряжение питания проседает до 1.8В — аппаратная неисправность, лечению не поддаётся. Плата в мусор, заказываем новую. Дедлайн перестал казаться далеким, а вечер - томным.

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

Клод настаивал что может. Я решил пойти другим путем: нашел какой-то проект для nice!nano с таким дисплеем, он оказался на circuitPy, и экран заработал! Начал разбираться с маппингом пинов — оказалось что в Adafruit BSP для Feather nRF52840 пины SDA/SCL по умолчанию смотрят на P0.12 и P0.11, а физические пины платы — P0.17 и P0.20. В CircuitPython, в REPL запросил board.SDA и board.SCL — получил правильные номера. Вручную исправил variant.h в BSP PlatformIO и экран показал долгожданное "Hello". Ну и сразу вгрузил визуал-концепцию

Прошивка

Еще один дисклеймер: поскольку прошивку, бэкенд и PWA писал не я, не мне и разделы эти заполнять. Мой уровень программирования примерно соответствует "понимаю, но сказать не могу". Поэтому слово Claude, текст не правлю из уважения к автору, свои примечания - курсивом. Если возникнут вопросы по данному разделу (а еще лучше - советы что переделать), пишите обязательно, обсужу с автором и может внедрю, автор сговорчивый.

Стек: PlatformIO + Arduino framework, Adafruit nRF52 BSP, библиотеки U8g2 (дисплей), RTClib (часы), Adafruit LittleFS (файловая система во Flash), Bluefruit (BLE).

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

Логика простая: фиксируем состояние экрана и время нажатия, при отпускании смотрим длительность. Длинное (>1 сек) — обрабатываем сразу. Короткое — ждём 500мс, не пришёл ли второй тап. Дребезг убираем не задержкой при отпускании (она съедала второй тап даблтапа), а минимальной длительностью нажатия: короче 20мс — игнорируем.

Дисплей. U8g2 вместо стандартного Adafruit SSD1306 — ради шрифтов. Конкретно u8g2_font_logisoso24_tn — моноширинный, только цифры, крупный, с нужной эстетикой. Треугольники рисуем примитивами drawTriangle — в шрифтах их нет. Индикатор батареи — шесть прямоугольников справа, заполняются снизу вверх. Напряжение батареи читаем с пина P0.04 через аналоговый вход, коэффициент делителя подобрали эмпирически через CircuitPython.

Хранение. Каждая сигарета — unix-timestamp в текстовом файле на внутренней Flash (LittleFS). Одна строка — одна запись. При старте читаем сохранённое время RTC из отдельного файла — чтобы часы не сбрасывались при перезагрузке. Статистика считается на лету при каждом открытии экрана: количество за сегодня, за аналогичный период вчера, время последней записи и длительность предыдущего перерыва — для треугольников.

BLE. Nordic UART Service поверх Bluefruit. Брелок принимает текстовые команды:

  • TIME:<timestamp> — синхронизация времени, сохраняется во Flash

  • GET — отдать весь лог, завершить строкой END

  • ADD:<timestamp> — добавить запись

  • CLEAR — очистить лог

Реклама включается по жесту (длинное нажатие с включённым экраном), идёт 60 секунд с обратным отсчётом на дисплее. После подключения браузера — показывает "BLE OK".

Сон. __WFE() (Wait For Event) через SoftDevice — процессор останавливается до следующего прерывания от кнопки. Не настоящий deep sleep, но потребление падает существенно. Уходим в сон после гашения экрана, если нет активного BLE.

Бэкенд

Тут всё скромно — задача простая, решение соответствующее.

FastAPI + SQLite + uvicorn, развёрнуто на домашнем сервере за Apache с Let's Encrypt. Три эндпоинта:

  • POST /smoke — добавить запись (timestamp, источник: brелок/веб/ручной ввод)

  • DELETE /smoke/{id} — удалить запись

  • GET /stats?days=30 — статистика за период, сгруппированная по дням

Авторизация — Bearer токен в заголовке, один на всё. Не OAuth, не JWT — просто строка в .env. Личный инструмент, не продукт.

CORS открыт — чтобы PWA с GitHub Pages могла стучаться на домашний сервер без плясок с заголовками.

Systemd юнит, запускается при старте сервера, перезапускается при падении. Всё.

Суммарно — около 100 строк Python. Меньше времени на написание чем на придумывание названия эндпоинтов. (Меня эта строка так умилила, что я даже попросил оценить время на то и другое, предсказуемо нормального ответа не получил - автор так шутить изволил)

PWA

Веб-приложение живёт на домашнем сервере рядом с бэкендом — один хост, один nginx, один сертификат чтобы править всеми. При желании легко портируется на GitHub Pages, код не привязан к инфраструктуре.

Один HTML файл, никаких фреймворков. CSS переменные для тёмной темы, canvas для графиков, Web Bluetooth API для связи с брелоком.

Главный экран. Большая круглая кнопка "ЗАКУРИЛ" — одно нажатие, запись уходит на сервер. Счётчик за сегодня, время с последней сигареты, кнопка отмены последней записи.

Статистика. Переключатель сегодня/неделя/месяц. График столбиками — по часам для текущего дня, по дням для периода. Список с дельтой к предыдущему дню — те же треугольники что на брелоке, только в браузере.

Синхронизация с брелоком. Web Bluetooth работает только в Chrome и только на HTTPS или localhost — важное ограничение. На iPhone не работает вообще, Apple не поддерживает Web Bluetooth.

Основной сценарий синхронизации — ноутбук с Chrome. Нажимаешь "Подключить брелок" → Chrome показывает диалог выбора BLE устройства → выбираешь SmokeFob → приложение отправляет TIME:<timestamp> для синхронизации часов → затем GET для получения лога → новые записи отправляются на сервер. Занимает секунд десять.

Добавить сигарету вручную можно с любого устройства через браузер — смартфон, планшет, рабочий компьютер. Это оказался неожиданно востребованный сценарий: не всегда брелок под рукой, зато браузер всегда.

Кэш. Данные кэшируются в localStorage — при перезагрузке страницы счётчик не сбрасывается в ноль пока не придёт ответ от сервера.

Корпус

А вот тут Claude мне не поможет. К сожалению или к счастью.
В анамнезе я инженер-станочник, ну и немножко навыков осталось, поэтому метод изготовления был понятен сразу - фрезеровка. Но что фрезеровать? Как? Зачем? Доколе? Едят ли курицу руками? Вопросов больше чем ответов. Первая мысль была - взять алюминиевую пластину и фрезернуть два полукорпуса.
Воркфлоу понятен - отрисовываем имеющиеся в наличии детальки примитивами хотя бы примерно в SolidWorks, компануем, прикидываем корпус. Получаем наружные размеры и из бруска пластелина штангенциркулем обрезаем габаритный макет - штука тактильная, размеры с формой важны. Уже на этом этапе я понял, что максимально плотная компоновка неэргономична - ощущения "маешь вещь" не возникает, надо удлинять.

Вторая компановка показалась удачней:

Ну или если возвращаться к первой - то может приспособить дубовую палку, которая непонятно зачем несколько лет лежит в цеху?

Просто выборка, внутрь выборки - электронные потроха, снаружи.... черт, что же снаружи? А, медная пластина. Ладно, пошел искать медную или латунную пластину толщиной в миллиметр. И тут факап. Нет у меня в хозяйтве таких. И где взять непонятно, уже пятница вечер.

Поехал в сантехмагазин посмотреть на трубы. Дай, думаю, разрежу и раскатаю - будет мне пластина. Но в процессе разглядывания разного обнаружил соединительную медную муфту 28 мм под пайку и прям влюбился. На удивление она оказалась 39,2 мм в длину - ровно по размеру экрана. И это определило её участь.

Обратно в цех, втулку в тиски, нежно гнем - и очередной факап (ну пятница же вечер, куда без этого), втулка гнется в восьмерку. Ок, время солида. Считаем внутренние размеры корпуса, сохраняя периметр втулки, получается 11*38 мм, примерно в размер из той самой дубовой палки вырезаю оснастку - разрезанный на клинья брусок, и потихоньку на нем обстукиваю.

Получилось, но конечно следы от молотка (тоже медного). Шлифмашинка и точило, шкурка и надфиль. Ок, можно спать.
В субботу с утра - снова солид. Отрисовываю уже все полностью:

боковые панельки из дуба - готовлю файл под фрезеровку и ставлю фрезероваться. Естественно обнаруживаю в очередной раз что припуски придуманы не зря - примерно ничего не влазит. Хотя есть исключения - некоторые элементы торчат.

Дорабатываю уже в нормальные размеры:

Спиливаю с заготовки ножовкой и меряю. Все подходит. Оставляю на сладкое на понедельник.

Сборка

В понедельник я обнаруживаю 3 вещи:

  1. У меня нет радиусной обгонной фрезы R3. Ну точней есть, но не у меня. Или у меня, но не знаю где.

  2. Я забыл про кнопку. Ну то есть кнопка есть как электронный компонент, но кнопки нет как пипки, на которую жать. А ведь я её любовно отрисовал в солиде. И станка токарного для обработки медного стержня 3-6 мм тоже нет.

  3. Имеющаяся уже работающая электроника собрана на огрызках витой пары и мне стыдно знать как это будет выглядеть внутри.

Ладно, решаем.

  1. Фаски - это красиво. Очень. Лучше скруглений. Да, я очень убедительный, особенно когда фрезы все равно нет.

  2. Две попытки обточить медную жилу 10мм2 в шуруповерте об надфиль не дали радости - некрасиво, шершаво, погано. Выручил дедовский гвоздильный метод: зажимаем в тиски две стальные пластины, просверливаем в стыке отверстие на глубину нужного "гвоздя", вставляем туда пруток и обплющиваем головку. Первый раз в жизни так делал, но получилось и получилось неплохо.

  3. На замену сгоревшей плате из ящика я купил сразу пару nRFок. Дисплей решил попробовать первый, который не заводился. На монтажный провод разобрал кусок сломанного USB. Ну и аккум в последний момент решил поменять на 5*20*30, тоже из загашника.

Ок, все проблемы решены. Собираем, матерясь - естественно, я обрезал провода коротковато и сборка представляет из себя тетрис на высокой сложности. Естественно, я забыл элемент, поджимающий экран - на его место втыкаем кусок уплотнения. Естественно, последняя крышка встает с матюками. Но встаёт. И вот - победоносен.

Это уже без ИИ, фотографировал на тапок, обрезанный шрифт - артефакт люминисцентного освещения.
Это уже без ИИ, фотографировал на тапок, обрезанный шрифт - артефакт люминисцентного освещения.

Заключение

Жена одарена и относительно в восторге. Кнопка только (тот самый гвоздь, предмет моей гордости) оказалась для нее туговата, так что будет еще итерация тюнинга. Но в общем - хороший проект. Себе наверное повторю, только с корпусом еще надо подумать.

Бюджет

Чуть не забыл, в тексте есть упоминания, но сгруппирую материальные затраты:

  1. Nice!nano - 270 руб

  2. OLED 0,91" - 100 руб

  3. Li-ion 101640 - 300 руб

  4. кнопка х2 - 1 руб

  5. муфта медная соединительная под пайку 28 мм - 185 руб

Комментарии (2)


  1. vesowoma
    20.07.2026 19:59

    Годная статья, пример того как надо работать с ИИшечкой, имея руки откуда надо!


    1. Ndochp
      20.07.2026 19:59

      Правила только нарушает, если их еще раз не подкорректировали. Но собственно нарушает в том месте, которое мне не нравится и работает примером того, что хотелось бы разрешить.