Skip to content

Commit ee8197c

Browse files
committed
Refactor Sample app to be a thin, stateless test harness
1 parent c960b37 commit ee8197c

4 files changed

Lines changed: 229 additions & 197 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Refactoring Plan: Thin Sample CLI App
2+
3+
To ensure the Sample CLI application behaves as a high-fidelity test harness, we must eliminate all duplicated state-tracking (`_pins`, `_lastValues`) and custom validation logic in `Program.cs` that mirrors code in the driver or simulation engine. Instead, the application should delegate completely to the native APIs and error-handling of the customized `System.Device.Gpio` shim.
4+
5+
---
6+
7+
## Principles
8+
9+
1. **Preserve Mirror-Image APIs**: The custom `System.Device.Gpio` shims must mirror the official dotnet API perfectly. We do not add custom APIs (like `GetOpenPins`) to the shimmed classes.
10+
2. **Stateless Test Harness**: The CLI app does not need to duplicate open/close or value state tracking. It should just naively invoke methods (e.g. `controller.OpenPin(...)`, `controller.Write(...)`) and let the driver/engine handle all boundary validations, propagating standard .NET exceptions (like `InvalidOperationException`) back to the CLI shell.
11+
3. **Stateless Pin Status**: To support the `status` command without introducing non-standard APIs to `GpioController`, we can simply query the standard `controller.IsPinOpen(pin)` API in a loop from pin 1 to 40 (the typical Raspberry Pi header range). This gives us a 100% compliant, standard-compatible status query.
12+
13+
---
14+
15+
## Refactoring Program.cs to be Thin
16+
17+
We will clean up and rebuild `Program.cs` to eliminate duplicate state management, background thread polling, and manual validation.
18+
19+
### 1. Eliminate Fields & Background Work
20+
- Delete `_pins` dictionary.
21+
- Delete `_lastValues` dictionary.
22+
- Remove `WatchInputPins` background polling method.
23+
- Remove `_watcherCts` and `_watcherTask`.
24+
25+
### 2. Event-Driven Input Tracking
26+
- Keep a reactive value change listener using the standard event callbacks:
27+
```csharp
28+
private static void OnPinValueChanged(object sender, PinValueChangedEventArgs args)
29+
{
30+
PinValue val = _controller.Read(args.PinNumber);
31+
lock (_consoleLock)
32+
{
33+
Console.ForegroundColor = ConsoleColor.Magenta;
34+
Console.Write($"\n[ALERT] Input state changed: Pin {args.PinNumber} is now {val}!");
35+
Console.ResetColor();
36+
PrintPrompt();
37+
}
38+
}
39+
```
40+
- In `OpenPin` helper, register the callback if the mode is an input mode.
41+
- In `ClosePin` helper, unregister the callback.
42+
- In the `setmode` / `sm` command, unregister first, change mode, and register if it became input.
43+
44+
### 3. Delegate Commands directly to GpioController
45+
- **`scheme`**: Reset scheme by reinstantiating `_controller` without needing to clear local state.
46+
- **`open`**: Call `OpenPin` directly. Let the controller/driver validate.
47+
- **`close`**: Call `ClosePin` directly. Let the controller/driver validate open status.
48+
- **`write`**: Convert input value to `PinValue` and call `controller.Write` directly. Let the driver/engine throw if invalid or unauthorized.
49+
- **`read`**: Call `controller.Read` directly.
50+
- **`setmode`**: Set the mode directly. Manage callbacks reactively.
51+
- **`status`**: Loop through pin 1 to 40, check `controller.IsPinOpen(pin)` naively, and print the current mode and value using only standard `GpioController` methods!
52+
53+
---
54+
55+
## Verification Plan
56+
57+
1. **Compilation**: Run `dotnet build` to verify clean compilation of all libraries and the sample application.
58+
2. **Unit Tests**: Run `dotnet test` to ensure existing driver and engine tests pass perfectly.
59+
3. **Interactive Testing**: Run the Sample app, open output/input pins, toggle values from both the CLI and the Web UI, and verify that the alerts and commands reflect state in real-time.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace DevDecoder.GpioSimulator.Sample
6+
{
7+
/// <summary>
8+
/// Provides auto-completion suggestions for the interactive GPIO board simulator terminal.
9+
/// </summary>
10+
public class CommandAutoCompleteHandler : IAutoCompleteHandler
11+
{
12+
private static readonly string[] Commands =
13+
{
14+
"open", "close", "write", "read", "setmode", "isopen",
15+
"issupported", "scheme", "schema", "status", "help",
16+
"exit", "quit"
17+
};
18+
19+
private static readonly string[] Modes = { "in", "out", "pullup", "pulldown" };
20+
private static readonly string[] Schemes = { "logical", "board" };
21+
private static readonly string[] WriteValues = { "1", "0", "high", "low" };
22+
23+
/// <summary>
24+
/// Gets or sets the characters that define the boundary for word separations.
25+
/// </summary>
26+
public char[] Separators { get; set; } = new char[] { ' ' };
27+
28+
/// <summary>
29+
/// Generates auto-completion suggestions based on the current input text and cursor index.
30+
/// </summary>
31+
/// <param name="text">The current input line text.</param>
32+
/// <param name="index">The current cursor index.</param>
33+
/// <returns>An array of suggestions matching the context.</returns>
34+
public string[] GetSuggestions(string text, int index)
35+
{
36+
if (string.IsNullOrEmpty(text))
37+
{
38+
return Commands;
39+
}
40+
41+
// Split the line by spaces
42+
string[] parts = text.Split(Separators, StringSplitOptions.None);
43+
if (parts.Length == 0)
44+
{
45+
return Commands;
46+
}
47+
48+
string cmd = parts[0].ToLower();
49+
50+
// If we're typing the first word (command)
51+
if (parts.Length == 1)
52+
{
53+
return Commands.Where(c => c.StartsWith(cmd, StringComparison.OrdinalIgnoreCase)).ToArray();
54+
}
55+
56+
// Auto-complete sub-commands or parameters
57+
// Case 1: scheme/schema parameter autocomplete (e.g., scheme <tab>)
58+
if ((cmd == "scheme" || cmd == "schema") && parts.Length == 2)
59+
{
60+
string arg = parts[1].ToLower();
61+
return Schemes.Where(s => s.StartsWith(arg, StringComparison.OrdinalIgnoreCase)).ToArray();
62+
}
63+
64+
// Case 2: open <pin> <mode> -> parts[2] is the mode
65+
if (cmd == "open" && parts.Length == 3)
66+
{
67+
string arg = parts[2].ToLower();
68+
return Modes.Where(m => m.StartsWith(arg, StringComparison.OrdinalIgnoreCase)).ToArray();
69+
}
70+
71+
// Case 3: setmode <pin> <mode> / set <pin> <mode> -> parts[2] is the mode
72+
if ((cmd == "setmode" || cmd == "set" || cmd == "sm") && parts.Length == 3)
73+
{
74+
string arg = parts[2].ToLower();
75+
return Modes.Where(m => m.StartsWith(arg, StringComparison.OrdinalIgnoreCase)).ToArray();
76+
}
77+
78+
// Case 4: issupported <pin> <mode> -> parts[2] is the mode
79+
if ((cmd == "issupported" || cmd == "is") && parts.Length == 3)
80+
{
81+
string arg = parts[2].ToLower();
82+
return Modes.Where(m => m.StartsWith(arg, StringComparison.OrdinalIgnoreCase)).ToArray();
83+
}
84+
85+
// Case 5: write <pin> <value> -> parts[2] is the value
86+
if ((cmd == "write" || cmd == "w") && parts.Length == 3)
87+
{
88+
string arg = parts[2].ToLower();
89+
return WriteValues.Where(v => v.StartsWith(arg, StringComparison.OrdinalIgnoreCase)).ToArray();
90+
}
91+
92+
return Array.Empty<string>();
93+
}
94+
}
95+
}

src/DevDecoder.GpioSimulator.Sample/DevDecoder.GpioSimulator.Sample.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PrivateAssets>all</PrivateAssets>
2020
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2121
</PackageReference>
22+
<PackageReference Include="ReadLine" Version="2.0.1" />
2223
</ItemGroup>
2324

2425
</Project>

0 commit comments

Comments
 (0)