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
1 change: 1 addition & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
publish:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push'

steps:
- name: Download NuGet packages
Expand Down
32 changes: 27 additions & 5 deletions src/TianWen.Lib.CLI/ConsoleHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,22 +122,44 @@ public async ValueTask RenderImageAsync(IMagickImage<float> image)
Console.Write(renderer.Render(image, widthScale));
}

public async Task<IReadOnlyCollection<Profile>> ListProfilesAsync(CancellationToken cancellationToken)
public async Task<IReadOnlyCollection<DeviceBase>> ListAllDevicesAsync(bool forceDiscovery, CancellationToken cancellationToken)
{
TimeSpan discoveryTimeout;
#if DEBUG
discoveryTimeout = TimeSpan.FromHours(1);
discoveryTimeout = TimeSpan.FromMinutes(15);
#else
discoveryTimeout = TimeSpan.FromSeconds(25);
#endif
using var cts = new CancellationTokenSource(discoveryTimeout, External.TimeProvider);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken);

if (await deviceManager.CheckSupportAsync(linked.Token))
await deviceManager.CheckSupportAsync(linked.Token);

if (forceDiscovery)
{
await deviceManager.DiscoverAsync(linked.Token);
}

return [.. deviceManager.RegisteredDeviceTypes.SelectMany(deviceManager.RegisteredDevices)];
}

public async Task<IReadOnlyCollection<TDevice>> ListDevicesAsync<TDevice>(DeviceType deviceType, bool forceDiscovery, CancellationToken cancellationToken)
where TDevice : DeviceBase
{
TimeSpan discoveryTimeout;
#if DEBUG
discoveryTimeout = TimeSpan.FromMinutes(15);
#else
discoveryTimeout = TimeSpan.FromSeconds(25);
#endif
using var cts = new CancellationTokenSource(discoveryTimeout, External.TimeProvider);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken);

if (await deviceManager.CheckSupportAsync(linked.Token) && forceDiscovery)
{
await deviceManager.DiscoverOnlyDeviceType(DeviceType.Profile, linked.Token);
await deviceManager.DiscoverOnlyDeviceType(deviceType, linked.Token);
}

return [..deviceManager.RegisteredDevices(DeviceType.Profile).OfType<Profile>()];
return [.. deviceManager.RegisteredDevices(deviceType).OfType<TDevice>()];
}
}
54 changes: 54 additions & 0 deletions src/TianWen.Lib.CLI/DeviceSubCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.CommandLine;
using TianWen.Lib.Devices;

namespace TianWen.Lib.CLI;

internal class DeviceSubCommand(IConsoleHost consoleHost)
{
private bool _discoveryRan;

public Command Build()
{
var discoverCommand = new Command("discover", "Discover all connected devices");
discoverCommand.SetAction(DiscoverDevicesAsync);

var listCommand = new Command("list", "List all connected devices");
listCommand.SetAction(ListDevicesAsync);

return new Command("device", "Manage connected devices")
{
Subcommands = {
discoverCommand,
listCommand
}
};
}

internal async Task DiscoverDevicesAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
foreach (var device in await DoListDevicesExceptProfiles(true, cancellationToken))
{
Console.WriteLine();
Console.WriteLine(device.ToString());
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant call to 'ToString' on a String object.

Copilot uses AI. Check for mistakes.
}
}

internal async Task ListDevicesAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
foreach (var device in await DoListDevicesExceptProfiles(false, cancellationToken))
{
Console.WriteLine();
Console.WriteLine(device.ToString());
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant call to 'ToString' on a String object.

Copilot uses AI. Check for mistakes.
}
}

private async Task<IEnumerable<DeviceBase>> DoListDevicesExceptProfiles(bool forceDiscovery, CancellationToken cancellationToken)
{
var result = (await consoleHost.ListAllDevicesAsync(forceDiscovery || !_discoveryRan, cancellationToken))
.Where(d => d.DeviceType is not DeviceType.Profile);

_discoveryRan = true;

return result;
}
}
8 changes: 5 additions & 3 deletions src/TianWen.Lib.CLI/IConsoleHost.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using ImageMagick;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TianWen.Lib.Devices;

namespace TianWen.Lib.CLI;
Expand All @@ -9,10 +8,13 @@ public interface IConsoleHost
{
Task<bool> HasSixelSupportAsync();

Task<IReadOnlyCollection<Profile>> ListProfilesAsync(CancellationToken cancellationToken);
Task<IReadOnlyCollection<TDevice>> ListDevicesAsync<TDevice>(DeviceType deviceType, bool forceDiscovery, CancellationToken cancellationToken)
where TDevice : DeviceBase;

Task<IReadOnlyCollection<DeviceBase>> ListAllDevicesAsync(bool forceDiscovery, CancellationToken cancellationToken);

IDeviceUriRegistry DeviceUriRegistry { get; }

IHostApplicationLifetime ApplicationLifetime { get; }

IExternal External { get; }
Expand Down
9 changes: 6 additions & 3 deletions src/TianWen.Lib.CLI/ProfileSubCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal async Task CreateProfileAsync(ParseResult parseResult, CancellationToke

internal async Task ListProfilesAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var allProfiles = await consoleHost.ListProfilesAsync(cancellationToken);
var allProfiles = await ListProfilesAsync(cancellationToken);

var selectedProfile = parseResult.GetSelected(allProfiles, selectedProfileOption);

Expand All @@ -69,7 +69,7 @@ internal async Task DeleteProfileAsync(ParseResult parseResult, CancellationToke
{
var profileNameOrId = parseResult.GetRequiredValue(profileNameOrIdArg);

var profiles = await consoleHost.ListProfilesAsync(cancellationToken);
var profiles = await ListProfilesAsync(cancellationToken);
Profile profileToDelete;
if (Guid.TryParse(profileNameOrId, out var profileId))
{
Expand Down Expand Up @@ -111,6 +111,9 @@ internal async Task DeleteProfileAsync(ParseResult parseResult, CancellationToke
Console.WriteLine($"Deleted profile '{profileToDelete.DisplayName}' ({profileToDelete.ProfileId})");

// refresh cache
var profilesAfterDelete = await consoleHost.ListProfilesAsync(cancellationToken);
var profilesAfterDelete = await ListProfilesAsync(cancellationToken);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to profilesAfterDelete is useless, since its value is never read.

Suggested change
var profilesAfterDelete = await ListProfilesAsync(cancellationToken);
await ListProfilesAsync(cancellationToken);

Copilot uses AI. Check for mistakes.
}

private Task<IReadOnlyCollection<Profile>> ListProfilesAsync(CancellationToken cancellationToken) =>
consoleHost.ListDevicesAsync<Profile>(DeviceType.Profile, true, cancellationToken);
}
135 changes: 4 additions & 131 deletions src/TianWen.Lib.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Pastel.ConsoleExtensions.Enable();
ConsoleExtensions.Enable();

var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { Args = args, DisableDefaults = true });
builder.Services
Expand Down Expand Up @@ -57,7 +57,8 @@
Options = { selectedProfileOption },
Subcommands =
{
new ProfileSubCommand(consoleHost, selectedProfileOption).Build()
new ProfileSubCommand(consoleHost, selectedProfileOption).Build(),
new DeviceSubCommand(consoleHost).Build()
}
};

Expand All @@ -76,132 +77,4 @@

await host.StopAsync();

await host.WaitForShutdownAsync();
/*
var argIdx = 0;
var mountDeviceId = args.Length > argIdx ? args[argIdx++] : "ASCOM.DeviceHub.Telescope";
var cameraDeviceId = args.Length > argIdx ? args[argIdx++] : "ASCOM.Simulator.Camera";
var coverDeviceId = args.Length > argIdx ? args[argIdx++] : "ASCOM.Simulator.CoverCalibrator";
var focuserDeviceId = args.Length > argIdx ? args[argIdx++] : "ASCOM.Simulator.Focuser";
var expDuration = TimeSpan.FromSeconds(args.Length > 2 ? int.Parse(args[argIdx++]) : 10);
var outputFolder = Directory.CreateDirectory(args.Length > 3 ? args[argIdx++] : Path.Combine(Directory.GetCurrentDirectory(), "TianWen.Lib.TestCli", "Light"));
IExternal external = new ConsoleOutput(outputFolder);


var observations = new List<Observation>
{
// new Observation(new Target(19.5, -20, "Mercury", CatalogIndex.Mercury), DateTimeOffset.Now, TimeSpan.FromMinutes(100), false, TimeSpan.FromSeconds(20)),
new Observation(new Target(5.5877777777777773, -5.389444444444444, "Orion Nebula", CatalogUtils.TryGetCleanedUpCatalogName("M42", out var catIdx) ? catIdx : null), DateTimeOffset.Now, TimeSpan.FromMinutes(100), false, TimeSpan.FromSeconds(20))
};

var guiderDevice = new GuiderDevice(DeviceType.PHD2, "localhost/1", "");

using var profile = new AscomProfile();
var allCameras = profile.RegisteredDevices(DeviceType.Camera);
var cameraDevice = allCameras.FirstOrDefault(e => e.DeviceId == cameraDeviceId);

var allMounts = profile.RegisteredDevices(DeviceType.Telescope);
var mountDevice = allMounts.FirstOrDefault(e => e.DeviceId == mountDeviceId);

var allCovers = profile.RegisteredDevices(DeviceType.CoverCalibrator);
var coverDevice = allCovers.FirstOrDefault(e => e.DeviceId == coverDeviceId);

var allFocusers = profile.RegisteredDevices(DeviceType.Focuser);
var focuserDevice = allFocusers.FirstOrDefault(e => e.DeviceId == focuserDeviceId);

Mount mount;
if (mountDevice is not null)
{
mount = new Mount(mountDevice, external);
external.LogInfo($"Found mount {mountDevice.DisplayName}, using {mount.Driver.DriverInfo ?? mount.Driver.GetType().Name}");
}
else
{
external.LogError($"Could not connect to mount {mountDevice?.ToString()}");
return 1;
}

Guider guider;
if (guiderDevice is not null)
{
guider = new Guider(guiderDevice, external);

if (guider.Driver.TryGetActiveProfileName(out var activeProfileName))
{
external.LogInfo($"Connected to {guider.Device.DeviceType} guider profile {activeProfileName}");
}
else
{
external.LogInfo($"Connected to {guider.Device.DeviceType} guider at {guider.Device.DeviceId}");
}
}
else
{
external.LogError("No guider was specified, aborting.");
return 1;
}

Camera camera;
if (cameraDevice is not null)
{
camera = new Camera(cameraDevice, external);
}
else
{
external.LogError("Could not connect to camera");
return 1;
}

Cover? cover;
if (coverDevice is not null)
{
cover = new Cover(coverDevice, external);
}
else
{
cover = null;
}

Focuser? focuser;
if (focuserDevice is not null)
{
focuser = new Focuser(focuserDevice, external);
}
else
{
focuser = null;
}

using var setup = new Setup(
mount,
guider,
new GuiderFocuser(),
new Telescope("Sim Scope", 250, camera, cover, focuser, new FocusDirection(false, true), null, null)
);

var sessionConfiguration = new SessionConfiguration(
SetpointCCDTemperature: new SetpointTemp(10, SetpointTempKind.Normal),
CooldownRampInterval: TimeSpan.FromSeconds(20),
WarmupRampInterval: TimeSpan.FromSeconds(30),
MinHeightAboveHorizon: 15,
DitherPixel: 30d,
SettlePixel: 0.3d,
DitherEveryNthFrame: 3,
SettleTime: TimeSpan.FromSeconds(30),
GuidingTries: 3
);

// TODO: implement DI
var analyser = new ImageAnalyser();
var plateSolver = new CombinedPlateSolver(new AstapPlateSolver(), new AstrometryNetPlateSolverMultiPlatform(), new AstrometryNetPlateSolverUnix());
if (!await plateSolver.CheckSupportAsync(cts.Token))
{
external.LogError("No proper plate solver configured, aborting!");
}

var session = new Session(setup, sessionConfiguration, analyser, plateSolver, external, observations);

session.Run(cts.Token);

return 0;
*/
await host.WaitForShutdownAsync();
2 changes: 1 addition & 1 deletion src/TianWen.Lib.CLI/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"Local": {
"commandName": "Project",
"commandLineArgs": "profile list"
"commandLineArgs": "device list"
}
}
}
Loading
Loading