The first two articles covered the GD6 as a product and its hardware design. This one gets into what developers care about most: how to use the code.

The GD6 firmware and web configuration system are open-source on GitHub (github.com/ManThink/gd6-open). This article walks through the repository structure, development environment setup, firmware compilation, multi-region builds, flashing, web interface customization, REST API usage, and the remote MQTT configuration channel.


Repository Overview

Basic Info

Item Details
Repository https://github.com/ManThink/gd6-open
Description LoRaWAN gateway based on ESP32
Primary language C (ESP-IDF framework)
Build system PlatformIO + ESP-IDF
License See LICENSE file in repo
Current version 1.0.2 (see version.txt)
Initial release July 23, 2026

Directory Structure

gd6-open/
├── src/                    # Main application code
│   ├── main.c              # Entry point, 6-phase boot
│   ├── ethernet_w5500.c    # W5500 Ethernet driver
│   ├── cellular_4g.c       # 4G PPP dial-up
│   ├── net_route_mgr.c     # Triple-link failover manager
│   ├── wifi_ap.c           # Wi-Fi AP + STA
│   ├── web_server.c        # HTTP REST API + static files
│   ├── config_rpc.c        # Config core (parse → validate → persist → apply)
│   ├── auth.c              # Login password + Token auth
│   ├── mqtt_bridge.c       # UDP↔MQTT bridge
│   ├── remote_config.c     # Remote MQTT config channel
│   ├── ota_update.c        # A/B dual-partition OTA
│   ├── sys_log.c           # In-memory ring buffer log
│   ├── uplink_driver.c     # pktfwd / station driver selection
│   ├── periph_power.c      # Peripheral power management
│   ├── status_led.c        # Status LED + watchdog
│   └── factory_reset.c     # Factory reset
├── components/             # Third-party components
│   ├── packet_forwarder/   # Semtech lora_pkt_fwd
│   ├── sx1302_hal/         # SX1302 HAL (Semtech v2.1.0 port)
│   ├── basicstation/       # Semtech BasicStation 2.0.6
│   └── esp_modem/          # 4G PPP modem component
├── frontend/               # Web UI source (Vite build)
├── data/                   # SPIFFS filesystem image
│   ├── web/                # Web UI static files (build output)
│   ├── global_conf.json    # Active radio + gateway config
│   ├── regions/            # LoRaWAN regional config templates
│   ├── eth_config.json     # Ethernet config
│   ├── wifi_ap.json        # Wi-Fi AP/STA config
│   ├── cellular_config.json # 4G config
│   ├── mqtt_bridge_config.json # MQTT bridge config
│   └── remote_config.json  # Remote config channel settings
├── scripts/                # Build scripts
│   ├── build_regions.py    # Per-region batch build
│   └── embed_defaults_copy.py # Default config sync
├── tools/                  # Flasher + crypto scripts
├── hardware/               # Hardware project files
├── partitions.csv          # Dual-OTA partition table
├── platformio.ini          # PlatformIO configuration
├── sdkconfig.defaults      # SDK configuration
└── CMakeLists.txt          # Top-level CMake

Key directories:

  • src/ contains the main application code. Each .c file corresponds to a functional module. Code is compiled with strict -Wall warnings.
  • components/ holds third-party components. The SX1302 HAL and Packet Forwarder are ported from Semtech's official source; the original reference code is preserved in .upstream/ for diffing before modifications.
  • frontend/ contains the Vue/Vite source for the web interface. Build output goes to data/web/.
  • data/ is the SPIFFS filesystem image — flashed to the gateway's configuration storage partition.

Relationship to Other ManThink Repositories

ManThink has other open-source repositories on GitHub:

Repository Purpose
ThinkLink ThinkLink IoT platform
TKL-EB-SDK EdgeBus SDK for edge device development
ThinkLink-docker ThinkLink platform Docker deployment

The GD6 repository is the gateway hardware open-source project. ThinkLink is the platform open-source project. They can be used together, but GD6 also works independently with ChirpStack, TTN, and other third-party network servers.


Development Environment Setup

Prerequisites

The GD6 build stack is PlatformIO + ESP-IDF. You do not need to install ESP-IDF separately — PlatformIO manages the framework version automatically.

You need:

  1. PlatformIO IDE: VS Code extension or PlatformIO Core CLI. See platformio.org/install.
  2. Python 3.x: Required by PlatformIO.
  3. Node.js 18+: Required if you want to modify the web frontend (frontend/).
  4. USB driver: ESP32-S3 USB serial driver (typically CP2102 or native USB CDC).

PlatformIO Version Locking

platformio.ini explicitly locks the platform version:

platform = espressif32 @ 6.13.0

This version corresponds to ESP-IDF 5.5.3. The comments specifically warn: do not use bare espressif32, as it will float to the latest version (7.0.x / IDF 6.0). IDF 6.0 removed the built-in mqtt component, which will cause compilation failures.

This is the first pitfall: if you update PlatformIO and compilation breaks, check whether the espressif32 version has changed.

Clone the Code

git clone https://github.com/ManThink/gd6-open.git
cd gd6-open

Verify the Environment

pio run                # Build firmware

If compilation succeeds and generates firmware.bin, your environment is ready. The first build downloads the ESP-IDF framework and dependencies, so it takes some time.


Compiling Firmware

Basic Build

pio run                          # Build firmware
pio run --target upload          # Flash firmware (via USB)
pio run --target uploadfs        # Flash SPIFFS image
pio device monitor               # Serial monitor (115200 baud)
pio run --target menuconfig      # SX1302 pin / SPI configuration
pio run --target clean           # Clean build/

Serial baud rate is locked to 115200. The platformio.ini comments explain: ESP-IDF console defaults to 115200; without this setting, pio run -t monitor would use 9600 and show garbled output.

Multi-Region Builds

The GD6 application code is frequency-agnostic — regional differences are entirely in global_conf.json. Regional templates are in data/regions/ (AS923 / EU868 / US915 / AU915 / CN470 / KR920 / IN865).

The scripts/build_regions.py script provides one-command builds:

# List available regions and center frequencies
python scripts/build_regions.py --list

# Set a region as development default (overwrites data/global_conf.json)
python scripts/build_regions.py --activate EU868
pio run && pio run -t uploadfs

# Build firmware + SPIFFS for all regions (output in dist/<REGION>/)
python scripts/build_regions.py

# Build specific regions only
python scripts/build_regions.py EU868 US915 AS923

# Rebuild SPIFFS only (faster when only config changed)
python scripts/build_regions.py --fs-only AS923

Build artifact naming (version from version.txt):

File Description Flash Offset
GD6-<ver>-<REGION>-full.bin Complete 8 MiB Flash image 0x0
GD6-<ver>-<REGION>-firmware.bin App firmware 0x20000
GD6-<ver>-<REGION>-spiffs.bin SPIFFS image 0x5A0000
GD6-<ver>-<REGION>-global_conf.json Config copy used at build time —
build_info.json Build metadata and offset table —

Note: RF front-end hardware must match the target band. 868 / 915 / 470 modules are not interchangeable — this is a physical constraint, not a software limitation (explained in detail in Article 2).


Flashing Firmware

Via USB Serial

For initial flashing or development debugging:

# Regular flash (firmware + SPIFFS)
pio run --target upload
pio run --target uploadfs

# Erase entire flash and write full image (initial flash or recovery)
python -m esptool --chip esp32s3 erase_flash
python -m esptool --chip esp32s3 write_flash 0x0 GD6-1.0.2-EU868-full.bin

Full erase overwrites all partitions and device data (including Gateway EUI). Back up any data you need to preserve first.

Via Web Console OTA

For online upgrades of deployed gateways:

  1. Open the web console → "Firmware Upgrade" page
  2. Verify current version and active partition
  3. Select a .bin firmware matching the gateway model, hardware version, and target region
  4. Wait for upload, verification, and write to complete
  5. Follow the prompt to restart; log in again and verify the version

OTA uses the A/B dual-partition mechanism: new firmware is written to the inactive partition, verified, then the boot partition is switched. Do not refresh the page, disconnect the network, or power off during the upgrade.

Via Browser Flasher

The repository's tools/flasher/ directory contains a browser-based flasher (built on ESP Web Serial API) that supports flashing firmware directly via USB in Chrome/Edge. This tool also integrates ESP32-S3 external watchdog (TPL5010) feed control, plus abort and exception reset protection.

Do Not Confuse Firmware Types

The manual explicitly warns: do not upload a full Flash manufacturing image (-full.bin) as a Web OTA firmware. Web OTA only accepts app firmware (-firmware.bin), because OTA writes to the inactive partition, not the entire Flash.


Partition Table and Flash Layout

GD6 uses a custom partition table (partitions.csv). The 8 MB Flash is divided as follows:

Partition Type Offset Size Description
nvs data/nvs 0x9000 24 KB Non-volatile storage (passwords, etc.)
otadata data/ota 0xF000 8 KB OTA boot slot record
phy_init data/phy 0x11000 4 KB RF PHY initialization
ota_0 app 0x20000 2.75 MB Firmware slot A
ota_1 app 0x2E0000 2.75 MB Firmware slot B
storage data/spiffs 0x5A0000 2.375 MB Web UI + config files

The table fills exactly 8 MB. Two OTA partitions are 2.75 MB each; the SPIFFS partition is 2.375 MB. If firmware exceeds 2.75 MB, the partition table needs to be adjusted.


Web Configuration System Customization

Frontend Source Structure

The web interface source is in frontend/, built with Vite:

cd frontend && npm install       # First-time dependency install
cd frontend && npm run build      # Build to data/web/
pio run --target uploadfs         # Flash to SPIFFS
cd frontend && npm run dev        # Local dev server

Build output goes to data/web/ (index.html, app.js, style.css), then is flashed as part of the SPIFFS image.

Do Not Edit data/web/ Directly

The README explicitly warns: do not modify data/web/* directly — they are build artifacts. The correct approach is to modify the source in frontend/, then run npm run build.

If you need to change web interface text, styles, or layout:

  1. Modify the source in frontend/
  2. npm run build to generate new data/web/ files
  3. pio run --target uploadfs to flash to the gateway

Local Development Debugging

npm run dev starts a Vite local dev server. /api/* requests are proxied to the target gateway via Vite proxy. This lets you modify frontend code and preview changes in real time without flashing.

web/ Directory Protection

Overwriting web/ files online may render the console inaccessible, and factory reset does not restore damaged web files. If you need to update the web interface online, ensure you have serial access as a fallback recovery method.


REST API Usage

Authentication

The GD6 REST API uses Token authentication:

  1. Call POST /api/login with the password to obtain a Token
  2. Include the Token in the header for subsequent requests
  3. Token is shared with the web login password — changing the password invalidates old tokens

Default login password: wifi!0804 (change immediately after first login).

API Endpoint List

Method Path Description
GET /api/status System/network/LoRa/SPIFFS status
POST /api/login · /api/logout Login / logout
PUT /api/password Change password
GET /api/auth_state Auth state query
GET/PUT /api/ethernet Ethernet config
POST /api/ethernet/power Ethernet power control
GET/PUT /api/wifi Wi-Fi config
GET /api/wifi/scan Wi-Fi scan
GET/PUT /api/lora LoRa gateway config
GET/PUT /api/cellular 4G config
GET /api/cellular/status 4G real-time status
GET/PUT /api/mqtt_bridge MQTT bridge config
GET /api/mqtt_bridge/status MQTT bridge status
POST/DELETE /api/mqtt_bridge/cert MQTT bridge certificates
GET/PUT /api/remote_config Remote config channel settings
GET /api/remote_config/status Remote config status
POST/DELETE /api/remote_config/cert Remote config certificates
GET /api/station BasicStation status
GET /api/logs Logs (incremental)
DELETE /api/logs Clear logs
GET /api/logs/download Download logs
GET /api/fs File listing
POST /api/fs/upload Upload file
GET /api/firmware Firmware info
POST /api/firmware/upload OTA upload
POST /api/restart Restart LoRa service or system

Usage Examples

Get system status:

# Login to get Token
TOKEN=$(curl -s -X POST http://192.168.4.1/api/login \
  -H "Content-Type: application/json" \
  -d '{"password":"wifi!0804"}' | jq -r '.token')

# Query system status
curl -s http://192.168.4.1/api/status \
  -H "Authorization: Bearer $TOKEN" | jq

Set Ethernet to static IP:

curl -s -X PUT http://192.168.4.1/api/ethernet \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "static",
    "ip": "192.168.1.200",
    "netmask": "255.255.255.0",
    "gateway": "192.168.1.1",
    "dns1": "8.8.8.8"
  }'

Batch management script for multiple gateways:

#!/bin/bash
# Batch query gateway status
GATEWAYS=("192.168.1.101" "192.168.1.102" "192.168.1.103")
PASSWORD="your_password"

for GW in "${GATEWAYS[@]}"; do
  TOKEN=$(curl -s -X POST "http://$GW/api/login" \
    -H "Content-Type: application/json" \
    -d "{\"password\":\"$PASSWORD\"}" | jq -r '.token')

  echo "=== $GW ==="
  curl -s "http://$GW/api/status" \
    -H "Authorization: Bearer $TOKEN" | jq '{firmware: .firmware_ver, uptime: .uptime, lora: .lora_status}'
done

Remote MQTT Configuration Channel

Difference from MQTT Bridge

The GD6 has two independent MQTT connections — do not confuse them:

MQTT Bridge Remote MQTT Config Channel
Purpose LoRaWAN data forwarding Remote device configuration read/write
Config file mqtt_bridge_config.json remote_config.json
Code module src/mqtt_bridge.c src/remote_config.c
Connection Independent TLS connection Independent TLS connection
Auth Broker credentials Device login password (RPC auth)

The two connections are fully isolated — do not mix Topics or credentials.

Security Constraint

The remote channel has a critical security design: the remote channel cannot modify its own Broker settings.

This prevents remote misconfiguration from causing device disconnection. If the remote channel could modify its own Broker address or credentials, a single erroneous configuration could permanently sever the device's remote management capability.

Configuration

Configure via the web console → "Remote Config" page:

  1. Enter Broker URI, Client ID, username/password
  2. Configure Topic and QoS
  3. Upload TLS certificates (if needed)
  4. Monitor connection status

Once configured, you can remotely read and write allowed device configuration items via MQTT messages and trigger reboots.


SPIFFS Configuration Files

Configuration File Overview

File Description
/spiffs/global_conf.json SX1302 radio params + full gateway config (includes uplink_driver selection)
/spiffs/eth_config.json Ethernet DHCP / static IP
/spiffs/wifi_ap.json Wi-Fi AP/STA config
/spiffs/cellular_config.json 4G APN / auth / priority
/spiffs/mqtt_bridge_config.json MQTT bridge broker / account / TLS / topic
/spiffs/remote_config.json Remote config channel broker / account / TLS / topic

These JSON files also serve as factory reset targets — they are embedded into the firmware. Factory reset extracts the default values from the firmware and overwrites the SPIFFS copies.

Configuration Application Flow

Configuration JSON changes require a gateway reboot to take effect. Once SPIFFS writes to Flash, the data is persistent and does not depend on RAM — it survives power loss, but requires a reboot to load the new configuration.

Managing Files via Web Console

The web console's "File System" page provides direct management of /spiffs files, supporting viewing and editing configuration JSON. This opens a path for deep customization — you can modify configuration fields directly, beyond the options exposed in the web interface.


Uplink Driver: Packet Forwarder vs Basic Station

The uplink_driver field in global_conf.json selects which driver occupies the SX1302. The two are mutually exclusive:

Driver Value Description
Packet Forwarder pktfwd Semtech UDP packet forwarder (default)
Basic Station station Semtech BasicStation 2.0.6, wss/TLS to LNS

Switching drivers requires a gateway reboot. In Basic Station mode, the channel plan is provisioned by the LNS, and credentials/config are stored in separate SPIFFS files (tc.* / cups.*).


Boot Process

Understanding the boot process helps with troubleshooting. app_main() (src/main.c) boots in 6 phases:

Phase Action
Phase 0 NVS init + event loop (status LED and factory reset button ready early)
Phase 1 Mount SPIFFS — mount failure disables web server and packet forwarder
Phase 2 Start Wi-Fi Soft-AP (async, non-blocking)
Phase 3 Route manager starts W5500 Ethernet → 4G PPP, primary/backup failover
Phase 4 Start HTTPD (REST API + static files) + remote MQTT config channel
Phase 5 SX1302 self-test → start packet forwarder or BasicStation per uplink_driver

Design principle: later phases tolerate non-fatal failures in earlier phases, ensuring the device can always enter recovery mode via Wi-Fi AP. Even if SPIFFS mount fails or SX1302 self-test fails, the web console remains accessible.


Secondary Development Examples

Example 1: Custom Channel Configuration

If you need non-standard channel configuration (e.g., custom center frequencies), modify data/global_conf.json directly:

python scripts/build_regions.py --activate EU868
# Then edit radio_0 / radio_1 center frequencies in data/global_conf.json
pio run --target uploadfs

Example 2: Integrate with Custom Operations System

Use the REST API to integrate GD6 gateways into your own operations system:

  • Poll /api/status periodically to collect gateway runtime status
  • Call /api/logs to fetch logs
  • Use /api/remote_config to batch-push configuration changes
  • Use /api/restart for remote restarts

Example 3: Custom Web Interface

Modify the Vue source in frontend/ to add custom pages or modify existing interfaces, then npm run build + pio run --target uploadfs.


Common Issues

Q: Compilation fails with mqtt component not found?

A: Check that platform = espressif32 @ 6.13.0 in platformio.ini has not changed. IDF 6.0 removed the built-in mqtt component — you must lock to 6.13.0 (IDF 5.5.3).

Q: Garbled serial output after flashing?

A: Confirm the baud rate is 115200. platformio.ini sets monitor_speed = 115200.

Q: Web OTA firmware upload fails to boot?

A: Check whether you uploaded app firmware (-firmware.bin) or full image (-full.bin). Web OTA only accepts app firmware. If a full image was uploaded, wait for automatic restart. If still inaccessible, use Wi-Fi AP or controlled serial recovery — do not repeatedly power-cycle.

Q: Web console won't open after factory reset?

A: Factory reset does not restore the web/ directory. If web/ files were previously overwritten and damaged, reflash the SPIFFS image via serial (pio run --target uploadfs).

Q: Modified data/ config but changes not taking effect?

A: After modifying files in data/, you must run pio run --target uploadfs to flash to SPIFFS. After modifying SPIFFS files online, a gateway reboot is required for changes to take effect.


Summary

The GD6 open-source repository is not a shell — it contains the complete firmware source, web interface source, build toolchain, and hardware project files. From cloning the code to compiling usable firmware, the entire process can be completed in under 30 minutes.

Key takeaways:

  1. PlatformIO version locked to 6.13.0 — do not float to the latest version
  2. Use build_regions.py for multi-region builds — do not manually edit global_conf.json region fields
  3. Edit frontend/, not data/web/ — the latter are build artifacts
  4. REST API and remote MQTT channel provide full programmatic management capabilities
  5. Two MQTT connections are fully isolated — data forwarding and remote config cannot be mixed

The next article moves into deployment — from unboxing to connecting ThinkLink / ChirpStack / TTN, walking through the entire deployment process step by step.


Related Links


ManThink — Making IoT development more efficient.