Skip to content

Commit 5a860b8

Browse files
author
Vinod Patmanathan
committed
feat: add NR NTN access detection
Detect Non-Terrestrial Network access by checking for the NRNTN-TAI-Information protocol extension (ID 287 per 3GPP TS 38.413) in UserLocationInformationNR received via NGAP, and track the result in a new NtnAccessInfo struct on AmfUe and RanUe. This is detection-only groundwork. No protocol behavior changes: nothing reads the flag yet. Future patches can extend NtnAccessInfo with satellite backhaul category, GEO satellite ID, and NTN TAI list as those fields become available in omec-project/openapi and omec-project/ngap. Signed-off-by: Vinod Patmanathan <vinod.patmanathan@forsway.com>
1 parent ba26ede commit 5a860b8

4 files changed

Lines changed: 92 additions & 2 deletions

File tree

context/amf_ue.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ type AmfUe struct {
101101
EventSubscriptionsInfo map[string]*AmfUeEventSubscription `json:"eventSubscriptionInfo,omitempty"`
102102
/* User Location*/
103103
RatType models.RatType `json:"ratType,omitempty"`
104+
NtnAccess *NtnAccessInfo `json:"ntnAccess,omitempty"`
104105
Location models.UserLocation `json:"location,omitempty"`
105106
Tai models.Tai `json:"tai,omitempty"`
106107
LocationChanged bool `json:"locationChanged,omitempty"`

context/ntn.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-FileCopyrightText: 2026 Intel Corporation
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package context
6+
7+
// NtnAccessInfo captures Non-Terrestrial Network access properties for a
8+
// UE, as derived from NGAP signaling. Only NTN-access presence is tracked
9+
// today; future 3GPP-defined fields (satelliteBackhaulCategory,
10+
// geoSatelliteId, NTN TAI list) can be added without reshaping AmfUe or
11+
// RanUe.
12+
type NtnAccessInfo struct {
13+
// Detected reflects whether the most recent NR UserLocationInformation
14+
// for this UE carried the NRNTN-TAI-Information extension. It is set
15+
// and cleared on every NR location update.
16+
Detected bool `json:"detected"`
17+
}
18+
19+
// IsNtn reports whether the UE is currently being served over NTN access,
20+
// based on the most recent NR location update.
21+
func (ue *AmfUe) IsNtn() bool {
22+
return ue.NtnAccess != nil && ue.NtnAccess.Detected
23+
}
24+
25+
// updateNtnAccess records the current NTN-detection state on this RanUe.
26+
// It allocates the NtnAccessInfo container on first use and logs at Debug
27+
// only when the state actually transitions, to keep the per-update path
28+
// quiet for steady-state UEs.
29+
func (ranUe *RanUe) updateNtnAccess(detected bool) {
30+
if ranUe.NtnAccess == nil {
31+
ranUe.NtnAccess = &NtnAccessInfo{}
32+
}
33+
if ranUe.NtnAccess.Detected == detected {
34+
return
35+
}
36+
ranUe.NtnAccess.Detected = detected
37+
if detected {
38+
ranUe.Log.Debugf("NR NTN access detected (NRNTN-TAI-Information extension present)")
39+
} else {
40+
ranUe.Log.Debugf("NR NTN access cleared (no NRNTN-TAI-Information extension)")
41+
}
42+
}

context/ran_ue.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/mohae/deepcopy"
1717
"github.com/omec-project/amf/logger"
18+
"github.com/omec-project/ngap"
1819
"github.com/omec-project/ngap/ngapConvert"
1920
"github.com/omec-project/ngap/ngapType"
2021
"github.com/omec-project/openapi/models"
@@ -46,8 +47,9 @@ type RanUe struct {
4647
TargetUe *RanUe `json:"-"`
4748

4849
/* UserLocation*/
49-
Tai models.Tai
50-
Location models.UserLocation
50+
Tai models.Tai
51+
Location models.UserLocation
52+
NtnAccess *NtnAccessInfo `json:"-"`
5153
/* context about udm */
5254
SupportVoPSn3gpp bool `json:"-"`
5355
SupportVoPS bool `json:"-"`
@@ -212,6 +214,8 @@ func (ranUe *RanUe) UpdateLocation(userLocationInformation *ngapType.UserLocatio
212214
ranUe.Location.NrLocation = new(models.NrLocation)
213215
}
214216

217+
ranUe.updateNtnAccess(ngap.HasUserLocationInformationNRExtension(locationInfoNR, ngapType.ProtocolIEIDNRNTNTAIInformation))
218+
215219
tAI := locationInfoNR.TAI
216220
plmnID, err := ngapConvert.PlmnIdToModels(tAI.PLMNIdentity)
217221
if err != nil {
@@ -250,6 +254,7 @@ func (ranUe *RanUe) UpdateLocation(userLocationInformation *ngapType.UserLocatio
250254
}
251255
ranUe.AmfUe.Location = deepcopy.Copy(ranUe.Location).(models.UserLocation)
252256
ranUe.AmfUe.Tai = deepcopy.Copy(*ranUe.AmfUe.Location.NrLocation.Tai).(models.Tai)
257+
ranUe.AmfUe.NtnAccess = deepcopy.Copy(ranUe.NtnAccess).(*NtnAccessInfo)
253258
}
254259
case ngapType.UserLocationInformationPresentUserLocationInformationN3IWF:
255260
locationInfoN3IWF := userLocationInformation.UserLocationInformationN3IWF

context/ran_ue_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-FileCopyrightText: 2026 Intel Corporation
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package context
6+
7+
import (
8+
"testing"
9+
10+
"go.uber.org/zap"
11+
)
12+
13+
func TestRanUeUpdateNtnAccess(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
initial *NtnAccessInfo
17+
detected bool
18+
wantDetected bool
19+
}{
20+
{name: "nil → not-detected allocates false", initial: nil, detected: false, wantDetected: false},
21+
{name: "nil → detected allocates true", initial: nil, detected: true, wantDetected: true},
22+
{name: "detected → detected stays true", initial: &NtnAccessInfo{Detected: true}, detected: true, wantDetected: true},
23+
{name: "detected → not-detected clears", initial: &NtnAccessInfo{Detected: true}, detected: false, wantDetected: false},
24+
{name: "not-detected → detected sets true", initial: &NtnAccessInfo{Detected: false}, detected: true, wantDetected: true},
25+
{name: "not-detected → not-detected stays false", initial: &NtnAccessInfo{Detected: false}, detected: false, wantDetected: false},
26+
}
27+
for _, tc := range tests {
28+
t.Run(tc.name, func(t *testing.T) {
29+
ranUe := &RanUe{
30+
NtnAccess: tc.initial,
31+
Log: zap.NewNop().Sugar(),
32+
}
33+
ranUe.updateNtnAccess(tc.detected)
34+
if ranUe.NtnAccess == nil {
35+
t.Fatalf("NtnAccess should always be non-nil after updateNtnAccess; got nil")
36+
}
37+
if ranUe.NtnAccess.Detected != tc.wantDetected {
38+
t.Errorf("Detected = %v, want %v", ranUe.NtnAccess.Detected, tc.wantDetected)
39+
}
40+
})
41+
}
42+
}

0 commit comments

Comments
 (0)