Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,6 @@ MigrationBackup/

# JetBrains IDEA files
.idea/

# Serena MCP files
.serena/
3 changes: 2 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<WarningLevel>9999</WarningLevel>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<NuGetAuditLevel>high</NuGetAuditLevel>
<NoWarn>$(NoWarn)</NoWarn>

<!-- Pack everything with embedded .pdb -->
Expand All @@ -28,7 +29,7 @@
</PropertyGroup>

<PropertyGroup>
<PackageBaseVersion>1.1.1</PackageBaseVersion>
<PackageBaseVersion>1.2.0</PackageBaseVersion>
<MinVerTagPrefix></MinVerTagPrefix>
</PropertyGroup>

Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageVersion Include="FFMpegCore" Version="5.4.0" />
<PackageVersion Include="MinVer" Version="6.0.0" />
Comment thread
alex1ozr marked this conversation as resolved.
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Moq" Version="4.20.72" />
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Each package provides HTTP clients for accessing external data sources with stro
|---------------------|-----------------------------------------------------------------------------------------------------------------------------|
| **RTSW Client** | NOAA real-time solar wind feeds — 1-minute magnetometer and plasma JSON data with `active` and `source` metadata |
| **KP-Index Client** | Geomagnetic activity indices — nowcast and forecast data (3-day and 27-day) |
| **WSA-ENLIL Client** | Solar wind forecast animation — downloads WSA-ENLIL model frames and encodes them as MP4 (H.264) via FFmpeg |
| **ACE Client** | Legacy text feeds (deprecated; use RTSW instead — see [Integrations #3](https://github.com/Aurora-Science-Hub/Integrations/issues/3)) |

See [detailed documentation](src/NoaaClient/README.md) for usage examples and API reference.
Expand Down Expand Up @@ -107,6 +108,7 @@ This repository provides production-ready HTTP clients for external space weathe

- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or later
- IDE: [Visual Studio 2025+](https://visualstudio.microsoft.com/), [Rider 2025+](https://www.jetbrains.com/rider/), or [VS Code](https://code.visualstudio.com/)
- **FFmpeg** — required for WSA-ENLIL animation encoding (see [installation instructions](#ffmpeg-installation))

### Building from Source

Expand All @@ -125,6 +127,35 @@ dotnet build
dotnet test
```

### FFmpeg Installation

The **WSA-ENLIL animation** feature requires FFmpeg to encode forecast frames into MP4 video. Install it for your platform:

**Windows:**
```powershell
winget install ffmpeg
```

**macOS:**
```bash
brew install ffmpeg
```

**Linux (Ubuntu/Debian):**
```bash
sudo apt-get install ffmpeg
```

**Linux (Fedora/RHEL):**
```bash
sudo dnf install ffmpeg-free
```

Verify the installation:
```bash
ffmpeg -version
```

## Code Style

The solution uses [EditorConfig](.editorconfig) based on [Azure SDK .NET](https://github.com/Azure/azure-sdk-for-net/blob/main/.editorconfig) to maintain consistent code style across all packages.
Expand Down
50 changes: 50 additions & 0 deletions samples/NoaaClientSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using AuroraScienceHub.Integrations.NoaaClient;
using AuroraScienceHub.Integrations.NoaaClient.Ace;
using AuroraScienceHub.Integrations.NoaaClient.WsaEnlil;
using AuroraScienceHub.Integrations.NoaaClient.KpIndex;
using AuroraScienceHub.Integrations.NoaaClient.Rtsw;
using AuroraScienceHub.Integrations.Samples.NoaaClientSample;
Expand All @@ -20,6 +21,7 @@
var aceClient = host.Services.GetRequiredService<IAceClient>();
var kpIndexClient = host.Services.GetRequiredService<IKpIndexClient>();
var rtswClient = host.Services.GetRequiredService<IRtswClient>();
var wsaEnlilClient = host.Services.GetRequiredService<IWsaEnlilClient>();

// Display header
AnsiConsole.Write(new FigletText("NOAA Client").Color(Color.Blue));
Expand All @@ -40,6 +42,9 @@
"KP Index 27-Day Forecast",
"KP Index 3-Day Forecast",
"KP Index Nowcast",
"WSA-ENLIL Animation",
"WSA-ENLIL Animation (small, 320px)",
"WSA-ENLIL Last Frame Time",
new string('-', 30),
"Execute All Requests",
"Exit"));
Expand Down Expand Up @@ -99,6 +104,28 @@ await AnsiConsole.Status()
var data = await kpIndexClient.GetKpIndexNowcastAsync(CancellationToken.None);
OutputFormatter.DisplayKpNowcast(data, 10);
}),
"WSA-ENLIL Animation" => FetchAndDisplay(async () =>
{
await using var stream = await wsaEnlilClient.GetEnlilAnimationAsync(
maxWidth: 480, cancellationToken: CancellationToken.None);
await SaveAnimationAsync(stream, "enlil_animation.mp4");
}),
"WSA-ENLIL Animation (small, 320px)" => FetchAndDisplay(async () =>
{
await using var stream = await wsaEnlilClient.GetEnlilAnimationAsync(
maxWidth: 320, cancellationToken: CancellationToken.None);
await SaveAnimationAsync(stream, "enlil_animation_small.mp4");
}),
"WSA-ENLIL Last Frame Time" => FetchAndDisplay(async () =>
{
var lastFrameTime = await wsaEnlilClient.GetLastFrameTimeAsync(
CancellationToken.None);
if (lastFrameTime is null)
AnsiConsole.MarkupLine("[yellow]No data available.[/]");
else
AnsiConsole.MarkupLine(
$"[green]Last frame time:[/] {lastFrameTime:yyyy-MM-dd HH:mm:ss} UTC");
}),
"Execute All Requests" => ExecuteAllRequests(),
_ => Task.CompletedTask
});
Expand Down Expand Up @@ -148,6 +175,12 @@ async Task ExecuteAllRequests()
{
var data = await kpIndexClient.GetKpIndexNowcastAsync(CancellationToken.None);
OutputFormatter.DisplayKpNowcast(data, 5);
}),
("WSA-ENLIL Animation", async () =>
{
await using var stream = await wsaEnlilClient.GetEnlilAnimationAsync(
maxWidth: 480, cancellationToken: CancellationToken.None);
await SaveAnimationAsync(stream, "enlil_animation.mp4");
})
};

Expand Down Expand Up @@ -185,4 +218,21 @@ await AnsiConsole.Progress()
_ => "Extreme"
};

// Saves MP4 animation stream to a temp file and displays the result
static async Task SaveAnimationAsync(Stream stream, string fileName)
{
if (stream.Length == 0)
{
AnsiConsole.MarkupLine("[red]No animation data returned.[/]");
return;
}

var outputPath = Path.Combine(Path.GetTempPath(), fileName);
await using var fileStream = File.Create(outputPath);
await stream.CopyToAsync(fileStream);

AnsiConsole.MarkupLine($"[green]Animation saved:[/] {outputPath}");
AnsiConsole.MarkupLine($"[dim]Size: {stream.Length / 1024} KB | Format: MP4[/]");
}


1 change: 1 addition & 0 deletions src/NoaaClient/NoaaClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageReference Include="AuroraScienceHub.Framework.Http" />
<PackageReference Include="AuroraScienceHub.Framework.Utilities" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="FFMpegCore" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>

Expand Down
22 changes: 22 additions & 0 deletions src/NoaaClient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ var forecast3Day = await kpIndexClient.GetKpIndex3DayForecastAsync(cancellationT
var forecast27Day = await kpIndexClient.GetKpIndex27DayForecastAsync(cancellationToken);
```

## WSA-ENLIL Client

Generates MP4 (H.264) animations from WSA-ENLIL solar wind forecast model frames provided by NOAA SWPC.

**Prerequisites:** FFmpeg must be installed on the system. See [FFmpeg Installation](../../README.md#ffmpeg-installation) for platform-specific instructions.

```csharp
// Download and encode WSA-ENLIL animation frames as MP4 video
await using var stream = await wsaEnlilClient.GetEnlilAnimationAsync(
maxWidth: 480,
cancellationToken: cancellationToken);

// Save to file
await using var fileStream = File.Create("enlil_animation.mp4");
await stream.CopyToAsync(fileStream);
```

- **maxWidth** — maximum output width in pixels; frames are scaled proportionally (default: 480)
- **Return value** — `MemoryStream` containing the complete MP4 video; caller is responsible for disposal
- Frame rate: 20 fps (50 ms per frame)
- Encoder: H.264 (libx264), CRF 23, pixel format yuv420p

## Deprecated API

`IAceClient` is obsolete and scheduled for removal ([#3](https://github.com/Aurora-Science-Hub/Integrations/issues/3)). Use `IRtswClient` for magnetometer and solar wind plasma data.
Expand Down
2 changes: 2 additions & 0 deletions src/NoaaClient/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AuroraScienceHub.Integrations.NoaaClient.Ace;
using AuroraScienceHub.Integrations.NoaaClient.WsaEnlil;
using AuroraScienceHub.Integrations.NoaaClient.KpIndex;
using AuroraScienceHub.Integrations.NoaaClient.Rtsw;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -26,6 +27,7 @@ public static IServiceCollection AddNoaaClients(this IServiceCollection services
#pragma warning restore CS0618
services.AddHttpClient<IKpIndexClient, KpIndexClient>();
services.AddHttpClient<IRtswClient, RtswClient>();
services.AddHttpClient<IWsaEnlilClient, WsaEnlilClient>();

return services;
}
Expand Down
38 changes: 38 additions & 0 deletions src/NoaaClient/WsaEnlil/IWsaEnlilClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace AuroraScienceHub.Integrations.NoaaClient.WsaEnlil;

/// <summary>
/// Client for creating WSA-ENLIL solar wind forecast animations from NOAA SWPC imagery.
/// </summary>
/// <remarks>
/// Requires FFmpeg to be installed on the system and available in PATH.
/// See README for platform-specific installation instructions.
/// </remarks>
public interface IWsaEnlilClient
{
/// <summary>
/// Downloads the WSA-ENLIL animation manifest and all frames, assembling them into an optimized MP4 (H.264) video.
/// </summary>
/// <param name="maxWidth">Maximum output width in pixels. Frames are resized proportionally. Default 480.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A stream containing the MP4 video data. The caller is responsible for disposing this stream.</returns>
/// <remarks>
/// The returned stream is a MemoryStream containing the complete MP4 data.
/// Typical usage: <c>await using var stream = await GetEnlilAnimationAsync(cancellationToken: cancellationToken);</c>
/// </remarks>
Task<Stream> GetEnlilAnimationAsync(
int maxWidth = 480,
CancellationToken cancellationToken = default);
Comment thread
Demosfen marked this conversation as resolved.

/// <summary>
/// Returns the timestamp of the last frame in the WSA-ENLIL animation, without downloading frames or encoding video.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The timestamp extracted from the last frame's URL, or <see langword="null"/> if the manifest is empty or unavailable.
/// </returns>
/// <remarks>
/// Fetches only the manifest JSON (~1 KB). Use this to check whether the animation has been updated
/// before calling <see cref="GetEnlilAnimationAsync"/>.
/// </remarks>
Task<DateTime?> GetLastFrameTimeAsync(CancellationToken cancellationToken = default);
}
11 changes: 11 additions & 0 deletions src/NoaaClient/WsaEnlil/Responses/WsaEnlilManifestEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace AuroraScienceHub.Integrations.NoaaClient.WsaEnlil.Responses;

/// <summary>
/// A single frame entry in the WSA-ENLIL animation manifest.
/// </summary>
/// <param name="Url">Relative frame URL, e.g. "/images/animations/enlil/enlil_com2_58426_20250118T120000.jpg".</param>
public sealed record WsaEnlilManifestEntry(
[property: JsonPropertyName("url")] string Url
Comment thread
Demosfen marked this conversation as resolved.
);
Loading
Loading