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
220 changes: 220 additions & 0 deletions NINA.Equipment/Equipment/MySwitch/IndiSwitchHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#region "copyright"

/*
Copyright © 2025-2026 Nico Trost <nico.trost57@gmail.com> and the PI.N.S. contributors

This file is part of PI 'N' Stars.

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

#endregion "copyright"

using NINA.Core.Locale;
using NINA.Core.Utility;
using NINA.Equipment.Equipment;
using NINA.Equipment.Interfaces;
using NINA.INDI;
using NINA.INDI.Devices;
using NINA.INDI.Interfaces;
using NINA.Profile.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace NINA.Equipment.Equipment.MySwitch {

/// <summary>
/// Equipment-layer wrapper for an INDI AUX device (e.g. Pegasus UPB) that
/// exposes power ports, USB ports, dew-heater duty cycles, and other
/// writable channels as a standard NINA <see cref="ISwitchHub"/>.
///
/// Switch collection is built once after connection by inspecting all writable
/// INDI property elements that the driver has advertised.
/// </summary>
public class IndiSwitchHub : IndiDevice<IINDISwitchHub>, ISwitchHub, IDisposable {

public IndiSwitchHub(INDIDeviceInfo info, IProfileService profileService = null) : base(info) {
this.profileService = profileService;
switches = new AsyncObservableCollection<ISwitch>();
}

private readonly IProfileService profileService;

private ICollection<ISwitch> switches;
public ICollection<ISwitch> Switches {
get => switches;
private set {
switches = value;
RaisePropertyChanged();
}
}

protected override string ConnectionLostMessage => Loc.Instance["LblSwitchConnectionLost"];

protected override IINDISwitchHub GetInstance() {
return device ??= new INDISwitchHub(_device);
}

protected override Task PreConnect() {
if (profileService != null) {
var s = profileService.ActiveProfile.SwitchSettings;
GetInstance().ConfigureConnectionProperties(
s.IndiConnectionMode,
s.IndiAutoSearch,
s.IndiAddress,
s.IndiPort,
s.IndiBaudRate
);
}
return Task.CompletedTask;
}

protected override async Task PostConnect() {
// Give the INDI driver a moment to push all initial property definitions.
await Task.Delay(TimeSpan.FromSeconds(1));
BuildSwitchCollection();
GetInstance().ValuesUpdated += OnIndiValuesUpdated;
}

protected override void PostDisconnect() {
GetInstance().ValuesUpdated -= OnIndiValuesUpdated;
Switches = new AsyncObservableCollection<ISwitch>();
}

private void OnIndiValuesUpdated(string propertyName) {
foreach (var sw in switches) {
if (sw is IndiReadSwitchItem r && r.PropertyName == propertyName) {
r.Poll();
} else if (sw is IndiWritableSwitchItem w && w.PropertyName == propertyName) {
// SyncFromDevice syncs TargetValue from the live device state so the
// Vue toggle reflects external changes (e.g. from another INDI client).
w.SyncFromDevice();
}
}
}

private void BuildSwitchCollection() {
var list = new AsyncObservableCollection<ISwitch>();
short id = 0;
foreach (var desc in device.GetDescriptors()) {
list.Add(desc.IsWritable
? (ISwitch)new IndiWritableSwitchItem(id++, desc, device)
: new IndiReadSwitchItem(id++, desc, device));
}
Switches = list;
Logger.Info($"IndiSwitchHub '{Name}': discovered {list.Count} switch channel(s)");
}

#region Unsupported

public IList<string> SupportedActions { get; } = new List<string>();
public string Action(string actionName, string actionParameters) => throw new NotImplementedException();
public void CommandBlind(string command, bool raw = false) => throw new NotImplementedException();
public bool CommandBool(string command, bool raw = false) => throw new NotImplementedException();
public string CommandString(string command, bool raw = false) => throw new NotImplementedException();

#endregion
}

// -----------------------------------------------------------------------
// Read-only switch item (INDI RO property element)
// -----------------------------------------------------------------------

internal sealed class IndiReadSwitchItem : BaseINPC, ISwitch {
private readonly INDISwitchDescriptor _descriptor;
private readonly IINDISwitchHub _hub;

public IndiReadSwitchItem(short id, INDISwitchDescriptor descriptor, IINDISwitchHub hub) {
Id = id;
_descriptor = descriptor;
_hub = hub;
Name = string.IsNullOrWhiteSpace(descriptor.ElementLabel) ? descriptor.ElementName : descriptor.ElementLabel;
Description = string.IsNullOrWhiteSpace(descriptor.PropertyLabel) ? descriptor.PropertyName : descriptor.PropertyLabel;
}

public short Id { get; }
public string Name { get; }
public string Description { get; }
public string PropertyName => _descriptor.PropertyName;
public double Value => _hub.GetValue(_descriptor);

public bool Poll() {
RaisePropertyChanged(nameof(Value));
return true;
}
}

// -----------------------------------------------------------------------
// Writable switch item – boolean toggle or numeric control
// -----------------------------------------------------------------------

internal sealed class IndiWritableSwitchItem : BaseINPC, IWritableSwitch {
private readonly INDISwitchDescriptor _descriptor;
private readonly IINDISwitchHub _hub;

public IndiWritableSwitchItem(short id, INDISwitchDescriptor descriptor, IINDISwitchHub hub) {
Id = id;
_descriptor = descriptor;
_hub = hub;
Name = string.IsNullOrWhiteSpace(descriptor.ElementLabel) ? descriptor.ElementName : descriptor.ElementLabel;
Description = string.IsNullOrWhiteSpace(descriptor.PropertyLabel) ? descriptor.PropertyName : descriptor.PropertyLabel;
PropertyName = descriptor.PropertyName;
Maximum = descriptor.Max;
Minimum = descriptor.Min;
StepSize = descriptor.Step > 0 ? descriptor.Step : 1.0;
targetValue = hub.GetValue(descriptor);
}

public short Id { get; }
public string Name { get; }
public string Description { get; }
public string PropertyName { get; }
public double Maximum { get; }
public double Minimum { get; }
public double StepSize { get; }

public double Value => _hub.GetValue(_descriptor);

private double targetValue;
public double TargetValue {
get => targetValue;
set {
if (targetValue != value) {
targetValue = value;
RaisePropertyChanged();
}
}
}

public bool Poll() {
RaisePropertyChanged(nameof(Value));
return true;
}

/// <summary>
/// Called when the INDI server pushes an unsolicited state update (setSwitchVector).
/// Syncs <see cref="TargetValue"/> to the actual device state so that the UI
/// toggle reflects external changes without disrupting the SwitchVM set-value
/// polling loop (which relies on Value != TargetValue to detect work-in-progress).
/// </summary>
public void SyncFromDevice() {
var liveValue = _hub.GetValue(_descriptor);
if (Math.Abs(targetValue - liveValue) > 0.001) {
targetValue = liveValue;
RaisePropertyChanged(nameof(TargetValue));
}
RaisePropertyChanged(nameof(Value));
}

public void SetValue() {
if (_descriptor.IsBoolSwitch) {
_hub.SetBoolElement(_descriptor, TargetValue >= 0.5);
} else {
_hub.SetNumberElement(_descriptor, TargetValue);
}
}
}
}
34 changes: 22 additions & 12 deletions NINA.Equipment/Utility/INDIInteraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This Source Code Form is subject to the terms of the Mozilla Public
using NINA.Equipment.Equipment.MyFilterWheel;
using NINA.Equipment.Equipment.MyFlatDevice;
using NINA.Equipment.Equipment.MyWeatherData;
using NINA.Equipment.Equipment.MySwitch;

namespace NINA.Equipment.Utility {

Expand Down Expand Up @@ -111,11 +112,9 @@ public async Task<List<IFilterWheel>> GetFilterWheels() {
return l;
}

public async Task<List<IFlatDevice>> GetFlatDevices()
{
public async Task<List<IFlatDevice>> GetFlatDevices() {
var l = new List<IFlatDevice>();
if (!await INDIClient.Instance.WaitForServerReadyAsync(TimeSpan.FromSeconds(15)))
{
if (!await INDIClient.Instance.WaitForServerReadyAsync(TimeSpan.FromSeconds(15))) {
Logger.Debug("INDI server not ready - skipping INDI flat device enumeration");
return l;
}
Expand All @@ -124,19 +123,16 @@ public async Task<List<IFlatDevice>> GetFlatDevices()
string driver = profileService.ActiveProfile.FlatDeviceSettings.IndiDriver;

// Query devices for this driver
foreach (var device in await INDIClient.Instance.GetDevices(DeviceInterface.LIGHTBOX_INTERFACE, driver))
{
foreach (var device in await INDIClient.Instance.GetDevices(DeviceInterface.LIGHTBOX_INTERFACE, driver)) {
IndiFlatDevice flatDevice = new(device, profileService);
l.Add(flatDevice);
}
return l;
}

public async Task<List<IWeatherData>> GetWeatherData()
{
public async Task<List<IWeatherData>> GetWeatherData() {
var l = new List<IWeatherData>();
if (!await INDIClient.Instance.WaitForServerReadyAsync(TimeSpan.FromSeconds(15)))
{
if (!await INDIClient.Instance.WaitForServerReadyAsync(TimeSpan.FromSeconds(15))) {
Logger.Debug("INDI server not ready - skipping INDI weather data enumeration");
return l;
}
Expand All @@ -145,14 +141,28 @@ public async Task<List<IWeatherData>> GetWeatherData()
string driver = profileService.ActiveProfile.WeatherDataSettings.IndiDriver;

// Query devices for this driver
foreach (var device in await INDIClient.Instance.GetDevices(DeviceInterface.WEATHER_INTERFACE, driver))
{
foreach (var device in await INDIClient.Instance.GetDevices(DeviceInterface.WEATHER_INTERFACE, driver)) {
IndiWeatherData weatherData = new(device, profileService);
l.Add(weatherData);
}
return l;
}

public async Task<List<ISwitchHub>> GetSwitches() {
var l = new List<ISwitchHub>();
if (!await INDIClient.Instance.WaitForServerReadyAsync(TimeSpan.FromSeconds(15))) {
Logger.Debug("INDI server not ready - skipping INDI switch hub enumeration");
return l;
}

string driver = profileService.ActiveProfile.SwitchSettings.IndiDriver;

foreach (var device in await INDIClient.Instance.GetDevices(DeviceInterface.AUX_INTERFACE, driver)) {
l.Add(new IndiSwitchHub(device, profileService));
}
return l;
}

public static string GetVersion() {
return INDIClient.Instance.GetServerVersionString();
}
Expand Down
Loading
Loading