A coaxial antenna splitter is made for switching between two antennas using one single radio. A standard antenna splitter, for instance, from Albrecht looks like depicted in the following image.
They are heavy builds with a strong an heavy chassis as well as a very large knob that requires quite some torque in order to commute between the two antenna connectors marked A
and B
.
Operating the antenna commuter would require the device to be in the proximity of the operator which can be quite inconvenient depending on the complexity of the setup. It would be interesting to design some device around the antenna commuter that would be able to switch between the two antennas without having to do so manually. This document describes a build that would allow an operator to remotely commute between the two antennas.
Antenna commuters are typically a heavy build due to the very strong RF Wattage that they are designed for. Typically, the switch could very well be replaced with a transitor switch, or anything similar and smaller but due to the high RF that can interfere with neighboring devices, the build of the antenna commuter will be leveraged and the switching will be designed to follow the large knob on top of the device.
Disassembling the Albrecht CX-201 is fairly easy, the bottom part being held in place by three screws that can be removed to reveal the inside of the Albrecht antenna commuter.
On the inside, we find a swivel that is connected to the knob on the outside of the Albrecht splitter through a hole, a spring and ball bearing that is meant to slide along the top of the chassis between two holes designed to house the ball and two copper or nickel blades or lamelles that are meant to be pushed into position by the switch. Intuitively, the ground is switched through the chassis of the Albrecht whilst the signal passes through the blades from one antenna connector to the output antenna connector.
The servo motor could be connected to the knob itself using some mechanism but since the Albrecht is already fully open, it might be wiser to throw away the entire swivel build, with the spring and ball bearing in favor of creating an axle that would be connected through the Albrecht chassis to the servo motor and would press or depress the blades. In doing so, a lot of friction is removed, given that the ball bearing is no longer removed such that the torque force required for the servo motor would be greatly reduced.
+-------+ +---------+ | Servo +----------+ ESP8266 | +---+---+ +----+----+ | | | | | . manual flip switch? | spindle . IoT MQTT Wireless? | . etc. | ANTENNA | ANTENNA A | B + | + +---|---------|---+ | \ | | | | \ + |<........ disconnected blade | \ \ | | | \ \ | | | \ | | +--------|--------+ + ANTENNA INPUT (RTX)
By connecting the servo motor to the ESP8266 there will be a whole range of possibilities available to control the antenna commuter. A DPST switch might be suitable and conventional enough to make the connection between the two antennas given that rigs are typically mobile such that there might be no wireless available to control the switching via IoT or MQTT. Nevertheless, it is of course possible to connect the ESP8266 to a wireless network and automate the switching via Node-Red or similar as we have done for other projects.
The servo motor is mounted on top of the Albrecht antenna commuter with the small motor axle protruding through the hole left over by removing the knob on top of the commuter. Since not much torque is needed, the servo motor is glued to the chassis of the antenna switch using some two part glue such as JB weld.
On the inside of the antenna commuter, the small motor axle can be observed through the hole where the spindle switch used to be.
Luckily, the servo has an inner hole that allows a threaded screw to be inserted and fixed in place. The servo motor is delivered with a few plastic rotors that can be fixed on top of the motor. One of the plastic rotors has a trapezoid shape that can be cut down in order to use just one half of the plastic rotor that will be used to press and depress the blades thereby making the connection between the two antenna connectors.
However, a threaded screw is prepared along with a washer in order to match the height necessary to reach the two blades. The top part of the screw is sawed off letting only the main axle of the screw hold the washer on top of which the plastic rotor will be fixed. In order to make sure that the washer on the lower part of the axle will not move around, some JB weld is used to fix the washer in place.
While the JB weld glue is left to set, some code is written to ensure that the servo motor is at . will be used to correspond to one antenna coupling and will correspond o the other antenna port.
#include <Servo.h> Servo myservo; void setup() { // put your setup code here, to run once: myservo.attach(D6); myservo.write(0); } void loop() { // put your main code here, to run repeatedly: delay(60); }
Using the Servo
library, the code will first attach to D6
representing the pin marked "D6" on the WeMoS PCB and then myservo.write(0);
is used just once to move the spindle of the servo motor to . Controlling the servo motor from now on will just be a task that will involve toggling between myservo.write(0)
and myservo.write(45)
.
With the main shaft in place that connects the servo motor to the chassis and the plastic rotor blade in place calibrated to , the code is changed to:
#include <Servo.h> Servo myservo; int o = true; void setup() { // put your setup code here, to run once: myservo.attach(D6); } void loop() { // put your main code here, to run repeatedly: if(o == true) { myservo.write(130); delay(5000); o = false; return; } myservo.write(0); delay(5000); o = true; }
that will commute between antennas every 5 seconds as can be observed in the following video.
All that remains is to decide whether to use a classic switch or some other mechanism to make the Albrecht commute between the two antenna ports.
Adding the WeMoS and a junction box to hold the circuitry, the project is now complete and ready to be used. It was decided to use a very basic on-off flip switch in order to commute between the A and B antennas. Ideally, the switch along with the long wires will be extended to some control panel next to the radio.
Now with an added flip switch, the code changes to:
#include <Servo.h> Servo myservo; #define SERVOS_PIN D6 #define SWITCH_PIN D7 void setup() { // put your setup code here, to run once: myservo.attach(SERVOS_PIN); pinMode(SWITCH_PIN, INPUT_PULLUP); } void loop() { bool state = digitalRead(SWITCH_PIN); if (state == LOW) { myservo.write(130); return; } myservo.write(0); }
where:
D7
is connected to the flip switch and the other end of the switch is connected to ground,D6
represents the servo motor control pinThe resulting schematic allows the servo to commute between the two SO-239 exit ports of the antenna commuter.
+----------+ +---------+ +12V | | | | ----+---+ +--+---------------------+ | | stepdown | | | | +---+ +--|-+-------------------+ WeMoS | | | | | | | ESP | | +----------+ | | +-----------------+ | | | | | | | | GND | | | / | | --- | +-|-------+ +--------+ | - | | | SW | | | | | +---------+ +-+-+-+-+ | servo | +--------
One interesting application of the antenna commuter is to allow the usage of an antenna tuner alongside a motorized antenna. Typically, for radios such as the FT-891, the tuner and the ATAS-120A are mutually exclusive and cannot be used together. Using the antenna commuter from this project, the output can be commuted between the FT-891 tuner an the ATAS-120A. Here is a sketch of what the setup looks like:
^ antenna | | | | + / \ automatic antenna switcher a1 + + a2 | | + | tuner | + | | | +---+ \ / dual antennas coupler (without balun) + | radio
where:
The Zetagi "Dual Antennas Coupler illustrated below:
is opened up and gutted entirely to replace the balun with a very simple straight-through connection with a copper wire beween all the three ports. In other words, the Zetagi now dumbly couples all the ports together without any additions which is necessary in order to feed both the ATAS-120A antenna and the FC-50 tuner the exact same signal.
With this setup, the operation of the ham goes like this:
ATAS
from the radio menu and tune the ATAS-120A,EXTERNAL
from the radio menu and tune the FC-50such that the combination of the two ATAS-120A and the FC-50 ensure the most minimal SWR possible.
One problem with this setup is that there is no feedback element such that using the switch will not guarantee that the antenna has been commuted based solely on the fact that the switch position has been changed. One idea would be to add strip contacts on the inside of the Albrecht antenna commuter that would ideally make contact once one of the commuter blades extends to the maximum with the strip connectors leading to the GPIO pins of the ESP8266. Nevertheless, for now, the project seems complete.
After some months of operation, it seems that the device works well and the desired goals have been reached. Some problems have been detected in the QA part of the device such that a new redesign of the device is warranted.
Here is a set of problems regarding the device that warranted a rebuild:
The circuit is mostly the same, only that it has been relocated inside a metal box, with the only highlight being the LM7805 voltage regulator that has been used to step down to and then power the ESP.
For reference, here is the electronic schematic that is used for the revised version of the antenna commuter.
+---------+ | ESP8266 | +--------------------------------+ | | | | | | +----------------------+ | /_\ ant. 12V +--------+ | 5V | | | . +-------+---+ LM1805 +--|---+-----+-----+ +------------+ GPIO | . | +----+---+ | | | | | | . --- | | --- 3 | + / COM | | . 47uf --- +------+ --- 4uF 3 | +/ GND | | . ant. output | | | | | +-----+------+ | +---^---+ +-------+--------+------|---+ 3 | + / | | | sig. | | | | | 3 | +/ | | GPIO +------------------+ | | | | | | | | | gnd | | | | +----+------+ +-----|------+ GPIO | +---------+ Motor | | | | | | | | 5V | | +------|--------|----------------+ +---------+ | +-----+ | | | | | | | | | +--------|------------------------------------------+ | +-^---^-+ | | | . . | +----------------------------------------------+ . . | . . GND --- A B ant. input /// +--------+--------------+ | YO1PIR | 09/10/2024 | +-----------------------+ | remote antenna swtich | +-----------------------+
One thing to observe here is that the relay commutes common to ground and the GPIOs connect to either relays. In other words, the flow is from the ESP GPIOs that are considered high that will be commuted via the relays controlled by the remote control to ground - the pull-up required to not buzz the circuit will be provided by the ESP by setting the GPIOs to INPUT_PULLUP
in the Arduino sketch. The former is due to having to use digital pins instead of analog, or else the common relay input could have been (given that the Expressif ESP8266 was determined to be tolerant. Alternatively, the double relay dongle could be made to split a 5V signal in order to read both relays with a single analog pin but that level of minimization does not seem necessary given that a whole WeMoS ESP8266 was used with plenty of available digital pins.
Unfortunately, due to a transport issue, the relay to the bottom of the screen was dislocated and got "desoldered" off the board. To fix the issue, a small piece of PCB was found, pins mounted onto it and a second stage was added because the relay is otherwise very difficult to solder onto the PCB due to the lack of clearance. Even though pulling the plug should be sufficient for such a device with no powercycling ever needed, a switch has been added none the less for a more convenient way to power the device on or off.
The idea of a PCB mounted on the side of the metal box through which connections can be made conviently, on both sides, thereby ensuring that all parts can be easily removed for repairs and/ or maintenance, from the rover one project seemed to be a success such that the same idea was used here to pass a JST PCB plug through the metal plates on the side of the box.
The remote is even more fitting given that it has two buttons A
and B
that correspond to either antenna input on the antenna splitter. Now you can just place the device next to the radio, sit back, relax and use the remote to commute between either inputs connected to the splitter.
Of course, the product can be scaled up, and a 4-to-1 antenna splitter could be created with a 4-button remote (that also seem very much common and available on Chinese markets).
The sketch is based on the WiFi preboot environment sketch and for the antenna application it implements moving the servo in order to commute between the two input ports on the antenna switch.
There is no magic in adjusting the servo angle corresponding to the A
, respectively B
inputs on the antenna switch, such that calibrating the servo motor is a matter of trial and error, perhaps even with a smaller template that can just sweep the motor in order to measure continuity through the antenna switcher at various positions and settle for numeric values for the template.
One other change has been to decouple the servo motor once the lamelle was moved by the user using the remote. The problem is that the servo motor is constantly powered by a PWM signal generated by the GPIO pin, such that any interference that would mess with the ESP or other circuitry, would end up scrambling the PWM which might end in the servo behaving erratically. In order to prevent that effect from occurring, the code only attaches to the servo motor when its lamelle has to be moved in order to commute to a different antenna input port, after which the code decouples from the servo motor, leaving it in its modified position such that any interference would not affect the servo motor in any way given that after detaching, the servo would just be as passive as a brick.
/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2024 - License: GNU MIT // // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // // rights of fair usage, the disclaimer and warranty conditions. // /////////////////////////////////////////////////////////////////////////// // This template supplements the software side of the HAM radio antenna // // commuter project that can be found at the following addresss: // // * https://grimore.org/ham_radio/ // // designing_a_remotely_controlled_motorized_antenna_commuter // // // // The template is used for remotely controlling an antenna switcher // // such that a single antenna can be used for two different radios. The // // template also implements the WiFi preboot environment from: // // * https://grimore.org/arduino/wifipreboot // // in order to ensure that the device can be easily connected to the // // WiFi network even after a disconnect. // // // // HAM antenna switch-specific variables are: // // * GPIO_BUTTON_A // // * GPIO_BUTTON_B // // * SERVO_GPIO_PIN // // * SERVO_ANGLE_A // // * SERVO_ANGLE_B // // that correspond to the "A", respectively "B" buttons on the remote // // and "SERVO_GPIO_PIN" coresponds to the GPIO pin connected to the // // servo signal pin. Similarly, "SERVO_ANGLE_A" and "SERVO_ANGLE_B" are // // both the angles that correspond to the button "A", respectively the // // button "B" on the remote also corresponding to the "A", respectively // // "B" output ports of the antenna switch. // // // // These variables must be set before uploading the sketch because they // // cannot be set later at runtime. Setting the angle of the servo is // // mostly a matter of trial and error and there is no magic that would // // yield the correct values for the servo. // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // configurable parameters // /////////////////////////////////////////////////////////////////////////// // comment out to enable debugging #define DEBUG // set the master password for OTA updates and access to the soft AP #define PREBOOT_MASTER_PASSWORD "" // the name and length of the cookie to use for authentication #define PREBOOT_COOKIE_NAME "ArduinoPrebootCookie" #define PREBOOT_COOKIE_MAX_LENGTH 256 // timeout to establish STA connection in milliseconds #define WIFI_RETRY_TIMEOUT 10000 // retries as multiples of WIFI_RETRY_TIMEOUT milliseconds #define WIFI_CONNECT_TRIES 30 // the time between blinking a single digit #define BLINK_DIT_LENGTH 250 // the time between blinking the whole number #define BLINK_DAH_LENGTH 2500 // antenna commuter application #define GPIO_BUTTON_A D5 #define GPIO_BUTTON_B D7 #define SERVO_GPIO_PIN D4 #define SERVO_ANGLE_A 0 #define SERVO_ANGLE_B 130 /////////////////////////////////////////////////////////////////////////// // includes // /////////////////////////////////////////////////////////////////////////// #include <Arduino.h> #if defined(ARDUINO_ARCH_ESP32) #include <WiFi.h> #include <WebServer.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #include <ESP8266mDNS.h> #include <ESP8266WebServer.h> #endif #include <FS.h> #include <LittleFS.h> #include <ArduinoJson.h> // Arduino OTA #include <WiFiUdp.h> #include <ArduinoOTA.h> #include <TickTwo.h> // antenna commuter application #include <Servo.h> // Platform specific defines. #if defined(ARDUINO_ARCH_ESP8266) #define GET_CHIP_ID() (ESP.getChipId()) #elif defined(ARDUINO_ARCH_ESP32) #define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32)) #endif #define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX)) #define CONFIGURATION_FILE_NAME "/config.json" #define CONFIGURATION_MAX_LENGTH 1024 /////////////////////////////////////////////////////////////////////////// // function definitions // /////////////////////////////////////////////////////////////////////////// byte* getHardwareAddress(void); char* getHardwareAddress(char colon); String computeTemporarySsid(void); void arduinoOtaTickCallback(void); void blinkDigitsDahTickCallback(void); void blinkDigitsDitTickCallback(void); void blinkDigitsBlinkTickCallback(void); void clientWifiTickCallback(void); void serverWifiTickCallback(void); void handleServerWifi(void); void handleClientWifi(void); void setConfiguration(const char* configurationFile, DynamicJsonDocument configuration, int bufferSize); DynamicJsonDocument getConfiguration(const char* configurationFile, int bufferSize); void handleRootHttpRequest(void); void handleSetupHttpRequest(void); void handleRootHttpGet(void); void handleSetupHttpGet(void); void handleRootHttpPost(void); void handleSetupHttpPost(void); void handleHttpNotFound(void); bool fsWriteFile(fs::FS &fs, const char *path, const char *payload); bool fsReadFile(fs::FS &fs, const char *path, char *payload, size_t maxLength); void rebootTickCallback(void); // antenna commuter application void buttonReadTickCallback(void); void antennaSwitchTickCallback(void); void antennaSwitchOffTickCallback(void); /////////////////////////////////////////////////////////////////////////// // variable declarations // /////////////////////////////////////////////////////////////////////////// #if defined(ARDUINO_ARCH_ESP8266) ESP8266WebServer server(80); #elif defined(ARDUINO_ARCH_ESP32) WebServer server(80); #endif TickTwo arduinoOtaTick(arduinoOtaTickCallback, 1000); TickTwo rebootTick(rebootTickCallback, 1000); TickTwo clientWifiTick(clientWifiTickCallback, 250); TickTwo serverWifiTick(serverWifiTickCallback, 250); TickTwo blinkDigitsDahTick(blinkDigitsDahTickCallback, BLINK_DAH_LENGTH); TickTwo blinkDigitsDitTick(blinkDigitsDitTickCallback, BLINK_DIT_LENGTH); TickTwo blinkDigitsBlinkTick(blinkDigitsBlinkTickCallback, 25); char* authenticationCookie = NULL; bool otaStarted; bool networkConnected; int connectionTries; bool rebootPending; int temporarySsidLength; int temporarySsidIndex; int* temporarySsidNumbers; int blinkLedState; // antenna commuter application TickTwo buttonReadTick(buttonReadTickCallback, 500); TickTwo antennaSwitchTick(antennaSwitchTickCallback, 500); TickTwo antennaSwitchOffTick(antennaSwitchOffTickCallback, 1000); typedef enum { BUTTON_NONE, BUTTON_A, BUTTON_B } antennaSwitch; antennaSwitch antennaDirection = BUTTON_NONE; // antenna commuter application Servo antennaServo; /////////////////////////////////////////////////////////////////////////// // HTML templates // /////////////////////////////////////////////////////////////////////////// const char* HTML_BOOT_TEMPLATE = R"html( <!DOCTYPE html> <html lang="en"> <head> <title>ESP Setup</title> </head> <body> <h1>ESP Setup</h1> <hr> AP: %AP%<br> MAC: %MAC%<br> <hr> <form method="POST" action="/setup"> <label for="name">Name: </label> <input id="name" type="text" name="name" value="%NAME%"> <br> <label for="Ssid">Ssid: </label> <input id="Ssid" type="text" name="Ssid"> <br> <label for="password">Password: </label> <input id="password" type="password" name="password"> <hr> <input type="submit" value="submit"> </form> </body> </html> )html"; const char* HTML_AUTH_TEMPLATE = R"html( <!DOCTYPE html> <html lang="en"> <head> <title>Preboot Access</title> </head> <body> <h1>Preboot Access</h1> <form method="POST"> <label for="password">Master password: </label> <input id="password" type="password" name="password"> <hr> <input type="submit" value="submit"> </form> </body> </html> )html"; /////////////////////////////////////////////////////////////////////////// // begin Arduino // /////////////////////////////////////////////////////////////////////////// void setup() { #ifdef DEBUG Serial.begin(115200); // wait for serial while (!Serial) { delay(100); } Serial.println(); #endif #if defined(ARDUINO_ARCH_ESP8266) if (!LittleFS.begin()) { #ifdef DEBUG Serial.println("LittleFS mount failed, formatting and rebooting..."); #endif LittleFS.format(); delay(1000); ESP.restart(); #elif defined(ARDUINO_ARCH_ESP32) if (!LittleFS.begin(true)) { #endif Serial.println("LittleFS mount failed..."); return; } #ifdef DEBUG Serial.printf("Checking if WiFi server must be started...\n"); #endif // check if Ssid is set and start soft AP or STA mode DynamicJsonDocument configuration = getConfiguration(CONFIGURATION_FILE_NAME, CONFIGURATION_MAX_LENGTH); if(configuration.isNull() || !configuration.containsKey("Ssid")) { #ifdef DEBUG Serial.printf("No stored STA Ssid found, proceeding to soft AP...\n"); #endif // start soft AP rebootTick.start(); serverWifiTick.start(); return; } #ifdef DEBUG Serial.printf("No stored STA Ssid found, proceeding to soft AP...\n"); #endif clientWifiTick.start(); // setup OTA ArduinoOTA.setHostname(configuration["name"].as<const char*>()); // allow flashing with the master password ArduinoOTA.setPassword(PREBOOT_MASTER_PASSWORD); ArduinoOTA.onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; } else { // U_FS type = "filesystem"; } // NOTE: if updating FS this would be the place to unmount FS using FS.end() Serial.println("Start updating " + type); }); ArduinoOTA.onEnd([]() { Serial.println("\nEnd"); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); if (error == OTA_AUTH_ERROR) { Serial.println("Auth Failed"); } else if (error == OTA_BEGIN_ERROR) { Serial.println("Begin Failed"); } else if (error == OTA_CONNECT_ERROR) { Serial.println("Connect Failed"); } else if (error == OTA_RECEIVE_ERROR) { Serial.println("Receive Failed"); } else if (error == OTA_END_ERROR) { Serial.println("End Failed"); } }); // start timers / threads arduinoOtaTick.start(); rebootTick.start(); // antenna commuter application pinMode(GPIO_BUTTON_A, INPUT_PULLUP); pinMode(GPIO_BUTTON_B, INPUT_PULLUP); buttonReadTick.start(); antennaSwitchTick.start(); } void loop() { arduinoOtaTick.update(); rebootTick.update(); clientWifiTick.update(); serverWifiTick.update(); blinkDigitsDitTick.update(); blinkDigitsDahTick.update(); blinkDigitsBlinkTick.update(); // antenna commuter application buttonReadTick.update(); antennaSwitchTick.update(); antennaSwitchOffTick.update(); } /////////////////////////////////////////////////////////////////////////// // end Arduino // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // antenna commuter application // /////////////////////////////////////////////////////////////////////////// void buttonReadTickCallback(void) { // D5 -> A, D7 -> B int a = digitalRead(GPIO_BUTTON_A); int b = digitalRead(GPIO_BUTTON_B); #ifdef DEBUG Serial.printf("Button A: %d, button B: %d\n", a, b); #endif if(a == LOW) { antennaDirection = BUTTON_A; return; } if(b == LOW) { antennaDirection = BUTTON_B; return; } antennaDirection = BUTTON_NONE; } void antennaSwitchOffTickCallback(void) { antennaSwitchOffTick.pause(); antennaServo.detach(); } void antennaSwitchTickCallback(void) { if(antennaDirection == BUTTON_NONE) { return; } antennaServo.attach(SERVO_GPIO_PIN); switch(antennaDirection) { case BUTTON_A: antennaServo.write(SERVO_ANGLE_A); break; case BUTTON_B: antennaServo.write(SERVO_ANGLE_B); break; } antennaSwitchOffTick.interval(1000); antennaSwitchOffTick.resume(); antennaDirection = BUTTON_NONE; } /////////////////////////////////////////////////////////////////////////// // OTA updates // /////////////////////////////////////////////////////////////////////////// void arduinoOtaTickCallback(void) { ArduinoOTA.handle(); if(!networkConnected) { return; } if(!otaStarted) { ArduinoOTA.begin(); otaStarted = true; } } /////////////////////////////////////////////////////////////////////////// // system-wide reboot // /////////////////////////////////////////////////////////////////////////// void rebootTickCallback(void) { // check if a reboot has been scheduled. if(!rebootPending) { return; } #ifdef DEBUG Serial.printf("Reboot pending, restarting in 1s...\n"); #endif ESP.restart(); } /////////////////////////////////////////////////////////////////////////// // HTTP route handling // /////////////////////////////////////////////////////////////////////////// void handleRootHttpPost(void) { String password; for(int i = 0; i < server.args(); ++i) { if(server.argName(i) == "password") { password = server.arg(i); continue; } } if(!password.equals(PREBOOT_MASTER_PASSWORD)) { server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } #ifdef DEBUG Serial.println("Authentication succeeded, setting cookie and redirecting."); #endif // clear old authentication cookie if(authenticationCookie != NULL) { free(authenticationCookie); authenticationCookie = NULL; } authenticationCookie = randomStringHex(8); char* buff = (char*) malloc(PREBOOT_COOKIE_MAX_LENGTH * sizeof(char)); snprintf(buff, PREBOOT_COOKIE_MAX_LENGTH, "%s=%s; Max-Age=600; SameSite=Strict", PREBOOT_COOKIE_NAME, authenticationCookie); #ifdef DEBUG Serial.printf("Preboot cookie set to: %s\n", buff); #endif server.sendHeader("Set-Cookie", buff); server.sendHeader("Location", "/setup"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); free(buff); } void handleSetupHttpPost(void) { String espName, staSsid, password; for(int i = 0; i < server.args(); ++i) { if(server.argName(i) == "name") { espName = server.arg(i); continue; } if(server.argName(i) == "Ssid") { staSsid = server.arg(i); continue; } if(server.argName(i) == "password") { password = server.arg(i); continue; } } if(espName == NULL || staSsid == NULL || password == NULL) { server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } #ifdef DEBUG Serial.printf("Ssid %s and password %s received from web application.\n", staSsid, password); #endif DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); configuration["name"] = espName; configuration["Ssid"] = staSsid; configuration["password"] = password; setConfiguration(CONFIGURATION_FILE_NAME, configuration, CONFIGURATION_MAX_LENGTH); server.send(200, "text/plain", "Parameters applied. Scheduling reboot..."); #ifdef DEBUG Serial.printf("Configuration applied...\n"); #endif rebootPending = true; } void handleRootHttpGet(void) { // send login form #ifdef DEBUG Serial.printf("Sending authentication webpage.\n"); #endif String processTemplate = String(HTML_AUTH_TEMPLATE); server.send(200, "text/html", processTemplate); } void handleSetupHttpGet(void) { DynamicJsonDocument configuration = getConfiguration(CONFIGURATION_FILE_NAME, CONFIGURATION_MAX_LENGTH); String espName = HOSTNAME(); if(configuration.containsKey("name")) { espName = configuration["name"].as<const char*>(); } // send default boot webpage #ifdef DEBUG Serial.printf("Sending configuration form webpage.\n"); #endif String processTemplate = String(HTML_BOOT_TEMPLATE); processTemplate.replace("%AP%", computeTemporarySsid()); processTemplate.replace("%MAC%", getHardwareAddress(':')); processTemplate.replace("%NAME%", espName); server.send(200, "text/html", processTemplate); } void handleRootHttpRequest(void) { switch(server.method()) { case HTTP_GET: handleRootHttpGet(); break; case HTTP_POST: handleRootHttpPost(); break; } } void handleSetupHttpRequest(void) { #ifdef DEBUG Serial.println("HTTP setup request received."); #endif if(!server.hasHeader("Cookie")) { #ifdef DEBUG Serial.println("No cookie header found."); #endif server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } String cookie = server.header("Cookie"); if(authenticationCookie == NULL || cookie.indexOf(authenticationCookie) == -1) { #ifdef DEBUG Serial.println("Authentication failed."); #endif server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } switch(server.method()) { case HTTP_GET: #ifdef DEBUG Serial.printf("HTTP GET request received for setup.\n"); #endif handleSetupHttpGet(); break; case HTTP_POST: #ifdef DEBUG Serial.printf("HTTP POST request received for setup.\n"); #endif handleSetupHttpPost(); break; } } void handleHttpNotFound(void) { server.sendHeader("Location", "/"); server.send(302); } /////////////////////////////////////////////////////////////////////////// // LittleFS file operations // /////////////////////////////////////////////////////////////////////////// bool fsWriteFile(fs::FS &fs, const char *path, const char *payload) { #if defined(ARDUINO_ARCH_ESP8266) File file = fs.open(path, "w"); #elif defined(ARDUINO_ARCH_ESP32) File file = fs.open(path, FILE_WRITE); #endif if (!file) { #ifdef DEBUG Serial.println("Failed to open file for writing."); #endif return false; } bool success = file.println(payload); file.close(); return success; } bool fsReadFile(fs::FS &fs, const char *path, char *payload, size_t maxLength) { #if defined(ARDUINO_ARCH_ESP8266) File file = fs.open(path, "r"); #elif defined(ARDUINO_ARCH_ESP32) File file = fs.open(path); #endif if (!file || file.isDirectory()) { #ifdef DEBUG Serial.println("Failed to open file for reading."); #endif return false; } int i = 0; while(file.available() && i < maxLength) { payload[i] = file.read(); ++i; } file.close(); payload[i] = '\0'; return true; } /////////////////////////////////////////////////////////////////////////// // set the current configuration // /////////////////////////////////////////////////////////////////////////// void setConfiguration(const char* configurationFile, DynamicJsonDocument configuration, int bufferSize) { char payload[bufferSize]; serializeJson(configuration, payload, bufferSize); if(!fsWriteFile(LittleFS, configurationFile, payload)) { #ifdef DEBUG Serial.printf("Unable to store configuration.\n"); #endif } } /////////////////////////////////////////////////////////////////////////// // get the current configuration // /////////////////////////////////////////////////////////////////////////// DynamicJsonDocument getConfiguration(const char* configurationFile, int bufferSize) { DynamicJsonDocument configuration(bufferSize); #ifdef DEBUG Serial.printf("Attempting to read configuration...\n"); #endif char* payload = (char *) malloc(bufferSize * sizeof(char)); if (fsReadFile(LittleFS, configurationFile, payload, bufferSize)) { #ifdef DEBUG Serial.printf("Found a valid configuration payload...\n"); #endif DeserializationError error = deserializeJson(configuration, payload); if(error) { #ifdef DEBUG Serial.printf("Deserialization of configuration failed.\n"); #endif } } #ifdef DEBUG Serial.printf("Configuration read complete.\n"); #endif free(payload); return configuration; } /////////////////////////////////////////////////////////////////////////// // generate random string // /////////////////////////////////////////////////////////////////////////// char* randomStringHex(int length) { const char alphabet[] = "0123456789abcdef"; char* payload = (char*) malloc(length * sizeof(char)); int i; for (i=0; i<length; ++i) { payload[i] = alphabet[random(16)]; } payload[i] = '\0'; return payload; } /////////////////////////////////////////////////////////////////////////// // get WiFi MAC address // /////////////////////////////////////////////////////////////////////////// byte* getHardwareAddress(void) { // get mac address byte* mac = (byte *)malloc(6 * sizeof(byte)); #if defined(ARDUINO_ARCH_ESP8266) WiFi.macAddress(mac); #elif defined(ARDUINO_ARCH_ESP32) Network.macAddress(mac); #endif return mac; } /////////////////////////////////////////////////////////////////////////// // convert MAC address to string // /////////////////////////////////////////////////////////////////////////// char* getHardwareAddress(char colon) { byte* mac = getHardwareAddress(); char* buff = (char *)malloc(18 * sizeof(char)); sprintf(buff, "%02x%c%02x%c%02x%c%02x%c%02x%c%02x", mac[0], colon, mac[1], colon, mac[2], colon, mac[3], colon, mac[4], colon, mac[5] ); free(mac); return buff; } /////////////////////////////////////////////////////////////////////////// // get WiFi soft AP // /////////////////////////////////////////////////////////////////////////// String computeTemporarySsid(void) { byte* mac = getHardwareAddress(); String ssid = String(mac[0] ^ mac[1] ^ mac[2] ^ mac[3] ^ mac[4] ^ mac[5], DEC); free(mac); return ssid; } /////////////////////////////////////////////////////////////////////////// // serve WiFi AP // /////////////////////////////////////////////////////////////////////////// void serverWifiTickCallback(void) { if(rebootPending) { return; } // create the boot Ssid String temporarySsid = computeTemporarySsid(); if(WiFi.softAPSSID().equals(temporarySsid)) { // run WiFi server loops server.handleClient(); if(blinkDigitsDahTick.state() == STOPPED) { temporarySsidLength = temporarySsid.length(); temporarySsidNumbers = (int *) malloc(temporarySsidLength * sizeof(int)); for(int i = 0; i < temporarySsidLength; ++i) { temporarySsidNumbers[i] = temporarySsid[i] - '0'; } #ifdef DEBUG //Serial.printf("Started blinking...\n"); #endif temporarySsidIndex = 0; blinkDigitsDahTick.start(); } return; } #ifdef DEBUG Serial.println("Starting HTTP server for Wifi server."); #endif // handle HTTP REST requests server.on("/", handleRootHttpRequest); server.on("/setup", handleSetupHttpRequest); server.onNotFound(handleHttpNotFound); #ifdef DEBUG Serial.println("Ensure HTTP headers are collected by the HTTP server."); #endif #if defined(ARDUINO_ARCH_ESP8266) server.collectHeaders("Cookie"); #elif defined(ARDUINO_ARCH_ESP32) const char* collectHeaders[] = { "Cookie" }; size_t headerkeyssize = sizeof(collectHeaders) / sizeof(char *); server.collectHeaders(collectHeaders, headerkeyssize); #endif // the soft AP (or WiFi) must be started before the HTTP server or it will result in a crash on ESP32 #ifdef DEBUG Serial.println("Starting temporary AP."); #endif WiFi.softAP(temporarySsid, String(), 1, false, 1); #ifdef DEBUG Serial.println("Starting HTTP server."); #endif server.begin(); } /////////////////////////////////////////////////////////////////////////// // connect to WiFi // /////////////////////////////////////////////////////////////////////////// void clientWifiTickCallback(void) { if(rebootPending) { return; } unsigned long callbackCount = clientWifiTick.counter(); #ifdef DEBUG //Serial.printf("Client tick %lu\n", callbackCount); #endif if(callbackCount == 1) { #ifdef DEBUG Serial.printf("Rescheduling client WiFi to check every 10s...\n"); #endif clientWifiTick.interval(WIFI_RETRY_TIMEOUT); clientWifiTick.resume(); } // if WiFi is already connected or a reboot is pending just bail out if(WiFi.status() == WL_CONNECTED) { #ifdef DEBUG Serial.printf("WiFi IP: %s\n", WiFi.localIP().toString().c_str()); #endif connectionTries = 0; networkConnected = true; return; } networkConnected = false; DynamicJsonDocument configuration = getConfiguration(CONFIGURATION_FILE_NAME, CONFIGURATION_MAX_LENGTH); // too many retries so reboot to soft AP if(++connectionTries > WIFI_CONNECT_TRIES) { // zap the Ssid in order to start softAP if(configuration.containsKey("Ssid")) { configuration.remove("Ssid"); } if(configuration.containsKey("password")) { configuration.remove("password"); } setConfiguration(CONFIGURATION_FILE_NAME, configuration, CONFIGURATION_MAX_LENGTH); #ifdef DEBUG Serial.printf("Restarting in 1 second...\n"); #endif rebootPending = true; return; } #ifdef DEBUG Serial.printf("Attempting to establish WiFi STA connecton [%d/%d]\n", (WIFI_CONNECT_TRIES - connectionTries) + 1, WIFI_CONNECT_TRIES); #endif #if defined(ARDUINO_ARCH_ESP8266) WiFi.hostname(configuration["name"].as<String>()); #elif defined(ARDUINO_ARCH_ESP32) WiFi.setHostname(configuration["name"].as<const char*>()); #endif String Ssid = configuration["Ssid"].as<String>(); String password = configuration["password"].as<String>(); #ifdef DEBUG Serial.printf("Trying connection to %s with %s...\n", Ssid, password); #endif WiFi.begin(Ssid, password); } /////////////////////////////////////////////////////////////////////////// // blink the temporary Ssid // /////////////////////////////////////////////////////////////////////////// void blinkDigitsDahTickCallback(void) { // wait for the dits to complete if(blinkDigitsDitTick.state() != STOPPED) { return; } if(temporarySsidIndex >= temporarySsidLength) { blinkDigitsDahTick.stop(); blinkDigitsDitTick.stop(); blinkDigitsBlinkTick.stop(); free(temporarySsidNumbers); #ifdef DEBUG Serial.println(); Serial.println("Dah-dit blink sequence completed."); #endif return; } #ifdef DEBUG Serial.printf("Starting to blink %d times: ", temporarySsidNumbers[temporarySsidIndex]); #endif pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); blinkDigitsDitTick.start(); } void blinkDigitsDitTickCallback(void) { #ifdef DEBUG Serial.printf("Dit: %d/%d\n", blinkDigitsDitTick.counter(), temporarySsidNumbers[temporarySsidIndex]); #endif if(blinkDigitsDitTick.counter() > temporarySsidNumbers[temporarySsidIndex]) { blinkDigitsDitTick.stop(); ++temporarySsidIndex; #ifdef DEBUG Serial.println("Dits completed..."); #endif return; } blinkDigitsDitTick.pause(); blinkDigitsBlinkTick.start(); } void blinkDigitsBlinkTickCallback(void) { if(blinkDigitsBlinkTick.counter() > 2) { blinkDigitsBlinkTick.stop(); blinkDigitsDitTick.resume(); return; } blinkLedState = !blinkLedState; digitalWrite(LED_BUILTIN, blinkLedState); }