diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..38965a5
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,6 @@
+# Repository Instructions
+
+- When using Ghidra MCP tools in this repository, do not run destructive actions unless the user explicitly asks for them.
+- Treat these actions as destructive and forbidden by default during analysis: deleting functions, deleting data, patching bytes, applying edit plans that remove or overwrite program structures, or any other write operation that changes binary semantics.
+- During analysis, allowed write actions are limited to renaming functions or data and adding comments.
+- Prefer read-only inspection first. If a write action seems necessary beyond renames/comments, stop and ask the user before proceeding.
\ No newline at end of file
diff --git a/.vscode/mcp.json b/.vscode/mcp.json
new file mode 100644
index 0000000..bfd8b5a
--- /dev/null
+++ b/.vscode/mcp.json
@@ -0,0 +1,9 @@
+{
+ "servers": {
+ "ghidra-web": {
+ "url": "http://127.0.0.1:8081/sse",
+ "type": "http"
+ }
+ },
+ "inputs": []
+}
\ No newline at end of file
diff --git a/RSNet.dll b/RSNet.dll
new file mode 100644
index 0000000..0ec4acb
Binary files /dev/null and b/RSNet.dll differ
diff --git a/Surveillance-Client.rep/idata/00/00000001.prp b/Surveillance-Client.rep/idata/00/00000001.prp
new file mode 100644
index 0000000..6ac5817
--- /dev/null
+++ b/Surveillance-Client.rep/idata/00/00000001.prp
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Surveillance-Client.rep/idata/00/~00000000.db/db.2.gbf b/Surveillance-Client.rep/idata/00/~00000000.db/db.4.gbf
similarity index 99%
rename from Surveillance-Client.rep/idata/00/~00000000.db/db.2.gbf
rename to Surveillance-Client.rep/idata/00/~00000000.db/db.4.gbf
index 2bf9afd..60f8120 100644
Binary files a/Surveillance-Client.rep/idata/00/~00000000.db/db.2.gbf and b/Surveillance-Client.rep/idata/00/~00000000.db/db.4.gbf differ
diff --git a/Surveillance-Client.rep/idata/00/~00000001.db/db.1.gbf b/Surveillance-Client.rep/idata/00/~00000001.db/db.1.gbf
new file mode 100644
index 0000000..9039871
Binary files /dev/null and b/Surveillance-Client.rep/idata/00/~00000001.db/db.1.gbf differ
diff --git a/Surveillance-Client.rep/idata/00/~00000001.db/db.2.gbf b/Surveillance-Client.rep/idata/00/~00000001.db/db.2.gbf
new file mode 100644
index 0000000..ac6d377
Binary files /dev/null and b/Surveillance-Client.rep/idata/00/~00000001.db/db.2.gbf differ
diff --git a/Surveillance-Client.rep/idata/~index.bak b/Surveillance-Client.rep/idata/~index.bak
index b1e697f..cd7eee3 100644
--- a/Surveillance-Client.rep/idata/~index.bak
+++ b/Surveillance-Client.rep/idata/~index.bak
@@ -1,4 +1,6 @@
VERSION=1
/
-NEXT-ID:0
+ 00000001:RSNet.dll:c0a86451d8fa89550806671100
+ 00000000:Surveillance_client.exe:c0a86480f39a29301594652600
+NEXT-ID:2
MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/Surveillance-Client.rep/idata/~index.dat b/Surveillance-Client.rep/idata/~index.dat
index ab3e57b..cd7eee3 100644
--- a/Surveillance-Client.rep/idata/~index.dat
+++ b/Surveillance-Client.rep/idata/~index.dat
@@ -1,5 +1,6 @@
VERSION=1
/
+ 00000001:RSNet.dll:c0a86451d8fa89550806671100
00000000:Surveillance_client.exe:c0a86480f39a29301594652600
-NEXT-ID:1
+NEXT-ID:2
MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/Surveillance-Client.rep/idata/~journal.bak b/Surveillance-Client.rep/idata/~journal.bak
deleted file mode 100644
index 781ea1b..0000000
--- a/Surveillance-Client.rep/idata/~journal.bak
+++ /dev/null
@@ -1,2 +0,0 @@
-IADD:00000000:/Surveillance_client.exe
-IDSET:/Surveillance_client.exe:c0a86480f39a29301594652600
diff --git a/Surveillance-Client.rep/user/00/00000001.prp b/Surveillance-Client.rep/user/00/00000001.prp
new file mode 100644
index 0000000..52a13dd
--- /dev/null
+++ b/Surveillance-Client.rep/user/00/00000001.prp
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Surveillance-Client.rep/user/00/~00000000.db/db.2.gbf b/Surveillance-Client.rep/user/00/~00000000.db/db.2.gbf
new file mode 100644
index 0000000..c080fa7
Binary files /dev/null and b/Surveillance-Client.rep/user/00/~00000000.db/db.2.gbf differ
diff --git a/Surveillance-Client.rep/user/00/~00000001.db/db.1.gbf b/Surveillance-Client.rep/user/00/~00000001.db/db.1.gbf
new file mode 100644
index 0000000..cc5e308
Binary files /dev/null and b/Surveillance-Client.rep/user/00/~00000001.db/db.1.gbf differ
diff --git a/Surveillance-Client.rep/user/~journal.dat b/Surveillance-Client.rep/user/~journal.dat
new file mode 100644
index 0000000..6f44725
--- /dev/null
+++ b/Surveillance-Client.rep/user/~journal.dat
@@ -0,0 +1,2 @@
+IADD:00000001:/udf_c0a86451d8fa89550806671100
+IDSET:/udf_c0a86451d8fa89550806671100:c0a86451e45f97977061057400
diff --git a/SurveillanceClient/.gitignore b/SurveillanceClient/.gitignore
new file mode 100644
index 0000000..a639c16
--- /dev/null
+++ b/SurveillanceClient/.gitignore
@@ -0,0 +1,74 @@
+# Build results
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# User-specific files
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
+*.rsuser
+
+# Visual Studio
+.vs/
+*.VisualState.xml
+*.pidb
+*.svclog
+
+# JetBrains Rider
+.idea/
+*.DotSettings.user
+
+# VS Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# NuGet
+*.nupkg
+*.snupkg
+.nuget/
+packages/
+!packages/build/
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# ReSharper
+_ReSharper*/
+*.[Rr]e[Ss]harper
+
+# Test results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Local secrets / configuration overrides (NEVER COMMIT CREDENTIALS)
+appsettings.Development.json
+appsettings.Local.json
+appsettings.*.Local.json
+secrets.json
+secrets.*.json
+*.secrets
+.env
+.env.*
+
+# OS
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+.DS_Store
+
+# Downloaded video output (local runtime artifact)
+downloads/
+output/
diff --git a/SurveillanceClient/Directory.Build.props b/SurveillanceClient/Directory.Build.props
new file mode 100644
index 0000000..167bafe
--- /dev/null
+++ b/SurveillanceClient/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+ net10.0-windows
+ latest
+ enable
+ enable
+ true
+
+ true
+
+ x86
+ x86
+
+
diff --git a/SurveillanceClient/README.md b/SurveillanceClient/README.md
new file mode 100644
index 0000000..f899fa2
--- /dev/null
+++ b/SurveillanceClient/README.md
@@ -0,0 +1,62 @@
+# SurveillanceClient
+
+.NET 10 host for driving the vendor `RSNet.dll` (32-bit) to download
+recorded video from a surveillance NVR/DVR.
+
+## Layout
+
+- `src/SurveillanceClient.Core` — class library that wraps the `RSNet.dll`
+ native exports behind a service interface (`IRsNetClient`).
+- `src/SurveillanceClient.Cli` — console host used for iterative,
+ manual testing of the Core library against a real device.
+
+## Platform
+
+`RSNet.dll` is a 32-bit Visual Studio DLL using `__stdcall`. Every
+project in this solution targets `x86` via `Directory.Build.props`.
+
+## Running
+
+The vendor DLL and its dependencies live in
+`i:\Apps\Surveillance_client`. The CLI sets that directory as the
+native DLL search path at startup so the DLL and its `.ini` config
+are found.
+
+## Credentials
+
+Credentials are **never committed**. Configure them via .NET user
+secrets (recommended) or a `appsettings.Local.json` file (ignored by
+`.gitignore`).
+
+### User secrets (recommended)
+
+```powershell
+cd src/SurveillanceClient.Cli
+dotnet user-secrets set "Device:Host" "192.168.100.92"
+dotnet user-secrets set "Device:Port" "9000"
+dotnet user-secrets set "Device:Username" "admin"
+dotnet user-secrets set "Device:Password" "your-password-here"
+```
+
+### appsettings.Local.json (alternative)
+
+Create `src/SurveillanceClient.Cli/appsettings.Local.json`:
+
+```json
+{
+ "Device": {
+ "Host": "192.168.100.92",
+ "Port": 9000,
+ "Username": "admin",
+ "Password": "your-password-here"
+ }
+}
+```
+
+## Build / Run
+
+```powershell
+cd SurveillanceClient
+dotnet build
+dotnet run --project src/SurveillanceClient.Cli
+```
diff --git a/SurveillanceClient/SurveillanceClient.slnx b/SurveillanceClient/SurveillanceClient.slnx
new file mode 100644
index 0000000..7df5b44
--- /dev/null
+++ b/SurveillanceClient/SurveillanceClient.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SurveillanceClient/src/SurveillanceClient.Cli/Program.cs b/SurveillanceClient/src/SurveillanceClient.Cli/Program.cs
new file mode 100644
index 0000000..86b6fe6
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Cli/Program.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using SurveillanceClient.Cli;
+using SurveillanceClient.Core;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+// Local, never-committed overrides for credentials etc.
+builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false);
+
+if (builder.Environment.IsDevelopment())
+{
+ builder.Configuration.AddUserSecrets(optional: true);
+}
+
+builder.Services.AddSurveillanceClientCore();
+builder.Services.AddHostedService();
+
+using var host = builder.Build();
+
+try
+{
+ await host.RunAsync();
+}
+catch (Exception ex)
+{
+ host.Services.GetRequiredService>()
+ .LogCritical(ex, "Unhandled exception");
+ return 1;
+}
+
+return 0;
+
+/// Marker type used to anchor user-secrets and logger categories.
+public partial class Program;
diff --git a/SurveillanceClient/src/SurveillanceClient.Cli/SurveillanceClient.Cli.csproj b/SurveillanceClient/src/SurveillanceClient.Cli/SurveillanceClient.Cli.csproj
new file mode 100644
index 0000000..12e14f5
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Cli/SurveillanceClient.Cli.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ SurveillanceClient.Cli
+ SurveillanceClient.Cli
+ false
+ surveillance-client-cli-4f0a1c0d
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SurveillanceClient/src/SurveillanceClient.Cli/TestHarness.cs b/SurveillanceClient/src/SurveillanceClient.Cli/TestHarness.cs
new file mode 100644
index 0000000..fb2588c
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Cli/TestHarness.cs
@@ -0,0 +1,87 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using SurveillanceClient.Core.Configuration;
+using SurveillanceClient.Core.Services;
+
+namespace SurveillanceClient.Cli;
+
+///
+/// Iterative manual-test harness. Runs once at startup against the
+/// configured device, logs status callbacks, then requests shutdown.
+/// Extended step-by-step as the native API is proven out.
+///
+internal sealed class TestHarness : BackgroundService
+{
+ private readonly ILogger _logger;
+ private readonly IRsNetClient _client;
+ private readonly DeviceOptions _device;
+ private readonly IHostApplicationLifetime _lifetime;
+
+ public TestHarness(
+ ILogger logger,
+ IRsNetClient client,
+ IOptions device,
+ IHostApplicationLifetime lifetime)
+ {
+ _logger = logger;
+ _client = client;
+ _device = device.Value;
+ _lifetime = lifetime;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ try
+ {
+ await RunAsync(stoppingToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ // graceful shutdown
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Test harness failed");
+ }
+ finally
+ {
+ _lifetime.StopApplication();
+ }
+ }
+
+ private async Task RunAsync(CancellationToken ct)
+ {
+ _logger.LogInformation("=== Stage 1: Initialize RSNet.dll ===");
+ await _client.InitializeAsync(ct).ConfigureAwait(false);
+ _logger.LogInformation("RSNet initialized successfully.");
+
+ if (string.IsNullOrWhiteSpace(_device.Host) ||
+ string.IsNullOrWhiteSpace(_device.Username))
+ {
+ _logger.LogWarning(
+ "Device credentials not configured. Set them via user-secrets or appsettings.Local.json " +
+ "to run the connection stage. Skipping connect.");
+ return;
+ }
+
+ _logger.LogInformation(
+ "=== Stage 2: Connect to {Host}:{Port} as {User} ===",
+ _device.Host, _device.Port, _device.Username);
+
+ using var session = _client.Connect(_device);
+ session.StatusChanged += (_, e) =>
+ _logger.LogInformation("Session status callback: code=0x{Code:X} ({CodeDec})", e.Code, e.Code);
+
+ _logger.LogInformation("Session handle: 0x{Handle:X}. Waiting up to 15s for login callbacks...",
+ session.Handle.ToInt64());
+
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(15), ct).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { }
+
+ _logger.LogInformation("=== Stage 2 complete. Stop here until login status codes are mapped. ===");
+ }
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Cli/appsettings.json b/SurveillanceClient/src/SurveillanceClient.Cli/appsettings.json
new file mode 100644
index 0000000..7754d5e
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Cli/appsettings.json
@@ -0,0 +1,18 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Warning",
+ "SurveillanceClient": "Debug"
+ }
+ },
+ "RsNet": {
+ "NativeDirectory": "i:\\Apps\\Surveillance_client"
+ },
+ "Device": {
+ "Host": "",
+ "Port": 9000,
+ "Username": "",
+ "Password": ""
+ }
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Configuration/DeviceOptions.cs b/SurveillanceClient/src/SurveillanceClient.Core/Configuration/DeviceOptions.cs
new file mode 100644
index 0000000..7d628df
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Configuration/DeviceOptions.cs
@@ -0,0 +1,15 @@
+namespace SurveillanceClient.Core.Configuration;
+
+///
+/// Credentials and endpoint for a target surveillance device.
+/// Supplied via user secrets or local config — never committed.
+///
+public sealed class DeviceOptions
+{
+ public const string SectionName = "Device";
+
+ public string Host { get; set; } = string.Empty;
+ public ushort Port { get; set; } = 9000;
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Configuration/RsNetOptions.cs b/SurveillanceClient/src/SurveillanceClient.Core/Configuration/RsNetOptions.cs
new file mode 100644
index 0000000..2042a61
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Configuration/RsNetOptions.cs
@@ -0,0 +1,22 @@
+namespace SurveillanceClient.Core.Configuration;
+
+///
+/// Runtime options for locating and loading RSNet.dll.
+///
+public sealed class RsNetOptions
+{
+ public const string SectionName = "RsNet";
+
+ ///
+ /// Absolute path to the directory that contains RSNet.dll
+ /// and its companion files (e.g. RSNet.ini, P2P helpers).
+ /// Typically the vendor install directory.
+ ///
+ public string NativeDirectory { get; set; } = @"i:\Apps\Surveillance_client";
+
+ ///
+ /// Optional encryption value passed to RSNetSetEncription.
+ /// null skips the call.
+ ///
+ public uint? Encryption { get; set; }
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Native/RsNetNative.cs b/SurveillanceClient/src/SurveillanceClient.Core/Native/RsNetNative.cs
new file mode 100644
index 0000000..b3826dd
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Native/RsNetNative.cs
@@ -0,0 +1,69 @@
+using System.Runtime.InteropServices;
+
+namespace SurveillanceClient.Core.Native;
+
+///
+/// Raw P/Invoke bindings for RSNet.dll.
+///
+/// ABI notes (see docs/surveillance-client-rsnet-download-analysis.md):
+///
+/// - Architecture: x86/32-bit. The host process MUST run as x86.
+/// - Calling convention: StdCall (callee cleans stack).
+/// - Strings: ANSI (char*), not UTF-16.
+///
+///
+/// Request structs are intentionally passed as opaque
+/// during the reverse-engineering phase. Strongly-typed wrappers may be
+/// introduced as the native layouts are confirmed.
+///
+internal static partial class RsNetNative
+{
+ private const string Dll = "RSNet.dll";
+
+ [LibraryImport(Dll, StringMarshalling = StringMarshalling.Utf8)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial void RSNetInit();
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial void RSNetRelease();
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial void RSNetSetEncription(uint value);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial IntPtr RSNetConnectionStart(IntPtr request);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial IntPtr RSNetConnectionStartEx(IntPtr request);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial int RSNetQueryRecord(IntPtr session, IntPtr queryRequest);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial int RSNetQueryRecordEx(IntPtr session, IntPtr queryRequest);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial IntPtr RSNetStartDownloadRecord(IntPtr session, IntPtr downloadRequest);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial IntPtr RSNetStartDownloadRecordEx(IntPtr session, IntPtr downloadRequest);
+
+ [LibraryImport(Dll)]
+ [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
+ internal static partial void RSNetReqRecordData(IntPtr recordPlayHandle);
+
+ ///
+ /// Session/status callback dispatched by the DLL's login/worker thread.
+ /// Exact signature is provisional — see the analysis doc.
+ ///
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ internal delegate void RsNetStatusCallback(uint code, IntPtr userContext);
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Native/RsNetStructs.cs b/SurveillanceClient/src/SurveillanceClient.Core/Native/RsNetStructs.cs
new file mode 100644
index 0000000..5763771
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Native/RsNetStructs.cs
@@ -0,0 +1,34 @@
+using System.Runtime.InteropServices;
+
+namespace SurveillanceClient.Core.Native;
+
+///
+/// Provisional layout for RSNetConnectionStart's request struct.
+///
+/// Based on decompilation of the copy-in path inside RSNet.dll:
+///
+/// - +0x00 pointer to ANSI address string
+/// - +0x04 16-bit port
+/// - +0x08 pointer to ANSI username string
+/// - +0x0C pointer to ANSI password string
+/// - +0x10..+0x28 misc 32-bit fields (mode/callback-related)
+///
+///
+/// This is NOT fully validated. Treat as a best-effort starting point.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RsNetConnectRequest
+{
+ public IntPtr AddressAnsi;
+ public ushort Port;
+ public ushort Padding0;
+ public IntPtr UsernameAnsi;
+ public IntPtr PasswordAnsi;
+ public uint Field10;
+ public uint Field14;
+ public uint Field18;
+ public uint Field1C;
+ public uint Field20;
+ public uint Field24;
+ public uint Field28;
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Native/Win32.cs b/SurveillanceClient/src/SurveillanceClient.Core/Native/Win32.cs
new file mode 100644
index 0000000..c3970f0
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Native/Win32.cs
@@ -0,0 +1,26 @@
+using System.Runtime.InteropServices;
+
+namespace SurveillanceClient.Core.Native;
+
+///
+/// Win32 entry points used to control the native DLL search path before
+/// loading RSNet.dll. The vendor DLL expects its install directory
+/// on the search path so it can find RSNet.ini and helper binaries.
+///
+internal static partial class Win32
+{
+ [LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetDllDirectoryW(string? lpPathName);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ internal static partial IntPtr AddDllDirectory(
+ [MarshalAs(UnmanagedType.LPWStr)] string NewDirectory);
+
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool SetDefaultDllDirectories(uint DirectoryFlags);
+
+ internal const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;
+ internal const uint LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400;
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/ServiceCollectionExtensions.cs b/SurveillanceClient/src/SurveillanceClient.Core/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a140317
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/ServiceCollectionExtensions.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using SurveillanceClient.Core.Configuration;
+using SurveillanceClient.Core.Services;
+
+namespace SurveillanceClient.Core;
+
+///
+/// DI registration helpers for the SurveillanceClient Core services.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Registers and binds its options from
+ /// configuration. The client is a singleton because RSNet.dll
+ /// holds process-wide global state.
+ ///
+ public static IServiceCollection AddSurveillanceClientCore(this IServiceCollection services)
+ {
+ services.AddOptions().BindConfiguration(RsNetOptions.SectionName);
+ services.AddOptions().BindConfiguration(DeviceOptions.SectionName);
+
+ services.TryAddSingleton();
+
+ return services;
+ }
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Services/IRsNetClient.cs b/SurveillanceClient/src/SurveillanceClient.Core/Services/IRsNetClient.cs
new file mode 100644
index 0000000..fefd089
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Services/IRsNetClient.cs
@@ -0,0 +1,46 @@
+using SurveillanceClient.Core.Configuration;
+
+namespace SurveillanceClient.Core.Services;
+
+///
+/// High-level, managed facade over RSNet.dll. Intended lifetime:
+/// singleton per process. must be called
+/// before any session is opened.
+///
+public interface IRsNetClient : IAsyncDisposable
+{
+ /// Whether RSNetInit has been called successfully.
+ bool IsInitialized { get; }
+
+ ///
+ /// Loads RSNet.dll from the configured directory and calls
+ /// RSNetInit. Safe to call more than once; subsequent calls are no-ops.
+ ///
+ Task InitializeAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Opens a direct-IP session by calling RSNetConnectionStart.
+ /// The returned session has not yet necessarily completed login — the
+ /// DLL performs login asynchronously on a worker thread.
+ ///
+ IRsNetSession Connect(DeviceOptions device);
+}
+
+///
+/// Handle-bearing wrapper around a single RSNet.dll session.
+/// Disposing closes the native session and releases any pinned buffers.
+///
+public interface IRsNetSession : IDisposable
+{
+ /// Native session handle returned by RSNetConnectionStart.
+ IntPtr Handle { get; }
+
+ /// Raised for each native status/event callback.
+ event EventHandler? StatusChanged;
+}
+
+/// Status event payload forwarded from the native callback.
+public sealed class RsNetStatusEventArgs(uint code) : EventArgs
+{
+ public uint Code { get; } = code;
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Services/RsNetClient.cs b/SurveillanceClient/src/SurveillanceClient.Core/Services/RsNetClient.cs
new file mode 100644
index 0000000..35426f8
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Services/RsNetClient.cs
@@ -0,0 +1,122 @@
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using SurveillanceClient.Core.Configuration;
+using SurveillanceClient.Core.Native;
+
+namespace SurveillanceClient.Core.Services;
+
+///
+internal sealed class RsNetClient : IRsNetClient
+{
+ private readonly ILogger _logger;
+ private readonly RsNetOptions _options;
+ private readonly SemaphoreSlim _initLock = new(1, 1);
+ private bool _initialized;
+ private bool _disposed;
+
+ public RsNetClient(ILogger logger, IOptions options)
+ {
+ _logger = logger;
+ _options = options.Value;
+ }
+
+ public bool IsInitialized => _initialized;
+
+ public async Task InitializeAsync(CancellationToken cancellationToken = default)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (_initialized) return;
+
+ await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (_initialized) return;
+
+ if (!OperatingSystem.IsWindows())
+ throw new PlatformNotSupportedException("RSNet.dll is Windows-only.");
+
+ if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
+ throw new PlatformNotSupportedException(
+ $"RSNet.dll is 32-bit (x86). Current process is {RuntimeInformation.ProcessArchitecture}. " +
+ "Build and run the host as x86.");
+
+ var dir = _options.NativeDirectory;
+ if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
+ throw new DirectoryNotFoundException(
+ $"RsNetOptions.NativeDirectory not found: '{dir}'. " +
+ "Configure it to the vendor install directory containing RSNet.dll.");
+
+ var dllPath = Path.Combine(dir, "RSNet.dll");
+ if (!File.Exists(dllPath))
+ throw new FileNotFoundException("RSNet.dll not found in NativeDirectory.", dllPath);
+
+ _logger.LogInformation("Configuring native DLL search path: {Directory}", dir);
+
+ // Use the modern search list so both RSNet.dll and its implicit
+ // dependencies are resolved from the vendor directory.
+ Win32.SetDefaultDllDirectories(
+ Win32.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | Win32.LOAD_LIBRARY_SEARCH_USER_DIRS);
+ Win32.AddDllDirectory(dir);
+
+ // Legacy fallback for any implicit LoadLibrary calls by RSNet.dll itself.
+ Win32.SetDllDirectoryW(dir);
+
+ // Many vendor DLLs read .ini from the *current* working directory
+ // rather than the DLL's own directory. Switch CWD so RSNet.ini resolves.
+ var previousCwd = Environment.CurrentDirectory;
+ Environment.CurrentDirectory = dir;
+ _logger.LogDebug("CWD changed {Previous} -> {New}", previousCwd, dir);
+
+ _logger.LogInformation("Calling RSNetInit()");
+ RsNetNative.RSNetInit();
+
+ if (_options.Encryption is { } enc)
+ {
+ _logger.LogInformation("Calling RSNetSetEncription({Value})", enc);
+ RsNetNative.RSNetSetEncription(enc);
+ }
+
+ _initialized = true;
+ }
+ finally
+ {
+ _initLock.Release();
+ }
+ }
+
+ public IRsNetSession Connect(DeviceOptions device)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (!_initialized)
+ throw new InvalidOperationException(
+ $"{nameof(RsNetClient)} is not initialized. Call InitializeAsync first.");
+
+ ArgumentException.ThrowIfNullOrWhiteSpace(device.Host);
+ ArgumentException.ThrowIfNullOrWhiteSpace(device.Username);
+
+ return RsNetSession.Open(_logger, device);
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ if (_initialized)
+ {
+ try
+ {
+ _logger.LogInformation("Calling RSNetRelease()");
+ RsNetNative.RSNetRelease();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "RSNetRelease threw during shutdown");
+ }
+ }
+
+ _initLock.Dispose();
+ await Task.CompletedTask;
+ }
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/Services/RsNetSession.cs b/SurveillanceClient/src/SurveillanceClient.Core/Services/RsNetSession.cs
new file mode 100644
index 0000000..230627c
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/Services/RsNetSession.cs
@@ -0,0 +1,122 @@
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using SurveillanceClient.Core.Configuration;
+using SurveillanceClient.Core.Native;
+
+namespace SurveillanceClient.Core.Services;
+
+///
+/// Owns a single RSNetConnectionStart session handle plus the
+/// unmanaged buffers (ANSI strings, request struct) that the DLL reads
+/// from during the asynchronous login worker's lifetime.
+///
+internal sealed class RsNetSession : IRsNetSession
+{
+ private readonly ILogger _logger;
+ private readonly List _unmanagedAllocs = [];
+ private readonly RsNetNative.RsNetStatusCallback? _callback;
+ private IntPtr _handle;
+ private bool _disposed;
+
+ public IntPtr Handle => _handle;
+ public event EventHandler? StatusChanged;
+
+ private RsNetSession(ILogger logger, RsNetNative.RsNetStatusCallback callback)
+ {
+ _logger = logger;
+ _callback = callback;
+ }
+
+ internal static RsNetSession Open(ILogger logger, DeviceOptions device)
+ {
+ RsNetSession? session = null;
+ RsNetNative.RsNetStatusCallback callback = null!;
+ try
+ {
+ // The callback MUST be held by a managed field so the GC does not
+ // collect the thunk while the native worker still holds the pointer.
+ session = new RsNetSession(logger, (uint code, IntPtr ctx) =>
+ {
+ logger.LogDebug("RSNet status callback: code={Code} ctx={Ctx}", code, ctx);
+ session?.RaiseStatus(code);
+ });
+ callback = session._callback!;
+
+ var hostPtr = MarshalAnsi(session, device.Host);
+ var userPtr = MarshalAnsi(session, device.Username);
+ var passPtr = MarshalAnsi(session, device.Password);
+
+ var request = new RsNetConnectRequest
+ {
+ AddressAnsi = hostPtr,
+ Port = device.Port,
+ Padding0 = 0,
+ UsernameAnsi = userPtr,
+ PasswordAnsi = passPtr,
+ // Fields 0x10..0x28 are mode/callback-related and not yet
+ // fully validated. Leaving them zero matches the minimal
+ // direct-IP login path observed in Surveillance_client.exe.
+ };
+
+ var requestPtr = Marshal.AllocHGlobal(Marshal.SizeOf());
+ session._unmanagedAllocs.Add(requestPtr);
+ Marshal.StructureToPtr(request, requestPtr, fDeleteOld: false);
+
+ logger.LogInformation(
+ "RSNetConnectionStart host={Host} port={Port} user={User}",
+ device.Host, device.Port, device.Username);
+
+ var handle = RsNetNative.RSNetConnectionStart(requestPtr);
+ if (handle == IntPtr.Zero)
+ throw new InvalidOperationException("RSNetConnectionStart returned NULL.");
+
+ session._handle = handle;
+ return session;
+ }
+ catch
+ {
+ session?.Dispose();
+ GC.KeepAlive(callback);
+ throw;
+ }
+ }
+
+ private void RaiseStatus(uint code)
+ {
+ try
+ {
+ StatusChanged?.Invoke(this, new RsNetStatusEventArgs(code));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Status handler threw for code {Code}", code);
+ }
+ }
+
+ private static IntPtr MarshalAnsi(RsNetSession session, string? value)
+ {
+ var ptr = Marshal.StringToHGlobalAnsi(value ?? string.Empty);
+ session._unmanagedAllocs.Add(ptr);
+ return ptr;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ // The exported stop-session API has not been positively identified in
+ // the current RSNet.dll symbol recovery (see analysis doc). For now
+ // we simply release native buffers and let RSNetRelease() at shutdown
+ // tear down session state. Once the stop export is confirmed, call it
+ // here with _handle before freeing buffers.
+ foreach (var p in _unmanagedAllocs)
+ {
+ if (p != IntPtr.Zero) Marshal.FreeHGlobal(p);
+ }
+ _unmanagedAllocs.Clear();
+
+ // Keep the callback alive until after native cleanup would have run.
+ GC.KeepAlive(_callback);
+ }
+}
diff --git a/SurveillanceClient/src/SurveillanceClient.Core/SurveillanceClient.Core.csproj b/SurveillanceClient/src/SurveillanceClient.Core/SurveillanceClient.Core.csproj
new file mode 100644
index 0000000..35b92ba
--- /dev/null
+++ b/SurveillanceClient/src/SurveillanceClient.Core/SurveillanceClient.Core.csproj
@@ -0,0 +1,16 @@
+
+
+
+ SurveillanceClient.Core
+ SurveillanceClient.Core
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/surveillance-client-dpi-analysis.md b/docs/surveillance-client-dpi-analysis.md
index 3f79b10..db842d2 100644
--- a/docs/surveillance-client-dpi-analysis.md
+++ b/docs/surveillance-client-dpi-analysis.md
@@ -1,234 +1,389 @@
-# Surveillance Client DPI Analysis
+# Surveillance Client Server And Recorded Video Download Analysis
-Date: 2026-03-25
+Date: 2026-04-17
Target binary: `Surveillance_client.exe`
-Analysis method: Ghidra MCP against the already-imported and analyzed program database.
+Analysis method: Ghidra MCP against the already-open executable, with non-destructive analysis only. During this pass, allowed writes were limited to function renames and comments.
## Objective
-Determine how the client initializes its UI, whether it already contains any DPI-awareness logic, and what practical patch points exist for making the application usable on high-resolution displays.
+Determine how the client connects to remote devices and how it searches for and downloads recorded video, with the practical goal of building a small standalone downloader that can fetch an arbitrary duration from an arbitrary camera.
## Executive Summary
-- The binary is a Qt Widgets desktop application, and the available imports strongly suggest an older Qt 4-era codebase.
-- No direct static use of Windows DPI-awareness APIs was found.
-- No obvious embedded DPI manifest strings were found in the loaded program strings.
-- The application persists window geometry and display layout state through `QSettings`, including explicit `QRect` geometry records.
-- That persistence layer is likely to matter for any DPI patch, because old pixel-based geometry can become incorrect or visually tiny once DPI behavior changes.
+- The EXE is mostly UI, orchestration, and request-shaping code.
+- The real connection, record-query, playback-control, and download logic is implemented in `RSNET.DLL`.
+- Local playback and file rendering are implemented in `RSPLAY.DLL`.
+- The EXE contains a clear playback/download workflow that prepares request structures and hands them to `RSNET.DLL` exports.
+- The current executable analysis is enough to map the high-level flow and the parameter shapes, but not enough to recover the on-the-wire protocol. For that, `RSNet.dll` needs to be loaded into Ghidra next.
-## Confirmed Entry Flow
+## Installed Module Layout
-The startup chain currently identified is:
+The installed client directory contains the key vendor modules:
-1. `___tmainCRTStartup` at `0x00708d44`
-2. `qt_startup_dispatch` at `0x00709b00`
-3. `qWinMain` thunk at `0x00709c02`
-4. Application main routine at `0x004c7260`
+- `RSNet.dll`
+- `RSPlay.dll`
+- `QRSosAdapter.dll`
+- `QtNetwork4.dll`
-Notes:
+This matches the import thunks present in the EXE.
-- `qt_startup_dispatch` converts `GetCommandLineW()` output into a local 8-bit argv-style buffer and forwards execution into Qt startup.
-- This is a useful early patch region if a future binary modification needs to happen before the first top-level window is created.
+## Confirmed DLL Boundary
-## Evidence This Is An Older Qt Widgets Application
+The following named functions in the EXE are import thunks, not native implementations:
-The import table and strings include multiple indicators that this is not a modern Qt high-DPI-aware build:
+- `RSNetQueryParam` at `0x006e96f0`
+- `RSNetQueryParamEx` at `0x006e9720`
+- `RSNetAlarmSubscribe` at `0x006e99a0`
+- `RSPlayGetPlayFileTimeRange` at `0x006ea760`
-- `QApplication`
-- `QPlastiqueStyle`
-- `QProxyStyle`
-- `QStyleOptionViewItemV4`
-- `desktop`
-- `screenGeometry`
-- `availableGeometry`
-- `primaryScreen`
+Example:
-Why that matters:
+- `RSNetQueryParam` is just `JMP dword ptr [0x0076e648]`
-- `QPlastiqueStyle` and `QStyleOptionViewItemV4` are strong Qt 4-era signals.
-- A Qt 4-era app is much less likely to support automatic per-monitor DPI scaling cleanly.
-- If DPI awareness is turned on without additional UI scaling work, the usual failure mode is a crisp but very small interface.
+External symbol resolution in the loaded EXE confirms these libraries and exports:
-## What Was Not Found
+### `RSNET.DLL`
-The following searches did not produce evidence of a built-in DPI implementation:
+Relevant exports confirmed in the import table:
-- `SetProcessDPIAware`
-- `SetProcessDpiAwareness`
-- `SetThreadDpiAwarenessContext`
-- `WM_DPICHANGED`
-- `devicePixelRatio`
-- `HighDpi`
-- `QT_AUTO_SCREEN_SCALE_FACTOR`
-- `AA_EnableHighDpiScaling`
+- `RSNetConnectionStart`
+- `RSNetConnectionStartEx`
+- `RSNetQueryRecordEx`
+- `RSNetStartRecordPlayEx`
+- `RSNetReqRecordPlayCtrol`
+- `RSNetStartDownloadRecord`
+- `RSNetStartDownloadRecordEx`
+- `RSNetStopDownloadRecord`
+- `RSNetReqRecordData`
+- `RSNetStopRecordPlay`
+- `RSNetReplayRecordData`
+- `RSNetReposRecordData`
+- `RSNetGetDevInfo`
+- `RSNetSearchDev`
-I also searched program strings for common manifest content such as:
+### `RSPLAY.DLL`
-- `dpiAware`
-- `asmv3`
-- `requestedExecutionLevel`
-- `trustInfo`
+Relevant playback exports confirmed in the import table:
-None of those were found in the loaded strings.
-
-## Resource Layout
-
-The PE section layout includes a resource section:
-
-- `.rsrc: 0x00dd7000 - 0x00df05ff`
-
-Even with a resource section present, no obvious manifest or DPI-related XML strings surfaced through the Ghidra string search during this pass.
-
-## Settings And Geometry Findings
-
-Several settings-related functions were isolated and renamed in the Ghidra database for future work.
-
-### `init_app_settings_defaults` at `0x0059ca30`
-
-This function initializes many application settings keys and default values.
-
-Relevant confirmed keys:
-
-- `mainwindow/fullscreen`
-- `mainwindow/size`
-- `window_status`
-- `gen/liveviewstatus`
-- `gen/autolive`
-- `screenAutoSwitch/interval`
-- `screenAutoSwitch/forceSync`
-- `screenAutoSwitch/isOn`
-
-Relevant confirmed defaults:
-
-- `mainwindow/size = 0x400 x 0x300` which is `1024 x 768`
-- `mainwindow/fullscreen = false`
+- `RSPlayCreatePlayInstance`
+- `RSPlaySetPlayWnd`
+- `RSPlayStartPlay`
+- `RSPlaySetCallbackMessageEx`
+- `RSPlayImageCallback`
+- `RSPlayOpenPlayFile`
+- `RSPlayOpenPlayFileListEx`
+- `RSPlayGetPlayFileTimeRange`
+- `RSPlaySetCurPlayedTimeEx`
+- `RSPlayInputNetFrame`
+- `RSPlayStartlocalrecord`
+- `RSPlayStoplocalrecord`
Interpretation:
-- The main window has a fixed persisted default size in pixels.
-- That is a strong warning sign for DPI patching, because a legacy saved size can remain visually small after changing DPI behavior.
+- `RSNET.DLL` is the module that matters for device login, remote record queries, remote playback, and remote download.
+- `RSPLAY.DLL` is mostly decode/render/local playback support, not the primary network protocol implementation.
-### `load_window_status_from_settings` at `0x005a72b0`
+## Confirmed EXE-Side Flow
-This function reads a `QSettings` array named `window_status` and loads nested UI state.
+### UI widgets involved
-Confirmed per-window fields:
+The strings and handlers in the EXE show a playback and file-download workflow:
-- `windowGeometry`
-- `isCurWindow`
-- `isFullScreen`
-- `isMainWindow`
-- `maxTabIndex`
+- `Widget_Playback`
+- `Widget_Playback_FileDownload`
+- `Download By Date`
+- `Download By Files`
+- `DownLoad Start Time`
+- `MainStream`
+- `SubStream`
+- `playback-download`
+- `QRSAdapterReqRecordPlayCtrol`
-Confirmed nested view/screen fields:
+Relevant strings also show signals/slots for:
-- `view_status`
-- `mainview_status`
-- `screenIndex`
-- `screenBeginIndex`
-- `isScreenEnlarge`
-- `autoSwitchInterval`
-- `autoSwitchIndependent`
-- `autoSwitchIsOn`
-- `stream_status`
-- `groupChannelID`
-- `videoIndex`
-- `isCurSelected`
-- `isSoundOn`
-- `Emap_status`
-- `emapIDIndex`
+- `searchRequest(QString)`
+- `playbackResponse()`
+- `fileDownload()`
+- `download()`
+- `stopdownload()`
+
+### Functions renamed during analysis
+
+The following EXE functions were renamed in the Ghidra database:
+
+- `0x004a9690` -> `PlaybackFileDownloadWidget_connectSignals`
+- `0x0049f570` -> `RecordPlaybackControl_toggleMode`
+- `0x004afb30` -> `PlaybackFileDownloadWidget_download`
+- `0x004b1be0` -> `PlaybackFileDownloadWidget_downloadByTime`
+
+These names reflect verified behavior from decompilation.
+
+## What The Download UI Does
+
+### `PlaybackFileDownloadWidget_connectSignals`
+
+This function wires the file-download UI:
+
+- download button -> `download()`
+- stop button -> `stopdownload()`
+- cancel button -> `cancel()`
+- date/time selector changes -> `GetFileNumAndSize(QDateTime)`
+
+That establishes the UI entrypoint cleanly.
+
+### `PlaybackFileDownloadWidget_download`
+
+This is the main dispatcher for the download dialog.
+
+It has two modes:
+
+1. File-based download mode
+2. Time-range download mode
+
+In file-based mode it:
+
+- iterates rows in the record table
+- checks selected rows
+- tracks row state in a per-row metadata structure
+- starts a timer to process queued download items
+
+In time-range mode it:
+
+- reads the selected start and end `QDateTime` values from the UI
+- iterates available remote record segments
+- compares the requested interval against each segment
+- computes overlap windows
+- schedules one or more download requests accordingly
+
+The function logs several debug markers:
+
+- `DownloadByTime 111111111111111111`
+- `DownloadByTime 222222222222222222`
+- `DownloadByTime 33333333333333333333`
+- `DownloadByTime 444444444444444444444`
+
+Those correspond to different interval-overlap cases between the selected range and a returned recording segment.
+
+### `PlaybackFileDownloadWidget_downloadByTime`
+
+This function handles the actual by-time iteration across remote record segments.
+
+It reconstructs begin/end timestamps from record metadata, converts them to `QDateTime`, compares them against the user-selected range, then dispatches a lower-level download request for the relevant slice.
+
+The logic strongly suggests the client does not ask the device for an arbitrary time range in one step unless the selected window aligns with a single record segment. Instead, it walks matching segments and issues one or more segment-bounded download requests.
+
+Implication for a standalone tool:
+
+- arbitrary time-range download likely requires:
+ - querying the device for record segments first
+ - intersecting those with the requested interval
+ - issuing one or more download requests
+ - stitching or sequencing the resulting data if needed
+
+## EXE-Side RSNET Wrapper Layer
+
+The EXE contains thin wrappers around the `RSNET.DLL` APIs. These wrappers are useful because they reveal request structure shapes and callback wiring even though the real protocol lives in the DLL.
+
+### Connection wrapper
+
+`FUN_006eaf70`
+
+Behavior:
+
+- copies fields from a caller-supplied struct into a local 0x38-byte request block
+- stores two callback/context values into the owning object
+- calls `RSNetConnectionStartEx(&local_38)`
Interpretation:
-- The application stores a substantial amount of layout state in settings.
-- `windowGeometry` is especially important because it appears to be persisted as a `QRect`.
-- If the process DPI mode changes, previously stored geometry may need migration, scaling, deletion, or selective reset.
+- connection/login is performed through `RSNetConnectionStartEx`
+- the caller passes a structured login/connect request, not loose scalar parameters
+- the request likely includes address or P2P information, credentials, and callback context
-### `save_window_status_to_settings` at `0x005a6170`
+### Remote record query wrapper
-This function writes back the same structures into `QSettings`.
+`FUN_006eb6f0`
+
+Behavior:
+
+- allocates a small 8-byte helper object from fields `param_2[0x12]` and `param_2[0x11]`
+- copies roughly 0x60 bytes of request data into a local buffer
+- assigns completion callbacks
+- calls `RSNetQueryRecordEx(handle, &local_64)`
Interpretation:
-- Even if a DPI hack works visually at first launch, the application can overwrite the new layout with old pixel-based assumptions on exit.
-- Any durable solution should consider both the load and save side of geometry persistence.
+- remote record search is performed with `RSNetQueryRecordEx`
+- the query request is non-trivial and includes multiple fields beyond just channel and time
+- two tail fields are treated specially and likely represent callback target/context or an auxiliary selection object
-### `apply_mainwindow_fullscreen_setting` at `0x0047c1f0`
+### Remote playback wrapper
-This function compares a key against `mainwindow/fullscreen` and activates the matching `QAction` when the value is true.
+`FUN_006e9e00`
+
+Behavior:
+
+- creates an `RSPlay` instance via `RSPlayCreatePlayInstance(1)`
+- optionally binds the playback window handle via `RSPlaySetPlayWnd`
+- starts local rendering via `RSPlayStartPlay`
+- registers callback handlers with `RSPlaySetCallbackMessageEx` and `RSPlayImageCallback`
+- fills a request block from the provided playback parameters
+- calls `RSNetStartRecordPlayEx(connectionHandle, &local_40)`
Interpretation:
-- The UI state is driven through Qt actions, not direct Win32 window management.
-- That reinforces the view that DPI fixes will likely need to be applied at Qt startup or Qt widget/layout level rather than through a narrow Win32 patch alone.
+- remote recorded playback is a network operation through `RSNET.DLL`
+- incoming data is then fed into an `RSPLAY.DLL` decode/render pipeline
+- the standalone downloader probably does not need the `RSPLAY` path unless raw playback streaming is required instead of file download
-## Implications For A DPI Fix
+### Download wrappers
-### Most likely current problem model
+`FUN_006e9810`
-Based on the evidence, the binary looks like a legacy Qt Widgets application with no obvious modern DPI-awareness implementation. That creates two realistic scenarios:
+Behavior:
-1. The process is effectively DPI-unaware and Windows bitmap scaling is not being applied in the way the user wants.
-2. The process is being made DPI-aware somewhere outside the obvious static import path, causing the UI to render at legacy pixel sizes and appear tiny.
+- allocates a 12-byte state object
+- copies a compact request from the caller
+- sets a callback function `FUN_006e8690`
+- calls `RSNetStartDownloadRecord(handle, &local_14)`
-This analysis did not prove which of those two runtime cases is active on the target system. It did establish that the executable itself does not appear to contain a straightforward built-in high-DPI implementation.
+`FUN_006e98b0`
-### Why a simple `SetProcessDPIAware` patch is risky
+Behavior:
-For an older Qt Widgets app:
+- allocates a 12-byte state object
+- copies a slightly larger request including an extra byte field
+- sets callback `FUN_006e8730`
+- calls `RSNetStartDownloadRecordEx(handle, &local_20)`
-- enabling DPI awareness alone often produces a sharp but tiny UI
-- stored `QRect` geometry may restore undersized windows
-- icon sizes, style metrics, and custom-painted controls may remain pixel-based
-- there is no evidence yet of `WM_DPICHANGED` handling for per-monitor updates
+`FUN_006e9980`
-### What a workable solution will probably require
+Behavior:
-At minimum, a successful patch likely needs one of these paths:
+- calls `RSNetStopDownloadRecord(downloadHandle)`
+- frees the associated state object
-1. A compatibility-style solution that lets Windows scale the app acceptably.
-2. An early startup patch plus application-wide Qt scaling adjustments before top-level widgets are shown.
-3. A deeper patch that also normalizes saved geometry and possibly icon/font/style metrics.
+Interpretation:
-## Ghidra Database Changes Made During Analysis
+- there are at least two remote download entrypoints: base and extended
+- the extended form includes one extra byte field that may be stream type, record type, or a mode selector
+- both return or populate a download handle that is later passed to `RSNetStopDownloadRecord`
-The following functions were renamed in the active program database:
+### Record data callback path
-- `0x00709b00` -> `qt_startup_dispatch`
-- `0x0059ca30` -> `init_app_settings_defaults`
-- `0x005a72b0` -> `load_window_status_from_settings`
-- `0x005a6170` -> `save_window_status_to_settings`
-- `0x0047c1f0` -> `apply_mainwindow_fullscreen_setting`
+`FUN_006e85b0`
-Decompiler comments were also added at those entrypoints summarizing their purpose.
+Behavior:
-## Current Confidence Level
+- if callback state is valid and `param_1 == 1`, it calls `RSNetReqRecordData(*param_2)`
+- otherwise it posts a Qt event with type `0x3ed` to a target object
-High confidence:
+Interpretation:
-- Qt-based GUI application
-- older Qt lineage is likely
-- no obvious static DPI-awareness implementation exists
-- window geometry persistence is real and important
+- remote playback/download is at least partly callback-driven
+- one callback state value appears to be a target `QObject`
+- `RSNetReqRecordData` is likely a pull/continue/read-next mechanism for record data transfer
-Medium confidence:
+### Record playback control
-- the best durable fix will require more than a manifest-only change
-- geometry migration/reset will likely be necessary for a clean result
+`RecordPlaybackControl_toggleMode`
-Low confidence without runtime testing:
+Behavior:
-- whether the best final mode is DPI-unaware with OS scaling, system-DPI-aware, or a custom Qt-side scaling patch
+- toggles internal playback state based on UI mode values `0x10` through `0x13`
+- logs `QRSAdapterReqRecordPlayCtrol`
+- calls a helper that eventually dispatches `RSNetReqRecordPlayCtrol`
-## Recommended Direction
+Interpretation:
-Treat this as a legacy-Qt DPI retrofit problem, not just a missing API call problem.
+- pause/resume/seek/speed or similar playback controls go through `RSNetReqRecordPlayCtrol`
+- this is playback control, not initial search or initial download setup
-The next pass should focus on:
+## Current Reconstructed Download Pipeline
-- locating the `QApplication` construction site inside `0x004c7260`
-- locating first top-level window creation/show
-- tracing where `windowGeometry` is applied to the main window
-- deciding whether to pursue an OS-scaling strategy or a real application-side scaling patch
\ No newline at end of file
+From the EXE alone, the likely pipeline is:
+
+1. Establish device/session connection through `RSNetConnectionStartEx`
+2. Query available recordings with `RSNetQueryRecordEx`
+3. In the UI, present file/segment rows to the user
+4. For time-based download, intersect the requested interval against returned record segments
+5. For each needed segment or sub-range, call either `RSNetStartDownloadRecord` or `RSNetStartDownloadRecordEx`
+6. Receive progress/data/completion via callback functions and Qt events
+7. Stop transfer with `RSNetStopDownloadRecord` when complete or canceled
+
+## What This Means For A Standalone Downloader
+
+At a high level, the minimum downloader architecture is likely:
+
+1. Login/connect request builder
+2. Record query request builder
+3. Segment intersection logic for arbitrary user time ranges
+4. Download request builder
+5. Callback/event loop or polling layer for transfer progress and completion
+
+The most important unresolved details are all inside `RSNet.dll`:
+
+- exact login request structure
+- whether connection uses direct IP, media port, P2P ID, or multiple modes under one API
+- exact query-record structure fields
+- exact download request structure fields
+- callback message formats and result codes
+- file format or chunk framing of downloaded data
+- authentication handshake and any encryption or session-key behavior
+
+## What Is Known Versus Unknown
+
+### Known from EXE analysis
+
+- The EXE does not appear to implement the wire protocol itself.
+- `RSNET.DLL` is the primary networking and remote-record module.
+- `RSPLAY.DLL` handles playback/render support.
+- The UI supports both file-based and time-based record download.
+- Arbitrary time-range download is implemented as interval intersection over returned recording segments.
+- The download path uses `RSNetStartDownloadRecord` or `RSNetStartDownloadRecordEx`.
+- Record search uses `RSNetQueryRecordEx`.
+- Record playback uses `RSNetStartRecordPlayEx` and `RSNetReqRecordPlayCtrol`.
+
+### Unknown until `RSNet.dll` is analyzed
+
+- packet layout and transport protocol
+- exact authentication mechanism
+- exact request structure fields
+- whether the client speaks directly to the device, uses P2P relay logic, or switches transport modes internally
+- how downloaded bytes are framed and persisted to disk
+
+## Recommended Next Step
+
+Load `RSNet.dll` into Ghidra next.
+
+That is the correct next target if the goal is to build an independent downloader rather than automate the existing EXE.
+
+After `RSNet.dll` is loaded, the next analysis pass should focus on:
+
+- `RSNetConnectionStartEx`
+- `RSNetQueryRecordEx`
+- `RSNetStartDownloadRecord`
+- `RSNetStartDownloadRecordEx`
+- `RSNetReqRecordData`
+- `RSNetStopDownloadRecord`
+- any credential, P2P, relay, encryption, or session-management helpers they call
+
+## Credentials And Live Validation
+
+Credentials are not required yet for the current static pass.
+
+They will become useful later for one of these reasons:
+
+- validating a reconstructed downloader against a real target
+- comparing dynamic behavior with captured traffic
+- understanding whether connection mode changes based on device type, P2P ID, or network reachability
+
+If live testing becomes necessary, credentials should be stored in an untracked local file such as a new `.gitignore`d notes/config file inside the repository or workspace.
+
+## Analysis State Notes
+
+- Earlier in the session, a few function definitions were accidentally deleted by using the wrong MCP endpoint on the writable program. Those functions were restored immediately.
+- A repository instruction file now explicitly forbids destructive Ghidra actions during analysis unless the user explicitly requests them.
+- Current repository instruction file: `.github/copilot-instructions.md`
\ No newline at end of file
diff --git a/docs/surveillance-client-rsnet-download-analysis.md b/docs/surveillance-client-rsnet-download-analysis.md
new file mode 100644
index 0000000..983d00d
--- /dev/null
+++ b/docs/surveillance-client-rsnet-download-analysis.md
@@ -0,0 +1,911 @@
+# RSNet Download Analysis
+
+Date: 2026-04-17
+
+Target binary: `RSNet.dll`
+
+Related binaries:
+
+- `Surveillance_client.exe`
+- `RSPlay.dll`
+
+Analysis method: Ghidra MCP against the loaded `RSNet.dll`, using read-only inspection plus function renames/comments only.
+
+## Objective
+
+Determine enough of the `RSNet.dll` API and behavior to build a simple script that can:
+
+1. initialize the DLL
+2. connect/login to a device
+3. query recorded segments for a camera
+4. request download of a chosen interval or file segment
+5. wait for completion and retrieve the saved file
+
+## Bottom Line
+
+At this point, the DLL has been analyzed far enough to outline a realistic first-pass downloader script that calls `RSNet.dll` directly with `ctypes` or similar FFI.
+
+What is established:
+
+- `RSNet.dll` owns the real connection, login, record-query, playback, and record-download logic.
+- `RSNet.dll` performs its own socket I/O and background worker management.
+- the EXE is mainly a UI and adapter layer that fills structs and hands them to `RSNet.dll`
+- the download worker inside `RSNet.dll` writes data locally through an internal record-file layer rather than requiring the caller to manually process frame packets
+- a simple standalone script is plausible without reversing the entire wire protocol, because the DLL already encapsulates the transport
+
+What is not fully nailed down yet:
+
+- exact C struct definitions for all exported APIs
+- the full callback signature set and all event codes
+- exact output container selection logic between AVI, MP4, or the vendor private format
+
+Even with those gaps, there is now enough to define the likely call sequence and the minimum reverse-engineering targets for a first prototype.
+
+## Exported API Surface Relevant To Downloading
+
+Confirmed exports in `RSNet.dll`:
+
+- `RSNetInit` at `0x1001ae30`
+- `RSNetSetEncription` at `0x1001b570`
+- `RSNetConnectionStart` at `0x1001c350`
+- `RSNetConnectionStartEx` at `0x1001c3a0`
+- `RSNetQueryRecord` at `0x1001c640`
+- `RSNetAsyncQueryRecord` at `0x1001c660`
+- `RSNetQueryRecordEx` at `0x1001c680`
+- `RSNetAsyncQueryRecordEx` at `0x1001c6a0`
+- `RSNetAsyncQueryRecordStop` at `0x1001c6c0`
+- `RSNetStartDownloadRecord` at `0x1001c780`
+- `RSNetStartDownloadRecordEx` at `0x1001c810`
+- `RSNetStartRecordPlay` at `0x1001c870`
+- `RSNetStartRecordPlayEx` at `0x1001c9a0`
+- `RSNetReqRecordData` at `0x1001ca00`
+- `RSNetReqRecordPlayCtrol` at `0x1001ca40`
+- `RSNetRelease` at `0x1001b4e0`
+
+Interpretation:
+
+- a standalone downloader should use the DLL exports directly
+- there is no need to reproduce the underlying socket protocol if the goal is just to automate download on Windows where the vendor DLL is available
+
+## Initialization
+
+### `RSNetInit`
+
+`RSNetInit`:
+
+- initializes logging from `RSNet.ini`
+- calls `WSAStartup(0x202, ...)`
+- prepares global DLL state
+
+Practical implication:
+
+- any standalone script must call `RSNetInit()` before any connection or query function
+
+### `RSNetSetEncription`
+
+`RSNetSetEncription(value)` simply stores a global value.
+
+Practical implication:
+
+- there is some configurable encryption mode or toggle
+- it may need to be set explicitly for some device families, but this has not yet been proven necessary for the basic flow
+
+## Connection / Login
+
+### Public entry points
+
+- `RSNetConnectionStart`
+- `RSNetConnectionStartEx`
+
+Both functions:
+
+- allocate a per-device/session object of size `0xe98`
+- initialize internal locks, events, strings, and worker state
+- copy caller-provided login parameters into the session object
+- queue a background login/connection worker
+- return a session handle pointer on success, or `NULL` on failure
+
+### `RSNetConnectionStart`
+
+This is the simpler path and likely the better first target for a prototype.
+
+From decompilation, the input struct used by `RSNetConnectionStart` appears to have this rough layout:
+
+```c
+struct RSNET_CONNECT_REQ_BASE {
+ char *address_or_id; // offset 0x00
+ uint16_t port; // offset 0x04
+ char *username; // offset 0x08
+ char *password; // offset 0x0c
+ uint32_t field10; // offset 0x10
+ uint32_t field14; // offset 0x14
+ uint32_t field18; // offset 0x18
+ uint32_t field1c; // offset 0x1c
+ uint32_t field20; // offset 0x20
+ uint32_t field24; // offset 0x24
+ uint32_t field28; // offset 0x28
+};
+```
+
+The DLL copies these into its internal session object as:
+
+- destination string at session `+0x04`
+- port at session `+0x20`
+- username at session `+0x28`
+- password at session `+0x44`
+- several mode or callback related fields at `+0x68 .. +0x80`
+
+### `RSNetConnectionStartEx`
+
+The extended path supports more than direct IP login. Decompiled behavior shows:
+
+- a connection mode field at request index `0x0b`
+- if that mode is zero, the DLL treats the first string as the device address directly
+- if non-zero, it stores the first string in an alternate field and forces the target address to `127.0.0.1`
+- when mode is `1` or `10`, it copies an additional string from request index `0x0c`
+
+Interpretation:
+
+- `RSNetConnectionStartEx` likely supports P2P / relay / localhost-tunneled connection modes
+- the simple downloader should avoid the `Ex` path initially unless the target system requires P2P rather than direct device access
+
+### Login worker behavior
+
+The session worker at `0x100074b0`:
+
+- attempts login / session setup
+- posts state changes through callback or message mechanisms
+- sends heartbeats every ~5 seconds after login
+- dispatches status notifications with integer codes
+
+Observed status/event codes from the worker:
+
+- `2` on one failure path
+- `3` on disconnect/connection-lost path
+- `0x65`
+- `0x79`
+- `0x7a`
+
+Strings in the DLL map login-related status messages to names such as:
+
+- `RSNetMsgLoginRequest`
+- `RSNetMsgLoginSuccess`
+- `RSNetMsgLoginUserLogined`
+- `RSNetMsgLoginNoUserName`
+- `RSNetMsgLoginPasswordError`
+- `RSNetMsgLoginNoRight`
+- `RSNetMsgOverMaxUser`
+- `RSNetMsgLoginUserDisable`
+- `RSNetMsgLoginFail`
+
+Practical implication:
+
+- a script should assume login is asynchronous
+- a callback or polling/wait mechanism is required before issuing record queries
+
+## Record Query
+
+### Public entry points
+
+- `RSNetQueryRecord`
+- `RSNetQueryRecordEx`
+
+The simpler function is:
+
+```c
+RSNetQueryRecord(session, query_req)
+```
+
+which routes into `FUN_10005ca0`.
+
+The extended function is:
+
+```c
+RSNetQueryRecordEx(session, query_req)
+```
+
+which routes into `FUN_10005eb0`.
+
+### Behavior
+
+Both paths:
+
+- build a request message with message subtype `0x6f`
+- send it through the session object
+- wait up to 20 seconds for a response
+- decode the returned payload into caller-facing record structures through callback helpers
+
+The record-query dispatcher distinguishes at least two output styles using the first field of the query struct:
+
+- `0x65`
+- `0x66`
+
+Those two cases use different callbacks:
+
+- `FUN_100046b0`
+- `FUN_10004750`
+
+`FUN_100046b0` iterates a result array in units of `0x1c` bytes.
+
+`FUN_10004750` iterates a result array in units of `0x0c` bytes.
+
+Interpretation:
+
+- one query result format is likely the full record segment list (`0x1c` bytes each)
+- the other is likely a compact per-day/per-file summary format (`0x0c` bytes each)
+
+For a downloader, the `0x1c` record entries are the important one because the EXE uses `0x1c`-byte records when building playback and download requests.
+
+### Likely record entry format
+
+The download worker consumes entries of size `0x1c` and treats them like timestamped record descriptors.
+
+From earlier EXE analysis and DLL behavior, each `0x1c` entry likely contains:
+
+- start date/time fields
+- end date/time fields
+- channel or stream selection data
+- a record type flag or subtype
+
+This matches how the EXE performed time-range intersection over returned recording segments.
+
+## Download
+
+### Public entry points
+
+- `RSNetStartDownloadRecord`
+- `RSNetStartDownloadRecordEx`
+
+These are the core APIs for a standalone downloader.
+
+### `RSNetStartDownloadRecord`
+
+This is the simpler form.
+
+The EXE-side wrapper showed this request shape:
+
+```c
+struct RSNET_DOWNLOAD_REQ_BASE {
+ uint32_t field00;
+ uint32_t field04;
+ uint32_t field08;
+ uint32_t callback_or_ctx;
+ uint32_t callback_user;
+};
+```
+
+Inside `RSNet.dll`, `FUN_10010330` adapts this into the extended worker request by supplying defaults:
+
+- count or mode fields are normalized
+- an extra byte field is forced to zero
+- a `local_14 = 1` flag is inserted before forwarding to the common worker builder
+
+Interpretation:
+
+- the base function is just a convenience wrapper around the extended worker
+- for a script, `RSNetStartDownloadRecord` is probably easier to prototype first
+
+### `RSNetStartDownloadRecordEx`
+
+This is the common implementation target.
+
+The decompiled request consumer `FUN_100103b0` proves the worker expects at least:
+
+- two required non-zero fields at offsets `+0x04` and `+0x08`
+- a count field at `+0x0c`
+- an extra mode byte at `+0x10`
+- a sequence of seven 32-bit values copied from offsets `+0x00 .. +0x18` into the worker object
+
+Then it:
+
+- allocates `count * 0x1c` bytes for one array
+- allocates `count * 0x108` bytes for a second array
+- copies source arrays from worker request indices `0x19` and `0x1a`
+- queues a background worker thread beginning at `0x100105b0`
+
+Interpretation:
+
+- the extended request supports batch download of one or more `0x1c` record descriptors
+- each record also has an associated `0x108` byte metadata block, likely including the destination path or output-file information
+
+This is a strong indication that a standalone downloader can submit one selected record entry at a time rather than needing a large complex batch.
+
+## Download Worker Behavior
+
+The worker at `0x100105b0` is the core of remote file download.
+
+Key verified behaviors:
+
+- waits on session/login readiness
+- opens a socket and configures timeouts
+- sends a request with message subtype:
+ - `0x83` in one mode
+ - `0x196` in another mode
+- copies one or more `0x1c` record entries into the outgoing message
+- optionally handles an additional payload block depending on mode
+- receives a response header and payload
+- processes download status and progress messages
+- writes or routes the resulting data into an internal record-file path
+
+Important status/message strings in the DLL:
+
+- `RSNetMsgDownloadRecordClosed`
+- `RSNetMsgDownloadRecordStoreFail`
+- `RSNetMsgDownloadRecordNoFile`
+- `RSNetMsgDownloadRecordOK`
+- `RSNetMsgDownloadRecordPercent`
+- `RSNetMsgDownloadRecordFail`
+
+Observed message codes in the worker:
+
+- `0x132`
+- `0x133`
+- `0x142`
+- `0x145`
+
+Interpretation:
+
+- the worker sends progress and completion back to the caller via callback/message dispatch
+- success/failure is not just the return value of `RSNetStartDownloadRecord*`; the returned handle only means the worker was started
+
+## Does The DLL Save The File Itself?
+
+Yes, that appears to be the case.
+
+Evidence:
+
+- the download worker is large and manages long-running transfer state
+- the DLL contains internal record-file classes:
+ - `.?AVCBaseRecordFile@@`
+ - `.?AVCRSAVIRecordFile@@`
+ - `.?AVCRSMP4RecordFile@@`
+ - `.?AVCRSPrivateRecordFile@@`
+- the DLL also contains AVI-writing error strings
+- cleanup paths include an object with a `FILE *` at offset `0x54` and a file/path buffer with small-string optimization behavior
+
+Practical implication:
+
+- the script most likely does not need to manually receive frame payloads and write them itself
+- instead, it should provide the request/path metadata that the DLL expects, start the download, and wait for the worker to finish writing the file
+
+## Callback And Handle Lifetime
+
+### Session handle
+
+The session handle returned by `RSNetConnectionStart*`:
+
+- owns socket state
+- runs a worker thread
+- dispatches status via callback or window message
+
+Important internal callback-related session fields:
+
+- callback window/message at offsets around `+0x68` and `+0x6c`
+- callback function at `+0x78`
+- callback user/context at `+0x70`
+
+### Download handle
+
+The download handle returned by `RSNetStartDownloadRecord*` is a heap object with:
+
+- session pointer at `+0x20`
+- event handle at `+0x2c`
+- worker state, copied record entries, and additional metadata
+
+There is a tiny internal stop/cleanup wrapper around `0x1001ca81` that:
+
+- validates the handle
+- calls a stop routine
+- destroys the download object via `FUN_1001cab0`
+
+So a full script should ideally also locate or call the exported stop-download function when cleanup is needed. The original EXE import table proves that such an export exists, even though the current symbol recovery in this DLL session did not label it cleanly.
+
+## Practical Downloader Strategy
+
+The simplest viable approach is not to reverse the raw network protocol. It is to call the vendor DLL directly.
+
+## C# Interop Feasibility
+
+Yes. C# can load `RSNet.dll` as an unmanaged native library and call its exports directly.
+
+The DLL is a good fit for `P/Invoke` or `NativeLibrary.GetExport`-based interop because the key exports are ordinary x86 functions, not COM interfaces or C++ object methods.
+
+### ABI details confirmed from `RSNet.dll`
+
+- architecture: `x86/little/32`
+- pointer size: `4`
+- compiler family: Visual Studio
+- export names are undecorated in the import table used by the EXE
+- the relevant exports use callee stack cleanup, which matches `StdCall`
+
+Observed export epilogues:
+
+- `RSNetInit` -> `RET`
+- `RSNetRelease` -> `RET`
+- `RSNetConnectionStart` -> `RET 0x4`
+- `RSNetConnectionStartEx` -> `RET 0x4`
+- `RSNetQueryRecord` -> `RET 0x8`
+- `RSNetQueryRecordEx` -> `RET 0x8`
+- `RSNetStartDownloadRecord` -> `RET 0x8`
+- `RSNetStartDownloadRecordEx` -> `RET 0x8`
+- `RSNetReqRecordData` -> `RET 0x4`
+
+Practical implication:
+
+- the first C# prototype should treat the exported functions as `CallingConvention.StdCall`
+- the process must be 32-bit
+
+### Runtime constraints for a C# host process
+
+The managed host should be built and run as:
+
+- `x86`, not `AnyCPU`
+- Windows only
+- from the vendor application directory, or with that directory added to the DLL search path
+
+The safest deployment model is:
+
+- place the C# test app in the same directory as `RSNet.dll`
+- or launch it with the current directory set to `i:/Apps/Surveillance_client`
+- or call `SetDllDirectory` / `AddDllDirectory` before loading `RSNet.dll`
+
+Why that matters:
+
+- `RSNet.dll` depends on the VC++ runtime and the rest of the vendor installation layout
+- it also expects `RSNet.ini` nearby for logging/config initialization
+- strings inside the DLL reference `RSP2PClient.exe` and `RSP2PDaemon.exe`, so P2P mode likely depends on those companion files being present
+
+For direct-IP downloading, the minimum runtime context is likely smaller than the full client install, but the easiest first run is still to execute from the installed client folder.
+
+## Recommended C# Loading Model
+
+There are two realistic options.
+
+### Option 1: Static `DllImport`
+
+Use this once the calling convention and signatures are stable enough.
+
+Advantages:
+
+- simplest code
+- easiest to read
+- easiest to debug once structs are correct
+
+Disadvantages:
+
+- more brittle while struct layouts are still being discovered
+
+### Option 2: `NativeLibrary.Load` plus delegates
+
+Use this during the reverse-engineering phase.
+
+Advantages:
+
+- lets you experiment with signatures more safely
+- avoids hard failure at process load if a dependency is missing
+- makes it easy to log export resolution one function at a time
+
+Disadvantages:
+
+- more boilerplate
+
+For the current stage, Option 2 is the better choice.
+
+## C# Interop Types To Use
+
+### Handles
+
+Treat these as opaque `IntPtr` values:
+
+- session handle returned by `RSNetConnectionStart*`
+- download handle returned by `RSNetStartDownloadRecord*`
+
+Do not model them as managed classes or attempt to dereference them directly in C#.
+
+### Strings
+
+Current evidence suggests the DLL uses ANSI `char *` strings, not UTF-16.
+
+In C#:
+
+- use `CharSet.Ansi`
+- marshal strings as `LPStr`
+- for uncertain layouts, prefer `IntPtr` plus `Marshal.StringToHGlobalAnsi`
+
+### Struct packing
+
+Use:
+
+- `StructLayout(LayoutKind.Sequential, Pack = 4)`
+
+Reason:
+
+- this is a 32-bit Visual Studio binary using 4-byte pointers and ordinary 32-bit alignment
+
+### Callbacks
+
+Use delegates marked with:
+
+```csharp
+[UnmanagedFunctionPointer(CallingConvention.StdCall)]
+```
+
+And keep the delegate alive for the entire native operation lifetime.
+
+Practical rule:
+
+- store callback delegates in fields, not local variables
+
+Otherwise the GC may collect them while the native worker thread still holds the callback pointer.
+
+## First-Pass C# API Shape
+
+Based on the confirmed export behavior, a first interop layer can be modeled like this:
+
+```csharp
+internal static class RsNetNative
+{
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
+ internal static extern void RSNetInit();
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern void RSNetRelease();
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern void RSNetSetEncription(uint value);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern IntPtr RSNetConnectionStart(ref RsNetConnectRequest request);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern IntPtr RSNetConnectionStartEx(IntPtr request);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern int RSNetQueryRecord(IntPtr session, IntPtr queryRequest);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern int RSNetQueryRecordEx(IntPtr session, IntPtr queryRequest);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern IntPtr RSNetStartDownloadRecord(IntPtr session, IntPtr downloadRequest);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern IntPtr RSNetStartDownloadRecordEx(IntPtr session, IntPtr downloadRequest);
+
+ [DllImport("RSNet.dll", CallingConvention = CallingConvention.StdCall)]
+ internal static extern void RSNetReqRecordData(IntPtr recordPlayHandle);
+}
+```
+
+Notes:
+
+- `RSNetConnectionStart` is the first function worth strongly typing
+- `RSNetConnectionStartEx`, `RSNetQueryRecord*`, and `RSNetStartDownloadRecord*` should initially stay as `IntPtr` request pointers until the layouts are fully proven
+- this reduces the chance of silent marshaling mistakes during the first prototype
+
+## First Strongly-Typed C# Struct Worth Trying
+
+The current best candidate for a partial typed struct is the direct-IP login request used by `RSNetConnectionStart`.
+
+Best current guess:
+
+```csharp
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RsNetConnectRequest
+{
+ public IntPtr AddressAnsi;
+ public ushort Port;
+ public ushort Padding0;
+ public IntPtr UsernameAnsi;
+ public IntPtr PasswordAnsi;
+ public uint Field10;
+ public uint Field14;
+ public uint Field18;
+ public uint Field1C;
+ public uint Field20;
+ public uint Field24;
+ public uint Field28;
+}
+```
+
+Why this is reasonable:
+
+- the decompiled function copies a pointer at offset `0x00`
+- a 16-bit port value is read from offset `0x04`
+- string pointers are copied from offsets `0x08` and `0x0c`
+- the remaining fields are treated as 32-bit values or callback-related fields
+
+This struct should still be treated as provisional.
+
+## Likely C# Callback Shape
+
+The session object stores either:
+
+- a callback window handle and message ID
+- or a callback function pointer plus a user/context value
+
+The worker invokes the function callback in patterns like:
+
+- `callback(code, userContext)`
+- or posts a window message with `PostMessageA(hwnd, msgId, code, userContext)`
+
+Best current guess for the login/status callback delegate:
+
+```csharp
+[UnmanagedFunctionPointer(CallingConvention.StdCall)]
+internal delegate void RsNetStatusCallback(uint code, IntPtr userContext);
+```
+
+This is not fully proven, but it matches the currently observed call sites better than any multi-parameter alternative.
+
+## Why C# Is A Better Fit Than Raw Packet Reimplementation
+
+For the current objective, C# plus unmanaged interop is likely better than reimplementing the network protocol because:
+
+- the vendor DLL already knows the login handshake
+- the vendor DLL already knows P2P versus direct-IP behavior
+- the vendor DLL already manages record-query formatting
+- the vendor DLL already performs the actual download worker loop
+- the vendor DLL appears to write the file itself
+
+That cuts the problem down to:
+
+- loading the DLL safely
+- building the request structs correctly
+- handling callbacks/events
+- waiting for the output file to appear and complete
+
+## Recommended C# Downloader Architecture
+
+### Layer 1: Native bootstrap
+
+Responsibilities:
+
+- locate the vendor install directory
+- set DLL search path
+- call `RSNetInit`
+- optionally call `RSNetSetEncription`
+
+### Layer 2: Session wrapper
+
+Responsibilities:
+
+- allocate unmanaged ANSI strings for device address, username, password
+- build the login request
+- call `RSNetConnectionStart`
+- keep the callback delegate alive
+- expose a `Task` or `ManualResetEventSlim` for login completion
+
+### Layer 3: Record query wrapper
+
+Responsibilities:
+
+- build an unmanaged query struct
+- invoke `RSNetQueryRecord`
+- collect returned `0x1c` entries into managed record models
+- expose them to higher-level range selection code
+
+### Layer 4: Download wrapper
+
+Responsibilities:
+
+- choose the target record entry or entries
+- build the native download request and per-item metadata
+- call `RSNetStartDownloadRecord`
+- monitor status/progress callbacks
+- resolve the final output file path
+
+### Layer 5: Cleanup
+
+Responsibilities:
+
+- call the native stop/cleanup entrypoints when known
+- free all unmanaged strings and unmanaged buffers
+- call `RSNetRelease` on process shutdown
+
+### What a minimal C# prototype should look like
+
+The first prototype should not attempt the whole workflow at once.
+
+Do it in stages:
+
+1. Load `RSNet.dll` successfully from C# in an `x86` process.
+2. Call `RSNetInit()` and verify it does not crash.
+3. Call `RSNetConnectionStart(...)` with a direct-IP request and log callback codes.
+4. Confirm which callback code means login success.
+5. Add `RSNetQueryRecord(...)` and dump raw `0x1c` entries.
+6. Only then attempt `RSNetStartDownloadRecord(...)`.
+
+That staged approach is important because the unstable part is the native struct layout, not the export resolution.
+
+## Risks Specific To C# Interop
+
+The main failure modes are:
+
+- running as `AnyCPU` or `x64` and failing to load the 32-bit DLL
+- incorrect calling convention
+- incorrect struct packing
+- wrong ANSI versus Unicode marshaling
+- GC moving or collecting delegates or unmanaged buffers too early
+- freeing unmanaged strings before the async worker is finished using them
+- assuming login is synchronous when it is not
+
+Operational rule:
+
+- keep every unmanaged buffer and every callback delegate alive until the native session or download handle has definitively finished
+
+## Implementation Checklist For A C# Version
+
+Before coding:
+
+- confirm the C# host runs as `x86`
+- run from the vendor install folder or configure DLL search paths explicitly
+- keep a copy of the vendor folder structure intact for first tests
+
+During interop setup:
+
+- use `StdCall`
+- use `CharSet.Ansi`
+- use `Pack = 4`
+- represent unknown native structs as `IntPtr` first
+- keep callback delegates pinned by strong references
+
+During login:
+
+- prefer `RSNetConnectionStart` over `RSNetConnectionStartEx`
+- use direct IP and media port first
+- do not assume immediate success after the function returns a non-null session handle
+
+During record query:
+
+- expect asynchronous or callback-driven result delivery
+- log raw bytes for each `0x1c` record entry before trying to reinterpret every field semantically
+
+During download:
+
+- start with one record entry, not a batch
+- expect completion via status event rather than via the return value alone
+- inspect the output directory for the file the DLL creates
+
+## What Still Needs To Be Recovered For A Clean C# Implementation
+
+The unresolved items are the same as for Python FFI, but C# can tolerate them if opaque pointers are used first.
+
+The most important remaining tasks are:
+
+1. exact `RSNetConnectionStart` request struct validation
+2. exact query-request struct layout
+3. exact query callback prototype
+4. exact download-request struct layout
+5. exact per-item `0x108` metadata layout
+6. exact stop-download export signature
+7. exact mapping from numeric event codes to login/query/download states
+
+## Revised Assessment
+
+Yes, a C# implementation is realistic.
+
+It is probably one of the better options if the end goal is a Windows-only utility that uses the vendor binary as-is.
+
+The current blocker is not `can C# do it?` but `how much of the request structs do we want to strongly type before writing the prototype?`
+
+At this point, a cautious C# prototype is feasible if it:
+
+- uses `x86`
+- calls the DLL with `StdCall`
+- uses direct IP login first
+- treats most request blobs as unmanaged buffers until the layouts are fully validated
+
+### Recommended first prototype flow
+
+1. Load `RSNet.dll` with `ctypes.WinDLL`
+2. Call `RSNetInit()`
+3. Optionally call `RSNetSetEncription(...)` if required by the target device family
+4. Build a direct-IP login struct for `RSNetConnectionStart`
+5. Wait until the async login callback reports success
+6. Build a record-query request for `RSNetQueryRecord`
+7. Collect returned `0x1c` record entries
+8. Pick the entries intersecting the user-requested time range
+9. Build a single-entry download request and call `RSNetStartDownloadRecord`
+10. Wait for `RSNetMsgDownloadRecordOK` or a terminal failure event
+11. Locate the output file written by the DLL
+12. Release/cleanup handles
+
+### Why direct-IP first
+
+The `Ex` login path clearly has extra logic for P2P or relay modes.
+
+To keep the prototype small:
+
+- use direct device IP / media port first
+- defer P2P-only targets until after the basic downloader works
+
+## Best Current Guess At Minimal Script Design
+
+### Language
+
+Python with `ctypes` is the fastest path.
+
+C# is now a credible alternative for a Windows-only implementation that loads the vendor DLL directly.
+
+### Components
+
+1. DLL loader and function prototypes
+2. callback function declared with `ctypes.WINFUNCTYPE`
+3. login request struct for `RSNetConnectionStart`
+4. query request struct for `RSNetQueryRecord`
+5. query result callback handling `0x1c` entries
+6. download request struct for `RSNetStartDownloadRecord`
+7. event loop or polling wait for download completion
+
+### Important caveat
+
+The exact `ctypes.Structure` layouts are not fully proven yet.
+
+So the first implementation should be treated as an iterative prototype, not a guaranteed drop-in script.
+
+Still, the reverse engineering so far is enough to support the following plan:
+
+- prototype direct login with `RSNetConnectionStart`
+- verify callback codes for login success and failure
+- prototype `RSNetQueryRecord` with a callback that logs each `0x1c` result entry
+- once one real query works, use a single returned record entry as input to `RSNetStartDownloadRecord`
+
+That is now a tractable task.
+
+## Gaps That Still Matter Before Writing The Actual Script
+
+These are the highest-value unresolved items:
+
+1. Exact layout of the `RSNetConnectionStart` input struct
+2. Exact layout of the `RSNetQueryRecord` request struct and callback prototype
+3. Exact layout of the `RSNetStartDownloadRecord` request struct, especially the destination-file metadata
+4. Exact numeric callback codes corresponding to:
+ - login success
+ - login failure types
+ - query complete
+ - download progress
+ - download success
+ - download failure
+5. Exact output file naming and format-selection rules
+
+## Recommended Next Reverse-Engineering Steps
+
+The next pass should focus on making the prototype script concrete:
+
+1. Rename key `RSNet.dll` internals:
+ - `FUN_100050a0`
+ - `FUN_100051f0`
+ - `FUN_10005ca0`
+ - `FUN_10005eb0`
+ - `FUN_10010330`
+ - `FUN_100103b0`
+ - `FUN_100105b0`
+ - `FUN_100074b0`
+
+2. Recover the exact login callback prototype from the session object fields around `+0x68`, `+0x6c`, `+0x70`, and `+0x78`
+
+3. Recover the exact query callback prototype used by result handlers `FUN_100046b0` and `FUN_10004750`
+
+4. Identify the structure at size `0x108` associated with each download item
+
+5. Identify where the output file path is copied into the download-item metadata
+
+6. Validate the call sequence against a real device using credentials
+
+## Current Assessment
+
+The original objective was to analyze until it became possible to figure out how to make a simple script that downloads a video file.
+
+That threshold has been reached in architectural terms:
+
+- use `RSNet.dll` directly
+- direct-IP login first
+- query recordings first
+- submit one record entry into the download API
+- let the DLL write the file locally
+- monitor callback/status events for completion
+
+The remaining work is no longer “what is the path?” but “what are the exact struct layouts?”
+
+That is a much narrower problem and should be solvable in a follow-up pass, especially with live credentials or one round of prototype-and-adjust testing.
\ No newline at end of file