About

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.

Requirements

  • an ESP8266 or similar, we conventionally use WeMoS due to their small size, availability, low cost and overall stability,

  • a servo motor capable of sufficient torque to push a copper blade inside the antenna commuter, rated at $5V$ or $3.3V$ digital; it is also possible to use a stepper motor, but a servo should be sufficient because the angle can be precisely controlled using the ESP8266,

  • a buck converter stepdown from $12V$ to $5$, assumingly due to the rig being powered off a $12V$ battery that is typical in cars

Disassembling the Antenna Splitter

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.

Drafting the Build

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.

Building the Mechanics

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 $0^{\circ}$. $0^{\circ}$ will be used to correspond to one antenna coupling and $45^{\circ}$ 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 $0^{\circ}$. 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 $0^{\circ}$, 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 pin

The 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 |
                        +--------

Usage with Mechanized Antennas and Tuners

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 radio is an FT-891,
  • the dual antenna coupler is a modification of a Zetagi "Dual Antennas Coupler",
  • the tuner is the FC-50,
  • the antenna is a motorized ATAS-120A

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:

  • flip the switch to "straight-through",
  • select ATAS from the radio menu and tune the ATAS-120A,
  • power-cycle the radio and flip the switch to "tuner",
  • select EXTERNAL from the radio menu and tune the FC-50

such that the combination of the two ATAS-120A and the FC-50 ensure the most minimal SWR possible.

Further Work

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.

Redesign, Rebuild and Remake

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.

Errata

Here is a set of problems regarding the device that warranted a rebuild:

  • The plastic case, is a little crass and metal (aluminum in this case), if available, should always be preferred due to its characteristic of hampering EMI due to its Farraday effect, the nice thermal properties being able to cool down components as well as its physical ruggedness and resistance to shock and temperature.
  • The idea of a switch connected to a wire is not bad, but the wire can grow longer than expected and cable management becomes an issue which is too much given that just two positions / two controls are used for this device. We have had great success with remote control relay devices built around $433MHz$ transmitters with each remote encoded to the specific receiver powering the relays; for example, the following device can be found on Amazon for a bargain and in different variations:

  • The idea of connecting wires to the PCB by directly soldering them onto the PCB is not only ugly but also makes a very bad bond. Once a wire is soldered directly onto a PCB and if the wire is externally visible, then it automatically installs a wear-and-tear counter after which the wire will break off. Ideally, all the wires should rather be connected to JST headers or similar and then the contact between the wires and the PCB should be managed mechanically by the plugs.
  • The original buck stepdown could be spared / recovered and replaced by a simple LM-voltage regulator.
  • A bug has been observed during transmissions where the motor would draw too much power and drain the line such that the ESP controller would remain without power for a very short period and for that very short period, the motor would flicker back and forth creating disruptions during tranmissions. To address the issue, a voltage regulator has been used instead of a buck stepdown, with a high amperage treshold (up to $1.5A$ such that the redesign fixed this issue.

Changes

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 $12V$ to $5V$ 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 $5v$ (given that the Expressif ESP8266 was determined to be $5V$ 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).

Software

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);
}
 

ham_radio/designing_a_remotely_controlled_motorized_antenna_commuter.txt · Last modified: 2024/10/09 05:56 by office

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.