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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
189 changes: 189 additions & 0 deletions framework-dotnet-cli-test/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -121,6 +162,154 @@ private static void WritePanel(Panel panel)
AnsiConsole.WriteLine();
}

/// <summary>
/// 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.
/// </summary>
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<string> 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<FrameworkEcGpioSnapshot> 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;
Expand Down
Loading
Loading