Новый домашний сервер

4-го августа я переехал со старого неттопа на новый GMKtec NucBox G3. Внутри — процессор Intel N100 Alder Lake 12-го поколения, 32 ГБ памяти Kingston PC4-25600 DDR4, 1 ТБ SSD Samsung 980 Pro и Wi-fi Realtek RTL8852BE (802.11ax), также имеется дополнительный порт для SSD-накопителя формата M.2 2242 (SATA). Я после установки системы Ubuntu 24.04 стал использовать wi-fi на время настройки, планируя потом подключиться к роутеру по проводу, да так на нём и остался — удобно, работает стабильно и скорость хорошая.

Внешний вид сервера Внешний вид сервера

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

  1. Выставить TDP на 8 Вт (с 10 Вт), макс. потребление будет 19 Вт (с 24 Вт). В покое 8-9 Вт, выключенный — 1,2 Вт. Power -> Power limit select: 8W
  2. Включить Cstates (по умолчанию выключено). Advance -> CPU - Power management control -> C States: enabled
  3. Выключить турборежим процессора при загрузке. Advance -> CPU - Power management control -> Boot performance mode: Max Non-Turbo performance
  4. Повысить порог срабатывания вентилятора. Advance -> Hardware monitor -> Smart fan function -> Fan off: 40, Fan start: 65

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

Frigate Frigate

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

Сервисы на сегодняший день такие:

  • Реверс-прокси Træfik — обновил c версии 2.6 на 3.1, немного изменился синтаксис ярлыков, теперь там используются регулярные выражения вместо перечисления нескольких суффиксов или имён хоста, а ещё HTTP/3 сменил экспериментальный статус на стабильный.
  • Вышеупомянутый Frigate, у которого я включил видеоускорение VAAPI и модель OpenVino. Позже, наверное, надо бы попробовать добавить к нему Home Assistant как управляющую оболочку.
  • Блог на прекрасном движке Datenstrom Yellow
  • Вики на DokuWiki
  • Файловый сервис Nextcloud
  • Фотосервис Photoprism
  • Генеалогическое древо Webtrees
  • Страничка мониторинга (node_exporter, docker metrics, glances, smartctl_exporter, Prometheus, Grafana)

С мониторингом была сложная история. Я использовал PhpSysInfo в контейнере, и мне хотелось избавиться от довольно кривого способа сбора информации раз в полчаса на самом хосте и подкладывания файлов в веб-каталог приложения. Оказалось, что PhpSysInfo умеет работать через SSH, поэтому я создал на хосте выделенного пользователя и разрешил ему выполнять команды sensors и docker stats через sudo без запроса пароля.

sudo visudo /etc/sudoers.d/phpsysinfo

Cmnd_Alias PHPSYSINFO=/usr/bin/docker stats*, /usr/sbin/smartctl
phpsysinfo ALL = NOPASSWD: PHPSYSINFO

Пришлось открыть пару тем в репозитории Гитхаба из-за того, что какие-то моменты были непонятны, а какие-то не работали, но автор приложения, как и раньше, невероятно отзывчив и решает проблемы просто молниеносно, так что всё более-менее работало и прочитанные/записанные данные диска теперь показывались в понятных гигабайтах, а не в секторах, как раньше. Тем не менее, теперь вывод страницы после захода на неё стал очень тормозной — PhpSysInfo каждый раз лезет на хост по SSH и собирает данные (на полях отмечу утилиту sshpass, позволяющую автоматически подставлять пароль для входа). Вдобавок, перестало нормально работать отображение скопившихся системных обновлений, так что я начал искать альтернативу.

Glances Glances

Через некоторое время я нашёл Glances, у которого мне очень понравился внешний вид, но проект пока сыроват и документация у него достаточно фрагментарная, поэтому я решил вернуться к давней идее, на которую у старого неттопа не хватало ресурсов — Prometheus + Grafana. В конце концов, популярная вещь и по работе пригодится. Провозившись несколько дней, вспоминая, как там всё устроено, я нарисовал страницу более-менее в соответствии с моими предпочтениями. Что до производительности, то неттоп легко справляется с новым мониторингом. Glances я оставил как сборщик данных о контейнерах: он умеет экспортировать метрики для Prometheus.

Grafana Grafana

Все контейнеры с БД я обновил до MariaDB 11.4 с версии 10.6, завелось без проблем. PHP был обновлён на версию 8.3 ещё на старом неттопе. Docker volumes переделал просто в mount points — мне кажется, мигрировать данные и делать резервные копии так удобнее, каких-то преимуществ docker named volumes в моём случае я не вижу.

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

Пока мне нравится, поживём — увидим.

Удаление плавающей заставки из мультсериала

Волею судеб с некоторых пор я последовательно скачиваю серии мультиков про Наруто. После скачивания для экономии места на сетевом хранилище я пережимаю эти серии в H.265, оставляя только само содержимое, удаляя финальные титры и песню-заставку (opening).

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

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

frame=12549 fps=353 q=-0.0 size=N/A time=00:08:24.36 bitrate=N/A speed=14.2x    
frame=12719 fps=353 q=-0.0 size=N/A time=00:08:31.16 bitrate=N/A speed=14.2x    
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12737 pblack:100 pts:12798 t:511.920000 type:I last_keyframe:12737
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12738 pblack:100 pts:12799 t:511.960000 type:P last_keyframe:12737
...
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12846 pblack:88 pts:12907 t:516.280000 type:P last_keyframe:12845
[Parsed_blackframe_1 @ 0000023f584beec0] frame:12847 pblack:89 pts:12908 t:516.320000 type:P last_keyframe:12845
frame=12891 fps=353 q=-0.0 size=N/A time=00:08:38.08 bitrate=N/A speed=14.2x    
frame=13088 fps=353 q=-0.0 size=N/A time=00:08:45.92 bitrate=N/A speed=14.2x

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

Ситуация дополнительно усложнялась следующими факторами:

  1. Кадры заставки от серии к серии могут немного меняться и фильтр перестаёт воспринимать кадр как похожий на образец на скриншоте. Ниже примеры, где один образец кадра наложен на другой и видно, что логотип был сдвинут или увеличен.
naruto_opening5_end.jpg
naruto_opening1_end.jpg
  1. Заставка сама по себе меняется примерно каждые 25 серий, так что тут придётся пробежаться по эпизодам сериала, записать их диапазоны с теми или иными заставками и наделать пары скриншотов для каждого диапазона.
  2. Серии (именно их содержательная часть) длятся неодинаковое время. У одних финальные титры начинаются в 20:06, а у других — на минуту позже.
  3. Наличие квадратных скобок в именах файлов и путях. Почему-то многие владельцы раздач на торрент-трекерах питают слабость к подобным символам, поэтому приходится добавлять в скрипт дополнительные ухищрения.

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

naruto_opening3_start.jpg

В целом получилось так: для полноцветных кадров я выставлял blackframe=80 при стандартном значении 98, а для кадров на чёрном фоне оставлял по умолчанию blackframe.

Чтобы определить длительность серий, я сделал быстрый просмотр, выбирая по 10 серий, чтобы индекс массива для удобства совпадал с последней цифрой серии, и нажимая F8 в Powershell ISE на соответствующей строке.

$episodes = 80..89 |% {$_.ToString("000")}
$e = dir *.avi |? name -match "$($episodes -join '|')"

ffplay -i $e[0].Name -ss 20:06 -an
ffplay -i $e[1].Name -ss 20:06 -an
ffplay -i $e[2].Name -ss 20:06 -an
ffplay -i $e[3].Name -ss 20:06 -an
ffplay -i $e[4].Name -ss 20:06 -an
ffplay -i $e[5].Name -ss 20:06 -an
ffplay -i $e[6].Name -ss 20:06 -an
ffplay -i $e[7].Name -ss 20:06 -an
ffplay -i $e[8].Name -ss 20:06 -an
ffplay -i $e[9].Name -ss 20:06 -an

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

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

Name;Start;End
Naruto_Shippuuden_TV2_[001_of_XXX]_[Rus_Jap]_[NIKITOS].avi;511.920000;601.760000
Naruto_Shippuuden_TV2_[002_of_XXX]_[Rus_Jap]_[NIKITOS].avi;119.000000;208.800000
Naruto_Shippuuden_TV2_[003_of_XXX]_[Rus_Jap]_[NIKITOS].avi;130.960000;220.800000
...
Naruto_Shippuuden_TV2_[131_of_XXX]_[Rus_Jap]_[NIKITOS].avi;448.489708;536.827964782715
Naruto_Shippuuden_TV2_[132_of_XXX]_[Rus_Jap]_[NIKITOS].avi;383.299583;471.637840270996
Naruto_Shippuuden_TV2_[133_of_XXX]_[Rus_Jap]_[NIKITOS].avi;1.501500;89.8814591169357

В файле CSV первые конечные метки отличаются от тех, что идут позже — у них меньше десятичных знаков. Дело в том, что сначала я указывал ffmpeg искать в первых 10 минутах видеофайла и начальный, и конечный кадр — в функции была одна команда. Потом я догадался, что конечный кадр ведь идёт после начального, поэтому нужно начинать искать конечный кадр с того места, где был начальный, а так как все заставки идут не более полутора минут, то ими надо поиск и ограничить. Длительность заставок выяснилась путём анализа того же CSV-файла.

Import-Csv 'C:\temp\log.txt' -Delimiter ';' |select *,@{n='Duration';e={$_.end - $_.start}}

Name                                                       Start      End        Duration
----                                                       -----      ---        -------
Naruto_Shippuuden_TV2_[001_of_XXX]_[Rus_Jap]_[NIKITOS].avi 511.920000 601.760000 89,84
Naruto_Shippuuden_TV2_[002_of_XXX]_[Rus_Jap]_[NIKITOS].avi 119.000000 208.800000  89,8
Naruto_Shippuuden_TV2_[003_of_XXX]_[Rus_Jap]_[NIKITOS].avi 130.960000 220.800000 89,84
Naruto_Shippuuden_TV2_[004_of_XXX]_[Rus_Jap]_[NIKITOS].avi 170.000000 259.840000 89,84
Naruto_Shippuuden_TV2_[005_of_XXX]_[Rus_Jap]_[NIKITOS].avi 81.040000  170.760000 89,72
Naruto_Shippuuden_TV2_[006_of_XXX]_[Rus_Jap]_[NIKITOS].avi 191.960000 281.760000  89,8
Naruto_Shippuuden_TV2_[007_of_XXX]_[Rus_Jap]_[NIKITOS].avi 0.000000   89.840000  89,84

Тем самым я убил двух зайцев: уменьшил время обработки и убрал дополнительную функцию, вычисляющую время между найденными кадрами и пишущую в лог ошибку и пропускающую перекодирование, если это время превышало 90 секунд. Но так как значение местонахождения конечного кадра было уже относительным к начальному (например, для начального теперь оно могло быть 150, а для конечного — 88), поэтому для команды кодирования нужно эти значения складывать, и это вычисленное значение даёт большее число знаков после запятой.

Для лучшего ориентирования я добавил вывод информации на экран. Здесь видно, что поиск начала занимает 43 секунды, а поиск конца уже 5.

2024.01.02 17:49:45 Поиск начала заставки в Naruto_Shippuuden_TV2_[147_of_XXX]_[Rus_Jap]_[NIKITOS].avi...
2024.01.02 17:50:28 Начало заставки найдено на 384.509125 сек.
2024.01.02 17:50:28 Поиск конца заставки в Naruto_Shippuuden_TV2_[147_of_XXX]_[Rus_Jap]_[NIKITOS].avi...
2024.01.02 17:50:33 Конец заставки найден на 472.847373962402 сек.

Есть и журнал ошибок. Какие-то файлы всё же приходится обрезать самому в Avidemux, пока компьютер перелопачивает остальные, к тому же, среди набора серий попадается неформат — склеенные или разделённые на несколько частей эпизоды. Но это всё равно лучше, чем обрабатывать руками все 235 видеофайлов.

2024.01.02 14:28:50 Конец заставки не найден в Naruto_Shippuuden_TV2_[125_of_XXX]_[Rus_Jap]_[NIKITOS].avi
2024.01.02 14:29:20 Начало заставки не найдено в Naruto_Shippuuden_TV2_[126_of_XXX]_[Rus_Jap]_[NIKITOS].avi
2024.01.03 07:23:11 Начало заставки не найдено в Naruto_Shippuuden_TV2_[183_of_XXX]_[Rus_Jap]_[NIKITOS].avi

Собственно, скрипт.

## Заставка 1 рассвет
#$episodes = 1..30
## Заставка 2 глаз
#$episodes = 31..53
## Заставка 3 птица
#$episodes = 54..77
#$episodesLong = ,55+71
## Заставка 4 облака
$episodes = 78..102                         # Диапазон обрабатываемых эпизодов
$episodesLong = ,79+80+82+85+91+92+100+102  # Эпизоды с увеличенной длиной
## Заставка 5 водопад
#$episodes = 103..128
#$episodesLong = ,104+106+107+111..115+120+123..125
## Заставка 6 мороженое
#$episodes = 129..153
#$episodesLong = ,131+133+135+138..141+143+145+148+149..155
## Заставка 7 зонтик
#$episodes = 154..179
#$episodesLong = 149..155+157..159+162+167+170..172+174..179
## Заставка 8 лицо
#$episodes = 180..196+199..205
#$episodesLong = 180..184+186..208
## Заставка 9 летающие острова
#$episodes = 206..230
#$episodesLong = ,208+210+211+213..235
## Заставка 10 скала
#$episodes = 231..235
#$episodesLong = 213..235

$inFolder = 'C:\Users\User\downloads\Naruto Shippuuden TV2 `[NIKITOS`] HWP' # Каталог с исходниками
$outFolder = 'C:\temp'             # Каталог с обработанными файлами
$logFolder = 'C:\temp'             # Каталог с логами
$csv = "$logFolder\log.txt"        # Журнал/CSV 
$logError = "$logFolder\error.txt" # Журнал ошибок
$ext = '*.avi'                     # Маска и расширение исходных файлов 
$episodes = $episodes |% {$_.ToString("000")}              # Переделать номера в трёхзначные для поиска файлов
$openingStartPic = Get-Item "$inFolder\opening4_start.jpg" # Картинка начального кадра
$openingEndPic = Get-Item "$inFolder\opening4_end.jpg"     # Картинка конечного кадра
$openingMax = 90          # Длина заставки (сек.), глубина поиска конечного кадра
$vidDuration = 1206       # Стандартная длина эпизода (сек.)
$vidDurationLong = 1266   # Увеличенная длина эпизода (сек.)

# Функция поиска кадров
function Find-Frame ($file,$pic,$startPoint) {
    if ($startPoint) {
        # Конечный кадр
        (& ffmpeg.exe -hwaccel_output_format qsv -ss $startPoint -t $openingMax -an -i $file -loop 1 -i $pic -filter_complex "blend=difference:shortest=1,blackframe=80" -f null - 2>&1) -match 'blackframe.*type:I'
    }
    else {
        # Начальный кадр
        (& ffmpeg.exe -hwaccel_output_format qsv -t 10:00 -an -i $file -loop 1 -i $pic -filter_complex "blend=difference:shortest=1,blackframe=80" -f null - 2>&1) -match 'blackframe.*type:I'
    }
}

# Функция логирования/вывода
function log ($text,$file,$color) {
    $t = (get-date).tostring("yyyy.MM.dd HH:mm:ss")
    if ($file) {
        Tee-Object -InputObject "$t $text" -FilePath $file -Append
    }
    else {
        if (-not $color) {$color = "white"}
        Write-Host -fore $color "$t $text"
    }
}

cd $inFolder

dir $ext |? name -match "$($episodes -join '|')" |% {

# Поиск начала заставки
log -text "Поиск начала заставки в $($_.name)..." -color yellow 
$openingStart = Find-Frame -file $_.name -pic $openingStartPic.fullname
if ($openingStart) {
    $openingStartFrame = ($openingStart[0] -split ' ')[-3] -replace "t:"
    log -text "Начало заставки найдено на $openingStartFrame сек." -color green
}
else {
    log -text "Начало заставки не найдено в $($_.name)" -file $logError
    continue
}

# Поиск конца заставки
log -text "Поиск конца заставки в $($_.name)..." -color yellow
$openingEnd = Find-Frame -file $_.name -pic $openingEndPic.fullname -startPoint $openingStartFrame
if ($openingEnd) {
    $openingEndFrame = [single]$openingStartFrame + [single](($openingEnd[-1] -split ' ')[-3] -replace "t:")
    log -text "Конец заставки найден на $openingEndFrame сек." -color green
}
else {
    log -text "Конец заставки не найден в $($_.name)" -file $logError
    continue
}

# CSV
"$($_.name);$openingStartFrame;$openingEndFrame" |Out-File $csv -Encoding default -Append

# Выбор длины серии в целом
if ($_.basename -match "$($episodesLong -join '|')") {$tail = $vidDurationLong}
else {$tail = $vidDuration}

# Обработка/кодирование
& ffmpeg.exe -y -hide_banner -hwaccel_output_format qsv -i $_.name `
-filter_complex `
"[0:0]trim=start=0:end=$($openingStartFrame),setpts=PTS-STARTPTS[av];
 [0:1]atrim=start=0:end=$($openingStartFrame),asetpts=PTS-STARTPTS[aa];
 [0:0]trim=start=$($openingEndFrame):end=$($tail),setpts=PTS-STARTPTS[bv];
 [0:1]atrim=start=$($openingEndFrame):end=$($tail),asetpts=PTS-STARTPTS[ba];
 [av][bv]concat[outv];[aa][ba]concat=v=0:a=1[outa]" `
-map [outv] -map [outa] -c:v hevc_qsv -global_quality:v 28 `
-c:a libopus -ac 1 -b:a 64k `
"$outFolder\$($_.basename).mp4"

Clear-Variable openingStart,openingEnd,openingStartFrame,openingEndFrame
}

После перекодирования всех серий они стали занимать 9,5 ГБ вместо исходных 67.

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

Хорошо не наступает

Самое большое количество суицидов — 45-47 лет, это тот самый «кризис среднего возраста». Когда мы растём, то нам говорят: если будешь делать так и так, то будет хорошо. И вот человек хорошо учится, старается всё делать правильно, находит плюс-минус интересную работу, поднимается по иерархической лестнице, у него семья, дети.

И вот уже дожил до 40, вроде бы всё делает правильно, но только «хорошо» не наступает и он понимает, что вроде бы уже и не наступит. Он попадает в ситуацию переосмысления и вспоминает, чего на самом деле хотел.

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

Конец велосезона - 2023

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

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

Приведу статистику пробега по годам с момента покупки велосипеда:

Год Пройдено км
2016 2809
2017 3720
2018 3973
2019 3456
2020 4237
2021 3076
2022 3943
2023 4542

Сейчас на счётчике 29756 км, до 30000 не доехал всего-то 250 км, но теперь уж в следующем сезоне.

Настройка пульта для встроенного ИК-датчика Orange Pi

Дано: Orange Pi PC Plus с установленным LibreELEC 11, которым хотелось бы управлять с помощью пульта. Инфракрасный приёмник у Orange Pi имеется, поэтому нужно просто научить систему понимать с этого пульта сигналы.

Прежде всего нужно купить какой-нибудь пульт или взять уже имеющийся, у которого есть кнопки стрелок, старт/пауза, стоп, ОК и тому подобные, чтобы можно было нормально управлять медиацентром. Я купил пульт для телевизора Supra RS41-MOUSE за 200 рублей.

Заходим по SSH в систему. В инструкции по настройке пультов сначала рекомендуют подбирать совместимые конфигурации из списка по пути /usr/lib/udev/rc_keymaps, но их там полторы сотни и нет ничего похожего по названию, так что я не стал возиться, а сразу перешёл к созданию собственной конфигурации, описанному в разделе Advanced.

Выводим список поддерживаемых протоколов управления (supported kernel protocols):

OrangePiPCPlus:~ # ir-keytable
Found /sys/class/rc/rc0/ with:
        Name: sunxi-ir
        Driver: sunxi-ir
        Default keymap: rc-empty
        Input device: /dev/input/event0
        LIRC device: /dev/lirc0
        Attached BPF protocols:
        Supported kernel protocols: lirc rc-5 rc-5-sz jvc sony nec sanyo mce_kbd rc-6 sharp xmp imon rc-mm
        Enabled kernel protocols: lirc
        bus: 25, vendor/product: 0001:0001, version: 0x0100
        Repeat delay = 500 ms, repeat period = 125 ms

Нужно подобрать протокол, с которым совместим пульт. В моём случае подошёл nec, после включения которого в консоли начали отображаться коды кнопок при их нажатии на пульте:

OrangePiPCPlus:~ # ir-keytable -p nec -t
Protocols changed to nec
Testing events. Please, press CTRL-C to abort.
1018.929441: lirc protocol(necx): scancode = 0x710205
1018.984459: lirc protocol(necx): scancode = 0x710205 repeat
1021.562363: lirc protocol(necx): scancode = 0x710205
1024.293455: lirc protocol(necx): scancode = 0x710204
1024.348472: lirc protocol(necx): scancode = 0x710204 repeat
1028.355072: lirc protocol(necx): scancode = 0x710268
1028.410087: lirc protocol(necx): scancode = 0x710268 repeat
1028.517783: lirc protocol(necx): scancode = 0x710268 repeat
1030.043425: lirc protocol(necx): scancode = 0x710262
1030.098426: lirc protocol(necx): scancode = 0x710262 repeat

Отлично, теперь нужно нарисовать карту кнопок (keymap), где прописывается протокол пульта и соответствие кодов кнопок с их функциями. Список функций можно посмотреть с помощью команды irrecord -l | grep ^KEY или в секции <remote device="devinput"> файла /usr/share/kodi/system/Lircmap.xml.

В итоге у меня получилась конфигурация, приведённая ниже. Закомментированы кнопки, к которым я не нашёл близкой функции, а кнопка POWER была чуть позже изменена на ENTER, потому что одноплатник выключался, но включить его потом с пульта было нельзя.

# table supra_rs41, type: nec
# 0x710202 KEY_POWER
0x710202 KEY_ENTER
# 0x71020f source
0x710220 KEY_RED
0x710234 KEY_GREEN
0x71022b KEY_YELLOW
0x71022c KEY_BLUE
0x710227 KEY_MUTE
0x710225 KEY_ZOOM
# 0x710200 freeze
0x710228 KEY_TEXT
0x710203 KEY_FAVORITES
0x710232 KEY_SUBTITLE
0x710240 KEY_AUDIO
0x710255 KEY_RECORD
0x710226 KEY_REWIND
0x71021e KEY_FORWARD
0x710239 KEY_PREVIOUS
0x710213 KEY_NEXT
0x71021a KEY_PLAY
0x710201 KEY_STOP
0x710260 KEY_UP
0x710261 KEY_DOWN
0x710265 KEY_LEFT
0x710262 KEY_RIGHT
0x710268 KEY_ENTER
0x71022d KEY_MENU
0x71021f KEY_ESC
0x710207 KEY_VOLUMEUP
0x71020b KEY_VOLUMEDOWN
0x710222 KEY_HOME
# 0x710221 mouse
0x710212 KEY_CHANNELUP
0x710210 KEY_CHANNELDOWN
0x710204 KEY_1
0x710205 KEY_2
0x710206 KEY_3
0x710208 KEY_4
0x710209 KEY_5
0x71020a KEY_6
0x71020c KEY_7
0x71020d KEY_8
0x71020e KEY_9
0x710211 KEY_0
0x710223 KEY_DISPLAYTOGGLE
# 0x710250 return

Теперь надо создать файл с нашей картой кнопок и запустить его:

OrangePiPCPlus:~ # nano /storage/.config/rc_keymaps/supra_rs41
OrangePiPCPlus:~ # ir-keytable -c -w /storage/.config/rc_keymaps/supra_rs41
Read supra_rs41 table
Old keytable cleared
Wrote 39 keycode(s) to driver
Protocols changed to nec

Пульт сразу же начинает работать. Осталось добавить эту конфигурацию в автозагрузку:

OrangePiPCPlus:~ # echo "* * supra_rs41" > /storage/.config/rc_maps.cfg