ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
modem_component.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2#include "modem_component.h"
3#include "soc/soc_caps.h"
4#include <driver/uart.h>
5
8#include "esphome/core/log.h"
12
13#include <esp_event.h>
14#include <esp_netif.h>
15#include <esp_netif_ppp.h>
16
17#include <algorithm>
18#include <cinttypes>
19#include <cstring>
20#include <iterator>
21
22namespace esphome::modem {
23
24using esp_modem::command_result;
25using esp_modem::create_uart_dte;
26using esp_modem::DTE;
27using esp_modem::modem_mode;
28
29ESPHOME_LOG_TAG(TAG, "modem");
30
31#ifdef SOC_UART_HP_NUM
32static constexpr int HP_UART_COUNT = SOC_UART_HP_NUM;
33#else
34static constexpr int HP_UART_COUNT = UART_NUM_MAX;
35#endif
36
37ModemComponent *global_modem_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
38
39static constexpr uint32_t WAIT_IP_TIMEOUT_MS = 50000;
40static constexpr uint32_t PROBE_TIMEOUT_MS = 100;
41// A silent modem is not going to answer the next second, so its probes are spaced out, doubling up to a minute
42static constexpr uint32_t PROBE_RETRY_DELAY_MS = 5000;
43static constexpr uint32_t PROBE_RETRY_MAX_MS = 60000;
44// How long the task waits for the main loop to run on_sync before attaching
45static constexpr uint32_t SYNC_ACK_TIMEOUT_MS = 5000;
46// esp_modem's command strings and the log lines are built on this stack
47static constexpr uint32_t TASK_STACK_SIZE = 5120;
48#ifdef USE_MODEM_PIN_CODE
49// AT+CPIN= can take a few seconds; the SIM may need longer still before AT+CPIN? says READY
50static constexpr uint32_t PIN_TIMEOUT_MS = 5000;
51static constexpr uint32_t PIN_UNLOCK_WAIT_MS = 10000;
52// Sends of one pin_code the SIM has not confirmed with READY, across reboots, before it is not sent again
53static constexpr uint8_t PIN_MAX_UNCONFIRMED_SENDS = 2;
54#endif
55// The attachment is polled every 4 s; the signal and operator queries only every 4th poll
56static constexpr uint32_t ATTACH_POLL_MS = 4000;
57static constexpr uint8_t ATTACH_DIAG_EVERY = 4;
58static constexpr int RADIO_STATE_RF_OFF = 4; // CFUN=4: transmit and receive off, SIM still powered
59static constexpr uint8_t UART_EVENT_QUEUE_SIZE = 10;
60static constexpr uint32_t BAUD_PREF_HASH = 76007670UL;
61static constexpr uint32_t PIN_PREF_HASH = 76007671UL;
62static constexpr int FALLBACK_BAUD_RATE = 115200;
63
64// Indexed by ModemState
65PROGMEM_STRING_TABLE(ModemStateStrings, "DISABLED", "ENABLING", "SYNCED", "CONNECTING", "WAIT_IP", "CONNECTED",
66 "DISCONNECTING", "UNKNOWN");
67static const LogString *state_to_str(ModemState state) {
68 return ModemStateStrings::get_log_str(static_cast<uint8_t>(state), ModemStateStrings::LAST_INDEX);
69}
70
71// Indexed by ModemFault
72PROGMEM_STRING_TABLE(ModemFaultStrings, "", "Modem not responding", "Configured baud rate failed",
73 "init_at command failed", "SIM not ready", "SIM locked", "SIM PIN not sent; check pin_code",
74 "PIN record not saved", "APN or radio setup failed", "No network attachment", "PPP link failed",
75 "Modem teardown failed");
76
77// Indexed by esp_modem::command_result
78PROGMEM_STRING_TABLE(CommandResultStrings, "OK", "FAIL", "TIMEOUT");
79static const LogString *result_to_str(command_result result) {
80 return CommandResultStrings::get_log_str(static_cast<uint8_t>(result), CommandResultStrings::LAST_INDEX);
81}
82
83ModemComponent::ModemComponent(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, const char *model, DceFactory factory,
84 const char *apn)
85 : tx_pin_(tx_pin), rx_pin_(rx_pin), model_(model), dce_factory_(factory), apn_(apn) {
87}
88
90 ESP_LOGI(TAG, "Setting modem target state to CONNECTED");
92 this->probe_backoff_.store(0, std::memory_order_relaxed);
93 // A link that is already up has nothing to wait for; a lost one re-arms the timer itself
96 }
97 this->wake_task_();
98}
99
101 ESP_LOGI(TAG, "Setting modem target state to DISABLED");
104 this->wake_task_();
105}
106
108 if (this->task_.is_created()) {
109 xTaskNotifyGive(this->task_.get_handle());
110 }
111}
112
113// Self-keyed so it still fires after a failed setup, where the component's own timers are skipped
115 // Measures time without a connection, like wifi: neither retry cycles nor a repeated enable() restart it
116 if (this->timeout_ == 0 || this->connect_timeout_armed_) {
117 return;
118 }
119 this->connect_timeout_armed_ = true;
120 App.scheduler.set_timeout(this, this->timeout_, [this]() {
121 // The task may have connected between enable() checking the state and arming this timer
122 this->connect_timeout_armed_ = false;
123 if (this->is_connected()) {
124 return;
125 }
126 ESP_LOGE(TAG, "Modem was not able to connect (timeout), rebooting");
127 App.reboot();
128 });
129}
130
132 this->connect_timeout_armed_ = false;
133 App.scheduler.cancel_timeout(this);
134}
135
137 network::IPAddresses addresses;
138 esp_netif_ip_info_t ip;
139 if (this->is_connected() && esp_netif_get_ip_info(this->ppp_netif_, &ip) == ESP_OK) {
140 addresses[0] = network::IPAddress(&ip.ip);
141#if USE_NETWORK_IPV6
142 network::fill_ip6_addresses(this->ppp_netif_, addresses);
143#endif /* USE_NETWORK_IPV6 */
144 }
145 return addresses;
146}
147
148void ModemComponent::fail_setup_(const LogString *reason) {
149 this->mark_failed(reason);
151 this->arm_connect_timeout_();
152 }
153}
154
155// The status flags, the automations and the preferences belong to the main loop, so the task leaves the
156// work in pending_ and wakes loop()
158 this->pending_.fetch_or(pending, std::memory_order_release);
160}
161
162// The task is the only writer, so a load and a store do; a byte-sized read-modify-write is a library call on Xtensa
164 // A wrong baud_rate or init_at is kept apart, so it stays visible once the modem connects
166 this->config_fault_.store(fault, std::memory_order_relaxed);
167 }
168 if (this->fault_.load(std::memory_order_relaxed) != fault) {
169 this->fault_.store(fault, std::memory_order_relaxed);
171 }
172}
173
174// The fault that ended the session says more than a failed cleanup after it
176 if (this->fault_.load(std::memory_order_relaxed) == ModemFault::MODEM_FAULT_NONE) {
178 }
179}
180
182 if (this->config_fault_.load(std::memory_order_relaxed) == fault) {
183 this->config_fault_.store(ModemFault::MODEM_FAULT_NONE, std::memory_order_relaxed);
185 }
186}
187
189 ModemFault fault = this->fault_.load(std::memory_order_relaxed);
190#ifdef USE_MODEM_PIN_CODE
191 if (this->pin_record_unsaved_) {
193 }
194#endif
195 if (fault == ModemFault::MODEM_FAULT_NONE) {
196 fault = this->config_fault_.load(std::memory_order_relaxed);
197 }
198 if (fault == ModemFault::MODEM_FAULT_NONE) {
199 this->status_clear_warning();
200 this->status_clear_error();
201 return;
202 }
203 const LogString *message = ModemFaultStrings::get_log_str(static_cast<uint8_t>(fault), ModemFaultStrings::LAST_INDEX);
204 // These stop the modem until the SIM or pin_code is fixed
207 this->status_clear_warning();
208 this->status_set_error(message);
209 } else {
210 this->status_clear_error();
211 this->status_set_warning(message);
212 }
213}
214
216 const uint32_t pending = this->pending_.exchange(0, std::memory_order_acquire);
218 // The task waits for the sync hand-over, so a pending disconnect came before a pending sync; connected now
219 // means a pending disconnect also came before the pending connect
220 const bool disconnect_first =
221 (pending & PENDING_ON_DISCONNECT) && (this->is_connected() || (pending & PENDING_ON_SYNC));
222 if (disconnect_first) {
224 }
225 if (pending & PENDING_ON_SYNC) {
227 this->sync_acked_.store(1, std::memory_order_release); // on_sync finished, so the task may attach
228 this->wake_task_();
229 }
230 if (pending & PENDING_ON_CONNECT) {
231 this->dump_connect_params_();
233 }
234 if ((pending & PENDING_ON_DISCONNECT) && !disconnect_first) {
236 }
237 }
238 if (pending & PENDING_STATUS) {
239 this->publish_status_();
240 }
241 if (pending & PENDING_CONNECT_TIMEOUT) {
242 if (this->target_state_.load() == ModemState::MODEM_STATE_CONNECTED && !this->is_connected()) {
243 this->arm_connect_timeout_();
244 } else {
246 }
247 }
248 if ((pending & PENDING_SAVE_BAUD) && !this->pref_.save(&this->restored_baud_)) {
249 ESP_LOGW(TAG, "Failed to save the baud rate");
250 }
251#ifdef USE_MODEM_PIN_CODE
252 // Written through at once: a power cycle before the periodic flush would send the PIN again
253 if (pending & PENDING_SAVE_PIN) {
254 // The task leaves pin_record_ alone until pin_save_state_ reports the outcome
255 const bool saved = this->pin_pref_.save(&this->pin_record_) && global_preferences->sync();
256 if (!saved) {
257 ESP_LOGE(TAG, "Failed to save the PIN record");
258 }
259 // A power cycle could send a refused PIN again, so a failed save shows until a later one succeeds
260 if (this->pin_record_unsaved_ != !saved) {
261 this->pin_record_unsaved_ = !saved;
262 this->publish_status_();
263 }
264 this->pin_save_state_.store(saved ? PinSave::PIN_SAVE_DONE : PinSave::PIN_SAVE_FAILED, std::memory_order_release);
265 this->wake_task_();
266 }
267#endif
268#ifdef USE_MODEM_URC
269 if (this->urc_pending_.load(std::memory_order_acquire)) {
271 this->urc_pending_.store(0, std::memory_order_release);
272 }
273#endif
274 // A hand-over after these checks re-enables the loop through enable_loop_soon_any_context()
275 if (this->pending_.load(std::memory_order_acquire) == 0) {
276 this->disable_loop();
277 }
278}
279
281 this->pref_ = global_preferences->make_preference<int>(BAUD_PREF_HASH);
282 this->pref_.load(&this->restored_baud_);
283#ifdef USE_MODEM_PIN_CODE
284 this->pin_pref_ = global_preferences->make_preference<PinRecord>(PIN_PREF_HASH);
285 if (!this->pin_pref_.load(&this->pin_record_)) {
286 // Also the first boot with pin_code; after a flash fault the send count starts over
287 ESP_LOGD(TAG, "No PIN record stored");
288 }
289 // A changed pin_code starts with a clean record and may be sent again
290 const uint32_t pin_hash = fnv1_hash(this->pin_code_);
291 if (this->pin_record_.hash != pin_hash) {
292 this->pin_record_ = PinRecord{pin_hash, 0};
293 if (!this->pin_pref_.save(&this->pin_record_)) {
294 ESP_LOGW(TAG, "Failed to clear the PIN record");
295 }
296 }
297#else
298 // A build without pin_code forgets the count, so flashing once without it clears the record
299 {
300 auto pin_pref = global_preferences->make_preference<PinRecord>(PIN_PREF_HASH);
301 PinRecord record{};
302 if (pin_pref.load(&record) && record.hash != 0) {
303 record = PinRecord{};
304 if (!pin_pref.save(&record)) {
305 ESP_LOGW(TAG, "Failed to clear the PIN record");
306 }
307 }
308 }
309#endif
310
311 // Highest free high-performance port first; UART0 stays with the logger and the low-power UART of the
312 // C6/P4 has restricted pins and clocks
313 for (int p = HP_UART_COUNT - 1; p > UART_NUM_0; p--) {
314 if (!uart_is_driver_installed(static_cast<uart_port_t>(p))) {
315 this->uart_port_ = p;
316 break;
317 }
318 }
319 if (this->uart_port_ == UART_NUM_MAX) {
320 this->mark_failed(LOG_STR("No free UART port for modem"));
321 return;
322 }
323
324 esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP();
325 this->ppp_netif_ = esp_netif_new(&netif_ppp_config);
326 if (this->ppp_netif_ == nullptr) {
327 this->fail_setup_(LOG_STR("PPP netif creation failed"));
328 return;
329 }
330
331 auto ip_event = [](void *arg, esp_event_base_t, int32_t event_id, void *event_data) {
332 const bool got_ip = event_id == IP_EVENT_PPP_GOT_IP;
333 if (!got_ip && event_id != IP_EVENT_PPP_LOST_IP) {
334 return;
335 }
336 if (got_ip) {
337 ESP_LOGD(TAG, "PPP IP event: Got IP " IPSTR, IP2STR(&static_cast<ip_event_got_ip_t *>(event_data)->ip_info.ip));
338 } else {
339 ESP_LOGD(TAG, "PPP IP event: Lost IP");
340 }
341 auto *self = static_cast<ModemComponent *>(arg);
342 self->got_ip_.store(got_ip, std::memory_order_release);
343 self->wake_task_();
344 };
345 // A dead peer or an LCP failure is reported here, not as a lost IP event; esp_modem enables these
346 // error events itself when it attaches the netif
347 auto ppp_status = [](void *arg, esp_event_base_t, int32_t event_id, void *) {
348 if (event_id < NETIF_PP_PHASE_OFFSET && event_id != NETIF_PPP_ERRORNONE) {
349 ESP_LOGW(TAG, "PPP error %d", static_cast<int>(event_id));
350 auto *self = static_cast<ModemComponent *>(arg);
351 self->ppp_error_.store(static_cast<uint8_t>(event_id), std::memory_order_release);
352 self->got_ip_.store(0, std::memory_order_release);
353 self->wake_task_();
354 }
355 };
356 if (esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ip_event, this) != ESP_OK ||
357 esp_event_handler_register(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, ppp_status, this) != ESP_OK) {
358 this->fail_setup_(LOG_STR("Event handler register failed"));
359 return;
360 }
361
362 // The DTE and DCE live for the life of the program; a baud probe only changes the UART divisor
363 esp_modem_dte_config_t dte_config = ESP_MODEM_DTE_DEFAULT_CONFIG();
364 dte_config.uart_config.tx_io_num = this->tx_pin_->get_pin();
365 dte_config.uart_config.rx_io_num = this->rx_pin_->get_pin();
366 dte_config.uart_config.rx_buffer_size = this->rx_buffer_size_;
367 dte_config.uart_config.tx_buffer_size = this->tx_buffer_size_;
368 dte_config.uart_config.event_queue_size = UART_EVENT_QUEUE_SIZE;
369 dte_config.uart_config.port_num = static_cast<uart_port_t>(this->uart_port_);
370 // The rate the modem answered at last time comes first: after a baud_rate change the modem may still sit in
371 // CMUX at the old rate, which only that rate can detect; the configured rate is applied once synced
372 const int start_baud = this->restored_baud_ != 0 ? this->restored_baud_ : this->baud_rate_;
373 if (start_baud != 0) {
374 dte_config.uart_config.baud_rate = start_baud;
375 }
376 if (this->rts_pin_ != nullptr) {
377 dte_config.uart_config.rts_io_num = this->rts_pin_->get_pin();
378 dte_config.uart_config.cts_io_num = this->cts_pin_->get_pin();
379 dte_config.uart_config.flow_control = ESP_MODEM_FLOW_CONTROL_HW;
380 }
381 dte_config.dte_buffer_size = this->dte_buffer_size_;
382 this->current_baud_rate_ = dte_config.uart_config.baud_rate;
383 this->dte_ = create_uart_dte(&dte_config);
384 esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG(this->apn_);
385 if (this->dte_) {
386 this->dce_ = this->dce_factory_(&dce_config, this->dte_, this->ppp_netif_);
387 }
388 if (!this->dce_) {
389 this->fail_setup_(LOG_STR("Modem DTE/DCE creation failed"));
390 return;
391 }
392 // The pin schema's inverted flag is applied to the UART signals, as the uart component does
393 uint32_t invert = 0;
394 if (this->tx_pin_->is_inverted()) {
395 invert |= UART_SIGNAL_TXD_INV;
396 }
397 if (this->rx_pin_->is_inverted()) {
398 invert |= UART_SIGNAL_RXD_INV;
399 }
400 if (this->rts_pin_ != nullptr) {
401 if (this->rts_pin_->is_inverted()) {
402 invert |= UART_SIGNAL_RTS_INV;
403 }
404 if (this->cts_pin_->is_inverted()) {
405 invert |= UART_SIGNAL_CTS_INV;
406 }
407 }
408 if (invert != 0 && uart_set_line_inverse(static_cast<uart_port_t>(this->uart_port_), invert) != ESP_OK) {
409 this->fail_setup_(LOG_STR("UART line inversion failed"));
410 return;
411 }
412#ifdef USE_MODEM_URC
413 // Runs on the UART task. CMUX hands each frame over on its own, so lines are assembled here; every
414 // chunk is consumed and a complete line is handed to loop() in urc_line_.
415 this->dce_->set_enhanced_urc([this](const DTE::UrcBufferInfo &buffer_info) {
416 // The response parser owns the buffer while a command runs; URCs in that window are best effort,
417 // and a line started before it would otherwise be glued to the next one
418 if (buffer_info.is_command_active) {
419 this->urc_reader_.reset();
420 return DTE::UrcConsumeInfo{DTE::UrcConsumeResult::CONSUME_NONE, 0};
421 }
422 bool dropped = false;
423 for (size_t i = 0; i < buffer_info.new_data_size; i++) {
424 const uint8_t c = buffer_info.new_data_start[i];
425 if (this->urc_pending_.load(std::memory_order_acquire)) {
426 // The line loop() has not published yet lives in the reader's buffer, which a dropped line leaves alone
427 dropped |= c != '\r' && c != '\n';
429 }
430 const char *line = this->urc_reader_.feed(c);
432 if (line != nullptr && line[0] != '\0') {
433 this->urc_line_ = line;
434 this->urc_pending_.store(1, std::memory_order_release);
436 }
437 }
438 if (dropped) {
439 ESP_LOGW(TAG, "URC dropped; the previous one is not published yet");
440 }
441 return DTE::UrcConsumeInfo{DTE::UrcConsumeResult::CONSUME_ALL, 0};
442 });
443#endif
444 if (!this->task_.create(ModemComponent::task_entry, "modem", TASK_STACK_SIZE, this, 1, false)) {
445 this->fail_setup_(LOG_STR("Task creation failed"));
446 }
447}
448
450#ifdef USE_MODEM_PIN_CODE
451 const char *pin_code = LOG_STR_LITERAL("Yes (not shown)");
452#else
453 const char *pin_code = LOG_STR_LITERAL("No");
454#endif
455 ESP_LOGCONFIG(TAG,
456 "Modem:\n"
457 " Model: %s\n"
458 " APN: %s\n"
459 " PIN code: %s\n"
460 " TX Pin: GPIO%u\n"
461 " RX Pin: GPIO%u\n"
462 " UART port: %d\n"
463 " Baud rate: %d\n"
464 " TX buffer size: %u\n"
465 " RX buffer size: %u\n"
466 " DTE buffer size: %u\n"
467 " Reboot timeout: %" PRIu32 " ms\n"
468 " Enabled: %s",
469 this->model_, this->apn_, pin_code, this->tx_pin_->get_pin(), this->rx_pin_->get_pin(),
470 static_cast<int>(this->uart_port_), this->current_baud_rate_, this->tx_buffer_size_,
471 this->rx_buffer_size_, this->dte_buffer_size_, this->timeout_, YESNO(this->is_enabled()));
472 this->dump_connect_params_();
473}
474
475void ModemComponent::task_entry(void *arg) { static_cast<ModemComponent *>(arg)->run_task_(); }
476
478 while (true) {
479#ifdef USE_MODEM_SEND_AT
480 // A command from send_at() goes out between state steps, never in the middle of a sequence
481 if (this->at_pending_.load(std::memory_order_acquire)) {
482 this->at_(this->at_command_, this->at_timeout_);
483 this->at_pending_.store(0, std::memory_order_release);
484 }
485#endif
486 this->step_delay_ms_ = 0;
487 this->transition_to_(this->compute_next_state_());
488 // Nothing to do while disabled or connected: enable(), disable() and the PPP events wake the task
489 const bool parked = this->component_state_.load() == this->target_state_.load();
490 ulTaskNotifyTake(pdTRUE, parked ? portMAX_DELAY : pdMS_TO_TICKS(this->step_delay_ms_));
491 }
492}
493
495 const uint8_t backoff = this->probe_backoff_.load(std::memory_order_relaxed);
496 const uint32_t delay_ms = std::min(PROBE_RETRY_DELAY_MS << backoff, PROBE_RETRY_MAX_MS);
497 if (delay_ms < PROBE_RETRY_MAX_MS) {
498 this->probe_backoff_.store(backoff + 1, std::memory_order_relaxed);
499 }
500 return delay_ms;
501}
502
504 this->step_delay_ms_ = delay_ms;
505 return state;
506}
507
509 const ModemState state = this->component_state_.load();
513 }
516 }
517 }
518
519 switch (state) {
521#ifdef USE_MODEM_PIN_CODE
522 // A new attempt may send the PIN again; a counted send that has not gone out yet still does
523 if (this->pin_step_ == PinStep::PIN_STEP_SENT) {
525 }
526#endif
529 return this->handle_state_enabling_();
531 return this->handle_state_synced_();
533 return this->handle_state_connecting_();
535 return this->handle_state_wait_ip_();
537 if (this->got_ip_.load(std::memory_order_acquire)) {
539 }
540 ESP_LOGW(TAG, "Lost IP; PPP error %u", this->ppp_error_.load(std::memory_order_acquire));
541 this->ppp_error_.store(0, std::memory_order_release);
545 return this->handle_state_disconnecting_();
546 }
547 return state;
548}
549
551 // Find the baud rate the modem answers at and leave it in command mode, synced
552 if (!this->dce_->set_mode(modem_mode::AUTODETECT)) {
553 // A failed detection keeps the previous mode, which would skip the baud rate search below
554 this->dce_->set_mode(modem_mode::UNDEF);
555 }
556 auto mode = this->dce_->get_mode();
557 ESP_LOGD(TAG, "Autodetect mode: %d", static_cast<int>(mode));
558
559 // A detection proves the modem answers; leaving CMUX or data mode has to be checked with a sync
560 bool exited = true;
561 switch (mode) {
562 case modem_mode::CMUX_MANUAL_MODE:
563 // A data session survived the ESP reboot; the radio off step below ends it
564 ESP_LOGD(TAG, "Modem warm reboot. Exiting CMUX");
565 if (!this->dce_->set_mode(modem_mode::CMUX_MANUAL_EXIT)) {
566 ESP_LOGW(TAG, "Failed to exit CMUX");
567 }
568 break;
569
570 case modem_mode::UNDEF: {
571 // The detection above probed the current rate; the other known rates are tried in this pass
572 const int detected = this->current_baud_rate_;
573 exited = false;
574 const int bauds[] = {this->restored_baud_, this->baud_rate_, FALLBACK_BAUD_RATE};
575 bool found = false;
576 bool probed = false;
577 bool rejected = false;
578 for (size_t i = 0; i != std::size(bauds) && !found; i++) {
579 const int baud = bauds[i];
580 if (baud == 0 || baud == detected || std::find(bauds, bauds + i, baud) != bauds + i) {
581 continue;
582 }
583 if (uart_set_baudrate(static_cast<uart_port_t>(this->uart_port_), baud) != ESP_OK) {
584 ESP_LOGW(TAG, "UART rejected baud rate %d", baud);
586 rejected = true;
587 continue;
588 }
589 this->current_baud_rate_ = baud;
590 probed = true;
591 std::string output;
592 found = this->dce_->at("AT", output, PROBE_TIMEOUT_MS) == command_result::OK;
593 if (!found) {
594 this->not_responding_(baud);
595 }
596 }
597 if (!found) {
598 // The detection at the current rate already failed; a UART that refused every other rate set BAUD
599 if (!probed && !rejected) {
600 this->not_responding_(detected);
601 }
603 }
604 ESP_LOGD(TAG, "Modem responded at baud %d", this->current_baud_rate_);
605 break;
606 }
607
608 case modem_mode::COMMAND_MODE:
609 exited = false;
610 break;
611
612 case modem_mode::DATA_MODE:
613 // Can block 20 s; esp_modem offers no shorter exit
614 ESP_LOGW(TAG, "Exiting DATA_MODE");
615 if (!this->dce_->set_mode(modem_mode::COMMAND_MODE)) {
616 ESP_LOGW(TAG, "Failed to leave data mode");
617 }
618 break;
619
620 default:
621 ESP_LOGW(TAG, "Modem in unexpected mode %d. Switching to COMMAND mode.", static_cast<int>(mode));
622 if (!this->dce_->set_mode(modem_mode::COMMAND_MODE)) {
623 ESP_LOGW(TAG, "Failed to switch to command mode");
624 }
625 this->dce_->set_mode(modem_mode::UNDEF);
626 break;
627 }
628
629 if (exited && this->dce_->sync() != command_result::OK) {
630 // The detection proved the modem answers, so the CMUX or data mode exit is what failed; the exit can block
631 // for 20 s, so it backs off like the probe
632 ESP_LOGW(TAG, "Modem did not return to command mode");
635 }
636 this->probe_backoff_.store(0, std::memory_order_relaxed);
637 // esp_modem keeps the last line before OK as the reply, which can be the echoed command
638 if (this->dce_->set_echo(false) != command_result::OK) {
639 ESP_LOGW(TAG, "Failed to disable command echo");
640 }
641
642 int radio_state = 0;
643 if (this->dce_->get_radio_state(radio_state) != command_result::OK) {
644 ESP_LOGW(TAG, "Failed to read the radio state; skipping the radio off step");
645 } else if (radio_state != RADIO_STATE_RF_OFF) {
646 // Drops a session left by a warm reboot; unlike CFUN=0, which powers the SIM down on the SIM7600, CFUN=4
647 // keeps the SIM readable for the PIN and the APN. A modem that refuses is used as it is.
648 if (this->dce_->set_radio_state(RADIO_STATE_RF_OFF) != command_result::OK) {
649 ESP_LOGW(TAG, "Failed to turn the radio off");
650 }
651 }
652
653 if (this->baud_rate_ != 0 && this->baud_rate_ != this->current_baud_rate_) {
654 ESP_LOGD(TAG, "Setting baud rate: %d -> %d", this->current_baud_rate_, this->baud_rate_);
655 if (this->dce_->set_baud(this->baud_rate_) == command_result::OK) {
656 delay(200); // NOLINT
657 if (uart_set_baudrate(static_cast<uart_port_t>(this->uart_port_), this->baud_rate_) != ESP_OK) {
658 ESP_LOGE(TAG, "UART rejected baud rate %d", this->baud_rate_);
661 }
662 this->current_baud_rate_ = this->baud_rate_;
663 if (this->dce_->sync() != command_result::OK) {
664 ESP_LOGE(TAG, "Failed to sync modem at baud rate %d.", this->baud_rate_);
667 }
668 ESP_LOGI(TAG, "Modem synced at baud rate %d.", this->current_baud_rate_);
669 } else {
670 // The modem still answers at the probed rate, so a rejected rate is not worth a retry loop
671 ESP_LOGW(TAG, "Modem rejected baud rate %d. Staying at %d.", this->baud_rate_, this->current_baud_rate_);
673 }
674 }
675
676 // The rate in use is the wanted one, so a baud fault from this or an earlier attempt no longer applies
677 if (this->baud_rate_ == 0 || this->baud_rate_ == this->current_baud_rate_) {
679 }
680 if (this->restored_baud_ != this->current_baud_rate_) {
683 }
684
685 // The DTE side was set up with the UART; the modem side has to be asked for RTS/CTS as well
686 if (this->rts_pin_ != nullptr && this->dce_->set_flow_control(2, 2) != command_result::OK) {
687 ESP_LOGW(TAG, "Modem refused hardware flow control; UART overruns are possible");
688 }
689 this->modem_synced_ = true;
691}
692
694 // transition_to_() gave on_sync its head start; a command it queued went out at the top of this pass
695 bool failed = false;
696 for (uint8_t i = 0; i != this->init_at_count_; i++) {
697 failed |= this->at_(this->init_at_commands_[i]) != command_result::OK; // at_() logs the failure
698 }
699 if (failed) {
701 } else {
703 }
706}
707
710 // ENABLING just synced; only a failed attachment read comes back here unsure of the modem
711 if (this->resync_) {
712 this->resync_ = false;
713 if (this->dce_->sync() != command_result::OK) {
714 ESP_LOGW(TAG, "Modem not synced during network init");
716 }
717 }
718 // ENABLING left the radio at CFUN=4, where the SIM answers but the modem cannot attach yet
719 switch (this->prepare_sim_()) {
721 break;
725 // A reboot would send nothing more, so the reboot timer is cancelled
730 // Retrying now could burn the SIM's PIN attempts; the reboot timer stays armed, since a reboot may
731 // still open the SIM
734 }
735 // The APN has to be in the PDP context before the radio comes on and the modem attaches; esp_modem only
736 // sends it when PPP starts
737 esp_modem::PdpContext pdp(this->apn_);
738 if (this->dce_->set_pdp_context(pdp) != command_result::OK) {
739 ESP_LOGW(TAG, "Failed to set the APN");
742 }
743 if (this->dce_->set_radio_state(1) != command_result::OK) {
744 ESP_LOGW(TAG, "Failed to turn the radio on");
747 }
749 this->attach_polls_ = 0;
750 }
751
753 // A modem still searching for a network refuses this; the attachment poll asks again
754 if (this->dce_->set_network_attachment_state(1) != command_result::OK) {
755 ESP_LOGD(TAG, "Network attachment request refused");
756 }
758 }
759
760 int attached = 0;
761 if (this->dce_->get_network_attachment_state(attached) != command_result::OK) {
762 ESP_LOGW(TAG, "Failed to read the network attachment state");
764 this->connect_step_ = ConnectStep::CONNECT_STEP_START; // start over with a sync on the next pass
765 this->resync_ = true;
767 }
768 if (!attached) {
769 if (this->attach_polls_++ % ATTACH_DIAG_EVERY == 0) {
770 int rssi = 99, ber = 99, registration = -1, mode = -1; // -1 stays when the query fails
771 this->dce_->get_network_registration_state(registration);
772 this->dce_->get_signal_quality(rssi, ber);
773 this->dce_->get_network_system_mode(mode);
774 ESP_LOGW(TAG, "Modem not attached yet: registration %d, network type %d, CSQ %d, BER %d", registration, mode,
775 rssi, ber);
776 this->connect_step_ = ConnectStep::CONNECT_STEP_RADIO_ON; // request the attachment again
777 }
779 return this->retry_(ModemState::MODEM_STATE_CONNECTING, ATTACH_POLL_MS);
780 }
781
782 ESP_LOGI(TAG, "Modem initialized and ready, starting PPP");
783 // A late event from the last session must not count
784 this->got_ip_.store(0, std::memory_order_release);
785 this->ppp_error_.store(0, std::memory_order_release);
786 if (!this->dce_->set_mode(modem_mode::CMUX_MODE)) {
787 ESP_LOGE(TAG, "Failed to enter PPP. Resetting modem.");
790 }
791 this->state_started_ms_ = millis();
793}
794
796 if (this->got_ip_.load(std::memory_order_acquire)) {
799 }
800 // A PPP failure (wrong APN, authentication, dead peer) ends the wait at once; the event logged the code
801 if (this->ppp_error_.load(std::memory_order_acquire) != 0) {
802 this->ppp_error_.store(0, std::memory_order_release);
805 }
806 const uint32_t elapsed = millis() - this->state_started_ms_;
807 if (elapsed < WAIT_IP_TIMEOUT_MS) {
808 // The IP event wakes the task sooner
809 return this->retry_(ModemState::MODEM_STATE_WAIT_IP, WAIT_IP_TIMEOUT_MS - elapsed);
810 }
811 ESP_LOGE(TAG, "Unable to get IP address");
814}
815
817 // Each teardown command holds the task for up to 20 s, so a modem that never answered is skipped
818 if (!this->modem_synced_) {
820 }
821 this->modem_synced_ = false;
822 // COMMAND_MODE stops PPP and leaves CMUX; the manual exit only applies to a warm reboot
823 if (this->dce_->get_mode() != modem_mode::COMMAND_MODE && !this->dce_->set_mode(modem_mode::COMMAND_MODE)) {
824 // The next enable() probes the mode again and retries the exit; the fault keeps this visible
825 ESP_LOGW(TAG, "Failed to leave data mode");
826 this->set_teardown_fault_();
827 }
828 // Turn the radio off; waiting for the lost IP event could take up to 2 min. A reconnect keeps the SIM powered
829 // so it is not unlocked again; only disable() powers it down.
830 const bool reconnect = this->target_state_.load() == ModemState::MODEM_STATE_CONNECTED;
831 if (this->dce_->set_radio_state(reconnect ? RADIO_STATE_RF_OFF : 0) == command_result::OK) {
832 ESP_LOGI(TAG, "Radio off");
833 } else {
834 ESP_LOGW(TAG, "Radio off failed");
835 this->set_teardown_fault_();
836 }
838}
839
841 const ModemState state = this->component_state_.load();
842 if (next_state == state) {
843 return;
844 }
846 ESP_LOGD(TAG, "State change: %s -> %s", LOG_STR_ARG(state_to_str(state)), LOG_STR_ARG(state_to_str(next_state)));
847 this->component_state_.store(next_state);
848 // The automations run on the main loop
849 if (next_state == ModemState::MODEM_STATE_SYNCED) {
850 // on_sync may queue an AT command or disable the modem; it gets a bounded head start, and loop() wakes the
851 // task once it ran. An earlier enable() or a send_at() inside on_sync also notifies, so the flag decides.
852 this->sync_acked_.store(0, std::memory_order_release);
854 const uint32_t started = millis();
855 while (this->sync_acked_.load(std::memory_order_acquire) == 0) {
856 const uint32_t elapsed = millis() - started;
857 if (elapsed >= SYNC_ACK_TIMEOUT_MS) {
858 ESP_LOGW(TAG, "on_sync did not run within %u ms; continuing", static_cast<unsigned>(SYNC_ACK_TIMEOUT_MS));
859 break;
860 }
861 ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(SYNC_ACK_TIMEOUT_MS - elapsed));
862 }
863 } else if (next_state == ModemState::MODEM_STATE_CONNECTED) {
865 } else if (next_state == ModemState::MODEM_STATE_DISCONNECTING) {
866 // The lost IP event can lag by minutes; a quick re-enable must wait for a fresh address
867 this->got_ip_.store(0, std::memory_order_release);
868 this->step_delay_ms_ = COMMAND_TIMEOUT_MS; // let the link settle before the first pass
869 // on_disconnect pairs with on_connect, so a failed attempt that never connected does not fire it; a lost
870 // connection starts the reconnect watchdog again
871 if (lost) {
873 }
874 }
875}
876
878 ESP_LOGW(TAG, "Modem not responding at %d baud. Check power, PWRKEY and TX/RX wiring.", baud);
880}
881
882#ifdef USE_MODEM_SEND_AT
883bool ModemComponent::send_at(const std::string &cmd, uint32_t timeout) { return this->send_at(cmd.c_str(), timeout); }
884
885bool ModemComponent::send_at(const char *cmd, uint32_t timeout) {
886 if (!this->task_.is_created()) {
887 ESP_LOGW(TAG, "Modem not ready, cannot send AT command: %s", cmd);
888 return false;
889 }
890 if (this->at_pending_.load(std::memory_order_acquire)) {
891 ESP_LOGW(TAG, "AT command '%s' dropped; the previous one is not sent yet", cmd);
892 return false;
893 }
894 const size_t len = strlen(cmd);
895 if (len >= sizeof(this->at_command_)) {
896 ESP_LOGW(TAG, "AT command longer than %u bytes dropped", static_cast<unsigned>(sizeof(this->at_command_) - 1));
897 return false;
898 }
899 memcpy(this->at_command_, cmd, len + 1);
900 this->at_timeout_ = timeout;
901 this->at_pending_.store(1, std::memory_order_release);
902 this->wake_task_();
903 return true;
904}
905
906#endif
907
908command_result ModemComponent::at_(const char *cmd, uint32_t timeout) {
909 std::string output;
910 const command_result result = this->dce_->at(cmd, output, timeout);
911 if (result == command_result::OK) {
912 ESP_LOGI(TAG, "AT '%s' -> OK: %s", cmd, output.c_str());
913 } else {
914 ESP_LOGW(TAG, "AT '%s' -> %s: %s", cmd, LOG_STR_ARG(result_to_str(result)), output.c_str());
915 }
916 return result;
917}
918
919// esp_modem's read_pin() takes NOT READY for READY and SIM PIN2 for the SIM PIN, so the status is read here
921#ifdef USE_MODEM_PIN_CODE
922 // Every write to pin_record_ happens below, so waiting here keeps the task off it while loop() saves it
923 if (this->pin_save_state_.load(std::memory_order_acquire) == PinSave::PIN_SAVE_PENDING) {
924 return SimResult::SIM_RESULT_RETRY; // the save wakes the task
925 }
926#endif
927 std::string output;
928 if (this->dce_->at("AT+CPIN?", output, COMMAND_TIMEOUT_MS) != command_result::OK) {
929 // A SIM that is still starting answers with an error for a second or so
930 ESP_LOGD(TAG, "SIM status not readable yet: %s", output.c_str());
933 }
934 // Anything else, such as an echoed command, is not a status and must not reach the lock checks below
935 const char *found = strstr(output.c_str(), "+CPIN:");
936 if (found != nullptr) {
937 found += sizeof("+CPIN:") - 1;
938 while (*found == ' ') {
939 found++;
940 }
941 }
942 const StringRef status = found != nullptr ? StringRef(found) : StringRef();
943 if (status.empty()) {
944 ESP_LOGD(TAG, "SIM status not reported yet: %s", output.c_str());
947 }
948 ESP_LOGD(TAG, "SIM: %s", status.c_str());
949
950 // A prefix match: "NOT READY" contains READY but does not start with it
951 if (status.starts_with("READY")) {
952#ifdef USE_MODEM_PIN_CODE
953 this->pin_step_ = PinStep::PIN_STEP_IDLE; // proven correct, so a modem self-reset may send it again
954 if (this->pin_record_.sends != 0) {
955 this->pin_record_.sends = 0;
956 this->save_pin_record_();
957 }
958#endif
960 }
961 if (strstr(status.c_str(), "PUK") != nullptr) {
962 ESP_LOGE(TAG, "SIM is PUK locked; unlock it in a phone");
965 }
966 // Only the plain SIM PIN takes pin_code; SIM PIN2 and the PH-* locks need other credentials
967 if (!status.starts_with("SIM PIN") || status.starts_with("SIM PIN2")) {
968 if (strstr(status.c_str(), "PIN") != nullptr) {
969 ESP_LOGE(TAG, "SIM lock %s needs a credential pin_code cannot provide", status.c_str());
972 }
973 ESP_LOGW(TAG, "SIM not ready: %s", status.c_str()); // e.g. NOT READY or NOT INSERTED
976 }
977#ifdef USE_MODEM_PIN_CODE
978 return this->send_pin_();
979#else
980 ESP_LOGE(TAG, "SIM requires a PIN but pin_code is not set");
983#endif
984}
985
986#ifdef USE_MODEM_PIN_CODE
988 this->pin_save_state_.store(PinSave::PIN_SAVE_PENDING, std::memory_order_release);
990}
991
992// Each send is counted in flash before the PIN goes out, so reboots cannot keep spending the SIM's attempts on a
993// PIN it never confirmed; READY clears the count and a refused send uses it up
995 switch (this->pin_step_) {
997 if (this->pin_record_.sends >= PIN_MAX_UNCONFIRMED_SENDS) {
998 ESP_LOGE(TAG, "The SIM never confirmed this pin_code; not sending it again");
1001 }
1002 this->pin_record_.sends++;
1004 this->save_pin_record_();
1006
1008 // prepare_sim_() waited for the outcome
1009 if (this->pin_save_state_.load(std::memory_order_acquire) != PinSave::PIN_SAVE_DONE) {
1010 ESP_LOGE(TAG, "The PIN attempt could not be recorded; not sending it");
1011 this->pin_record_.sends--;
1015 }
1017 this->state_started_ms_ = millis();
1018 std::string cmd = "AT+CPIN=\"";
1019 cmd += this->pin_code_;
1020 cmd += '"';
1021 if (this->at_(cmd.c_str(), PIN_TIMEOUT_MS) == command_result::FAIL) {
1022 // A blind resend could PUK-lock the SIM, so a refusal uses up the count until pin_code changes
1023 ESP_LOGE(TAG, "The SIM refused the PIN; check pin_code");
1024 this->pin_record_.sends = PIN_MAX_UNCONFIRMED_SENDS;
1025 this->save_pin_record_();
1028 }
1029 return SimResult::SIM_RESULT_RETRY; // AT+CPIN? confirms READY on the next pass
1030 }
1031
1033 // Some SIMs take seconds to unlock, so AT+CPIN? is asked again for a while
1034 if (millis() - this->state_started_ms_ < PIN_UNLOCK_WAIT_MS) {
1036 }
1037 ESP_LOGE(TAG, "SIM still locked after sending the PIN; check the SIM");
1038 if (this->pin_record_.sends >= PIN_MAX_UNCONFIRMED_SENDS) {
1041 }
1042 // This attempt stops rather than sending the PIN blind again; a reboot sends it once more
1045 }
1047}
1048#endif
1049
1051 esp_netif_ip_info_t ip;
1052 if (!this->got_ip_.load(std::memory_order_acquire) || esp_netif_get_ip_info(this->ppp_netif_, &ip) != ESP_OK) {
1053 ESP_LOGCONFIG(TAG, " Connection: not connected");
1054 return;
1055 }
1056 auto dns_str = [this](esp_netif_dns_type_t type, char *buf) -> const char * {
1057 esp_netif_dns_info_t dns{};
1058 if (esp_netif_get_dns_info(this->ppp_netif_, type, &dns) != ESP_OK) {
1059 return LOG_STR_LITERAL("unavailable");
1060 }
1061 return network::IPAddress(&dns.ip.u_addr.ip4).str_to(buf);
1062 };
1063 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
1064 char netmask_buf[network::IP_ADDRESS_BUFFER_SIZE];
1065 char gw_buf[network::IP_ADDRESS_BUFFER_SIZE];
1066 char dns_main_buf[network::IP_ADDRESS_BUFFER_SIZE];
1067 char dns_backup_buf[network::IP_ADDRESS_BUFFER_SIZE];
1068 ESP_LOGCONFIG(TAG,
1069 " Connection:\n"
1070 " IP Address: %s\n"
1071 " Hostname: '%s'\n"
1072 " Subnet: %s\n"
1073 " Gateway: %s\n"
1074 " DNS main: %s\n"
1075 " DNS backup: %s",
1076 network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
1077 network::IPAddress(&ip.netmask).str_to(netmask_buf), network::IPAddress(&ip.gw).str_to(gw_buf),
1078 dns_str(ESP_NETIF_DNS_MAIN, dns_main_buf), dns_str(ESP_NETIF_DNS_BACKUP, dns_backup_buf));
1079}
1080
1081} // namespace esphome::modem
1082
1083#endif
BedjetMode mode
BedJet operating mode.
uint8_t status
Definition bl0942.h:8
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
void mark_failed()
Mark this component as failed.
void status_clear_error()
Definition component.h:302
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:296
char * feed(uint8_t c)
Feed one byte; returns the completed line (empty lines included), valid until the next call,...
Definition line_reader.h:23
void reset()
Forget the partial line and start a new one with the next byte.
Definition line_reader.h:76
void log_if_overflowed(const char *tag) const
Log the shared "line dropped" warning when the last feed() overflowed.
Definition line_reader.h:69
void discard_line()
Drop everything up to and including the next terminator.
Definition line_reader.h:85
bool is_created() const
Check if the task has been created and not yet destroyed.
Definition static_task.h:28
TaskHandle_t get_handle() const
Get the FreeRTOS task handle.
Definition static_task.h:31
bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, bool use_psram, BaseType_t core_id=tskNO_AFFINITY)
Allocate the TCB (always internal RAM) and the stack, then create the task.
StringRef is a reference to a string owned by something else.
Definition string_ref.h:31
constexpr const char * c_str() const
Definition string_ref.h:78
bool starts_with(const StringRef &prefix) const
True if the view begins with the given prefix (std::string::starts_with-like)
Definition string_ref.h:85
std::shared_ptr< esp_modem::DTE > dte_
std::atomic< ModemState > component_state_
std::atomic< uint8_t > ppp_error_
bool send_at(const char *cmd, uint32_t timeout=COMMAND_TIMEOUT_MS)
Queue an AT command for the modem task, which sends it at its next pass and logs the result and respo...
char at_command_[AT_COMMAND_SIZE]
network::IPAddresses get_ip_addresses()
void hand_over_(uint32_t pending)
std::atomic< ModemFault > fault_
std::atomic< ModemFault > config_fault_
ModemComponent(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, const char *model, DceFactory factory, const char *apn)
LineReader< URC_LINE_SIZE > urc_reader_
const char *const * init_at_commands_
uint32_t next_probe_delay_()
Pause the task before the next pass, then go to state.
std::atomic< uint8_t > at_pending_
std::unique_ptr< esp_modem::DCE > dce_
std::atomic< ModemState > target_state_
std::atomic< uint8_t > urc_pending_
LazyCallbackManager< void(ModemState)> on_state_callback_
void transition_to_(ModemState next_state)
void clear_config_fault_(ModemFault fault)
static void task_entry(void *arg)
std::atomic< uint8_t > got_ip_
void set_fault_(ModemFault fault)
std::atomic< uint8_t > sync_acked_
std::atomic< PinSave > pin_save_state_
void fail_setup_(const LogString *reason)
std::atomic< uint32_t > pending_
esp_modem::command_result at_(const char *cmd, uint32_t timeout=COMMAND_TIMEOUT_MS)
Send an AT command from the task and log the result; blocks for up to the timeout.
ModemState retry_(ModemState state, uint32_t delay_ms=RETRY_DELAY_MS)
text_sensor::TextSensor * urc_text_sensor_
std::atomic< uint8_t > probe_backoff_
static constexpr uint32_t COMMAND_TIMEOUT_MS
void publish_state(const std::string &state)
Definition text_sensor.h:42
const LogString * message
Definition component.cpp:35
uint8_t type
bool state
Definition fan.h:2
ModemComponent * global_modem_component
PROGMEM_STRING_TABLE(ModemStateStrings, "DISABLED", "ENABLING", "SYNCED", "CONNECTING", "WAIT_IP", "CONNECTED", "DISCONNECTING", "UNKNOWN")
std::unique_ptr< esp_modem::DCE >(*)(const esp_modem::dce_config *, std::shared_ptr< esp_modem::DTE >, esp_netif_t *) DceFactory
ESPHOME_LOG_TAG(TAG, "modem")
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:369
void fill_ip6_addresses(esp_netif_t *netif, IPAddresses &addresses)
Copies the IPv6 addresses of an esp_netif into addresses[1..], warning once when some do not fit.
Definition util.cpp:13
const void size_t len
Definition hal.h:68
ESPPreferences * global_preferences
uint32_t fnv1_hash(const char *str)
Calculate a FNV-1 hash of str.
Definition helpers.cpp:169
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:33
char * str_to(char *buf) const
Definition ip_address.h:153
uint8_t event_id
Definition tt21100.cpp:3