программирование

Ещё про инженерный пароль от «Войи»

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

Как я говорил, у «Флиппера» есть проблема — он ничего не знает о часовом поясе. В каждом приложении, где нужно его знать, приходится что-то решать отдельно, централизованного решения нет.

Я пока не стал заморачиваться, просто показываю три кода из трёх разных дней. Это, конечно, не совсем правильно. Учитывая, что часовые пояса у нас бывают от GMT-12:00 до GMT+14:00, есть небольшое окошко, где можно показать не три, а два кода. Но вряд ли это существенно что-то меняет, поэтому я этого делать не стал.

В общем, решил сделать как все — окошко с настройкой часового пояса. Если пояс задан, он будет показан на кнопке и выводиться будет только один код, если нет, на кнопке будет написано что-то типа «set timezone» и кодов будет три.

Скетч интерфейса выбора времени: блок «03:00» с стрелками, подписями «Black», «Gray», «GMT+3» и рукописными пометками на фоне расписания дня.
Проектирование интерфейса для установки часового пояса

Оставить комментарий

Инженерный пароль от «Войи»

Чтобы совсем не позабыть как писать и компилировать программы для «Флиппера», написал небольшую программу, которая выводит инженерный пароль для моей машины.

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

День берётся по часовому поясу Китая, а поскольку «Флиппер» ничего про часовые пояса не знает, поэтому приходится показывать три числа — для разных дней.

Оранжево-белое устройство с жёлтым экраном, на котором написано «Today's passwords: 2928 or 2929 or 2927» и логотип в виде крыльев.
Пароль от инженерного меню «Войи» на сегодня

Полезное упражнение, подзабыл некоторые особенности, как самой сборки, так и наличествующего АПИ.

Оставить комментарий

The Sensor Watch

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

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

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

В общем, очередная моя хотелка — Sensor Watch, маленькая платка, стоящая 39 долларов и вставляемая вместо оригинальной электроники в ряд часов «Касио».

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

Поскольку в прошлой жизни я был электронщиком и программистом на ассемблере, такие ограниченные ресурсы всегда возбуждают моё любопытство. Платку я себе заказал, а пока написал в эмуляторе порт знаменитой в свои годы игрушки «Hunt The Wumpus», я про неё уже как-то писал.

Схема эмулятора часов Casio F-91W для игры Wumpus Hunt: экран показывает ID приложения, номер комнаты, действие и опасности (W — Вомпус, Bt — Летучая мышь, Pt — Ловушка).

На скриншоте видно как приходилось изворачиваться. Поскольку не все буквы получается вывести в сегментах часов как есть, приходится проявлять фантазию. Например, справа внизу я пытался изобразить букву «W» (это значит что где-то по соседству находится Wumpus).

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

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

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

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

8 комментариев

Brainfuck в шрифте

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

Скриншот интерфейса FontGoggles с кодом Brainfuck и результатом его выполнения — числом 3.141135232456, отображённым крупным шрифтом на тёмном фоне.
Число «Пи», считаемое внутри шрифта программой на Брейнфаке

Поскольку никакого ввода, кроме строки, которую нужно написать шрифтом, тут не предусмотрено, но некоторые программы требуют ввод (для этого в Брейнфаке есть конструкция «запятая»), пришлось выкручиваться.

Я сделал так: программа и входные данные у меня записаны в одну строку и разделены слешем. Часть после слеша считается тем, что введено в программу с клавиатуры.

Оставить комментарий

99 бутылок: bootBASIC

Очередная «песня о пиве», в последнее время я что-то прям зачастил.

78. bootBASIC — интерпретатор простенького диалекта Бейсика, занимающий 512 байт. В наше время, когда некоторые программы занимают гигабайты, удивительно видеть что-то настолько крошечное.

А тут целый язык, со строками, поддержкой математики, ветвлением и даже небольшим редактором кода. Правда я уже писал «песню о пиве» на миниатюрном Си. Он занимал чуть меньше, но и возможности там были в чём-то скромнее.

Текст песни «11 бутылок пива на стене» в терминале QEMU, начинающийся с 11 бутылок и заканчивающийся покупкой 99 новых.

Конечно, при таком размере есть свои ограничения — арифметика только целочисленная, до 65535 и нет операций сравнения, — можно проверить только, что число не равно нулю.

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

Обидно, что нет gosub для перехода к подпрограмме, вместо этого приходится использовать goto на переменную. Ладно хоть такая возможность есть. Комментариев нет, но можно использовать для этого команду print""; — дальше неё интерпретатор в строке ничего не видит.

1 print""; Written by Evgeny Stepanischev https://bolknote.ru
10 b=99
20 r=30
21 goto 200

30 print"of beer on ";
31 print"the wall, ";

40 r=50
41 goto 200

50 print"of beer."
60 b=b-1

70 print"Take one do";
71 print"wn and pass";
72print" it around, ";

80 r=90
81 goto 200

90 print"of beer on ";
91 print"the wall."

100 if b goto 20
110 goto 900

200 if b goto 203
201 print"no";
202 goto 210
203 print b;
210 print" bottle";
220 if b-1 goto 250
230 print" ";
240 goto r
250 print"s ";
299 goto r

900print"No more bott";
901print"les of beer ";
902print"on the wall,";
903print" no more bot";
904print"tles of beer."
905print"Go to the st";
906print"ore and buy ";
907print"some more, 9";
908print"9 bottles of";
909print" beer on the";
910print" wall.

Оставить комментарий

Библиотека itertools для PHP

В качестве развлечения написал тесты к своей старой библиотеке itertools для ПХП и выяснил, что в ней полным-полно ошибок. В итоге, переписал всё на класс, исправил ошибки, добавил функции, которые появились в оригинальной библиотеке за это время, обмазал всё типами, вставил тесты и поддержку «композера».

Пример использования теперь стал выглядеть вот так:

use Itertools\Itertools as it;

foreach (it::islice(it::cycle('ABC'), 10) as $element) {
    echo $element;
}

2 комментария

99 бутылок: mkdir + find

77. mkdir + find — тут некий японец придумал как писать программы, используя только утилиты mkdir и find. Мне такое очень нравится, поэтому и я решил что-нибудь написать на связке этих утилит. Сразу вспомнил, что давно у меня не было «песни о пиве». Есть у меня такое развлечение — писать её на всём подряд.

Программу вы можете видеть на экране, а как она работает расскажу чуть ниже.

# Written by Evgeny Stepanischev https://bolknote.ru

rm -rf bottles; mkdir bottles
find bottles -maxdepth 98 -execdir mkdir bottles/bottles \;
find bottles -empty -name bottles -execdir mkdir bottles/bottle \;

find bottles \
        -empty -printf "1 bottle of beer on the wall, 1 bottle of beer.
Take one down and pass it around, no bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.\n" -o \
        -regex '.*/bottles/bottles' \
        -execdir find . \
                -empty -printf "%d bottles of beer on the wall, %d bottles of beer.\n" \; \
        -execdir find bottles \
                -empty -printf "Take one down and pass it around, %d %f of beer on the wall.\n\n" \;

# Clean up
rm -rf bottles

Прежде всего — что за две команды тут используются? rm не в счёт, она для очистки мусора, можно без неё.

Команда mkdir, наверное, многим знакома — это создание директория, а find — утилита для поиска файлов и директориев по различным критериям. Причём с найденным она может делать некоторые простые действия.

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

Во-первых, создание вложенных директориев командой find с параметром -execdir mkdir имя.

Параметр запускает указанную команду в директории, который сейчас просматривает find. Эти вложенные директории в дальнейшем используются как конечный цикл для следующей команды find. Вложенность при этом можно регулировать параметром -maxdepth.

Во-вторых, параметр -printf поддерживает аргумент %d, который позволяет вывести текущий уровень вложенности и полезен в тех же циклах.

В-третьих, фильтры команды find, а так же директивы -o («ИЛИ») и -and («И») можно использовать для организации условий.

Мне пришлось дополнительно решить ещё две проблемы.

Во-первых, цикл у меня обратный. Чтобы его получить, я запускаю две команды find — одну в другой. Первая считает директории в одну сторону по возрастанию, вторая запускается на каждом шаге и итерируется по остатку директориев до конца. Поскольку это тоже цикл, а мне нужно только последнее значение, я использую параметр -empty, чтобы получить только последний директорий — тот, в котором пусто.

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

Во-вторых, мне нужно поддержать окончание множественного числа. Тут я немного смухлевал и слегка замёл эту проблему в конец — там я вывожу дополнительные строки «1 bottle of beer on the wall, 1 bottle of beer. / Take one down and pass it around, no bottles of beer on the wall».

Но в строке перед этим текстом у меня сделано более честно. Как видно, структура директориев у меня такая: 98 раз созданы вложенные bottles, а конечная директория называется bottle.

Это мне позволяет в одном из «циклов», огранизованных через find, использовать аргумент %f параметра -printf, который выводит имя текущего директория. Таким образом в самом конце я получаю bottle вместо bottles.

Добавлено: мне подсказали, что обратный цикл можно огранизовать проще, через ключ -depth, переделал программу под него:

# Written by Evgeny Stepanischev https://bolknote.ru

find start -delete 2>&-
mkdir -p start/bottle/bottles
find start/bottle -maxdepth 97 -name bottles -execdir mkdir bottles/bottles \;

find start -mindepth 1 -depth \
        -empty -printf "%d %f of beer on the wall, %d %f of beer.\n" -o \
        ! -empty -printf "Take one down and pass it around, %d %f of beer on the wall.\n\n" \
        -printf "%d %f of beer on the wall, %d %f of beer.\n" \
        -name bottle -printf "Take one down and pass it around, no bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.\n"

rm -rf start

4 комментария

Интернет-время «Своч»

Интересно насколько по-разному люди записывают время. Если я пишу от руки, предпочитаю писать минуты в верхнем индексе — «8³⁵». Более свежие люди так не делают, пишут «8:35» или, гораздо реже, «8.35».swatch internet time

Последняя форма записи подтолкнула мои размышления к тому, какой у нас, в сущности, странный способ измерения времени.

«8.35» похоже на десятичную дробь, но на самом деле основание дробной части тут — 60, а у целой — 12 или 24, в зависимости от обстоятельств. Наверняка все об этом думают время от времени.

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

Серебристый с чёрными вставками пейджер Ericsson с антенной, на экране которого отображается текст «WORLD @618», значок сигнала и заряда батареи.
Интернет-время на экране телефона Ericsson T20e

Швейцарская компания «Своч» (Swatch) занимается наручными часами. Не знаю как у неё сейчас с известностью, а конце 90-х—начале 2000-х, у нас в стране она была очень узнаваемой, думаю, как и во всём мире.

Видимо на волне этой популярности, в 1998-м году компания анонсировала так называемое интернет-время. Интернет тогда был крошечный и об этом писали многие, событие было широкоизвестное.

Идея была в том, чтобы разделить сутки на тысячу «битов» — от нуля до 999, каждый «бит» (beat) равнялся 86,4 секунды, при этом отсчёт начинался с часового пояса UTC+01:00.

Время записывалось, начиная с символа «@» (он тогда прочно ассоциировался с интернетом) и добивалось нулями до трёх разрядов. Например: «@007», «@013» и так далее.

Вот небольшая программа на «Расте» для вывода текущего времени в этом формате:

use chrono::{Utc, Timelike, Duration as D};
use std::thread;
use std::time::Duration;

fn main() {
    const SECS_IN_BEAT: f64 = 86.4;

    loop {
        let seconds = (Utc::now() + D::hours(1)).num_seconds_from_midnight() as f64;
        let beat = (seconds / SECS_IN_BEAT).floor();

        print!("🕰️ @{}\n\x1b[A\x1b[G", format!("{:03}", beat));

        let duration = (1. + beat) * SECS_IN_BEAT - seconds;
        thread::sleep(Duration::from_secs_f64(duration));
    }
}

Идея свою минуту славы получила, — я помню, компания «Эрикссон» выпустила телефон, который умел отображать время в этом формате, в языке ПХП добавили поддержку этого формата в функции работы со временем, а «Своч» выпустила часы, где в первой строке было традиционное время, а ниже — время в интернет-формате.

Не дожив даже до середины 2000-х, формат постепенно потерял популярность и был забыт. Идея была интересная, но в каком-то смысле опередившая своё время. Интернет был недостаточно распространён, чтобы было много поводов договариваться о чём-то в едином временном поле.

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

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

15 комментариев

Немного о WASM'е в шрифтах

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

Давайте опять немного о том, почему язык программирования в шрифте — это удобно.

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

Это делается для красоты, чтобы шрифт казался однородным. Буквы не квадратные, одни шире, другие у́же, «о» круглая, а «ж» похожа на причёску Альберта Эйнштейна. Если писать буквы как есть, то будет выглядеть неаккуратно, поэтому в местах их прилегания расстояния подбирают так, чтобы смотрелось хорошо.

Но вот нам хочется добавить в Юникод египетские иероглифы, а там имя фараона пишется в продолговатом овале — картуше. Как это решить в шрифте? Да никак. Мы можем ввести символы начала картуша и конца, а линии, которыми они должны соединяться, запрограммируем в движке, который рисует нам символы. Причём дизайнер шрифта никак не будет влиять на то, как эти линии рисуются.

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

Может показаться, что это не так уж и важно, наверное правда в этом есть, но это всего лишь пример. Есть и более актуальные проблемы, например, в арабском письме, но там их объяснить сложнее. По крайней мере, сложнее для меня.

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

Как это работает?

Библиотека, если сильно упрощать, получает на вход от приложения место на экране (координаты) и строку, которую надо написать в этом месте. Строка, перед тем как начнётся отрисовка, засовывается в программу, которая хранится внутри шрифта. Эта программа производит с символами какие-то манипуляции (я, например, заменил каждую букву на эквивалент в азбуке Морзе) и полученная строка отображается на экране.

Внутри программы можно менять символы, сдвигать их на другие позиции и так далее. Самое главное — программе доступен контекст в виде целой строки. Таким образом можно обнаружить (в случае египетских иероглифов), что слово начинается с символа начала картуша и нарисовать линии до признака его конца.

8 комментариев

WASM в шрифтах

Ну вот мы и дожили до момента, когда в шрифтах появился язык программирования. В harfbuzz, лидирующей библиотеке для отрисовки шрифтов появилась встроенная поддержка WASM.

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

В общем, в шрифтах появился свой язык программирования.

Естественно, все побежали делать на нём всякую весёлую дичь. Внутрь шрифтов уже засунули «Тетрис» и даже какую-то языковую модель.

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

Скриншот программы FontGoggles: в поле ввода текст «BOLK», ниже отображён шрифт Morse-Regular.ttf, где буквы закодированы точками и тире азбуки Морзе.
Азбука Морзе отрисовывается через ВебАссемблер (WASM)

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

2 комментария

И опять чинил PogodaStatusBar

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

В общем, прошла неделя и мне снова прикрыли лавочку — урл «Гисметео», который я использовал, чтобы определять айди населённого пункта по названию вдруг меня забанил. На любой запрос выдаётся «доступ запрещён». Да что ж такое!

Полез я снова смотреть погодный плагин, смотреть как сделано там. Он же как-то определяет текущий населённый пункт, значит и я как-то могу получить эти данные. Оказалось, что есть у «Гисметео» ещё один сервис — services.gismeteo.net/inform-service/inf_chrome/cities/, который определяет местоположение по АйПи (ошибается в моём случае) или по координатам.

Координаты у меня уже есть, я их могу забрать у «Яндекса», надо только распарсить:

@functools.cache
def _getRegionData(self):
    try:
        url = "https://yandex.ru/tune/geo/"
        content = urllib.request.urlopen(url).read().decode('utf-8')
        region_data = re.search(r'data-bem="([^"]+coords[^"]+)"', content).group(1)
        parsed_region_data = json.loads(html.unescape(region_data))

        # {'id': xx, 'region': 'XXXXX', 'coords': ['XX.XXXXX', 'XX.XXXXX'], 'accuracy': 'XXXXXX'}
        return parsed_region_data['checkbox']['auto']
    except (IOError, AttributeError):
        return None

В итоге, получилась такая схема: в «Яндексе» получаем координаты и айди населённого пункта у «Яндекса», по этому айди получаем информацию по пробкам. Далее по координатам получаем айди населённого пункта у «Гисметео», по которому получаем погоду.

Ей-богу я в полушаге от того, чтобы прекратить борьбу и забросить проект.

6 комментариев

Опять чинил PogodaStatusBar

Сегодня участвовал в довольно скучной видеоконференции. Моё участие там было хоть и необходимое, но небольшое. А чтобы не одуреть от безделья, пока я слушал всю эту скуку, решил чем-то занять руки. Давно хотел починить свой плагин для «Саблайма» «PogodaStatusBar», этим и занялся.

Строка состояния редактора кода: город Казань, температура 29°C, иконка солнца с облаком, число 3, зелёный индикатор, строка 127, столбец 1.
Внешний вид плагина на настоящий момент

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

Пробки, кстати, тоже перестали нормально работать. Когда-то регион определялся автоматически, но это сломали. Я чинил это через парсинг страницы погоды, но теперь и это не работает — «Яндекс» борется с автоматическими роботами. Поэтому пришлось искать другой способ.

Я перебрал несколько малоизвестных сервисов, пока, наконец, не увидел, что страница yandex.ru/tune/geo/ содержит в себе необходимый номер. Этого хватило, чтобы починить показ пробок.

С «Гисметео» пришлось повозиться. Не нашёл официальную информацию о каких-либо АПИ, которые можно вызвать без токена, но внутри погодного плагина для браузера нашёлся адрес services.gismeteo.ru/inform-service/inf_chrome/forecast. В нём есть всё что нужно — текущая температура и закодированная иконка погоды.

Определить номер города в формате «Гисметео» помогает сервис www.gismeteo.ru/rmq/search/{название}/1/, куда я подставляю название, полученное от «Яндекс.Пробок» — там он возвращается вместе с остальными данными.

Оставалось только расшифровать иконку погоды. В документации есть часть описания (остаток можно понять, скачав с «Гисметео» значки погоды), выглядит всё несложно. Например, d.r3 означает «день» (day) и «дождь максимальной интенсивности» (rain 3).

Поскольку у меня Юникод, в нём есть не все необходимые значки, я сделал вот такой объект, содержащий признаки, которые позволяют мне выбрать какую иконку показать:

_icons = {
    "🌞": {"d"}, # day
    "🌙": {"n"}, # nite
    "☁️": {"n", "c"}, # night, cloud
    "🌧": {"rs", "c", "r"}, # rain+snow, cloud, rain
    "🌤": {"d", "c"}, # day, cloud
    "🌦": {"d", "r"}, # day, rain
    "🌩️": {"c", "st"}, # #day, storm
    "⛈️": {"c", "st", "r"}, # cloud, storm, rain
    "🌨": {"c", "s"}, # cloud, storm
    "💨": {"mist"}, # mist
    "⚡️": {"st"}, # storm
}

И вот такой простой алгоритм для выбора иконки:

def _getStatus(self, coded_weather):
    codes = {x.strip("0123456789") for x in coded_weather.split(".")}

    max_icon = None
    max_score = 0

    for icon, code in self._icons.items():
        result = codes & code
        score = sum(len(x) for x in result)
        if score > max_score:
            max_icon, max_score = icon, score

    return max_icon

Идея простая — чем больше значков совпало, тем лучше; если совпал значок с более длинным названием (st, mist), считаем его более конкретным и важным.

4 комментария

Посещал ли пользователь сайт?

Атака не работает — в браузерах давно реализована защита, у меня идея «сработала» в силу того, что я не выполнил все необходимые проверки.

В прошлом было достаточно много методов определить ходил ли пользовать на какой-то сайт или нет. В основном, идея была проста: у себя на сайте через CSS каким-то способом выделяем посещённые (:visited) ссылки — например, делаем таким ссылкам шрифт больше, а потом через ДжаваСкрипт смотрим у каких ссылок ширина стала больше расчётной.

Это считалось проблемой, поэтому свойство изменили так, чтобы никакой утечки информации о посещении в ДжаваСкрипт не происходило. Все современные браузеры уже много лет от этого защищены.

Но недавно меня осенило, что есть способ обойти эту защиту!

Ведь если пользователь посещал целевой сайт, то скорее всего какие-то его ресурсы легли в кеш. Если найти какой-то ресурс этого сайта большого размера, загрузить его и попытаться определить за какое время это произошло, то по этой разнице можно понять — был он в кеше или нет.

Я попробовал сделать это с «Гитхабом». Самый большой файл, который загружается там с первой страницей — vendors-node_modules_github_mini-throttle_dist_index_js-node_modules_primer_octicons-react_di-b40d97-9b98c5140e22.js. Занимает он 136 килобайт, вполне подходящий размер.

Как же определить момент когда он загрузится? Если посмотреть внутрь, видно, что он после скачивания создаёт свойство webpackChunk в объекте globalThis. Если перехватить обращение к этому свойству, можно понять, что файл загружен:

const t = new Date();
Object.defineProperty(globalThis, 'webpackChunk', {
  set: function() {
    console.log(new Date() - t);
  }
});

Ниже этого кода подключаем загрузку указанного файла прямо с «Гитхаба» через тег <script> и дело в шляпе.

Я проверил, если очистить кеш браузера, то файл у меня загружается примерно за 100—400 миллисекунд, если берётся из кеша — это 0—12. Легко отличить одно от другого.

10 комментариев

Графики в стиле xkcd

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

У xkcd очень узнаваемый стиль рисовки и, когда я их вдруг встречаю, они по-прежнему вызывают у меня улыбку. Сегодня я узнал, что в «Пайтоне» есть возможность рисовать графики в этом стиле. Надо добавить всего одну строку — вызов метода plt.xkcd().

Вот код:

import matplotlib.pyplot as plt
import numpy as np

t = np.linspace(0, 2*np.pi, 1000)
x = 16 * np.sin(t)**3
y = 13 * np.cos(t) - 5 * np.cos(2*t) - 2 * np.cos(3*t) - np.cos(4*t)

plt.xkcd()

plt.figure(figsize=(8, 6))
plt.plot(x, y, color='b')
plt.title('cos(…) = ?')
plt.show()

А вот результат:

График синей кривой в форме сердца на координатной плоскости с осями от -15 до 15 и подписью «cos(...) = ?» над графиком.

Оставить комментарий

Сложности проверки версий

Случайно обратил внимание какое-то время назад, что у меня на сайте почему-то перестало проигрываться аудио в формате MP3 в «Опере», моём основном браузере. Никак не доходили руки разобраться, но в эти длинные праздники я всё-таки докопался до сути. Суть оказалось смешной до нелепого.

Дело в том, что в движке, который я использую для своего дневника, для проигрывания аудио используется библиотека «Жуэль», а её текущая версия, в свою очередь использует старую версию библиотеки «howler.js», в которой есть вот такой код:

self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null;
var checkOpera = self._navigator && self._navigator.userAgent.match(/OPR\/([0-6].)/g);
var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33);

Проблема в регулярном выражении OPR\/([0-6].) и в версии «Оперы». Недавно версия перевалила за 100 и эта регулярка, вместо корректного номера, стала возвращать «10».

Из-за этого третья строка считает мою «Оперу» слишком старой (потому что 10 меньше 33), чтобы поддерживать проигрывание файлов MP3.

Заменил у себя на сайте howler.js на актуальную версию и всё заработало. Заодно предложил патч в «Жуэль», надеюсь примут оперативно.

4 комментария

cmd.sh ★

Я тут случайно в спор ввязался — можно ли переписать бат-файл так, чтобы он запускался и выводил примерно одинаковый результат на трёх основных операционных системах — Виндоузе, Линуксе и МакОСи.

Речь шла о вполне определённом файле, там запрограммировано небольшое меню и в зависимости от выбранного пункта запускается та или иная, одинаковая для всех ОС, последовательность команд.

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

Что ж. Основная моя идея была в том, чтобы заменить команды батника на вызовы баш-функций, которые я спрячу в специальных конструкциях. В Виндоузе файл обработает обычный cmd.exe, на остальных операционках — bash.

То, что мне предстояло адаптировать начиналось вполне стандартно:

@ECHO off
CHCP 1251
CLS

Что тут происходит? Выключается вывод команд на экран (при адаптации это можно игнорировать), ставится однобайтовая кодировка CP1251 (это хорошо бы учесть) и очищается экран.

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

:; @ECHO() { :;}; CLS() { clear; }; CHCP() { ENC="$1"; }

Следующее, чему надо научиться — выводить текст в указанной в батнике кодировке. Это просто:

:; ECHO.() { echo $@ | iconv -f "CP$ENC"; }

Если теперь сделать файл запускаемым и добавить впереди #!/bin/bash (и пренебречь тем, что cmd.exe успевает ругнуться на эту строку до очистки экрана), у нас получится следующий файл, работающий во всех трёх операционках (кодировка должна быть Windows-1251):

#!/bin/bash
:; @ECHO() { :;}; CLS() { clear; }; CHCP() { ENC="$1"; }
@ECHO off
CHCP 1251
CLS
:; ECHO.() { echo $@ | iconv -f "CP$ENC"; }
ECHO. Всем привет

А вот дальше сложнее. Само меню организовано в оригинале так:

SET /p opt=Введите цифру:

IF %opt%==1 GOTO dns_auto
IF %opt%==2 GOTO dnschange
IF %opt%==3 GOTO exit

В «баше» goto (переход к метке) отсутствует и это проблема. Аналогов тоже нет. Единственное, что тут можно сделать — использовать вызов функций, но синтаксис в «бате» и «баше» сильно различается.

Как быть? Во-первых, сэмулировать функцией команду SET, во-вторвых, упростить это место, чтобы меньше было писать кода на «баше»:

:; SET() { shift; ECHO. -n "${@#*=}"; read -n1 "${1%%=*}"; echo; }
:; CALL() { eval "$(sed 's/%\(.*\)%/$\1/g' <<< "$1")" 2>&-; }

SET /p opt=Введите цифру:
CALL :menu_%opt%

Первая функция парсит команду SET выводит строку после равно, ждёт ввода и записывает значение в имя переменной, указанной слева от равно.

Вторая — заменяет CALL. В ней %variable% заменяется на $variable, выполняется подстановка переменной и получившееся имя выполняется как команда или функция «баша». Конструкция 2>&- нужна, чтобы избежать вывода ошибки в ситуации, если пользователь введёт что-нибудь не то.

Теперь надо как-то научиться определять функции так, чтобы их нормально «видел» и cmd.exe, и «баш». В батнике функции — просто любое место программы, начинающееся с метки и заканчивающееся вызовом GOTO :EOF:

:function_1
ECHO это якобы функция
GOTO :EOF

В «баше» то же самое могло бы выглядеть, например, так:

function_1() {
    echo это функция
}

Как это объединить? Я придумал следующий подход:

:; GOTO() { :; };

GOTO ;#start
:function_1 (){
ECHO. это [якобы] функция
GOTO :EOF
:; }

:;#start

В первой строке определяется пустая функция для «баша», потому что эта часть синтаксиса бат-файла нам не нужна.

Ниже идёт строка GOTO ;#start. С точки зрения «баша» тут две конструкции — GOTO, которая вызывает определённую выше пустую функцию и #start — строка комментария, так как в«баше» с «решётки» начинаются комментарии.

В «батнике» же эта же строка будет означать переход к метке с именем ;#start. Этот переход нужен нам, чтобы «обогнуть» строки, которые определены ниже, иначе cmd.exe начнёт сразу их выполнять, а нам этого не нужно. Их нельзя «спрятать» ниже основной программы, так как в «баше» функции должны определены раньше их вызова.

Что происходит дальше? С точки зрения cmd.exe ниже расположена метка :function_1 (как показывают мои эксперименты, часть после пробела просто отбрасывается), потом тело функции, команда её завершения GOTO :EOF и метка с именем :;}.

С точки зрения «баша» там определяется функция с именем :function_1, ниже идут вызовы уже определённых мною функций ECHO. и GOTO, а ещё ниже — уже знакомая нам пустая команда : и фигурная скобка, завершающая тело функции.

Соединяем всё вместе и получается следующая программа:

#!/bin/bash
:; @ECHO() { :;}; CLS() { clear; }; CHCP() { ENC="$1"; }
@ECHO off
CHCP 1251
CLS

:; GOTO() { :; };
:; ECHO.() { echo $@ | iconv -f "CP$ENC"; }; PAUSE() { read; }; 
:; SET() { shift; ECHO. -n "${@#*=}"; read "${1%%=*}"; }
:; CALL() { eval "$(sed 's/%\(.*\)%/$\1/g' <<< "$1")" 2>&-; }

GOTO ;#start

:menu_1 (){
ECHO. Пункт первый
GOTO :EOF
:; }

:menu_2 (){
ECHO. Пункт второй
GOTO :EOF
:; }

:;#start

SET /p opt=Введите цифру:
CALL :menu_%opt%

Мой оппонент признал, что в споре я победил.

2 комментария

Юникод во «Флиппере»: глубока ли кроличья нора

Юникод во «Флиппере» неисчерпаем, как атом.

Сначала мне стало просто интересно почему для «Флиппера» нет ни одного русскоязычного приложения, потом я выяснил что мешает это сделать и научился обходить эти ограничения, потом написал русскоязычную игру, после чего залез в прошивку и начал добавлять в неё поддержку Юникода, погружаясь всё глубже и глубже.

К моим коммитам отнеслись довольно благосклонно, но в один момент дело застопорилось — авторы прошивки решили обновить сторонний фреймворк для вывода на экран, что могло повлиять на мои изменения, поэтому работу над ними я отложил. Но уже тогда у меня возникла мысль, что надо бы добавить Юникод и в библиотеку работы со строками, которую использует «Флиппер Зеро».

Пока я ждал обновления, занял себя векторизацией функции подсчёта длины строки в кодировке UTF-8, а когда коммит отклонили, поставил всю эту движуху на паузу.

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

Размер доработок всё ширится, а результат всё дальше. Интересно, насколько глубока кроличья нора?

1 комментарий

Управление «Алисой»

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

Официальный документации нет, но хорошие люди провели обратную разработку и задача выглядит как будто бы не очень сложно. Получаем токен, в сети через mDNS по имени _yandexio._tcp.local. ищем нужную колонку, находим, команды отдаём по протоколу WebSocket.

Так как я сейчас стараюсь больше времени проводить за книгой, чем за компьютером, всё, что успел — освоить модуль zeroconf для Гоу, чтобы по имени колонки получать её айпи-адрес и порт.

Получившийся код выглядит так:

import "github.com/grandcat/zeroconf"
// …тут остальные импорты…

const Service = "_yandexio._tcp"
const Domain = "local."

func getAlisaAddr(instance string) (*zeroconf.ServiceEntry, error) {
    resolver, err := zeroconf.NewResolver(nil)
    if err != nil {
        log.Fatalln("Failed to initialize resolver:", err)
    }

    entries := make(chan *zeroconf.ServiceEntry)
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

    err = resolver.Lookup(ctx, instance, Service, Domain, entries)
    if err != nil {
        return nil, err
    }

    entry := <-entries
    cancel()

    <-ctx.Done()

    if entry == nil {
        return nil, fmt.Errorf(
              "Failed to lookup for service instance '%s'",
              instance
       )
    }

    return entry, nil
}

Теперь надо разобраться где взять токен и как выглядят команды.

Оставить комментарий

Конец ускоренной strlen на «Флиппере»

В итоге, эпопея с ускорением функции замера длины строки в кодировке UTF-8 на «Флиппере Зеро» подошла к концу — коммит не взяли.

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

Тем не менее, я с удовольствием повозился инструкциями SIMD. До этого мини-проекта у меня было теоретическое понимание о том, что это за функции, но на практике я их не использовал.

Моя функция, кстати, претерпела некоторые изменения:

size_t furi_string_utf8_length(FuriString* str) {
    size_t len = string_size(str->string);
    const char * cstr = string_get_cstr(str->string);
    const char* end = cstr + len;

    if(len >= sizeof(uint8x4_t)) {
        const char* vend = end - sizeof(uint8x4_t);

        int8x4_t zero = 0x00000000;
        int8x4_t one = 0x01010101;
        int8x4_t threshold = -1077952577; // -65, -65, -65, -65

        do {
            int8x4_t vec = *(int8x4_t*)cstr;

            __ssub8(threshold, vec);
            uint8x4_t result = __sel(one, zero);

            len -= __usada8(result, zero, 0);

            cstr += sizeof(uint8x4_t);
        } while(cstr <= vend);
    }

    while(cstr < end) {
        signed char c = *cstr++;
        len -= c < -64;
    }

    return len;
}

Оказалось, что тип FuriString хранит в себе бинарную длину строки, поэтому я переделал код, чтобы он не пытался читать чужую память. В векторизованой части это было вероятно, но своих рассуждений и дискуссии по коммиту я сделал вывод, что в случае «Флиппера» это не имеет какого-либо значения.

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

В итоге, решая эту, возможно несуществующую, проблему, я опробовал на практике векторные инструкции расширений NEON и DSP SIMD, научился делать два параллельных запроса на вливание в «Гитхабе» и ещё раз вспомнил как компилировать и заливать свою прошивку во «Флиппер Зеро».

Оставить комментарий

Сделал PR с новой strlen во «Флиппер»

Спасибо Шурику Бабаеву за помощь, добавил свой коммит с векторизованной функцией для измерения длины строки в кодировке UTF-8 в кодовую базу прошивки «Флиппера».

Попутно выяснилось, что «Флиппер» использует функцию замера длины из библиотеки M*LIB. Можно было бы закоммитить новую версию в неё, но не стал этого делать по двум причинам.

Во-первых, моя версия заточена под конкретную платформу, а M*LIB, кажется, универсальная библиотека. Во-вторых, если бы мой коммит в неё и приняли, надо было бы ещё каким-то образом добиться обновления библиотеки в прошивке «Флиппера», а это, наверняка, непростая задача.

Кстати, сделал итоговые замеры. Миллион итераций с четырьмя короткими строками (от ЯБ12341 до ЯБ12341234) моя версия делает за шесть секунд, тогда как прежняя — за 39.

Оставить комментарий

← 1 11 12 13 14 15 67 →