Опять про ошибки

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

Итак, возвращаемся к ошибкам и их обработке.

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

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

Почему я выделил слово «допустимых»? Да собственно, потому, что алгоритм — это своего рода функция, которая отображает множество X, называемое входными данными на множество Y, называемое результатом. Соответственно, скучные понятия, известные еще со школьной математики, такие как ОО и ОДЗ, справедливы и для алгоритмов. Думаю, немало баллов ЕГЭ потеряно из-за действий, расширяющих или сужающих ОДЗ… Но вот почему-то, в отношении алгоритмов, никто особо на этот счет не задумывается, и программы пишутся вообще без учета этих понятий.

Я не утверждаю, что все так делают и все программы такие. Но редко когда в дискуссиях всплывают подобные темы. А начинающие программисты вообще, зачастую об этом и не подозревают.

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

Но, даже идеально вылизанный алгоритм не застрахован от ошибки. Дальнейшие ограничения накладывает архитектура. Аппаратная и программная. К примеру, молодой программист на Python, прогуливавший уроки информатики в школе (при условии еще, что их вел адекватный учитель, что возможно не всегда), может с удивлением узнать, что нельзя просто так взять и возвести 10 в 10-ю степень, используя язык Си. Или, вдруг, обнаружит, что нельзя посчитать биномиальный коэффициент 10 по 30 по формуле 30!/((30-10)!*10!) в лоб. Ну просто в силу того 30! не уместится ни в один из базовых типов данных C/C++, включая long long, хотя само по себе значение коэффициента вполне спокойно уместится в 32-битный int. И это, достаточно вульгарный, но показательный пример, когда архитектура влияет на работу алгоритма, и для простых, на первый взгляд, расчетов, приходится ухищряться и вспоминать математику, теорию чисел и множеств, и многое такое, что когда-то казалось ненужным, отвлекающим от «настоящего» программирования….

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

И я еще не упомянул пользователей…

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

И тут, я возвращаюсь к, недавно приобретенному, модулю на чипе ESP32-S3 и попыткам поднять на нем WiFi. В прошлой заметке я упомянул про API-шную функцию esp_netif_create_default_wifi_sta() и ее способность прибить приложение в случае какой-либо ошибки. Я не поленился и залез в описание данной функции:

esp_netif_t* esp_netif_create_default_wifi_sta(void)
{
    esp_netif_config_t cfg = ESP_NETIF_DEFAULT_WIFI_STA();
    esp_netif_t *netif = esp_netif_new(&cfg);
    assert(netif);
    ESP_ERROR_CHECK(esp_netif_attach_wifi_station(netif));
    ESP_ERROR_CHECK(esp_wifi_set_default_wifi_sta_handlers());
    return netif;
}

Вы серьезно? В функции API на 5 строк 3 ассерта? В смысле, что 1 assert и 2 подобных ему макроса? Я все понимаю, конечно, но на мой взгляд, это перебор. Еще ладно, нативный assert используется правильно. Ну в том плане, что он обрабатывает значение, возвращенной функцией, а не вызывает саму функцию. Но использование макроса ESP_ERROR_CHECK() за гранью добра и зла. Напомню, что ESP_ERROR_CHECK() — это assert на стероидах, то есть он обрабатывает int вместо bool и выдает расширенное описание ошибки. В остальном, его поведение полностью аналогично assert.

Да, assert — классная штука для обнаружения и локализации ошибок и здорово помогает программисту. Но! Его задача — обнаружение и локализация ошибок! В коде, который выпускается в продакшн их быть не должно!!! Тем более, в API-шном вызове! Тем более, при работе с периферией, которая сама по себе может работать нестабильно. Это априоре ставит крест на попытках написать отказоустойчивое приложение, работающее 24/7/366. Ну просто представьте ситуацию. Автопилот на автомобиле. Авто едет со скоростью 90 км в час. Впереди едет трактор с дисковой бороной в транспортном положении, которая выползает на половину соседних полос с каждой стороны. Нужно совершить обгон. Но предварительно, необходимо включить поворотник. И тут диагностика выдает, что одна из лампочек перегорела. Поворотник включить нельзя, значит нельзя совершать маневр, ведь правильно? Следовательно, это исключительная ситуация и нужно вызвать assert и убить приложение, отвечающее за управлением автомобилем. Ну а водителю, если так хочется жить, нужно успеть это сообразить и перезагрузить ЭБУ, чтобы вернуть контроль над машиной. Ведь классный сценарий, правда? Кажется, я начинаю понимать, что вызвало целую волну сообщений о том, что автомобили Tesla самопроизвольно выезжали на встречку или творили подобную дичь. Видимо, в их блоках управления было ПО, написанное подобно API ESP32…

Далее, я выскажу сугубо свое личное мнение.

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

Протоколирование ошибок обязано быть в функциях API. Причем, настраиваемое. От полного молчания, до пошагового сообщения итогов того или иного шага. Но гробить приложение по abort() по любому непонятному чиху функция API не имеет права!!!

В целом, на скору руку я бы переписал код многострадальной функции следующим образом:

esp_netif_t* esp_netif_create_default_wifi_sta(void)
{
    esp_netif_config_t cfg = ESP_NETIF_DEFAULT_WIFI_STA();
    esp_netif_t *netif = esp_netif_new(&cfg);
    if(!netif){
    	ESP_ERROR_CHECK_WITHOUT_ABORT(SMTH_ERR_CODE);
    	return netif;
    } else {
        int ret_code=esp_netif_attach_wifi_station(netif);
        if(ret_code!=ESP_OK){
        	ESP_ERROR_CHECK_WITHOUT_ABORT(ret_code);
        	return NULL;
        } else {
            	int ret_code=esp_wifi_set_default_wifi_sta_handlers();
            	if(ret_code!=ESP_OK){
            		ESP_ERROR_CHECK_WITHOUT_ABORT(ret_code);
            		return NULL;
            	}
        }
    }
    return netif;
}

Да, есть над чем подумать и что изменить, но в целом, сценарий именно такой. Да, выглядит громоздко. Но я решаю, что делать, если не удалось поднять WiFi. И для меня это совсем не повод, чтобы убивать приложение. Есть еще Bluetooth, IrDA и прочие способы достучаться до контроллера. Но это уже решать не авторам API.

P.S. Думаю, надо создать отдельную рубрику для ESP32.

PP.SS. Все, что я пишу — я пишу, в основном, для себя. Чтобы не забыть. Или, чтобы лучше осмыслить. Если кому-то это полезно, буду рад. Нет, так а я, собственно, ничего и не обещал никому…

Погружение в ESP32

Disclaimer. Я не профессиональный программист. Ну, когда-то приходилось кодить на asm-e, C/C++. Работал в 1С-франчайзинговой конторе, реально кодил в 1С. Но это времена далекой молодости. Сейчас, программирую от случая к случаю, для себя и в образовательных целях. Уровень — ну так, несложные олимпиадные задачи. Поэтому, в качестве программиста я являюсь любителем.

Итак, после того, как я научился раскрашивать on-board RGB-светодиод на ESP32-S3, пришло время более серьезных задач. Сразу скажу, что я решил использовать нативный ESP-IDF в связке с Espressif-IDE. Ну, я так решил, что родной SDK будет роднее и ближе.

В свое время, на Arduino мне сильно не хватало WiFi и прочих коммуникаций, поэтому, первое, за что я решил схватиться — это WiFi. Ну, сказано — сделано, полез в доки.

Где-то в недрах прочитал мысль, вкратце, по-русски, анализируйте ошибки как можно чаще. Ну что-ж, совет не лишен смысла, я сам придерживаюсь такого же мнения. Поэтому, первым делом, полез в раздел Error handling…

Раздел 4.12 Error Handling сообщает нам, что существует 2 типа ошибок: восстановимые (recoverable) и невосстановимые (unrecoverable). Ну, хорошо, согласен. Дальше идут примеры ошибок обоих типов. Так, к восстановимым ошибкам относятся ошибки, возвращаемые в виде кодов ошибок функциями или полученные в результате перехвата исключений в C++. А невосстановимые ошибки — это ошибки системного уровня, ошибки неправильных инструкций ЦП (с чем я согласен) и ошибки, вызванные ассертами или аналогичными методами! Стоп! То есть, к невосстановимым ошибкам относятся те, которые изначально запрограммированные, как невосстановимые, без учета их тяжести! И это уже начало напрягать, и как выяснилось, не зря!

Второе, что меня напрягло — это обилие макросов ESP_ERROR_CHECK() в примерах. Полез в доки посмореть, что за зверь:

ESP_ERROR_CHECK macro serves similar purpose as assert, except that it checks esp_err_t value rather
than a bool condition. If the argument of ESP_ERROR_CHECK is not equal ESP_OK, then an error message is
printed on the console, and abort() is called.

Стоп-стоп-стоп…. Что значит «бух в котел и там сварился…»? В том плане, что если аргумент не ESP_OK, то печатается сообщение об ошибке и затем вызывается abort()? А с какой целью, извините?

Постараюсь проиллюстрировать примером из туториала по WiFi:

void wifi_init_sta(void)
{
    s_wifi_event_group = xEventGroupCreate();

    ESP_ERROR_CHECK(esp_netif_init());

То есть, если по какой-то причине, не удалось инициализировать WiFi, то все приложение останавливается? Так?

Хорошо, придирчивый читатель скажет, что никто не заставляет использовать макрос ESP_ERROR_CHECK(), Во-вторых, есть макрос ESP_ERROR_CHECK_WITHOUT_ABORT().

Отлично, но давайте почитаем описание функции esp_netif_create_default_wifi_sta():

Creates default WIFI STA. In case of any init error this API aborts.
Note: The API creates esp_netif object with default WiFi station config, attaches the netif to wifi and registers
wifi handlers to the default event loop. This API uses assert() to check for potential errors, so it could abort
the program.
(Note that the default event loop needs to be created prior to calling this API)

Прекрасно! Просто великолепно! Если что-то пошло не так с инициализацией WiFi — гробим все приложение! То есть, вполне себе управляемую ситуацию неполадок с WiFi мы переводим в разряд невосстановимых ошибок просто используя assert()? Не потому, что это какая-то критическая ошибка, а потому, что мы так написали?

Эх, чувствую я, это только начало. Как бы не пришлось всю библиотеку перелопачивать, потому что так писать нельзя…

Новая игрушка

Пришла позавчера платка ESP32-S3. Игрушка забавная, но непростая. Вчера научился изменять цвет встроенного светодиода. Теперь надо научиться коннектиться к WiFi. SDK у нее непростой, мануал на 3 с гаком тысячи страниц. Это вам не Arduino. Но и возможностей поболе… В общем, задача №1 — собрать простенькую метеостанцию на BMP280 с регистрацией информации куда-нить в инет. Чтобы потом натравить нейросетку — пущай погоду предсказывает.

Первая запись

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

Также, на этой неделе запустил в колодце новый насос Водомет от Джилекс с гидроаккумулятором на 100 литров. По сравнению с насосной станцией — небо и земля. Не орет, как потерпевший, на все СНТ. Плюс пересадил два куста жимолости и три сливы.

Не успел — обработать деревяшки для нового компостера и загрунтовать железный каркас для него же….