Commit 82198b0b authored by Yann Garcia's avatar Yann Garcia
Browse files

Adding RNI support (except L2Meas not supported yet)

parent 0ffce05e
Loading
Loading
Loading
Loading
+12 −7
Changes for go-apps/meep-gis-engine/server/gis-engine.go: 12 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -526,19 +526,16 @@ func setUe(asset *Asset, pl *dataModel.PhysicalLocation, geoData *AssetGeoData)
			geoData.mode = am.PathModeLoop
		}

		// Fill UE data
		// Fill UE data. Velocity is independent of path: Simu5G/SUMO UEs
		// have speed but no GIS path (external motion).
		ueData[am.FieldConnected] = pl.Connected
		ueData[am.FieldPriority] = initWirelessType(pl.Wireless, pl.WirelessType)
		ueData[am.FieldPosition] = geoData.position
		ueData[am.FieldRadius] = geoData.radius
		if geoData.path != "" {
			ueData[am.FieldPath] = geoData.path
			// Set default EOP mode to LOOP if not provided
			if geoData.mode == "" {
				geoData.mode = am.PathModeLoop
			}
		ueData[am.FieldMode] = geoData.mode
		ueData[am.FieldVelocity] = geoData.velocity
		if geoData.path != "" {
			ueData[am.FieldPath] = geoData.path
		}

		// Create UE
@@ -556,6 +553,14 @@ func setUe(asset *Asset, pl *dataModel.PhysicalLocation, geoData *AssetGeoData)
				ueData[am.FieldPosition] = geoData.position
				asset.geoData.position = geoData.position
			}
			if geoData.velocity != asset.geoData.velocity {
				ueData[am.FieldVelocity] = geoData.velocity
				asset.geoData.velocity = geoData.velocity
			}
			if geoData.mode != "" && geoData.mode != asset.geoData.mode {
				ueData[am.FieldMode] = geoData.mode
				asset.geoData.mode = geoData.mode
			}
			if geoData.path != "" && (geoData.path != asset.geoData.path ||
				geoData.mode != asset.geoData.mode || geoData.velocity != asset.geoData.velocity) {
				ueData[am.FieldPath] = geoData.path
+32 −23
Changes for go-apps/meep-simu5g-bridge/bridge.go: 32 added lines, 23 removed lines.
Original line number Diff line number Diff line
@@ -72,6 +72,18 @@ func NewBridgeInstance(cfg *bridge.IntegrationConfig) (*BridgeInstance, error) {
	return b, nil
}

/**
 * @brief Closes the TCP client socket and clears conn so TUN writes skip until reconnect.
 */
func (b *BridgeInstance) closeConn() {
	b.mutex.Lock()
	defer b.mutex.Unlock()
	if b.conn != nil {
		_ = b.conn.Close()
		b.conn = nil
	}
}

/**
 * @brief Dials simulator host S and sends HELLO with live NAT tables.
 * @return Error from net.DialTimeout or TCP write.
@@ -103,42 +115,33 @@ func (b *BridgeInstance) connectAndHello() error {
}

/**
 * @brief Runs uplink and downlink relay goroutines until context cancel or fatal error.
 * @brief Relays TUN uplink and TCP downlink until context cancel.
 * @param ctx Cancellation stops loops cleanly; TCP errors trigger reconnect.
 * @return ctx.Err() on shutdown, or error if reconnect is aborted.
 */
func (b *BridgeInstance) Run(ctx context.Context) error {
	defer func() {
		b.mutex.Lock()
		if b.conn != nil {
			_ = b.conn.Close()
		}
		b.mutex.Unlock()
	}()
	defer b.closeConn()
	defer b.tun.Close()

	for {
		errCh := make(chan error, 2)
		go func() { errCh <- b.loopTunToSocket(ctx) }()
		go func() { errCh <- b.loopSocketToTun(ctx) }()
	// One TUN reader for the process lifetime. Restarting it on TCP RST
	// stacked extra goroutines on the same TUN and the same TCP conn.
	go func() { _ = b.loopTunToSocket(ctx) }()

		select {
		case <-ctx.Done():
	for {
		err := b.loopSocketToTun(ctx)
		if ctx.Err() != nil {
			return nil
		case err := <-errCh:
		}
		if err == nil {
			continue
		}
			if ctx.Err() != nil {
				return nil
			}
		log.Warn("Bridge loop error: ", err, " — reconnecting to ", b.cfg.BridgeTCPAddr())
		b.closeConn()
		if rerr := b.reconnect(ctx); rerr != nil {
			return rerr
		}
	}
}
}

/**
 * @brief Reconnects to S with exponential backoff until success or context cancel.
@@ -151,13 +154,12 @@ func (b *BridgeInstance) reconnect(ctx context.Context) error {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		case <-time.After(wait):
		}
		if err := b.connectAndHello(); err == nil {
			log.Info(b.cfg.SandboxLogPrefix(), " Reconnected to simulator at ", b.cfg.BridgeTCPAddr())
			return nil
		}
		time.Sleep(wait)
		if wait < maxReconnectWait {
			wait *= 2
		}
@@ -206,10 +208,14 @@ func (b *BridgeInstance) loopTunToSocket(ctx context.Context) error {
		frame := bridge.EncodeDataULFrame(ueID, pkt, 0)
		b.mutex.Lock()
		conn := b.conn
		_, err = conn.Write(frame)
		b.mutex.Unlock()
		if conn == nil {
			continue
		}
		_, err = conn.Write(frame)
		if err != nil {
			return err
			log.Debug("TUN UL write skipped (TCP down): ", err)
			continue
		}
	}
}
@@ -231,6 +237,9 @@ func (b *BridgeInstance) loopSocketToTun(ctx context.Context) error {
		b.mutex.Lock()
		conn := b.conn
		b.mutex.Unlock()
		if conn == nil {
			return errors.New("tcp conn closed")
		}

		_, err := io.ReadFull(conn, hdr)
		if err != nil {
+3 −0
Changes for go-apps/meep-simu5g-mobility-adapter/publisher/gis_test.go: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -46,6 +46,9 @@ func TestGeoDataFromUEPointOrder(t *testing.T) {
	if geo.Location.Coordinates[0] != float32(7.4216) || geo.Location.Coordinates[1] != float32(43.728) {
		t.Fatalf("coordinates %v want lon,lat", geo.Location.Coordinates)
	}
	if geo.Velocity != float32(13.8) {
		t.Fatalf("velocity %v", geo.Velocity)
	}
}

func TestPublishUEPostsGeodata(t *testing.T) {
+36 −3
Changes for go-apps/meep-simu5g-rnis-adapter/README.md: 36 added lines, 3 removed lines.
Original line number Diff line number Diff line
# meep-simu5g-rnis-adapter

Runs on **NativeEdge Connector**. Polls `RniUdpQueryResponder` on **Network Emulation Stack (Simu5G/SimuLTE)** (`deployment.simHost:9201`) and writes KPIs to Redis for `meep-rnis` (TODO).
Runs on **EdgeNative-Connector** (host B). Polls `RniUdpQueryResponder` on the **Network Emulation Stack** (`deployment.simHost:rni.simPort`, default UDP 9201) and writes:

- L2 RSRP/RSRQ → GIS cache `SetPoaMeasurement` (meep-rnis `refreshMeasurements`)
- throughput / latency / loss → metric store (meep-rnis `GetCachedNetworkMetrics`)

Cell-change is logged when RAB `ecgiCellIdentity` maps to a different `cells[].poaId` than the previous poll. GIS `servingPoa` remains Epic 3 (logical wifi>5g>4g). `meep-rnis` CellChangeNotification still comes from SBI when the scenario parent POA changes.

## Run

```bash
go run . --config /path/to/simu5g-config.json
go run . --config /home/ubuntu-template/etsi-mec-sandbox/tools/simu5g/monaco-harbor-integration-config.json --sandbox sbx4lesbpo
# decode only:
go run . --config ... --dry-run
```

The adapter runs **on the node**, not in a pod. Kubernetes names such as `meep-redis-master-0` do not resolve here, and Redis data is **6379** (not 9379).

```bash
# ClusterIP is reachable from this host with Calico iptables:
kubectl get svc meep-redis-master -o jsonpath='{.spec.clusterIP}'; echo
./bin/meep-simu5g-rnis-adapter --config ... --sandbox sbx4lesbpo --redis 10.107.157.48:6379
```

If `--redis` is a pod hostname, the adapter looks up `meep-redis-master` ClusterIP via `kubectl` and uses port 6379. Wire errors do not abort the process.

Set `rni.meAppInst` to the scenario MEC application name that `meep-rnis` will query as `GetCachedNetworkMetrics("*", appName)`.

## Replay and capture (R4.3)

When S is down, apply a JSONL capture of hex UDP datagrams (no OMNeT++):

```bash
./bin/meep-simu5g-rnis-adapter --config ... --sandbox sbx4lesbpo --redis 10.107.157.48:6379 \
  --replay ../../go-packages/meep-simu5g-bridge/testdata/rni-golden.jsonl
```

Uses `rni.pollIntervalMs` and `rni.meAppInst` from the shared integration config.
Capture the first successful live poll for later replay:

```bash
./bin/meep-simu5g-rnis-adapter --config ... --sandbox sbx4lesbpo --redis 10.107.157.48:6379 \
  --capture /tmp/rni-golden.jsonl --once
```
+74 −0
Changes for go-apps/meep-simu5g-rnis-adapter/convert.go: 74 added lines, 0 removed lines.
Original line number Diff line number Diff line
package main

import (
	"math"

	bridge "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-simu5g-bridge"
)

/** @brief Converts Simu5G L2 `*Db10` (dB×10) to GIS-cache dB / dBm. Sentinel -10000 is not converted. */
func dbFromDb10(v int32) float32 {
	if !bridge.L2RadioAvailable(v) {
		return 0
	}
	return float32(v) / 10
}

/** @brief Converts Simu5G bps to the metric-store Mbps used by meep-rnis. */
func tputMbps(bps int32) float64 {
	if bps < 0 {
		return 0
	}
	return float64(bps) / 1e6
}

/**
 * @brief Maps Simu5G discard-metric mean to RNIS packet-loss percent.
 * @details -1 means no collector (treat as 0). Values above 100 are dropped to 0
 *          because meep-rnis `calculateMetrics` divides by 100 as a percent.
 */
func lossPercent(mean int32) float64 {
	if mean < 0 || mean > 100 {
		return 0
	}
	return float64(mean)
}

/** @brief Picks a non-negative latency in ms (prefer DL). */
func latencyMs(dl, ul int32) int32 {
	if dl >= 0 {
		return dl
	}
	if ul >= 0 {
		return ul
	}
	return 0
}

/** @brief Serving sandbox POA from RAB ECGI, else NR/LTE serving node. */
func servingPoaFromRab(cfg *bridge.IntegrationConfig, ue bridge.RniRabUe) (string, bool) {
	if cfg == nil {
		return "", false
	}
	if poa, ok := cfg.ResolvePoaFromCellID(ue.EcgiIdentity); ok {
		return poa, true
	}
	if ue.NrServingNodeId != 0 {
		if poa, ok := cfg.ResolvePoaFromSimModule("gnb"); ok {
			return poa, true
		}
	}
	if ue.ServingNodeId != 0 {
		if poa, ok := cfg.ResolvePoaFromSimModule("enb"); ok {
			return poa, true
		}
	}
	return "", false
}

func finite32(v float32) float32 {
	if math.IsNaN(float64(v)) || math.IsInf(float64(v), 0) {
		return 0
	}
	return v
}
Loading