From 7435c50300e113b276c508456286381429c26af7 Mon Sep 17 00:00:00 2001 From: David Crocker Date: Mon, 19 Sep 2022 12:59:22 +0100 Subject: Support optional WiFi extension to MB6HC --- .cproject | 2 +- src/Config/Pins_Duet3_MB6HC.h | 30 +++++++++++-- src/Config/Pins_DuetNG.h | 7 ++- src/Hardware/SAM4E/Devices.cpp | 12 ++--- src/Hardware/SAM4E/Devices.h | 2 +- src/Hardware/SAME70/Devices.cpp | 22 ++++++++++ src/Hardware/SAME70/Devices.h | 4 ++ src/Networking/ESP8266WiFi/WiFiInterface.cpp | 25 +++++++---- src/Networking/Network.cpp | 66 ++++++++++++++++------------ src/Networking/Network.h | 27 ++++++++++-- src/Platform/RepRap.cpp | 3 ++ 11 files changed, 144 insertions(+), 56 deletions(-) diff --git a/.cproject b/.cproject index 1280d93a..ec0fce31 100644 --- a/.cproject +++ b/.cproject @@ -612,7 +612,7 @@ - + diff --git a/src/Config/Pins_Duet3_MB6HC.h b/src/Config/Pins_Duet3_MB6HC.h index 2dab95d8..5705b09d 100644 --- a/src/Config/Pins_Duet3_MB6HC.h +++ b/src/Config/Pins_Duet3_MB6HC.h @@ -14,10 +14,12 @@ #define IAP_CAN_LOADER_FILE "Duet3_CANiap32_" BOARD_SHORT_NAME ".bin" constexpr uint32_t IAP_IMAGE_START = 0x20458000; // last 32kb of RAM +#define WIFI_FIRMWARE_FILE "DuetWiFiServer_32S3.bin" + // Features definition // Networking support #define HAS_LWIP_NETWORKING 1 -#define HAS_WIFI_NETWORKING 0 +#define HAS_WIFI_NETWORKING 1 // Storage support #define HAS_SBC_INTERFACE 1 @@ -386,6 +388,28 @@ constexpr Pin APIN_SBC_SPI_SCK = PortCPin(24); constexpr Pin APIN_SBC_SPI_SS0 = PortCPin(25); constexpr GpioPinFunction SBCPinPeriphMode = GpioPinFunction::C; +// WiFi pins, mostly shared with the SBC interface +#define ESP_SPI SPI1 +#define ESP_SPI_INTERFACE_ID ID_SPI1 +#define ESP_SPI_IRQn SPI1_IRQn +#define ESP_SPI_HANDLER SPI1_Handler + +constexpr Pin APIN_ESP_SPI_MOSI = PortCPin(27); +constexpr Pin APIN_ESP_SPI_MISO = PortCPin(26); +constexpr Pin APIN_ESP_SPI_SCK = PortCPin(24); +constexpr Pin APIN_ESP_SPI_SS0 = PortCPin(25); +constexpr GpioPinFunction SPIPeriphMode = GpioPinFunction::C; + +constexpr Pin APIN_SerialWiFi_TXD = PortDPin(19); +constexpr Pin APIN_SerialWiFi_RXD = PortDPin(18); +constexpr GpioPinFunction SerialWiFiPeriphMode = GpioPinFunction::C; + +constexpr Pin EspResetPin = PortCPin(14); // Low on this in holds the WiFi module in reset (ESP_RESET) +constexpr Pin EspEnablePin = NoPin; // High to enable the WiFi module, low to power it down (ESP_CH_PD) +constexpr Pin EspDataReadyPin = PortAPin(2); // Input from the WiFi module indicating that it wants to transfer data (ESP GPIO0) +constexpr Pin SamTfrReadyPin = PortEPin(2); // Output from the SAM to the WiFi module indicating we can accept a data transfer (ESP GPIO4 via 7474) +constexpr Pin SamCsPin = PortCPin(25); // SPI NPCS pin, input from WiFi module + // CAN constexpr Pin APIN_CAN0_RX = PortBPin(3); constexpr Pin APIN_CAN0_TX = PortBPin(2); @@ -417,8 +441,8 @@ constexpr Pin SbcTfrReadyPin = PortEPin(2); // DMA channel allocation constexpr DmaChannel DmacChanHsmci = 0; // this is hard coded in the ASF HSMCI driver -//constexpr DmaChannel DmacChanWiFiTx = 1; // only on v0.3 board -//constexpr DmaChannel DmacChanWiFiRx = 2; // only on v0.3 board +constexpr DmaChannel DmacChanWiFiTx = 1; +constexpr DmaChannel DmacChanWiFiRx = 2; constexpr DmaChannel DmacChanTmcTx = 3; constexpr DmaChannel DmacChanTmcRx = 4; constexpr DmaChannel DmacChanSbcTx = 5; diff --git a/src/Config/Pins_DuetNG.h b/src/Config/Pins_DuetNG.h index 09e53fe6..e9c507dd 100644 --- a/src/Config/Pins_DuetNG.h +++ b/src/Config/Pins_DuetNG.h @@ -139,7 +139,6 @@ constexpr size_t MaxSpindles = 4; // Maximum number of configurable spindles constexpr size_t NumSerialChannels = 2; // The number of serial IO channels not counting the WiFi serial connection (USB and one auxiliary UART) #define SERIAL_MAIN_DEVICE SerialUSB #define SERIAL_AUX_DEVICE Serial -#define SERIAL_WIFI_DEVICE Serial1 constexpr Pin UsbVBusPin = PortCPin(22); // Pin used to monitor VBUS on USB port @@ -483,9 +482,9 @@ constexpr Pin APIN_Serial0_TXD = PortAPin(10); constexpr GpioPinFunction Serial0PeriphMode = GpioPinFunction::A; // Serial1 -constexpr Pin APIN_Serial1_RXD = PortAPin(5); -constexpr Pin APIN_Serial1_TXD = PortAPin(6); -constexpr GpioPinFunction Serial1PeriphMode = GpioPinFunction::C; +constexpr Pin APIN_SerialWiFi_RXD = PortAPin(5); +constexpr Pin APIN_SerialWiFi_TXD = PortAPin(6); +constexpr GpioPinFunction SerialWiFiPeriphMode = GpioPinFunction::C; // Duet pin numbers to control the WiFi interface on the Duet WiFi #define ESP_SPI SPI diff --git a/src/Hardware/SAM4E/Devices.cpp b/src/Hardware/SAM4E/Devices.cpp index c5a5fcae..2dbbfc08 100644 --- a/src/Hardware/SAM4E/Devices.cpp +++ b/src/Hardware/SAM4E/Devices.cpp @@ -11,8 +11,8 @@ #include #include -AsyncSerial Serial (UART0, UART0_IRQn, ID_UART0, 512, 512, [](AsyncSerial*) noexcept { }, [](AsyncSerial*) noexcept { }); -AsyncSerial Serial1(UART1, UART1_IRQn, ID_UART1, 512, 512, [](AsyncSerial*) noexcept { }, [](AsyncSerial*) noexcept { }); +AsyncSerial Serial(UART0, UART0_IRQn, ID_UART0, 512, 512, [](AsyncSerial*) noexcept { }, [](AsyncSerial*) noexcept { }); +AsyncSerial SerialWiFi(UART1, UART1_IRQn, ID_UART1, 512, 512, [](AsyncSerial*) noexcept { }, [](AsyncSerial*) noexcept { }); SerialCDC SerialUSB; void UART0_Handler(void) noexcept @@ -22,7 +22,7 @@ void UART0_Handler(void) noexcept void UART1_Handler(void) noexcept { - Serial1.IrqHandler(); + SerialWiFi.IrqHandler(); } void SerialInit() noexcept @@ -31,9 +31,9 @@ void SerialInit() noexcept SetPinFunction(APIN_Serial0_TXD, Serial0PeriphMode); EnablePullup(APIN_Serial0_RXD); - SetPinFunction(APIN_Serial1_RXD, Serial1PeriphMode); - SetPinFunction(APIN_Serial1_TXD, Serial1PeriphMode); - EnablePullup(APIN_Serial1_RXD); + SetPinFunction(APIN_SerialWiFi_RXD, SerialWiFiPeriphMode); + SetPinFunction(APIN_SerialWiFi_TXD, SerialWiFiPeriphMode); + EnablePullup(APIN_SerialWiFi_RXD); } void SdhcInit() noexcept diff --git a/src/Hardware/SAM4E/Devices.h b/src/Hardware/SAM4E/Devices.h index dbac6b94..df072e76 100644 --- a/src/Hardware/SAM4E/Devices.h +++ b/src/Hardware/SAM4E/Devices.h @@ -11,7 +11,7 @@ #include extern AsyncSerial Serial; -extern AsyncSerial Serial1; +extern AsyncSerial SerialWiFi; #define SUPPORT_USB 1 // needed by SerialCDC.h #include diff --git a/src/Hardware/SAME70/Devices.cpp b/src/Hardware/SAME70/Devices.cpp index 2af3ed6a..1e47f4a5 100644 --- a/src/Hardware/SAME70/Devices.cpp +++ b/src/Hardware/SAME70/Devices.cpp @@ -37,6 +37,21 @@ USARTClass Serial1(USART2, USART2_IRQn, ID_USART2, 512, 512, } ); +#if defined(DUET3_MB6HC) +AsyncSerial SerialWiFi(UART4, UART4_IRQn, ID_UART4, 512, 512, + [](AsyncSerial*) noexcept + { + SetPinFunction(APIN_SerialWiFi_RXD, SerialWiFiPeriphMode); + SetPinFunction(APIN_SerialWiFi_TXD, SerialWiFiPeriphMode); + }, + [](AsyncSerial*) noexcept + { + ClearPinFunction(APIN_SerialWiFi_RXD); + ClearPinFunction(APIN_SerialWiFi_TXD); + } + ); +#endif + SerialCDC SerialUSB; void UART2_Handler(void) noexcept @@ -49,6 +64,13 @@ void USART2_Handler(void) noexcept Serial1.IrqHandler(); } +#if defined(DUET3_MB6HC) +void UART4_Handler(void) noexcept +{ + SerialWiFi.IrqHandler(); +} +#endif + void SdhcInit() noexcept { SetPinFunction(HsmciMclkPin, HsmciMclkPinFunction); diff --git a/src/Hardware/SAME70/Devices.h b/src/Hardware/SAME70/Devices.h index 22e03e47..87b5de23 100644 --- a/src/Hardware/SAME70/Devices.h +++ b/src/Hardware/SAME70/Devices.h @@ -14,6 +14,10 @@ extern AsyncSerial Serial; extern USARTClass Serial1; +#if defined(DUET3_MB6HC) +extern AsyncSerial SerialWiFi; +#endif + #define SUPPORT_USB 1 // needed by SerialCDC.h #include "SerialCDC.h" diff --git a/src/Networking/ESP8266WiFi/WiFiInterface.cpp b/src/Networking/ESP8266WiFi/WiFiInterface.cpp index 9e08438b..07ea4e6b 100644 --- a/src/Networking/ESP8266WiFi/WiFiInterface.cpp +++ b/src/Networking/ESP8266WiFi/WiFiInterface.cpp @@ -36,7 +36,10 @@ static_assert(SsidLength == SsidBufferLength, "SSID lengths in NetworkDefs.h and # define USE_DMAC_MANAGER 0 // use SAMD/SAME DMA controller via DmacManager module # define USE_XDMAC 0 // use SAME7 XDMA controller -#elif defined(DUET3_V03) || defined(SAME70XPLD) +#elif defined(DUET3_MB6HC) + +# include +# include # define USE_PDC 0 // use SAM4 peripheral DMA controller # define USE_DMAC 0 // use SAM4 general DMA controller @@ -186,6 +189,10 @@ void SERIAL_WIFI_ISR3() noexcept SerialWiFiDevice->Interrupt3(); } +#else + +#define SERIAL_WIFI_DEVICE (SerialWiFi) + #endif static volatile bool transferPending = false; @@ -2042,8 +2049,8 @@ void WiFiInterface::StartWiFi() noexcept #endif #if !SAME5x && !defined(__LPC17xx__) - SetPinFunction(APIN_Serial1_TXD, Serial1PeriphMode); // connect the pins to the UART - SetPinFunction(APIN_Serial1_RXD, Serial1PeriphMode); // connect the pins to the UART + SetPinFunction(APIN_SerialWiFi_TXD, SerialWiFiPeriphMode); // connect the pins to the UART + SetPinFunction(APIN_SerialWiFi_RXD, SerialWiFiPeriphMode); // connect the pins to the UART #endif SERIAL_WIFI_DEVICE.begin(WiFiBaudRate); // initialise the UART, to receive debug info debugMessageChars = 0; @@ -2061,8 +2068,8 @@ void WiFiInterface::ResetWiFi() noexcept #endif #if !defined(SAME5x) - pinMode(APIN_Serial1_TXD, INPUT_PULLUP); // just enable pullups on TxD and RxD pins - pinMode(APIN_Serial1_RXD, INPUT_PULLUP); + pinMode(APIN_SerialWiFi_TXD, INPUT_PULLUP); // just enable pullups on TxD and RxD pins + pinMode(APIN_SerialWiFi_RXD, INPUT_PULLUP); #endif currentMode = WiFiState::disabled; @@ -2113,15 +2120,15 @@ void WiFiInterface::ResetWiFiForUpload(bool external) noexcept if (external) { #if !defined(DUET3MINI) - pinMode(APIN_Serial1_TXD, INPUT_PULLUP); // just enable pullups on TxD and RxD pins - pinMode(APIN_Serial1_RXD, INPUT_PULLUP); + pinMode(APIN_SerialWiFi_TXD, INPUT_PULLUP); // just enable pullups on TxD and RxD pins + pinMode(APIN_SerialWiFi_RXD, INPUT_PULLUP); #endif } else { #if !SAME5x && !defined(__LPC17xx__) - SetPinFunction(APIN_Serial1_TXD, Serial1PeriphMode); // connect the pins to the UART - SetPinFunction(APIN_Serial1_RXD, Serial1PeriphMode); // connect the pins to the UART + SetPinFunction(APIN_SerialWiFi_TXD, SerialWiFiPeriphMode); // connect the pins to the UART + SetPinFunction(APIN_SerialWiFi_RXD, SerialWiFiPeriphMode); // connect the pins to the UART #endif } diff --git a/src/Networking/Network.cpp b/src/Networking/Network.cpp index 376b90f6..91193ca5 100644 --- a/src/Networking/Network.cpp +++ b/src/Networking/Network.cpp @@ -86,23 +86,17 @@ Network::Network(Platform& p) noexcept : platform(p) #endif { #if HAS_NETWORKING -#if defined(DUET3_MB6HC) || defined(DUET3_MB6XD) +# if defined(DUET3_MB6HC) || defined(DUET3_MB6XD) interfaces[0] = new LwipEthernetInterface(p); -#elif defined(DUET_NG) || defined(DUET3MINI_V04) +# elif defined(DUET_NG) || defined(DUET3MINI_V04) interfaces[0] = nullptr; // we set this up in Init() -#elif defined(FMDC_V02) || defined(FMDC_V03) +# elif defined(FMDC_V02) || defined(FMDC_V03) interfaces[0] = new WiFiInterface(p); -#elif defined(DUET_M) +# elif defined(DUET_M) interfaces[0] = new W5500Interface(p); -#elif defined(__LPC17xx__) -# if HAS_WIFI_NETWORKING - interfaces[0] = new WiFiInterface(p); - #else - interfaces[0] = new RTOSPlusTCPEthernetInterface(p); - #endif -#else -# error Unknown board -#endif +# else +# error Unknown board +# endif #endif // HAS_NETWORKING } @@ -113,8 +107,10 @@ Network::Network(Platform& p) noexcept : platform(p) constexpr ObjectModelArrayDescriptor Network::interfacesArrayDescriptor = { - nullptr, - [] (const ObjectModel *self, const ObjectExplorationContext& context) noexcept -> size_t { return NumNetworkInterfaces; }, + // 0. Interfaces + { + nullptr, + [] (const ObjectModel *self, const ObjectExplorationContext& context) noexcept -> size_t { return ((Network*)self)->GetNumNetworkInterfaces(); }, #if HAS_NETWORKING [] (const ObjectModel *self, ObjectExplorationContext& context) noexcept -> ExpressionValue { return ExpressionValue(((Network*)self)->interfaces[context.GetIndex(0)]); } #endif @@ -200,10 +196,24 @@ void Network::Init() noexcept slowLoop = 0; } +#if defined(DUET3_MB6HC) + +// Create the additional interface. Called after we have established that we are not running in SBC mode but before config.g is run. +void Network::CreateAdditionalInterface() noexcept +{ + if (platform.GetBoardType() >= BoardType::Duet3_6HC_v102) + { + interfaces[1] = new WiFiInterface(platform); + numActualNetworkInterfaces = 2; + } +} + +#endif + GCodeResult Network::EnableProtocol(unsigned int interface, NetworkProtocol protocol, int port, int secure, const StringRef& reply) noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { return interfaces[interface]->EnableProtocol(protocol, port, secure, reply); } @@ -219,7 +229,7 @@ GCodeResult Network::EnableProtocol(unsigned int interface, NetworkProtocol prot GCodeResult Network::DisableProtocol(unsigned int interface, NetworkProtocol protocol, const StringRef& reply) noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { NetworkInterface * const iface = interfaces[interface]; const GCodeResult ret = iface->DisableProtocol(protocol, reply); @@ -280,7 +290,7 @@ GCodeResult Network::DisableProtocol(unsigned int interface, NetworkProtocol pro GCodeResult Network::ReportProtocols(unsigned int interface, const StringRef& reply) const noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { return interfaces[interface]->ReportProtocols(reply); } @@ -296,7 +306,7 @@ GCodeResult Network::ReportProtocols(unsigned int interface, const StringRef& re GCodeResult Network::EnableInterface(unsigned int interface, int mode, const StringRef& ssid, const StringRef& reply) noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { NetworkInterface * const iface = interfaces[interface]; const GCodeResult ret = iface->EnableInterface(mode, ssid, reply); @@ -485,7 +495,7 @@ void Network::Exit() noexcept GCodeResult Network::GetNetworkState(unsigned int interface, const StringRef& reply) noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { return interfaces[interface]->GetNetworkState(reply); } @@ -501,7 +511,7 @@ GCodeResult Network::GetNetworkState(unsigned int interface, const StringRef& re bool Network::IsWiFiInterface(unsigned int interface) const noexcept { #if HAS_NETWORKING - return interface < NumNetworkInterfaces && interfaces[interface]->IsWiFiInterface(); + return interface < GetNumNetworkInterfaces() && interfaces[interface]->IsWiFiInterface(); #else return false; #endif @@ -601,7 +611,7 @@ void Network::Diagnostics(MessageType mtype) noexcept int Network::EnableState(unsigned int interface) const noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { return interfaces[interface]->EnableState(); } @@ -626,7 +636,7 @@ IPAddress Network::GetIPAddress(unsigned int interface) const noexcept { return #if HAS_NETWORKING - (interface < NumNetworkInterfaces) ? interfaces[interface]->GetIPAddress() : + (interface < GetNumNetworkInterfaces()) ? interfaces[interface]->GetIPAddress() : #endif IPAddress(); } @@ -635,7 +645,7 @@ IPAddress Network::GetNetmask(unsigned int interface) const noexcept { return #if HAS_NETWORKING - (interface < NumNetworkInterfaces) ? interfaces[interface]->GetNetmask() : + (interface < GetNumNetworkInterfaces()) ? interfaces[interface]->GetNetmask() : #endif IPAddress(); } @@ -644,7 +654,7 @@ IPAddress Network::GetGateway(unsigned int interface) const noexcept { return #if HAS_NETWORKING - (interface < NumNetworkInterfaces) ? interfaces[interface]->GetGateway() : + (interface < GetNumNetworkInterfaces()) ? interfaces[interface]->GetGateway() : #endif IPAddress(); } @@ -652,7 +662,7 @@ IPAddress Network::GetGateway(unsigned int interface) const noexcept bool Network::UsingDhcp(unsigned int interface) const noexcept { #if HAS_NETWORKING - return interface < NumNetworkInterfaces && interfaces[interface]->UsingDhcp(); + return interface < GetNumNetworkInterfaces() && interfaces[interface]->UsingDhcp(); #else return false; #endif @@ -696,7 +706,7 @@ void Network::SetHostname(const char *name) noexcept GCodeResult Network::SetMacAddress(unsigned int interface, const MacAddress& mac, const StringRef& reply) noexcept { #if HAS_NETWORKING - if (interface < NumNetworkInterfaces) + if (interface < GetNumNetworkInterfaces()) { return interfaces[interface]->SetMacAddress(mac, reply); } @@ -711,7 +721,7 @@ GCodeResult Network::SetMacAddress(unsigned int interface, const MacAddress& mac const MacAddress& Network::GetMacAddress(unsigned int interface) const noexcept { #if HAS_NETWORKING - if (interface >= NumNetworkInterfaces) + if (interface >= GetNumNetworkInterfaces()) { interface = 0; } diff --git a/src/Networking/Network.h b/src/Networking/Network.h index 2543443d..eef40cd5 100644 --- a/src/Networking/Network.h +++ b/src/Networking/Network.h @@ -14,10 +14,10 @@ #include #include -#if defined(DUET3_V03) -const size_t NumNetworkInterfaces = 2; +#if defined(DUET3_MB6HC) && HAS_WIFI_NETWORKING +const size_t MaxNetworkInterfaces = 2; #elif defined(DUET3_MB6HC) || defined(DUET3_MB6XD) || defined(DUET_NG) || defined(DUET_M) || defined(__LPC17xx__) || defined(PCCB) || defined(DUET3MINI) -const size_t NumNetworkInterfaces = 1; +const size_t MaxNetworkInterfaces = 1; #else # error Wrong Network.h file included #endif @@ -78,6 +78,10 @@ public: void Diagnostics(MessageType mtype) noexcept; bool IsWiFiInterface(unsigned int interface) const noexcept; +#if defined(DUET3_MB6HC) + void CreateAdditionalInterface() noexcept; +#endif + GCodeResult EnableInterface(unsigned int interface, int mode, const StringRef& ssid, const StringRef& reply) noexcept; GCodeResult EnableProtocol(unsigned int interface, NetworkProtocol protocol, int port, int secure, const StringRef& reply) noexcept; GCodeResult DisableProtocol(unsigned int interface, NetworkProtocol protocol, const StringRef& reply) noexcept; @@ -121,12 +125,13 @@ protected: OBJECT_MODEL_ARRAY(interfaces) private: + unsigned int GetNumNetworkInterfaces() const noexcept; WiFiInterface *FindWiFiInterface() const noexcept; Platform& platform; #if HAS_NETWORKING - NetworkInterface *interfaces[NumNetworkInterfaces]; + NetworkInterface *interfaces[MaxNetworkInterfaces]; #endif #if HAS_RESPONDERS @@ -146,7 +151,21 @@ private: #if SUPPORT_HTTP String corsSite; #endif + +#ifdef DUET3_MB6HC + unsigned int numActualNetworkInterfaces = 1; // don't add a second interface until we know whether the board supports it +#endif + char hostname[16]; // Limit DHCP hostname to 15 characters + terminating 0 }; +inline unsigned int Network::GetNumNetworkInterfaces() const noexcept +{ +#if defined(DUET3_MB6HC) + return numActualNetworkInterfaces; +#else + return MaxNetworkInterfaces; +#endif +} + #endif /* SRC_NETWORK_NETWORK_H_ */ diff --git a/src/Platform/RepRap.cpp b/src/Platform/RepRap.cpp index cf3d9cbb..0353012c 100644 --- a/src/Platform/RepRap.cpp +++ b/src/Platform/RepRap.cpp @@ -607,6 +607,9 @@ void RepRap::Init() noexcept if (rslt == GCodeResult::ok) { +# if defined(DUET3_MB6HC) + network->CreateAdditionalInterface(); // do this now because config.g may refer to it +# endif // Run the configuration file if (!RunStartupFile(GCodes::CONFIG_FILE) && !RunStartupFile(GCodes::CONFIG_BACKUP_FILE)) { -- cgit v1.2.3