Skip to content

Downlink support for multiple antennas - #8018

Open
vlasebian wants to merge 3 commits into
v3.37from
feat/downlink-support-for-multiple-antennas
Open

Downlink support for multiple antennas#8018
vlasebian wants to merge 3 commits into
v3.37from
feat/downlink-support-for-multiple-antennas

Conversation

@vlasebian

@vlasebian vlasebian commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

References: #48.

Summary

Changes

  • Drop the DOWNLINK_PATH_CONSTRAINT_NEVER constraint to allow using other antennas for the downlink path, not only antenna 0.
  • Align the antenna gains with the frequency plans and set them in the LBS router_config. Fallback on the gain of the first antenna if fewer antennas are registered than frequency plans.

Testing

Using a local running instance of The Things Stack, I've used the examples in the https://github.com/lorabasics/basicstation repository to simulate a gateway.

Steps

Test setup:

station.conf

{
    /* If slave-X.conf present this acts as default settings */
    "SX1301_conf": {                 /* Actual channel plan is controlled by server */
        "lorawan_public": true,      /* is default */
        "clksrc": 1,                 /* radio_1 provides clock to concentrator */
        "device": "spidev",
        "radio_0": {
            /* freq/enable provided by LNS - only HW specific settings listed here */
            "type": "SX1257",
            "rssi_offset": -166.0,
            "tx_enable": true,
            "antenna_gain": 0,
            "antenna_type": "sector"
        },
        "radio_1": {
            "type": "SX1257",
            "rssi_offset": -166.0,
            "tx_enable": false
        }
        /* chan_multiSF_X, chan_Lora_std, chan_FSK provided by LNS */
    },
    "station_conf": {
        "routerid": "AA:A0:CB:FF:FE:80:0D:B1",
        /* "log_file":  "station.log", */
        "log_file":  "stderr",
        "log_level": "XDEBUG",  /* XDEBUG,DEBUG,VERBOSE,INFO,NOTICE,WARNING,ERROR,CRITICAL */
        "log_size":  10000000,
        "log_rotate":  3,
        /* required for success checks of tests */
        "nodc": true,
        "CLASS_C_BACKOFF_BY": "100ms",
        "CLASS_C_BACKOFF_MAX": 10
    }
}

Gateway Setup

ttn-lw-cli gateways create sim2ant --user-id admin --gateway-eui "AAA0CBFFFE800DB1" --frequency-plan-ids EU_863_870,EU_863_870
ttn-lw-cli gateways set sim-2ant --antenna.gain 3 --antenna.add
ttn-lw-cli gateways set sim-2ant --antenna.index 1 --antenna.gain 6 --antenna.add

I started the gateway and watched the requests between TTS and the simulated LBS.

Results

These can be seen in the station logs:

  • "hwspec":"sx1301/2" — the GS declared two boards
  • sx1301_conf[0] carries "antenna_gain": 3 and sx1301_conf[1] carries "antenna_gain": 6, on each board's radios
  • The station's master accepted the config:
2026-09-04 13:21:21.020 [RAL:INFO] Region plan hwspec 'sx1301/2' mapped to 2 slaves 'sx1301/1'
2026-09-04 13:21:21.020 [RAL:INFO] Master sending 713 bytes of JSON sx1301conf to slave (0)
2026-09-04 13:21:21.020 [RAL:INFO] Master sending 573 bytes of JSON sx1301conf to slave (1)
2026-09-04 13:21:21.020 [S2E:INFO] Configuring for region: EU868 -- 863.0MHz..870.0MHz
Full logs.

2026-09-04 13:21:21.017 [AIO:XDEB] [5|WS] > {"msgtype":"version","station":"2.0.6(linux/testms)","firmware":null,"package":null,"model":"linux","protocol":2,"features":"rmtsh"}
2026-09-04 13:21:21.017 [AIO:XDEB] [5] socket write bytes=140
2026-09-04 13:21:21.018 [AIO:XDEB] [5] socket read  bytes=1712
2026-09-04 13:21:21.018 [AIO:XDEB] [5|WS] < {"msgtype":"router_config","NetID":null,"JoinEui":null,"region":"EU863","hwspec":"sx1301/2","freq_range":[863000000,870000000],"DRs":[[12,125,0],[11,125,0],[10,125,0],[9,125,0],[8,125,0],[7,125,0],[7,250,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"sx1301_conf":[{"radio_0":{"enable":true,"freq":867500000,"antenna_gain":3},"radio_1":{"enable":true,"freq":868500000,"antenna_gain":3},"chan_multiSF_0":{"enable":true,"radio":1,"if"
2026-09-04 13:21:21.019 [AIO:XDEB] [5|WS] . :-400000},"chan_multiSF_1":{"enable":true,"radio":1,"if":-200000},"chan_multiSF_2":{"enable":true,"radio":1,"if":0},"chan_multiSF_3":{"enable":true,"radio":0,"if":-400000},"chan_multiSF_4":{"enable":true,"radio":0,"if":-200000},"chan_multiSF_5":{"enable":true,"radio":0,"if":0},"chan_multiSF_6":{"enable":true,"radio":0,"if":200000},"chan_multiSF_7":{"enable":true,"radio":0,"if":400000},"chan_Lora_std":{"enable":true,"radio":1,"if":-200000,"bandwidth":250000,"
2026-09-04 13:21:21.019 [AIO:XDEB] [5|WS] . spread_factor":7},"chan_FSK":{"enable":true,"radio":1,"if":300000,"datarate":50000}},{"radio_0":{"enable":true,"freq":867500000,"antenna_gain":6},"radio_1":{"enable":true,"freq":868500000,"antenna_gain":6},"chan_multiSF_0":{"enable":true,"radio":1,"if":-400000},"chan_multiSF_1":{"enable":false},"chan_multiSF_2":{"enable":false},"chan_multiSF_3":{"enable":false},"chan_multiSF_4":{"enable":false},"chan_multiSF_5":{"enable":false},"chan_multiSF_6":{"enable":fal
2026-09-04 13:21:21.019 [AIO:XDEB] [5|WS] . se},"chan_multiSF_7":{"enable":false},"chan_Lora_std":{"enable":true,"radio":1,"if":-200000,"bandwidth":250000,"spread_factor":7},"chan_FSK":{"enable":true,"radio":1,"if":300000,"datarate":50000}}],"nocca":true,"nodc":true,"nodwell":true,"MuxTime":1788528081.019047,"bcning":{"DR":3,"layout":[2,8,17],"freqs":[869525000]}}
2026-09-04 13:21:21.020 [RAL:INFO] Region plan hwspec 'sx1301/2' mapped to 2 slaves 'sx1301/1'
2026-09-04 13:21:21.020 [RAL:INFO] Master sending 713 bytes of JSON sx1301conf to slave (0)
2026-09-04 13:21:21.020 [RAL:INFO] Master sending 573 bytes of JSON sx1301conf to slave (1)
2026-09-04 13:21:21.020 [S2E:INFO] Configuring for region: EU868 -- 863.0MHz..870.0MHz

These can be seen in the live data of the gateway for simulated uplinks.

This uplink does not have any antenna index because because proto3 JSON omits the zero-valued:

"rx_metadata": [
  {
    "gateway_ids": {
      "gateway_id": "sim-2ant",
      "eui": "AAA0CBFFFE800DB1"
    },
    "time": "2026-09-04T12:21:11.046260118Z",
    "timestamp": 388669041,
    "rssi": -50,
    "channel_rssi": -50,
    "snr": 9,
    "uplink_token": "ChYKFAoIc2ltLTJhbnQSCKqgy//+gA2xEPG8qrkBGgsIt+/q1AYQkLCzHCDo0uvzpws=",
    "received_at": "2026-09-04T12:21:11.055968989Z"
  }
]

But this uplink has the antenna index, the uplink token and there is no DOWNLINK_PATH_CONSTRAINT_NEVER:

"rx_metadata": [
  {
    "gateway_ids": {
      "gateway_id": "sim-2ant",
      "eui": "AAA0CBFFFE800DB1"
    },
    "antenna_index": 1, // antenna_index appears here.
    "time": "2026-09-04T12:21:21.061058044Z",
    "timestamp": 398691101,
    "rssi": -50,
    "channel_rssi": -50,
    "snr": 9,
    "uplink_token": "ChgKFAoIc2ltLTJhbnQSCKqgy//+gA2xEAEQnZaOvgEaCwjB7+rUBhDg9L8gIMjS3Z7NCw==",
    "received_at": "2026-09-04T12:21:21.064577973Z"
  }
]
Regressions

...

Notes for Reviewers

...

Checklist

  • Scope: The referenced issue is addressed, there are no unrelated changes.
  • Compatibility: The changes are backwards compatible with existing API, storage, configuration and CLI, according to the compatibility commitments in README.md for the chosen target branch.
  • Documentation: Relevant documentation is added or updated.
  • Testing: The steps/process to test this feature are clearly explained including testing for regressions.
  • Infrastructure: If infrastructural changes (e.g., new RPC, configuration) are needed, a separate issue is created in the infrastructural repositories.
  • Changelog: Significant features, behavior changes, deprecations and fixes are added to CHANGELOG.md.
  • Commits: Commit messages follow guidelines in CONTRIBUTING.md, there are no fixup commits left.

Uplinks received on antennas other than the first now carry an uplink
token and inherit the gateway's downlink path constraint, so that the
Network Server can use any antenna as a downlink path.

References #48
@vlasebian vlasebian self-assigned this Sep 2, 2026
@github-actions github-actions Bot added c/gateway server This is related to the Gateway Server c/gateway conf server This is related to the Gateway Configuration Server labels Sep 2, 2026
@vlasebian

vlasebian commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@johanstokking I started doing some work on this. I covered what was in your last comment, but I see there are some changes that have to be done in the GS scheduler regarding the emissions too:

pkg/gatewayserver/scheduling.SubBand should have an emissions array per antenna as conflicts are per antenna, not per gateway. The duty-cycle is still the compound of these arrays, as that is per gateway

From my understanding, there has to be a per-gateway duty cycle budget that takes into consideration the air time of all the messages regardless of the antennas. That should stay as is.

However, multiple antennas allow for concurrent downlinks to be sent and two downlinks sent on different antennas will not conflict. Currently, the code does see it as a conflict and has to be updated to allow for it. This is tracked in the emisisons of the gateway scheduler - this scheduler is per frequency plan (not the sub-band scheduler emissions). This has to be changed to an array that tracks emissions per antenna.

So something like:

type Scheduler struct {
	clock                *RolloverClock
	fps                  []*frequencyplans.FrequencyPlan
	timeOffAir           frequencyplans.TimeOffAir
	timeSource           TimeSource
	subBands             []*SubBand
	mu                   sync.RWMutex
	emissionsPerAntenna  []Emissions // this becomes an array indexed by antenna index
	scheduleAnytimeDelay time.Duration
}

Did I get this right?

@johanstokking

Copy link
Copy Markdown
Member

I have a bit of progressive insight on #48 (comment).

If we would support concurrent downlink, we would indeed have to account for gateway-level duty-cycle and we would have to avoid conflicts where two downlinks use the same frequency.

For now, I would keep it simple and not design for concurrent downlinks. So if we have a multiple antennas, downlink should be sent from the right antenna but not concurrently with downlink on other antennas. This is still beneficial over using one antenna only for downlink, because antennas can be directional (like two antennas covering 180 degrees).

Each SX1301_conf object in the router configuration now carries the
gain of its own board's antenna instead of the first antenna's gain
applied to all boards. Antenna gains are aligned index-for-index with
the gateway's frequency plans, falling back to the first antenna's
gain when fewer antennas are registered than frequency plans.
Fractional gains are no longer truncated to integers.

References #48
@vlasebian
vlasebian force-pushed the feat/downlink-support-for-multiple-antennas branch from 15e9ff6 to b76cbd6 Compare September 3, 2026 07:33
@vlasebian
vlasebian marked this pull request as ready for review September 4, 2026 13:33
@vlasebian
vlasebian requested review from a team as code owners September 4, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c/gateway conf server This is related to the Gateway Configuration Server c/gateway server This is related to the Gateway Server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants