- Информация о материале
- Автор: Андрей
- Категория: Статейки
- Просмотров: 103
- L - расчетная длина прыжка в обычном пространстве;
- Lc - критическая навигационная дистанция, принимаемая равной 1,2 светового года;
- σ(L) - характеристический радиус локализации выхода;
- τ(L) - характеристический радиус временной локализации выхода;
- r0 - расчетная точка выхода;
- RH - радиус Хаббла;
- RG - эффективный радиус захвата Врат;
- Λ - постоянная Найяра;
- σc - радиус локализации на критической дистанции, принимаемый равным 9 световых минут;
- τc - временная неопределенность на критической дистанции.
Формулировка. При уменьшении длины прыжка ниже критической дистанции Lc радиус локализации выхода растет экспоненциально по обратной длине прыжка. На дальних дистанциях рост сохраняется, но становится медленным и логарифмическим.
Нормировка:
σc = 9 св. минут
Λ ≈ 284 св. минут
α ≈ 0.12
Следствия.
- При L = Lc радиус локализации стабильно определяется в пределах около 9 световых минут.
- При L → 0+ величина σ(L) стремится к бесконечности.
- На больших расстояниях радиус локализации растет слабо, примерно как ln(L).
Формулировка. Гравитация сама по себе не запрещает гиперпрыжок, однако локальная кривизна пространства-времени и, в особенности, ее градиенты искажают фазовую геометрию прожига гипер-ядра. В результате расчетная траектория перехода получает дополнительное смещение относительно идеального решения.
Физический смысл. ГПТ-чувствительный контур реагирует не на однородное ускорение как таковое, а на приливную неоднородность метрики. Поэтому определяющим параметром является не величина поля вида GM/r², а величина его пространственного градиента, пропорциональная GM/r³.
где:
- ΔR_g - добавочное гравитационное смещение точки выхода;
- μ - эмпирический коэффициент ГПТ-чувствительности;
- G - гравитационная постоянная;
- M - масса ближайшего доминирующего тела;
- r - расстояние до центра масс;
- D - расчетная длина прыжка.
Обобщенная форма записи:
где ||T|| - норма приливного тензора локальной кривизны.
- Чем ближе старт к массивному телу, тем хуже предсказуемость выхода.
- Чем дальше прыжок, тем сильнее накапливается гравитационная ошибка.
- Низкие орбиты и глубокие гравитационные колодцы считаются навигационно неблагоприятными зонами.
- Точки Лагранжа, высокие орбиты и межпланетное пространство являются предпочтительными областями старта.
Суммарная эффективная ошибка локализации может оцениваться как:
где σ(L) - базовая ошибка локализации, задаваемая правилом Найяра.
Формулировка. Для устойчивой работы Врат требуется не только малая пространственная ошибка, но и минимальная приливная деформация рабочей метрики в объеме фазовой синхронизации.
Практически это означает, что Врата стандартного класса устанавливаются только в областях, где гравитационное преломление траектории мало и стабильно во времени.
Нормативно предпочтительные зоны:
- точки Лагранжа;
- высокие стационарные орбиты;
- межпланетные якорные области;
- зоны с малым градиентом гравитационного поля.
Следствие. Если одна пара Врат размещена в зоне слабой приливной кривизны, а другая - в низкой орбите или у поверхности массивного тела, общий канал становится плавающим и не обеспечивает гарантированного попадания в приемную камеру.
Военные структуры могут временно развертывать тактические Врата в областях с повышенной приливной кривизной - в том числе на низких орбитах или вблизи планетарных тел. Однако при этом аварийность канала резко возрастает.
Практические риски:
- нестабильность окна выхода;
- неполное совмещение канала;
- выход объекта вне приемной камеры;
- врезание в твердую породу, конструкцию или атмосферу;
- размазывание полезной нагрузки по пространственно-временному окну выхода.
По этой причине тактические Врата рассматриваются как средство чрезвычайного применения, а не как допустимая гражданская инфраструктура.
ГПТ-смещение не следует путать с общей ошибкой навигационного решения. Даже идеально рассчитанный прыжок может оказаться неудачным, если старт выполнен в зоне, где локальная приливная геометрия недостаточно благоприятна для стабильного прожига гипер-ядра.
Формулировка. Существует такая малая длина прыжка LH, при которой радиус локализации сравним с радиусом Хаббла:
В принятой инженерной нормировке это достигается примерно при:
Интерпретация. При попытке прыгнуть “слишком близко” область допустимого выхода разрастается до космологических масштабов. Объект может выйти практически в любом направлении относительно цели, включая направление, противоположное расчетному.
Важно: пока L > 0, вероятность выхода вблизи расчетной точки остается ненулевой, сколь бы малой она ни была.
Формулировка. Прыжок с нулевой расчетной длиной является сингулярным предельным случаем и не допускает конечной области локализации выхода.
Для точного нулевого прыжка вероятность выхода в любой конечной области пространства принимается равной нулю:
Практический вывод. Нулевой прыжок запрещен как форма гарантированной безвозвратной утраты отправляемого объекта. То есть, для человека это фактически смерть.
Формулировка. Гиперпрыжок определяется не относительно локального времени отправителя, а относительно глобальной гиперхронологической поверхности, задаваемой структурой гиперсреды.
Поэтому мгновенный переход не означает нулевой временной сдвиг в произвольной системе отсчета.
где:
- Δt_h - системный положительный лаг перехода;
- ε_t - случайная временная флуктуация;
- математическое ожидание ε_t близко к нулю, но физически запрещены сценарии нарушения причинности.
Физический смысл. Выход может быть размазан во времени, но стандартный гиперпрыжок не допускает прибытия в собственное прошлое.
Формулировка. Рост пространственной неопределенности выхода сопровождается ростом временной неопределенности, связанной с ней степенным законом.
Для инженерной нормировки допустимо принимать:
β = 2/3
Интерпретация.
- На критической дистанции временной разброс составляет секунды.
- На безопасных дальних прыжках он обычно несущественен.
- На опасных ближних прыжках он может возрастать до часов, дней, лет и более.
Формулировка. Ни одна последовательность допустимых гиперпрыжков не может перевести объект в область пространства-времени, причинно предшествующую моменту прожига его гипер-ядра.
Следствие. Стандартная ГПТ не может быть использована для построения машины времени, несмотря на наличие временного разброса выхода.
Формулировка. Никакой конечный экспериментальный протокол не может строго доказать абсолютную тождественность субъекта или объекта на входе и на выходе гиперпрыжка.
Пусть X_in - объект на входе, X_out - объект на выходе. Тогда даже если конечный набор наблюдаемых полностью совпадает:
из этого не следует строгая онтологическая идентичность:
Следствие. ГПТ допускает доказательство структурной, функциональной, биографической и информационной непрерывности, но не абсолютного метафизического тождества.
Формулировка. Врата работают как фазово-навигационные маяки, уменьшающие область локализации выхода, но только при дистанции не ниже критической:
Для допустимого режима:
где η << 1 - коэффициент навигационного сжатия.
Инженерный смысл. Врата не отменяют правило Найяра. Они лишь делают дальний прыжок инженерно управляемым, сужая окно выхода до нескольких метров или десятков метров.
Формулировка. Врата уменьшают не только пространственную, но и временную область локализации выхода.
где:
- TG - временное окно Врат;
- ηt - коэффициент темпорального сжатия;
- обычно TG лежит в пределах от миллисекунд до микросекунд.
Следствие. Без Врат гиперпрыжок - это искусство попасть примерно туда и примерно тогда. Со Вратами - это уже транспорт.
Формулировка. Построение сети Врат на расстояниях меньше критической дистанции Lc не дает устойчивой навигационной выгоды.
При L < Lc вероятность промаха по Вратам растет в соответствии с общим законом ближней расходимости локализации.
Следствие. Вратные сети строятся редкими магистральными узлами, а не густой локальной решеткой.
Формулировка. Расход гипертоплива не зависит от длины прыжка и определяется только параметрами прожига гипер-ядра и массой транспортируемой системы.
где:
- mf - масса гипертоплива;
- M - полная масса системы;
- χ - класс гипер-ядра;
- ν - режим прожига.
Ключевое следствие. Прыжок до Андромеды и прыжок до Стрельца A* стоят одинаково по топливу при одинаковой массе и классе прожига. Ограничение задается не “дальностью как ценой”, а навигацией и доступностью топлива.
Формулировка. Гипертопливо не переносится через образующуюся при прожиге червоточину и потому не может быть доставлено “само собой” в точку выхода.
Следствие. Невозможно организовать простую схему стратегического накопления гипертоплива на удаленных узлах за счет обычной гипердоставки. Каждый крупный узел должен иметь собственную производственную или квазипроизводственную базу.
Практический смысл. Главная проблема ГПТ - не цена прыжка как такового, а сверхзатратное производство, кратковременная стабилизация и жесткие ограничения хранения гипертоплива.
Формулировка. В момент прожига гипер-ядра объект на стороне входа необратимо утрачивает устойчивую локальную материальную конфигурацию. Этот процесс сопровождается кратковременным выбросом излучения и частиц, однако суммарная локально наблюдаемая энергия выброса определяется не полной массой корабля, а лишь малой долей энергии гипертоплива, реально диссипирующей в исходное пространство.
Иначе говоря: корабль, полезная нагрузка и топливо на стороне входа действительно погибают как локально существующая система, однако наблюдаемый “пшгик” не является полной энергетической разверткой всей отправляемой массы. Локально фиксируется только входной остаток, связанный с частичным распадом энергетики прожига.
В современной инженерной модели расход гипертоплива задаётся как малая доля отправляемой массы:
где:
- m_fuel - масса гипертоплива, подаваемого на прожиг;
- η - удельная топливная доля;
- M_ship - полная масса корабля вместе с полезной нагрузкой.
Для стандартного человеческого ГПТ-флота принимается:
То есть на каждый килограмм отправляемой массы требуется примерно 0.0001 кг гипертоплива. Иными словами:
- 0.1 г топлива на 1 кг массы;
- 100 г топлива на 1 тонну массы;
- 100 кг топлива на 1000 тонн массы.
Полная энергетическая ёмкость топлива описывается как:
где:
- E_fuel - полная энергия топлива, вовлечённого в прожиг;
- ξ - коэффициент эффективной энергетической развёртки топлива;
- c - скорость света.
В эксплуатационных расчётах обычно полагают:
Это не означает, что наблюдатель на стороне входа увидит полную релятивистскую развертку топлива. Напротив - именно здесь появляется понятие входного остатка.
Локально наблюдаемая энергия выброса на стороне входа составляет лишь малую долю от полной энергии топлива:
или, развёрнуто:
где:
- E_res - энергия локально наблюдаемого входного остатка;
- κ - коэффициент входного остатка, причём κ << 1;
- ξ - коэффициент энергетической развёртки топлива;
- η - удельная топливная доля;
- M_ship - полная масса корабля;
- c - скорость света.
Типичный инженерный диапазон для штатных гражданских прожигов:
При этом в большинстве мирных транспортных операций за рабочую норму принимают порядок:
Следствие 1. Даже сравнительно яркий входной прожиг соответствует лишь очень малой доле энергии топлива:
Следствие 2. Наблюдаемый “пышщщ” не является аннигиляцией корабля. Он является остаточным эффектом прожига и частичного локального сброса энергии топлива.
Следствие 3. Закон Риттера–Аникеева согласуется с теоремой Цзянь–Риттера: корабль на стороне входа гибнет полностью, но по эту сторону не остаётся энергетического эквивалента всей его массы.
Если принять:
ξ = 1
κ_typ = 10^(-8)
то получаем:
или, если масса корабля выражена в килограммах:
или, если масса корабля выражена в тоннах:
Для грубого перевода в условный тротиловый эквивалент можно использовать:
Это означает, что в мирном режиме энергия входного остатка растёт линейно с массой корабля и остаётся опасной, но не апокалиптической. Главная проблема - не только суммарная энергия, но и её форма: жесткое излучение, плазменный выброс, локальная ионизация, электромагнитная наводка и вторичные радиационные эффекты.
Ниже приведена ориентировочная таблица для штатного гражданского прожига при η = 10^(-4), ξ = 1 и κ = 10^(-8). Под безопасной дистанцией понимается минимальное расстояние от доков, жилых орбитальных объектов, терминалов, складов, ремонтных платформ и иной чувствительной гражданской инфраструктуры.
| Класс корабля | Типичная масса | Масса топлива | Оценка E_res | Условный эквивалент | Рекомендуемая дистанция старта |
|---|---|---|---|---|---|
| Эсминец (минимальный класс с бортовым ГПТ) |
20 000 т | 2 т | ≈ 1.8 * 10^12 Дж | ≈ 430 кг ТНТ | не менее 800 км |
| Крейсер | 80 000 т | 8 т | ≈ 7.2 * 10^12 Дж | ≈ 1.72 т ТНТ | не менее 1 500 км |
| Тяжёлый транспорт / танкер | 150 000 т | 15 т | ≈ 1.35 * 10^13 Дж | ≈ 3.23 т ТНТ | не менее 2 200 км |
| Пассажирский лайнер дальнего следования | 250 000 т | 25 т | ≈ 2.25 * 10^13 Дж | ≈ 5.38 т ТНТ | не менее 3 000 км |
| Сверхтяжёлый грузовик / контейнеровоз | 500 000 т | 50 т | ≈ 4.5 * 10^13 Дж | ≈ 10.76 т ТНТ | не менее 5 000 км |
Примечание. В человеческой практике корабли легче эсминца бортовым ГПТ-оборудованием, как правило, не оснащаются. Причины: громоздкость ядра, жесткие требования к радиационной защите, необходимость в сложном сопусвующем оборудовани, огромная стоимость, сложность обслуживания и нерациональность размещения столь тяжёлой системы на малых судах. Поэтому в таблице не рассматриваются шаттлы, тендеры и прочие малые аппараты.
- Отбытие в гиперпрыжок производится вдали от доков. Корабль сначала выводится на удалённую стартовую позицию и только затем прожигает гипер-ядро.
- Посадка пассажиров и экипажа осуществляется через тендеры, шлюпки, челноки и служебные катера. Для крупных лайнеров и грузовиков это штатная практика.
- Прожиг вблизи гражданских станций, жилых орбит и плотной сервисной инфраструктуры запрещён. Основные риски: гамма-компонента, ионизация среды, плазменный выброс и электромагнитные перегрузки.
- Стандартные Врата используются в первую очередь на приём. Они работают как управляемый объём захвата и безопасного появления корабля, а не как симметричная дверца туда-сюда.
- На передачу обычные Врата гражданского класса, как правило, не работают. Одноразовые и специальные режимы теоретически возможны, но дороги, капризны и инфраструктурно неудобны.
Прибывший через Врата корабль не “подруливает к пирсу” сам по себе. Его сначала принимает специальный объем безопасности, после чего в работу вступают орбитальные буксиры, тяговые фермы, швартовочные сети и диспетчерские алгоритмы разведения масс.
Крупнотоннажные лайнеры и грузовики, как правило, либо медленно перегоняются буксирными команами к внешним докам, либо обслуживаются на удаленных рейдах через контейнерные лихтеры, пассажирские тендеры и сервисные модули. Именно поэтому большой межзвёздный корабль обычно висит далеко за пределами основной доковой суеты.
Входной остаток объясняет, почему межзвездный флот выглядит не как стая маршруток, а как тяжелая инфраструктура с удаленными стартовыми позициями, тендерами, буксирами, рейдами и очень нервными диспетчерами.
Формулировка. Входной остаток проявляется преимущественно как краткий импульс жесткого электромагнитного излучения с доминирующей фотонной компонентой, сопровождаемый потоком слабовзаимодействующих и экзотических частиц.
Условная запись спектрального баланса:
где:
- E_gamma - энергия фотонной компоненты (основной вклад);
- E_nu - энергия нейтринной компоненты;
- E_ex - энергия экзотических частиц или квазичастиц, фиксируемых косвенно;
- E_th - энергия вторичных тепловых и плазменных эффектов.
Инженерная оценка для штатного прожига:
E_nu / E_res ≈ 0.01 ... 0.10
E_ex / E_res ≈ 0.01 ... 0.20
E_th / E_res <= 0.05
Интерпретация. Для невооруженного наблюдателя входной прожиг выглядит как краткий ослепительный световой срыв, иногда сопровождаемый ударной волной, плазменным ореолом и остаточным свечением. При инструментальной регистрации доминирует именно фотонный импульс, что и создает субъективное впечатление, будто объект “улетел в свет”.
Формулировка. На замедленной съемке и при фазово-чувствительной регистрации входная деструкция локального состояния имеет две характерные стадии:
- предсрывное свечение - вещество объекта начинает неравномерно светиться, теряя обычные оптические свойства;
- фаза "схлопывания" - контуры объекта нарушаются, после чего он визуально исчезает, оставляя фотонно-плазменный остаток.
Условная временная запись процесса:
где:
- S_matter - обычное локальное материальное состояние объекта;
- S_lum - краткая светящаяся предсрывная фаза;
- ∅_local - отсутствие объекта как допустимого локального состояния;
- R_res - наблюдаемый входной остаток.
Практический вывод. На стороне входа не остается сомнений, что объекту “по эту сторону” пришел конец. Именно поэтому философские споры о тождестве субъекта после прыжка для большинства людей начинаются уже после того, как они видят прожиг хотя бы однажды вблизи.
Формулировка. Гипер-ядро наводится не на текущее видимое положение звезды или планетной системы, а на расчетную фазово-гравитационную конфигурацию области выхода, в которой цель окажется в момент ожидаемого выхода из гиперперехода.
Иначе говоря, прыжок совершается не туда, где цель видна, а туда, где она должна быть после учета движения системы, гравитационной топографии региона, временного сдвига и статистической локализационной ошибки.
где:
- Target - целевое решение для прожига гипер-ядра;
- r_future - прогнозное положение цели на момент выхода;
- Phi_metric - карта локальной и маршрутной гравитационной метрики;
- tau_exit - ожидаемое временное окно выхода;
- sigma_loc - допустимая ошибка локализации.
Интерпретация. Для ГПТ-навигации звезда или планета являются не столько оптическим ориентиром, сколько гравитационно-фазовым узлом. Именно поэтому в основе гиперпрыжковой навигации лежат не обычные звездные карты, а прогнозные карты гравитационных колодцев, приливных градиентов, фазовых якорей и устойчивых метрических опор.
Следствие 1. Минимальный горизонт надежного прогноза не может быть меньше критической дистанции прыжка. По этой причине целеуказание всегда строится на прогнозе будущего состояния системы, а не на ее “настоящем” визуальном положении.
Следствие 2. Ошибка в карте гравитационных колодцев опаснее простой угловой ошибки наведения. Неправильная оценка метрики может привести к выходу “рядом со звездной системой”, но в принципиально неблагоприятном гравитационном окне.
Следствие 3. Врата играют роль внешних фазово-навигационных якорей. При наличии Врат гипер-ядро использует не только общую прогнозную карту, но и готовую приводную сигнатуру, резко уменьшающую флуктуации выхода.
На практике экипаж не “вводит координаты звезды”, а загружает в навигационную автоматику решение прожига, в котором уже учтены:
- текущая карта локальной гравитационной среды;
- маршрутная гравитационная топография;
- прогноз будущего состояния цели;
- масса корабля и инерционный профиль;
- режим подачи гипертоплива;
- наличие Врат или иных фазовых маяков.
По этой причине даже хорошо обученные навигаторы, как правило, понимают ГПТ только в прикладном смысле. Сама процедура целеуказания остается во многом черным ящиком, и именно это делает гиперпрыжок скорее ремеслом высокой точности, чем прозрачной инженерной процедурой.
ГПТ прыгает не к звезде, а к тому месту, куда звезда прилетит, когда ты там вывалишься.
Формулировка. Гипер-ядро является многоразовым прожиговым реактором, однако его эксплуатационный ресурс ограничен. Каждый гиперпрыжок вызывает накопление необратимых изменений в активной зоне, фазовом каркасе и системах стабилизации. По этой причине ядро не может использоваться неограниченно часто и требует периодического обслуживания, перекалибровки и, в пределе, замены ключевых компонентов.
Иначе говоря: гипер-ядро - это не волшебная кнопка и не вечный двигатель, а сложный, дорогой и капризный узел, который можно “надорвать” плохим прожигом, неудачной навигацией или злоупотреблением тяжелыми режимами.
где:
- Wear_jump - условная стоимость одного прожига по ресурсу ядра;
- W0 - базовый износ штатного прожига;
- K_range - поправка на длину прыжка и геометрию решения;
- K_grav - поправка на гравитационную сложность стартовой области;
- K_stab - поправка на чистоту и стабильность прожига;
- K_gate - поправка на наличие или отсутствие внешней приводной стабилизации Врат.
Интерпретация коэффициентов.
- K_range обычно близок к единице в штатных дальних режимах, но может резко возрастать в ближних и нестандартных прыжках;
- K_grav возрастает вблизи сильных гравитационных градиентов, на низких орбитах и в иных неблагоприятных стартовых зонах;
- K_stab отражает качество работы автоматики, стабильность топлива и чистоту фазового режима;
- K_gate обычно меньше единицы при работе на готовый приводной якорь и больше единицы при полностью автономном прожиге.
Следствие 1. Износ ядра определяется не только числом прыжков, но и качеством этих прыжков. Два корабля с одинаковым количеством прожигов могут иметь принципиально разное остаточное состояние ядра.
Следствие 2. Штатный дальний прыжок из хорошей стартовой зоны может быть для ядра менее вредным, чем формально более короткий, но грязный или гравитационно неудобный прожиг.
Следствие 3. Ближние прыжки, аварийные режимы, работа без Врат и прожиги из неблагоприятных гравитационных областей ускоряют деградацию активной зоны и фазового каркаса.
В гражданском флоте гипер-ядро рассматривается как один из главных активов корабля. После каждого прожига проводится:
- диагностика активной зоны;
- контроль фазового каркаса;
- оценка остаточного дрейфа параметров;
- сверка с эталонной сигнатурой прожига;
- при необходимости - перекалибровка и частичная замена узлов.
Военные суда допускают более агрессивный режим эксплуатации, но расплачиваются за это ускоренной выработкой ресурса и повышенной вероятностью тяжелых отказов.
Именно по причине ограниченного ресурса гипер-ядра межзвездный корабль не ведет себя как “маршрутка”, прыгающая по мелочи каждые полчаса. Даже при наличии топлива каждый прожиг имеет:
- цену по ресурсу ядра;
- цену по ресурсу обслуживающей автоматики;
- цену по времени на послепрыжковую диагностику;
- цену по риску накопления скрытых дефектов.
В этом смысле хороший капитан экономит не только топливо, но и прожиги.
- выполнить прожиг ядра - срвершить гипер прыжок;
- грязный прожиг - прыжок с плохой фазовой чистотой;
- надорвать ядро - сильно ускорить деградацию тяжелым режимом;
- ядро повело - накопился опасный дрейф параметров;
- сухое окно - хороший стабильный режим выхода.
Примечание. Точный механизм деградации гипер-ядра остается предметом спора. В инженерной практике достаточно того, что ядро статистически изнашивается, предсказуемо ухудшает параметры после тяжелых прожигов и не прощает слишком вольного обращения. Как и сама ГПТ в целом, эта область гораздо лучше описана эмпирически, чем понята теоретически.
Несмотря на то, что входной остаток много слабее полного энергетического эквивалента массы корабля и топлива, зона прожига остается крайне опасной из-за:
- жесткого фотонного импульса;
- возможного гамма-излучения;
- вторичной ионизации среды;
- плазменного выброса;
- локальных электромагнитных перегрузок.
По этой причине присутствие незащищенного персонала в непосредственной близости от зоны прожига категорически запрещено даже при штатном прыжке.
Закон Риттера-Аникеева усиливает практический смысл теоремы Брукса-Аникеева о трансгиперной неидентифицируемости:
- Чем ближе цель - тем опаснее прыжок.
- Очень далекий прыжок не дороже ближнего по топливу.
- Врата не отменяют физику ГПТ, а цивилизованно живут в ее рамках.
- ГПТ обеспечивает функциональную непрерывность, но не дает строгого доказательства абсолютной идентичности субъекта.
- Прыжок “в ноль” является гарантированной формой безвозвратной утраты.
В прикладной навигации удачным обычно считается прыжок, при котором пространственная ошибка локализации не превышает 5-10 световых минут. Все более точные режимы требуют либо очень качественной навигационной привязки, либо использования Врат как пространственно-временных приводных маяков.
Ниже приведен сводный реестр ключевых ученых, чьи имена закрепились в теории гиперпрыжков, инженерии Врат, физике гипертоплива, философии тождества и медицине гиперперехода.
Ученый Марк Фейнман, фигурирующий в данном реестре, является лишь однофамильцем известного физика Ричарда Фейнмана и не отождествляется с ним в каноне данной вселенной.
| № | Ученый | Где фигурирует | Специализация | Научная роль | Место в истории ГПТ |
|---|---|---|---|---|---|
| 1 | Аравинд Найяр | Правило Найяра о ближней расходимости локализации; Постоянная Найяра (Λ); Следствие Найяра-Шеноя | Физик-теоретик, основатель ГПТ | Открыл фундаментальный закон локализации и доказал контринтуитивную опасность ближних прыжков. До него экспедиции улетали вникуда и не возвращались. | Первая эпоха; центральная фигура всей теории ГПТ |
| 2 | Киран Шеной | Следствие Найяра-Шеноя о пороге космологической дезориентации | Космолог, математический физик | Связал ошибку прыжка с космологическими масштабами и радиусом Хаббла | Первая эпоха; школа Найяра |
| 3 | Адити Шанкара | Теорема Шанкары о нулевом прыжке | Математик, специалист по сингулярностям | Доказала, что нулевой прыжок является предельным и физически патологическим случаем | Первая эпоха; формирование строгой математики ГПТ |
| 4 | Мадхава Кесаван | Принцип Мадхавы о гиперхронологической привязке | Физик-хронолог | Сформулировал временную модель гиперпрыжка и задал понятие гиперхронологической поверхности | Вторая эпоха; становление хронотеории ГПТ |
| 5 | Сринивас Айенгар | Теорема Айенгара о сопряженности навигационной и хронологической ошибки | Математик-статистик | Вывел закон связи между пространственной и временной ошибками выхода | Вторая эпоха; развитие хроностатистики ГПТ |
| 6 | Виджай Менон | Теорема Менона о недостижимости собственного прошлого | Физик-теоретик, специалист по причинности | Установил строгие ограничения, не позволяющие использовать ГПТ как полноценную машину времени | Вторая эпоха; хронологическая защита |
| 7 | Артур Брукс | Теорема Брукса-Аникеева о трансгиперной неидентифицируемости | Философ науки, логик | Поставил вопрос о недоказуемости идентичности субъекта на входе и на выходе | Философский кризис ГПТ |
| 8 | Дмитрий Аникеев | Теорема Брукса-Аникеева; Закон Риттера-Аникеева о входном остатке | Физик-экспериментатор | Связал философию тождества с наблюдаемой физикой входа и физикой остаточного излучения | Философский кризис + экспериментальная физика прожига |
| 9 | Иоганн Риттер | Принцип Риттера-Моханти; Принцип Субраманьяна-Риттера; Закон Риттера-Аникеева; Теорема Цзянь-Риттера | Инженер-физик, специалист по Вратам и физике прожига | Один из главных инженеров практической эпохи ГПТ; довел теорию до надежной транспортной технологии | Великая инженерная эпоха ГПТ |
| 10 | Раджеш Моханти | Принцип Риттера-Моханти о приводных Вратах | Инженер Врат | Разработал подход к использованию Врат как приводных маяков, повышающих точность выхода | Инженеризация Врат |
| 11 | Субраманьян Рао | Принцип Субраманьяна-Риттера; Принцип Гревса-Субраманьяна | Физик-синхронист | Разработал фазовую и временную фиксацию Врат, стабилизирующую окно выхода | Инженеризация Врат; школа фазовой стабилизации |
| 12 | Клаус Гревс | Принцип Гревса-Субраманьяна о допустимых зонах установки Врат | Инженер-сетевик, проектировщик ГПТ-инфраструктуры | Показал, что Врата нельзя строить где попало: гравитационная среда влияет на стабильность канала | Инженеризация Врат; инфраструктурная школа |
| 13 | Ян Ковальский | Теорема Ковальского-Хартмана о невозможности короткой вратной стяжки | Тополог ГПТ-сетей | Доказал бесперспективность чрезмерно плотной локальной сети Врат на малых дистанциях | Эпоха сетевой оптимизации |
| 14 | Эрих Хартман | Теорема Ковальского-Хартмана | Математик, специалист по топологии транспортных сетей | Соформализовал топологические ограничения вратных сетей и их масштабирования | Эпоха сетевой оптимизации |
| 15 | Сергей Карпов | Закон Фейнмана-Карпова об изодальном расходе гипертоплива | Инженер-энергетик, специалист по гипертопливу | Довел идею изодального расхода до инженерной формы и эксплуатационных расчетов | Промышленная стабилизация ГПТ |
| 16 | Марк Фейнман | Закон Фейнмана-Карпова об изодальном расходе гипертоплива | Теоретик физики гипертоплива | Предложил общую модель, согласно которой расход гипертоплива не зависит от длины прыжка | Промышленная стабилизация ГПТ; однофамилец Ричарда Фейнмана |
| 17 | Антон Лобач | Теорема Лобача-Кюна о непереносимости гипертоплива через канал прожига | Физик гипертоплива | Показал, что гипертопливо нельзя транспортировать через уже сформированный канал как обычный груз | Промышленная стабилизация; физика каналов |
| 18 | Мартин Кюн | Теорема Лобача-Кюна | Физик-теоретик, специалист по каналам прожига | Соформализовал ограничения переноса гипертоплива и устойчивости канала | Промышленная стабилизация; физика каналов |
| 19 | Ли Цзянь | Теорема Цзянь-Риттера о входной деструкции локального состояния | Физик высокоэнергетического входа | Строго доказал, что прожиг на входе - это не разрушение объекта по обычным физическим каналам, а распад его локальной допустимости как состояния. | Поздняя физика прожига; китайская школа диагностики входа |
| 20 | Чжао Вэньцзе | Синдром Чжао; медицина гиперпрыжка | Нейрофизиолог, врач гиперперехода | Первым системно описал психоневрологические и вегетативные последствия выхода из гиперпрыжка | Медицина массового гипертранспорта |
- Первая эпоха - фундаментальная теория: Найяр, Шеной, Шанкара.
- Вторая эпоха - хронотеория и причинность: Мадхава, Айенгар, Менон.
- Третья эпоха - философский кризис тождества: Брукс, Аникеев.
- Четвертая эпоха - инженеризация Врат: Риттер, Моханти, Субраманьян, Гревс.
- Пятая эпоха - сетевое и промышленное развитие: Ковальский, Хартман, Карпов, Фейнман, Лобач, Кюн, Цзянь.
- Шестая эпоха - медицина массового гипертранспорта: Чжао Вэньцзе.
ме
- Информация о материале
- Автор: Андрей
- Категория: Статейки
- Просмотров: 161
🛠 Главный файл шаблона для DWIN + UART5 + RS-485 + Modbus

😌 Не надо бояться Keil и TL51
У многих при виде Keil, T5L или TL51 сразу включается внутренний хтонический ужас, будто впереди не работа с микроконтроллером, а какое-то особо извращенное инженерное испытание. Я лично и сам мандражировал как девственница перед гэнг-бэнгом. На самом деле, все оказалось довольно просто. TL51 — это не магия, не инопланетная технология и не черный ящик. Это вполне обычный микроконтроллер, и, насколько можно судить по логике работы, речь идет о вполне понятной 8-битной архитектуре, со своими регистрами, UART, таймерами, прерываниями и прочими знакомыми вещами.
То есть подход здесь примерно такой же, как и в старых добрых проектах под AVR или ту же ATmega: настраиваем периферию, инициализируем порты, запускаем UART, крутим основной цикл, обрабатываем события. Никакой принципиально новой вселенной тут нет. Если человек уже работал с микроконтроллерами не только через яркие кнопочки Arduino IDE, а понимает, что такое регистры, биты, таймеры и обмен по последовательному интерфейсу, то и Keil, и TL51 он вполне осилит без мистических обрядов и кровавых жертвоприношений.
Да, местами тут есть свои странности: названия регистров могут быть непривычными, документация — мутной, а некоторые вещи приходится выяснять почти археологическими методами. Но это не делает платформу какой-то запредельно сложной. Порог входа здесь психологически завышен сильнее, чем технически. И если не страдать недугом «Arduino головного мозга», где любой шаг вне готовой библиотеки вызывает экзистенциальный кризис, то с этой системой можно работать совершенно нормально.
Поэтому правильный настрой в начале такой: перед нами обычный микроконтроллер с UART, таймерами и памятью, а не древнее проклятие инженеров DWIN. Нужно просто спокойно разобрать, что и где инициализируется, какие функции за что отвечают, и как связаны между собой экран, UART5, RS-485 и Modbus. А дальше всё становится гораздо менее страшным и гораздо более похожим на обычную прикладную разработку под железо.
/****************************************************************************** * Project : TEMPLATE * File : APP.c * Author : Andrey madmentat * Site : https://madmentat.ru * Year : 2026 * * Brief: * Главный файл шаблона приложения для DWIN/DGUS. * Содержит базовую прикладную логику проекта: * инициализацию UART5, RS-485, Modbus RTU и основной цикл обработки. * modbus rtu slave 8n1 115200 * Notes: * - Используется как стартовая точка для проектов с DWIN HMI * - Подходит для обмена данными по Modbus RTU Slave * - Может быть расширен пользовательской логикой страниц, VP и меню ******************************************************************************/ #include "APP.h" #include "SYSTEM.h" #include "UART.h" #include "TIMER.h" #include "modbus.h" #include <string.h> // ==================== ОСНОВНЫЕ НАСТРОЙКИ ==================== // Примеры часто используемых VP (можно добавлять свои) #define STATUS_VP 0x3000 // Статус/сообщение для STM32 #define CONTROL_VP 0x3002 // Команда от STM32 к DWIN #define VALUE_VP 0x3100 // Пример числового значения #define TEXT_VP 0x1005 // Пример текстового VP (26 символов) // ==================== ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ==================== xdata uint32_t SysTimCnt = 0; // Счётчик системного таймера (инкремент в ISR) xdata uint32_t Uptime = 0; // Время работы в секундах xdata uint32_t UptimeOld = 0; // Для сравнения (периодические задачи) xdata uint16_t CurrentPage = 0; // Текущая страница (для отслеживания смены) // ==================== ПРОТОТИПЫ ==================== static void WriteDgusByte(uint16_t vp, uint8_t offset, uint8_t value); static void ClearText(uint16_t vp); static void WriteText(uint16_t vp, const char* str); // ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ DGUS ==================== static void WriteDgusByte(uint16_t vp, uint8_t offset, uint8_t value) { uint16_t current_word = ReadDgus(vp + (offset / 2)); if (offset % 2 == 0) current_word = (current_word & 0x00FF) | ((uint16_t)value << 8); else current_word = (current_word & 0xFF00) | value; WriteDgus(vp + (offset / 2), current_word); } static void ClearText(uint16_t vp) { uint8_t i; WriteDgus(vp, 0xFFFF); // Очистка первого слова for (i = 0; i < 26; i++) // 26 символов — стандарт для DWIN { WriteDgusByte(vp, i, 0x20); // Пробел } } static void WriteText(uint16_t vp, const char* str) { uint8_t len = 0; uint8_t i; while (str[len] && len < 26) len++; ClearText(vp); for (i = 0; i < len; i++) { WriteDgusByte(vp, i, (uint8_t)str[i]); } } // ==================== ИНИЦИАЛИЗАЦИЯ ==================== void AppInit(void) { UartInit(); TimerInit(); ModbusInit(); SysTimCnt = 0; Uptime = 0; UptimeOld = 0; CurrentPage = 0; // Пример начальной инициализации VP WriteText(TEXT_VP, "READY"); WriteDgus(STATUS_VP, 0x0000); // Статус = 0 WriteDgus(CONTROL_VP, 0x0000); // Можно сразу перейти на главную страницу // PageChange(0); } // ==================== ОСНОВНОЙ ЦИКЛ ==================== void AppProcess(void) { uint16_t new_page; ModbusProcess(); // ← Обработка Modbus RTU Slave (самое важное!) new_page = GetPageID(); // Отслеживаем смену страницы (удобно для реакций) if (new_page != CurrentPage) { CurrentPage = new_page; // Здесь можно добавить действия при смене страницы } // Пример периодической задачи раз в секунду if (Uptime != UptimeOld) { UptimeOld = Uptime; // Можно обновлять STATUS_VP или делать heartbeat // WriteDgus(STATUS_VP, (uint16_t)Uptime); } // Если используете тач/клавиатуру — сюда можно добавить обработчик // ProcessTouch() или аналог UartProcess(); // Если нужно (обычно пустой или для отладки) }
Этот файл — главный прикладной файл шаблона. Именно здесь собирается базовая логика работы программы: инициализация UART, таймера, Modbus, работа с DGUS/DWIN-переменными и выполнение основного цикла.
Если говорить совсем по-простому, то этот файл отвечает за следующее:
- 📡 запускает связь по UART5;
- 🔁 готовит программу к работе по RS-485;
- 📘 подключает обработку Modbus RTU Slave;
- 🖥 умеет писать текст и значения в DWIN/DGUS;
- ⏱ выполняет периодические действия в основном цикле.
📂 Какие файлы подключаются
В начале файла подключаются заголовки:
#include "APP.h" #include "SYSTEM.h" #include "UART.h" #include "TIMER.h" #include "modbus.h" #include <string.h>
Что это значит:
- APP.h — заголовок самого прикладного модуля;
- SYSTEM.h — общие системные объявления;
- UART.h — работа с UART, в том числе UART5;
- TIMER.h — системный таймер;
- modbus.h — поддержка протокола Modbus;
- string.h — стандартные функции для строк.
💡 То есть этот файл сам по себе не делает всё в одиночку. Он скорее выступает как дирижёр, который вызывает нужные модули.
⚙ Основные настройки VP
#define STATUS_VP 0x3000 #define CONTROL_VP 0x3002 #define VALUE_VP 0x3100 #define TEXT_VP 0x1005
Это адреса переменных VP в памяти DWIN/DGUS.
- 📌 STATUS_VP — адрес для статуса или сообщения;
- 📌 CONTROL_VP — адрес для управляющей команды;
- 📌 VALUE_VP — адрес для числового значения;
- 📌 TEXT_VP — адрес текстового поля на экране.
Проще говоря, это ячейки экрана, в которые программа может записывать данные.
🧠 Глобальные переменные
xdata uint32_t SysTimCnt = 0; xdata uint32_t Uptime = 0; xdata uint32_t UptimeOld = 0; xdata uint16_t CurrentPage = 0;
Они нужны для хранения текущего состояния программы:
- ⏱ SysTimCnt — системный счётчик тиков таймера;
- ⌛ Uptime — время работы программы;
- 🔄 UptimeOld — предыдущее значение времени, чтобы понимать, прошла ли секунда;
- 📄 CurrentPage — текущая страница DWIN, чтобы отслеживать переключения.
То есть это не какие-то «магические числа», а обычные служебные переменные, нужные для контроля времени и экрана.
🖊 Вспомогательные функции для DGUS
В файле есть три важных служебных функции:
- WriteDgusByte()
- ClearText()
- WriteText()
1️⃣ WriteDgusByte()
Эта функция записывает один байт в текстовую область DWIN. Поскольку DGUS обычно работает словами по 16 бит, приходится:
- сначала прочитать текущее слово,
- поменять в нём только нужный байт,
- записать слово обратно.
🔧 Это нужно потому, что текст на DWIN часто хранится не как обычная C-строка, а как набор байтов внутри 16-битных слов.
2️⃣ ClearText()
Эта функция очищает текстовое поле. Сначала записывает специальное значение, а потом заполняет область пробелами.
🧹 Проще говоря: было что-то написано на экране — функция стирает это и делает поле пустым.
3️⃣ WriteText()
Эта функция выводит текст в заданный VP. Она:
- считает длину строки;
- очищает текстовое поле;
- записывает символы один за другим.
🖥 Благодаря этому можно делать, например, такие надписи:
WriteText(TEXT_VP, "READY");
То есть на экране появится слово READY.
🚀 Что делает AppInit()
Функция AppInit() — это начальная инициализация программы.
void AppInit(void)
{
UartInit();
TimerInit();
ModbusInit();
SysTimCnt = 0;
Uptime = 0;
UptimeOld = 0;
CurrentPage = 0;
WriteText(TEXT_VP, "READY");
WriteDgus(STATUS_VP, 0x0000);
WriteDgus(CONTROL_VP, 0x0000);
}
По шагам:
- 📡 UartInit() — запускает UART;
- ⏱ TimerInit() — запускает таймер;
- 📘 ModbusInit() — подготавливает Modbus;
- 🧹 обнуляет служебные переменные;
- 🖥 пишет на экран слово READY;
- 📌 сбрасывает значения статуса и управления.
То есть после включения устройство приводит всё в понятное начальное состояние.
✅ Иными словами: AppInit() — это «подготовить железку к жизни».
🔁 Что делает AppProcess()
Функция AppProcess() — это основной рабочий цикл программы. Она вызывается снова и снова, пока устройство включено.
void AppProcess(void)
{
uint16_t new_page;
ModbusProcess();
new_page = GetPageID();
if (new_page != CurrentPage)
{
CurrentPage = new_page;
}
if (Uptime != UptimeOld)
{
UptimeOld = Uptime;
}
UartProcess();
}
Разберём по-человечески.
1️⃣ ModbusProcess()
Это самая важная часть цикла. Здесь идёт обработка обмена по Modbus RTU Slave.
📡 Именно тут устройство:
- принимает запросы от мастера;
- разбирает пакеты;
- отвечает по RS-485;
- обновляет регистры и данные.
Если убрать или сломать эту часть, то Modbus просто работать не будет.
2️⃣ GetPageID()
Дальше программа узнаёт, какая сейчас открыта страница на DWIN.
Это полезно, если нужно делать разную логику для разных экранов. Например:
- на главной странице читать только основные параметры;
- в инженерном меню читать другой набор переменных;
- не опрашивать всё подряд без необходимости.
💡 Это как раз тот путь, который помогает уменьшить лишние задержки и нагрузку.
3️⃣ Контроль времени
Дальше идёт проверка:
if (Uptime != UptimeOld)
{
UptimeOld = Uptime;
}
Это значит: если прошла ещё одна секунда — можно выполнить какое-нибудь периодическое действие.
Например:
- 💓 послать heartbeat;
- 🧾 обновить статус;
- 🔍 проверить наличие связи;
- 🖥 вывести диагностическую информацию на экран.
4️⃣ UartProcess()
В конце вызывается UartProcess(). В зависимости от проекта там может быть:
- обработка буферов приёма и передачи;
- отладка;
- или вообще почти ничего, если всё уже делается в других местах.
📡 Как тут связаны UART5, RS-485 и Modbus
Сам этот файл не содержит всей низкоуровневой магии UART5 в явном виде, но он управляет общей логикой через вызовы:
- UartInit() — инициализация UART;
- UartProcess() — обработка UART;
- ModbusInit() — запуск Modbus;
- ModbusProcess() — работа протокола Modbus.
То есть реальная работа с UART5 и RS-485 обычно находится в модулях UART.c и modbus.c, а данный файл управляет этим на уровне приложения.
📘 Упрощённая схема такая:
Modbus master (например Modbus Poll)
↓
RS-485
↓
UART5
↓
modbus.c / UART.c
↓
APP.c
↓
переменные и экран DWIN
🖥 Зачем вообще нужен этот файл
Этот файл удобен как главный шаблон проекта, потому что в нём уже есть:
- ✅ базовая структура программы;
- ✅ инициализация всех нужных подсистем;
- ✅ работа с текстом на DWIN;
- ✅ основной цикл обработки;
- ✅ место для добавления своей логики.
То есть можно не начинать с полного нуля, а взять этот шаблон и постепенно наращивать функциональность.
🔍 Что в этом шаблоне пока простое, а что можно улучшить
Сейчас шаблон хороший как основа, но он ещё базовый.
Что уже есть:
- ✔ понятная инициализация;
- ✔ вывод текста в DWIN;
- ✔ вызов Modbus в основном цикле;
- ✔ отслеживание текущей страницы;
- ✔ заготовка для периодических действий.
Что можно развить дальше:
- 🔧 добавить реакцию на конкретные страницы;
- 🔧 опрашивать только нужные VP в зависимости от экрана;
- 🔧 сделать индикацию потери связи;
- 🔧 добавить heartbeat в Modbus или DGUS;
- 🔧 реализовать меню, кнопки, ввод пароля и т. д.
🧾 Итог простыми словами
Если совсем по-простому, то этот файл делает следующее:
- 🚀 запускает UART, таймер и Modbus;
- 🖥 подготавливает DWIN-экран;
- 🔁 в бесконечном цикле обрабатывает обмен по Modbus;
- 📄 следит за текущей страницей экрана;
- ⏱ позволяет выполнять действия раз в секунду или по событиям.
То есть это не «весь Modbus целиком», а главный управляющий файл приложения, в котором сходятся вместе: UART5, RS-485, DWIN/DGUS и Modbus RTU Slave.
💬 Пояснение для маленьких и тупых (все, как мы любим)
- 🤖 AppInit() — это «включили всё нужное»;
- 🔁 AppProcess() — это «крутимся по кругу и работаем»;
- 🖥 WriteText() — это «пишем текст на экран»;
- 📡 ModbusProcess() — это «слушаем мастера и отвечаем ему»;
- 📄 GetPageID() — это «смотрим, какой экран открыт».
Вот и все. Не магия. Просто аккуратно разложенная по функциям логика.
P.S. ⚠ Подготовка шаблона под DMG80480C043_01WTR
При подготовке этого шаблона к работе с конкретной панелью DMG80480C043_01WTR пришлось столкнуться с довольно типичной для таких устройств проблемой: исходный примерный проект был рассчитан под другую частоту микроконтроллера. А это значит, что настройки UART из чужого шаблона нельзя было просто взять и бездумно оставить как есть.
Проблема заключалась в том, что реальная частота моего микроконтроллера фактически была неизвестна. Документация на такие панели часто не даёт нормальной уверенности в подобных деталях, а если и даёт, то далеко не всегда понятно, насколько этим данным можно доверять на практике. В результате обычный путь вида «вписал стандартное значение и пошёл дальше» здесь не сработал.
Поэтому пришлось действовать более практичным, инженерным и местами слегка шаманским методом: была написана специальная тестовая программа, задача которой состояла в том, чтобы методом перебора подбирать нужное значение переменной i для функции static void Uart5Init(void) из файла UART.c.
Смысл теста был предельно простой:
- ⚙ программа выставляла очередное значение i;
- 📡 запускала UART5 с этими настройками;
- 🖥 отправляла текст через UART5 примерно раз в одну секунду;
- 👀 оставалось смотреть, в какой момент на экране появится нормально читаемый текст, а не мусор и абракадабра.
Как только на экране начал отображаться читаемый текст, стало ясно, что найденное значение i является близким к правильному, а значит, параметры инициализации UART5 наконец-то совпали с реальными условиями работы данной панели. Любопытно, что подходящим оказался не один-единственный параметр. В процессе перебора был найден довольно широкий рабочий диапазон — примерно около 16 значений, при которых текст уже становился читаемым. Это полезное наблюдение, потому что оно косвенно показывает: даже при неизвестной точной частоте МК можно эмпирически подобрать достаточно устойчивую настройку UART.
Иными словами, в данном случае настройка UART5 выполнялась не от красивой теории из документации, а от практического результата. Сначала подбиралось значение, при котором связь реально начинала работать, и только после этого шаблон можно было считать пригодным для дальнейшего использования.
✅ Именно таким способом и удалось определить оптимальное значение переменной i, которое затем было использовано в рабочем варианте шаблона.
Ну что, уже ручки чешутся посмотреть поближе, о чем речь? Ну вот мы и добрались до этого момента!
- Информация о материале
- Автор: Андрей
- Категория: Статейки
- Просмотров: 224
🔐 Автообновление SSL: Certbot + Telegram + systemd timer
Это заметка-памятка: как я сделал обновление сертификатов Let’s Encrypt автоматическим, предсказуемым и наблюдаемым. Всё завязано на один bash-скрипт и нормальную демонизацию через systemd.
fullchain.pem/privkey.pem. Это лечится правильными путями в конфиге и reload/restart.🎯 Задача
- Обновлять SSL-сертификаты Let’s Encrypt (Certbot) для нескольких доменов.
- Работать без ручного вмешательства и «зависаний».
- Присылать в Telegram не спам, а короткий отчёт (2 сообщения).
- Запускаться автоматически 2 раза в месяц в боевом режиме:
DRY_RUN=0.
🧩 Почему certbot standalone
В режиме --standalone certbot поднимает временный HTTP-сервер на 80 порту для прохождения HTTP-01 challenge. Поэтому на время обновления нужно освободить порт. Проще всего — кратко остановить nginx.
➕ Плюсы
- Минимум зависимости от конфигов nginx (
webrootне нужен). - Меньше «хрупких мест» и ручных условий.
➖ Минусы
- Нужно освобождать 80 порт (стоп/старт nginx).
- Короткий простой веба возможен (обычно секунды).
📦 Группировка доменов по сертификатам
Доменов много, но certbot удобно запускать группами: 1 строка = 1 сертификат. Это удобно и для диагностики, и для отчёта.
ard-s.ru www.ard-s.ruasterisk.ard-s.ru www.asterisk.ard-s.ruficus.ard-s.ru www.ficus.ard-s.ruhypervisor.ard-s.ru www.hypervisor.ard-s.rumail.ard-s.rumikrotik.ard-s.ru www.mikrotik.ard-s.rupacs.ard-s.ru www.pacs.ard-s.rusecure.ard-s.rutest.ard-s.ru www.test.ard-s.ru
🧠 Как работает скрипт
1) Строгий режим bash
set -e,set -u,set -o pipefail
Идея простая: лучше громко упасть, чем тихо сделать ерунду.
2) Логи
Скрипт пишет в /var/log/certbot-update.log (плюс systemd-журнал).
3) Telegram-уведомления
- 🟡 старт
- 🟢/🟠 итоговый отчёт (сколько успешно, сколько с ошибкой, статус nginx)
printf) и curl --data-urlencode — иначе Telegram может показать «\\n» вместо новой строки.4) Критичный момент: set -e и $(...)
Если запускать certbot внутри подстановки $(...) при включённом set -e, bash может завершить скрипт раньше, чем мы успеем обработать код возврата. Поэтому в месте запуска certbot временно отключается set -e, а затем включается обратно.
5) Неприятная, но типовая проблема: nginx отдаёт старый сертификат
Certbot может обновить сертификат в /etc/letsencrypt/live/..., но если в nginx прописаны неверные пути к ssl_certificate/ssl_certificate_key, клиент продолжит видеть старый сертификат.
live/.../fullchain.pem и live/.../privkey.pem и сделать reload / restart nginx.📄 Листинг скрипта
nano /usr/local/bin/update_certificates.sh
#!/bin/bash # Обновление SSL-сертификатов certbot (standalone) для набора доменов. # Telegram: РОВНО 2 сообщения (старт + финальный отчет). # Логи: /var/log/certbot-update.log # # Запуск: # DRY_RUN=1 bash /usr/local/bin/update_certificates.sh # тестовый прогон # bash /usr/local/bin/update_certificates.sh # реальный прогон (DRY_RUN=0) set -euo pipefail LOG_FILE="/var/log/certbot-update.log" # --- Telegram --- # ВАЖНО: вставь СВЕЖИЙ токен (старый уже светился) BOT_TOKEN="ВСТАВЬ_НОВЫЙ_ТОКЕН_СЮДА" CHAT_ID="-1003310217653" # --- Наборы доменов (1 сертификат на строку) --- DOMAINS=( "ard-s.ru www.ard-s.ru" "asterisk.ard-s.ru www.asterisk.ard-s.ru" "ficus.ard-s.ru www.ficus.ard-s.ru" "hypervisor.ard-s.ru www.hypervisor.ard-s.ru" "mail.ard-s.ru" "mikrotik.ard-s.ru www.mikrotik.ard-s.ru" "pacs.ard-s.ru www.pacs.ard-s.ru" # фикс опечатки "secure.ard-s.ru" "test.ard-s.ru www.test.ard-s.ru" ) # 0 = реальный прогон, 1 = certbot --dry-run DRY_RUN="${DRY_RUN:-0}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S %z')] $*" | tee -a "$LOG_FILE" } tg_send() { local action="$1" local status="$2" local message="${3:-}" local details="${4:-}" local emoji="⚪" local status_text="$status" case "$status" in start) emoji="🟡"; status_text="НАЧАЛО ОБНОВЛЕНИЯ" ;; success) emoji="🟢"; status_text="УСПЕШНО" ;; warning) emoji="🟠"; status_text="С ПРЕДУПРЕЖДЕНИЯМИ" ;; error) emoji="🔴"; status_text="ОШИБКА" ;; info) emoji="ℹ️"; status_text="ИНФОРМАЦИЯ" ;; esac export TZ='Europe/Moscow' local current_time current_time=$(date '+%d.%m.%Y %H:%M:%S') local hostname hostname=$(hostname) # Собираем текст с РЕАЛЬНЫМИ переводами строк local text text=$( printf "🔐 <b>Обновление SSL-сертификатов</b>\n\n" printf "%s <b>Статус:</b> %s\n" "$emoji" "$status_text" printf "📅 <b>Дата и время:</b> %s\n" "$current_time" printf "📝 <b>Действие:</b> %s\n" "$action" if [[ -n "$message" ]]; then printf "\n💬 <b>Сообщение:</b>\n<code>%s</code>\n" "$message" fi if [[ -n "$details" ]]; then # Ограничим размер, чтобы Telegram не отверг сообщение if (( ${#details} > 3000 )); then details="${details:0:2997}..." fi printf "\n📋 <b>Детали:</b>\n<pre>%s</pre>\n" "$details" fi printf "\n🖥 <b>Сервер:</b> %s\n" "$hostname" ) # Если токен не задан — не валим скрипт, но логируем if [[ -z "${BOT_TOKEN}" || "${BOT_TOKEN}" == "ВСТАВЬ_НОВЫЙ_ТОКЕН_СЮДА" ]]; then log "⚠️ BOT_TOKEN не задан/шаблонный — Telegram уведомление не отправлено." return 0 fi local url="https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" if command -v curl >/dev/null 2>&1; then curl -sS -X POST "$url" \ --connect-timeout 10 --max-time 20 \ --data-urlencode "chat_id=${CHAT_ID}" \ --data-urlencode "parse_mode=HTML" \ --data-urlencode "disable_web_page_preview=true" \ --data-urlencode "text=${text}" \ >/dev/null 2>&1 || log "⚠️ Не удалось отправить уведомление в Telegram (curl)." elif command -v wget >/dev/null 2>&1; then # У wget нет удобного urlencode — поэтому используем curl предпочтительно. # Но чтобы хоть как-то работало, отправим как есть (может сломаться на спецсимволах). wget -q --timeout=20 --tries=2 \ --post-data="chat_id=${CHAT_ID}&parse_mode=HTML&disable_web_page_preview=true&text=$(printf '%s' "$text")" \ "$url" -O /dev/null 2>&1 || log "⚠️ Не удалось отправить уведомление в Telegram (wget)." else log "⚠️ Нет curl/wget — Telegram уведомление невозможно." fi } build_d_args() { local domain_set="$1" local args=() local d for d in $domain_set; do args+=("-d" "$d") done printf '%q ' "${args[@]}" } # --- Управление nginx и trap --- nginx_was_running=0 nginx_started_by_script=0 restore_nginx() { # Поднимем nginx только если он был запущен до старта и мы его останавливали if [[ "$nginx_was_running" -eq 1 && "$nginx_started_by_script" -eq 0 ]]; then log "🚀 Возврат nginx (trap)..." systemctl start nginx >/dev/null 2>&1 || true fi } trap restore_nginx EXIT # --- Start --- JOB_START_EPOCH=$(date +%s) log "=== START ===" if systemctl is-active --quiet nginx; then nginx_was_running=1 else nginx_was_running=0 fi tg_send \ "Запуск автоматического обновления SSL-сертификатов" \ "start" \ "Наборов доменов: ${#DOMAINS[@]}. DRY_RUN=${DRY_RUN}" if [[ "$nginx_was_running" -eq 1 ]]; then log "🛑 Остановка nginx..." systemctl stop nginx else log "ℹ️ nginx уже остановлен — stop пропущен." fi # Проверка порта 80 — standalone должен поднять временный сервер if ss -lntp 2>/dev/null | grep -qE ':(80)\s'; then local80="$(ss -lntp 2>/dev/null | grep -E ':(80)\s' || true)" log "❌ Порт 80 занят. certbot --standalone не сможет стартовать." log "$local80" tg_send \ "Проверка перед certbot" \ "error" \ "Порт 80 занят — standalone не стартует" \ "$local80" exit 1 fi SUCCESSFUL="" FAILED="" TOTAL=${#DOMAINS[@]} PROCESSED=0 for domain_set in "${DOMAINS[@]}"; do PROCESSED=$((PROCESSED + 1)) log "🔄 [$PROCESSED/$TOTAL] certbot для: $domain_set" start_time=$(date +%s) # Ключевой фикс против преждевременного выхода из-за set -e внутри $(...) set +e if [[ "$DRY_RUN" == "1" ]]; then output=$(certbot certonly --standalone --dry-run \ $(build_d_args "$domain_set") \ --non-interactive --agree-tos --expand 2>&1) else output=$(certbot certonly --standalone \ $(build_d_args "$domain_set") \ --non-interactive --agree-tos --expand 2>&1) fi exit_code=$? set -e end_time=$(date +%s) duration=$((end_time - start_time)) if [[ $exit_code -eq 0 ]]; then log "✅ OK: $domain_set (${duration}s)" SUCCESSFUL+=$'\n'"$domain_set" else log "❌ FAIL: $domain_set (${duration}s)" log " certbot (первые 30 строк):" echo "$output" | head -n 30 | sed 's/^/ /' | tee -a "$LOG_FILE" >/dev/null FAILED+=$'\n'"$domain_set" fi log "---" sleep 2 done # Поднимаем nginx обратно, если он был запущен до старта NGINX_STATUS="⚪ nginx неизвестно" if [[ "$nginx_was_running" -eq 1 ]]; then log "🚀 Запуск nginx..." systemctl start nginx nginx_started_by_script=1 if systemctl is-active --quiet nginx; then log "✅ nginx запущен" NGINX_STATUS="🟢 nginx запущен" else log "❌ nginx не запустился!" NGINX_STATUS="🔴 nginx не запущен" fi else NGINX_STATUS="ℹ️ nginx был остановлен до старта" fi # Короткая выжимка certbot certificates CERT_INFO_SNIP="" set +e CERT_INFO_FULL=$(certbot certificates 2>/dev/null) set -e if [[ -n "${CERT_INFO_FULL:-}" ]]; then CERT_INFO_SNIP=$(echo "$CERT_INFO_FULL" | grep -E 'Certificate Name:|Domains:|Expiry Date:' | head -n 60) fi # Счётчики по наборам (а не по словам) ok_sets=0 bad_sets=0 if [[ -n "$SUCCESSFUL" ]]; then ok_sets=$(echo "$SUCCESSFUL" | sed '/^\s*$/d' | wc -l) fi if [[ -n "$FAILED" ]]; then bad_sets=$(echo "$FAILED" | sed '/^\s*$/d' | wc -l) fi end_epoch=$(date +%s) total_duration=$((end_epoch - JOB_START_EPOCH)) # Финальный отчёт — 2-е сообщение details=$( printf "⏱ Длительность: %s сек\n" "$total_duration" printf "%s\n" "$NGINX_STATUS" printf "DRY_RUN=%s\n\n" "$DRY_RUN" printf "📦 Наборов доменов: %s\n" "$TOTAL" printf "✅ Успешно: %s\n" "$ok_sets" printf "❌ Ошибки: %s\n\n" "$bad_sets" if [[ -n "$SUCCESSFUL" ]]; then printf "✅ Успешно:\n" echo "$SUCCESSFUL" | sed '/^\s*$/d' | sed 's/^/ • /' printf "\n" fi if [[ -n "$FAILED" ]]; then printf "❌ С ошибками:\n" echo "$FAILED" | sed '/^\s*$/d' | sed 's/^/ • /' printf "\n⚠️ Проверь: DNS A/AAAA, порт 80, firewall, и лог certbot.\n" fi if [[ -n "$CERT_INFO_SNIP" ]]; then printf "\ncertbot certificates (кратко):\n%s\n" "$CERT_INFO_SNIP" fi ) if [[ "$bad_sets" -eq 0 ]]; then tg_send \ "Завершение обновления SSL-сертификатов" \ "success" \ "Обновление завершено успешно" \ "$details" else tg_send \ "Завершение обновления SSL-сертификатов" \ "warning" \ "Обновление завершено с ошибками" \ "$details" fi log "=== DONE ===" # Код выхода if [[ "$bad_sets" -gt 0 ]]; then exit 1 else exit 0 fi
🧿 Демонизация: systemd service + timer
Чтобы не запускать руками, используется пара unit-файлов: certbot-update.service и certbot-update.timer.
systemd service
Type=oneshot— задача разовая, не висящий демон.Environment=DRY_RUN=0— принудительно боевой режим.ExecStart=/usr/local/bin/update_certificates.sh— запуск скрипта.
systemd timer
- Запуск 1-го и 15-го числа (ночью).
Persistent=true— догоняет пропущенные запуски.RandomizedDelaySec— при желании добавляет небольшой рандом.
📄 Листинг юнита systemd
nano /etc/systemd/system/certbot-update.service
GNU nano 8.4 /etc/systemd/system/certbot-update.service * [Unit] Description=Update SSL certificates (certbot standalone) + Telegram report Wants=network-online.target After=network-online.target [Service] Type=oneshot # Важно: принудительно боевой режим Environment=DRY_RUN=0 # Скрипт должен быть исполняемым ExecStart=/usr/local/bin/update_certificates.sh # На всякий случай: не зависать бесконечно TimeoutStartSec=30min # Логи будут в journald + твой /var/log/certbot-update.log # (скрипт сам пишет в /var/log/certbot-update.log)
📄 Таймер
nano /etc/systemd/system/certbot-update.timer
GNU nano 8.4 /etc/systemd/system/certbot-update.timer * [Unit] Description=Run certbot-update twice a month [Timer] OnCalendar=*-*-01 04:20:00 OnCalendar=*-*-15 04:20:00 # Если сервер был выключен в момент запуска — догонит после включения Persistent=true # Небольшой рандом, чтобы не долбить LE ровно в одно время (полезно, но не обязательно) RandomizedDelaySec=10min Unit=certbot-update.service [Install] WantedBy=timers.target
Активация и проверка:
# Перезагружаем systemd для загрузки новых юнитов
sudo systemctl daemon-reload
# Включаем и запускаем таймер
sudo systemctl enable --now certbot-update.timer
# Проверяем статус таймера
sudo systemctl status certbot-update.timer
# Смотрим список предстоящих запусков
sudo systemctl list-timers --all | grep certbot-update
# Проверяем лог последнего запуска
sudo journalctl -u certbot-update.service -n 50
✅ Итог
- Сертификаты обновляются автоматически.
- nginx корректно останавливается и поднимается обратно.
- Telegram даёт понятный отчёт: старт и итог.
- systemd timer делает процесс регулярным и незаметным.
🔔 P.S
- Говорят, у nginx есть какой-то свой механизм мягкого обновления сертификатов, который работает значительно быстрее. Но я, пожалуй, не буду вносить изменения в скрипт, пока жаренный петух не клюнет в жопу. Работает - и так сойдет!
- Информация о материале
- Автор: Андрей
- Категория: Статейки
- Просмотров: 185
STM32 - MODBUS RTU MASTER 8N1 115200 / DWIN;
Пример рабочий и спокойненько коннектится с DWIN, но тут есть одна подъебка, которая мешает принимать ответы и про которую я помню, но никому не скажу. Наверно, опытный программист и сам поймет, а неопытный пусть пострадает, потому что в таких муках зарождается ум.
#include "main.h" #include <string.h> #include <stdint.h> /* ===================== Конфигурация ===================== */ #define SYSCLK_HZ 8000000UL #define BAUDRATE 115200UL #define TIMEOUT_MS 100U #define IWDG_TIMEOUT_MS 2000U #define FLASH_STATE_ADDR 0x08007C00UL #define FLASH_MAGIC_VALUE 0xDEADBEEFUL /* RS485 direction pin */ #define RS485_CONTROL_PIN GPIO_PIN_1 #define RS485_CONTROL_PORT GPIOF /* DWIN / Modbus */ #define MODBUS_SLAVE_ID 1U #define DWIN_VP_READ 0x1001U #define DWIN_VP_WRITE 0x1500U #define MODBUS_POLL_MS 10U #define READ_QTY 10U /* Светодиоды */ #define LED_COUNT 6 static const uint16_t led_pins[LED_COUNT] = { GPIO_PIN_15, GPIO_PIN_12, GPIO_PIN_11, GPIO_PIN_10, GPIO_PIN_9, GPIO_PIN_8 }; /* ===================== Глобальные переменные ===================== */ volatile uint32_t timer_overflows = 0; volatile uint32_t WatchdogTIMER_1 = 0; volatile uint32_t WatchdogTIMER_2 = 1000; typedef struct { uint8_t led_states[LED_COUNT]; uint32_t reset_count; uint32_t magic; uint32_t checksum; } SystemState; SystemState system_state = {0}; /* ===================== Прототипы ===================== */ void SystemClock_Config(void); void MX_GPIO_Init(void); void UART_Init(void); void RS485_Init(void); void RS485_SetTransmitMode(void); void RS485_SetReceiveMode(void); void Timer_Init(void); void WatchdogTimer_Init(void); void IWDG_Init(void); void Delay_us(uint32_t us); uint32_t millis(void); void CheckResetCause(uint8_t *is_iwdg_reset); void LoadSystemState(void); void SaveSystemState(void); uint32_t CalculateChecksum(SystemState *state); void ApplyLedStates(void); void UART_ClearRx(void); void UART_SendBytes(const uint8_t *data, uint16_t len); uint8_t UART_ReceiveExact(uint8_t *buf, uint16_t len, uint32_t timeout_ms); uint16_t Modbus_CRC16(const uint8_t *data, uint16_t len); uint8_t Modbus_ReadHoldingRegisters(uint8_t slave_id, uint16_t start_addr, uint16_t quantity, uint16_t *dest); uint8_t Modbus_WriteSingleRegister(uint8_t slave_id, uint16_t reg_addr, uint16_t value); void Error_Handler(void); /* ===================== Задержка ===================== */ void Delay_us(uint32_t us) { uint32_t cycles = (SYSCLK_HZ / 1000000UL) * us; while (cycles--) { __NOP(); } } /* ===================== millis ===================== */ uint32_t millis(void) { uint32_t overflows; uint32_t cnt; __disable_irq(); overflows = timer_overflows; cnt = TIM3->CNT; __enable_irq(); return (overflows * 1000UL) + cnt; } /* ===================== RS485 ===================== */ void RS485_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOF_CLK_ENABLE(); GPIO_InitStruct.Pin = RS485_CONTROL_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(RS485_CONTROL_PORT, &GPIO_InitStruct); RS485_SetReceiveMode(); } void RS485_SetTransmitMode(void) { HAL_GPIO_WritePin(RS485_CONTROL_PORT, RS485_CONTROL_PIN, GPIO_PIN_SET); Delay_us(50); } void RS485_SetReceiveMode(void) { HAL_GPIO_WritePin(RS485_CONTROL_PORT, RS485_CONTROL_PIN, GPIO_PIN_RESET); Delay_us(50); } /* ===================== UART ===================== */ void UART_Init(void) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA2 = TX, PA3 = RX, AF1 */ GPIOA->MODER &= ~(GPIO_MODER_MODER2_Msk | GPIO_MODER_MODER3_Msk); GPIOA->MODER |= (GPIO_MODER_MODER2_1 | GPIO_MODER_MODER3_1); GPIOA->AFR[0] &= ~((0xFUL << GPIO_AFRL_AFSEL2_Pos) | (0xFUL << GPIO_AFRL_AFSEL3_Pos)); GPIOA->AFR[0] |= ((0x1UL << GPIO_AFRL_AFSEL2_Pos) | (0x1UL << GPIO_AFRL_AFSEL3_Pos)); USART1->CR1 = 0; USART1->CR2 = 0; USART1->CR3 = 0; /* 115200, 8N1 */ USART1->BRR = (SYSCLK_HZ + (BAUDRATE / 2U)) / BAUDRATE; USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; } void UART_ClearRx(void) { while (USART1->ISR & USART_ISR_RXNE) { volatile uint8_t dummy = (uint8_t)USART1->RDR; (void)dummy; } if (USART1->ISR & (USART_ISR_ORE | USART_ISR_PE | USART_ISR_FE | USART_ISR_NE)) { USART1->ICR = USART_ICR_ORECF | USART_ICR_PECF | USART_ICR_FECF | USART_ICR_NCF; } } void UART_SendBytes(const uint8_t *data, uint16_t len) { RS485_SetTransmitMode(); for (uint16_t i = 0; i < len; i++) { while ((USART1->ISR & USART_ISR_TXE) == 0U) { } USART1->TDR = data[i]; } while ((USART1->ISR & USART_ISR_TC) == 0U) { } Delay_us(50); RS485_SetReceiveMode(); } uint8_t UART_ReceiveExact(uint8_t *buf, uint16_t len, uint32_t timeout_ms) { uint32_t start = millis(); uint16_t index = 0; while (index < len) { if (USART1->ISR & USART_ISR_RXNE) { buf[index++] = (uint8_t)USART1->RDR; start = millis(); } if (USART1->ISR & (USART_ISR_ORE | USART_ISR_PE | USART_ISR_FE | USART_ISR_NE)) { USART1->ICR = USART_ICR_ORECF | USART_ICR_PECF | USART_ICR_FECF | USART_ICR_NCF; return 0; } if ((millis() - start) >= timeout_ms) { return 0; } } return 1; } /* ===================== Timer TIM3 для millis ===================== */ void Timer_Init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); TIM3->PSC = (SYSCLK_HZ / 1000UL) - 1UL; TIM3->ARR = 1000UL - 1UL; TIM3->CNT = 0; TIM3->DIER |= TIM_DIER_UIE; NVIC_SetPriority(TIM3_IRQn, 1); NVIC_EnableIRQ(TIM3_IRQn); TIM3->CR1 |= TIM_CR1_CEN; } /* ===================== Timer TIM14 для watchdog ===================== */ void WatchdogTimer_Init(void) { __HAL_RCC_TIM14_CLK_ENABLE(); TIM14->PSC = (SYSCLK_HZ / 1000UL) - 1UL; TIM14->ARR = 1UL; TIM14->CNT = 0; TIM14->DIER |= TIM_DIER_UIE; NVIC_SetPriority(TIM14_IRQn, 2); NVIC_EnableIRQ(TIM14_IRQn); TIM14->CR1 |= TIM_CR1_CEN; } void TIM14_IRQHandler(void) { if (TIM14->SR & TIM_SR_UIF) { TIM14->SR &= ~TIM_SR_UIF; WatchdogTIMER_1++; } } void TIM3_IRQHandler(void) { if (TIM3->SR & TIM_SR_UIF) { TIM3->SR &= ~TIM_SR_UIF; timer_overflows++; } } /* ===================== IWDG ===================== */ void IWDG_Init(void) { __HAL_RCC_LSI_ENABLE(); while ((RCC->CSR & RCC_CSR_LSIRDY) == 0U) { } IWDG->KR = 0x5555; IWDG->PR = 0x04; IWDG->RLR = 2500U; IWDG->KR = 0xAAAA; IWDG->KR = 0xCCCC; } /* ===================== Flash state ===================== */ uint32_t CalculateChecksum(SystemState *state) { uint32_t sum = 0; for (int i = 0; i < LED_COUNT; i++) { sum += state->led_states[i]; } sum += state->reset_count; return sum; } void LoadSystemState(void) { SystemState *flash_state = (SystemState *)FLASH_STATE_ADDR; if ((flash_state->magic == FLASH_MAGIC_VALUE) && (flash_state->checksum == CalculateChecksum(flash_state))) { memcpy(&system_state, flash_state, sizeof(SystemState)); } else { memset(system_state.led_states, 0, sizeof(system_state.led_states)); system_state.reset_count = 0; system_state.magic = FLASH_MAGIC_VALUE; system_state.checksum = CalculateChecksum(&system_state); SaveSystemState(); } } void SaveSystemState(void) { HAL_StatusTypeDef status; FLASH_EraseInitTypeDef erase_init = {0}; uint32_t page_error = 0; uint32_t *state_ptr = (uint32_t *)&system_state; system_state.checksum = CalculateChecksum(&system_state); system_state.magic = FLASH_MAGIC_VALUE; HAL_FLASH_Unlock(); erase_init.TypeErase = FLASH_TYPEERASE_PAGES; erase_init.PageAddress = FLASH_STATE_ADDR; erase_init.NbPages = 1; status = HAL_FLASHEx_Erase(&erase_init, &page_error); if (status != HAL_OK) { HAL_FLASH_Lock(); return; } for (uint32_t i = 0; i < (sizeof(SystemState) / 4U); i++) { status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_STATE_ADDR + (i * 4U), state_ptr[i]); if (status != HAL_OK) { break; } } HAL_FLASH_Lock(); } /* ===================== LED ===================== */ void ApplyLedStates(void) { for (int i = 0; i < LED_COUNT; i++) { HAL_GPIO_WritePin(GPIOA, led_pins[i], system_state.led_states[i] ? GPIO_PIN_SET : GPIO_PIN_RESET); } } /* ===================== Reset cause ===================== */ void CheckResetCause(uint8_t *is_iwdg_reset) { uint32_t reset_flags = RCC->CSR; *is_iwdg_reset = (reset_flags & RCC_CSR_IWDGRSTF) ? 1U : 0U; RCC->CSR |= RCC_CSR_RMVF; } /* ===================== GPIO ===================== */ void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_15, GPIO_PIN_RESET); } /* ===================== Clock ===================== */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) { Error_Handler(); } } /* ===================== Modbus CRC ===================== */ uint16_t Modbus_CRC16(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFFU; for (uint16_t pos = 0; pos < len; pos++) { crc ^= (uint16_t)data[pos]; for (uint8_t i = 0; i < 8; i++) { if (crc & 0x0001U) { crc >>= 1; crc ^= 0xA001U; } else { crc >>= 1; } } } return crc; } /* ===================== Modbus RTU Master ===================== */ uint8_t Modbus_ReadHoldingRegisters(uint8_t slave_id, uint16_t start_addr, uint16_t quantity, uint16_t *dest) { uint8_t tx[8]; uint8_t rx[64]; uint16_t crc; uint16_t expected_len; if ((quantity == 0U) || (quantity > 29U) || (dest == NULL)) { return 0; } tx[0] = slave_id; tx[1] = 0x03; tx[2] = (uint8_t)(start_addr >> 8); tx[3] = (uint8_t)(start_addr & 0xFF); tx[4] = (uint8_t)(quantity >> 8); tx[5] = (uint8_t)(quantity & 0xFF); crc = Modbus_CRC16(tx, 6); tx[6] = (uint8_t)(crc & 0xFF); tx[7] = (uint8_t)((crc >> 8) & 0xFF); UART_ClearRx(); UART_SendBytes(tx, 8); expected_len = (uint16_t)(5U + (2U * quantity)); if (!UART_ReceiveExact(rx, expected_len, TIMEOUT_MS)) { return 0; } if (rx[0] != slave_id) return 0; if (rx[1] & 0x80U) return 0; if (rx[1] != 0x03U) return 0; if (rx[2] != (uint8_t)(quantity * 2U)) return 0; crc = Modbus_CRC16(rx, expected_len - 2U); if ((rx[expected_len - 2U] != (uint8_t)(crc & 0xFF)) || (rx[expected_len - 1U] != (uint8_t)((crc >> 8) & 0xFF))) { return 0; } for (uint16_t i = 0; i < quantity; i++) { dest[i] = ((uint16_t)rx[3U + (2U * i)] << 8) | (uint16_t)rx[4U + (2U * i)]; } return 1; } uint8_t Modbus_WriteSingleRegister(uint8_t slave_id, uint16_t reg_addr, uint16_t value) { uint8_t tx[8]; uint8_t rx[8]; uint16_t crc; tx[0] = slave_id; tx[1] = 0x06; tx[2] = (uint8_t)(reg_addr >> 8); tx[3] = (uint8_t)(reg_addr & 0xFF); tx[4] = (uint8_t)(value >> 8); tx[5] = (uint8_t)(value & 0xFF); crc = Modbus_CRC16(tx, 6); tx[6] = (uint8_t)(crc & 0xFF); tx[7] = (uint8_t)((crc >> 8) & 0xFF); UART_ClearRx(); UART_SendBytes(tx, 8); if (!UART_ReceiveExact(rx, 8, TIMEOUT_MS)) { return 0; } if (memcmp(tx, rx, 8) != 0) { return 0; } return 1; } /* ===================== main ===================== */ int main(void) { uint8_t is_iwdg_reset = 0; uint32_t last_poll_time = 0; uint16_t src_value = 0; uint8_t heartbeat = 0; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); RS485_Init(); UART_Init(); Timer_Init(); WatchdogTimer_Init(); IWDG_Init(); CheckResetCause(&is_iwdg_reset); LoadSystemState(); system_state.reset_count++; if (!is_iwdg_reset) { memset(system_state.led_states, 0, sizeof(system_state.led_states)); } ApplyLedStates(); SaveSystemState(); while (1) { if (WatchdogTIMER_1 >= WatchdogTIMER_2) { IWDG->KR = 0xAAAA; WatchdogTIMER_1 = 0; } if ((millis() - last_poll_time) >= MODBUS_POLL_MS) { last_poll_time = millis(); if (Modbus_ReadHoldingRegisters(MODBUS_SLAVE_ID, DWIN_VP_READ, 1, &src_value)) { if (Modbus_WriteSingleRegister(MODBUS_SLAVE_ID, DWIN_VP_WRITE, src_value)) { heartbeat ^= 1U; system_state.led_states[0] = heartbeat; system_state.led_states[1] = 1; system_state.led_states[2] = 0; } else { system_state.led_states[1] = 0; system_state.led_states[2] = 1; } } else { system_state.led_states[1] = 0; system_state.led_states[2] = 1; } ApplyLedStates(); } } } /* ===================== Error ===================== */ void Error_Handler(void) { __disable_irq(); while (1) { } } #ifdef USE_FULL_ASSERT void assert_failed(uint8_t *file, uint32_t line) { (void)file; (void)line; } #endif
- Информация о материале
- Автор: Андрей
- Категория: Статейки
- Просмотров: 238
⚡ madLENotify: уведомления об отключениях Ленэнерго в Telegram
madLENotify — маленький самописный демон на C++17, который периодически проверяет страницу плановых работ 🌐 и отправляет уведомления в Telegram 📨 при появлении новых отключений в выбранном населённом пункте.
✅ Что умеет:
• 📅 парсит плановые отключения (дата/время/адрес/комментарий)
• 🔔 фильтрует по окну уведомлений (например, за 3 дня вперёд)
• 🕘 отправляет сообщения после заданного времени (send_time)
• 🧠 не спамит: ведёт журнал отправок (sent_log.json)
• 🌐 имеет простой HTTP API для просмотра/обновления конфига (по желанию)
• 🧾 умеет прислать историю прошедших отключений (history)
🧩 Как это работает
- Демон раз в
check_interval_secсекунд качает страницу planned_work. - Парсит HTML-таблицу и выделяет события отключений.
- Фильтрует события по посёлку/городу (settlement) и по диапазону дней (alert_days).
- Если наступило время отправки (
send_time), то формирует сообщение по шаблону и шлёт в Telegram. - Факт отправки фиксируется в
sent_log.json, чтобы не дублить одно и то же.
🛡️ Про лимит daily_limit
Параметр daily_limit ограничивает количество сообщений в сутки на каждое отдельное событие, а не “одно на всё”. То есть если в один день появилось два разных отключения — можно получить два уведомления даже при daily_limit = 1. Это важно, когда Ленэнерго публикует несколько объявлений на разные интервалы времени.
📦 Установка и сборка
🧰 Зависимости
- 🐧 Linux сервер (обычно Debian/Ubuntu).
- 🧱 Компилятор
g++с поддержкой C++17. - 🌐 libcurl (
-lcurl). - 🧵 pthread (
-lpthread). - 📄
json.hpp(nlohmann/json) иhttplib.h(если используешь HTTP API).
🔨 Сборка
Пример команды сборки:
g++ -o lenotify lenotify.cpp -std=c++17 -O2 -lcurl -lpthread -I.После сборки рядом появится бинарник lenotify. Его можно запускать вручную, либо оформить как systemd-сервис.
⚙️ Настройка: settings.json
Конфигурация хранится в JSON. Основные поля:
- 📍
settlement— населённый пункт для фильтра (например, “Пески”). - 🔗
url_base— базовый URL planned_work. - 🔔
alert_days— за сколько дней вперёд предупреждать. - 🕘
send_time— время (МСК), после которого можно отправлять уведомления. - ⏱
check_interval_sec— период проверки (в секундах). - 🚦
daily_limit— лимит сообщений в сутки по каждому событию. - 💬
default_message— шаблон сообщения с плейсхолдерами. - 📨 Telegram:
telegram_bot_token,telegram_api_url,chat_ids. - 🌐
api_port— порт локального HTTP API (если нужно).
🧾 Листинг конфигурации:
{ "settlement": "Пески", "url_base": "https://rosseti-lenenergo.ru/planned_work/", "check_interval_sec": 1800, "daily_limit": 5, "alert_days": 3, "send_time": "09:00", "test_mode": false, "max_message_len": 320, "default_message": "⚡ Плановое отключение электроэнергии: {badge} {when} {time_range} — {address_short}{comment_opt}" ,м "tg_bot_token": "8356004729:ABHt4Rw8XTujAl_QQLyTbsddCQEcRrbXMD8", "tg_chat_id": "-1003310217653", "api_port": 8080 }
🧠 Код демона: lenotify.cpp
Внутри программы есть несколько важных частей:
- 🌐 загрузка HTML через libcurl;
- 🧽 очистка HTML и парсинг строк таблицы отключений;
- 🧾 формирование хеша события (дата/время/адрес/комментарий) — чтобы отличать события друг от друга;
- 📓
sent_log.json— учёт отправленных сообщений, чтобы не повторяться; - 📨 отправка в Telegram через
sendMessage; - 🌐 небольшой HTTP API для просмотра/обновления конфига (опционально).
🧾 Основной исходник:
// lenotify.cpp // ----------------------------------------------------------------------------- // madLENotify v1.5 - Уведомления об отключениях Ленэнерго через Telegram // // Build: // g++ -o lenotify lenotify.cpp -std=c++17 -O2 -lcurl -lpthread -I. // // Run: // ./lenotify // ./lenotify --once // ./lenotify --test --force --once // ./lenotify --test-send // ./lenotify --history // ./lenotify --tg-check // ./lenotify --config /root/settings.json --history // ./lenotify --man // // Notes: // - Источник дат по МСК. Чтобы days_left совпадал, можно запускать так: // TZ=Europe/Moscow ./lenotify --once // ----------------------------------------------------------------------------- #include <curl/curl.h> #include <algorithm> #include <chrono> #include <cctype> #include <ctime> #include <fstream> #include <iomanip> #include <iostream> #include <mutex> #include <regex> #include <sstream> #include <string> #include <thread> #include <vector> #include "json.hpp" #include "httplib.h" using json = nlohmann::json; // ------------------------- Logging ------------------------- static void log_line(const std::string& msg) { std::ofstream logfile("log.txt", std::ios::app); auto now = std::chrono::system_clock::now(); std::time_t t = std::chrono::system_clock::to_time_t(now); logfile << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S") << " " << msg << "\n"; std::cout << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S") << " " << msg << "\n"; } // ------------------------- CLI man/help ------------------------- static const char* MAN_TEXT = R"MAN( madLENotify v1.5 — оповещения об отключениях (Ленэнерго planned_work) через Telegram бота Использование: ./lenotify [--config FILE] [--once] [--test] [--force] [--test-send] [--debug] [--history] [--tg-check] [--man|--help] Опции: --config FILE Явно указать путь к файлу настроек (JSON). Если не задано — пробует settings.json затем config.json в текущей директории. --man, --help Показать справку. --once Один цикл проверки и выход. --test Отладка: слать сразу (игнор send_time и daily_limit). --force Принудительно слать даже если уже слали сегодня (игнор sent_log). --test-send Отправить тестовое сообщение и выйти. --debug Подробный вывод отладочной информации. --history Отправить в Telegram таблицу истории (прошедшие отключения). --tg-check Проверить Telegram токен через getMe и выйти. Поддерживаемые ключи в JSON (оба варианта): Новый/унифицированный формат: telegram_bot_token, telegram_api_url, chat_ids (array of strings) Старый/короткий формат: tg_bot_token, tg_api_url, tg_chat_id (string) Остальное: settlement, url_base, alert_days, send_time, check_interval_sec, daily_limit, max_message_len, default_message, api_port, history_days, history_max_rows, debug_telegram_url Важно: Токен бота нельзя публиковать. Если засветили — перевыпустите в @BotFather. )MAN"; // ------------------------- Helpers: strings ------------------------- static std::string trim_ws(std::string s) { auto issp = [](unsigned char c){ return std::isspace(c) != 0; }; while (!s.empty() && issp((unsigned char)s.front())) s.erase(s.begin()); while (!s.empty() && issp((unsigned char)s.back())) s.pop_back(); return s; } static std::string collapse_spaces(std::string s) { s = std::regex_replace(s, std::regex("\\s+"), " "); return trim_ws(s); } static std::string replace_all(std::string s, const std::string& what, const std::string& with) { if (what.empty()) return s; size_t pos = 0; while ((pos = s.find(what, pos)) != std::string::npos) { s.replace(pos, what.size(), with); pos += with.size(); } return s; } static std::string strip_tags(std::string s) { s = std::regex_replace(s, std::regex("<[^>]*>"), " "); s = std::regex_replace(s, std::regex(" | "), " "); s = std::regex_replace(s, std::regex("&"), "&"); s = std::regex_replace(s, std::regex("""), "\""); s = std::regex_replace(s, std::regex("<"), "<"); s = std::regex_replace(s, std::regex(">"), ">"); return collapse_spaces(s); } static std::string remove_uuids(std::string s) { s = std::regex_replace( s, std::regex(R"(\b[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}\b)"), "" ); return collapse_spaces(s); } static std::string html_escape(std::string s) { s = replace_all(s, "&", "&"); s = replace_all(s, "<", "<"); s = replace_all(s, ">", ">"); return s; } // ------------------------- UTF-8 validation + cp1251->utf8 fallback ------------------------- static bool is_valid_utf8(const std::string& s) { const unsigned char* p = (const unsigned char*)s.data(); size_t n = s.size(); size_t i = 0; while (i < n) { unsigned char c = p[i]; if (c <= 0x7F) { i++; continue; } if ((c & 0xE0) == 0xC0) { if (i + 1 >= n) return false; unsigned char c1 = p[i + 1]; if ((c1 & 0xC0) != 0x80) return false; if (c == 0xC0 || c == 0xC1) return false; i += 2; continue; } if ((c & 0xF0) == 0xE0) { if (i + 2 >= n) return false; unsigned char c1 = p[i + 1], c2 = p[i + 2]; if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) return false; if (c == 0xE0 && (c1 & 0xE0) == 0x80) return false; if (c == 0xED && (c1 & 0xE0) == 0xA0) return false; i += 3; continue; } if ((c & 0xF8) == 0xF0) { if (i + 3 >= n) return false; unsigned char c1 = p[i + 1], c2 = p[i + 2], c3 = p[i + 3]; if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) return false; if (c == 0xF0 && (c1 & 0xF0) == 0x80) return false; if (c > 0xF4) return false; if (c == 0xF4 && c1 > 0x8F) return false; i += 4; continue; } return false; } return true; } static std::string cp1251_to_utf8(const std::string& in) { std::string out; out.reserve(in.size() * 2); auto append_utf8 = [&](uint32_t cp) { if (cp <= 0x7F) out.push_back((char)cp); else if (cp <= 0x7FF) { out.push_back((char)(0xC0 | (cp >> 6))); out.push_back((char)(0x80 | (cp & 0x3F))); } else { out.push_back((char)(0xE0 | (cp >> 12))); out.push_back((char)(0x80 | ((cp >> 6) & 0x3F))); out.push_back((char)(0x80 | (cp & 0x3F))); } }; for (unsigned char ch : in) { if (ch < 0x80) { out.push_back((char)ch); continue; } uint32_t cp = 0; if (ch == 0xA8) cp = 0x0401; else if (ch == 0xB8) cp = 0x0451; else if (ch >= 0xC0 && ch <= 0xDF) cp = 0x0410 + (ch - 0xC0); else if (ch >= 0xE0 && ch <= 0xFF) cp = 0x0430 + (ch - 0xE0); else { switch (ch) { case 0x82: cp = 0x201A; break; case 0x84: cp = 0x201E; break; case 0x85: cp = 0x2026; break; case 0x86: cp = 0x2020; break; case 0x87: cp = 0x2021; break; case 0x91: cp = 0x2018; break; case 0x92: cp = 0x2019; break; case 0x93: cp = 0x201C; break; case 0x94: cp = 0x201D; break; case 0x96: cp = 0x2013; break; case 0x97: cp = 0x2014; break; default: cp = 0x003F; break; } } append_utf8(cp); } return out; } static bool meta_says_cp1251(const std::string& html) { std::string h = html; std::transform(h.begin(), h.end(), h.begin(), [](unsigned char c){ return (char)std::tolower(c); }); return (h.find("charset=windows-1251") != std::string::npos) || (h.find("charset=cp1251") != std::string::npos) || (h.find("charset=1251") != std::string::npos); } static std::string ensure_utf8_for_telegram(const std::string& s) { if (is_valid_utf8(s)) return s; std::string converted = cp1251_to_utf8(s); if (is_valid_utf8(converted)) return converted; std::string out; out.reserve(s.size()); for (unsigned char c : s) out.push_back((c < 0x80) ? (char)c : '?'); return out; } // ------------------------- CURL helpers ------------------------- static size_t write_callback(void* contents, size_t size, size_t nmemb, std::string* s) { s->append(static_cast<char*>(contents), size * nmemb); return size * nmemb; } static std::string curl_escape_value(const std::string& value) { CURL* curl = curl_easy_init(); if (!curl) return value; char* encoded = curl_easy_escape(curl, value.c_str(), 0); std::string result = encoded ? encoded : ""; if (encoded) curl_free(encoded); curl_easy_cleanup(curl); return result; } static bool curl_http_get(const std::string& url, std::string& out_body, std::string& out_err, long timeout_sec=15) { out_body.clear(); out_err.clear(); CURL* curl = curl_easy_init(); if (!curl) { out_err = "Failed to initialize CURL"; return false; } curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out_body); curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_sec); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "madLENotify/1.5 (curl)"); CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { out_err = std::string("CURL error: ") + curl_easy_strerror(res); curl_easy_cleanup(curl); return false; } curl_easy_cleanup(curl); return true; } static bool curl_http_post_form(const std::string& url, const std::string& post_data, std::string& out_body, std::string& out_err, long timeout_sec=15) { out_body.clear(); out_err.clear(); CURL* curl = curl_easy_init(); if (!curl) { out_err = "Failed to initialize CURL"; return false; } struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded"); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out_body); curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_sec); curl_easy_setopt(curl, CURLOPT_USERAGENT, "madLENotify/1.5 (curl)"); CURLcode res = curl_easy_perform(curl); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { out_err = std::string("CURL error: ") + curl_easy_strerror(res); return false; } return true; } // ------------------------- Fetch planned_work HTML ------------------------- static std::string fetch_html(const std::string& url) { std::string response, err; bool ok = curl_http_get(url, response, err, 30); if (!ok) { log_line("Fetch failed: " + err); return ""; } return response; } // ------------------------- Time/date helpers ------------------------- static std::string today_ymd() { time_t now = time(nullptr); tm* lt = localtime(&now); std::ostringstream ss; ss << (lt->tm_year + 1900) << "-" << std::setw(2) << std::setfill('0') << (lt->tm_mon + 1) << "-" << std::setw(2) << std::setfill('0') << lt->tm_mday; return ss.str(); } static void load_daily_sent(const std::string& file, std::string& day, int& sent) { day.clear(); sent = 0; std::ifstream f(file); if (!f) return; f >> day >> sent; if (!f) { day.clear(); sent = 0; } } static void save_daily_sent(const std::string& file, const std::string& day, int sent) { std::ofstream f(file, std::ios::trunc); f << day << " " << sent; } static bool parse_hhmm(const std::string& s, int& hh, int& mm) { hh = mm = 0; if (sscanf(s.c_str(), "%d:%d", &hh, &mm) != 2) return false; if (hh < 0 || hh > 23 || mm < 0 || mm > 59) return false; return true; } static int minutes_now_local() { time_t now = time(nullptr); tm* lt = localtime(&now); return lt->tm_hour * 60 + lt->tm_min; } static std::string dmy_dash_to_dot(std::string s) { for (char& c : s) if (c == '-') c = '.'; return s; } static std::string date_dot_to_ddmm(const std::string& d) { int day=0, mon=0, year=0; if (sscanf(d.c_str(), "%d.%d.%d", &day, &mon, &year) != 3) return ""; std::ostringstream ss; ss << std::setw(2) << std::setfill('0') << day << "." << std::setw(2) << std::setfill('0') << mon; return ss.str(); } static bool parse_dt_dot(const std::string& date_dot, const std::string& time_hm, std::tm& out) { int d=0,m=0,y=0, hh=0, mm=0; if (sscanf(date_dot.c_str(), "%d.%d.%d", &d, &m, &y) != 3) return false; if (!time_hm.empty()) { if (sscanf(time_hm.c_str(), "%d:%d", &hh, &mm) != 2) return false; } std::tm t{}; t.tm_mday = d; t.tm_mon = m - 1; t.tm_year = y - 1900; t.tm_hour = hh; t.tm_min = mm; t.tm_sec = 0; t.tm_isdst = -1; out = t; return true; } static std::tm today_date_tm() { time_t now = time(nullptr); tm t = *localtime(&now); t.tm_hour = t.tm_min = t.tm_sec = 0; t.tm_isdst = -1; return t; } static int days_between_dates_local(const std::tm& a, const std::tm& b) { std::tm aa = a; std::tm bb = b; aa.tm_hour = aa.tm_min = aa.tm_sec = 0; bb.tm_hour = bb.tm_min = bb.tm_sec = 0; time_t ta = mktime(&aa); time_t tb = mktime(&bb); double diff = difftime(tb, ta); return (int)(diff / 86400.0); } // ------------------------- Config: defaults + compatibility ------------------------- static void ensure_config_defaults(json& cfg) { if (!cfg.contains("settlement")) cfg["settlement"] = "Пески"; if (!cfg.contains("url_base")) cfg["url_base"] = "https://rosseti-lenenergo.ru/planned_work/"; if (!cfg.contains("daily_limit")) cfg["daily_limit"] = 5; if (!cfg.contains("check_interval_sec")) cfg["check_interval_sec"] = 1800; if (!cfg.contains("alert_days")) cfg["alert_days"] = 3; if (!cfg.contains("send_time")) cfg["send_time"] = "09:00"; if (!cfg.contains("test_mode")) cfg["test_mode"] = false; if (!cfg.contains("max_message_len")) cfg["max_message_len"] = 4096; if (!cfg.contains("default_message")) cfg["default_message"] = "⚡ {badge} {when} {time_range} — {address_short}{comment_opt}"; if (!cfg.contains("api_port")) cfg["api_port"] = 8080; if (!cfg.contains("history_days")) cfg["history_days"] = 30; if (!cfg.contains("history_max_rows")) cfg["history_max_rows"] = 200; if (!cfg.contains("debug_telegram_url")) cfg["debug_telegram_url"] = true; // Telegram API base if (!cfg.contains("telegram_api_url") && cfg.contains("tg_api_url")) cfg["telegram_api_url"] = cfg["tg_api_url"]; if (!cfg.contains("telegram_api_url")) cfg["telegram_api_url"] = "https://api.telegram.org"; // Token compatibility if (!cfg.contains("telegram_bot_token") && cfg.contains("tg_bot_token")) cfg["telegram_bot_token"] = cfg["tg_bot_token"]; if (!cfg.contains("telegram_bot_token")) cfg["telegram_bot_token"] = ""; // Chat ids compatibility if (!cfg.contains("chat_ids")) { if (cfg.contains("tg_chat_id")) { cfg["chat_ids"] = json::array({ cfg["tg_chat_id"] }); } else { cfg["chat_ids"] = json::array(); } } else { // ensure array if (!cfg["chat_ids"].is_array()) { json arr = json::array(); arr.push_back(cfg["chat_ids"]); cfg["chat_ids"] = arr; } } } static bool file_exists(const std::string& path) { std::ifstream f(path); return f.good(); } static bool load_config_from_file(const std::string& path, json& out_cfg, std::string& out_err) { out_err.clear(); std::ifstream f(path); if (!f.is_open()) { out_err = "Cannot open file: " + path; return false; } try { f >> out_cfg; } catch (const std::exception& e) { out_err = std::string("JSON parse error: ") + e.what(); return false; } ensure_config_defaults(out_cfg); return true; } // ------------------------- URL planned_work builder ------------------------- static std::string build_planned_work_url(const json& cfg) { std::string base = cfg.value("url_base", "https://rosseti-lenenergo.ru/planned_work/"); if (base.find('?') == std::string::npos) { base += "?reg=&date_start=&date_finish=&res=&"; } else { if (base.back() != '&' && base.back() != '?') base += "&"; } const std::string settlement = cfg.value("settlement", "Пески"); const std::string city = curl_escape_value(settlement); if (base.find("city=") == std::string::npos) base += "city=" + city + "&"; if (base.find("street=") == std::string::npos) base += "street="; return base; } // ------------------------- Outage + hashing ------------------------- struct Outage { std::string date_start_dot; // dd.mm.yyyy std::string time_start; // HH:MM std::string date_end_dot; std::string time_end; std::string address; std::string comment; std::tm start_tm{}; bool start_tm_ok = false; std::string key_string() const { return date_start_dot + "|" + time_start + "|" + date_end_dot + "|" + time_end + "|" + address + "|" + comment; } std::string hash64_hex() const { const uint64_t FNV_OFFSET = 1469598103934665603ull; const uint64_t FNV_PRIME = 1099511628211ull; uint64_t h = FNV_OFFSET; std::string k = key_string(); for (unsigned char c : k) { h ^= (uint64_t)c; h *= FNV_PRIME; } std::ostringstream ss; ss << std::hex << std::setfill('0') << std::setw(16) << h; return ss.str(); } }; // ------------------------- Parser helpers ------------------------- static bool contains_settlement(const std::string& row_text, const std::string& settlement) { if (settlement.empty() || row_text.empty()) return false; std::string lower_row = row_text; std::transform(lower_row.begin(), lower_row.end(), lower_row.begin(), [](unsigned char c){ return (char)std::tolower(c); }); std::string lower_settlement = settlement; std::transform(lower_settlement.begin(), lower_settlement.end(), lower_settlement.begin(), [](unsigned char c){ return (char)std::tolower(c); }); if (lower_row.find(lower_settlement) != std::string::npos) return true; std::vector<std::string> prefixes = { "д ", "дер ", "деревня ", "пос ", "пос. ", "посёлок ", "п ", "п. ", "с ", "село ", "г ", "город ", "рп ", "рабочий поселок ", "тер ", "тер. ", "территория ", "мкр ", "микрорайон ", "ул ", "улица ", "пер ", "переулок ", "пр ", "проспект ", "б-р ", "бульвар ", "ш ", "шоссе " }; for (const auto& prefix : prefixes) { if (lower_row.find(prefix + lower_settlement) != std::string::npos) return true; if (lower_row.find(prefix + " " + lower_settlement) != std::string::npos) return true; } return false; } static bool looks_like_dmy_dash(const std::string& s) { return std::regex_match(s, std::regex(R"(\d{2}-\d{2}-\d{4})")); } static bool looks_like_hhmm(const std::string& s) { return std::regex_match(s, std::regex(R"(\d{1,2}:\d{2})")); } // ------------------------- Parser main ------------------------- static std::vector<Outage> parse_outages(const std::string& html_utf8, const std::string& settlement, bool debug=false) { std::vector<Outage> outages; std::regex tr_re(R"(<tr[^>]*>([\s\S]*?)</tr>)", std::regex::icase); std::regex td_re(R"(<td[^>]*>([\s\S]*?)</td>)", std::regex::icase); for (auto tr_it = std::sregex_iterator(html_utf8.begin(), html_utf8.end(), tr_re); tr_it != std::sregex_iterator(); ++tr_it) { std::string tr_body = (*tr_it)[1].str(); if (tr_body.find("<th") != std::string::npos || tr_body.find("</th>") != std::string::npos) continue; std::vector<std::string> cells; for (auto td_it = std::sregex_iterator(tr_body.begin(), tr_body.end(), td_re); td_it != std::sregex_iterator(); ++td_it) { std::string cell = strip_tags((*td_it)[1].str()); cell = remove_uuids(cell); cells.push_back(cell); } if (cells.empty()) continue; std::string row_text; for (auto& c : cells) if (!c.empty()) row_text += c + " "; row_text = collapse_spaces(remove_uuids(row_text)); if (row_text.empty()) continue; if (!contains_settlement(row_text, settlement)) continue; Outage o; if (cells.size() >= 7 && looks_like_dmy_dash(cells[3]) && looks_like_hhmm(cells[4]) && looks_like_dmy_dash(cells[5]) && looks_like_hhmm(cells[6])) { o.date_start_dot = dmy_dash_to_dot(cells[3]); o.time_start = cells[4]; o.date_end_dot = dmy_dash_to_dot(cells[5]); o.time_end = cells[6]; } else { std::smatch m; std::regex re_date(R"(\b(\d{1,2}[.\-]\d{1,2}[.\-]\d{4})\b)"); std::regex re_time(R"(\b(\d{1,2}:\d{2})\b)"); std::string d1, d2, t1, t2; if (std::regex_search(row_text, m, re_date)) { d1 = dmy_dash_to_dot(m[1].str()); std::string tail = row_text.substr((size_t)m.position(1) + (size_t)m.length(1)); std::smatch m2; if (std::regex_search(tail, m2, re_time)) t1 = m2[1].str(); if (std::regex_search(tail, m2, re_date)) { d2 = dmy_dash_to_dot(m2[1].str()); std::string tail2 = tail.substr((size_t)m2.position(1) + (size_t)m2.length(1)); std::smatch m3; if (std::regex_search(tail2, m3, re_time)) t2 = m3[1].str(); } } o.date_start_dot = d1; o.time_start = t1; o.date_end_dot = d2; o.time_end = t2; } if (cells.size() >= 3 && !cells[2].empty()) o.address = cells[2]; else o.address = row_text; if (cells.size() >= 10 && !cells[9].empty()) o.comment = cells[9]; else o.comment.clear(); std::tm tm_start{}; if (parse_dt_dot(o.date_start_dot, o.time_start, tm_start)) { o.start_tm = tm_start; o.start_tm_ok = true; } if (!o.start_tm_ok) continue; outages.push_back(std::move(o)); } if (debug) std::cout << "Total outages found: " << outages.size() << "\n"; return outages; } // ------------------------- Address shorten ------------------------- static std::string address_shorten(std::string a) { a = collapse_spaces(a); a = std::regex_replace(a, std::regex(R"(Ленинградская область\s*)"), ""); a = std::regex_replace(a, std::regex(R"(\s*р-н\s+\S+)"), ""); a = std::regex_replace(a, std::regex(R"(\s*район\s+\S+)"), ""); a = collapse_spaces(a); if (a.size() > 90) { a.resize(90); a += "..."; } return a; } // ------------------------- Template tags ------------------------- static std::string when_label(int days_left, const std::string& date_ddmm) { if (days_left == 0) return "СЕГОДНЯ"; if (days_left == 1) return "ЗАВТРА"; if (days_left >= 2 && days_left <= 9) { std::ostringstream ss; ss << "ЧЕРЕЗ " << days_left << " ДН."; return ss.str(); } return date_ddmm.empty() ? "СКОРО" : date_ddmm; } static std::string badge_label(int days_left) { if (days_left == 0) return "🟥"; if (days_left == 1) return "🟨"; return "⬜"; } static std::string time_range_ascii(const std::string& a, const std::string& b) { if (!a.empty() && !b.empty()) return a + "-" + b; if (!a.empty()) return a; if (!b.empty()) return b; return ""; } static std::string format_message(const json& cfg, const Outage& o) { std::string tmpl = cfg.value("default_message", "⚡ {badge} {when} {time_range} — {address_short}{comment_opt}"); std::tm today = today_date_tm(); int days_left = days_between_dates_local(today, o.start_tm); std::string ddmm = date_dot_to_ddmm(o.date_start_dot); std::string when = when_label(days_left, ddmm); std::string badge = badge_label(days_left); std::string addr_short = address_shorten(o.address); std::string tr = time_range_ascii(o.time_start, o.time_end); std::string comment = collapse_spaces(o.comment); std::string comment_opt = comment.empty() ? "" : (". " + comment); std::string msg = tmpl; msg = replace_all(msg, "{badge}", badge); msg = replace_all(msg, "{when}", when); msg = replace_all(msg, "{days_left}", std::to_string(days_left)); msg = replace_all(msg, "{date}", ddmm); msg = replace_all(msg, "{date_full}", o.date_start_dot); msg = replace_all(msg, "{time_start}", o.time_start); msg = replace_all(msg, "{time_end}", o.time_end); msg = replace_all(msg, "{time_range}", tr); msg = replace_all(msg, "{address}", o.address); msg = replace_all(msg, "{address_short}", addr_short); msg = replace_all(msg, "{comment}", comment); msg = replace_all(msg, "{comment_opt}", comment_opt); msg = remove_uuids(msg); msg = collapse_spaces(msg); int maxlen = cfg.value("max_message_len", 4096); if (maxlen < 40) maxlen = 40; if ((int)msg.size() > maxlen) { msg.resize((size_t)maxlen); msg = collapse_spaces(msg); if (msg.size() + 3 <= (size_t)maxlen) msg += "..."; } return msg; } // ------------------------- Policy ------------------------- static bool is_within_alert_window(const Outage& o, int alert_days) { std::tm today = today_date_tm(); int days_left = days_between_dates_local(today, o.start_tm); return days_left >= 0 && days_left <= alert_days; } static bool time_to_send_today(const json& cfg, bool test_mode_cli) { if (test_mode_cli) return true; if (cfg.value("test_mode", false)) return true; int hh=0, mm=0; std::string st = cfg.value("send_time", "09:00"); if (!parse_hhmm(st, hh, mm)) return true; int nowm = minutes_now_local(); int target = hh * 60 + mm; return nowm >= target; } // ------------------------- Persistence: sent_log.json ------------------------- static json load_sent_log() { std::ifstream f("sent_log.json"); if (!f) return json::object(); try { json j; f >> j; if (!j.is_object()) return json::object(); return j; } catch (...) { return json::object(); } } static void save_sent_log(const json& j) { std::ofstream("sent_log.json", std::ios::trunc) << j.dump(2); } static bool sent_today(json& sent_log, const std::string& day, const std::string& chat_id, const std::string& h) { if (!sent_log.contains(day)) return false; if (!sent_log[day].contains(chat_id)) return false; for (auto& x : sent_log[day][chat_id]) { if (x.is_string() && x.get<std::string>() == h) return true; } return false; } static void mark_sent_today(json& sent_log, const std::string& day, const std::string& chat_id, const std::string& h) { if (!sent_log.contains(day) || !sent_log[day].is_object()) sent_log[day] = json::object(); if (!sent_log[day].contains(chat_id) || !sent_log[day][chat_id].is_array()) sent_log[day][chat_id] = json::array(); sent_log[day][chat_id].push_back(h); } static void cleanup_sent_log(json& sent_log, int keep_days = 14) { if (!sent_log.is_object()) return; std::vector<std::string> keys; for (auto it = sent_log.begin(); it != sent_log.end(); ++it) { if (it.key().size() == 10) keys.push_back(it.key()); } std::sort(keys.begin(), keys.end()); if ((int)keys.size() <= keep_days) return; int drop = (int)keys.size() - keep_days; for (int i = 0; i < drop; ++i) sent_log.erase(keys[i]); } // ------------------------- Telegram: unified ------------------------- static std::string tg_api_base(const json& cfg) { std::string base = trim_ws(cfg.value("telegram_api_url", "https://api.telegram.org")); while (!base.empty() && base.back() == '/') base.pop_back(); // If user mistakenly put /bot... in api_url, strip it auto p = base.find("/bot"); if (p != std::string::npos) base = base.substr(0, p); return base; } static std::string tg_token(const json& cfg) { return trim_ws(cfg.value("telegram_bot_token", "")); } static std::string tg_token_preview(const std::string& token) { if (token.empty()) return "(empty)"; if (token.size() <= 16) return token; return token.substr(0, 6) + "..." + token.substr(token.size()-6); } static std::string tg_url_method(const json& cfg, const std::string& method) { std::string token = tg_token(cfg); if (token.empty()) return ""; return tg_api_base(cfg) + "/bot" + token + "/" + method; } static bool telegram_getMe(const json& cfg, std::string& out_err) { out_err.clear(); std::string url = tg_url_method(cfg, "getMe"); if (url.empty()) { out_err = "telegram_bot_token is empty"; return false; } if (cfg.value("debug_telegram_url", true)) log_line("Telegram URL: " + url); std::string body, err; if (!curl_http_get(url, body, err, 15)) { out_err = err; return false; } if (body.find("\"ok\":true") != std::string::npos) return true; out_err = "Telegram API error: " + body; return false; } static bool send_telegram(const json& cfg, const std::string& chat_id, const std::string& text_any, std::string& out_err, const std::string& parse_mode /* "" or "HTML" */) { out_err.clear(); std::string token = tg_token(cfg); if (token.empty()) { out_err = "telegram_bot_token is empty"; return false; } std::string url = tg_url_method(cfg, "sendMessage"); if (url.empty()) { out_err = "failed to build telegram sendMessage url"; return false; } if (cfg.value("debug_telegram_url", true)) log_line("Telegram URL: " + url); std::string msg = ensure_utf8_for_telegram(text_any); // IMPORTANT: escape both chat_id and text like curl -d does std::string post = "chat_id=" + curl_escape_value(chat_id) + "&text=" + curl_escape_value(msg) + "&disable_web_page_preview=true"; if (!parse_mode.empty()) post += "&parse_mode=" + curl_escape_value(parse_mode); std::string body, err; if (!curl_http_post_form(url, post, body, err, 15)) { out_err = err; return false; } if (body.find("\"ok\":true") != std::string::npos) return true; out_err = "Telegram API error: " + body; return false; } // ------------------------- Test message ------------------------- static bool send_test_message(const json& cfg, const std::string& chat_id, std::string& out_err) { std::time_t now = std::time(nullptr); std::tm* local_time = std::localtime(&now); char time_buf[80]; std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", local_time); std::string m = "madLENotify — Тестовое сообщение\n\n" "✅ Бот успешно запущен и готов к работе!\n" "🕒 Время сервера: " + std::string(time_buf) + "\n" "📍 Населенный пункт: " + cfg.value("settlement", "Пески") + "\n" "⏰ Период проверки: " + std::to_string(cfg.value("check_interval_sec", 1800) / 60) + " мин.\n" "🔔 Оповещение за: " + std::to_string(cfg.value("alert_days", 3)) + " дня\n"; return send_telegram(cfg, chat_id, m, out_err, ""); } // ------------------------- History table builder ------------------------- static std::string dt_line(const Outage& o, bool is_start) { const std::string& d = is_start ? o.date_start_dot : o.date_end_dot; const std::string& t = is_start ? o.time_start : o.time_end; if (d.empty() && t.empty()) return ""; if (d.empty()) return t; if (t.empty()) return d; return d + " " + t; } static std::vector<std::string> build_history_messages_html(const json& cfg, const std::vector<Outage>& outages) { std::tm today = today_date_tm(); int history_days = cfg.value("history_days", 30); int max_rows = cfg.value("history_max_rows", 200); std::vector<Outage> past; for (const auto& o : outages) { int days_left = days_between_dates_local(today, o.start_tm); if (days_left < 0 && (-days_left <= history_days)) past.push_back(o); } std::sort(past.begin(), past.end(), [](const Outage& a, const Outage& b) { std::tm aa = a.start_tm; std::tm bb = b.start_tm; return mktime(&aa) > mktime(&bb); }); if ((int)past.size() > max_rows) past.resize((size_t)max_rows); if (past.empty()) { return { "🕘 Прошедшие отключения\n\nЗа последние " + std::to_string(history_days) + " дней отключений не найдено." }; } struct Row { std::string a, b; }; std::vector<Row> rows; for (const auto& o : past) rows.push_back({ dt_line(o, true), dt_line(o, false) }); // fixed width for pre const int W = 19; // "dd.mm.yyyy hh:mm" = 16, + запас const int rows_per_msg = 55; std::vector<std::string> out; for (int i = 0; i < (int)rows.size(); i += rows_per_msg) { int j = std::min((int)rows.size(), i + rows_per_msg); std::ostringstream ss; ss << "🕘 Прошедшие отключения\n"; ss << "<pre>"; ss << std::left << std::setw(W) << "Начало работ" << " " << std::setw(W) << "Окончание работ" << "\n"; for (int k = i; k < j; ++k) { std::string a = html_escape(rows[k].a); std::string b = html_escape(rows[k].b); ss << std::left << std::setw(W) << a << " " << std::setw(W) << b << "\n"; } ss << "</pre>\n"; ss << "События определяются по дате МСК; формат источника может меняться."; out.push_back(ss.str()); } return out; } // ------------------------- Save last html ------------------------- static void save_last_html(const std::string& html_utf8) { std::ofstream("last.html", std::ios::trunc) << html_utf8; } // ------------------------- HTTP API ------------------------- static void start_api_thread(json& config, std::mutex& config_mtx) { std::thread([&config, &config_mtx]() { httplib::Server svr; svr.Get("/config", [&config, &config_mtx](const httplib::Request&, httplib::Response& res) { std::lock_guard<std::mutex> lk(config_mtx); res.set_content(config.dump(2), "application/json"); }); svr.Post("/config", [&config, &config_mtx](const httplib::Request& req, httplib::Response& res) { try { json new_cfg = json::parse(req.body); ensure_config_defaults(new_cfg); { std::lock_guard<std::mutex> lk(config_mtx); config = new_cfg; std::ofstream("settings.json", std::ios::trunc) << config.dump(2); } log_line("Config updated via API -> settings.json"); res.set_content("Updated", "text/plain"); } catch (...) { res.status = 400; res.set_content("Invalid JSON", "text/plain"); } }); int port = 8080; { std::lock_guard<std::mutex> lk(config_mtx); port = config.value("api_port", 8080); } std::cout << "🌐 madLENotify API запущен на порту " << port << "\n"; log_line("madLENotify API started on port " + std::to_string(port)); bool ok = svr.listen("0.0.0.0", port); log_line(std::string("API listen result: ") + (ok ? "OK" : "FAIL")); }).detach(); } // ------------------------- History mode ------------------------- static void run_history_mode(json& config, std::mutex& config_mtx) { json cfg; { std::lock_guard<std::mutex> lk(config_mtx); cfg = config; } std::cout << "madLENotify: Запуск режима истории...\n"; // TG check { std::string tgerr; bool ok = telegram_getMe(cfg, tgerr); if (!ok) { log_line("Telegram getMe FAILED: " + tgerr); std::cerr << "Ошибка Telegram (getMe): " << tgerr << "\n"; return; } log_line("Telegram getMe OK"); } std::string url = build_planned_work_url(cfg); std::cout << "URL: " << url << "\n"; std::string html = fetch_html(url); if (html.empty()) { std::cerr << "Ошибка: не удалось скачать страницу\n"; const auto chat_ids = cfg["chat_ids"].get<std::vector<std::string>>(); for (const auto& chat_id : chat_ids) { std::string err; send_telegram(cfg, chat_id, "madLENotify: Не удалось скачать данные с сайта Ленэнерго\nURL: " + url, err, ""); } return; } if (meta_says_cp1251(html) || !is_valid_utf8(html)) { log_line("History: converting HTML to UTF-8 (cp1251 fallback)"); html = cp1251_to_utf8(html); } else { html = ensure_utf8_for_telegram(html); } save_last_html(html); std::cout << "Страница скачана (" << html.size() << " байт)\n"; std::string settlement = cfg.value("settlement", "Пески"); auto outages = parse_outages(html, settlement, false); std::cout << "НАЙДЕНО ОТКЛЮЧЕНИЙ: " << outages.size() << "\n"; auto msgs = build_history_messages_html(cfg, outages); const auto chat_ids = cfg["chat_ids"].get<std::vector<std::string>>(); for (const auto& chat_id : chat_ids) { for (const auto& m : msgs) { std::string err; bool ok = send_telegram(cfg, chat_id, m, err, "HTML"); if (!ok) { log_line("Failed to send history chunk to chat " + chat_id + ": " + err); std::cerr << "Ошибка отправки history: " << err << "\n"; break; } std::this_thread::sleep_for(std::chrono::milliseconds(250)); } } } // ------------------------- One cycle ------------------------- static void run_one_cycle(json& config, std::mutex& config_mtx, json& sent_log, bool test_mode_cli, bool force_cli, bool debug_mode) { json cfg; { std::lock_guard<std::mutex> lk(config_mtx); cfg = config; } std::cout << "madLENotify: Начало проверки...\n"; // TG check { std::string tgerr; bool ok = telegram_getMe(cfg, tgerr); if (!ok) { log_line("Telegram getMe FAILED: " + tgerr); std::cerr << "Ошибка Telegram (getMe): " << tgerr << "\n"; return; } log_line("Telegram getMe OK"); } std::string today = today_ymd(); std::string last_day; int sent_count = 0; load_daily_sent("daily_sent.txt", last_day, sent_count); if (last_day != today) sent_count = 0; int daily_limit = cfg.value("daily_limit", 5); std::cout << "Отправлено сегодня: " << sent_count << " (лимит " << daily_limit << ")\n"; std::string url = build_planned_work_url(cfg); std::cout << "URL: " << url << "\n"; std::string html = fetch_html(url); if (html.empty()) { log_line("Fetch failed: empty HTML"); std::cout << "Ошибка загрузки страницы\n"; const auto chat_ids = cfg["chat_ids"].get<std::vector<std::string>>(); for (const auto& chat_id : chat_ids) { std::string err; send_telegram(cfg, chat_id, "madLENotify: Ошибка загрузки данных\nURL: " + url, err, ""); } return; } if (meta_says_cp1251(html) || !is_valid_utf8(html)) { log_line("Cycle: converting HTML to UTF-8 (cp1251 fallback)"); html = cp1251_to_utf8(html); } else { html = ensure_utf8_for_telegram(html); } save_last_html(html); std::cout << "Страница скачана (размер: " << html.size() << " байт)\n"; std::string settlement = cfg.value("settlement", "Пески"); auto outages = parse_outages(html, settlement, debug_mode); int alert_days = cfg.value("alert_days", 3); std::vector<Outage> eligible; for (const auto& o : outages) if (is_within_alert_window(o, alert_days)) eligible.push_back(o); std::cout << "Найдено отключений: " << outages.size() << ", подходящих (в пределах " << alert_days << " дней): " << eligible.size() << "\n"; if (!time_to_send_today(cfg, test_mode_cli)) { std::cout << "Не время для отправки (send_time=" << cfg.value("send_time", "09:00") << ")\n"; log_line("Not time to send yet"); return; } const auto chat_ids = cfg["chat_ids"].get<std::vector<std::string>>(); for (const auto& o : eligible) { std::string h = o.hash64_hex(); for (const auto& chat_id : chat_ids) { if (!test_mode_cli && sent_count >= daily_limit) { log_line("Daily limit reached"); std::cout << "Достигнут дневной лимит сообщений\n"; goto DONE_SENDING; } if (!force_cli && sent_today(sent_log, today, chat_id, h)) { std::cout << "Сообщение уже отправлено сегодня (hash: " << h.substr(0, 8) << "...)\n"; continue; } std::string msg = format_message(cfg, o); std::cout << "Отправка -> chat " << chat_id << ": " << msg << "\n"; std::string err; bool ok = send_telegram(cfg, chat_id, msg, err, ""); if (!ok) { std::cout << "Ошибка отправки в chat " << chat_id << "\n"; log_line("Send failed to chat " + chat_id + ": " + err); std::cerr << "DETAILS:\n" << err << "\n"; } else { sent_count++; mark_sent_today(sent_log, today, chat_id, h); log_line("Sent to chat " + chat_id + ": " + msg); std::cout << "Сообщение отправлено в chat " << chat_id << "\n"; } } } DONE_SENDING: cleanup_sent_log(sent_log, 14); save_sent_log(sent_log); save_daily_sent("daily_sent.txt", today, sent_count); std::cout << "madLENotify: Проверка завершена\n"; std::cout << " Найдено отключений: " << outages.size() << "\n"; std::cout << " В пределах " << alert_days << " дней: " << eligible.size() << "\n"; std::cout << " Отправлено сообщений: " << sent_count << "\n"; } // ------------------------- main ------------------------- int main(int argc, char** argv) { bool once = false; bool test_cli = false; bool force_cli = false; bool test_send = false; bool debug_mode = false; bool history_mode = false; bool tg_check = false; std::string config_path; for (int i = 1; i < argc; ++i) { std::string a = argv[i]; if (a == "--man" || a == "--help") { std::cout << MAN_TEXT; return 0; } else if (a == "--once") { once = true; } else if (a == "--test") { test_cli = true; } else if (a == "--force") { force_cli = true; } else if (a == "--test-send") { test_send = true; } else if (a == "--debug") { debug_mode = true; } else if (a == "--history") { history_mode = true; } else if (a == "--tg-check") { tg_check = true; } else if (a == "--config") { if (i + 1 >= argc) { std::cerr << "Ошибка: --config требует путь к файлу\n"; return 2; } config_path = argv[++i]; } else { std::cerr << "Неизвестная опция: " << a << "\n\n" << MAN_TEXT; return 2; } } curl_global_init(CURL_GLOBAL_DEFAULT); std::cout << "madLENotify v1.5 — Уведомления об отключениях Ленэнерго\n"; std::cout << "=====================================================\n"; // choose config if (config_path.empty()) { if (file_exists("settings.json")) config_path = "settings.json"; else config_path = "config.json"; } json config; std::string err; if (!load_config_from_file(config_path, config, err)) { std::cerr << "Ошибка загрузки " << config_path << ": " << err << "\n"; curl_global_cleanup(); return 1; } // Print what we loaded std::string token = tg_token(config); std::cout << "Файл настроек: " << config_path << "\n"; std::cout << "Населенный пункт: " << config.value("settlement", "Пески") << "\n"; std::cout << "URL: " << build_planned_work_url(config) << "\n"; std::cout << "Интервал проверки: " << config.value("check_interval_sec", 1800) << " сек.\n"; std::cout << "Оповещение за: " << config.value("alert_days", 3) << " дня\n"; std::cout << "History days: " << config.value("history_days", 30) << "\n"; std::cout << "History max rows: " << config.value("history_max_rows", 200) << "\n"; std::cout << "Telegram api: " << tg_api_base(config) << "\n"; std::cout << "Telegram token preview: " << tg_token_preview(token) << " (len=" << token.size() << ")\n"; if (!config["chat_ids"].is_array() || config["chat_ids"].empty()) { std::cout << "⚠ chat_ids пустой! Добавь chat_ids: [\"-100...\"] или tg_chat_id\n"; } else { std::cout << "chat_ids: " << config["chat_ids"].dump() << "\n"; } if (tg_check) { std::string e; bool ok = telegram_getMe(config, e); if (ok) { std::cout << "Telegram getMe: OK\n"; log_line("Telegram getMe: OK"); curl_global_cleanup(); return 0; } std::cerr << "Telegram getMe: FAIL: " << e << "\n"; log_line("Telegram getMe: FAIL: " + e); curl_global_cleanup(); return 1; } if (test_send) { std::cout << "\nОтправка тестового сообщения...\n"; std::string e; if (!telegram_getMe(config, e)) { std::cerr << "Ошибка Telegram (getMe): " << e << "\n"; log_line("Telegram getMe FAILED: " + e); curl_global_cleanup(); return 1; } const auto chat_ids = config["chat_ids"].get<std::vector<std::string>>(); bool any_sent = false; for (const auto& chat_id : chat_ids) { std::string se; bool ok = send_test_message(config, chat_id, se); if (ok) { std::cout << "Тестовое сообщение отправлено в chat " << chat_id << "\n"; log_line("Test message sent to chat " + chat_id); any_sent = true; } else { std::cerr << "Ошибка отправки в chat " << chat_id << ": " << se << "\n"; log_line("Failed to send test message to chat " + chat_id + ": " + se); } } curl_global_cleanup(); return any_sent ? 0 : 1; } if (history_mode) { std::mutex config_mtx; run_history_mode(config, config_mtx); curl_global_cleanup(); return 0; } std::mutex config_mtx; start_api_thread(config, config_mtx); std::cout << "\nКонфигурация загружена\n"; json sent_log = load_sent_log(); while (true) { std::cout << "\n" << std::string(50, '=') << "\n"; run_one_cycle(config, config_mtx, sent_log, test_cli, force_cli, debug_mode); if (once) { std::cout << "\nЗавершено (--once)\n"; break; } json cfg; { std::lock_guard<std::mutex> lk(config_mtx); cfg = config; } int sleep_sec = cfg.value("check_interval_sec", 1800); std::cout << "\nОжидание " << sleep_sec << " секунд...\n"; std::this_thread::sleep_for(std::chrono::seconds(sleep_sec)); } curl_global_cleanup(); std::cout << "\nmadLENotify завершает работу\n"; return 0; }
🧷 Запуск как systemd-сервис
Чтобы демон работал постоянно и автоматически стартовал после перезагрузки, оформим его как systemd unit 🧩.
📄 Создаём юнит
Обычно кладём файл в /etc/systemd/system/lenotify.service. Внутри задаём путь к бинарнику, рабочую директорию, перезапуск и вывод в журнал.
[Unit] Description=LENotify - Telegram notifications for Leningrad power outages After=network.target [Service] Type=simple User=root WorkingDirectory=/root Environment=TZ=Europe/Moscow ExecStart=/root/lenotify Restart=always RestartSec=30 StandardOutput=journal StandardError=journal SyslogIdentifier=lenotify # Ограничения ресурсов MemoryMax=512M CPUQuota=50% [Install] WantedBy=multi-user.target
✅ Включаем и запускаем
🚀 Команды включения юнита и т. п.
# Перезагружаем systemd sudo systemctl daemon-reload # Включаем автозапуск sudo systemctl enable lenotify # Запускаем сервис sudo systemctl start lenotify # Проверяем статус sudo systemctl status lenotify # Смотрим логи sudo journalctl -u lenotify -f
🛠 Полезные команды
Когда сервис живёт в systemd, самое полезное — смотреть статус и журнал. Плюс иногда нужно перезагрузить конфиги systemd после правок.
# Остановить сервис sudo systemctl stop lenotify # Перезапустить сервис sudo systemctl restart lenotify # Проверить логи sudo journalctl -u lenotify -n 50 # Следить за логами в реальном времени sudo journalctl -u lenotify -f # Проверить, запущен ли sudo systemctl is-active lenotify
🧪 Режимы запуска и отладка
- 🔁
./lenotify— обычный режим: вечный демон, периодически проверяет и шлёт. - 🕐
./lenotify --once— один цикл проверки и выход (удобно для отладки). - 🧪
./lenotify --test --force --once— отладка “в лоб”: игнорирует send_time и историю отправок. - 📨
./lenotify --test-send— тестовое сообщение (проверка Telegram). - 🧾
./lenotify --history— отправка истории прошедших отключений. - 🤖
./lenotify --tg-check— проверка токена через Telegram getMe. - 🪵
./lenotify --debug— более подробный вывод по парсингу.
📌 Советы по эксплуатации
- 🔒 Не публикуй токен бота. Если засветился — перевыпусти в @BotFather.
- 🧾 Проверь, что
chat_idsзаполнен (например,"-100..."для каналов/супергрупп). - 🕘 Убедись, что TZ на сервере корректный (или задай
Environment=TZ=Europe/Moscowв юните). - 🧰 Если что-то “не шлёт”, сначала смотри журнал systemd — там обычно всё видно.
🏁 Итого
madLENotify — простой, понятный и автономный способ получать уведомления об отключениях электричества в Telegram. Он живёт как сервис, пишет логи, умеет историю и не превращает чат в мусорку благодаря учёту отправок 🧯.
