diff --git a/README.md b/README.md index e1e3de9..b586650 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ while (true) ## Public API at a glance -The current public API is centered around two main entry points: +The current public API is centered around three main entry points: - `FrameworkSystem` - detects platform and product information @@ -115,6 +115,26 @@ The current public API is centered around two main entry points: - `IFrameworkEcConnection` - reads firmware, power, fan capability, and thermal snapshots - sends fan control commands + - exposes the rest of the EC surface through the facets below +- `FrameworkPeripherals` + - reads stylus battery, camera, input module, USB hub, audio card and NVMe firmware versions + - controls the touchscreen and touchpad, which talk to HID/USB directly rather than through the EC + +The EC facets, reached as properties on `IFrameworkEcConnection`: + +| Facet | Covers | +| --- | --- | +| `Diagnostics` | liveness (`hello`), protocol info, sysinfo, saved panic data, port 80 history, switch positions, AP throttle status, raw ADC channels, host command probing | +| `Gpio` | reads, writes and enumerates embedded controller GPIO lines | +| `Thermal` | per-sensor thresholds, EC-reported sensor names, authoritative fan count | +| `Battery` | Smart Battery data set, pack authentication, cutoff (ship mode) state, charging state, charge rate limit | +| `PowerDelivery` | PD controller firmware versions, per-port charger negotiation state, retimer version | +| `Input` | per-key RGB, keyboard matrix remapping, PS/2 emulation, fingerprint LED brightness | +| `PowerManagement` | hibernate delay, standalone (batteryless) mode, expansion-bay GPU serial | + +Two calls are deliberately expensive and must not be polled: `Battery.GetSmartBatterySnapshot()` +performs many I2C round trips, and `FrameworkPeripherals.GetAudioCardVersion()` claims the HID +interface for up to a few seconds. Read both on demand only. Main snapshot types: @@ -213,6 +233,7 @@ Public API methods throw specific managed exception types rather than requiring Examples include: - `FrameworkEcResponseException` and its derived EC response exceptions +- `FrameworkNotSupportedStatusException`, raised when a capability is not compiled in for the current platform. This is permanent, unlike `FrameworkDataUnavailableStatusException`, which signals a transient read failure. NVMe version readback on non-Linux hosts is the current case. - `FrameworkInvalidFanIndexException` - `FrameworkBatteryStateException` and derived battery state exceptions - `FrameworkTemperatureStateException` and derived temperature state exceptions @@ -269,5 +290,9 @@ This project is an independent community project and is not affiliated with, end ## Current limitations - The managed API currently infers `FrameworkThermalSnapshot.SensorCount` because the Rust layer does not yet provide a dedicated sensor count value. +- There is no max-fan-RPM reader. `Thermal.GetThresholds(...)` reports `FanOff` and `FanMax` as the *temperature* setpoints at which the EC starts and maxes active cooling; they are not RPM limits, and the EC fan table ceiling stays firmware-enforced. +- Touchpad haptic intensity and click force are write-only. The firmware never answers `GET_FEATURE`, so they cannot be read back. +- There is no GPU serial write path, by design. `PowerManagement.GetGpuSerial()` is read-only because programming a serial changes persistent expansion-bay identity. +- `Input.RemapCapsLockToControl()` targets the Framework Laptop 12 matrix position. The keyboard matrix differs per model, so Framework Laptop 13 needs `Input.RemapKey(4, 4, 0x0014)` instead, and the Framework Laptop 16 keyboard is not EC-remappable. - The public fixed-slot snapshot members intentionally mirror the current native Rust struct layout. - Some command responses still echo request identity such as `FanIndex` for clarity and traceability. diff --git a/framework-dotnet-cli-test/Program.cs b/framework-dotnet-cli-test/Program.cs index 62c0689..f3cf02c 100644 --- a/framework-dotnet-cli-test/Program.cs +++ b/framework-dotnet-cli-test/Program.cs @@ -23,8 +23,15 @@ static void Main(string[] args) try { FrameworkSystem frameworkSystem = new FrameworkSystem(); + FrameworkPeripherals peripherals = new FrameworkPeripherals(); using IFrameworkEcConnection ec = frameworkSystem.OpenDefaultEc(); + // Read the expensive surfaces once rather than on every refresh. The Smart Battery data + // set costs many I2C round trips, and the audio card query claims the HID interface for + // up to a few seconds; neither belongs in a polling loop. + AnsiConsole.MarkupLine("[grey]Reading one-time diagnostics (Smart Battery and peripheral firmware)...[/]"); + string oneTimeReport = BuildOneTimeReport(ec, peripherals); + while (true) { AnsiConsole.Clear(); @@ -50,6 +57,40 @@ static void Main(string[] args) WritePanel(CreateOptionalPanel("[bold green]Expansion Bay Modules[/]", Color.Green, () => FormatExpansionBayModules(ec.GetExpansionBayModulesSnapshot()))); WritePanel(CreateOptionalPanel("[bold blue]Module Inventory[/]", Color.Blue, () => FormatModuleInventory(ec.GetModuleInventorySnapshot()))); + // Diagnostics facet. + WritePanel(CreateOptionalPanel("[bold cyan]EC Switches[/]", Color.Cyan, () => FormatSimpleSnapshot(ec.Diagnostics.GetSwitches()))); + WritePanel(CreateOptionalPanel("[bold cyan]EC System Info[/]", Color.Cyan, () => FormatSimpleSnapshot(ec.Diagnostics.GetSystemInfo()))); + WritePanel(CreateOptionalPanel("[bold cyan]EC Protocol Info[/]", Color.Cyan, () => FormatSimpleSnapshot(ec.Diagnostics.GetProtocolInfo()))); + WritePanel(CreateOptionalPanel("[bold cyan]EC Liveness[/]", Color.Cyan, () => FormatSimpleSnapshot(ec.Diagnostics.CheckHello()))); + WritePanel(CreateOptionalPanel("[bold red]AP Throttle Status[/]", Color.Red, () => FormatSimpleSnapshot(ec.Diagnostics.GetApThrottleStatus()))); + WritePanel(CreateOptionalPanel("[bold yellow]Port 80 History[/]", Color.Yellow, () => FormatPort80History(ec.Diagnostics.GetPort80History()))); + WritePanel(CreateOptionalPanel("[bold red]EC Panic Info[/]", Color.Red, () => FormatSimpleSnapshot(ec.Diagnostics.GetPanicInfo()))); + + // Thermal control facet. + WritePanel(CreateOptionalPanel("[bold red]Thermal Thresholds[/]", Color.Red, () => FormatThermalThresholds(ec))); + + // Battery facet. + WritePanel(CreateOptionalPanel("[bold green]Charging State[/]", Color.Green, () => FormatSimpleSnapshot(ec.Battery.GetChargingState()))); + WritePanel(CreateOptionalPanel("[bold green]Battery Cutoff[/]", Color.Green, () => $"Cutoff State: {ec.Battery.GetCutoffState()}")); + + // Power Delivery facet. + WritePanel(CreateOptionalPanel("[bold magenta]PD Controller Versions[/]", Color.Magenta, () => FormatSimpleSnapshot(ec.PowerDelivery.GetControllerVersions()))); + WritePanel(CreateOptionalPanel("[bold magenta]PD Charger Info[/]", Color.Magenta, () => FormatPowerDeliveryPorts(ec))); + WritePanel(CreateOptionalPanel("[bold magenta]Retimer Version[/]", Color.Magenta, () => FormatSimpleSnapshot(ec.PowerDelivery.GetRetimerVersion()))); + + // Power management facet. + WritePanel(CreateOptionalPanel("[bold blue]Hibernate Delay[/]", Color.Blue, () => $"Hibernate Delay: {ec.PowerManagement.GetHibernateDelay()}")); + WritePanel(CreateOptionalPanel("[bold blue]Standalone Mode[/]", Color.Blue, () => FormatSimpleSnapshot(ec.PowerManagement.GetStandaloneMode()))); + WritePanel(CreateOptionalPanel("[bold blue]Expansion Bay GPU Serial[/]", Color.Blue, () => $"GPU Serial: {ec.PowerManagement.GetGpuSerial()}")); + + // GPIO facet. + WritePanel(CreateOptionalPanel("[bold yellow]GPIO[/]", Color.Yellow, () => FormatGpio(ec))); + + // Peripherals (no EC handle - HID/USB direct). + WritePanel(CreateOptionalPanel("[bold green]Stylus Battery[/]", Color.Green, () => FormatSimpleSnapshot(peripherals.GetStylusBattery()))); + + WritePanel(CreatePanel("[bold grey]One-Time Diagnostics[/]", Color.Grey, oneTimeReport)); + Thread.Sleep(RefreshInterval); } } @@ -121,6 +162,154 @@ private static void WritePanel(Panel panel) AnsiConsole.WriteLine(); } + /// + /// Reads the surfaces that are too expensive to poll. The Smart Battery data set costs many I2C + /// round trips, and the audio card query claims the HID interface for up to a few seconds. + /// + private static string BuildOneTimeReport(IFrameworkEcConnection ec, IFrameworkPeripherals peripherals) + { + var content = new StringBuilder(); + + AppendOneTimeSection(content, "Smart Battery", () => FormatSimpleSnapshot(ec.Battery.GetSmartBatterySnapshot())); + AppendOneTimeSection(content, "Camera Firmware", () => FormatSimpleSnapshot(peripherals.GetCameraVersions())); + AppendOneTimeSection(content, "Input Module Firmware", () => FormatSimpleSnapshot(peripherals.GetInputModuleVersions())); + AppendOneTimeSection(content, "USB Hub Firmware", () => FormatSimpleSnapshot(peripherals.GetUsbHubVersions())); + AppendOneTimeSection(content, "Audio Card Firmware", () => FormatSimpleSnapshot(peripherals.GetAudioCardVersion())); + + return content.ToString().TrimEnd(); + } + + private static void AppendOneTimeSection(StringBuilder content, string title, Func contentFactory) + { + content.AppendLine($"{title}:"); + + try + { + content.AppendLine(contentFactory()); + } + catch (FrameworkNotSupportedStatusException) + { + content.AppendLine(" Not supported on this platform."); + } + catch (FrameworkDataUnavailableStatusException) + { + content.AppendLine(" Unavailable on this device."); + } + catch (FrameworkException ex) + { + content.AppendLine($" Framework error: {ex.Message}"); + } + + content.AppendLine(); + } + + private static string FormatPort80History(FrameworkEcPort80HistorySnapshot history) + { + var content = new StringBuilder(); + content.AppendLine($"Writes: {history.Writes.ToString(CultureInfo.InvariantCulture)}"); + content.AppendLine($"History Size: {history.HistorySize.ToString(CultureInfo.InvariantCulture)}"); + content.AppendLine($"Newest Index: {history.NewestIndex.ToString(CultureInfo.InvariantCulture)}"); + + if (history.CodesNewestFirst.Count == 0) + { + content.Append("No POST codes recorded."); + return content.ToString(); + } + + content.AppendLine(); + content.AppendLine("Newest first (first 16):"); + + foreach (ushort code in history.CodesNewestFirst.Take(16)) + { + string marker = Enum.IsDefined((FrameworkPort80Event)code) + ? $" <-- {(FrameworkPort80Event)code}" + : string.Empty; + + content.AppendLine($" 0x{code.ToString("X4", CultureInfo.InvariantCulture)}{marker}"); + } + + return content.ToString().TrimEnd(); + } + + private static string FormatThermalThresholds(IFrameworkEcConnection ec) + { + FrameworkThermalSnapshot thermal = ec.GetThermalSnapshot(); + var content = new StringBuilder(); + + content.AppendLine($"EC Fan Count: {ec.Thermal.GetFanCount().ToString(CultureInfo.InvariantCulture)}"); + content.AppendLine(); + + for (byte sensorIndex = 0; sensorIndex < thermal.SensorCount; sensorIndex++) + { + content.Append($"Sensor {sensorIndex.ToString(CultureInfo.InvariantCulture)}"); + + try + { + FrameworkTemperatureSensorNameSnapshot name = ec.Thermal.GetSensorName(sensorIndex); + content.Append($" ({name.FirmwareName} -> {name.MappedName}, {name.SensorType})"); + } + catch (FrameworkException) + { + // The firmware does not name this slot; the thresholds below are still meaningful. + } + + content.AppendLine(":"); + + try + { + content.AppendLine($" {FormatSimpleSnapshot(ec.Thermal.GetThresholds(sensorIndex)).Replace(Environment.NewLine, Environment.NewLine + " ")}"); + } + catch (FrameworkException ex) + { + content.AppendLine($" Unavailable: {ex.Message}"); + } + } + + return content.ToString().TrimEnd(); + } + + private static string FormatPowerDeliveryPorts(IFrameworkEcConnection ec) + { + var content = new StringBuilder(); + FrameworkModuleInventorySnapshot inventory = ec.GetModuleInventorySnapshot(); + + for (int port = 0; port < inventory.UsbCSlotCount; port++) + { + try + { + content.AppendLine($"Port {port.ToString(CultureInfo.InvariantCulture)}: {ec.PowerDelivery.GetPowerInfo(port)}"); + } + catch (FrameworkException ex) + { + content.AppendLine($"Port {port.ToString(CultureInfo.InvariantCulture)}: {ex.Message}"); + } + } + + return content.Length == 0 ? "No USB-C ports reported." : content.ToString().TrimEnd(); + } + + private static string FormatGpio(IFrameworkEcConnection ec) + { + IReadOnlyList lines = ec.Gpio.GetAll(); + var content = new StringBuilder(); + + content.AppendLine($"GPIO Count: {lines.Count.ToString(CultureInfo.InvariantCulture)}"); + + if (lines.Count == 0) + { + return content.ToString().TrimEnd(); + } + + content.AppendLine(); + + foreach (FrameworkEcGpioSnapshot line in lines) + { + content.AppendLine($" {line}"); + } + + return content.ToString().TrimEnd(); + } + private static string FormatSimpleSnapshot(object snapshot) { return snapshot.ToString()?.Replace(", ", Environment.NewLine) ?? string.Empty; diff --git a/framework-dotnet-hardware-tests/FrameworkHardwareTests.cs b/framework-dotnet-hardware-tests/FrameworkHardwareTests.cs index 8094142..0124df3 100644 --- a/framework-dotnet-hardware-tests/FrameworkHardwareTests.cs +++ b/framework-dotnet-hardware-tests/FrameworkHardwareTests.cs @@ -1,4 +1,5 @@ using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; using FrameworkDotnet.Exceptions.EcResponseDetails; using FrameworkDotnet.Exceptions.StatusCodes; using FrameworkDotnet.Interfaces; @@ -480,6 +481,398 @@ private static void AssertExpansionBayClassification(FrameworkExpansionBaySnapsh } } + [Test] + [Description("Port 80 ordering is pure decode logic, so it is verified without hardware. Writes is the NEXT slot the EC will write, so the newest entry is the slot before it.")] + public void Port80History_NewestEntry_IsTheSlotBeforeTheWriteCursor() + { + // A wrapped ring: 10 writes into a 4-entry buffer. The cursor sits at 10 % 4 == 2, so the + // newest code is at index 1 and the walk backwards is 1, 0, 3, 2. + var wrapped = new FrameworkEcPort80HistorySnapshot(10, 4, [0xAA, 0xBB, 0xCC, 0xDD]); + + Assert.That(wrapped.NewestIndex, Is.EqualTo(1)); + Assert.That(wrapped.CodesNewestFirst, Is.EqualTo(new ushort[] { 0xBB, 0xAA, 0xDD, 0xCC }).AsCollection); + + // A partially filled ring reports only the slots that were actually written. + var partial = new FrameworkEcPort80HistorySnapshot(2, 4, [0xAA, 0xBB, 0x00, 0x00]); + + Assert.That(partial.NewestIndex, Is.EqualTo(1)); + Assert.That(partial.CodesNewestFirst, Is.EqualTo(new ushort[] { 0xBB, 0xAA }).AsCollection); + + // Nothing recorded yet: the sentinel, not a fabricated ordering. + var empty = new FrameworkEcPort80HistorySnapshot(0, 4, [0x00, 0x00, 0x00, 0x00]); + + Assert.That(empty.NewestIndex, Is.EqualTo(-1)); + Assert.That(empty.CodesNewestFirst, Is.Empty); + } + + [Test] + public void Diagnostics_ShouldReturnExpectedInformationOrReportUnavailable() + { + AssertOptionalReadback( + () => ec.Diagnostics.GetSwitches(), + switches => Assert.That(switches.ToString(), Is.Not.Null.And.Not.Empty)); + + AssertOptionalReadback( + () => ec.Diagnostics.GetSystemInfo(), + systemInfo => Assert.That(Enum.IsDefined(systemInfo.CurrentImage))); + + AssertOptionalReadback( + () => ec.Diagnostics.GetProtocolInfo(), + protocolInfo => + { + Assert.That(protocolInfo.MaxRequestPacketSize.Bytes, Is.GreaterThan(0)); + Assert.That(protocolInfo.MaxResponsePacketSize.Bytes, Is.GreaterThan(0)); + Assert.That(protocolInfo.SupportedProtocolVersions, Is.Not.Empty); + }); + + AssertOptionalReadback( + () => ec.Diagnostics.GetApThrottleStatus(), + throttle => Assert.That(throttle.ToString(), Is.Not.Null.And.Not.Empty)); + + AssertOptionalReadback( + () => ec.Diagnostics.GetPanicInfo(), + panic => Assert.That(panic.Data, Is.Not.Null)); + } + + [Test] + [Description("hello echoes a fixed transform of the input, so a matching response proves the EC is answering rather than returning stale bytes.")] + public void Diagnostics_Hello_ShouldEchoTheRequestOrReportUnavailable() + { + AssertOptionalReadback( + () => ec.Diagnostics.CheckHello(), + hello => Assert.That(hello.IsExpectedEcho, Is.True, "The EC did not echo the expected hello response.")); + + AssertOptionalReadback( + () => ec.Diagnostics.SendHello(0xA0B0C0D0), + hello => Assert.That(hello.IsExpectedEcho, Is.True, "The EC did not echo the expected hello response.")); + } + + [Test] + public void Diagnostics_Port80History_ShouldBeSelfConsistentOrReportUnavailable() + { + // The FFI crate reads the history itself rather than calling CrosEc::port80_read, which + // rejects the longer-than-requested buffers the Windows driver reports. A DeviceError here + // means that workaround regressed, so it is deliberately not tolerated. + AssertOptionalReadback( + () => ec.Diagnostics.GetPort80History(), + AssertPort80HistoryIsSelfConsistent); + } + + private static void AssertPort80HistoryIsSelfConsistent(FrameworkEcPort80HistorySnapshot history) + { + Assert.That(history.Codes, Is.Not.Null); + Assert.That(history.CodesNewestFirst.Count, Is.LessThanOrEqualTo(history.Codes.Count)); + + if (history.CodesNewestFirst.Count == 0) + { + Assert.That(history.NewestIndex, Is.EqualTo(-1)); + return; + } + + Assert.That(history.NewestIndex, Is.InRange(0, history.Codes.Count - 1)); + Assert.That(history.CodesNewestFirst[0], Is.EqualTo(history.Codes[history.NewestIndex])); + } + + [Test] + public void Diagnostics_CommandVersionProbe_ShouldAnswerForAKnownCommand() + { + AssertOptionalReadback( + () => ec.Diagnostics.IsCommandVersionSupported(0x0000, 0), + _ => Assert.Pass()); + } + + [Test] + public void Gpio_Enumeration_ShouldMatchReportedCountOrReportUnavailable() + { + AssertOptionalReadback( + () => ec.Gpio.GetAll(), + lines => + { + Assert.That(lines, Is.Not.Null); + Assert.That(lines.Count, Is.EqualTo(ec.Gpio.GetCount())); + + foreach (var line in lines) + { + Assert.That(line.Name, Is.Not.Null.And.Not.Empty, "Every enumerated GPIO must carry a firmware name."); + } + }); + } + + [Test] + public void Gpio_GetValue_ShouldRejectNullAndEmptyNames() + { + Assert.That(() => ec.Gpio.GetValue(null!), Throws.TypeOf()); + Assert.That(() => ec.Gpio.GetValue(string.Empty), Throws.InstanceOf()); + } + + [Test] + [Description("A disabled threshold reads back as -273 C from firmware, so it must surface as null rather than as a temperature.")] + public void ThermalThresholds_DisabledThresholds_ShouldBeNullNeverMinus273() + { + FrameworkThermalSnapshot thermal = ec.GetThermalSnapshot(); + + for (byte sensorIndex = 0; sensorIndex < thermal.SensorCount; sensorIndex++) + { + byte index = sensorIndex; + + AssertOptionalReadback( + () => ec.Thermal.GetThresholds(index), + thresholds => + { + foreach (var threshold in new[] { thresholds.Warn, thresholds.High, thresholds.Halt, thresholds.FanOff, thresholds.FanMax }) + { + if (threshold.HasValue) + { + Assert.That( + threshold.Value.DegreesCelsius, + Is.GreaterThan(-273), + "A threshold that reads back as -273 C is disabled and must be surfaced as null."); + } + } + }); + } + } + + [Test] + public void ThermalControl_FanCountAndSensorNames_ShouldAgreeWithTheThermalSnapshot() + { + AssertOptionalReadback( + () => ec.Thermal.GetFanCount(), + fanCount => Assert.That(fanCount, Is.EqualTo(ec.GetThermalSnapshot().FanCount))); + + AssertOptionalReadback( + () => ec.Thermal.GetSensorName(0), + name => + { + Assert.That(name.FirmwareName, Is.Not.Null); + Assert.That(Enum.IsDefined(name.MappedName)); + Assert.That(Enum.IsDefined(name.SensorType)); + + // The second read must come from the cache and agree with the first. + Assert.That(ec.Thermal.GetSensorName(0), Is.EqualTo(name)); + }); + } + + [Test] + public void ThermalControl_SensorNameCache_ShouldThrowAfterTheConnectionIsDisposed() + { + IFrameworkEcConnection connection = frameworkSystem.OpenDefaultEc(); + IFrameworkEcThermalControl thermal = connection.Thermal; + + try + { + _ = thermal.GetSensorName(0); + } + catch (FrameworkDataUnavailableStatusException) + { + Assert.Ignore("Sensor names are not available on this device."); + } + + connection.Dispose(); + + Assert.That( + () => thermal.GetSensorName(0), + Throws.TypeOf(), + "A cached sensor name must not be served after the owning connection is disposed."); + } + + [Test] + public void Battery_ReadOnlySurfaces_ShouldReturnExpectedInformationOrReportUnavailable() + { + AssertOptionalReadback( + () => ec.Battery.GetChargingState(), + charging => Assert.That(charging.ToString(), Is.Not.Null.And.Not.Empty)); + + AssertOptionalReadback( + () => ec.Battery.GetCutoffState(), + cutoff => Assert.That(Enum.IsDefined(cutoff))); + } + + [Test] + [Description("The Smart Battery read costs many I2C round trips, so it is exercised exactly once.")] + public void Battery_SmartBatterySnapshot_ShouldReturnConsistentUnitsOrReportUnavailable() + { + AssertOptionalReadback( + () => ec.Battery.GetSmartBatterySnapshot(), + battery => + { + Assert.That(battery.ManufacturerName, Is.Not.Null); + Assert.That(battery.CellVoltages.Count, Is.EqualTo(4)); + + // Exactly one of the two parallel capacity sets is populated, chosen by CAPACITY_MODE. + if (battery.IsCapacityReportedInEnergyUnits) + { + Assert.That(battery.RemainingCapacity, Is.Null); + Assert.That(battery.RemainingEnergy, Is.Not.Null); + } + else + { + Assert.That(battery.RemainingCapacity, Is.Not.Null); + Assert.That(battery.RemainingEnergy, Is.Null); + } + + // The sealed groups are null rather than zero-filled. + if (!battery.IsUnsealed) + { + Assert.That(battery.StateOfHealth, Is.Null); + Assert.That(battery.Safety, Is.Null); + Assert.That(battery.LifetimeData, Is.Null); + } + }); + } + + [Test] + public void Battery_Authenticate_ShouldRejectKeysThatAreNotSixteenBytes() + { + Assert.That(() => ec.Battery.Authenticate(null!), Throws.TypeOf()); + Assert.That(() => ec.Battery.Authenticate(new byte[15]), Throws.InstanceOf()); + Assert.That(() => ec.Battery.Authenticate(new byte[17]), Throws.InstanceOf()); + } + + [Test] + public void PowerDelivery_ControllerVersions_ShouldOnlyReportPresentSlots() + { + AssertOptionalReadback( + () => ec.PowerDelivery.GetControllerVersions(), + versions => + { + foreach (var controller in versions.PresentControllers) + { + Assert.That(controller.IsPresent, Is.True, "PresentControllers must not yield an absent slot."); + Assert.That(Enum.IsDefined(controller.Slot)); + } + }); + } + + [Test] + public void PowerDelivery_GetPowerInfo_ShouldRejectPortsOutsideTheByteRange() + { + Assert.That(() => ec.PowerDelivery.GetPowerInfo(-1), Throws.TypeOf()); + Assert.That(() => ec.PowerDelivery.GetPowerInfo(256), Throws.TypeOf()); + } + + [Test] + [Description("The retimer sits behind the Framework 16 expansion bay; other families reject the underlying EC command.")] + public void PowerDelivery_RetimerVersion_ShouldReadOnFramework16OrThrowElsewhere() + { + if (frameworkSystem.GetPlatformFamily() != FrameworkPlatformFamily.Framework16) + { + Assert.That(() => ec.PowerDelivery.GetRetimerVersion(), Throws.InstanceOf()); + return; + } + + AssertOptionalReadback( + () => ec.PowerDelivery.GetRetimerVersion(), + retimer => + { + Assert.That(retimer.Version, Is.Not.Null); + + // The version is four raw register bytes, never text. + if (retimer.IsPresent && retimer.Version.Count >= 4) + { + Assert.That(retimer.VersionString, Does.Match("^[0-9A-F]+(\\.[0-9A-F]+){3}$")); + } + }); + } + + [Test] + public void PowerManagement_ReadOnlySurfaces_ShouldReturnExpectedInformationOrReportUnavailable() + { + AssertOptionalReadback( + () => ec.PowerManagement.GetHibernateDelay(), + delay => Assert.That(delay.Seconds, Is.GreaterThanOrEqualTo(0))); + + AssertOptionalReadback( + () => ec.PowerManagement.GetStandaloneMode(), + standalone => Assert.That(standalone.ToString(), Is.Not.Null.And.Not.Empty)); + } + + [Test] + public void Input_WriteGuards_ShouldRejectImpossibleArgumentsBeforeTouchingHardware() + { + // 64 keys is the per-call maximum the native ABI accepts. + Assert.That( + () => ec.Input.SetRgbKeyboardColors(0, new FrameworkKeyboardColor[65]), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.SetRgbKeyboardColors(0, []), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.SetRgbKeyboardColors(0, null!), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.SetRgbKeyboardColors(-1, [default]), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.SetFingerprintLedBrightness(Ratio.FromPercent(-1)), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.SetFingerprintLedBrightness(Ratio.FromPercent(101)), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.RemapKey(-1, 0, 0x0014), + Throws.TypeOf()); + + Assert.That( + () => ec.Input.RemapKey(0, 256, 0x0014), + Throws.TypeOf()); + } + + [Test] + public void Peripherals_ReadOnlySurfaces_ShouldReturnExpectedInformationOrReportUnavailable() + { + IFrameworkPeripherals peripherals = new FrameworkPeripherals(); + + AssertOptionalReadback( + () => peripherals.GetStylusBattery(), + stylus => Assert.That(stylus.ChargeLevel.Percent, Is.InRange(0, 100))); + + AssertOptionalReadback( + () => peripherals.GetCameraVersions(), + cameras => Assert.That(cameras.Peripherals, Is.Not.Null)); + + AssertOptionalReadback( + () => peripherals.GetUsbHubVersions(), + hubs => Assert.That(hubs.Peripherals, Is.Not.Null)); + } + + [Test] + public void Peripherals_WriteGuards_ShouldRejectImpossibleArgumentsBeforeTouchingHardware() + { + IFrameworkPeripherals peripherals = new FrameworkPeripherals(); + + Assert.That( + () => peripherals.SetTouchpadHapticIntensity(Ratio.FromPercent(-1)), + Throws.TypeOf()); + + Assert.That( + () => peripherals.SetTouchpadHapticIntensity(Ratio.FromPercent(101)), + Throws.TypeOf()); + + Assert.That( + () => peripherals.GetNvmeVersion(null!), + Throws.TypeOf()); + } + + [Test] + [Platform("Win", Reason = "The NVMe passthrough is gated to Linux upstream, so other platforms must report NotSupported.")] + [Description("Verifies the new FrameworkStatusCode.NotSupported maps to a managed exception instead of falling through to ArgumentOutOfRangeException.")] + public void Peripherals_NvmeVersion_ShouldReportNotSupportedOnNonLinuxPlatforms() + { + IFrameworkPeripherals peripherals = new FrameworkPeripherals(); + + Assert.That( + () => peripherals.GetNvmeVersion("/dev/nvme0"), + Throws.TypeOf(), + "NotSupported must map to FrameworkNotSupportedStatusException, not to an unhandled status code."); + } + private static void AssertOptionalReadback(Func readback, Action assertions) { try @@ -489,5 +882,8 @@ private static void AssertOptionalReadback(Func readback, Action assert catch (FrameworkDataUnavailableStatusException) { } + catch (FrameworkNotSupportedStatusException) + { + } } } diff --git a/framework-dotnet/Ec/FrameworkEcBattery.cs b/framework-dotnet/Ec/FrameworkEcBattery.cs new file mode 100644 index 0000000..d37c587 --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcBattery.cs @@ -0,0 +1,115 @@ +using System; + +using FrameworkDotnet.Enums; +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Implements the battery facet of an embedded controller connection. +/// +internal sealed class FrameworkEcBattery : IFrameworkEcBattery +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The facet never owns the embedded controller handle. It borrows it through , which the owning supplies as a closure over its own validated handle, so the connection keeps sole responsibility for lifetime and for raising once it has been disposed. + /// + /// A callback returning the current embedded controller handle. It is expected to throw when the owning connection has been disposed. + /// Thrown when is . + internal FrameworkEcBattery(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + public FrameworkSmartBatterySnapshot GetSmartBatterySnapshot(uint? unsealKey = null) + { + byte useUnsealKey = unsealKey.HasValue ? (byte)1 : (byte)0; + + unsafe + { + return Native.NativeMethods.framework_ec_get_smart_battery_data(HandlePointer, useUnsealKey, unsealKey ?? 0u).GetValueOrThrow(); + } + } + + /// + public FrameworkBatteryCutoffState GetCutoffState() + { + unsafe + { + return (FrameworkBatteryCutoffState)(int)Native.NativeMethods.framework_ec_get_battery_cutoff_status(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkChargingStateSnapshot GetChargingState() + { + unsafe + { + return Native.NativeMethods.framework_ec_is_charging(HandlePointer).GetValueOrThrow(); + } + } + + /// + public bool Authenticate(byte[] authenticationKey) + { + ArgumentNullException.ThrowIfNull(authenticationKey); + + if (authenticationKey.Length != AuthenticationKeyLength) + { + throw new ArgumentException($"The authentication key must be exactly {AuthenticationKeyLength} bytes long.", nameof(authenticationKey)); + } + + unsafe + { + fixed (byte* authenticationKeyPointer = authenticationKey) + { + return Native.NativeMethods.framework_ec_authenticate_battery(HandlePointer, authenticationKeyPointer).GetValueOrThrow(); + } + } + } + + /// + public void SetChargeRateLimit(ElectricCurrent rateLimit, Ratio? batterySoc = null) + { + double amperes = rateLimit.Amperes; + ArgumentOutOfRangeException.ThrowIfNegative(amperes, nameof(rateLimit)); + + if (batterySoc.HasValue) + { + double percent = batterySoc.Value.Percent; + ArgumentOutOfRangeException.ThrowIfNegative(percent, nameof(batterySoc)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(percent, 100.0, nameof(batterySoc)); + } + + float batterySocPercent = batterySoc.HasValue ? (float)batterySoc.Value.Percent : UnconditionalBatterySocPercent; + + unsafe + { + Native.NativeMethods.framework_ec_set_charge_rate_limit(HandlePointer, (float)amperes, batterySocPercent).ThrowIfFailure(); + } + } + + private unsafe Native.FrameworkEcHandle* HandlePointer => (Native.FrameworkEcHandle*)handleAccessor(); + + /// + /// The exact length, in bytes, the native authentication entry point reads from the supplied key pointer. + /// + private const int AuthenticationKeyLength = 16; + + /// + /// The sentinel the native layer interprets as "apply the charge rate limit unconditionally". + /// + private const float UnconditionalBatterySocPercent = -1.0f; + + private readonly Func handleAccessor; +} diff --git a/framework-dotnet/Ec/FrameworkEcDiagnostics.cs b/framework-dotnet/Ec/FrameworkEcDiagnostics.cs new file mode 100644 index 0000000..d9504fb --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcDiagnostics.cs @@ -0,0 +1,134 @@ +using System; + +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Implements the embedded controller diagnostic facet on top of a borrowed native EC handle. +/// +/// +/// +/// The facet does not own the handle. It borrows it from the connection that created it through +/// a caller-supplied accessor, which is expected to perform the connection's own disposed check +/// and hand back the current native handle. That keeps the facet decoupled from the concrete +/// connection type while preserving the connection's lifetime guarantees: once the connection is +/// disposed every call through this facet throws +/// from inside the accessor. +/// +/// +/// Because nothing here is owned, the facet is deliberately not disposable and is safe to hold +/// for the lifetime of the owning connection. +/// +/// +internal sealed class FrameworkEcDiagnostics : IFrameworkEcDiagnostics +{ + private readonly Func handleAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// A callback returning the native EC handle of the owning connection. It is invoked on every call and is expected to throw when the owning connection has been disposed. + /// Thrown when is . + internal FrameworkEcDiagnostics(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + public FrameworkEcHelloSnapshot SendHello(uint inData) + { + unsafe + { + return Native.NativeMethods.framework_ec_hello(HandlePointer, inData).GetValueOrThrow(); + } + } + + /// + public FrameworkEcHelloSnapshot CheckHello() + { + unsafe + { + return Native.NativeMethods.framework_ec_check_hello(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkEcProtocolInfoSnapshot GetProtocolInfo() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_protocol_info(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkEcSystemInfoSnapshot GetSystemInfo() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_sysinfo(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkEcPanicInfoSnapshot GetPanicInfo() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_panic_info(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkEcPort80HistorySnapshot GetPort80History() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_port80_history(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkEcSwitchesSnapshot GetSwitches() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_switches(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkEcApThrottleSnapshot GetApThrottleStatus() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_ap_throttle_status(HandlePointer).GetValueOrThrow(); + } + } + + /// + public int ReadAdcChannel(byte channel) + { + unsafe + { + return Native.NativeMethods.framework_ec_adc_read(HandlePointer, channel).GetValueOrThrow(); + } + } + + /// + public bool IsCommandVersionSupported(uint command, byte version) + { + unsafe + { + return Native.NativeMethods.framework_ec_command_version_supported(HandlePointer, command, version).GetValueOrThrow(); + } + } + + private unsafe Native.FrameworkEcHandle* HandlePointer => (Native.FrameworkEcHandle*)handleAccessor(); +} diff --git a/framework-dotnet/Ec/FrameworkEcGpio.cs b/framework-dotnet/Ec/FrameworkEcGpio.cs new file mode 100644 index 0000000..472e8c2 --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcGpio.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Implements the embedded controller general-purpose input/output facet over a live native EC handle. +/// +/// +/// The facet does not own the native handle. It borrows it through the accessor supplied by the owning +/// , so the connection stays the single point of lifetime control and +/// keeps its disposal check on every call. +/// +internal sealed class FrameworkEcGpio : IFrameworkEcGpio +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// A delegate returning the live native embedded controller handle as an . It is + /// invoked immediately before every native call, so the owning connection can validate its own state: + /// the delegate is expected to throw once the connection has been + /// closed, and must never hand back a stale or closed handle. + /// + /// Thrown when is . + internal FrameworkEcGpio(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + public int GetCount() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_gpio_count(HandlePointer).GetValueOrThrow(); + } + } + + /// + public bool GetValue(string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + unsafe + { + byte* nameBytes = stackalloc byte[MaximumNameLengthInBytes]; + int nameLength = EncodeName(name, new Span(nameBytes, MaximumNameLengthInBytes)); + + return Native.NativeMethods.framework_ec_get_gpio(HandlePointer, nameBytes, nameLength).GetValueOrThrow(); + } + } + + /// + public void SetValue(string name, bool value) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + unsafe + { + byte* nameBytes = stackalloc byte[MaximumNameLengthInBytes]; + int nameLength = EncodeName(name, new Span(nameBytes, MaximumNameLengthInBytes)); + + Native.NativeMethods.framework_ec_set_gpio(HandlePointer, nameBytes, nameLength, value).ThrowIfFailure(); + } + } + + /// + public FrameworkEcGpioSnapshot GetInfo(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThan(index, byte.MaxValue); + + unsafe + { + return Native.NativeMethods.framework_ec_get_gpio_info(HandlePointer, (byte)index).GetValueOrThrow(); + } + } + + /// + public IReadOnlyList GetAll() + { + int count = GetCount(); + List snapshots = new(count); + + for (int index = 0; index < count; index++) + { + snapshots.Add(GetInfo(index)); + } + + return snapshots; + } + + /// + /// Encodes a GPIO line name into the caller-supplied buffer as UTF-8 without a terminating NUL. + /// + /// The line name to encode. + /// The buffer receiving the encoded bytes. + /// The number of bytes written, which is the length the native layer expects. + /// Thrown when encodes to more than UTF-8 bytes. + private static int EncodeName(string name, Span destination) + { + int byteCount = Encoding.UTF8.GetByteCount(name); + + if (byteCount > MaximumNameLengthInBytes) + { + throw new ArgumentException($"The GPIO name must encode to at most {MaximumNameLengthInBytes} UTF-8 bytes, but '{name}' requires {byteCount}. Longer names are truncated by the embedded controller and would address the wrong line.", nameof(name)); + } + + return Encoding.UTF8.GetBytes(name, destination); + } + + private unsafe Native.FrameworkEcHandle* HandlePointer => (Native.FrameworkEcHandle*)handleAccessor(); + + /// + /// The largest GPIO line name the embedded controller host command can carry, in UTF-8 bytes. + /// + private const int MaximumNameLengthInBytes = 32; + + private readonly Func handleAccessor; +} diff --git a/framework-dotnet/Ec/FrameworkEcInput.cs b/framework-dotnet/Ec/FrameworkEcInput.cs new file mode 100644 index 0000000..b320edf --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcInput.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Implements the embedded controller input-device controls on top of a borrowed embedded controller handle. +/// +/// +/// The facet does not own the native handle. It reads it through the accessor supplied at construction time, so the +/// owning connection keeps sole responsibility for opening, disposal and disposed-state checks. +/// +internal sealed class FrameworkEcInput : IFrameworkEcInput +{ + /// + /// The maximum number of per-key colors the embedded controller accepts in a single set-color command. + /// + private const int MaxColorsPerCall = 64; + + /// + /// The number of bytes the embedded controller expects per key color: red, green and blue. + /// + private const int BytesPerColor = 3; + + /// + /// The largest value the native layer can accept for an argument that is marshalled as a single byte. + /// + private const int MaxByteValue = 255; + + private readonly Func handleAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A callback that returns the native embedded controller handle owned by the connection. The owning connection + /// is expected to perform its own disposed-state validation inside the callback and to return + /// only when no usable handle exists. + /// + /// Thrown when is . + internal FrameworkEcInput(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + [FrameworkPlatformSpecific(FrameworkPlatformFamily.FrameworkDesktop, Message = "Upstream framework-system documents the RGB LED surface on Framework Desktop only. The Framework Laptop 16 keyboard is not driven by the embedded controller.")] + public void SetRgbKeyboardColors(int startKey, IReadOnlyList colors) + { + ArgumentNullException.ThrowIfNull(colors); + ArgumentOutOfRangeException.ThrowIfNegative(startKey); + ArgumentOutOfRangeException.ThrowIfGreaterThan(startKey, MaxByteValue); + + // Read Count exactly once. It is a caller-supplied interface, so a concurrent mutation + // between the validation, the allocation and the length handed to native would let the + // FFI read past the pinned buffer - native only rejects a negative count. + int count = colors.Count; + ArgumentOutOfRangeException.ThrowIfZero(count, nameof(colors)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(count, MaxColorsPerCall, nameof(colors)); + + byte[] flattened = new byte[count * BytesPerColor]; + + for (int index = 0; index < count; index++) + { + FrameworkKeyboardColor color = colors[index]; + int offset = index * BytesPerColor; + + flattened[offset] = color.Red; + flattened[offset + 1] = color.Green; + flattened[offset + 2] = color.Blue; + } + + unsafe + { + fixed (byte* colorPointer = flattened) + { + Native.NativeMethods.framework_ec_set_rgb_keyboard_colors(HandlePointer, (byte)startKey, colorPointer, count).ThrowIfFailure(); + } + } + } + + /// + public void RemapKey(int row, int column, ushort scanCode) + { + ArgumentOutOfRangeException.ThrowIfNegative(row); + ArgumentOutOfRangeException.ThrowIfGreaterThan(row, MaxByteValue); + ArgumentOutOfRangeException.ThrowIfNegative(column); + ArgumentOutOfRangeException.ThrowIfGreaterThan(column, MaxByteValue); + + unsafe + { + Native.NativeMethods.framework_ec_remap_key(HandlePointer, (byte)row, (byte)column, scanCode).ThrowIfFailure(); + } + } + + /// + public void RemapCapsLockToControl() + { + unsafe + { + Native.NativeMethods.framework_ec_remap_caps_to_ctrl(HandlePointer).ThrowIfFailure(); + } + } + + /// + public void SetPs2EmulationEnabled(bool enabled) + { + unsafe + { + Native.NativeMethods.framework_ec_ps2_emulation_enable(HandlePointer, enabled).ThrowIfFailure(); + } + } + + /// + public void SetFingerprintLedBrightness(Ratio brightness) + { + double percent = brightness.Percent; + + if (!double.IsFinite(percent)) + { + throw new ArgumentOutOfRangeException(nameof(brightness), percent, "The fingerprint LED brightness must be a finite percentage between 0 and 100."); + } + + ArgumentOutOfRangeException.ThrowIfNegative(percent, nameof(brightness)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(percent, 100.0, nameof(brightness)); + + unsafe + { + Native.NativeMethods.framework_ec_set_fingerprint_led_percentage(HandlePointer, (byte)Math.Round(percent)).ThrowIfFailure(); + } + } + + private unsafe Native.FrameworkEcHandle* HandlePointer + { + get + { + IntPtr handle = handleAccessor(); + + if (handle == IntPtr.Zero) + { + throw new ObjectDisposedException(nameof(IFrameworkEcInput), "The embedded controller connection that owns this input facet has been disposed."); + } + + return (Native.FrameworkEcHandle*)handle; + } + } +} diff --git a/framework-dotnet/Ec/FrameworkEcPowerDelivery.cs b/framework-dotnet/Ec/FrameworkEcPowerDelivery.cs new file mode 100644 index 0000000..78e26ef --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcPowerDelivery.cs @@ -0,0 +1,70 @@ +using System; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Implements the USB Power Delivery facet of an embedded controller connection. +/// +/// +/// The facet does not own the native embedded controller handle. It borrows the handle through an accessor supplied by the owning connection, so the +/// connection remains the single owner and the single place where handle lifetime is enforced. +/// +internal sealed class FrameworkEcPowerDelivery : IFrameworkEcPowerDelivery +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// A delegate returning the native embedded controller handle owned by the connection. The delegate is invoked once per call and is expected to throw + /// when the owning connection has been closed or disposed. + /// + /// Thrown when is . + internal FrameworkEcPowerDelivery(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + public FrameworkPowerDeliveryControllerVersionsSnapshot GetControllerVersions() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_pd_controller_versions(HandlePointer).GetValueOrThrow(); + } + } + + /// + public FrameworkPowerDeliveryPowerInfoSnapshot GetPowerInfo(int port) + { + ArgumentOutOfRangeException.ThrowIfNegative(port); + ArgumentOutOfRangeException.ThrowIfGreaterThan(port, byte.MaxValue); + + unsafe + { + return Native.NativeMethods.framework_ec_get_pd_power_info(HandlePointer, (byte)port).GetValueOrThrow(); + } + } + + /// + [FrameworkPlatformSpecific(FrameworkPlatformFamily.Framework16, Message = "The Parade retimer sits behind the Framework Laptop 16 expansion-bay discrete GPU; other platform families reject the underlying EC command.")] + public FrameworkPowerDeliveryRetimerVersionSnapshot GetRetimerVersion() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_retimer_version(HandlePointer).GetValueOrThrow(); + } + } + + private readonly Func handleAccessor; + + private unsafe Native.FrameworkEcHandle* HandlePointer => (Native.FrameworkEcHandle*)handleAccessor(); +} diff --git a/framework-dotnet/Ec/FrameworkEcPowerManagement.cs b/framework-dotnet/Ec/FrameworkEcPowerManagement.cs new file mode 100644 index 0000000..3319896 --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcPowerManagement.cs @@ -0,0 +1,100 @@ +using System; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Provides the embedded controller power-management operations and the read-only expansion-bay GPU +/// identity for an owning FrameworkEcConnection. +/// +internal sealed class FrameworkEcPowerManagement : IFrameworkEcPowerManagement +{ + private readonly Func handleAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A callback that returns the native embedded controller handle of the owning connection. The + /// owning connection is responsible for validating its own lifetime inside the callback and for + /// throwing once it has been disposed, so that every member + /// of this facet observes the same disposal semantics as the connection itself. + /// + /// Thrown when is . + internal FrameworkEcPowerManagement(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + public Duration GetHibernateDelay() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_hibernate_delay(HandlePointer).GetValueOrThrow(); + } + } + + /// + public void SetHibernateDelay(Duration delay) + { + uint seconds = ToWholeSeconds(delay); + + unsafe + { + Native.NativeMethods.framework_ec_set_hibernate_delay(HandlePointer, seconds).ThrowIfFailure(); + } + } + + /// + public FrameworkStandaloneModeSnapshot GetStandaloneMode() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_standalone_mode(HandlePointer).GetValueOrThrow(); + } + } + + /// + [FrameworkPlatformSpecific(FrameworkPlatformFamily.Framework16, Message = "Upstream framework-system currently documents the expansion-bay GPU surface on Framework Laptop 16 only.")] + public string GetGpuSerial() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_gpu_serial(HandlePointer).GetValueOrThrow(); + } + } + + private static uint ToWholeSeconds(Duration delay) + { + double seconds = delay.Seconds; + + if (double.IsNaN(seconds) || double.IsInfinity(seconds)) + { + throw new ArgumentOutOfRangeException(nameof(delay), seconds, "The hibernate delay must be a finite duration."); + } + + ArgumentOutOfRangeException.ThrowIfNegative(seconds, nameof(delay)); + + double wholeSeconds = Math.Round(seconds, MidpointRounding.AwayFromZero); + + if (wholeSeconds > uint.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(delay), seconds, "The hibernate delay must not exceed 4294967295 seconds."); + } + + return (uint)wholeSeconds; + } + + private unsafe Native.FrameworkEcHandle* HandlePointer => (Native.FrameworkEcHandle*)handleAccessor(); +} diff --git a/framework-dotnet/Ec/FrameworkEcThermalControl.cs b/framework-dotnet/Ec/FrameworkEcThermalControl.cs new file mode 100644 index 0000000..e0ec00e --- /dev/null +++ b/framework-dotnet/Ec/FrameworkEcThermalControl.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Concurrent; + +using FrameworkDotnet.Enums; +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Requests; +using FrameworkDotnet.Snapshots; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet.Ec; + +/// +/// Implements the embedded controller thermal control surface on top of a live EC handle. +/// +/// +/// The instance does not own the embedded controller handle. It borrows it through the accessor +/// supplied to the constructor, so the owning connection stays responsible for the handle's +/// lifetime and for rejecting use after disposal. +/// +internal sealed class FrameworkEcThermalControl : IFrameworkEcThermalControl +{ + /// + /// The native argument that tells the embedded controller to keep a threshold unchanged. Any + /// negative value works; -1 is used for readability. + /// + private const int KeepCurrentArgument = -1; + + /// + /// The native argument that tells the embedded controller to disable a threshold. + /// + private const int DisableArgument = 0; + + private readonly Func handleAccessor; + + private readonly ConcurrentDictionary> sensorNameCache = new(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Returns the owning connection's native embedded controller handle as an + /// . It is invoked once per host command rather than captured, so the + /// owning connection can validate its own state on every call; it is expected to throw + /// once the connection is closed or disposed. + /// + /// Thrown when is . + internal FrameworkEcThermalControl(Func handleAccessor) + { + ArgumentNullException.ThrowIfNull(handleAccessor); + + this.handleAccessor = handleAccessor; + } + + /// + public FrameworkThermalThresholdsSnapshot GetThresholds(byte sensorIndex) + { + unsafe + { + return Native.NativeMethods.framework_ec_get_thermal_thresholds(HandlePointer, sensorIndex).GetValueOrThrow(); + } + } + + /// + public void SetThresholds( + byte sensorIndex, + FrameworkThermalThresholdSetting warn = default, + FrameworkThermalThresholdSetting high = default, + FrameworkThermalThresholdSetting halt = default, + FrameworkThermalThresholdSetting fanOff = default, + FrameworkThermalThresholdSetting fanMax = default) + { + int warnArgument = ToNativeArgument(warn, nameof(warn)); + int highArgument = ToNativeArgument(high, nameof(high)); + int haltArgument = ToNativeArgument(halt, nameof(halt)); + int fanOffArgument = ToNativeArgument(fanOff, nameof(fanOff)); + int fanMaxArgument = ToNativeArgument(fanMax, nameof(fanMax)); + + unsafe + { + Native.NativeMethods.framework_ec_set_thermal_thresholds( + HandlePointer, + sensorIndex, + warnArgument, + highArgument, + haltArgument, + fanOffArgument, + fanMaxArgument).ThrowIfFailure(); + } + } + + /// + public FrameworkTemperatureSensorNameSnapshot GetSensorName(byte sensorIndex) + { + // Touch the accessor before consulting the cache. Every other member reaches the handle on + // every call, which is how the owning connection raises ObjectDisposedException; a cache hit + // would otherwise return a stale reading after the connection was disposed. + _ = handleAccessor(); + + // Lazy so that two threads racing on the same index issue one host command rather than two. + // ConcurrentDictionary.GetOrAdd invokes its factory outside the lock. + return sensorNameCache.GetOrAdd( + sensorIndex, + static (index, self) => new Lazy(() => self.ReadSensorName(index)), + this).Value; + } + + /// + public void ClearSensorNameCache() + { + sensorNameCache.Clear(); + } + + /// + public byte GetFanCount() + { + unsafe + { + return Native.NativeMethods.framework_ec_get_fan_count(HandlePointer).GetValueOrThrow(); + } + } + + /// + /// Encodes one threshold setting into the native argument, whose sign carries the caller's + /// intent: negative keeps the current threshold, zero disables it, and a positive value is + /// degrees Celsius. + /// + private static int ToNativeArgument(FrameworkThermalThresholdSetting setting, string parameterName) + { + switch (setting.Action) + { + case FrameworkThermalThresholdAction.KeepCurrent: + return KeepCurrentArgument; + + case FrameworkThermalThresholdAction.Disable: + return DisableArgument; + + case FrameworkThermalThresholdAction.Set: + return ToPositiveCelsius(setting, parameterName); + + default: + throw new ArgumentOutOfRangeException(parameterName, setting.Action, "The thermal threshold action is not recognized."); + } + } + + private static int ToPositiveCelsius(FrameworkThermalThresholdSetting setting, string parameterName) + { + if (!setting.Temperature.HasValue) + { + throw new ArgumentOutOfRangeException(parameterName, setting, "A threshold that is being set must carry a temperature."); + } + + double degreesCelsius = setting.Temperature.Value.DegreesCelsius; + + if (double.IsNaN(degreesCelsius) || double.IsInfinity(degreesCelsius)) + { + throw new ArgumentOutOfRangeException(parameterName, degreesCelsius, "The threshold temperature must be a finite value."); + } + + double rounded = Math.Round(degreesCelsius, MidpointRounding.AwayFromZero); + + if (rounded < 1 || rounded > int.MaxValue) + { + throw new ArgumentOutOfRangeException(parameterName, degreesCelsius, "The threshold temperature must round to at least 1 degree Celsius, because the embedded controller reserves zero for disabling a threshold and negative values for keeping the current one. Use FrameworkThermalThresholdSetting.Disable to disable the threshold."); + } + + return (int)rounded; + } + + private FrameworkTemperatureSensorNameSnapshot ReadSensorName(byte sensorIndex) + { + unsafe + { + return Native.NativeMethods.framework_ec_get_temp_sensor_name(HandlePointer, sensorIndex).GetValueOrThrow(); + } + } + + private unsafe Native.FrameworkEcHandle* HandlePointer => (Native.FrameworkEcHandle*)handleAccessor(); +} diff --git a/framework-dotnet/Enums/FrameworkBatteryCutoffState.cs b/framework-dotnet/Enums/FrameworkBatteryCutoffState.cs new file mode 100644 index 0000000..5e5cb19 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkBatteryCutoffState.cs @@ -0,0 +1,22 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the battery cutoff (ship mode) state reported by the embedded controller. +/// +public enum FrameworkBatteryCutoffState +{ + /// + /// The embedded controller did not answer the cutoff query. + /// + Unknown = 0, + + /// + /// The battery is connected and not in ship mode. + /// + NotCutOff = 1, + + /// + /// The battery has been cut off and is in ship mode. + /// + CutOff = 2, +} diff --git a/framework-dotnet/Enums/FrameworkClickForce.cs b/framework-dotnet/Enums/FrameworkClickForce.cs new file mode 100644 index 0000000..41b4136 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkClickForce.cs @@ -0,0 +1,26 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the click force threshold applied to a haptic touchpad. +/// +/// +/// The click force is write-only: the firmware never answers a feature report for it, so the +/// current value cannot be read back. +/// +public enum FrameworkClickForce +{ + /// + /// The touchpad registers a click at a low actuation force. + /// + Low = 1, + + /// + /// The touchpad registers a click at a medium actuation force. + /// + Medium = 2, + + /// + /// The touchpad registers a click at a high actuation force. + /// + High = 3, +} diff --git a/framework-dotnet/Enums/FrameworkEcProtocolFlag.cs b/framework-dotnet/Enums/FrameworkEcProtocolFlag.cs new file mode 100644 index 0000000..08b9813 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkEcProtocolFlag.cs @@ -0,0 +1,13 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the optional host command protocol capabilities reported by the embedded controller. +/// +[System.Flags] +public enum FrameworkEcProtocolFlag : uint +{ + /// + /// The controller can report an in-progress result for long-running host commands. + /// + InProgressSupported = 0x01, +} diff --git a/framework-dotnet/Enums/FrameworkEcResetFlag.cs b/framework-dotnet/Enums/FrameworkEcResetFlag.cs new file mode 100644 index 0000000..cfeeb73 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkEcResetFlag.cs @@ -0,0 +1,123 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the reasons recorded by the embedded controller for its most recent reset. +/// +[System.Flags] +public enum FrameworkEcResetFlag : uint +{ + /// + /// The reset had a cause that is not covered by any other flag. + /// + Other = 0x00000001, + + /// + /// The reset was triggered through the reset pin. + /// + ResetPin = 0x00000002, + + /// + /// The reset was caused by a brownout. + /// + Brownout = 0x00000004, + + /// + /// The reset was caused by the supply being powered on. + /// + PowerOn = 0x00000008, + + /// + /// The reset was caused by the watchdog timer. + /// + Watchdog = 0x00000010, + + /// + /// The reset was a soft reset requested in firmware. + /// + Soft = 0x00000020, + + /// + /// The reset resumed the controller from hibernation. + /// + Hibernate = 0x00000040, + + /// + /// The reset was triggered by a real-time clock alarm. + /// + RtcAlarm = 0x00000080, + + /// + /// The reset was triggered through a wake pin. + /// + WakePin = 0x00000100, + + /// + /// The reset was caused by a low battery condition. + /// + LowBattery = 0x00000200, + + /// + /// The reset was caused by a jump between firmware images. + /// + Sysjump = 0x00000400, + + /// + /// The reset was a hard reset. + /// + Hard = 0x00000800, + + /// + /// The application processor was off across the reset. + /// + ApOff = 0x00001000, + + /// + /// The reset flags were preserved across the reset. + /// + Preserved = 0x00002000, + + /// + /// The reset resumed the controller from a USB resume event. + /// + UsbResume = 0x00004000, + + /// + /// The reset was triggered by the debug detection module. + /// + Rdd = 0x00008000, + + /// + /// The reset was triggered by the reset box. + /// + Rbox = 0x00010000, + + /// + /// The reset was triggered by a security event. + /// + Security = 0x00020000, + + /// + /// The reset was triggered by the application processor watchdog. + /// + ApWatchdog = 0x00040000, + + /// + /// The controller was asked to stay in the read-only image after the reset. + /// + StayInRo = 0x00080000, + + /// + /// The reset was caused by early firmware selection. + /// + Efs = 0x00100000, + + /// + /// The application processor was idle across the reset. + /// + ApIdle = 0x00200000, + + /// + /// The reset was the initial power-up of the controller. + /// + InitialPwr = 0x00400000, +} diff --git a/framework-dotnet/Enums/FrameworkEcSysinfoFlag.cs b/framework-dotnet/Enums/FrameworkEcSysinfoFlag.cs new file mode 100644 index 0000000..799de80 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkEcSysinfoFlag.cs @@ -0,0 +1,38 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the system information flags reported by the embedded controller. +/// +[System.Flags] +public enum FrameworkEcSysinfoFlag : uint +{ + /// + /// Write protect is asserted and debug features are disabled. + /// + Locked = 0x01, + + /// + /// The embedded controller is locked even though write protect is deasserted. + /// + ForceLocked = 0x02, + + /// + /// Jumping to another firmware image is enabled. + /// + JumpEnabled = 0x04, + + /// + /// The embedded controller jumped to the image it is currently running. + /// + JumpedToCurrentImage = 0x08, + + /// + /// The embedded controller will reboot when the host shuts down. + /// + RebootAtShutdown = 0x10, + + /// + /// The system is in manual recovery mode. + /// + InManualRecovery = 0x20, +} diff --git a/framework-dotnet/Enums/FrameworkPdApplication.cs b/framework-dotnet/Enums/FrameworkPdApplication.cs new file mode 100644 index 0000000..2098ad4 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkPdApplication.cs @@ -0,0 +1,27 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the application a USB Power Delivery controller firmware targets. +/// +public enum FrameworkPdApplication +{ + /// + /// The firmware targets a notebook application. + /// + Notebook = 0, + + /// + /// The firmware targets a monitor application. + /// + Monitor = 1, + + /// + /// The firmware targets the AA application variant. + /// + AA = 2, + + /// + /// The controller reported an application value that is not valid. + /// + Invalid = 3, +} diff --git a/framework-dotnet/Enums/FrameworkPdFwMode.cs b/framework-dotnet/Enums/FrameworkPdFwMode.cs new file mode 100644 index 0000000..16ad86a --- /dev/null +++ b/framework-dotnet/Enums/FrameworkPdFwMode.cs @@ -0,0 +1,27 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents which firmware image a USB Power Delivery controller is currently running. +/// +public enum FrameworkPdFwMode +{ + /// + /// The running firmware image could not be determined. + /// + Unknown = -1, + + /// + /// The controller is running its boot loader. + /// + BootLoader = 0, + + /// + /// The controller is running the backup firmware image. + /// + BackupFw = 1, + + /// + /// The controller is running the main firmware image. + /// + MainFw = 2, +} diff --git a/framework-dotnet/Enums/FrameworkPort80Event.cs b/framework-dotnet/Enums/FrameworkPort80Event.cs new file mode 100644 index 0000000..44c9733 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkPort80Event.cs @@ -0,0 +1,21 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the marker codes the embedded controller inserts into the port 80 history buffer. +/// +/// +/// These are sentinel entries rather than port 80 codes emitted by the host firmware, so an +/// entry matching one of these values marks a boundary in the history instead of a POST code. +/// +public enum FrameworkPort80Event : ushort +{ + /// + /// The system resumed from a low-power state at this point in the history. + /// + Resume = 0x1001, + + /// + /// The system was reset at this point in the history. + /// + Reset = 0x1002, +} diff --git a/framework-dotnet/Enums/FrameworkPowerDeliveryControllerSlot.cs b/framework-dotnet/Enums/FrameworkPowerDeliveryControllerSlot.cs new file mode 100644 index 0000000..b6868e4 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkPowerDeliveryControllerSlot.cs @@ -0,0 +1,27 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Identifies a fixed USB Power Delivery controller slot reported by the embedded controller. +/// +/// +/// The slot order is fixed by the native probe order and never varies: index 0 is , index 1 is and index 2 is . +/// Framework laptops populate and ; Framework Desktop populates only. +/// Because the populated slots are not contiguous across platform families, always test the presence flag on a controller before reading its firmware versions. +/// +public enum FrameworkPowerDeliveryControllerSlot +{ + /// + /// The controller driving the right-hand pair of USB-C ports, reported in slot index 0. + /// + Right01 = 0, + + /// + /// The controller driving the left-hand pair of USB-C ports, reported in slot index 1. + /// + Left23 = 1, + + /// + /// The rear controller of a Framework Desktop, reported in slot index 2. + /// + Back = 2, +} diff --git a/framework-dotnet/Enums/FrameworkTemperatureSensorType.cs b/framework-dotnet/Enums/FrameworkTemperatureSensorType.cs new file mode 100644 index 0000000..fcaccdf --- /dev/null +++ b/framework-dotnet/Enums/FrameworkTemperatureSensorType.cs @@ -0,0 +1,38 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Identifies how the embedded controller classifies a temperature sensor slot. +/// +/// +/// The values mirror the raw EC sensor-type tag reported alongside a temperature sensor's firmware +/// name. Firmware that reports a tag this version does not recognize surfaces as the raw numeric +/// value cast onto this enumeration, so callers should treat unnamed values as "unclassified" +/// rather than assuming the set is closed. +/// +public enum FrameworkTemperatureSensorType : byte +{ + /// + /// The slot carries no usable sensor and the embedded controller ignores it. + /// + Ignored = 0, + + /// + /// The sensor measures the CPU or SoC package. + /// + Cpu = 1, + + /// + /// The sensor measures the mainboard. + /// + Board = 2, + + /// + /// The sensor measures the chassis skin. + /// + Case = 3, + + /// + /// The sensor measures the battery pack. + /// + Battery = 4, +} diff --git a/framework-dotnet/Enums/FrameworkThermalThresholdAction.cs b/framework-dotnet/Enums/FrameworkThermalThresholdAction.cs new file mode 100644 index 0000000..04289c3 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkThermalThresholdAction.cs @@ -0,0 +1,38 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Identifies what a thermal threshold write is asked to do to one individual threshold. +/// +/// +/// +/// Writing thermal thresholds is a read-modify-write against embedded controller firmware, and the +/// native ABI encodes the caller's intent in the sign of each argument: a negative value keeps the +/// threshold exactly as firmware currently holds it, zero disables the threshold, and a positive +/// value is a temperature in degrees Celsius. This enumeration names those three intents so that +/// "keep the current value" and "disable the threshold" can never be confused with one another. +/// +/// +/// Note that the read path uses a different convention: a disabled threshold reads back as -273 +/// degrees Celsius, so a reader must consult the enabled mask instead of the reported temperature. +/// +/// +public enum FrameworkThermalThresholdAction +{ + /// + /// Leaves the threshold exactly as embedded controller firmware currently holds it, whether it + /// is enabled or disabled. This is the default so that an unspecified threshold is never + /// changed by accident. + /// + KeepCurrent = 0, + + /// + /// Disables the threshold, so that the embedded controller stops acting on it entirely. A + /// subsequent read reports the threshold's enabled bit as clear. + /// + Disable = 1, + + /// + /// Enables the threshold and sets it to an explicit temperature. + /// + Set = 2, +} diff --git a/framework-dotnet/Enums/FrameworkThermalThresholdFlag.cs b/framework-dotnet/Enums/FrameworkThermalThresholdFlag.cs new file mode 100644 index 0000000..b004622 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkThermalThresholdFlag.cs @@ -0,0 +1,52 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents which thermal thresholds the embedded controller firmware currently has enabled. +/// +/// +/// A clear bit means the firmware has that threshold disabled. Always test this mask instead of +/// comparing the reported Celsius value, because a disabled threshold reads back as -273. +/// +[System.Flags] +public enum FrameworkThermalThresholdFlag : uint +{ + /// + /// The warning threshold is enabled. + /// + Warn = 0x01, + + /// + /// The high-temperature threshold is enabled. + /// + High = 0x02, + + /// + /// The halt threshold is enabled. + /// + Halt = 0x04, + + /// + /// The warning release threshold is enabled. + /// + WarnRelease = 0x08, + + /// + /// The high-temperature release threshold is enabled. + /// + HighRelease = 0x10, + + /// + /// The halt release threshold is enabled. + /// + HaltRelease = 0x20, + + /// + /// The fan-off threshold is enabled. + /// + FanOff = 0x40, + + /// + /// The fan-maximum threshold is enabled. + /// + FanMax = 0x80, +} diff --git a/framework-dotnet/Enums/FrameworkUsbChargingType.cs b/framework-dotnet/Enums/FrameworkUsbChargingType.cs new file mode 100644 index 0000000..b553e98 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkUsbChargingType.cs @@ -0,0 +1,57 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents how an attached charger is supplying power to a USB-C port. +/// +public enum FrameworkUsbChargingType +{ + /// + /// No charger is supplying power. + /// + None = 0, + + /// + /// Power is supplied through a negotiated USB Power Delivery contract. + /// + Pd = 1, + + /// + /// Power is supplied through USB Type-C current advertisement. + /// + TypeC = 2, + + /// + /// Power is supplied through a proprietary charging scheme. + /// + Proprietary = 3, + + /// + /// Power is supplied by a USB Battery Charging 1.2 dedicated charging port. + /// + Bc12Dcp = 4, + + /// + /// Power is supplied by a USB Battery Charging 1.2 charging downstream port. + /// + Bc12Cdp = 5, + + /// + /// Power is supplied by a USB Battery Charging 1.2 standard downstream port. + /// + Bc12Sdp = 6, + + /// + /// Power is supplied by a charger that does not match any of the classified types. + /// + Other = 7, + + /// + /// Power is supplied over VBUS without a recognised charging protocol. + /// + VBus = 8, + + /// + /// The charging type could not be determined. + /// + Unknown = 9, +} diff --git a/framework-dotnet/Enums/FrameworkUsbPowerRole.cs b/framework-dotnet/Enums/FrameworkUsbPowerRole.cs new file mode 100644 index 0000000..19fc7a7 --- /dev/null +++ b/framework-dotnet/Enums/FrameworkUsbPowerRole.cs @@ -0,0 +1,27 @@ +namespace FrameworkDotnet.Enums; + +/// +/// Represents the power role a USB Power Delivery port has negotiated. +/// +public enum FrameworkUsbPowerRole +{ + /// + /// Nothing is attached to the port. + /// + Disconnected = 0, + + /// + /// The port is providing power to the attached device. + /// + Source = 1, + + /// + /// The port is consuming power from the attached device. + /// + Sink = 2, + + /// + /// The port is a sink but is not currently drawing charge. + /// + SinkNotCharging = 3, +} diff --git a/framework-dotnet/Exceptions/FrameworkStatusException.cs b/framework-dotnet/Exceptions/FrameworkStatusException.cs index 14252cd..3581085 100644 --- a/framework-dotnet/Exceptions/FrameworkStatusException.cs +++ b/framework-dotnet/Exceptions/FrameworkStatusException.cs @@ -59,6 +59,8 @@ internal static FrameworkStatusException GetCorrectException(Framework.System.In return new FrameworkUnknownResponseCodeStatusException(); case Framework.System.Interop.FrameworkStatusCode.DataUnavailable: return new FrameworkDataUnavailableStatusException(); + case Framework.System.Interop.FrameworkStatusCode.NotSupported: + return new FrameworkNotSupportedStatusException(); default: throw new ArgumentOutOfRangeException(nameof(statusCode), statusCode, "Unhandled status code."); } diff --git a/framework-dotnet/Exceptions/StatusCodes/FrameworkNotSupportedStatusException.cs b/framework-dotnet/Exceptions/StatusCodes/FrameworkNotSupportedStatusException.cs new file mode 100644 index 0000000..eb931d3 --- /dev/null +++ b/framework-dotnet/Exceptions/StatusCodes/FrameworkNotSupportedStatusException.cs @@ -0,0 +1,28 @@ +using Framework.System.Interop; + +namespace FrameworkDotnet.Exceptions.StatusCodes; + +/// +/// Represents a native failure, raised when the +/// requested capability is not compiled in for this platform. +/// +/// +/// +/// The condition is permanent for this build and host operating system: the native library +/// contains no implementation of the requested capability, so retrying the call can never +/// succeed. This is the distinction from , +/// which reports a transient read failure where the capability does exist but the value could +/// not be obtained on this attempt and a later attempt may succeed. +/// +/// +/// At present only the NVMe drive version readback (framework_get_nvme_version) reports +/// this status, and only on non-Linux hosts, because the underlying NVMe admin passthrough +/// ioctl is gated to Linux upstream. +/// +/// +public class FrameworkNotSupportedStatusException : FrameworkStatusCodeException +{ + internal FrameworkNotSupportedStatusException() : base(FrameworkStatusCode.NotSupported) + { + } +} diff --git a/framework-dotnet/FrameworkEcConnection.cs b/framework-dotnet/FrameworkEcConnection.cs index 8fbdcfe..bf548dd 100644 --- a/framework-dotnet/FrameworkEcConnection.cs +++ b/framework-dotnet/FrameworkEcConnection.cs @@ -23,8 +23,40 @@ public sealed class FrameworkEcConnection : SafeHandleZeroOrMinusOneIsInvalid, I private FrameworkEcConnection() : base(true) { + // The facets borrow the handle through this accessor rather than capturing it, so every + // facet call runs the same disposal check the members on this type do. + Func handleAccessor = GetHandlePointer; + + Diagnostics = new Ec.FrameworkEcDiagnostics(handleAccessor); + Gpio = new Ec.FrameworkEcGpio(handleAccessor); + Thermal = new Ec.FrameworkEcThermalControl(handleAccessor); + Battery = new Ec.FrameworkEcBattery(handleAccessor); + PowerDelivery = new Ec.FrameworkEcPowerDelivery(handleAccessor); + Input = new Ec.FrameworkEcInput(handleAccessor); + PowerManagement = new Ec.FrameworkEcPowerManagement(handleAccessor); } + /// + public IFrameworkEcDiagnostics Diagnostics { get; } + + /// + public IFrameworkEcGpio Gpio { get; } + + /// + public IFrameworkEcThermalControl Thermal { get; } + + /// + public IFrameworkEcBattery Battery { get; } + + /// + public IFrameworkEcPowerDelivery PowerDelivery { get; } + + /// + public IFrameworkEcInput Input { get; } + + /// + public IFrameworkEcPowerManagement PowerManagement { get; } + /// public FrameworkEcDriver GetActiveDriver() { @@ -437,4 +469,9 @@ private unsafe Native.FrameworkEcHandle* HandlePointer return (Native.FrameworkEcHandle*)handle; } } + + private unsafe IntPtr GetHandlePointer() + { + return (IntPtr)HandlePointer; + } } diff --git a/framework-dotnet/FrameworkPeripherals.cs b/framework-dotnet/FrameworkPeripherals.cs new file mode 100644 index 0000000..45d45b2 --- /dev/null +++ b/framework-dotnet/FrameworkPeripherals.cs @@ -0,0 +1,183 @@ +using System; +using System.Text; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Interfaces; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +using Native = Framework.System.Interop; + +namespace FrameworkDotnet; + +/// +/// Provides a safe entry point for the Framework peripherals that are reached over HID and USB directly. +/// +/// +/// +/// The class holds no state and owns no native resource, so instances are cheap to create and every member +/// is safe to call from any thread. The underlying devices are not: two threads issuing device input/output +/// to the same peripheral at once contend for the same interface, so serialise calls that target one device +/// and never call concurrently with itself. +/// +/// +/// No member touches the embedded controller, so none of them needs an +/// and none of them is affected by embedded controller driver availability. +/// +/// +public class FrameworkPeripherals : IFrameworkPeripherals +{ + /// + public FrameworkStylusBatterySnapshot GetStylusBattery() + { + unsafe + { + return Native.NativeMethods.framework_get_stylus_battery().GetValueOrThrow(); + } + } + + /// + public void SetTouchscreenEnabled(bool enabled) + { + unsafe + { + Native.NativeMethods.framework_touchscreen_enable(enabled).ThrowIfFailure(); + } + } + + /// + public void SetTouchpadHapticIntensity(Ratio intensity) + { + byte level = ToHapticIntensityLevel(intensity); + + unsafe + { + Native.NativeMethods.framework_touchpad_set_haptic_intensity(level).ThrowIfFailure(); + } + } + + /// + public void SetTouchpadClickForce(FrameworkClickForce force) + { + if (!Enum.IsDefined(force)) + { + throw new ArgumentOutOfRangeException(nameof(force), force, "The click force must be one of the defined threshold levels; the touchpad firmware accepts no other value."); + } + + unsafe + { + Native.NativeMethods.framework_touchpad_set_click_force((Native.FrameworkClickForce)(int)force).ThrowIfFailure(); + } + } + + /// + public FrameworkPeripheralVersionsSnapshot GetCameraVersions() + { + unsafe + { + return Native.NativeMethods.framework_get_camera_versions().GetValueOrThrow(); + } + } + + /// + [FrameworkPlatformSpecific(FrameworkPlatformFamily.Framework16, Message = "Input modules, including the LED matrix, are specific to Framework Laptop 16. Other platform families report an empty table.")] + public FrameworkPeripheralVersionsSnapshot GetInputModuleVersions() + { + unsafe + { + return Native.NativeMethods.framework_get_input_module_versions().GetValueOrThrow(); + } + } + + /// + public FrameworkPeripheralVersionsSnapshot GetUsbHubVersions() + { + unsafe + { + return Native.NativeMethods.framework_get_usb_hub_versions().GetValueOrThrow(); + } + } + + /// + public FrameworkPeripheralVersionsSnapshot GetAudioCardVersion() + { + unsafe + { + return Native.NativeMethods.framework_get_audio_card_version().GetValueOrThrow(); + } + } + + /// + public FrameworkNvmeVersionSnapshot GetNvmeVersion(string devicePath) + { + ArgumentException.ThrowIfNullOrEmpty(devicePath); + + if (devicePath.Contains('\0', StringComparison.Ordinal)) + { + throw new ArgumentException("The NVMe device path must not contain an embedded null character.", nameof(devicePath)); + } + + byte[] pathBytes = Encoding.UTF8.GetBytes(devicePath); + + unsafe + { + fixed (byte* pathPointer = pathBytes) + { + return Native.NativeMethods.framework_get_nvme_version(pathPointer, pathBytes.Length).GetValueOrThrow(); + } + } + } + + /// + /// Converts a requested haptic intensity into the byte level the touchpad firmware accepts. + /// + /// The requested intensity. + /// The whole percentage to send to the firmware. + /// + /// The HID descriptor advertises a logical range of 0 to 100, but the haptic firmware implements only + /// the five steps in and rejects anything else. Validating + /// here turns a firmware rejection, which is indistinguishable from a missing touchpad at the native + /// boundary, into an argument error that names the problem. + /// + /// Thrown when is not finite, or is not one of the five supported steps. + private static byte ToHapticIntensityLevel(Ratio intensity) + { + double percent = intensity.Percent; + + if (double.IsNaN(percent) || double.IsInfinity(percent)) + { + throw new ArgumentOutOfRangeException(nameof(intensity), percent, "The touchpad haptic intensity must be a finite percentage."); + } + + double rounded = Math.Round(percent); + + if (Math.Abs(percent - rounded) <= HapticIntensityTolerancePercent && rounded >= 0.0 && rounded <= 100.0) + { + byte level = (byte)rounded; + + if (Array.IndexOf(HapticIntensityLevelsPercent, level) >= 0) + { + return level; + } + } + + throw new ArgumentOutOfRangeException(nameof(intensity), percent, "The touchpad haptic intensity must be 0, 25, 50, 75 or 100 percent. The haptic firmware implements only those five steps and rejects every other value."); + } + + /// + /// The five haptic intensity steps, in whole percent, that the touchpad firmware implements. + /// + private static readonly byte[] HapticIntensityLevelsPercent = [0, 25, 50, 75, 100]; + + /// + /// The tolerance, in percent, applied when matching a requested intensity onto a supported step. + /// + /// + /// A built from a decimal fraction rather than a percentage can land a fraction of a + /// percent away from the intended whole number. The tolerance absorbs that rounding without accepting a + /// value that was genuinely meant to be a different step. + /// + private const double HapticIntensityTolerancePercent = 1e-6; +} diff --git a/framework-dotnet/Generated/FrameworkPdAppVersion.cs b/framework-dotnet/Generated/FrameworkPdAppVersion.cs new file mode 100644 index 0000000..f0d4828 --- /dev/null +++ b/framework-dotnet/Generated/FrameworkPdAppVersion.cs @@ -0,0 +1,27 @@ +using ManagedPowerDeliveryApplicationVersionSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryApplicationVersionSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkPdAppVersion +{ + internal readonly ManagedPowerDeliveryApplicationVersionSnapshot ToManagedSnapshot() + { + return new ManagedPowerDeliveryApplicationVersionSnapshot( + ToApplication(), + major, + minor, + circuit); + } + + private readonly FrameworkDotnet.Enums.FrameworkPdApplication ToApplication() + { + return application switch + { + FrameworkPdApplication.Notebook => FrameworkDotnet.Enums.FrameworkPdApplication.Notebook, + FrameworkPdApplication.Monitor => FrameworkDotnet.Enums.FrameworkPdApplication.Monitor, + FrameworkPdApplication.AA => FrameworkDotnet.Enums.FrameworkPdApplication.AA, + FrameworkPdApplication.Invalid => FrameworkDotnet.Enums.FrameworkPdApplication.Invalid, + _ => FrameworkDotnet.Enums.FrameworkPdApplication.Invalid, + }; + } +} diff --git a/framework-dotnet/Generated/FrameworkPdBaseVersion.cs b/framework-dotnet/Generated/FrameworkPdBaseVersion.cs new file mode 100644 index 0000000..eaa54cc --- /dev/null +++ b/framework-dotnet/Generated/FrameworkPdBaseVersion.cs @@ -0,0 +1,15 @@ +using ManagedPowerDeliveryBaseVersionSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryBaseVersionSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkPdBaseVersion +{ + internal readonly ManagedPowerDeliveryBaseVersionSnapshot ToManagedSnapshot() + { + return new ManagedPowerDeliveryBaseVersionSnapshot( + major, + minor, + patch, + build_number); + } +} diff --git a/framework-dotnet/Generated/FrameworkPdControllerFirmwares.cs b/framework-dotnet/Generated/FrameworkPdControllerFirmwares.cs new file mode 100644 index 0000000..31396a9 --- /dev/null +++ b/framework-dotnet/Generated/FrameworkPdControllerFirmwares.cs @@ -0,0 +1,30 @@ +using ManagedPowerDeliveryControllerFirmwareSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryControllerFirmwareSnapshot; +using ManagedPowerDeliveryControllerSlot = FrameworkDotnet.Enums.FrameworkPowerDeliveryControllerSlot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkPdControllerFirmwares +{ + internal readonly ManagedPowerDeliveryControllerFirmwareSnapshot ToManagedSnapshot(ManagedPowerDeliveryControllerSlot slot) + { + return new ManagedPowerDeliveryControllerFirmwareSnapshot( + slot, + present != 0, + ToFirmwareMode(), + bootloader.ToManagedSnapshot(), + backup_fw.ToManagedSnapshot(), + main_fw.ToManagedSnapshot()); + } + + private readonly FrameworkDotnet.Enums.FrameworkPdFwMode ToFirmwareMode() + { + return active_fw switch + { + FrameworkPdFwMode.Unknown => FrameworkDotnet.Enums.FrameworkPdFwMode.Unknown, + FrameworkPdFwMode.BootLoader => FrameworkDotnet.Enums.FrameworkPdFwMode.BootLoader, + FrameworkPdFwMode.BackupFw => FrameworkDotnet.Enums.FrameworkPdFwMode.BackupFw, + FrameworkPdFwMode.MainFw => FrameworkDotnet.Enums.FrameworkPdFwMode.MainFw, + _ => FrameworkDotnet.Enums.FrameworkPdFwMode.Unknown, + }; + } +} diff --git a/framework-dotnet/Generated/FrameworkPdControllerVersion.cs b/framework-dotnet/Generated/FrameworkPdControllerVersion.cs new file mode 100644 index 0000000..c3325cf --- /dev/null +++ b/framework-dotnet/Generated/FrameworkPdControllerVersion.cs @@ -0,0 +1,13 @@ +using ManagedPowerDeliveryControllerImageSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryControllerImageSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkPdControllerVersion +{ + internal readonly ManagedPowerDeliveryControllerImageSnapshot ToManagedSnapshot() + { + return new ManagedPowerDeliveryControllerImageSnapshot( + @base.ToManagedSnapshot(), + app.ToManagedSnapshot()); + } +} diff --git a/framework-dotnet/Generated/FrameworkPeripheralVersion.cs b/framework-dotnet/Generated/FrameworkPeripheralVersion.cs new file mode 100644 index 0000000..19c771b --- /dev/null +++ b/framework-dotnet/Generated/FrameworkPeripheralVersion.cs @@ -0,0 +1,30 @@ +using ManagedPeripheralVersionSnapshot = FrameworkDotnet.Snapshots.FrameworkPeripheralVersionSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkPeripheralVersion +{ + /// + /// Copies one peripheral slot into managed memory without releasing the native product name buffer. + /// + /// The zero-based fixed slot this record occupies in the enclosing result. + /// A managed snapshot holding its own copy of every value in the slot. + /// + /// The product_name buffer is owned by the enclosing + /// and must be released as part of that aggregate through framework_peripheral_versions_free. + /// This method therefore copies the string with and must + /// never free the buffer itself. + /// + internal readonly ManagedPeripheralVersionSnapshot ToManagedSnapshot(int slotIndex) + { + return new ManagedPeripheralVersionSnapshot( + slotIndex, + present != 0, + version_major, + version_minor, + version_sub_minor, + vendor_id, + product_id, + product_name.ToUtf8String()); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcAdcResult.cs b/framework-dotnet/Generated/Result/FrameworkEcAdcResult.cs new file mode 100644 index 0000000..68d5c20 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcAdcResult.cs @@ -0,0 +1,23 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcAdcResult +{ + /// + /// Returns the raw analog-to-digital converter count, or throws when the read failed. + /// + /// + /// The channel field is only an echo of the requested channel and carries no + /// information the caller does not already have, so it is not surfaced. + /// + internal readonly int GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return value; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcApThrottleResult.cs b/framework-dotnet/Generated/Result/FrameworkEcApThrottleResult.cs new file mode 100644 index 0000000..3bd8b7c --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcApThrottleResult.cs @@ -0,0 +1,19 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcApThrottleResult +{ + internal readonly FrameworkEcApThrottleSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkEcApThrottleSnapshot( + soft_throttled != 0, + hard_throttled != 0); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcBatteryAuthResult.cs b/framework-dotnet/Generated/Result/FrameworkEcBatteryAuthResult.cs new file mode 100644 index 0000000..1698676 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcBatteryAuthResult.cs @@ -0,0 +1,23 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcBatteryAuthResult +{ + /// + /// Returns whether the pack answered the challenge correctly, throwing only when the exchange itself failed. + /// + /// + /// A success status with authenticated == 0 means the pack answered and failed the challenge. That is a + /// legitimate negative answer, not an error, so it is returned as . + /// + internal readonly bool GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return authenticated != 0; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcBatteryCutoffResult.cs b/framework-dotnet/Generated/Result/FrameworkEcBatteryCutoffResult.cs new file mode 100644 index 0000000..c2ebc2c --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcBatteryCutoffResult.cs @@ -0,0 +1,26 @@ +using System; + +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcBatteryCutoffResult +{ + internal readonly FrameworkBatteryCutoffState GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + switch (state) + { + case FrameworkBatteryCutoffState.Unknown: + case FrameworkBatteryCutoffState.NotCutOff: + case FrameworkBatteryCutoffState.CutOff: + return state; + default: + throw new ArgumentOutOfRangeException(nameof(state), state, "Unhandled battery cutoff state."); + } + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcChargingStateResult.cs b/framework-dotnet/Generated/Result/FrameworkEcChargingStateResult.cs new file mode 100644 index 0000000..22991db --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcChargingStateResult.cs @@ -0,0 +1,17 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcChargingStateResult +{ + internal readonly FrameworkChargingStateSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkChargingStateSnapshot(is_charging != 0, ac_present != 0); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcCommandSupportResult.cs b/framework-dotnet/Generated/Result/FrameworkEcCommandSupportResult.cs new file mode 100644 index 0000000..81b5718 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcCommandSupportResult.cs @@ -0,0 +1,24 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcCommandSupportResult +{ + /// + /// Returns whether the embedded controller implements the probed command at the probed + /// version, or throws when the probe itself failed. + /// + /// + /// The command and version fields are only echoes of the probe arguments, so + /// they are not surfaced. + /// + internal readonly bool GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return supported != 0; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcFanCountResult.cs b/framework-dotnet/Generated/Result/FrameworkEcFanCountResult.cs new file mode 100644 index 0000000..0ace572 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcFanCountResult.cs @@ -0,0 +1,16 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcFanCountResult +{ + internal readonly byte GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return fan_count; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcGpioCountResult.cs b/framework-dotnet/Generated/Result/FrameworkEcGpioCountResult.cs new file mode 100644 index 0000000..1022bf5 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcGpioCountResult.cs @@ -0,0 +1,16 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcGpioCountResult +{ + internal readonly int GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return count; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcGpioInfoResult.cs b/framework-dotnet/Generated/Result/FrameworkEcGpioInfoResult.cs new file mode 100644 index 0000000..521487b --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcGpioInfoResult.cs @@ -0,0 +1,30 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcGpioInfoResult +{ + internal readonly FrameworkEcGpioSnapshot GetValueOrThrow() + { + var nameBuffer = name; + + if (status.IsFailure) + { + try + { + throw FrameworkStatusException.GetCorrectException(status); + } + finally + { + nameBuffer.Free(); + } + } + + return new FrameworkEcGpioSnapshot( + index, + nameBuffer.ToUtf8StringAndFree(), + value != 0, + flags); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcGpioValueResult.cs b/framework-dotnet/Generated/Result/FrameworkEcGpioValueResult.cs new file mode 100644 index 0000000..20ec520 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcGpioValueResult.cs @@ -0,0 +1,16 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcGpioValueResult +{ + internal readonly bool GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return value != 0; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcGpuSerialResult.cs b/framework-dotnet/Generated/Result/FrameworkEcGpuSerialResult.cs new file mode 100644 index 0000000..af30d36 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcGpuSerialResult.cs @@ -0,0 +1,25 @@ +using FrameworkDotnet.Exceptions; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcGpuSerialResult +{ + internal readonly string GetValueOrThrow() + { + var value = serial; + + if (status.IsFailure) + { + try + { + throw FrameworkStatusException.GetCorrectException(status); + } + finally + { + value.Free(); + } + } + + return value.ToUtf8StringAndFree(); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcHelloResult.cs b/framework-dotnet/Generated/Result/FrameworkEcHelloResult.cs new file mode 100644 index 0000000..b53c74d --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcHelloResult.cs @@ -0,0 +1,19 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcHelloResult +{ + internal readonly FrameworkEcHelloSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkEcHelloSnapshot( + out_data, + is_expected != 0); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcHibernateDelayResult.cs b/framework-dotnet/Generated/Result/FrameworkEcHibernateDelayResult.cs new file mode 100644 index 0000000..5c46566 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcHibernateDelayResult.cs @@ -0,0 +1,18 @@ +using FrameworkDotnet.Exceptions; + +using UnitsNet; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcHibernateDelayResult +{ + internal readonly Duration GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return Duration.FromSeconds((double)seconds); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcPanicInfoResult.cs b/framework-dotnet/Generated/Result/FrameworkEcPanicInfoResult.cs new file mode 100644 index 0000000..b40acaa --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcPanicInfoResult.cs @@ -0,0 +1,33 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcPanicInfoResult +{ + internal readonly FrameworkEcPanicInfoSnapshot GetValueOrThrow() + { + var buffer = data; + + if (status.IsFailure) + { + try + { + throw FrameworkStatusException.GetCorrectException(status); + } + finally + { + buffer.Free(); + } + } + + return new FrameworkEcPanicInfoSnapshot( + buffer.ToArrayAndFree(), + arch, + struct_version, + flags, + is_valid != 0, + struct_size, + magic); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcPdControllerVersionsResult.cs b/framework-dotnet/Generated/Result/FrameworkEcPdControllerVersionsResult.cs new file mode 100644 index 0000000..7be163b --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcPdControllerVersionsResult.cs @@ -0,0 +1,23 @@ +using FrameworkDotnet.Exceptions; + +using ManagedPowerDeliveryControllerSlot = FrameworkDotnet.Enums.FrameworkPowerDeliveryControllerSlot; +using ManagedPowerDeliveryControllerVersionsSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryControllerVersionsSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcPdControllerVersionsResult +{ + internal readonly ManagedPowerDeliveryControllerVersionsSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new ManagedPowerDeliveryControllerVersionsSnapshot( + controller_count, + controller_0.ToManagedSnapshot(ManagedPowerDeliveryControllerSlot.Right01), + controller_1.ToManagedSnapshot(ManagedPowerDeliveryControllerSlot.Left23), + controller_2.ToManagedSnapshot(ManagedPowerDeliveryControllerSlot.Back)); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcPdPowerInfoResult.cs b/framework-dotnet/Generated/Result/FrameworkEcPdPowerInfoResult.cs new file mode 100644 index 0000000..83d193a --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcPdPowerInfoResult.cs @@ -0,0 +1,61 @@ +using System; + +using FrameworkDotnet.Exceptions; + +using UnitsNet; + +using ManagedPowerDeliveryPowerInfoSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryPowerInfoSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcPdPowerInfoResult +{ + internal readonly ManagedPowerDeliveryPowerInfoSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new ManagedPowerDeliveryPowerInfoSnapshot( + port, + dualrole != 0, + ToPowerRole(), + ToChargingType(), + ElectricPotential.FromMillivolts(voltage_max_mv), + ElectricPotential.FromMillivolts(voltage_now_mv), + ElectricCurrent.FromMilliamperes(current_max_ma), + ElectricCurrent.FromMilliamperes(current_lim_ma), + Power.FromMicrowatts(max_power_uw)); + } + + private readonly FrameworkDotnet.Enums.FrameworkUsbPowerRole ToPowerRole() + { + return role switch + { + FrameworkUsbPowerRole.Disconnected => FrameworkDotnet.Enums.FrameworkUsbPowerRole.Disconnected, + FrameworkUsbPowerRole.Source => FrameworkDotnet.Enums.FrameworkUsbPowerRole.Source, + FrameworkUsbPowerRole.Sink => FrameworkDotnet.Enums.FrameworkUsbPowerRole.Sink, + FrameworkUsbPowerRole.SinkNotCharging => FrameworkDotnet.Enums.FrameworkUsbPowerRole.SinkNotCharging, + _ => throw new ArgumentOutOfRangeException(nameof(role), role, "The native layer reported a Power Delivery power role that is not recognized by the managed API."), + }; + } + + private readonly FrameworkDotnet.Enums.FrameworkUsbChargingType ToChargingType() + { + return charging_type switch + { + FrameworkUsbChargingType.None => FrameworkDotnet.Enums.FrameworkUsbChargingType.None, + FrameworkUsbChargingType.Pd => FrameworkDotnet.Enums.FrameworkUsbChargingType.Pd, + FrameworkUsbChargingType.TypeC => FrameworkDotnet.Enums.FrameworkUsbChargingType.TypeC, + FrameworkUsbChargingType.Proprietary => FrameworkDotnet.Enums.FrameworkUsbChargingType.Proprietary, + FrameworkUsbChargingType.Bc12Dcp => FrameworkDotnet.Enums.FrameworkUsbChargingType.Bc12Dcp, + FrameworkUsbChargingType.Bc12Cdp => FrameworkDotnet.Enums.FrameworkUsbChargingType.Bc12Cdp, + FrameworkUsbChargingType.Bc12Sdp => FrameworkDotnet.Enums.FrameworkUsbChargingType.Bc12Sdp, + FrameworkUsbChargingType.Other => FrameworkDotnet.Enums.FrameworkUsbChargingType.Other, + FrameworkUsbChargingType.VBus => FrameworkDotnet.Enums.FrameworkUsbChargingType.VBus, + FrameworkUsbChargingType.Unknown => FrameworkDotnet.Enums.FrameworkUsbChargingType.Unknown, + _ => FrameworkDotnet.Enums.FrameworkUsbChargingType.Unknown, + }; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcPort80HistoryResult.cs b/framework-dotnet/Generated/Result/FrameworkEcPort80HistoryResult.cs new file mode 100644 index 0000000..8f65c98 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcPort80HistoryResult.cs @@ -0,0 +1,49 @@ +using System.Buffers.Binary; + +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcPort80HistoryResult +{ + private const int EntrySizeInBytes = sizeof(ushort); + + internal readonly FrameworkEcPort80HistorySnapshot GetValueOrThrow() + { + var buffer = codes; + + if (status.IsFailure) + { + try + { + throw FrameworkStatusException.GetCorrectException(status); + } + finally + { + buffer.Free(); + } + } + + try + { + // The native buffer holds history_size little-endian u16 entries in buffer order. + // Decode explicitly rather than reinterpreting the bytes so the layout stays correct + // regardless of host endianness. + var rawBytes = buffer.AsSpan(); + var entryCount = rawBytes.Length / EntrySizeInBytes; + var entries = new ushort[entryCount]; + + for (var index = 0; index < entryCount; index++) + { + entries[index] = BinaryPrimitives.ReadUInt16LittleEndian(rawBytes.Slice(index * EntrySizeInBytes, EntrySizeInBytes)); + } + + return new FrameworkEcPort80HistorySnapshot(writes, history_size, entries); + } + finally + { + buffer.Free(); + } + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcProtocolInfoResult.cs b/framework-dotnet/Generated/Result/FrameworkEcProtocolInfoResult.cs new file mode 100644 index 0000000..5dd9f04 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcProtocolInfoResult.cs @@ -0,0 +1,24 @@ +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcProtocolInfoResult +{ + internal readonly FrameworkEcProtocolInfoSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkEcProtocolInfoSnapshot( + protocol_versions, + Information.FromBytes(max_request_packet_size), + Information.FromBytes(max_response_packet_size), + (FrameworkEcProtocolFlag)flags); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcRetimerVersionResult.cs b/framework-dotnet/Generated/Result/FrameworkEcRetimerVersionResult.cs new file mode 100644 index 0000000..1a9c7a7 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcRetimerVersionResult.cs @@ -0,0 +1,29 @@ +using FrameworkDotnet.Exceptions; + +using ManagedPowerDeliveryRetimerVersionSnapshot = FrameworkDotnet.Snapshots.FrameworkPowerDeliveryRetimerVersionSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcRetimerVersionResult +{ + internal readonly ManagedPowerDeliveryRetimerVersionSnapshot GetValueOrThrow() + { + var buffer = version; + + if (status.IsFailure) + { + try + { + throw FrameworkStatusException.GetCorrectException(status); + } + finally + { + buffer.Free(); + } + } + + return new ManagedPowerDeliveryRetimerVersionSnapshot( + present != 0, + buffer.ToArrayAndFree()); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcSmartBatteryResult.cs b/framework-dotnet/Generated/Result/FrameworkEcSmartBatteryResult.cs new file mode 100644 index 0000000..5a67f4f --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcSmartBatteryResult.cs @@ -0,0 +1,131 @@ +using System; +using System.Buffers.Binary; + +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcSmartBatteryResult +{ + /// + /// Copies the whole Smart Battery record into managed memory and then releases the native record. + /// + /// + /// owns ten byte buffers and must be released as a whole through + /// framework_smart_battery_data_free. The individual buffers must never be freed with + /// framework_byte_buffer_free. Everything is copied into managed memory before the aggregate is released, + /// and the release happens in a finally so it also runs on the failure path. + /// + internal readonly FrameworkSmartBatterySnapshot GetValueOrThrow() + { + FrameworkSmartBatteryData owned = data; + + try + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return ToManagedSnapshot(in owned); + } + finally + { + NativeMethods.framework_smart_battery_data_free(&owned); + } + } + + private static FrameworkSmartBatterySnapshot ToManagedSnapshot(in FrameworkSmartBatteryData value) + { + bool isUnsealed = value.unsealed != 0; + + return new FrameworkSmartBatterySnapshot( + value.mode, + value.serial_number, + value.manufacture_date_raw, + CreateManufactureDate(in value), + value.device_name.ToUtf8String(), + value.manufacturer_name.ToUtf8String(), + value.device_chemistry.ToUtf8String(), + value.firmware_version.ToArray(), + Temperature.FromKelvins(value.temperature_decikelvin / 10.0), + ElectricPotential.FromMillivolts((int)value.voltage_mv), + ElectricPotential.FromMillivolts((int)value.cell_voltage_1_mv), + ElectricPotential.FromMillivolts((int)value.cell_voltage_2_mv), + ElectricPotential.FromMillivolts((int)value.cell_voltage_3_mv), + ElectricPotential.FromMillivolts((int)value.cell_voltage_4_mv), + ElectricCurrent.FromMilliamperes((int)value.current_ma), + ElectricCurrent.FromMilliamperes((int)value.avg_current_ma), + value.cycle_count, + Ratio.FromPercent((int)value.rel_state_of_charge), + Ratio.FromPercent((int)value.abs_state_of_charge), + value.remaining_capacity, + value.full_charge_capacity, + value.design_capacity, + ElectricPotential.FromMillivolts((int)value.design_voltage_mv), + ElectricCurrent.FromMilliamperes((int)value.charging_current_ma), + ElectricPotential.FromMillivolts((int)value.charging_voltage_mv), + value.battery_status, + isUnsealed, + isUnsealed ? CreateStateOfHealth(in value) : null, + isUnsealed ? CreateSafety(in value) : null, + isUnsealed ? CreateLifetimeData(in value) : null); + } + + private static DateOnly? CreateManufactureDate(in FrameworkSmartBatteryData value) + { + int year = value.manufacture_year; + int month = value.manufacture_month; + int day = value.manufacture_day; + + if (year < 1 || year > 9999 || month < 1 || month > 12) + { + return null; + } + + if (day < 1 || day > DateTime.DaysInMonth(year, month)) + { + return null; + } + + return new DateOnly(year, month, day); + } + + private static FrameworkBatteryStateOfHealthSnapshot CreateStateOfHealth(in FrameworkSmartBatteryData value) + { + byte[] raw = value.state_of_health.ToArray(); + + ElectricCharge? chargeCapacity = raw.Length >= 2 + ? ElectricCharge.FromMilliampereHours((int)BinaryPrimitives.ReadUInt16LittleEndian(raw.AsSpan(0, 2))) + : null; + + Energy? energyCapacity = raw.Length >= 4 + ? Energy.FromWattHours(BinaryPrimitives.ReadUInt16LittleEndian(raw.AsSpan(2, 2)) / 100.0) + : null; + + return new FrameworkBatteryStateOfHealthSnapshot(chargeCapacity, energyCapacity, raw); + } + + private static FrameworkBatterySafetySnapshot CreateSafety(in FrameworkSmartBatteryData value) + { + return new FrameworkBatterySafetySnapshot( + value.operation_status, + value.safety_alert, + value.safety_status, + value.pf_alert, + value.pf_status); + } + + private static FrameworkBatteryLifetimeDataSnapshot CreateLifetimeData(in FrameworkSmartBatteryData value) + { + return new FrameworkBatteryLifetimeDataSnapshot( + value.lifetime_1.ToArray(), + value.lifetime_2.ToArray(), + value.lifetime_3.ToArray(), + value.lifetime_4.ToArray(), + value.lifetime_5.ToArray()); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcStandaloneModeResult.cs b/framework-dotnet/Generated/Result/FrameworkEcStandaloneModeResult.cs new file mode 100644 index 0000000..b81788e --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcStandaloneModeResult.cs @@ -0,0 +1,19 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcStandaloneModeResult +{ + internal readonly FrameworkStandaloneModeSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkStandaloneModeSnapshot( + is_standalone != 0, + standalone_mode != 0); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcSwitchesResult.cs b/framework-dotnet/Generated/Result/FrameworkEcSwitchesResult.cs new file mode 100644 index 0000000..9832283 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcSwitchesResult.cs @@ -0,0 +1,22 @@ +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcSwitchesResult +{ + internal readonly FrameworkEcSwitchesSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkEcSwitchesSnapshot( + raw, + lid_open != 0, + power_button_pressed != 0, + write_protect_disabled != 0, + dedicated_recovery != 0); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcSysinfoResult.cs b/framework-dotnet/Generated/Result/FrameworkEcSysinfoResult.cs new file mode 100644 index 0000000..cc6f825 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcSysinfoResult.cs @@ -0,0 +1,37 @@ +using System; + +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using ManagedEcCurrentImage = FrameworkDotnet.Enums.FrameworkEcCurrentImage; +using ManagedEcResetFlag = FrameworkDotnet.Enums.FrameworkEcResetFlag; +using ManagedEcSysinfoFlag = FrameworkDotnet.Enums.FrameworkEcSysinfoFlag; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcSysinfoResult +{ + internal readonly FrameworkEcSystemInfoSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new FrameworkEcSystemInfoSnapshot( + ToManagedCurrentImage(), + (ManagedEcResetFlag)reset_flags, + (ManagedEcSysinfoFlag)flags); + } + + private readonly ManagedEcCurrentImage ToManagedCurrentImage() + { + return current_image switch + { + FrameworkEcCurrentImage.Unknown => ManagedEcCurrentImage.Unknown, + FrameworkEcCurrentImage.Ro => ManagedEcCurrentImage.Ro, + FrameworkEcCurrentImage.Rw => ManagedEcCurrentImage.Rw, + _ => throw new ArgumentOutOfRangeException(nameof(current_image), current_image, "Unhandled EC current image.") + }; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcTempSensorNameResult.cs b/framework-dotnet/Generated/Result/FrameworkEcTempSensorNameResult.cs new file mode 100644 index 0000000..91fa2e4 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcTempSensorNameResult.cs @@ -0,0 +1,31 @@ +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcTempSensorNameResult +{ + internal readonly FrameworkTemperatureSensorNameSnapshot GetValueOrThrow() + { + var value = name; + + if (status.IsFailure) + { + try + { + throw FrameworkStatusException.GetCorrectException(status); + } + finally + { + value.Free(); + } + } + + return new FrameworkTemperatureSensorNameSnapshot( + sensor_index, + value.ToUtf8StringAndFree(), + (FrameworkDotnet.Enums.FrameworkSensorName)(ushort)mapped_name, + (FrameworkTemperatureSensorType)sensor_type); + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkEcThermalThresholdsResult.cs b/framework-dotnet/Generated/Result/FrameworkEcThermalThresholdsResult.cs new file mode 100644 index 0000000..92c87ce --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkEcThermalThresholdsResult.cs @@ -0,0 +1,43 @@ +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkEcThermalThresholdsResult +{ + internal readonly FrameworkThermalThresholdsSnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + var enabled = (FrameworkThermalThresholdFlag)thresholds.enabled_mask; + + return new FrameworkThermalThresholdsSnapshot( + sensor_index, + enabled, + ToTemperature(enabled, FrameworkThermalThresholdFlag.Warn, thresholds.warn_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.High, thresholds.high_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.Halt, thresholds.halt_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.WarnRelease, thresholds.warn_release_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.HighRelease, thresholds.high_release_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.HaltRelease, thresholds.halt_release_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.FanOff, thresholds.fan_off_celsius), + ToTemperature(enabled, FrameworkThermalThresholdFlag.FanMax, thresholds.fan_max_celsius)); + } + + /// + /// A disabled threshold reads back as -273 degrees Celsius, so the enabled mask is the only + /// authoritative source for whether a threshold is active. Never test the Celsius value. + /// + private static Temperature? ToTemperature(FrameworkThermalThresholdFlag enabledMask, FrameworkThermalThresholdFlag flag, int celsius) + { + return (enabledMask & flag) == flag + ? Temperature.FromDegreesCelsius(celsius) + : null; + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkNvmeVersionResult.cs b/framework-dotnet/Generated/Result/FrameworkNvmeVersionResult.cs new file mode 100644 index 0000000..c847a5d --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkNvmeVersionResult.cs @@ -0,0 +1,41 @@ +using FrameworkDotnet.Exceptions; + +using ManagedNvmeVersionSnapshot = FrameworkDotnet.Snapshots.FrameworkNvmeVersionSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkNvmeVersionResult +{ + /// + /// Copies both identity strings into managed memory and releases the native buffers exactly once. + /// + /// A managed snapshot that shares no memory with the native record. + /// + /// Unlike , this record has no aggregate free. Both + /// buffers are released individually through the plain framework_byte_buffer_free path, on the + /// throwing path as well as the successful one. On a non-Linux host the native layer answers with + /// and two empty buffers, which release cleanly. + /// + internal readonly ManagedNvmeVersionSnapshot GetValueOrThrow() + { + FrameworkByteBuffer modelNumberBuffer = model_number; + FrameworkByteBuffer firmwareVersionBuffer = firmware_version; + + try + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new ManagedNvmeVersionSnapshot( + modelNumberBuffer.ToUtf8String(), + firmwareVersionBuffer.ToUtf8String()); + } + finally + { + modelNumberBuffer.Free(); + firmwareVersionBuffer.Free(); + } + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkPeripheralVersionsResult.cs b/framework-dotnet/Generated/Result/FrameworkPeripheralVersionsResult.cs new file mode 100644 index 0000000..05efa15 --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkPeripheralVersionsResult.cs @@ -0,0 +1,56 @@ +using FrameworkDotnet.Exceptions; + +using ManagedPeripheralVersionsSnapshot = FrameworkDotnet.Snapshots.FrameworkPeripheralVersionsSnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkPeripheralVersionsResult +{ + /// + /// Copies every peripheral slot into managed memory and releases the native record exactly once. + /// + /// A managed snapshot that shares no memory with the native record. + /// + /// + /// This is the single release path for the four native entry points that return this record + /// (framework_get_camera_versions, framework_get_input_module_versions, + /// framework_get_usb_hub_versions and framework_get_audio_card_version). The record owns + /// eight product_name buffers that must be released as a whole through + /// framework_peripheral_versions_free; the individual buffers must never be released with + /// framework_byte_buffer_free, and the aggregate must never be released twice. + /// + /// + /// The record is copied into a local first so the native free receives a writable address, and the free + /// runs in a block so the buffers are released on the throwing path as well. + /// Every string is copied into managed memory before the free, so the returned snapshot stays valid + /// afterwards. + /// + /// + internal readonly ManagedPeripheralVersionsSnapshot GetValueOrThrow() + { + FrameworkPeripheralVersionsResult owned = this; + + try + { + if (owned.status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(owned.status); + } + + return new ManagedPeripheralVersionsSnapshot( + owned.count, + owned.peripheral_0.ToManagedSnapshot(0), + owned.peripheral_1.ToManagedSnapshot(1), + owned.peripheral_2.ToManagedSnapshot(2), + owned.peripheral_3.ToManagedSnapshot(3), + owned.peripheral_4.ToManagedSnapshot(4), + owned.peripheral_5.ToManagedSnapshot(5), + owned.peripheral_6.ToManagedSnapshot(6), + owned.peripheral_7.ToManagedSnapshot(7)); + } + finally + { + NativeMethods.framework_peripheral_versions_free(&owned); + } + } +} diff --git a/framework-dotnet/Generated/Result/FrameworkStylusBatteryResult.cs b/framework-dotnet/Generated/Result/FrameworkStylusBatteryResult.cs new file mode 100644 index 0000000..b7d57de --- /dev/null +++ b/framework-dotnet/Generated/Result/FrameworkStylusBatteryResult.cs @@ -0,0 +1,22 @@ +using FrameworkDotnet.Exceptions; + +using UnitsNet; + +using ManagedStylusBatterySnapshot = FrameworkDotnet.Snapshots.FrameworkStylusBatterySnapshot; + +namespace Framework.System.Interop; + +internal unsafe partial struct FrameworkStylusBatteryResult +{ + internal readonly ManagedStylusBatterySnapshot GetValueOrThrow() + { + if (status.IsFailure) + { + throw FrameworkStatusException.GetCorrectException(status); + } + + return new ManagedStylusBatterySnapshot( + present != 0, + Ratio.FromPercent(level_percent)); + } +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcBattery.cs b/framework-dotnet/Interfaces/IFrameworkEcBattery.cs new file mode 100644 index 0000000..89e824c --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcBattery.cs @@ -0,0 +1,99 @@ +using System; + +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the battery surface of an embedded controller connection. +/// +/// +/// Instances are obtained from an and remain bound to the lifetime of that connection. Every member throws once the owning connection has been disposed. +/// +public interface IFrameworkEcBattery +{ + /// + /// Reads the full Smart Battery data set from the pack. + /// + /// + /// + /// This call performs many I2C passthrough round trips and is far slower than . Treat it strictly as an on-demand read: never place it in a polling loop, and keep it off the UI thread. + /// + /// + /// Supplying unlocks the manufacturer-access register group. When the pack accepts the key, is and the state-of-health, safety and lifetime groups are populated; otherwise those groups are . + /// + /// + /// The manufacturer-access unseal key, or to read only the sealed-mode subset. + /// The Smart Battery snapshot, already copied into managed memory. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkSmartBatterySnapshot GetSmartBatterySnapshot(uint? unsealKey = null); + + /// + /// Gets the battery cutoff (ship mode) state. + /// + /// The reported cutoff state, or when the embedded controller did not answer the query. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native layer reports a cutoff state value that is not recognized by the managed API. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkBatteryCutoffState GetCutoffState(); + + /// + /// Gets the current charging state together with the external power adapter state. + /// + /// The charging state snapshot. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkChargingStateSnapshot GetChargingState(); + + /// + /// Runs the Smart Battery SHA-1 HMAC challenge and reports whether the pack answered it correctly. + /// + /// + /// A pack that answers the challenge but fails it is a legitimate negative answer, not a failure: the method returns in that case rather than throwing. Exceptions are reserved for packs that do not answer at all and for transport failures. + /// + /// The authentication key. It must be exactly 16 bytes long. + /// when the pack answered the challenge correctly; otherwise, . + /// Thrown when is . + /// Thrown when is not exactly 16 bytes long. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status, which includes a pack that did not answer the challenge at all. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + bool Authenticate(byte[] authenticationKey); + + /// + /// Sets the battery charge rate limit, optionally conditioned on a battery state-of-charge threshold. + /// + /// + /// The nullable threshold mirrors : passing applies the limit unconditionally, which the native layer expresses as a negative state-of-charge value. + /// + /// The maximum charge rate. The native layer takes amperes as a single-precision value, so the quantity is narrowed to at the boundary. + /// The battery state-of-charge threshold (0-100%) below which the limit is applied, or to apply it unconditionally. + /// Thrown when is negative, or when is outside the 0-100 percent range. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetChargeRateLimit(ElectricCurrent rateLimit, Ratio? batterySoc = null); +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcConnection.cs b/framework-dotnet/Interfaces/IFrameworkEcConnection.cs index 17f9e20..18007d0 100644 --- a/framework-dotnet/Interfaces/IFrameworkEcConnection.cs +++ b/framework-dotnet/Interfaces/IFrameworkEcConnection.cs @@ -13,8 +13,53 @@ namespace FrameworkDotnet.Interfaces; /// /// Defines a safe embedded controller connection. /// +/// +/// The members declared directly on this interface cover firmware, power, thermal and fan control. The +/// remainder of the embedded controller surface is grouped into facets reached through the properties +/// below. Every facet borrows this connection's handle and must not be used after it is disposed. +/// public interface IFrameworkEcConnection : IDisposable { + /// + /// Gets the diagnostic surface: liveness, protocol and system information, saved panic data, + /// port 80 history, switch positions, throttle status, raw ADC channels and host command probing. + /// + IFrameworkEcDiagnostics Diagnostics { get; } + + /// + /// Gets the general-purpose I/O surface, which reads, writes and enumerates embedded controller GPIO lines. + /// + IFrameworkEcGpio Gpio { get; } + + /// + /// Gets the thermal control surface: per-sensor threshold configuration, sensor identity and the + /// authoritative fan count. Live temperature and fan readings come from . + /// + IFrameworkEcThermalControl Thermal { get; } + + /// + /// Gets the battery surface: the Smart Battery data set, pack authentication, cutoff state, + /// charging state and the charge rate limit. + /// + IFrameworkEcBattery Battery { get; } + + /// + /// Gets the USB Power Delivery surface: controller firmware versions, per-port charger negotiation + /// state and the retimer firmware version. + /// + IFrameworkEcPowerDelivery PowerDelivery { get; } + + /// + /// Gets the input device surface: per-key RGB lighting, keyboard matrix remapping, PS/2 emulation + /// and fine-grained fingerprint LED brightness. + /// + IFrameworkEcInput Input { get; } + + /// + /// Gets the power management surface: hibernate delay, standalone mode and the expansion-bay GPU serial. + /// + IFrameworkEcPowerManagement PowerManagement { get; } + /// /// Gets the active driver for the current connection. /// diff --git a/framework-dotnet/Interfaces/IFrameworkEcDiagnostics.cs b/framework-dotnet/Interfaces/IFrameworkEcDiagnostics.cs new file mode 100644 index 0000000..a2a7d8c --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcDiagnostics.cs @@ -0,0 +1,180 @@ +using System; + +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the embedded controller diagnostic surface: liveness probes, host command protocol +/// capabilities, boot and reset provenance, stored panic data, port 80 POST code history, raw +/// ADC channels and the physical switch positions. +/// +/// +/// +/// Instances are obtained from an and borrow that +/// connection's native handle, so they are only usable for as long as the owning connection is +/// open. Disposing the connection invalidates the facet; the facet itself owns nothing and is +/// therefore not . +/// +/// +/// Every member issues at least one host command against the embedded controller, so none of +/// them are free. Treat them as on-demand diagnostics rather than poll-loop telemetry. +/// +/// +public interface IFrameworkEcDiagnostics +{ + /// + /// Sends the embedded controller hello command with a caller-supplied payload and + /// reports what the controller echoed back. + /// + /// The arbitrary payload to send. A healthy controller answers with plus 0x01020304, computed with unsigned wraparound. + /// The echoed payload together with a flag indicating whether it matched the expected transform. + /// + /// This is the cheapest end-to-end check that host command communication actually works. A + /// returned snapshot whose is + /// means the controller answered but answered wrongly, which + /// indicates a corrupt transport rather than a failed call, so it is reported as a value and + /// not as an exception. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcHelloSnapshot SendHello(uint inData); + + /// + /// Sends the embedded controller hello command using the same magic payload the + /// upstream liveness check uses, and reports what the controller echoed back. + /// + /// The echoed payload together with a flag indicating whether it matched the expected transform. + /// + /// This is a convenience wrapper over . Inspect + /// to decide whether the controller is + /// responding correctly. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcHelloSnapshot CheckHello(); + + /// + /// Gets the host command protocol capabilities reported by the embedded controller. + /// + /// The supported protocol versions, maximum packet sizes and optional protocol capabilities. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcProtocolInfoSnapshot GetProtocolInfo(); + + /// + /// Gets the embedded controller system information: which firmware image is running, why the + /// controller last reset, and the current lock and jump state. + /// + /// The embedded controller system information. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native layer reports an EC current image value that is not recognized by the managed API. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcSystemInfoSnapshot GetSystemInfo(); + + /// + /// Gets the panic data the embedded controller saved from its last crash. + /// + /// The raw panic blob together with the decoded header and trailer fields. An empty blob means the controller has no stored panic. + /// + /// The panic payload is exposed as an opaque blob because the per-architecture decode + /// structures are private to the upstream firmware headers. Use + /// and + /// to select a decoder. Where the + /// controller implements the versioned read command this call does not mark the panic as + /// consumed, so other tools still observe it. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcPanicInfoSnapshot GetPanicInfo(); + + /// + /// Gets the port 80 POST code history recorded by the embedded controller. + /// + /// The history ring in buffer order together with a newest-first ordered view. + /// + /// The controller stores POST codes in a wrapping ring buffer. Entries matching a + /// value are markers the controller inserted itself + /// rather than codes emitted by host firmware. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcPort80HistorySnapshot GetPort80History(); + + /// + /// Gets the live positions of the physical switches the embedded controller monitors. + /// + /// The lid, power button, write protect and dedicated recovery switch positions. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcSwitchesSnapshot GetSwitches(); + + /// + /// Gets whether the embedded controller is currently throttling the application processor + /// for thermal reasons. + /// + /// The soft and hard throttle states. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcApThrottleSnapshot GetApThrottleStatus(); + + /// + /// Reads one raw analog-to-digital converter channel on the embedded controller. + /// + /// The zero-based ADC channel index. The set of valid channels is firmware defined; the controller rejects unknown channels. + /// The raw converter count for the channel. + /// + /// The value is the unscaled converter reading. The native layer documents no unit or + /// reference voltage for it, so no physical quantity conversion is applied here; the meaning + /// of a channel and its scaling are firmware defined. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure, which includes the controller rejecting an unknown channel. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + int ReadAdcChannel(byte channel); + + /// + /// Determines whether the embedded controller implements a host command at a given version. + /// + /// The host command identifier to probe. + /// The host command version to probe. + /// when the controller implements at ; otherwise . + /// + /// Worth calling before the newer commands: support varies by platform and firmware + /// revision, and this separates "not implemented" from "the call failed". + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + bool IsCommandVersionSupported(uint command, byte version); +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcGpio.cs b/framework-dotnet/Interfaces/IFrameworkEcGpio.cs new file mode 100644 index 0000000..db27121 --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcGpio.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; + +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the general-purpose input/output surface of a Framework embedded controller connection. +/// +/// +/// +/// This is an advanced diagnostic API. The embedded controller GPIO table is the firmware's own +/// pin map: it contains power-sequencing enables, reset lines, bus selects, interrupt lines and rail +/// monitors. Reading a line is harmless, but drives a real hardware +/// line and can cut a power rail, hold a device in reset, or contend with firmware that is driving the +/// same pin. Writing an arbitrary GPIO can destabilise or hard-hang the system, corrupt an in-flight +/// device transaction, or leave the machine in a state that only a full power cycle clears. Use it only +/// against a line you have positively identified, and never as part of routine telemetry. +/// +/// +/// Line names are addressed as UTF-8 and firmware accepts at most 32 bytes. Names longer than that are +/// rejected by the managed layer rather than silently truncated by the embedded controller. +/// +/// +/// Every member issues a synchronous host command to the embedded controller. Keep calls off the UI +/// thread, and prefer over a hand-rolled loop when enumerating the whole table. +/// +/// +public interface IFrameworkEcGpio +{ + /// + /// Gets the number of general-purpose input/output lines the embedded controller exposes. + /// + /// The number of lines in the embedded controller GPIO table. + /// Valid indices for are 0 through the returned count minus one. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + int GetCount(); + + /// + /// Reads the current logic level of a general-purpose input/output line addressed by name. + /// + /// The firmware-assigned name of the line, at most 32 UTF-8 bytes. + /// when the line reads as logic high; otherwise, . + /// Names are matched exactly and case-sensitively against the firmware pin map; use to discover them. + /// Thrown when is . + /// Thrown when is empty or encodes to more than 32 UTF-8 bytes. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure, including when the embedded controller does not know the requested line name. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + bool GetValue(string name); + + /// + /// Drives a general-purpose input/output line addressed by name to the requested logic level. + /// + /// The firmware-assigned name of the line, at most 32 UTF-8 bytes. + /// to drive the line logic high; to drive it logic low. + /// + /// + /// This writes a real hardware line. Driving an arbitrary embedded controller GPIO can power + /// down a rail, hold a peripheral in reset, break an in-flight bus transaction, or fight firmware that + /// owns the same pin — any of which can destabilise or hang the running system, and some of which + /// survive until a full power cycle. Restrict this to lines you have positively identified, and treat + /// it as a debugging and bring-up tool rather than a supported control surface. + /// + /// + /// The embedded controller reports success once it has accepted the command; it does not confirm that + /// the pin settled at the requested level. Read the line back with when + /// the resulting state matters, and be aware that firmware may immediately re-drive a pin it owns. + /// + /// + /// Thrown when is . + /// Thrown when is empty or encodes to more than 32 UTF-8 bytes. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure, including when the embedded controller does not know the requested line name or refuses to drive it. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetValue(string name, bool value); + + /// + /// Reads the name, logic level and configuration flags of a single line addressed by table index. + /// + /// The zero-based index of the line, in the range 0 through minus one. + /// A snapshot describing the requested line. + /// Thrown when is negative or greater than 255, the largest index the embedded controller command can address. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure, including when is beyond the reported line count. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkEcGpioSnapshot GetInfo(int index); + + /// + /// Reads every general-purpose input/output line the embedded controller exposes, in index order. + /// + /// A snapshot for each line, ordered by . + /// + /// The enumeration is count-aware: it reads once and then issues one + /// host command per index. The result is fully materialised before it is + /// returned, so the embedded controller is not held open while a caller iterates. On a laptop the table + /// typically holds well over a hundred lines, so treat this as an on-demand discovery call rather than + /// something to poll. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + IReadOnlyList GetAll(); +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcInput.cs b/framework-dotnet/Interfaces/IFrameworkEcInput.cs new file mode 100644 index 0000000..00e4da5 --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcInput.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the embedded controller input-device controls: per-key RGB lighting, keyboard matrix +/// remapping, PS/2 emulation and fine-grained fingerprint LED brightness. +/// +/// +/// Every member of this facet is a write path: each call issues an embedded controller host command that +/// changes hardware behaviour immediately. Upstream framework-system does not document whether any of these +/// settings survives an EC reset or a power cycle, so treat them as volatile embedded controller state and +/// re-apply them after resume rather than relying on them persisting. The facet borrows the embedded +/// controller handle owned by and must not outlive it. +/// +public interface IFrameworkEcInput +{ + /// + /// Sets the per-key RGB keyboard colors for a contiguous run of keys beginning at the given key index. + /// + /// The zero-based embedded controller key index at which the run of colors begins. Valid values are 0 through 255. + /// The colors to apply, one per key, in ascending key order. At most 64 colors may be sent in a single call. + /// + /// Each color is transmitted as three bytes in red, green, blue order, so the managed list length is the key + /// count and the stride cannot be misstated. Upstream framework-system documents this surface for the Framework + /// Desktop RGB LEDs; the Framework Laptop 16 keyboard is not driven by the embedded controller and is configured + /// through the vendor's own keyboard tooling instead. + /// + /// Upstream framework-system does not document whether the colors survive an EC reset or a power cycle, and the + /// embedded controller may overwrite them whenever it resumes driving its own lighting behaviour. Treat them as + /// volatile embedded controller state and re-apply them as needed. To color more than 64 keys, issue several + /// calls with successive values. + /// + /// + /// Thrown when is . + /// Thrown when is outside the 0–255 range, or when is empty or contains more than 64 entries. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + [FrameworkPlatformSpecific(FrameworkPlatformFamily.FrameworkDesktop, Message = "Upstream framework-system documents the RGB LED surface on Framework Desktop only. The Framework Laptop 16 keyboard is not driven by the embedded controller.")] + void SetRgbKeyboardColors(int startKey, IReadOnlyList colors); + + /// + /// Remaps the key at the given keyboard matrix position to the given scan code. + /// + /// The zero-based keyboard matrix row. Valid values are 0 through 255. + /// The zero-based keyboard matrix column. Valid values are 0 through 255. + /// The scan set 2 code the matrix position should report. + /// + /// This is an advanced API. The embedded controller rewrites its keyboard matrix map, so the change applies to + /// the built-in keyboard before the operating system ever sees a key event, and it therefore cannot be undone + /// from software that only remaps at the operating system layer. Writing a wrong row, column or scan code can + /// make a key report the wrong character, report nothing at all, or shadow a modifier, which can leave the + /// keyboard difficult to use. Know the matrix position for the target device before calling this, and keep an + /// external keyboard available while experimenting. + /// + /// The matrix is model-specific. Framework Laptop 12 and Framework Laptop 13 use different row and column + /// assignments for the same physical keys, so a position taken from one model will address a different key on + /// the other. The Framework Laptop 16 keyboard is not driven by the embedded controller, so this call does not + /// change its behaviour. Scan codes are shared across models. + /// + /// + /// Upstream framework-system does not document whether the mapping survives an EC reset or a power cycle. Treat + /// it as volatile embedded controller state and re-apply it rather than relying on it persisting. + /// + /// + /// Thrown when or is outside the 0–255 range. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + /// + void RemapKey(int row, int column, ushort scanCode); + + /// + /// Remaps the key at Framework Laptop 12 matrix position row 6, column 15 to Control. + /// + /// + /// + /// This is the named shorthand the native layer provides for at matrix + /// row 6, column 15, with scan code 0x0014. That position is Caps Lock on Framework Laptop 12 only + /// — the matrix differs per model, so this call does not remap Caps Lock on other families and will silently + /// remap whatever key occupies that position instead: + /// + /// + /// On Framework Laptop 13, call with row 4, column 4 and scan code + /// 0x0014 to reach Caps Lock. The Framework Laptop 16 keyboard is not remappable through the embedded + /// controller at all. + /// + /// + /// Where the position is correct, the physical Caps Lock key reports Control afterwards and Caps Lock can no + /// longer be toggled from that key, so the same advanced-API cautions apply. Upstream framework-system does not + /// document whether the mapping survives an EC reset or a power cycle; treat it as volatile embedded controller + /// state. + /// + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + /// + void RemapCapsLockToControl(); + + /// + /// Enables or disables PS/2 emulation on the embedded controller. + /// + /// to enable PS/2 emulation; to disable it. + /// + /// + /// This is a debug-only control. Upstream framework-system hides the equivalent command from its own help + /// output and describes it as affecting the touchpad, with the documented recovery being to reboot the + /// system if the touchpad stops working. The native ABI comment describes it as keyboard emulation; where the + /// two disagree, upstream framework-system is the behaviour that ships. Expect either pointing or key input to be + /// affected, and do not assume toggling the flag back is enough to recover. + /// + /// + /// Upstream does not document whether the setting survives an EC reset or a power cycle. Treat it as volatile + /// embedded controller state. + /// + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetPs2EmulationEnabled(bool enabled); + + /// + /// Sets the fingerprint reader LED brightness as a percentage. + /// + /// The target brightness as a percentage-like ratio (0–100%). The value is rounded to the nearest whole percent before it is sent to the embedded controller. + /// + /// This is the fine-grained counterpart to , + /// which selects one of the discrete firmware levels instead. The embedded controller changes the power-button + /// fingerprint reader LED brightness immediately. + /// + /// Upstream framework-system does not document whether the brightness survives an EC reset or a power cycle, and + /// the embedded controller may override the value whenever it drives the LED for its own status indications. + /// Treat it as volatile embedded controller state and re-apply it as needed. + /// + /// + /// Thrown when is not a finite value inside the 0–100 percent range. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + /// + void SetFingerprintLedBrightness(Ratio brightness); +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcPowerDelivery.cs b/framework-dotnet/Interfaces/IFrameworkEcPowerDelivery.cs new file mode 100644 index 0000000..42693c4 --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcPowerDelivery.cs @@ -0,0 +1,83 @@ +using System; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the USB Power Delivery surface of an embedded controller connection. +/// +/// +/// The members of this facet describe the charger side of the USB-C subsystem: the firmware carried by the Power Delivery controllers, the charger +/// negotiation state of an individual port, and the retimer firmware version. The Type-C link state of a port is reported separately through the module +/// inventory as . +/// +public interface IFrameworkEcPowerDelivery +{ + /// + /// Gets the firmware versions of every USB Power Delivery controller slot. + /// + /// The Power Delivery controller firmware versions. + /// + /// The three controller slots are fixed by the native probe order: slot 0 is , slot 1 is + /// and slot 2 is . Framework laptops + /// populate slots 0 and 1, Framework Desktop populates slot 2 only, so the populated slots are not contiguous. Enumerate + /// , or test + /// , before reading any controller version. + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkPowerDeliveryControllerVersionsSnapshot GetControllerVersions(); + + /// + /// Gets the charger negotiation state of a single USB Power Delivery port. + /// + /// The zero-based port index to query. + /// The charger negotiation state of the requested port. + /// + /// This reports what the attached charger offers and what the port has negotiated from it: the power role, the charging type, the advertised voltage and + /// current limits and the maximum negotiated power. It is deliberately distinct from in the module + /// inventory, which reports the Type-C link itself. A port with nothing attached reports + /// and rather than failing. + /// + /// Thrown when is negative or greater than 255, or when the native layer reports a power role that is not recognized by the managed API. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status, including when the requested port does not exist on this platform. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkPowerDeliveryPowerInfoSnapshot GetPowerInfo(int port); + + /// + /// Gets the retimer firmware version reported by the embedded controller. + /// + /// The retimer firmware version. + /// + /// + /// The Parade retimer sits behind the Framework Laptop 16 expansion-bay discrete GPU. This query is backed by the expansion-bay GPU PCIe host + /// command, which other platform families reject, so calling it on Framework 12, Framework 13 or Desktop raises + /// rather than returning a not-present reading. + /// + /// + /// On a Framework Laptop 16 whose expansion bay carries no compatible discrete GPU, the query succeeds with + /// set to and an empty version; that is a normal + /// reading rather than an error. + /// + /// + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library returns an error status, including on every platform family other than Framework Laptop 16, whose embedded controller rejects the underlying command. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + [FrameworkPlatformSpecific(FrameworkPlatformFamily.Framework16, Message = "The Parade retimer sits behind the Framework Laptop 16 expansion-bay discrete GPU; other platform families reject the underlying EC command.")] + FrameworkPowerDeliveryRetimerVersionSnapshot GetRetimerVersion(); +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcPowerManagement.cs b/framework-dotnet/Interfaces/IFrameworkEcPowerManagement.cs new file mode 100644 index 0000000..3e75c6f --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcPowerManagement.cs @@ -0,0 +1,90 @@ +using System; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the embedded controller power-management surface, together with the read-only +/// expansion-bay GPU identity it exposes. +/// +/// +/// Every member issues a host command against the embedded controller of the owning connection, so +/// the lifetime of an implementation is bound to that connection. Once the connection is disposed, +/// every member throws . +/// +public interface IFrameworkEcPowerManagement +{ + /// + /// Gets the delay the EC waits with the system off before it hibernates. + /// + /// The configured hibernate delay. + /// The EC stores this delay as a whole number of seconds, so the returned duration is always an integral number of seconds. + /// Thrown when the connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + Duration GetHibernateDelay(); + + /// + /// Sets the delay the EC waits with the system off before it hibernates. + /// + /// The hibernate delay to program. Must be a finite, non-negative duration of at most 4294967295 seconds. + /// The EC stores the delay as a whole number of seconds, so is rounded to the nearest second before it is written. + /// Thrown when is negative, not finite, or rounds to more than 4294967295 seconds. + /// Thrown when the connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetHibernateDelay(Duration delay); + + /// + /// Gets the standalone (batteryless) mode state reported by the EC. + /// + /// The standalone mode snapshot. + /// + /// Standalone mode describes a system that runs without a battery pack, the normal configuration + /// for Framework Desktop. The call is valid on every platform family, but a + /// reading is not proof that no battery is fitted: upstream falls back to + /// as a safe default whenever the embedded controller power-info read produces nothing, and + /// reports success while doing so. See . + /// + /// Thrown when the connection has been disposed. + /// Thrown when the native Framework library returns an error status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkStandaloneModeSnapshot GetStandaloneMode(); + + /// + /// Reads the serial number of the expansion-bay GPU module. + /// + /// The expansion-bay GPU serial, or an empty string when the EC reports no serial. + /// + /// + /// This surface is deliberately read-only. The matching write path is not exposed by this + /// library and no setter will be added: programming a serial rewrites persistent expansion-bay + /// identity, and the upstream framework-system implementation copies the supplied bytes + /// into a fixed-size slice without a length check. + /// + /// + /// Upstream framework-system currently documents the expansion-bay GPU surface on + /// Framework Laptop 16 only. Other Framework platform families may return data-unavailable + /// statuses or firmware-specific values depending on native support. + /// + /// + /// Thrown when the connection has been disposed. + /// Thrown when the native Framework library returns an error status, including data-unavailable conditions on unsupported platforms. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + [FrameworkPlatformSpecific(FrameworkPlatformFamily.Framework16, Message = "Upstream framework-system currently documents the expansion-bay GPU surface on Framework Laptop 16 only.")] + string GetGpuSerial(); +} diff --git a/framework-dotnet/Interfaces/IFrameworkEcThermalControl.cs b/framework-dotnet/Interfaces/IFrameworkEcThermalControl.cs new file mode 100644 index 0000000..fecb2fb --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkEcThermalControl.cs @@ -0,0 +1,133 @@ +using System; + +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Requests; +using FrameworkDotnet.Snapshots; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the embedded controller thermal control surface: per-sensor threshold configuration, +/// sensor identity, and the authoritative fan count. +/// +/// +/// +/// This surface configures how the embedded controller reacts to temperature. Live temperature and +/// fan readings come from the thermal snapshot on the owning connection, which is the surface +/// intended for polling. +/// +/// +/// A disabled threshold reads back from firmware as -273 degrees Celsius, so every threshold on +/// is exactly when its bit +/// is clear in the snapshot's enabled mask. Never infer "disabled" from a temperature value. +/// +/// +public interface IFrameworkEcThermalControl +{ + /// + /// Gets the thermal threshold configuration the embedded controller holds for one temperature + /// sensor. + /// + /// The temperature sensor slot to read, matching the temperature slot order of the thermal snapshot. + /// The thresholds for the requested sensor. Every threshold is when its bit is clear in the returned enabled mask. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library reports a failure, for example when the sensor index is out of range for the platform. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkThermalThresholdsSnapshot GetThresholds(byte sensorIndex); + + /// + /// Writes thermal thresholds for one temperature sensor. + /// + /// + /// + /// The write is a read-modify-write against firmware: any threshold left at its default + /// value is preserved exactly as it + /// is, and so are the three release points, which this call never touches. Calling the method + /// with no threshold arguments is therefore a no-op host command. + /// + /// + /// Each threshold argument carries one of three unambiguous intents: + /// leaves it untouched, + /// turns it off, and + /// (or an implicitly converted + /// temperature) enables it at that temperature. A set temperature must round to at least one + /// degree Celsius, because the native ABI reserves zero for "disable" and negative values for + /// "keep current". + /// + /// + /// Raising or disabling and removes thermal + /// protection the embedded controller would otherwise apply. Treat those two as safety-critical. + /// + /// + /// The temperature sensor slot to write, matching the temperature slot order of the thermal snapshot. + /// The temperature above which the embedded controller warns the application processor. Defaults to keeping the current value. + /// The temperature above which the embedded controller throttles the application processor. Defaults to keeping the current value. + /// The temperature above which the embedded controller shuts the system down. Defaults to keeping the current value. + /// The temperature setpoint below which no active cooling is required, so the fans stop. This is a temperature, not a fan speed or an RPM limit. Defaults to keeping the current value. + /// The temperature setpoint above which active cooling runs at maximum, so the fans reach full speed. This is a temperature, not a fan speed or an RPM limit. Defaults to keeping the current value. + /// Thrown when the owning connection has been disposed. + /// Thrown when a threshold requests a temperature that is not finite or that does not round to at least one degree Celsius. + /// Thrown when the native Framework library reports a failure, for example when the sensor index is out of range for the platform. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetThresholds( + byte sensorIndex, + FrameworkThermalThresholdSetting warn = default, + FrameworkThermalThresholdSetting high = default, + FrameworkThermalThresholdSetting halt = default, + FrameworkThermalThresholdSetting fanOff = default, + FrameworkThermalThresholdSetting fanMax = default); + + /// + /// Gets the identity of one temperature sensor slot: the raw firmware name, the managed sensor + /// role it maps onto, and the embedded controller's classification tag. + /// + /// + /// Each distinct sensor index costs at most one host command after the first successful read, + /// after which the answer is cached for the lifetime of this instance. Sensor names do not change while the system is running, + /// so read them once per session and keep polling the thermal snapshot on the owning connection + /// for live values. Use if a cached answer must be discarded. + /// + /// The temperature sensor slot to identify, matching the temperature slot order of the thermal snapshot. + /// The identity of the requested sensor slot. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library reports a failure, for example when the sensor index is out of range for the platform. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkTemperatureSensorNameSnapshot GetSensorName(byte sensorIndex); + + /// + /// Discards every cached temperature sensor name, so that the next + /// for each slot issues a fresh host command. + /// + /// + /// Sensor names are stable while the system is running, so this is only needed after the + /// embedded controller has been reflashed or reset underneath the process. + /// + void ClearSensorNameCache(); + + /// + /// Gets the number of fans the embedded controller reports. + /// + /// + /// This is more authoritative than the fan count carried on the thermal snapshot and the fan + /// capabilities snapshot, both of which infer fan presence from a memory-map sentinel value. + /// Prefer this count when deciding how many fan slots are real; the two agree on healthy + /// hardware. + /// + /// The number of fans reported by the embedded controller. + /// Thrown when the owning connection has been disposed. + /// Thrown when the native Framework library reports a failure. + /// Thrown when the native Framework library returns an EC response failure. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + byte GetFanCount(); +} diff --git a/framework-dotnet/Interfaces/IFrameworkPeripherals.cs b/framework-dotnet/Interfaces/IFrameworkPeripherals.cs new file mode 100644 index 0000000..ea09aa0 --- /dev/null +++ b/framework-dotnet/Interfaces/IFrameworkPeripherals.cs @@ -0,0 +1,232 @@ +using System; + +using FrameworkDotnet.Attributes; +using FrameworkDotnet.Enums; +using FrameworkDotnet.Exceptions; +using FrameworkDotnet.Exceptions.StatusCodes; +using FrameworkDotnet.Snapshots; + +using UnitsNet; + +namespace FrameworkDotnet.Interfaces; + +/// +/// Defines the Framework peripheral operations that reach HID and USB devices directly. +/// +/// +/// +/// None of these operations involve the embedded controller. They enumerate and talk to the touchscreen, +/// the haptic touchpad, cameras, input modules, USB hubs, the audio expansion card and NVMe drives over +/// their own transports, so no is needed and none of them can be +/// affected by an embedded controller driver being unavailable. +/// +/// +/// Every member issues synchronous device input/output and opens the underlying device for the duration +/// of the call. Keep all of them off the UI thread. is the slowest by a +/// wide margin and is called out separately on its own member. +/// +/// +/// Access to raw HID and USB devices is permission-gated by the host operating system. On Linux these +/// calls require a udev rule granting access to the device node, or elevated privileges; without it a +/// device that is physically present is reported as absent rather than raising a distinct error. +/// +/// +public interface IFrameworkPeripherals +{ + /// + /// Reads the charge level of the stylus paired with the touchscreen. + /// + /// A snapshot describing the stylus charge level. + /// + /// The query is answered by the touchscreen controller over HID. A successful read where + /// is means no stylus is + /// paired, or the touchscreen does not report a stylus battery; that is a normal outcome and does not + /// raise an exception. + /// + /// Thrown when the native Framework library reports a failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkStylusBatterySnapshot GetStylusBattery(); + + /// + /// Enables or disables touch input on the touchscreen. + /// + /// to enable touch input; to disable it. + /// + /// + /// The setting is applied to the touchscreen controller over HID and persists until it is changed again + /// or the controller is power-cycled. There is no matching read: the firmware does not report the + /// current state, so the caller must track it if the state matters. + /// + /// + /// Disabling touch input removes an input device from the running system. On a convertible or tablet + /// with no other pointing device attached this can leave the machine without usable input until the + /// setting is reversed or the system is power-cycled. + /// + /// + /// Thrown when no supported touchscreen answered the request. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetTouchscreenEnabled(bool enabled); + + /// + /// Sets the haptic feedback intensity of a haptic touchpad. + /// + /// The requested intensity, which must be exactly 0, 25, 50, 75 or 100 percent. + /// + /// + /// This is a write-only control and is deliberately not modelled as a settable property. The touchpad + /// firmware accepts the HID SET_FEATURE report that carries the intensity but never answers the + /// matching GET_FEATURE report, so the current value cannot be read back and no round trip exists. + /// Track the last value written if the application needs to display it. + /// + /// + /// The HID descriptor advertises a logical range of 0 to 100, but the Boreas haptic firmware implements + /// only five discrete steps and rejects anything else. Requests are validated against those five steps + /// before they reach the device. + /// + /// + /// Only haptic touchpads answer this report. On a system fitted with a conventional touchpad the + /// request fails rather than being silently ignored. + /// + /// + /// Thrown when is not exactly 0, 25, 50, 75 or 100 percent, or is not a finite value. + /// Thrown when no haptic touchpad answered the request. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetTouchpadHapticIntensity(Ratio intensity); + + /// + /// Sets the click force threshold of a haptic touchpad. + /// + /// The actuation force at which the touchpad registers a click. + /// + /// + /// This is a write-only control and is deliberately not modelled as a settable property, for the same + /// reason as : the firmware accepts the HID SET_FEATURE + /// report but never answers the matching GET_FEATURE report, so there is no way to read the threshold + /// back. Track the last value written if the application needs to display it. + /// + /// + /// Only haptic touchpads answer this report. On a system fitted with a conventional touchpad the + /// request fails rather than being silently ignored. + /// + /// + /// Thrown when is not a defined value. + /// Thrown when no haptic touchpad answered the request. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + void SetTouchpadClickForce(FrameworkClickForce force); + + /// + /// Reads the firmware versions of the connected Framework cameras. + /// + /// A snapshot holding one slot per detected camera. + /// + /// The version is decoded from the USB bcdDevice descriptor field, so the enumeration itself does + /// not open the device. A system whose camera is disabled by the hardware privacy switch reports no + /// camera at all, which is a normal reading rather than an error. + /// + /// Thrown when the native Framework library reports a failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkPeripheralVersionsSnapshot GetCameraVersions(); + + /// + /// Reads the firmware versions of the connected Framework 16 input modules. + /// + /// A snapshot holding one slot per detected input module. + /// + /// Input modules are a Framework Laptop 16 concept: the keyboard, numeric pad, macropad, spacers and + /// the LED matrix modules all report through this call. Other platform families have no input modules + /// to enumerate and answer successfully with an empty table. + /// + /// Thrown when the native Framework library reports a failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + [FrameworkPlatformSpecific(FrameworkPlatformFamily.Framework16, Message = "Input modules, including the LED matrix, are specific to Framework Laptop 16. Other platform families report an empty table.")] + FrameworkPeripheralVersionsSnapshot GetInputModuleVersions(); + + /// + /// Reads the firmware versions of the USB hubs fitted to the system. + /// + /// A snapshot holding one slot per detected hub. + /// + /// This covers the Realtek and Genesys hubs Framework systems use internally. Which hubs are present, + /// and how many, varies by platform generation, so treat the result as discovery output rather than a + /// fixed inventory. + /// + /// Thrown when the native Framework library reports a failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkPeripheralVersionsSnapshot GetUsbHubVersions(); + + /// + /// Reads the firmware version of the audio expansion card. + /// + /// A snapshot holding a single populated slot when an audio card is fitted, or an empty table when none is. + /// + /// + /// This call must be kept off the UI thread. Unlike the other version queries it cannot read a + /// USB descriptor: it performs a Synaptics CAPE exchange over HID control transfers, which claims the + /// card's HID interface for the duration of the call. A card in a wedged state does not answer, and the + /// bounded retry loop then runs for up to roughly three seconds before giving up. The call blocks the + /// calling thread for that whole period. + /// + /// + /// While the interface is claimed, other software cannot talk to the card. Call this on demand, never + /// on a timer, and never concurrently with itself. + /// + /// + /// A system with no audio expansion card fitted answers successfully with an empty table, which is a + /// normal reading rather than an error. + /// + /// + /// Thrown when the native Framework library reports a failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkPeripheralVersionsSnapshot GetAudioCardVersion(); + + /// + /// Reads the model number and firmware revision of the NVMe drive at the given device node. + /// + /// The path of the NVMe device node, for example /dev/nvme0. + /// A snapshot holding the drive's identity strings. + /// + /// + /// This operation is Linux only. The readback issues an NVMe admin passthrough ioctl, which the + /// underlying native library compiles only for Linux. On Windows, and on every other host operating + /// system, the call always fails with no matter what + /// path is supplied. That is a permanent capability gap for the platform, not a transient failure, so + /// there is no point retrying; gate the feature on the host operating system instead. + /// + /// + /// The path is passed to the native layer as UTF-8 bytes plus a length rather than as a terminated + /// string, so it may contain any character the file system accepts except an embedded null. + /// + /// + /// Opening an NVMe device node and issuing an admin passthrough command normally requires elevated + /// privileges. + /// + /// + /// Thrown when is . + /// Thrown when is empty or contains an embedded null character. + /// Thrown on every host operating system other than Linux, where the native library contains no NVMe implementation. + /// Thrown when the drive could not be opened or did not answer the identify command. + /// Thrown when the native Framework library reports any other failure status. + /// Thrown when the native Framework library cannot be located. + /// Thrown when the native Framework library is incompatible with the current process architecture. + /// Thrown when the required native entry point is unavailable. + FrameworkNvmeVersionSnapshot GetNvmeVersion(string devicePath); +} diff --git a/framework-dotnet/Requests/FrameworkThermalThresholdSetting.cs b/framework-dotnet/Requests/FrameworkThermalThresholdSetting.cs new file mode 100644 index 0000000..cd47fe8 --- /dev/null +++ b/framework-dotnet/Requests/FrameworkThermalThresholdSetting.cs @@ -0,0 +1,98 @@ +using System.Globalization; + +using FrameworkDotnet.Enums; + +namespace FrameworkDotnet.Requests; + +/// +/// Represents the change requested for a single embedded controller thermal threshold. +/// +/// +/// +/// Writing thermal thresholds is a read-modify-write, and every threshold carries three distinct +/// intents that must stay unambiguous: keep the value firmware currently holds, disable the +/// threshold outright, or set it to an explicit temperature. A plain nullable temperature cannot +/// express all three, so each threshold argument is described by one of these settings instead. +/// +/// +/// The default value of this type is , so a threshold that is not +/// mentioned in a write is never modified. +/// +/// +public readonly record struct FrameworkThermalThresholdSetting +{ + private FrameworkThermalThresholdSetting(FrameworkThermalThresholdAction action, UnitsNet.Temperature? temperature) + { + Action = action; + Temperature = temperature; + } + + /// + /// Gets the setting that leaves the threshold exactly as embedded controller firmware currently + /// holds it. This is also the default value of the type. + /// + public static FrameworkThermalThresholdSetting KeepCurrent => default; + + /// + /// Gets the setting that disables the threshold, so that the embedded controller stops acting + /// on it. This is distinct from , which changes nothing. + /// + public static FrameworkThermalThresholdSetting Disable => new(FrameworkThermalThresholdAction.Disable, null); + + /// + /// Gets the requested action for the threshold. + /// + public FrameworkThermalThresholdAction Action { get; } + + /// + /// Gets the requested threshold temperature, or when + /// is not . + /// + public UnitsNet.Temperature? Temperature { get; } + + /// + /// Creates a setting that enables the threshold and sets it to an explicit temperature. + /// + /// The temperature the embedded controller should act on. It must round to at least one degree Celsius, because zero and negative values are reserved by the native ABI for "disable" and "keep current". + /// A setting describing the requested temperature. + public static FrameworkThermalThresholdSetting FromTemperature(UnitsNet.Temperature temperature) + { + return new FrameworkThermalThresholdSetting(FrameworkThermalThresholdAction.Set, temperature); + } + + /// + /// Creates a setting that enables the threshold and sets it to an explicit temperature in + /// degrees Celsius. + /// + /// The temperature in degrees Celsius. It must round to at least one, because zero and negative values are reserved by the native ABI for "disable" and "keep current". + /// A setting describing the requested temperature. + public static FrameworkThermalThresholdSetting FromDegreesCelsius(double degreesCelsius) + { + return FromTemperature(UnitsNet.Temperature.FromDegreesCelsius(degreesCelsius)); + } + + /// + /// Converts a temperature into a setting that enables the threshold at that temperature. + /// + /// The temperature the embedded controller should act on. + public static implicit operator FrameworkThermalThresholdSetting(UnitsNet.Temperature temperature) + { + return FromTemperature(temperature); + } + + /// + /// Returns a readable description of the requested change. + /// + /// A readable description of the requested change. + public override string ToString() + { + return Action switch + { + FrameworkThermalThresholdAction.Disable => "Thermal Threshold Setting: Disable", + FrameworkThermalThresholdAction.Set => Temperature.HasValue + ? $"Thermal Threshold Setting: Set to {Temperature.Value.ToString(CultureInfo.InvariantCulture)}" + : "Thermal Threshold Setting: Set to an unspecified temperature", + _ => "Thermal Threshold Setting: Keep current", + }; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkBatteryLifetimeDataSnapshot.cs b/framework-dotnet/Snapshots/FrameworkBatteryLifetimeDataSnapshot.cs new file mode 100644 index 0000000..7e8fce1 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkBatteryLifetimeDataSnapshot.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the five raw Smart Battery lifetime data blocks read through manufacturer access. +/// +/// +/// The blocks are only readable once the pack has been unsealed, so this snapshot is only produced when is . Their layout is defined by the pack's gas-gauge firmware and varies between vendors, so the bytes are surfaced verbatim rather than decoded. +/// +public sealed record FrameworkBatteryLifetimeDataSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The first raw lifetime data block. + /// The second raw lifetime data block. + /// The third raw lifetime data block. + /// The fourth raw lifetime data block. + /// The fifth raw lifetime data block. + public FrameworkBatteryLifetimeDataSnapshot( + IReadOnlyList block_1, + IReadOnlyList block_2, + IReadOnlyList block_3, + IReadOnlyList block_4, + IReadOnlyList block_5) + { + Block_1 = block_1; + Block_2 = block_2; + Block_3 = block_3; + Block_4 = block_4; + Block_5 = block_5; + } + + /// + /// Gets the first raw lifetime data block. + /// + public IReadOnlyList Block_1 { get; init; } + + /// + /// Gets the second raw lifetime data block. + /// + public IReadOnlyList Block_2 { get; init; } + + /// + /// Gets the third raw lifetime data block. + /// + public IReadOnlyList Block_3 { get; init; } + + /// + /// Gets the fourth raw lifetime data block. + /// + public IReadOnlyList Block_4 { get; init; } + + /// + /// Gets the fifth raw lifetime data block. + /// + public IReadOnlyList Block_5 { get; init; } + + /// + /// Gets all five raw lifetime data blocks in index order. + /// + public IReadOnlyList> Blocks => [Block_1, Block_2, Block_3, Block_4, Block_5]; + + public override string ToString() + { + return $"Battery Lifetime Data: Block Lengths: {string.Join(", ", Blocks.Select(block => block.Count.ToString(CultureInfo.InvariantCulture)))}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkBatterySafetySnapshot.cs b/framework-dotnet/Snapshots/FrameworkBatterySafetySnapshot.cs new file mode 100644 index 0000000..afce16d --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkBatterySafetySnapshot.cs @@ -0,0 +1,64 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the Smart Battery operation, safety and permanent-failure status words. +/// +/// +/// These registers live behind manufacturer access and are only readable once the pack has been unsealed, so this snapshot is only produced when is . The words are reported verbatim; their bit layout is defined by the pack's gas-gauge firmware and is deliberately not interpreted here. +/// +public sealed record FrameworkBatterySafetySnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The raw gas-gauge operation status word. + /// The raw safety alert word. + /// The raw safety status word. + /// The raw permanent-failure alert word. + /// The raw permanent-failure status word. + public FrameworkBatterySafetySnapshot( + uint operationStatus, + uint safetyAlert, + uint safetyStatus, + uint permanentFailureAlert, + uint permanentFailureStatus) + { + OperationStatus = operationStatus; + SafetyAlert = safetyAlert; + SafetyStatus = safetyStatus; + PermanentFailureAlert = permanentFailureAlert; + PermanentFailureStatus = permanentFailureStatus; + } + + /// + /// Gets the raw gas-gauge operation status word. + /// + public uint OperationStatus { get; init; } + + /// + /// Gets the raw safety alert word, which reports conditions the pack is currently warning about. + /// + public uint SafetyAlert { get; init; } + + /// + /// Gets the raw safety status word, which reports conditions the pack has latched. + /// + public uint SafetyStatus { get; init; } + + /// + /// Gets the raw permanent-failure alert word. + /// + public uint PermanentFailureAlert { get; init; } + + /// + /// Gets the raw permanent-failure status word. A non-zero value indicates the pack has permanently disabled itself. + /// + public uint PermanentFailureStatus { get; init; } + + public override string ToString() + { + return $"Battery Safety: Operation Status: 0x{OperationStatus.ToString("X8", CultureInfo.InvariantCulture)}, Safety Alert: 0x{SafetyAlert.ToString("X8", CultureInfo.InvariantCulture)}, Safety Status: 0x{SafetyStatus.ToString("X8", CultureInfo.InvariantCulture)}, PF Alert: 0x{PermanentFailureAlert.ToString("X8", CultureInfo.InvariantCulture)}, PF Status: 0x{PermanentFailureStatus.ToString("X8", CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkBatteryStateOfHealthSnapshot.cs b/framework-dotnet/Snapshots/FrameworkBatteryStateOfHealthSnapshot.cs new file mode 100644 index 0000000..bd18964 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkBatteryStateOfHealthSnapshot.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Globalization; + +using UnitsNet; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the Smart Battery state-of-health block read through manufacturer access. +/// +/// +/// The block is only readable once the pack has been unsealed, so this snapshot is only produced when is . The first two little-endian 16-bit words of the block carry the remaining health expressed in milliampere-hours and in centiwatt-hours respectively; anything beyond them is vendor specific and is surfaced only through . +/// +public sealed record FrameworkBatteryStateOfHealthSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The state-of-health charge capacity, or when the block was too short to carry it. + /// The state-of-health energy capacity, or when the block was too short to carry it. + /// The complete raw state-of-health block exactly as the pack returned it. + public FrameworkBatteryStateOfHealthSnapshot(ElectricCharge? chargeCapacity, Energy? energyCapacity, IReadOnlyList rawData) + { + ChargeCapacity = chargeCapacity; + EnergyCapacity = energyCapacity; + RawData = rawData; + } + + /// + /// Gets the state-of-health charge capacity, or when the block was too short to carry it. + /// + public ElectricCharge? ChargeCapacity { get; init; } + + /// + /// Gets the state-of-health energy capacity, or when the block was too short to carry it. + /// + public Energy? EnergyCapacity { get; init; } + + /// + /// Gets the complete raw state-of-health block exactly as the pack returned it. + /// + public IReadOnlyList RawData { get; init; } + + public override string ToString() + { + string chargeCapacity = ChargeCapacity.HasValue ? ChargeCapacity.Value.ToString(CultureInfo.InvariantCulture) : "unavailable"; + string energyCapacity = EnergyCapacity.HasValue ? EnergyCapacity.Value.ToString(CultureInfo.InvariantCulture) : "unavailable"; + + return $"Battery State Of Health: Charge Capacity: {chargeCapacity}, Energy Capacity: {energyCapacity}, Raw Bytes: {RawData.Count.ToString(CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkChargingStateSnapshot.cs b/framework-dotnet/Snapshots/FrameworkChargingStateSnapshot.cs new file mode 100644 index 0000000..6d1e8eb --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkChargingStateSnapshot.cs @@ -0,0 +1,36 @@ +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the charging state reported by the embedded controller. +/// +public sealed record FrameworkChargingStateSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether the battery is currently being charged. + /// A value indicating whether an external power adapter is attached. + public FrameworkChargingStateSnapshot(bool isCharging, bool isAcPresent) + { + IsCharging = isCharging; + IsAcPresent = isAcPresent; + } + + /// + /// Gets a value indicating whether the battery is currently being charged. + /// + public bool IsCharging { get; init; } + + /// + /// Gets a value indicating whether an external power adapter is attached. + /// + /// + /// An adapter can be attached without the battery charging, for example when the pack is already full or a charge limit is active. + /// + public bool IsAcPresent { get; init; } + + public override string ToString() + { + return $"Charging State: Charging: {IsCharging}, AC Present: {IsAcPresent}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcApThrottleSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcApThrottleSnapshot.cs new file mode 100644 index 0000000..a59311e --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcApThrottleSnapshot.cs @@ -0,0 +1,48 @@ +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents whether the embedded controller is throttling the application processor for +/// thermal reasons. +/// +public sealed record FrameworkEcApThrottleSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether the application processor is soft throttled. + /// A value indicating whether the application processor is hard throttled. + public FrameworkEcApThrottleSnapshot(bool softThrottled, bool hardThrottled) + { + SoftThrottled = softThrottled; + HardThrottled = hardThrottled; + } + + /// + /// Gets a value indicating whether the application processor is soft throttled. + /// + /// + /// Soft throttling asks the operating system to reduce demand and is the gentler of the two + /// responses. + /// + public bool SoftThrottled { get; init; } + + /// + /// Gets a value indicating whether the application processor is hard throttled. + /// + /// + /// Hard throttling is applied by the controller itself, without the operating system's + /// cooperation, and indicates a more severe thermal condition than . + /// + public bool HardThrottled { get; init; } + + /// + /// Gets a value indicating whether the application processor is throttled in either way. + /// + public bool Throttled => SoftThrottled || HardThrottled; + + /// + public override string ToString() + { + return $"EC AP Throttle: Soft Throttled: {SoftThrottled}, Hard Throttled: {HardThrottled}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcGpioSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcGpioSnapshot.cs new file mode 100644 index 0000000..1f3b9c0 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcGpioSnapshot.cs @@ -0,0 +1,64 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents a single general-purpose input/output line exposed by the Framework embedded controller. +/// +/// +/// A snapshot is a point-in-time reading. The level is sampled at the moment the +/// embedded controller answers the host command and is not refreshed afterwards. +/// +public sealed record FrameworkEcGpioSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The zero-based position of the line within the embedded controller GPIO table. + /// The firmware-assigned name of the line. + /// A value indicating whether the line reads as logic high. + /// The raw firmware-defined configuration bitmask for the line. + public FrameworkEcGpioSnapshot(int index, string name, bool isHigh, uint flags) + { + Index = index; + Name = name; + IsHigh = isHigh; + Flags = flags; + } + + /// + /// Gets the zero-based position of the line within the embedded controller GPIO table. + /// + public int Index { get; init; } + + /// + /// Gets the firmware-assigned name of the line. + /// + /// + /// The name is the identifier the by-name read and write APIs expect. Firmware truncates names to + /// 32 bytes, so every reported name is safe to pass straight back to those APIs. + /// + public string Name { get; init; } + + /// + /// Gets a value indicating whether the line reads as logic high. + /// + public bool IsHigh { get; init; } + + /// + /// Gets the raw firmware-defined configuration bitmask for the line. + /// + /// + /// The embedded controller reports the pin configuration word verbatim (direction, drive mode, pull + /// resistors, interrupt triggers and lock state). The bit layout belongs to the embedded controller + /// firmware, is not part of the stable native contract, and is therefore surfaced undecoded. Treat it + /// as diagnostic data and do not branch production logic on individual bits. + /// + public uint Flags { get; init; } + + /// + public override string ToString() + { + return $"GPIO {Index.ToString(CultureInfo.InvariantCulture)} ({Name}): {(IsHigh ? "High" : "Low")}, Flags: 0x{Flags.ToString("X8", CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcHelloSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcHelloSnapshot.cs new file mode 100644 index 0000000..39cae42 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcHelloSnapshot.cs @@ -0,0 +1,45 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the answer to the embedded controller hello diagnostic command. +/// +/// +/// A healthy controller answers the payload it was sent plus 0x01020304, computed with +/// unsigned wraparound. reports whether that held. +/// +public sealed record FrameworkEcHelloSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The payload the embedded controller echoed back. + /// A value indicating whether the echoed payload matched the expected transform of the sent payload. + public FrameworkEcHelloSnapshot(uint outData, bool isExpectedEcho) + { + OutData = outData; + IsExpectedEcho = isExpectedEcho; + } + + /// + /// Gets the payload the embedded controller echoed back. + /// + public uint OutData { get; init; } + + /// + /// Gets a value indicating whether the echoed payload matched the expected transform of the sent payload. + /// + /// + /// means the controller answered the command but answered with the + /// wrong value, which points at a corrupt host command transport rather than at a controller + /// that is not responding at all. + /// + public bool IsExpectedEcho { get; init; } + + /// + public override string ToString() + { + return $"EC Hello: Out Data: 0x{OutData.ToString("X8", CultureInfo.InvariantCulture)}, Expected Echo: {IsExpectedEcho}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcPanicInfoSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcPanicInfoSnapshot.cs new file mode 100644 index 0000000..ae6df2f --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcPanicInfoSnapshot.cs @@ -0,0 +1,112 @@ +using System; +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the panic data the embedded controller saved from its last crash. +/// +/// +/// +/// The payload is kept as an opaque blob on purpose: the per-architecture decode structures are +/// private to the upstream firmware headers, so there is no stable managed shape to project them +/// onto. and identify which decoder a +/// caller would need. +/// +/// +/// A snapshot with an empty means the controller simply has no stored panic; +/// that is a normal, healthy reading rather than a failure. +/// +/// +public sealed record FrameworkEcPanicInfoSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The raw panic blob exactly as the controller reported it. + /// The architecture tag read from the blob header. + /// The structure version read from the blob header. + /// The panic data flags read from the blob header. + /// A value indicating whether the blob trailer is self-consistent. + /// The structure size the controller reported in the blob trailer. + /// The magic value read from the blob trailer. + /// Thrown when is . + public FrameworkEcPanicInfoSnapshot(byte[] data, byte architecture, byte structVersion, byte flags, bool isValid, uint structSize, uint magic) + { + ArgumentNullException.ThrowIfNull(data); + + Data = data; + Architecture = architecture; + StructVersion = structVersion; + Flags = flags; + IsValid = isValid; + StructSize = structSize; + Magic = magic; + } + + /// + /// Gets the raw panic blob exactly as the controller reported it. + /// + /// + /// The array is a private copy owned by this snapshot; the native buffer it came from has + /// already been released. + /// + public byte[] Data { get; init; } + + /// + /// Gets the architecture tag read from the blob header. + /// + /// + /// Selects which per-architecture layout the remainder of follows. + /// + public byte Architecture { get; init; } + + /// + /// Gets the structure version read from the blob header. + /// + public byte StructVersion { get; init; } + + /// + /// Gets the panic data flags read from the blob header. + /// + public byte Flags { get; init; } + + /// + /// Gets a value indicating whether the blob trailer is self-consistent. + /// + /// + /// This is only when the trailer magic matches and the reported + /// agrees with the length of . When it is + /// the header and trailer fields should not be trusted. + /// + public bool IsValid { get; init; } + + /// + /// Gets the structure size the controller reported in the blob trailer. + /// + public uint StructSize { get; init; } + + /// + /// Gets the magic value read from the blob trailer. + /// + /// + /// A valid trailer carries 0x21636E50, the little-endian encoding of "Pnc!". + /// + public uint Magic { get; init; } + + /// + /// Gets a value indicating whether the controller reported any stored panic data at all. + /// + public bool HasPanicData => Data.Length > 0; + + /// + public override string ToString() + { + if (!HasPanicData) + { + return "EC Panic Info: No stored panic"; + } + + return $"EC Panic Info: Length: {Data.Length.ToString(CultureInfo.InvariantCulture)} B, Architecture: {Architecture.ToString(CultureInfo.InvariantCulture)}, Struct Version: {StructVersion.ToString(CultureInfo.InvariantCulture)}, Flags: 0x{Flags.ToString("X2", CultureInfo.InvariantCulture)}, Valid: {IsValid}, Struct Size: {StructSize.ToString(CultureInfo.InvariantCulture)}, Magic: 0x{Magic.ToString("X8", CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcPort80HistorySnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcPort80HistorySnapshot.cs new file mode 100644 index 0000000..dd6f9ec --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcPort80HistorySnapshot.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +using FrameworkDotnet.Enums; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the port 80 POST code history recorded by the embedded controller. +/// +/// +/// +/// The controller keeps POST codes in a wrapping ring buffer. is that ring +/// in raw buffer order, and walks it backwards from the newest +/// entry so that a caller can read the boot in reverse chronological order without doing the +/// modular arithmetic itself. +/// +/// +/// Entries whose value matches a member are markers the +/// controller inserted rather than POST codes emitted by host firmware. Use +/// to classify an entry. +/// +/// +public sealed record FrameworkEcPort80HistorySnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The total number of port 80 writes the controller has recorded since it booted. + /// The size of the controller's history buffer, in entries. + /// The history ring in raw buffer order. + /// Thrown when is . + public FrameworkEcPort80HistorySnapshot(uint writes, uint historySize, IReadOnlyList codes) + { + ArgumentNullException.ThrowIfNull(codes); + + Writes = writes; + HistorySize = historySize; + Codes = codes; + + var count = codes.Count; + if (count == 0 || writes == 0) + { + NewestIndex = -1; + CodesNewestFirst = []; + return; + } + + // `writes` is the next slot the controller will write, so the newest entry is the slot + // before it. Upstream's own printer walks `tail..head` exclusive of `head` and labels the + // last code it emits as the newest, which is `codes[(writes - 1) % history_size]`. + var newestIndex = (int)((writes - 1u) % (uint)count); + + // Before the ring has wrapped only `writes` slots hold real codes; the rest were never + // written. Upstream clamps the same way with `tail = head.saturating_sub(history_size)`. + var populated = writes < (uint)count ? (int)writes : count; + + var ordered = new ushort[populated]; + for (var offset = 0; offset < populated; offset++) + { + ordered[offset] = codes[((newestIndex - offset) + count) % count]; + } + + NewestIndex = newestIndex; + CodesNewestFirst = ordered; + } + + /// + /// Gets the total number of port 80 writes the controller has recorded since it booted. + /// + /// + /// This counts every write, so it keeps growing after the ring has wrapped and is therefore + /// larger than on any system that has been running for a while. + /// + public uint Writes { get; init; } + + /// + /// Gets the size of the controller's history buffer, in entries. + /// + public uint HistorySize { get; init; } + + /// + /// Gets the history ring in raw buffer order. + /// + /// + /// This is the buffer exactly as the controller stores it, so index 0 is the start of the + /// ring and not the oldest entry. points at the newest entry. + /// + public IReadOnlyList Codes { get; init; } + + /// + /// Gets the history walked backwards from the newest entry, so that index 0 is the most + /// recently written POST code. + /// + /// + /// Only slots the controller has actually written are included, so before the ring has + /// wrapped this is shorter than and is empty when + /// is zero. + /// + public IReadOnlyList CodesNewestFirst { get; init; } + + /// + /// Gets the index into of the newest entry, or -1 when the + /// controller returned no entries or has recorded no writes. + /// + /// + /// Computed as (writes - 1) % history_size. is the slot the + /// controller will write next, so the newest entry is the slot before it; upstream's own + /// history printer walks an exclusive upper bound and labels the same slot as the newest. + /// The writes % history_size formula stated in the native ABI comment names the next + /// write slot, which holds the oldest entry once the ring has wrapped. + /// + public int NewestIndex { get; init; } + + /// + /// Classifies a history entry as one of the marker events the controller inserts. + /// + /// The history entry to classify. + /// The marker event the entry represents, or when the entry is an ordinary POST code. + public static FrameworkPort80Event? GetMarkerEvent(ushort code) + { + return code switch + { + (ushort)FrameworkPort80Event.Resume => FrameworkPort80Event.Resume, + (ushort)FrameworkPort80Event.Reset => FrameworkPort80Event.Reset, + _ => null, + }; + } + + /// + public override string ToString() + { + var preview = string.Join(", ", CodesNewestFirst.Take(8).Select(static code => $"0x{code.ToString("X4", CultureInfo.InvariantCulture)}")); + + return $"EC Port 80 History: Writes: {Writes.ToString(CultureInfo.InvariantCulture)}, History Size: {HistorySize.ToString(CultureInfo.InvariantCulture)}, Entries: {Codes.Count.ToString(CultureInfo.InvariantCulture)}, Newest Index: {NewestIndex.ToString(CultureInfo.InvariantCulture)}, Newest First: [{preview}]"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcProtocolInfoSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcProtocolInfoSnapshot.cs new file mode 100644 index 0000000..b6cf16e --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcProtocolInfoSnapshot.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Globalization; + +using FrameworkDotnet.Enums; + +using UnitsNet; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the host command protocol capabilities reported by the embedded controller. +/// +public sealed record FrameworkEcProtocolInfoSnapshot +{ + /// + /// The number of bits in , and therefore one past the + /// highest protocol version the mask can describe. + /// + private const int ProtocolVersionBitCount = 32; + + /// + /// Initializes a new instance of the class. + /// + /// The bitmask of supported host command protocol versions, where bit N set means version N is supported. + /// The largest host command request packet the controller accepts. + /// The largest host command response packet the controller produces. + /// The optional protocol capabilities the controller advertises. + public FrameworkEcProtocolInfoSnapshot(uint protocolVersionMask, Information maxRequestPacketSize, Information maxResponsePacketSize, FrameworkEcProtocolFlag flags) + { + ProtocolVersionMask = protocolVersionMask; + MaxRequestPacketSize = maxRequestPacketSize; + MaxResponsePacketSize = maxResponsePacketSize; + Flags = flags; + + var supportedVersions = new List(); + for (var version = 0; version < ProtocolVersionBitCount; version++) + { + if ((protocolVersionMask & (1U << version)) != 0U) + { + supportedVersions.Add(version); + } + } + + SupportedProtocolVersions = supportedVersions; + } + + /// + /// Gets the bitmask of supported host command protocol versions, where bit N set means + /// version N is supported. + /// + /// + /// This is a plain version bitmask and is unrelated to , which describes + /// optional protocol capabilities. + /// + public uint ProtocolVersionMask { get; init; } + + /// + /// Gets the supported host command protocol versions in ascending order. + /// + public IReadOnlyList SupportedProtocolVersions { get; init; } + + /// + /// Gets the largest host command request packet the controller accepts. + /// + public Information MaxRequestPacketSize { get; init; } + + /// + /// Gets the largest host command response packet the controller produces. + /// + public Information MaxResponsePacketSize { get; init; } + + /// + /// Gets the optional protocol capabilities the controller advertises. + /// + public FrameworkEcProtocolFlag Flags { get; init; } + + /// + /// Determines whether the controller supports a given host command protocol version. + /// + /// The protocol version to test. + /// when is supported; otherwise . + /// + /// Versions outside the range the bitmask can describe are reported as unsupported rather + /// than rejected, so a caller can probe freely. + /// + public bool IsProtocolVersionSupported(int version) + { + if (version < 0 || version >= ProtocolVersionBitCount) + { + return false; + } + + return (ProtocolVersionMask & (1U << version)) != 0U; + } + + /// + public override string ToString() + { + return $"EC Protocol Info: Supported Versions: {string.Join(", ", SupportedProtocolVersions)}, Max Request: {MaxRequestPacketSize.Bytes.ToString(CultureInfo.InvariantCulture)} B, Max Response: {MaxResponsePacketSize.Bytes.ToString(CultureInfo.InvariantCulture)} B, Flags: {Flags}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcSwitchesSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcSwitchesSnapshot.cs new file mode 100644 index 0000000..c93b581 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcSwitchesSnapshot.cs @@ -0,0 +1,75 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the live positions of the physical switches the embedded controller monitors. +/// +public sealed record FrameworkEcSwitchesSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The raw switch byte as read from the memory-mapped region. + /// A value indicating whether the lid is open. + /// A value indicating whether the power button is currently held down. + /// A value indicating whether firmware write protect is disabled. + /// A value indicating whether the dedicated recovery switch is asserted. + public FrameworkEcSwitchesSnapshot(byte rawSwitchByte, bool lidOpen, bool powerButtonPressed, bool writeProtectDisabled, bool dedicatedRecovery) + { + RawSwitchByte = rawSwitchByte; + LidOpen = lidOpen; + PowerButtonPressed = powerButtonPressed; + WriteProtectDisabled = writeProtectDisabled; + DedicatedRecovery = dedicatedRecovery; + } + + /// + /// Gets the raw switch byte as read from the memory-mapped region. + /// + /// + /// Exposed for callers that need bits the managed surface does not name. Prefer the named + /// properties wherever they cover the bit of interest. + /// + public byte RawSwitchByte { get; init; } + + /// + /// Gets a value indicating whether the lid is open. + /// + public bool LidOpen { get; init; } + + /// + /// Gets a value indicating whether the power button is currently held down. + /// + public bool PowerButtonPressed { get; init; } + + /// + /// Gets a value indicating whether firmware write protect is disabled. + /// + /// + /// The underlying hardware bit has inverted sense: it is set when write protect is + /// disabled. This property preserves that sense; use + /// for the positive reading. + /// + public bool WriteProtectDisabled { get; init; } + + /// + /// Gets a value indicating whether the dedicated recovery switch is asserted. + /// + public bool DedicatedRecovery { get; init; } + + /// + /// Gets a value indicating whether firmware write protect is currently in force. + /// + /// + /// This is the inverse of , provided so that callers do + /// not have to reason about the inverted hardware sense. + /// + public bool WriteProtected => !WriteProtectDisabled; + + /// + public override string ToString() + { + return $"EC Switches: Lid Open: {LidOpen}, Power Button Pressed: {PowerButtonPressed}, Write Protected: {WriteProtected}, Dedicated Recovery: {DedicatedRecovery}, Raw: 0x{RawSwitchByte.ToString("X2", CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkEcSystemInfoSnapshot.cs b/framework-dotnet/Snapshots/FrameworkEcSystemInfoSnapshot.cs new file mode 100644 index 0000000..e9aced5 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkEcSystemInfoSnapshot.cs @@ -0,0 +1,48 @@ +using FrameworkDotnet.Enums; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the embedded controller system information: which firmware image is running, why +/// the controller last reset, and its current lock and jump state. +/// +public sealed record FrameworkEcSystemInfoSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The firmware image the controller is currently executing. + /// The reasons recorded for the controller's most recent reset. + /// The controller's current lock and jump state. + public FrameworkEcSystemInfoSnapshot(FrameworkEcCurrentImage currentImage, FrameworkEcResetFlag resetFlags, FrameworkEcSysinfoFlag flags) + { + CurrentImage = currentImage; + ResetFlags = resetFlags; + Flags = flags; + } + + /// + /// Gets the firmware image the controller is currently executing. + /// + public FrameworkEcCurrentImage CurrentImage { get; init; } + + /// + /// Gets the reasons recorded for the controller's most recent reset. + /// + /// + /// More than one reason can be recorded for a single reset, so test individual flags rather + /// than comparing the whole value. + /// + public FrameworkEcResetFlag ResetFlags { get; init; } + + /// + /// Gets the controller's current lock and jump state. + /// + public FrameworkEcSysinfoFlag Flags { get; init; } + + /// + public override string ToString() + { + return $"EC System Info: Current Image: {CurrentImage}, Reset Flags: {ResetFlags}, Flags: {Flags}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkKeyboardColor.cs b/framework-dotnet/Snapshots/FrameworkKeyboardColor.cs new file mode 100644 index 0000000..e11841a --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkKeyboardColor.cs @@ -0,0 +1,51 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents a single per-key keyboard color expressed as 8-bit red, green and blue components. +/// +/// +/// The embedded controller consumes per-key colors as three consecutive bytes in red, green, blue order. +/// This type exists so that a managed caller cannot accidentally supply a byte buffer with the wrong stride: +/// one instance always describes exactly one key. +/// +public readonly record struct FrameworkKeyboardColor +{ + /// + /// Initializes a new instance of the struct. + /// + /// The red component. + /// The green component. + /// The blue component. + public FrameworkKeyboardColor(byte red, byte green, byte blue) + { + Red = red; + Green = green; + Blue = blue; + } + + /// + /// Gets the red component of the color. + /// + public byte Red { get; init; } + + /// + /// Gets the green component of the color. + /// + public byte Green { get; init; } + + /// + /// Gets the blue component of the color. + /// + public byte Blue { get; init; } + + /// + /// Returns a culture-invariant textual representation of the color. + /// + /// A string describing the red, green and blue components. + public override string ToString() + { + return $"Keyboard Color: R: {Red.ToString(CultureInfo.InvariantCulture)}, G: {Green.ToString(CultureInfo.InvariantCulture)}, B: {Blue.ToString(CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkNvmeVersionSnapshot.cs b/framework-dotnet/Snapshots/FrameworkNvmeVersionSnapshot.cs new file mode 100644 index 0000000..2b8b0d5 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkNvmeVersionSnapshot.cs @@ -0,0 +1,43 @@ +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the identity strings an NVMe drive reports through its Identify Controller data. +/// +/// +/// Both strings come straight from the drive's Identify Controller structure and are already trimmed of +/// the padding spaces the NVMe specification mandates. Either can be empty when the drive leaves the +/// corresponding field blank. +/// +public sealed record FrameworkNvmeVersionSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The model number the drive reports. + /// The firmware revision the drive reports. + public FrameworkNvmeVersionSnapshot(string modelNumber, string firmwareVersion) + { + ModelNumber = modelNumber; + FirmwareVersion = firmwareVersion; + } + + /// + /// Gets the model number the drive reports. + /// + public string ModelNumber { get; init; } + + /// + /// Gets the firmware revision the drive reports. + /// + /// + /// This is a vendor-defined revision string, not a dotted version number, so it is surfaced verbatim + /// rather than parsed. Compare it for equality; do not attempt an ordering comparison. + /// + public string FirmwareVersion { get; init; } + + /// + public override string ToString() + { + return $"NVMe Drive: Model: {ModelNumber}, Firmware: {FirmwareVersion}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPeripheralVersionSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPeripheralVersionSnapshot.cs new file mode 100644 index 0000000..9ff7908 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPeripheralVersionSnapshot.cs @@ -0,0 +1,119 @@ +using System; +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents one USB or HID peripheral and the firmware version it reports. +/// +/// +/// +/// The peripheral occupies a fixed slot in the enclosing , +/// so an empty slot is still reported. When is no device +/// was found for that slot and every other member carries no meaningful data. +/// +/// +/// For cameras, Framework 16 input modules and USB hubs the version is decoded from the USB +/// bcdDevice descriptor field; for the audio expansion card it comes from the Synaptics CAPE +/// version command instead. Both are reported through the same three components. +/// +/// +public sealed record FrameworkPeripheralVersionSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The zero-based fixed slot the peripheral occupies. + /// A value indicating whether the slot is populated. + /// The major component of the reported firmware version. + /// The minor component of the reported firmware version. + /// The sub-minor component of the reported firmware version. + /// The USB vendor identifier of the peripheral. + /// The USB product identifier of the peripheral. + /// The USB product string of the peripheral, or an empty string when it could not be read. + public FrameworkPeripheralVersionSnapshot(int slotIndex, bool isPresent, byte major, byte minor, byte subMinor, ushort vendorId, ushort productId, string productName) + { + SlotIndex = slotIndex; + IsPresent = isPresent; + Major = major; + Minor = minor; + SubMinor = subMinor; + VendorId = vendorId; + ProductId = productId; + ProductName = productName; + } + + /// + /// Gets the zero-based fixed slot the peripheral occupies. + /// + /// + /// The slot is a position in the native report, not a physical port. It is stable only within a single + /// read, so do not persist it as a device identity; use and + /// for that. + /// + public int SlotIndex { get; init; } + + /// + /// Gets a value indicating whether the slot is populated. + /// + /// This flag is authoritative. Read the remaining members only when it is . + public bool IsPresent { get; init; } + + /// + /// Gets the major component of the reported firmware version. + /// + public byte Major { get; init; } + + /// + /// Gets the minor component of the reported firmware version. + /// + public byte Minor { get; init; } + + /// + /// Gets the sub-minor component of the reported firmware version. + /// + public byte SubMinor { get; init; } + + /// + /// Gets the USB vendor identifier of the peripheral. + /// + public ushort VendorId { get; init; } + + /// + /// Gets the USB product identifier of the peripheral. + /// + public ushort ProductId { get; init; } + + /// + /// Gets the USB product string of the peripheral, or an empty string when it could not be read. + /// + /// + /// Reading the product string requires opening the device. A device that is present but could not be + /// opened, typically for want of permission, still reports its version and identifiers with an empty + /// product name. + /// + public string ProductName { get; init; } + + /// + /// Gets the reported firmware version as a comparable value. + /// + /// + /// The components map onto , and + /// ; the revision component is unused. The value is only meaningful + /// when is . + /// + public Version Version => new Version(Major, Minor, SubMinor); + + /// + public override string ToString() + { + if (!IsPresent) + { + return $"Peripheral {SlotIndex.ToString(CultureInfo.InvariantCulture)}: Not Present"; + } + + string name = string.IsNullOrEmpty(ProductName) ? "Unknown" : ProductName; + + return $"Peripheral {SlotIndex.ToString(CultureInfo.InvariantCulture)} ({name}): {Major.ToString(CultureInfo.InvariantCulture)}.{Minor.ToString(CultureInfo.InvariantCulture)}.{SubMinor.ToString(CultureInfo.InvariantCulture)}, USB {VendorId.ToString("X4", CultureInfo.InvariantCulture)}:{ProductId.ToString("X4", CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPeripheralVersionsSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPeripheralVersionsSnapshot.cs new file mode 100644 index 0000000..93fac9c --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPeripheralVersionsSnapshot.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the firmware versions of one category of USB or HID peripherals. +/// +/// +/// +/// The native layer reports a fixed table of eight slots for every category, filled from slot zero +/// upwards, and states how many of them it populated in . Enumerate +/// rather than indexing blindly; the individual slot properties exist +/// only to mirror the native layout. +/// +/// +/// A category with nothing connected reports a of zero. That is a normal reading and +/// not an error: a system with no audio expansion card fitted, or a camera the host could not enumerate, +/// answers successfully with an empty table. +/// +/// +public sealed record FrameworkPeripheralVersionsSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The number of populated slots reported by the native layer. + /// The first peripheral slot. + /// The second peripheral slot. + /// The third peripheral slot. + /// The fourth peripheral slot. + /// The fifth peripheral slot. + /// The sixth peripheral slot. + /// The seventh peripheral slot. + /// The eighth peripheral slot. + public FrameworkPeripheralVersionsSnapshot(byte count, FrameworkPeripheralVersionSnapshot peripheral_0, FrameworkPeripheralVersionSnapshot peripheral_1, FrameworkPeripheralVersionSnapshot peripheral_2, FrameworkPeripheralVersionSnapshot peripheral_3, FrameworkPeripheralVersionSnapshot peripheral_4, FrameworkPeripheralVersionSnapshot peripheral_5, FrameworkPeripheralVersionSnapshot peripheral_6, FrameworkPeripheralVersionSnapshot peripheral_7) + { + Count = count; + Peripheral_0 = peripheral_0; + Peripheral_1 = peripheral_1; + Peripheral_2 = peripheral_2; + Peripheral_3 = peripheral_3; + Peripheral_4 = peripheral_4; + Peripheral_5 = peripheral_5; + Peripheral_6 = peripheral_6; + Peripheral_7 = peripheral_7; + } + + /// + /// Gets the number of populated slots reported by the native layer. + /// + /// + /// Slots are filled contiguously from index zero, so the populated slots are the first + /// entries of . The value never exceeds eight, the fixed + /// table size of the native report; a category with more devices attached is truncated. + /// + public byte Count { get; init; } + + /// + /// Gets the first peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_0 { get; init; } + + /// + /// Gets the second peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_1 { get; init; } + + /// + /// Gets the third peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_2 { get; init; } + + /// + /// Gets the fourth peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_3 { get; init; } + + /// + /// Gets the fifth peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_4 { get; init; } + + /// + /// Gets the sixth peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_5 { get; init; } + + /// + /// Gets the seventh peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_6 { get; init; } + + /// + /// Gets the eighth peripheral slot. + /// + public FrameworkPeripheralVersionSnapshot Peripheral_7 { get; init; } + + /// + /// Gets all peripheral slots in index order, populated or not. + /// + public IReadOnlyList Peripherals => [Peripheral_0, Peripheral_1, Peripheral_2, Peripheral_3, Peripheral_4, Peripheral_5, Peripheral_6, Peripheral_7]; + + /// + /// Gets the populated peripheral slots in index order. + /// + /// + public IEnumerable ReportedPeripherals => Peripherals.Take(Count); + + /// + public override string ToString() + { + return Count == 0 + ? "Peripheral Versions: None Reported" + : $"Peripheral Versions: Count: {Count.ToString(CultureInfo.InvariantCulture)}, Peripherals: {string.Join(", ", ReportedPeripherals)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryApplicationVersionSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryApplicationVersionSnapshot.cs new file mode 100644 index 0000000..086af06 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryApplicationVersionSnapshot.cs @@ -0,0 +1,53 @@ +using System.Globalization; + +using FrameworkDotnet.Enums; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the Cypress application firmware version of a USB Power Delivery controller image. +/// +/// The native layer formats this version as Major.Minor.Circuit. +public sealed record FrameworkPowerDeliveryApplicationVersionSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The application the firmware image targets. + /// The major component of the application version. + /// The minor component of the application version. + /// The circuit component of the application version. + public FrameworkPowerDeliveryApplicationVersionSnapshot(FrameworkPdApplication application, byte major, byte minor, byte circuit) + { + Application = application; + Major = major; + Minor = minor; + Circuit = circuit; + } + + /// + /// Gets the application the firmware image targets. + /// + public FrameworkPdApplication Application { get; init; } + + /// + /// Gets the major component of the application version. + /// + public byte Major { get; init; } + + /// + /// Gets the minor component of the application version. + /// + public byte Minor { get; init; } + + /// + /// Gets the circuit component of the application version. + /// + public byte Circuit { get; init; } + + /// + public override string ToString() + { + return $"{Major.ToString(CultureInfo.InvariantCulture)}.{Minor.ToString(CultureInfo.InvariantCulture)}.{Circuit.ToString(CultureInfo.InvariantCulture)} ({Application})"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryBaseVersionSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryBaseVersionSnapshot.cs new file mode 100644 index 0000000..e036a9b --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryBaseVersionSnapshot.cs @@ -0,0 +1,51 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the Cypress base firmware version of a USB Power Delivery controller image. +/// +/// The native layer formats this version as Major.Minor.Patch.BuildNumber. +public sealed record FrameworkPowerDeliveryBaseVersionSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The major component of the base version. + /// The minor component of the base version. + /// The patch component of the base version. + /// The build number of the base version. + public FrameworkPowerDeliveryBaseVersionSnapshot(byte major, byte minor, byte patch, ushort buildNumber) + { + Major = major; + Minor = minor; + Patch = patch; + BuildNumber = buildNumber; + } + + /// + /// Gets the major component of the base version. + /// + public byte Major { get; init; } + + /// + /// Gets the minor component of the base version. + /// + public byte Minor { get; init; } + + /// + /// Gets the patch component of the base version. + /// + public byte Patch { get; init; } + + /// + /// Gets the build number of the base version. + /// + public ushort BuildNumber { get; init; } + + /// + public override string ToString() + { + return $"{Major.ToString(CultureInfo.InvariantCulture)}.{Minor.ToString(CultureInfo.InvariantCulture)}.{Patch.ToString(CultureInfo.InvariantCulture)}.{BuildNumber.ToString(CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerFirmwareSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerFirmwareSnapshot.cs new file mode 100644 index 0000000..e8fb062 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerFirmwareSnapshot.cs @@ -0,0 +1,71 @@ +using FrameworkDotnet.Enums; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the three firmware images stored on a single USB Power Delivery controller. +/// +/// +/// The controller occupies a fixed slot, so an unpopulated slot is still reported. When is the controller +/// is not fitted on this platform and , , and carry no meaningful data. +/// +public sealed record FrameworkPowerDeliveryControllerFirmwareSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The fixed slot the controller occupies. + /// A value indicating whether the slot is populated on this platform. + /// The firmware image the controller is currently running. + /// The version of the boot loader image. + /// The version of the backup firmware image. + /// The version of the main firmware image. + public FrameworkPowerDeliveryControllerFirmwareSnapshot(FrameworkPowerDeliveryControllerSlot slot, bool isPresent, FrameworkPdFwMode activeFirmware, FrameworkPowerDeliveryControllerImageSnapshot bootLoader, FrameworkPowerDeliveryControllerImageSnapshot backupFirmware, FrameworkPowerDeliveryControllerImageSnapshot mainFirmware) + { + Slot = slot; + IsPresent = isPresent; + ActiveFirmware = activeFirmware; + BootLoader = bootLoader; + BackupFirmware = backupFirmware; + MainFirmware = mainFirmware; + } + + /// + /// Gets the fixed slot the controller occupies. + /// + public FrameworkPowerDeliveryControllerSlot Slot { get; init; } + + /// + /// Gets a value indicating whether the slot is populated on this platform. + /// + /// This flag is authoritative. Read the firmware versions only when it is . + public bool IsPresent { get; init; } + + /// + /// Gets the firmware image the controller is currently running. + /// + public FrameworkPdFwMode ActiveFirmware { get; init; } + + /// + /// Gets the version of the boot loader image. + /// + public FrameworkPowerDeliveryControllerImageSnapshot BootLoader { get; init; } + + /// + /// Gets the version of the backup firmware image. + /// + public FrameworkPowerDeliveryControllerImageSnapshot BackupFirmware { get; init; } + + /// + /// Gets the version of the main firmware image. + /// + public FrameworkPowerDeliveryControllerImageSnapshot MainFirmware { get; init; } + + /// + public override string ToString() + { + return IsPresent + ? $"Power Delivery Controller {Slot}: Active: {ActiveFirmware}, Boot Loader: [{BootLoader}], Backup: [{BackupFirmware}], Main: [{MainFirmware}]" + : $"Power Delivery Controller {Slot}: Not Present"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerImageSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerImageSnapshot.cs new file mode 100644 index 0000000..f6d8c52 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerImageSnapshot.cs @@ -0,0 +1,35 @@ +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the version pair of a single firmware image stored on a USB Power Delivery controller. +/// +/// Each image carries both a Cypress base version and a Cypress application version. +public sealed record FrameworkPowerDeliveryControllerImageSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The Cypress base version of the image. + /// The Cypress application version of the image. + public FrameworkPowerDeliveryControllerImageSnapshot(FrameworkPowerDeliveryBaseVersionSnapshot baseVersion, FrameworkPowerDeliveryApplicationVersionSnapshot applicationVersion) + { + BaseVersion = baseVersion; + ApplicationVersion = applicationVersion; + } + + /// + /// Gets the Cypress base version of the image. + /// + public FrameworkPowerDeliveryBaseVersionSnapshot BaseVersion { get; init; } + + /// + /// Gets the Cypress application version of the image. + /// + public FrameworkPowerDeliveryApplicationVersionSnapshot ApplicationVersion { get; init; } + + /// + public override string ToString() + { + return $"Base: {BaseVersion}, App: {ApplicationVersion}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerVersionsSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerVersionsSnapshot.cs new file mode 100644 index 0000000..8eef540 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryControllerVersionsSnapshot.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the firmware versions of every USB Power Delivery controller slot reported by the embedded controller. +/// +/// +/// The three controller slots are fixed and are always reported: Controller_0 is the right-hand controller, Controller_1 the left-hand controller and +/// Controller_2 the rear controller of a Framework Desktop. Framework laptops populate slots 0 and 1, Framework Desktop populates slot 2 only, so the +/// populated slots are not contiguous. Enumerate rather than indexing blindly. +/// +public sealed record FrameworkPowerDeliveryControllerVersionsSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The number of populated controller slots reported by the native layer. + /// The right-hand controller slot. + /// The left-hand controller slot. + /// The rear controller slot. + public FrameworkPowerDeliveryControllerVersionsSnapshot(byte controllerCount, FrameworkPowerDeliveryControllerFirmwareSnapshot controller_0, FrameworkPowerDeliveryControllerFirmwareSnapshot controller_1, FrameworkPowerDeliveryControllerFirmwareSnapshot controller_2) + { + ControllerCount = controllerCount; + Controller_0 = controller_0; + Controller_1 = controller_1; + Controller_2 = controller_2; + } + + /// + /// Gets the number of populated controller slots reported by the native layer. + /// + /// + /// This is a count of populated slots, not a contiguous slot range: a Framework Desktop reports a single populated controller that occupies slot index 2. + /// Use it to size a display, but use to decide whether a given slot may be read. + /// + public byte ControllerCount { get; init; } + + /// + /// Gets the right-hand controller slot. + /// + public FrameworkPowerDeliveryControllerFirmwareSnapshot Controller_0 { get; init; } + + /// + /// Gets the left-hand controller slot. + /// + public FrameworkPowerDeliveryControllerFirmwareSnapshot Controller_1 { get; init; } + + /// + /// Gets the rear controller slot. + /// + public FrameworkPowerDeliveryControllerFirmwareSnapshot Controller_2 { get; init; } + + /// + /// Gets all controller slots in index order, populated or not. + /// + public IReadOnlyList Controllers => [Controller_0, Controller_1, Controller_2]; + + /// + /// Gets the populated controller slots in index order. + /// + /// + public IEnumerable PresentControllers => Controllers.Where(controller => controller.IsPresent); + + /// + public override string ToString() + { + return $"Power Delivery Controller Versions: Controller Count: {ControllerCount.ToString(CultureInfo.InvariantCulture)}, Controllers: {string.Join(", ", PresentControllers)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryPowerInfoSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryPowerInfoSnapshot.cs new file mode 100644 index 0000000..3b3c3a5 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryPowerInfoSnapshot.cs @@ -0,0 +1,94 @@ +using System.Globalization; + +using FrameworkDotnet.Enums; + +using UnitsNet; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the charger negotiation state of a single USB Power Delivery port as reported by the embedded controller. +/// +/// +/// This snapshot describes what the attached charger offers and what the port has negotiated from it. It is distinct from +/// , which is surfaced through the module inventory and describes the USB Type-C link itself +/// (connection state, data role, CC orientation and alt-mode bits). Reading one does not tell you what the other reports. +/// +public sealed record FrameworkPowerDeliveryPowerInfoSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The zero-based port index the reading belongs to. + /// A value indicating whether the port supports dual-role power. + /// The negotiated power role of the port. + /// The way the attached charger is supplying power. + /// The maximum voltage the attached charger advertises. + /// The voltage currently present on the port. + /// The maximum current the attached charger advertises. + /// The current limit currently in force on the port. + /// The maximum negotiated power. + public FrameworkPowerDeliveryPowerInfoSnapshot(byte port, bool supportsDualRole, FrameworkUsbPowerRole role, FrameworkUsbChargingType chargingType, ElectricPotential maximumVoltage, ElectricPotential voltage, ElectricCurrent maximumCurrent, ElectricCurrent currentLimit, Power maximumPower) + { + Port = port; + SupportsDualRole = supportsDualRole; + Role = role; + ChargingType = chargingType; + MaximumVoltage = maximumVoltage; + Voltage = voltage; + MaximumCurrent = maximumCurrent; + CurrentLimit = currentLimit; + MaximumPower = maximumPower; + } + + /// + /// Gets the zero-based port index the reading belongs to. + /// + public byte Port { get; init; } + + /// + /// Gets a value indicating whether the port supports dual-role power. + /// + public bool SupportsDualRole { get; init; } + + /// + /// Gets the negotiated power role of the port. + /// + public FrameworkUsbPowerRole Role { get; init; } + + /// + /// Gets the way the attached charger is supplying power. + /// + public FrameworkUsbChargingType ChargingType { get; init; } + + /// + /// Gets the maximum voltage the attached charger advertises. + /// + public ElectricPotential MaximumVoltage { get; init; } + + /// + /// Gets the voltage currently present on the port. + /// + public ElectricPotential Voltage { get; init; } + + /// + /// Gets the maximum current the attached charger advertises. + /// + public ElectricCurrent MaximumCurrent { get; init; } + + /// + /// Gets the current limit currently in force on the port. + /// + public ElectricCurrent CurrentLimit { get; init; } + + /// + /// Gets the maximum negotiated power. + /// + public Power MaximumPower { get; init; } + + /// + public override string ToString() + { + return $"Power Delivery Power Info: Port: {Port.ToString(CultureInfo.InvariantCulture)}, Role: {Role}, Charging Type: {ChargingType}, Dual Role: {SupportsDualRole}, Voltage: {Voltage.ToString(CultureInfo.InvariantCulture)} (max {MaximumVoltage.ToString(CultureInfo.InvariantCulture)}), Current Limit: {CurrentLimit.ToString(CultureInfo.InvariantCulture)} (max {MaximumCurrent.ToString(CultureInfo.InvariantCulture)}), Maximum Power: {MaximumPower.ToString(CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkPowerDeliveryRetimerVersionSnapshot.cs b/framework-dotnet/Snapshots/FrameworkPowerDeliveryRetimerVersionSnapshot.cs new file mode 100644 index 0000000..5a0f568 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkPowerDeliveryRetimerVersionSnapshot.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the Parade retimer firmware version reported by the embedded controller. +/// +/// +/// +/// The retimer sits behind the Framework Laptop 16 expansion-bay discrete GPU. The underlying EC command is the +/// expansion-bay GPU PCIe query, which other platform families reject, so reading this on any family other than +/// raises a +/// rather than returning a not-present reading. +/// +/// +/// On a Framework Laptop 16 whose expansion bay carries no compatible discrete GPU, the query succeeds with +/// set to and an empty ; that is a normal +/// reading and not an error. +/// +/// +/// is the raw four-byte register payload read over I2C from the retimer, not text. Upstream +/// renders it as a dot-separated hexadecimal quad, which reproduces. +/// +/// +public sealed record FrameworkPowerDeliveryRetimerVersionSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether a retimer answered the query. + /// The raw retimer version register bytes, or an empty sequence when no retimer answered. + /// is . + public FrameworkPowerDeliveryRetimerVersionSnapshot(bool isPresent, byte[] version) + { + ArgumentNullException.ThrowIfNull(version); + + IsPresent = isPresent; + Version = version; + } + + /// + /// Gets a value indicating whether a retimer answered the query. + /// + /// if a retimer answered; otherwise, . + public bool IsPresent { get; init; } + + /// + /// Gets the raw retimer version register bytes. + /// + /// + /// The four bytes read from the retimer version register, or an empty sequence when no retimer answered. + /// The firmware may return fewer than four bytes, so callers must not index blindly. + /// + public IReadOnlyList Version { get; init; } + + /// + /// Gets the retimer version rendered as a dot-separated hexadecimal quad. + /// + /// + /// A string such as 1.2.A.1F, matching the upstream rendering, or an empty string when no retimer + /// answered or fewer than four bytes were returned. + /// + public string VersionString => Version.Count >= 4 + ? string.Join('.', Version.Take(4).Select(component => component.ToString("X", CultureInfo.InvariantCulture))) + : string.Empty; + + /// + public override string ToString() + { + return IsPresent + ? $"Retimer Version: {VersionString}" + : "Retimer Version: Not Present"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkSmartBatterySnapshot.cs b/framework-dotnet/Snapshots/FrameworkSmartBatterySnapshot.cs new file mode 100644 index 0000000..4e2543f --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkSmartBatterySnapshot.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +using UnitsNet; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the full Smart Battery data set read from the pack over I2C passthrough. +/// +/// +/// +/// Producing this snapshot costs many I2C round trips and is far slower than . Read it on demand only; never place it in a polling loop. +/// +/// +/// reports whether the manufacturer-access register group was unlocked and read. When it is , , and are rather than zero-filled. +/// +/// +public sealed record FrameworkSmartBatterySnapshot +{ + /// + /// The CAPACITY_MODE bit of the Smart Battery BatteryMode register. When set, the + /// capacity registers count 10 mWh units instead of mAh. + /// + private const ushort CapacityModeMask = 0x8000; + + /// + /// The watt-hours one capacity unit represents when the pack reports in energy mode. + /// + private const double EnergyUnitWattHours = 0.01; + + /// + /// Initializes a new instance of the class. + /// + /// The raw Smart Battery BatteryMode register. + /// The pack serial number. + /// The raw packed Smart Battery ManufactureDate word. + /// The decoded manufacture date, or when the pack reported an unusable date. + /// The pack device name. + /// The pack manufacturer name. + /// The pack cell chemistry. + /// The raw manufacturer-access firmware version block. + /// The pack temperature. + /// The pack terminal voltage. + /// The first cell voltage. + /// The second cell voltage. + /// The third cell voltage. + /// The fourth cell voltage. + /// The instantaneous pack current, negative while discharging. + /// The averaged pack current, negative while discharging. + /// The number of charge cycles the pack has recorded. + /// The charge level relative to the full charge capacity of the pack. + /// The charge level relative to the design capacity of the pack. + /// The raw Smart Battery RemainingCapacity register, in whichever unit selects. + /// The raw Smart Battery FullChargeCapacity register, in whichever unit selects. + /// The raw Smart Battery DesignCapacity register, in whichever unit selects. + /// The voltage the pack was designed for. + /// The charging current the pack is currently requesting. + /// The charging voltage the pack is currently requesting. + /// The raw Smart Battery BatteryStatus register. + /// A value indicating whether the manufacturer-access register group was unlocked and read. + /// The state-of-health block, or when the pack was not unsealed. + /// The operation, safety and permanent-failure words, or when the pack was not unsealed. + /// The raw lifetime data blocks, or when the pack was not unsealed. + public FrameworkSmartBatterySnapshot( + ushort batteryMode, + ushort serialNumber, + ushort manufactureDateRaw, + DateOnly? manufactureDate, + string deviceName, + string manufacturerName, + string deviceChemistry, + IReadOnlyList firmwareVersionRaw, + Temperature temperature, + ElectricPotential voltage, + ElectricPotential cellVoltage_1, + ElectricPotential cellVoltage_2, + ElectricPotential cellVoltage_3, + ElectricPotential cellVoltage_4, + ElectricCurrent current, + ElectricCurrent averageCurrent, + uint cycleCount, + Ratio relativeStateOfCharge, + Ratio absoluteStateOfCharge, + ushort remainingCapacityRaw, + ushort fullChargeCapacityRaw, + ushort designCapacityRaw, + ElectricPotential designVoltage, + ElectricCurrent chargingCurrent, + ElectricPotential chargingVoltage, + ushort batteryStatus, + bool isUnsealed, + FrameworkBatteryStateOfHealthSnapshot? stateOfHealth, + FrameworkBatterySafetySnapshot? safety, + FrameworkBatteryLifetimeDataSnapshot? lifetimeData) + { + BatteryMode = batteryMode; + SerialNumber = serialNumber; + ManufactureDateRaw = manufactureDateRaw; + ManufactureDate = manufactureDate; + DeviceName = deviceName; + ManufacturerName = manufacturerName; + DeviceChemistry = deviceChemistry; + FirmwareVersionRaw = firmwareVersionRaw; + Temperature = temperature; + Voltage = voltage; + CellVoltage_1 = cellVoltage_1; + CellVoltage_2 = cellVoltage_2; + CellVoltage_3 = cellVoltage_3; + CellVoltage_4 = cellVoltage_4; + Current = current; + AverageCurrent = averageCurrent; + CycleCount = cycleCount; + RelativeStateOfCharge = relativeStateOfCharge; + AbsoluteStateOfCharge = absoluteStateOfCharge; + RemainingCapacityRaw = remainingCapacityRaw; + FullChargeCapacityRaw = fullChargeCapacityRaw; + DesignCapacityRaw = designCapacityRaw; + DesignVoltage = designVoltage; + ChargingCurrent = chargingCurrent; + ChargingVoltage = chargingVoltage; + BatteryStatus = batteryStatus; + IsUnsealed = isUnsealed; + StateOfHealth = stateOfHealth; + Safety = safety; + LifetimeData = lifetimeData; + } + + /// + /// Gets the raw Smart Battery BatteryMode register. + /// + /// + /// Bit 15 is the Smart Battery CAPACITY_MODE selector, decoded for you as . It chooses which of the two parallel capacity property sets this snapshot populates, so callers do not need to test it themselves. + /// + public ushort BatteryMode { get; init; } + + /// + /// Gets the pack serial number. + /// + public ushort SerialNumber { get; init; } + + /// + /// Gets the raw packed Smart Battery ManufactureDate word. + /// + public ushort ManufactureDateRaw { get; init; } + + /// + /// Gets the decoded manufacture date, or when the pack reported an unusable date. + /// + public DateOnly? ManufactureDate { get; init; } + + /// + /// Gets the pack device name. + /// + public string DeviceName { get; init; } + + /// + /// Gets the pack manufacturer name. + /// + public string ManufacturerName { get; init; } + + /// + /// Gets the pack cell chemistry, for example LION. + /// + public string DeviceChemistry { get; init; } + + /// + /// Gets the raw manufacturer-access firmware version block. + /// + /// + /// This is the unmodified response to manufacturer-access sub-command 0x0002, which carries the sub-command echo, the device number, the firmware version and the build. The field widths are gas-gauge specific, so the bytes are surfaced verbatim rather than decoded. The block is empty when the pack did not answer. + /// + public IReadOnlyList FirmwareVersionRaw { get; init; } + + /// + /// Gets the pack temperature. + /// + public Temperature Temperature { get; init; } + + /// + /// Gets the pack terminal voltage. + /// + public ElectricPotential Voltage { get; init; } + + /// + /// Gets the first cell voltage. + /// + public ElectricPotential CellVoltage_1 { get; init; } + + /// + /// Gets the second cell voltage. + /// + public ElectricPotential CellVoltage_2 { get; init; } + + /// + /// Gets the third cell voltage. + /// + public ElectricPotential CellVoltage_3 { get; init; } + + /// + /// Gets the fourth cell voltage. + /// + public ElectricPotential CellVoltage_4 { get; init; } + + /// + /// Gets the instantaneous pack current. The value is negative while the pack is discharging. + /// + public ElectricCurrent Current { get; init; } + + /// + /// Gets the averaged pack current. The value is negative while the pack is discharging. + /// + public ElectricCurrent AverageCurrent { get; init; } + + /// + /// Gets the number of charge cycles the pack has recorded. + /// + public uint CycleCount { get; init; } + + /// + /// Gets the charge level relative to the full charge capacity of the pack. + /// + public Ratio RelativeStateOfCharge { get; init; } + + /// + /// Gets the charge level relative to the design capacity of the pack. + /// + public Ratio AbsoluteStateOfCharge { get; init; } + + /// + /// Gets a value indicating whether the pack reports its three capacity registers in energy + /// units rather than charge units. + /// + /// + /// if the CAPACITY_MODE bit of is set, + /// so the capacity registers count 10 mWh units; otherwise, , so they + /// count mAh. + /// + /// + /// This selects which of the two parallel capacity property sets is populated: the + /// ones when , the + /// ones when . The unset set is . + /// + public bool IsCapacityReportedInEnergyUnits => (BatteryMode & CapacityModeMask) != 0; + + /// + /// Gets the raw Smart Battery RemainingCapacity register. + /// + /// The register value, in mAh or in 10 mWh units according to . + public ushort RemainingCapacityRaw { get; init; } + + /// + /// Gets the raw Smart Battery FullChargeCapacity register. + /// + /// The register value, in mAh or in 10 mWh units according to . + public ushort FullChargeCapacityRaw { get; init; } + + /// + /// Gets the raw Smart Battery DesignCapacity register. + /// + /// The register value, in mAh or in 10 mWh units according to . + public ushort DesignCapacityRaw { get; init; } + + /// + /// Gets the remaining capacity as a charge. + /// + /// The remaining charge, or when is . + /// + public ElectricCharge? RemainingCapacity => IsCapacityReportedInEnergyUnits + ? null + : ElectricCharge.FromMilliampereHours(RemainingCapacityRaw); + + /// + /// Gets the capacity of the pack when fully charged, as a charge. + /// + /// The full charge, or when is . + /// + public ElectricCharge? FullChargeCapacity => IsCapacityReportedInEnergyUnits + ? null + : ElectricCharge.FromMilliampereHours(FullChargeCapacityRaw); + + /// + /// Gets the capacity the pack was designed for, as a charge. + /// + /// The design charge, or when is . + /// + public ElectricCharge? DesignCapacity => IsCapacityReportedInEnergyUnits + ? null + : ElectricCharge.FromMilliampereHours(DesignCapacityRaw); + + /// + /// Gets the remaining capacity as an energy. + /// + /// The remaining energy, or when is . + /// + public Energy? RemainingEnergy => IsCapacityReportedInEnergyUnits + ? Energy.FromWattHours(RemainingCapacityRaw * EnergyUnitWattHours) + : null; + + /// + /// Gets the energy the pack holds when fully charged. + /// + /// The full charge energy, or when is . + /// + public Energy? FullChargeEnergy => IsCapacityReportedInEnergyUnits + ? Energy.FromWattHours(FullChargeCapacityRaw * EnergyUnitWattHours) + : null; + + /// + /// Gets the energy the pack was designed for. + /// + /// The design energy, or when is . + /// + public Energy? DesignEnergy => IsCapacityReportedInEnergyUnits + ? Energy.FromWattHours(DesignCapacityRaw * EnergyUnitWattHours) + : null; + + /// + /// Gets the voltage the pack was designed for. + /// + public ElectricPotential DesignVoltage { get; init; } + + /// + /// Gets the charging current the pack is currently requesting. + /// + public ElectricCurrent ChargingCurrent { get; init; } + + /// + /// Gets the charging voltage the pack is currently requesting. + /// + public ElectricPotential ChargingVoltage { get; init; } + + /// + /// Gets the raw Smart Battery BatteryStatus register. + /// + public ushort BatteryStatus { get; init; } + + /// + /// Gets a value indicating whether the manufacturer-access register group was unlocked and read. + /// + /// + /// When this is the pack answered in sealed mode and , and are . + /// + public bool IsUnsealed { get; init; } + + /// + /// Gets the state-of-health block, or when the pack was not unsealed. + /// + public FrameworkBatteryStateOfHealthSnapshot? StateOfHealth { get; init; } + + /// + /// Gets the operation, safety and permanent-failure words, or when the pack was not unsealed. + /// + public FrameworkBatterySafetySnapshot? Safety { get; init; } + + /// + /// Gets the raw lifetime data blocks, or when the pack was not unsealed. + /// + public FrameworkBatteryLifetimeDataSnapshot? LifetimeData { get; init; } + + /// + /// Gets the four cell voltages in index order. + /// + public IReadOnlyList CellVoltages => [CellVoltage_1, CellVoltage_2, CellVoltage_3, CellVoltage_4]; + + public override string ToString() + { + string manufactureDate = ManufactureDate.HasValue ? ManufactureDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) : "unknown"; + string stateOfHealth = StateOfHealth is null ? "sealed" : StateOfHealth.ToString(); + string safety = Safety is null ? "sealed" : Safety.ToString(); + string lifetimeData = LifetimeData is null ? "sealed" : LifetimeData.ToString(); + + static string FormatCapacity(ElectricCharge? charge, Energy? energy) + { + if (charge.HasValue) + { + return charge.Value.ToString(CultureInfo.InvariantCulture); + } + + return energy.HasValue ? energy.Value.ToString(CultureInfo.InvariantCulture) : "unavailable"; + } + + (string remaining, string full, string design) capacities = ( + FormatCapacity(RemainingCapacity, RemainingEnergy), + FormatCapacity(FullChargeCapacity, FullChargeEnergy), + FormatCapacity(DesignCapacity, DesignEnergy)); + + return $"Smart Battery: {ManufacturerName} {DeviceName} (SN: {SerialNumber.ToString(CultureInfo.InvariantCulture)}), Chemistry: {DeviceChemistry}, Manufactured: {manufactureDate}, Temperature: {Temperature.ToString(CultureInfo.InvariantCulture)}, Voltage: {Voltage.ToString(CultureInfo.InvariantCulture)}, Cell Voltages: {string.Join(", ", CellVoltages)}, Current: {Current.ToString(CultureInfo.InvariantCulture)}, Average Current: {AverageCurrent.ToString(CultureInfo.InvariantCulture)}, Cycle Count: {CycleCount.ToString(CultureInfo.InvariantCulture)}, Relative Charge: {RelativeStateOfCharge.ToString(CultureInfo.InvariantCulture)}, Absolute Charge: {AbsoluteStateOfCharge.ToString(CultureInfo.InvariantCulture)}, Remaining Capacity: {capacities.remaining}, Full Charge Capacity: {capacities.full}, Design Capacity: {capacities.design}, Design Voltage: {DesignVoltage.ToString(CultureInfo.InvariantCulture)}, Charging Current: {ChargingCurrent.ToString(CultureInfo.InvariantCulture)}, Charging Voltage: {ChargingVoltage.ToString(CultureInfo.InvariantCulture)}, Battery Mode: 0x{BatteryMode.ToString("X4", CultureInfo.InvariantCulture)}, Battery Status: 0x{BatteryStatus.ToString("X4", CultureInfo.InvariantCulture)}, Unsealed: {IsUnsealed}, State Of Health: {stateOfHealth}, Safety: {safety}, Lifetime Data: {lifetimeData}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkStandaloneModeSnapshot.cs b/framework-dotnet/Snapshots/FrameworkStandaloneModeSnapshot.cs new file mode 100644 index 0000000..bbdcf18 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkStandaloneModeSnapshot.cs @@ -0,0 +1,60 @@ +using System.Globalization; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the standalone (batteryless) mode state reported by the EC. +/// +/// +/// +/// Standalone mode describes a system that runs without a battery pack installed, which is the +/// normal configuration for Framework Desktop. +/// +/// +/// does not prove there is no battery. Upstream derives both values +/// from a "no battery reported" check and falls back to whenever the +/// embedded controller power-info read produces nothing, as a safe default. The native call +/// reports success in that case, so on a battery-equipped family means +/// battery status could not be read rather than that no battery is fitted, and callers cannot +/// distinguish it from a genuine Desktop by the status code. +/// +/// +public sealed record FrameworkStandaloneModeSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The EC's own standalone reading. + /// The effective standalone state, including the platform default. + public FrameworkStandaloneModeSnapshot(bool isEcStandalone, bool isStandalone) + { + IsEcStandalone = isEcStandalone; + IsStandalone = isStandalone; + } + + /// + /// Gets a value indicating whether the EC itself reports that the system runs without a battery. + /// + /// + /// Upstream currently computes this and from the same "no battery + /// reported" check, so the two agree in practice. They are surfaced separately because the + /// native ABI reports both, and upstream marks the second as unfinished work. + /// + public bool IsEcStandalone { get; init; } + + /// + /// Gets a value indicating whether the system is in standalone (batteryless) mode. + /// + /// + /// Upstream carries this as a placeholder that currently repeats the same computation as + /// , pending a real platform-default path. Expect the two to agree + /// until that lands; neither is more authoritative than the other today. + /// + public bool IsStandalone { get; init; } + + /// + public override string ToString() + { + return $"Standalone Mode: EC Reading: {IsEcStandalone.ToString(CultureInfo.InvariantCulture)}, Effective: {IsStandalone.ToString(CultureInfo.InvariantCulture)}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkStylusBatterySnapshot.cs b/framework-dotnet/Snapshots/FrameworkStylusBatterySnapshot.cs new file mode 100644 index 0000000..4c24326 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkStylusBatterySnapshot.cs @@ -0,0 +1,56 @@ +using System.Globalization; + +using UnitsNet; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the charge level a paired stylus reports over the touchscreen HID interface. +/// +/// +/// +/// The reading comes from the touchscreen controller rather than the embedded controller, so it is +/// available without an embedded controller connection. +/// +/// +/// A successful read with set to is a normal outcome and +/// not an error: it means no stylus is paired, or the touchscreen does not report a stylus battery at +/// all. In that case is zero and carries no meaning. +/// +/// +public sealed record FrameworkStylusBatterySnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether a stylus answered the query. + /// The reported stylus charge level, valid only when is . + public FrameworkStylusBatterySnapshot(bool isPresent, Ratio chargeLevel) + { + IsPresent = isPresent; + ChargeLevel = chargeLevel; + } + + /// + /// Gets a value indicating whether a stylus answered the query. + /// + /// This flag is authoritative. Read only when it is . + public bool IsPresent { get; init; } + + /// + /// Gets the reported stylus charge level. + /// + /// + /// The touchscreen reports a whole percentage between 0 and 100. The value is meaningful only when + /// is ; otherwise it is zero. + /// + public Ratio ChargeLevel { get; init; } + + /// + public override string ToString() + { + return IsPresent + ? $"Stylus Battery: {ChargeLevel.Percent.ToString(CultureInfo.InvariantCulture)}%" + : "Stylus Battery: Not Present"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkTemperatureSensorNameSnapshot.cs b/framework-dotnet/Snapshots/FrameworkTemperatureSensorNameSnapshot.cs new file mode 100644 index 0000000..ce24b9e --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkTemperatureSensorNameSnapshot.cs @@ -0,0 +1,66 @@ +using System.Globalization; + +using FrameworkDotnet.Enums; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the identity of one embedded controller temperature sensor slot. +/// +/// +/// Reading a sensor name costs one host command, so names are read once per session and cached. +/// Live temperature values come from the thermal snapshot instead, which is the surface intended +/// for polling. +/// +public sealed record FrameworkTemperatureSensorNameSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The temperature sensor slot the name belongs to. + /// The raw sensor name reported by embedded controller firmware. + /// The firmware name reconciled onto the stable managed sensor role names. + /// The embedded controller's classification tag for the slot. + public FrameworkTemperatureSensorNameSnapshot(uint sensorIndex, string firmwareName, FrameworkSensorName mappedName, FrameworkTemperatureSensorType sensorType) + { + SensorIndex = sensorIndex; + FirmwareName = firmwareName; + MappedName = mappedName; + SensorType = sensorType; + } + + /// + /// Gets the temperature sensor slot this name belongs to. The index matches the temperature slot + /// order of the thermal snapshot. + /// + public uint SensorIndex { get; init; } + + /// + /// Gets the raw sensor name exactly as embedded controller firmware reports it. Firmware wording + /// varies between platforms and firmware revisions, so treat it as display text rather than as a + /// stable identifier. + /// + public string FirmwareName { get; init; } + + /// + /// Gets the firmware name reconciled onto the stable managed sensor role names, or + /// when firmware uses a name this version does not + /// recognize. Use this rather than when branching in code. + /// + public FrameworkSensorName MappedName { get; init; } + + /// + /// Gets the embedded controller's classification tag for the slot, which says what the sensor + /// physically measures. + /// + public FrameworkTemperatureSensorType SensorType { get; init; } + + /// + /// Returns a readable description of the sensor identity. + /// + /// A readable description of the sensor identity. + public override string ToString() + { + return $"Temperature Sensor Name Snapshot: Sensor Index: {SensorIndex.ToString(CultureInfo.InvariantCulture)}, Firmware Name: {FirmwareName}, Mapped Name: {MappedName}, Sensor Type: {SensorType}"; + } +} diff --git a/framework-dotnet/Snapshots/FrameworkThermalThresholdsSnapshot.cs b/framework-dotnet/Snapshots/FrameworkThermalThresholdsSnapshot.cs new file mode 100644 index 0000000..1f87dd0 --- /dev/null +++ b/framework-dotnet/Snapshots/FrameworkThermalThresholdsSnapshot.cs @@ -0,0 +1,153 @@ +using System.Globalization; + +using FrameworkDotnet.Enums; + +using UnitsNet; + +namespace FrameworkDotnet.Snapshots; + +/// +/// Represents the thermal threshold configuration the embedded controller holds for one temperature +/// sensor. +/// +/// +/// +/// Embedded controller firmware stores thresholds in Kelvin with zero meaning "disabled", so a +/// disabled threshold arrives over the native ABI as -273 degrees Celsius. Never test a reported +/// temperature to decide whether a threshold is active: every threshold on this snapshot is +/// exactly when its bit is clear in , which +/// is the only authoritative source. +/// +/// +/// and are temperature setpoints, not fan speeds. +/// +/// +public sealed record FrameworkThermalThresholdsSnapshot +{ + /// + /// Initializes a new instance of the class. + /// + /// The temperature sensor slot the thresholds belong to. + /// The set of thresholds the embedded controller currently has enabled. + /// The warning threshold, or when it is disabled. + /// The throttling threshold, or when it is disabled. + /// The shutdown threshold, or when it is disabled. + /// The release point for , or when it is disabled. + /// The release point for , or when it is disabled. + /// The release point for , or when it is disabled. + /// The temperature below which no active cooling is required, or when it is disabled. + /// The temperature above which active cooling runs at maximum, or when it is disabled. + public FrameworkThermalThresholdsSnapshot( + uint sensorIndex, + FrameworkThermalThresholdFlag enabledThresholds, + Temperature? warn, + Temperature? high, + Temperature? halt, + Temperature? warnRelease, + Temperature? highRelease, + Temperature? haltRelease, + Temperature? fanOff, + Temperature? fanMax) + { + SensorIndex = sensorIndex; + EnabledThresholds = enabledThresholds; + Warn = warn; + High = high; + Halt = halt; + WarnRelease = warnRelease; + HighRelease = highRelease; + HaltRelease = haltRelease; + FanOff = fanOff; + FanMax = fanMax; + } + + /// + /// Gets the temperature sensor slot these thresholds belong to. The index matches the + /// temperature slot order of the thermal snapshot. + /// + public uint SensorIndex { get; init; } + + /// + /// Gets the set of thresholds the embedded controller currently has enabled. A clear bit means + /// firmware has that threshold disabled, and the matching property on this snapshot is + /// . + /// + public FrameworkThermalThresholdFlag EnabledThresholds { get; init; } + + /// + /// Gets the temperature above which the embedded controller warns the application processor, or + /// when the warning threshold is disabled. + /// + public Temperature? Warn { get; init; } + + /// + /// Gets the temperature above which the embedded controller throttles the application + /// processor, or when the throttling threshold is disabled. + /// + public Temperature? High { get; init; } + + /// + /// Gets the temperature above which the embedded controller shuts the system down, or + /// when the shutdown threshold is disabled. + /// + public Temperature? Halt { get; init; } + + /// + /// Gets the temperature at which the condition is released, or + /// when that release point is disabled. Firmware treats a disabled + /// release point as a default one-degree hysteresis below the threshold itself. + /// + public Temperature? WarnRelease { get; init; } + + /// + /// Gets the temperature at which the condition is released, or + /// when that release point is disabled. + /// + public Temperature? HighRelease { get; init; } + + /// + /// Gets the temperature at which the condition is released, or + /// when that release point is disabled. + /// + public Temperature? HaltRelease { get; init; } + + /// + /// Gets the temperature below which the embedded controller needs no active cooling, or + /// when the setpoint is disabled. This is a temperature setpoint at + /// which the fans stop, not a fan speed or an RPM limit. + /// + public Temperature? FanOff { get; init; } + + /// + /// Gets the temperature above which the embedded controller applies maximum active cooling, or + /// when the setpoint is disabled. This is a temperature setpoint at + /// which the fans reach full speed, not a fan speed or an RPM limit. + /// + public Temperature? FanMax { get; init; } + + /// + /// Determines whether the embedded controller currently has a given threshold enabled. + /// + /// The threshold to test. Pass a single flag; passing a combination reports whether every flag in the combination is enabled. + /// when the threshold is enabled in firmware; otherwise, . + public bool IsEnabled(FrameworkThermalThresholdFlag threshold) + { + return (EnabledThresholds & threshold) == threshold; + } + + /// + /// Returns a readable description of the thresholds. + /// + /// A readable description of the thresholds. + public override string ToString() + { + return $"Thermal Thresholds Snapshot: Sensor Index: {SensorIndex.ToString(CultureInfo.InvariantCulture)}, Enabled: {EnabledThresholds}, Warn: {Describe(Warn)}, High: {Describe(High)}, Halt: {Describe(Halt)}, Warn Release: {Describe(WarnRelease)}, High Release: {Describe(HighRelease)}, Halt Release: {Describe(HaltRelease)}, Fan Off: {Describe(FanOff)}, Fan Max: {Describe(FanMax)}"; + } + + private static string Describe(Temperature? temperature) + { + return temperature.HasValue + ? temperature.Value.ToString(CultureInfo.InvariantCulture) + : "Disabled"; + } +} diff --git a/framework-system-ffi-extensions b/framework-system-ffi-extensions index b3142e5..40f98e0 160000 --- a/framework-system-ffi-extensions +++ b/framework-system-ffi-extensions @@ -1 +1 @@ -Subproject commit b3142e572b13ca81cd5c5a86cb66ede4685dab5b +Subproject commit 40f98e0f4e265d9c78f131592f5f415d9a8eaf35