# How to compile the RNode firmware yourself

This document describes my efforts to add a new board to the Reticulum RNode firmware. It is published in the hope that it is useful to other makers who want to make their own RNodes.

## Required knowledge

To follow this manual, some basic programming knowledge like using text editors to edit source code and using basic commands like `make` is required. Plus some basic Linux knowledge. But you don't have to be an expert.

## Dependencies

Make sure *git*, *make*, *python* and *pip* are installed. Most likely these are already available on your system. Otherwise do:

    $ sudo apt install git
    $ sudo apt install make
    $ sudo apt install pip
    $ sudo apt install python

Install Reticulum:

    $ pip install rns --break-system-packages

Add new directories to path:

    $ nano ~/.bashrc

Add `export PATH=~/.local/bin:~/bin:$PATH` to the end of the file. Exit with `CTRL-x` and type `y` to save the changes.

Now reload the shell so the changes to the path will take effect: `exec $SHELL`. You can also logout and login again.

Download arduino-cli:

    $ cd
    $ curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh

## Clone git repo

    $ cd
    $ git clone https://github.com/liberatedsystems/RNode_Firmware_CE.git

## Prepare

Install the required BSP and libraries for the ESP32 system.

    $ cd RNode_Firmware_CE/
    $ make prep-esp32

Sometimes this command stalls. If this happens, stop the command by hiting `CTRL-c` and restarted it.

## Test

To test if you can compile the firmware try it:

    $ make firmware-heltec32_v3

Be patient, this can take up to 15 minutes to complete. Wait for the command prompt to return. You should not see any errors.

## Define a new board

Go to the directory `~/RNode_Firmware_CE`. This is the source code of the RNode firmware. Here you will find all the files needed to compile the firmware. You can edit these files with any text editor you like.

Let's say we want to add support for our homebrew ESP32-S3 with an SX1278 LoRa transceiver. First we have to define a BOARD_MODEL. This is an 8 bit value that is used in the source code to select the right code for the hardware. Let's choose  0x61 as the BOARD_MODEL as this number is not used yet. Optional we can also define a BOARD_VARIANT. This defines the variant of the board. A board could come with different LoRa transceivers, for example. It is also a unique 8 bit number. For now, we ignore this BOARD_VARIANT and only use the BOARD_MODEL.

### Makefile

The source code is compiled using `make` and the file called *Makefile*.

Every target board has a section `firmware-<board_name>`, `upload-<board-name>` and `release-<board-name>`. In the Makefile, three new sections for our new board have to be added. The exact place is not critical. But it is good practice to put them alongside the already present sections for firmware, upload and release.

```
# Added board from Mees Electronics
firmware-mees-esp32-s3:
	    arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\""
```

```
# Added board from Mees Electronics
upload-mees-esp32-s3:
    arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:esp32s3
    @sleep 1
    rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware_CE.ino.bin)
    @sleep 3
    python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
```

```
release-mees-esp32-s3:
    arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\""
    cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_mees-esp32-s3.boot_app0
    cp build/esp32.esp32.mees-esp32-s3/RNode_Firmware_CE.ino.bin build/rnode_firmware_mees-esp32-s3.bin
    cp build/esp32.esp32.mees-esp32-s3/RNode_Firmware_CE.ino.bootloader.bin build/rnode_firmware_mees-esp32-s3.bootloader
    cp build/esp32.esp32.mees-esp32-s3/RNode_Firmware_CE.ino.partitions.bin build/rnode_firmware_mees-esp32-s3.partitions
    zip --junk-paths ./Release/rnode_firmware_mees-esp32-s3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_mees-esp32-s3.boot_app0 build/rnode_firmware_mees-esp32-s3.bin build/rnode_firmware_mees-esp32-s3.bootloader build/rnode_firmware_mees-esp32-s3.partitions
    rm -r build
```

The `upload-mees-esp32-s3` section assumes the ESP32 S3 is connected to serial port /dev/ttyACM0. On your system, this could be another port. Manual change all the `/dev/ttyACM0` to the port on your system.

### Boards.h

The file `Board.h` is the most important file to edit. This is the place where the supported boards are defined. For our new board, a new section has to be added. This section defines the pinout of the SPI port and if it has certain peripherals such as a screen or BLE.

First, the new board has to be defined at the beginning of the file. Search for the line `#define MODEL_FF  0xFF // Homebrew board, max 14dBm output power` and add the new definition below that line:

    // Board added by Mees Electronics
    #define BOARD_MEES_ESP32_S3 0x61 // Mees electronics ESP32 S3

Next, search for the line `#if MCU_VARIANT == MCU_ESP32`. This is the part where all the ESP32 variant are defined. This section stops at the line `#elif MCU_VARIANT == MCU_NRF52`. From here the definitions for the NRF52 board begin.

Somewhere between these to lines we have to add our board definition. It is good practice to add it at the end, just before the following code:

    #else
      #error An unsupported ESP32 board was selected. Cannot compile RNode firmware.
    #endif

The new code:

    // Board definition added by Mees Electronics
    #elif BOARD_MODEL == BOARD_MEES_ESP32_S3
      #define IS_ESP32S3 true
      #define HAS_DISPLAY false
      #define HAS_BLUETOOTH false
      #define HAS_BLE true
      #define HAS_PMU true
      #define HAS_CONSOLE true
      #define HAS_EEPROM true
      #define INTERFACE_COUNT 1

      #define HAS_NP true
      const int pin_np = 21;
      #if HAS_NP == false
        #if defined(EXTERNAL_LEDS)
          const int pin_led_rx = 16;
          const int pin_led_tx = 17;
        #else
          const int pin_led_rx = 21;
          const int pin_led_tx = 21;
        #endif
      #endif

      const uint8_t interfaces[INTERFACE_COUNT] = {SX1278};
      const bool interface_cfg[INTERFACE_COUNT][3] = {
                    // SX1278
          {
              false, // DEFAULT_SPI
              false, // HAS_TCXO
              false  // DIO2_AS_RF_SWITCH
          },
      };
      const int8_t interface_pins[INTERFACE_COUNT][10] = {
                  // SX1278
          {
               10, // pin_ss
               12, // pin_sclk
               11, // pin_mosi
               13, // pin_miso
               -1, // pin_busy
               15, // pin_dio
               14, // pin_reset
               -1, // pin_txen
               -1, // pin_rxen
               -1  // pin_tcxo_enable
          }
      };

A lot of the abilities of the board is defined here. Most of it is self explanatory. The above values should be good enough to get you started.

|Name|What does it do|
|----|-------|
|IS_ESP32S3|Is the board an ESP32 S3|
|HAS_DISPLAY| Has the board a display|
|HAS_BLUETOOTH| Has the board Bluetooth|
|HAS_BLE|Has the board Bluetooth LE|
|HAS_PMU|Unknown yet|
|HAS_CONSOLE|RNode Bootstrap Console|
|HAS_EEPROM|All ESP32 have EEPROM|
|INTERFACE_COUNT|Unknown, set to 1|
|HAS_NP|Has the board a NeoPixel|
|EXTERNAL_LEDS|Has the board external LEDs|

The SPI bus is defined in the structs `interfaces`, `interface_cfg` and `interface_pins`. The comments in the code explain it well enough, I think. When the value of `interface_cfg[0]` is set to *false*, we can define all the IO pins in the struct `interface_pins`. The numbers are the GPIO numbers of the ESP32 S3. So 10 means GPIO10. If the value is set to -1 the pin is not used. For example, the SX1278 transceiver doesn't have a busy pin, so it is set to -1.

### Utilities.h

In this file, the routines for the optional external LEDs are defined. If the new board has a NeoPixel, you don't have to change these. But keep reading, because there are some other functions you have to change even if your board has a NeoPixel!

Search for the line `#if MCU_VARIANT == MCU_ESP32`. Because there are several of these lines, make sure you have the right one: the next line should be `#if HAS_NP == true`. Here you will find a bunch of `#elif BOARD_MODEL ==` lines. Find the last one of this if block, which ends with `#endif`. Add the new section for our board just before the `#endif`:

    	#elif BOARD_MODEL == BOARD_MEES_ESP32_S3
            void led_rx_on()  { digitalWrite(pin_led_rx, HIGH); }
            void led_rx_off() {	digitalWrite(pin_led_rx, LOW); }
            void led_tx_on()  { digitalWrite(pin_led_tx, HIGH); }
            void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
            void led_id_on()  { }
            void led_id_off() { }

Search for the code `bool eeprom_model_valid() {`. This code checks if the BOARD_MODEL, which is stored in the EEPROM corresponds with the running firmware version. If these are not the same, the firmware will not boot.

After adding our new board section, the code looks something like this:

```
#if BOARD_MODEL == BOARD_RNODE
	if (model == MODEL_A4 || model == MODEL_A9 || model == MODEL_FF || model == MODEL_FE) {
	#elif BOARD_MODEL == BOARD_RNODE_NG_20
	if (model == MODEL_A3 || model == MODEL_A8) {
	#elif BOARD_MODEL == BOARD_RNODE_NG_21
	if (model == MODEL_A2 || model == MODEL_A7) {
	#elif BOARD_MODEL == BOARD_T3S3
	if (model == MODEL_A1 || model == MODEL_A6 || model == MODEL_A5 || model == MODEL_AA || model == MODEL_AC) {
	#elif BOARD_MODEL == BOARD_HMBRW
	if (model == MODEL_FF || model == MODEL_FE) {
	#elif BOARD_MODEL == BOARD_TBEAM
	if (model == MODEL_E4 || model == MODEL_E9 || model == MODEL_E3 || model == MODEL_E8) {
	#elif BOARD_MODEL == BOARD_TDECK
	if (model == MODEL_D4 || model == MODEL_D9) {
	#elif BOARD_MODEL == BOARD_TECHO
	if (model == MODEL_16 || model == MODEL_17) {
	#elif BOARD_MODEL == BOARD_TBEAM_S_V1
	if (model == MODEL_DB || model == MODEL_DC) {
	#elif BOARD_MODEL == BOARD_LORA32_V1_0
	if (model == MODEL_BA || model == MODEL_BB) {
	#elif BOARD_MODEL == BOARD_LORA32_V2_0
	if (model == MODEL_B3 || model == MODEL_B8) {
	#elif BOARD_MODEL == BOARD_LORA32_V2_1
	if (model == MODEL_B4 || model == MODEL_B9) {
	#elif BOARD_MODEL == BOARD_HELTEC32_V2
	if (model == MODEL_C4 || model == MODEL_C9) {
	#elif BOARD_MODEL == BOARD_HELTEC32_V3
	if (model == MODEL_C5 || model == MODEL_CA) {
    #elif BOARD_MODEL == BOARD_H_W_PAPER
    if (model == MODEL_C8) {
    #elif BOARD_MODEL == BOARD_HELTEC_T114
    if (model == MODEL_C6 || model == MODEL_C7) {
    #elif BOARD_MODEL == BOARD_RAK4631
    if (model == MODEL_11 || model == MODEL_12 || model == MODEL_13 || model == MODEL_14) {
    #elif BOARD_MODEL == BOARD_OPENCOM_XL
    if (model == MODEL_21) {
	#elif BOARD_MODEL == BOARD_HUZZAH32
	if (model == MODEL_FF) {
	#elif BOARD_MODEL == BOARD_HMBRW
	if (model == MODEL_FF || model == MODEL_FE) {
	#elif BOARD_MODEL == BOARD_GENERIC_ESP32
	if (model == MODEL_FF || model == MODEL_FE) {
	// Added by Mees Electronics
	#elif BOARD_MODEL == BOARD_MEES_ESP32_S3
	if (model == MODEL_A4) {
	#else
```

Last, find the line `void setTXPower(RadioInterface* radio, int txp) {`. This is the code that handles the RF power settings of the LoRa transceiver. Here you have to add the proper code for the LoRa module you use. After adding the code for the SX1278 module we use in this example the code should look something like this:

```
if (model == MODEL_A1) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_A2) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_A3) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
    if (model == MODEL_A4) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
    if (model == MODEL_A5) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
    if (model == MODEL_A6) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_A7) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_A8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_A9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_AA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_AC) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);

    if (model == MODEL_B3) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_B4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_B8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_B9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_BA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_BB) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);

    if (model == MODEL_C4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_C9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_C5) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_C6) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
    if (model == MODEL_C7) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
    if (model == MODEL_CA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);

    if (model == MODEL_D4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_D9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);

    if (model == MODEL_DB) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_DC) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);

    if (model == MODEL_E4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_E9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_E3) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_E8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);

    if (model == MODEL_FE) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
    if (model == MODEL_FF) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);

     // Added by Mees Electronics
    if (model == MODEL_A4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
```

The `MODEL_A4` is an 8 bit value read from the EEPROM. This is the value we will write to the EEPROM later in this document with the command `rnodeconf /dev/ttyACM0 -r --platform ESP32 --model a4 --product f0 --hwrev 3`. The model number a4 is chosen from a list published in the file [https://git.meezenest.nl/marcel/RNode_Firmware_CE/src/branch/master/Documentation/BUILDING.md](https://git.meezenest.nl/marcel/RNode_Firmware_CE/src/branch/master/Documentation/BUILDING.md). It is shared with another board and it is probably better to choose a unique value. But `rnodeconf` uses this value and choosing a new number breaks Reticulum. Further research is needed. But for now let's use a4, because it works.

### Display.h

The file **display.h** also has a board specific definition section. If the new board has a display, make sure there is a proper entry for it.  Our example board doesn't have a display, so we don't have to change this file. If your board has a display, make sure you make the proper changes.

### Power.h

The file **Power.h** must also have a board definition. And also proper definitions in **void measure_battery()** and **bool init_pmu()**. These are more elaborate and not yet fully understood, but only used when the board has a battery installed. Our example board doesn't have a battery, so we don't have to change this file. If your board has a display, make sure you make the proper changes.

### Compiling the code and flashing the board

Make sure you are in the dialout group. If not:

    $ sudo usermod -aG dialout <username>

If you are flashing a custom board, you will need to generate a signing key in rnodeconf prior to flashing if you do not already have one by running:

    $ rnodeconf -k

Compile the code:

    $ make firmware-mees-esp32-s3

Than flash the firmware:

    $ make upload-mees-esp32-s3

This first time you flash a new board, this command will end with the error `This device has not been provisioned yet, cannot set firmware hash`. That is because the EEPROM is not programmes yet You will need to provision the EEPROM before use:

    $ rnodeconf /dev/ttyACM0 -r --platform ESP32 --model a4 --product f0 --hwrev 3

### Migrating to another system

When the rnode is flashed, it is signed with a key. If you connect the RNode to another system, this key cannot be validated. It is possible to write a new key to the RNode by erasing the eeprom and writing the local key to it.

    $ rnodeconf /dev/ttyACM0 --eeprom-wipe
    $ rnodeconf /dev/ttyACM0 -r --platform ESP32 --model a4 --product f0 --hwrev 3

## Adding support for SX1268 module

### Boards.h

Change the following section:

````
      const uint8_t interfaces[INTERFACE_COUNT] = {SX1278};
      const bool interface_cfg[INTERFACE_COUNT][3] = {
                    // SX1278
          {
              false, // DEFAULT_SPI
              false, // HAS_TCXO
              false  // DIO2_AS_RF_SWITCH
          },
      };
      const int8_t interface_pins[INTERFACE_COUNT][10] = {
                  // SX1278
          {
               10, // pin_ss
               12, // pin_sclk
               11, // pin_mosi
               13, // pin_miso
               -1, // pin_busy
               15, // pin_dio
               14, // pin_reset
               -1, // pin_txen
               -1, // pin_rxen
               -1  // pin_tcxo_enable
          }
      };
````

with this:

````
      #if BOARD_VARIANT == MODEL_F1
      const uint8_t interfaces[INTERFACE_COUNT] = {SX1278};
      const bool interface_cfg[INTERFACE_COUNT][3] = {
                    // SX1278
          {
              false, // DEFAULT_SPI
              false, // HAS_TCXO
              false  // DIO2_AS_RF_SWITCH
          },
      };
      const int8_t interface_pins[INTERFACE_COUNT][10] = {
                  // SX1278
          {
               10, // pin_ss
               12, // pin_sclk
               11, // pin_mosi
               13, // pin_miso
               -1, // pin_busy
               15, // pin_dio
               14, // pin_reset
               -1, // pin_txen
               -1, // pin_rxen
               -1  // pin_tcxo_enable
          }
      };
      #elif BOARD_VARIANT == MODEL_F2
      const uint8_t interfaces[INTERFACE_COUNT] = {SX1262};
      const bool interface_cfg[INTERFACE_COUNT][3] = {
                    // SX1262
          {
              false, // DEFAULT_SPI
              true, // HAS_TCXO
              true  // DIO2_AS_RF_SWITCH
          },
      };
      const int8_t interface_pins[INTERFACE_COUNT][10] = {
                  // SX1262
          {
              10, // pin_ss
              12, // pin_sclk
              11, // pin_mosi
              13, // pin_miso
              16, // pin_busy
              15, // pin_dio
              14, // pin_reset
              -1, // pin_txen
              17, // pin_rxen
              -1  // pin_tcxo_enable
          }
      };
      #endif
````

### Radio.cpp

Add the following statement to the function **sx126x::enableTCXO()**:

    // Board added by Mees Electronics
    #elif BOARD_MODEL == BOARD_MEES_ESP32_S3
      uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};

Add the following statement to the function **int sx126x::endPacket()** just after `setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);`:

    // Added by Mees Electronics: RXEN on E22 module back to low
    #if BOARD_MODEL == BOARD_MEES_ESP32_S3 && BOARD_VARIANT == MODEL_F2
    if (_rxen != -1) {
      digitalWrite(_rxen, LOW);
    }
    #endif

The E22 module has an RXEN and a TXEN input. Both must be used and both cannot be logic 1 at the same time. RXEN must be controlled by the firmware while TXEN can be controlled by either the firmware or the DIO2 pin, by using the DIO2_AS_RF_SWITCH function in the firmware. My current solution is to control TXEN by the DIO2 pin. If you forget to control the TXEN line, the maximum output will be 22 dBm instead of 33 dBm. It would be better to alter the code to make it more universal (like with the *sx128x* code), but for now it works.

### Makefile

Change the following section:

````
# Added board from Mees Electronics
firmware-mees-esp32-s3:
	arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\""
````

with this:

````
# Added board from Mees Electronics
firmware-mees-esp32-s3:
	arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x61\" \"-DBOARD_VARIANT=0xF2\""
````

If you want to compile the board for the SX1278 just change **BOARD_VARIANT** to *0xF1*.

# License

(C) 2025 M.T. Konstapel https://meezenest.nl/mees

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Many thanks to Tom ON3DHC for proofreading this document.
