6 min read
Azure Orbital: Ground Station as a Service
Azure Orbital provides ground station as a service, enabling communication with satellites without owning physical infrastructure. This opens space data capabilities to organizations of all sizes.
Understanding Azure Orbital
Azure Orbital offers:
- Global ground station network
- Satellite communication management
- Data processing pipelines
- Integration with Azure services
Setting Up a Spacecraft Contact
Configure satellite communication:
resource orbital 'Microsoft.Orbital/spacecrafts@2022-03-01' = {
name: 'weather-sat-1'
location: 'global'
properties: {
noradId: '12345'
titleLine: 'WEATHER-SAT-1'
tleLine1: '1 12345U 22001A 22121.50000000 .00000000 00000-0 00000-0 0 9999'
tleLine2: '2 12345 98.2000 120.0000 0001000 90.0000 270.0000 14.50000000000000'
links: [
{
name: 'downlink'
centerFrequencyMHz: 8200
bandwidthMHz: 50
direction: 'Downlink'
polarization: 'RHCP'
}
{
name: 'uplink'
centerFrequencyMHz: 2025
bandwidthMHz: 5
direction: 'Uplink'
polarization: 'LHCP'
}
]
}
}
resource contactProfile 'Microsoft.Orbital/contactProfiles@2022-03-01' = {
name: 'weather-contact-profile'
location: 'westus2'
properties: {
minimumViableContactDuration: 'PT5M'
minimumElevationDegrees: 10
autoTrackingConfiguration: 'Disabled'
links: [
{
name: 'downlink-profile'
polarization: 'RHCP'
direction: 'Downlink'
gainOverTemperature: 25
eirpdBW: 0
channels: [
{
name: 'channel1'
centerFrequencyMHz: 8200
bandwidthMHz: 50
endPoint: {
endPointName: 'data-endpoint'
ipAddress: '10.0.0.5'
port: '50000'
protocol: 'UDP'
}
modulationConfiguration: {
modulation: 'QPSK'
}
demodulationConfiguration: {
modulation: 'QPSK'
}
encodingConfiguration: null
decodingConfiguration: {
decodingType: 'viterbidecoder'
modulation: 'QPSK'
}
}
]
}
]
}
}
Scheduling Satellite Contacts
using Azure.ResourceManager.Orbital;
using Azure.ResourceManager.Orbital.Models;
public class SatelliteContactService
{
private readonly ArmClient _armClient;
private readonly string _subscriptionId;
public SatelliteContactService(string subscriptionId)
{
_armClient = new ArmClient(new DefaultAzureCredential());
_subscriptionId = subscriptionId;
}
public async Task<IEnumerable<AvailableContact>> GetAvailableContactsAsync(
string spacecraftName,
string groundStationName,
DateTime startTime,
DateTime endTime)
{
var subscription = await _armClient.GetDefaultSubscriptionAsync();
var spacecraft = await subscription.GetSpacecrafts()
.GetAsync(spacecraftName);
var parameters = new ContactParameters
{
ContactProfile = new ResourceIdentifier(
$"/subscriptions/{_subscriptionId}/resourceGroups/orbital-rg/providers/Microsoft.Orbital/contactProfiles/weather-contact-profile"),
GroundStationName = groundStationName,
StartTime = startTime,
EndTime = endTime
};
var contacts = spacecraft.Value.GetAvailableContacts(parameters);
return contacts;
}
public async Task<SpacecraftContactResource> ScheduleContactAsync(
string spacecraftName,
string contactName,
AvailableContact availableContact)
{
var subscription = await _armClient.GetDefaultSubscriptionAsync();
var spacecraft = await subscription.GetSpacecrafts()
.GetAsync(spacecraftName);
var contactData = new SpacecraftContactData
{
ContactProfile = availableContact.ContactProfile,
GroundStationName = availableContact.GroundStationName,
ReservationStartTime = availableContact.StartTime,
ReservationEndTime = availableContact.EndTime
};
var contact = await spacecraft.Value.GetSpacecraftContacts()
.CreateOrUpdateAsync(WaitUntil.Completed, contactName, contactData);
return contact.Value;
}
}
Processing Satellite Data
Azure Functions for data processing:
public class SatelliteDataProcessor
{
private readonly BlobServiceClient _blobClient;
private readonly ILogger<SatelliteDataProcessor> _logger;
[FunctionName("ProcessSatelliteData")]
public async Task Run(
[BlobTrigger("satellite-raw/{name}", Connection = "StorageConnection")]
Stream inputBlob,
string name,
[Blob("satellite-processed/{name}", FileAccess.Write)]
Stream outputBlob)
{
_logger.LogInformation($"Processing satellite data: {name}");
// Parse raw satellite telemetry
var telemetry = await ParseTelemetryAsync(inputBlob);
// Process and extract data
var processedData = ProcessTelemetry(telemetry);
// Save processed data
await SaveProcessedDataAsync(processedData, outputBlob);
// Send to Event Hub for real-time analysis
await SendToEventHubAsync(processedData);
}
private async Task<SatelliteTelemetry> ParseTelemetryAsync(Stream stream)
{
using var reader = new BinaryReader(stream);
// Parse CCSDS packet structure
var primaryHeader = new CcsdsHeader
{
VersionNumber = (reader.ReadByte() >> 5) & 0x07,
PacketType = (reader.ReadByte() >> 4) & 0x01,
SecondaryHeaderFlag = (reader.ReadByte() >> 3) & 0x01,
ApplicationId = reader.ReadUInt16(),
SequenceFlags = (reader.ReadByte() >> 6) & 0x03,
SequenceCount = reader.ReadUInt16(),
PacketDataLength = reader.ReadUInt16()
};
// Read payload
var payload = reader.ReadBytes(primaryHeader.PacketDataLength + 1);
return new SatelliteTelemetry
{
Header = primaryHeader,
Timestamp = DateTime.UtcNow,
RawPayload = payload
};
}
private ProcessedSatelliteData ProcessTelemetry(SatelliteTelemetry telemetry)
{
// Extract sensor readings from payload
var temperature = ExtractTemperature(telemetry.RawPayload);
var position = ExtractPosition(telemetry.RawPayload);
var imageData = ExtractImageData(telemetry.RawPayload);
return new ProcessedSatelliteData
{
SatelliteId = telemetry.Header.ApplicationId.ToString(),
Timestamp = telemetry.Timestamp,
Temperature = temperature,
Position = position,
ImageData = imageData
};
}
private double ExtractTemperature(byte[] payload)
{
// Temperature at offset 0, 2 bytes, 0.01 degree resolution
var raw = BitConverter.ToInt16(payload, 0);
return raw * 0.01;
}
private GeoPosition ExtractPosition(byte[] payload)
{
// Position at offset 2, 12 bytes (lat: 4, lon: 4, alt: 4)
return new GeoPosition
{
Latitude = BitConverter.ToSingle(payload, 2),
Longitude = BitConverter.ToSingle(payload, 6),
Altitude = BitConverter.ToSingle(payload, 10)
};
}
private byte[] ExtractImageData(byte[] payload)
{
// Image data starts at offset 14
var imageLength = payload.Length - 14;
var imageData = new byte[imageLength];
Array.Copy(payload, 14, imageData, 0, imageLength);
return imageData;
}
private async Task SendToEventHubAsync(ProcessedSatelliteData data)
{
var eventHubClient = new EventHubProducerClient(
Environment.GetEnvironmentVariable("EventHubConnection"),
"satellite-telemetry");
var eventData = new EventData(JsonSerializer.SerializeToUtf8Bytes(data));
await eventHubClient.SendAsync(new[] { eventData });
}
}
public record CcsdsHeader
{
public int VersionNumber { get; init; }
public int PacketType { get; init; }
public int SecondaryHeaderFlag { get; init; }
public ushort ApplicationId { get; init; }
public int SequenceFlags { get; init; }
public ushort SequenceCount { get; init; }
public ushort PacketDataLength { get; init; }
}
public record SatelliteTelemetry
{
public CcsdsHeader Header { get; init; }
public DateTime Timestamp { get; init; }
public byte[] RawPayload { get; init; }
}
public record ProcessedSatelliteData
{
public string SatelliteId { get; init; }
public DateTime Timestamp { get; init; }
public double Temperature { get; init; }
public GeoPosition Position { get; init; }
public byte[] ImageData { get; init; }
}
public record GeoPosition
{
public float Latitude { get; init; }
public float Longitude { get; init; }
public float Altitude { get; init; }
}
Real-Time Visualization
Display satellite data on a map:
// React component for satellite tracking
import { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Polyline } from 'react-leaflet';
interface SatellitePosition {
satelliteId: string;
latitude: number;
longitude: number;
altitude: number;
timestamp: string;
}
export const SatelliteTracker: React.FC = () => {
const [positions, setPositions] = useState<SatellitePosition[]>([]);
const [track, setTrack] = useState<[number, number][]>([]);
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/satellite/realtime');
ws.onmessage = (event) => {
const position: SatellitePosition = JSON.parse(event.data);
setPositions(prev => [...prev.slice(-100), position]);
setTrack(prev => [...prev.slice(-500), [position.latitude, position.longitude]]);
};
return () => ws.close();
}, []);
const latestPosition = positions[positions.length - 1];
return (
<div className="satellite-tracker">
<MapContainer center={[0, 0]} zoom={2} style={{ height: '600px' }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{latestPosition && (
<Marker position={[latestPosition.latitude, latestPosition.longitude]} />
)}
{track.length > 1 && (
<Polyline positions={track} color="blue" weight={2} />
)}
</MapContainer>
{latestPosition && (
<div className="satellite-info">
<h3>Satellite: {latestPosition.satelliteId}</h3>
<p>Latitude: {latestPosition.latitude.toFixed(4)}</p>
<p>Longitude: {latestPosition.longitude.toFixed(4)}</p>
<p>Altitude: {latestPosition.altitude.toFixed(2)} km</p>
<p>Last Update: {new Date(latestPosition.timestamp).toLocaleTimeString()}</p>
</div>
)}
</div>
);
};
Summary
Azure Orbital enables:
- Global satellite communication
- Managed ground station infrastructure
- Integration with Azure data services
- Real-time data processing
- Space data democratization
Access space capabilities without massive infrastructure investment.
References: