mirror of
https://github.com/esphome/esphome.git
synced 2026-08-20 21:30:57 +08:00
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
384 lines
12 KiB
C++
384 lines
12 KiB
C++
#include "ethernet_component.h"
|
|
|
|
#if defined(USE_ETHERNET) && defined(USE_RP2)
|
|
|
|
#include "esphome/core/application.h"
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/core/log.h"
|
|
|
|
#include "esphome/components/rp2/gpio.h"
|
|
|
|
#include <SPI.h>
|
|
#include <lwip/dns.h>
|
|
#include <lwip/netif.h>
|
|
|
|
namespace esphome::ethernet {
|
|
|
|
static const char *const TAG = "ethernet";
|
|
|
|
void EthernetComponent::setup() {
|
|
// Configure SPI pins
|
|
#if !defined(USE_ETHERNET_W6300)
|
|
SPI.setRX(this->miso_pin_);
|
|
SPI.setTX(this->mosi_pin_);
|
|
SPI.setSCK(this->clk_pin_);
|
|
#endif
|
|
// W6300 uses PIO QSPI with hardcoded pins, not Arduino SPI.
|
|
// SPI pin config is skipped; Wiznet6300lwIPFixed (needsSPI()=false)
|
|
// prevents LwipIntfDev::begin() from calling SPI.begin().
|
|
|
|
// Toggle reset pin if configured
|
|
if (this->reset_pin_ >= 0) {
|
|
rp2::RP2GPIOPin reset_pin;
|
|
reset_pin.set_pin(this->reset_pin_);
|
|
reset_pin.set_flags(gpio::FLAG_OUTPUT);
|
|
reset_pin.setup();
|
|
reset_pin.digital_write(false);
|
|
delay(1); // NOLINT
|
|
reset_pin.digital_write(true);
|
|
// W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms
|
|
delay(RESET_DELAY_MS); // NOLINT
|
|
}
|
|
|
|
// Create the SPI Ethernet device instance
|
|
#if defined(USE_ETHERNET_W5500)
|
|
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
|
|
#elif defined(USE_ETHERNET_W5100)
|
|
this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
|
|
#elif defined(USE_ETHERNET_W6100)
|
|
this->eth_ = new Wiznet6100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
|
|
#elif defined(USE_ETHERNET_W6300)
|
|
this->eth_ = new Wiznet6300lwIPFixed(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
|
|
#elif defined(USE_ETHERNET_ENC28J60)
|
|
this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
|
|
#endif
|
|
|
|
// Set hostname before begin() so the LWIP netif gets it
|
|
this->eth_->hostname(App.get_name().c_str());
|
|
|
|
// Configure static IP if set (must be done before begin())
|
|
#ifdef USE_ETHERNET_MANUAL_IP
|
|
if (this->manual_ip_.has_value()) {
|
|
IPAddress ip(this->manual_ip_->static_ip);
|
|
IPAddress gateway(this->manual_ip_->gateway);
|
|
IPAddress subnet(this->manual_ip_->subnet);
|
|
IPAddress dns1(this->manual_ip_->dns1);
|
|
IPAddress dns2(this->manual_ip_->dns2);
|
|
this->eth_->config(ip, gateway, subnet, dns1, dns2);
|
|
}
|
|
#endif
|
|
|
|
// Begin with fixed MAC or auto-generated
|
|
bool success;
|
|
if (this->fixed_mac_.has_value()) {
|
|
success = this->eth_->begin(this->fixed_mac_->data());
|
|
} else {
|
|
success = this->eth_->begin();
|
|
}
|
|
|
|
if (!success) {
|
|
ESP_LOGE(TAG, "Failed to initialize Ethernet");
|
|
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
|
|
this->eth_ = nullptr;
|
|
this->mark_failed();
|
|
return;
|
|
}
|
|
|
|
// Make this the default interface for routing
|
|
this->eth_->setDefault(true);
|
|
|
|
// The arduino-pico LwipIntfDev automatically handles packet processing
|
|
// via __addEthernetPacketHandler when no interrupt pin is used,
|
|
// or via GPIO interrupt when one is provided.
|
|
|
|
// Don't set started_ here — let the link polling in loop() set it
|
|
// when the link is actually up. Setting it prematurely causes
|
|
// a "Starting → Stopped → Starting" log sequence because the chip
|
|
// needs time after begin() before the PHY link is ready.
|
|
}
|
|
|
|
void EthernetComponent::loop() {
|
|
// On RP2040, we need to poll connection state since there are no events.
|
|
const uint32_t now = App.get_loop_component_start_time();
|
|
|
|
// Throttle link/IP polling to avoid excessive SPI transactions.
|
|
// W5500/ENC28J60 read PHY register via SPI on every linkStatus() call.
|
|
// W5100 can't detect link state, so we skip the SPI read and assume link-up.
|
|
// connected() reads netif->ip_addr without LwIPLock, but this is a single
|
|
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
|
|
// value, which is benign for polling.
|
|
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
|
|
this->last_link_check_ = now;
|
|
#if defined(USE_ETHERNET_W5100)
|
|
// W5100 can't detect link (isLinkDetectable() returns false), so linkStatus()
|
|
// returns Unknown — assume link is up after successful begin()
|
|
bool link_up = true;
|
|
#else
|
|
bool link_up = this->eth_->linkStatus() == LinkON;
|
|
#endif
|
|
bool has_ip = this->eth_->connected();
|
|
|
|
if (!link_up) {
|
|
if (this->started_) {
|
|
this->started_ = false;
|
|
this->connected_ = false;
|
|
}
|
|
} else {
|
|
if (!this->started_) {
|
|
this->started_ = true;
|
|
}
|
|
bool was_connected = this->connected_;
|
|
this->connected_ = has_ip;
|
|
if (this->connected_ && !was_connected) {
|
|
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
|
this->notify_ip_state_listeners_();
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// State machine
|
|
switch (this->state_) {
|
|
case EthernetComponentState::STOPPED:
|
|
if (this->started_) {
|
|
ESP_LOGI(TAG, "Starting connection");
|
|
this->state_ = EthernetComponentState::CONNECTING;
|
|
this->start_connect_();
|
|
}
|
|
break;
|
|
case EthernetComponentState::CONNECTING:
|
|
if (!this->started_) {
|
|
ESP_LOGI(TAG, "Stopped connection");
|
|
this->state_ = EthernetComponentState::STOPPED;
|
|
} else if (this->connected_) {
|
|
// connection established
|
|
ESP_LOGI(TAG, "Connected");
|
|
this->state_ = EthernetComponentState::CONNECTED;
|
|
|
|
this->dump_connect_params_();
|
|
this->status_clear_warning();
|
|
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
|
this->connect_trigger_.trigger();
|
|
#endif
|
|
} else if (now - this->connect_begin_ > 15000) {
|
|
ESP_LOGW(TAG, "Connecting failed; reconnecting");
|
|
this->start_connect_();
|
|
}
|
|
break;
|
|
case EthernetComponentState::CONNECTED:
|
|
if (!this->started_) {
|
|
ESP_LOGI(TAG, "Stopped connection");
|
|
this->state_ = EthernetComponentState::STOPPED;
|
|
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
|
this->disconnect_trigger_.trigger();
|
|
#endif
|
|
} else if (!this->connected_) {
|
|
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
|
this->state_ = EthernetComponentState::CONNECTING;
|
|
this->start_connect_();
|
|
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
|
|
this->disconnect_trigger_.trigger();
|
|
#endif
|
|
} else {
|
|
this->finish_connect_();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EthernetComponent::dump_config() {
|
|
const char *type_str = "Unknown";
|
|
#if defined(USE_ETHERNET_W5500)
|
|
type_str = "W5500";
|
|
#elif defined(USE_ETHERNET_W5100)
|
|
type_str = "W5100";
|
|
#elif defined(USE_ETHERNET_W6100)
|
|
type_str = "W6100";
|
|
#elif defined(USE_ETHERNET_W6300)
|
|
type_str = "W6300";
|
|
#elif defined(USE_ETHERNET_ENC28J60)
|
|
type_str = "ENC28J60";
|
|
#endif
|
|
#if defined(USE_ETHERNET_W6300)
|
|
// W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used
|
|
ESP_LOGCONFIG(TAG,
|
|
"Ethernet:\n"
|
|
" Type: %s (PIO QSPI)\n"
|
|
" Connected: %s\n"
|
|
" IRQ Pin: %d\n"
|
|
" Reset Pin: %d",
|
|
type_str, YESNO(this->is_connected()), this->interrupt_pin_, this->reset_pin_);
|
|
#else
|
|
ESP_LOGCONFIG(TAG,
|
|
"Ethernet:\n"
|
|
" Type: %s\n"
|
|
" Connected: %s\n"
|
|
" CLK Pin: %u\n"
|
|
" MISO Pin: %u\n"
|
|
" MOSI Pin: %u\n"
|
|
" CS Pin: %u\n"
|
|
" IRQ Pin: %d\n"
|
|
" Reset Pin: %d",
|
|
type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
|
|
this->interrupt_pin_, this->reset_pin_);
|
|
#endif
|
|
this->dump_connect_params_();
|
|
}
|
|
|
|
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
|
network::IPAddresses addresses;
|
|
if (this->eth_ != nullptr) {
|
|
LwIPLock lock;
|
|
addresses[0] = network::IPAddress(this->eth_->localIP());
|
|
}
|
|
return addresses;
|
|
}
|
|
|
|
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
|
LwIPLock lock;
|
|
const ip_addr_t *dns_ip = dns_getserver(num);
|
|
return dns_ip;
|
|
}
|
|
|
|
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
|
if (this->eth_ != nullptr) {
|
|
this->eth_->macAddress(mac);
|
|
} else {
|
|
memset(mac, 0, 6);
|
|
}
|
|
}
|
|
|
|
std::string EthernetComponent::get_eth_mac_address_pretty() {
|
|
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
|
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
|
|
}
|
|
|
|
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
|
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
|
|
uint8_t mac[6];
|
|
get_eth_mac_address_raw(mac);
|
|
format_mac_addr_upper(mac, buf.data());
|
|
return buf.data();
|
|
}
|
|
|
|
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
|
// W5100, W5500, and ENC28J60 are full-duplex on RP2040
|
|
return ETH_DUPLEX_FULL;
|
|
}
|
|
|
|
eth_speed_t EthernetComponent::get_link_speed() {
|
|
#ifdef USE_ETHERNET_ENC28J60
|
|
// ENC28J60 is 10Mbps only
|
|
return ETH_SPEED_10M;
|
|
#else
|
|
// W5100 and W5500 are 100Mbps
|
|
return ETH_SPEED_100M;
|
|
#endif
|
|
}
|
|
|
|
bool EthernetComponent::powerdown() {
|
|
ESP_LOGI(TAG, "Powering down ethernet");
|
|
if (this->eth_ != nullptr) {
|
|
this->eth_->end();
|
|
}
|
|
this->connected_ = false;
|
|
this->started_ = false;
|
|
return true;
|
|
}
|
|
|
|
void EthernetComponent::start_connect_() {
|
|
this->got_ipv4_address_ = false;
|
|
this->connect_begin_ = millis();
|
|
this->status_set_warning(LOG_STR("waiting for IP configuration"));
|
|
|
|
// Hostname is already set in setup() via LwipIntf::setHostname()
|
|
|
|
#ifdef USE_ETHERNET_MANUAL_IP
|
|
if (this->manual_ip_.has_value()) {
|
|
// Static IP was already configured before begin() in setup()
|
|
// Set DNS servers
|
|
LwIPLock lock;
|
|
if (this->manual_ip_->dns1.is_set()) {
|
|
ip_addr_t d;
|
|
d = this->manual_ip_->dns1;
|
|
dns_setserver(0, &d);
|
|
}
|
|
if (this->manual_ip_->dns2.is_set()) {
|
|
ip_addr_t d;
|
|
d = this->manual_ip_->dns2;
|
|
dns_setserver(1, &d);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void EthernetComponent::finish_connect_() {
|
|
// No additional work needed on RP2040 for now
|
|
// IPv6 link-local could be added here in the future
|
|
}
|
|
|
|
void EthernetComponent::dump_connect_params_() {
|
|
if (this->eth_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
|
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
|
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
|
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
|
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
|
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
|
|
|
// Copy all lwIP state under the lock to avoid races with IRQ callbacks
|
|
ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
|
|
{
|
|
LwIPLock lock;
|
|
auto *netif = this->eth_->getNetIf();
|
|
ip_addr = netif->ip_addr;
|
|
netmask = netif->netmask;
|
|
gw = netif->gw;
|
|
dns1_addr = *dns_getserver(0);
|
|
dns2_addr = *dns_getserver(1);
|
|
}
|
|
ESP_LOGCONFIG(TAG,
|
|
" IP Address: %s\n"
|
|
" Hostname: '%s'\n"
|
|
" Subnet: %s\n"
|
|
" Gateway: %s\n"
|
|
" DNS1: %s\n"
|
|
" DNS2: %s\n"
|
|
" MAC Address: %s",
|
|
network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
|
|
network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
|
|
network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
|
|
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
|
|
}
|
|
|
|
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
|
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
|
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
|
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
|
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
|
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
|
|
|
void EthernetComponent::enable() {
|
|
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
|
|
// there is no clean enable/disable hook today. The YAML option is accepted on
|
|
// RP2040 for schema parity but has no effect.
|
|
if (!this->disabled_)
|
|
return;
|
|
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
|
|
this->disabled_ = false;
|
|
}
|
|
|
|
void EthernetComponent::disable() {
|
|
if (this->disabled_)
|
|
return;
|
|
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
|
|
this->disabled_ = true;
|
|
}
|
|
|
|
} // namespace esphome::ethernet
|
|
|
|
#endif // USE_ETHERNET && USE_RP2
|