From b38c16c59817b605cc40161f3f4c5f47ce091fd6 Mon Sep 17 00:00:00 2001 From: Kamil Rataj Date: Tue, 14 Jul 2026 18:45:35 +0200 Subject: [PATCH] Document robot controls and add regression tests --- CLAUDE.md | 1 + README.md | 8 ++ README.pl.md | 9 ++ docs/architecture.md | 41 +++++++++ docs/getting-started.md | 21 +++++ docs/server.md | 19 ++++ .../RobotMovementInputTests.cs | 89 +++++++++++++++++++ 7 files changed, 188 insertions(+) create mode 100644 CLAUDE.md create mode 100644 src/server/SmartBotBlazorApp.Tests/RobotMovementInputTests.cs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index e5ffddf..acd317b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,14 @@ A short local driving-test montage and a recorded live presentation of the compl - **Authenticated control plane** — dashboard pages and browser hub connections require ASP.NET Core Identity; the robot authenticates to the same hub with a separate API key. - **Cloud-ready** — a Dockerfile and .NET container metadata (`kamilr616/smartbotblazorapp`), plus a GitHub Actions pipeline that builds, tests, and publishes a deployable artifact. +### One-joystick differential drive + +One proportional joystick combines throttle and steering, allowing straight driving, +smooth arcs, and rotation around the robot's own axis. Arrow keys provide an +alternative control method, while two speedometer gauges show the motor PWM commands. +See the [motion-control documentation](docs/architecture.md#motion-control) for the +mixing equations, dead zones, timing, key mapping, and safety behavior. + ## Tech Stack | Layer | Technology | diff --git a/README.pl.md b/README.pl.md index 2eae2a7..307748e 100644 --- a/README.pl.md +++ b/README.pl.md @@ -104,6 +104,15 @@ Krótki montaż z lokalnych testów jazdy oraz nagranie prezentacji kompletnego - **Uwierzytelniony panel sterowania** — strony panelu i połączenia przeglądarki z hubem wymagają ASP.NET Core Identity, a robot uwierzytelnia się w tym samym hubie oddzielnym kluczem API. - **Gotowość do wdrożenia w chmurze** — Dockerfile i metadane kontenera .NET (`kamilr616/smartbotblazorapp`) oraz pipeline GitHub Actions budujący, testujący i publikujący artefakt gotowy do wdrożenia. +### Sterowanie różnicowe jednym joystickiem + +Jeden proporcjonalny joystick łączy sterowanie prędkością i kierunkiem, umożliwiając +jazdę prosto, płynne pokonywanie łuków oraz obrót robota wokół własnej osi. Klawisze +strzałek są alternatywną metodą sterowania, a dwa prędkościomierze pokazują komendy +PWM silników. Równania miksowania, strefy martwe, częstotliwość komend, mapowanie +klawiszy i zabezpieczenia opisano w +[dokumentacji sterowania](docs/architecture.md#motion-control). + ## Stack technologiczny | Warstwa | Technologia | diff --git a/docs/architecture.md b/docs/architecture.md index 87fc438..50fde9e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,6 +37,47 @@ sequenceDiagram A capture of real handshake and invocation messages is available in `other/signalr_json.txt`. +## Motion Control + +`JoystickInputHandler` treats the on-screen joystick as a two-dimensional, +proportional differential-drive controller. After converting browser coordinates so +positive `y` means forward motion, it applies two dead zones and mixes throttle with +steering: + +```text +if |x| < 0.15 and |y| < 0.15: + left = right = 0 +else if |x| < 0.35: + left = right = y +else: + left = y + x + right = y - x + +motor PWM = clamp(motor, -1, 1) × 255 +``` + +The mix is continuous rather than limited to the dominant direction displayed by the +UI label. Equal motor values produce straight motion, unequal values produce a curve, +and opposite values at `y = 0` rotate the robot around its own axis. The output range +is `-255…+255`, where the sign selects motor direction and the magnitude selects PWM +duty. + +`keyboardInputHandler` provides a discrete fallback using the same motor-command +contract: + +| Input | Left motor | Right motor | Motion | +|---|---:|---:|---| +| `ArrowUp` | 255 | 255 | Forward | +| `ArrowDown` | -255 | -255 | Reverse | +| `ArrowLeft` | 255 | -255 | Rotate left in place | +| `ArrowRight` | -255 | 255 | Rotate right in place | +| Pointer/key release | 0 | 0 | Stop | + +The dashboard samples the held joystick continuously but throttles outgoing SignalR +commands to one every 250 ms. Pointer/key release sends `(0, 0)` immediately. The +firmware independently clamps received values and stops both motors after 700 ms +without a command, so loss of browser or network control fails safe. + ## Hub Contract (`Hubs/SignalHub.cs`) ### Client → Server (invocable methods) diff --git a/docs/getting-started.md b/docs/getting-started.md index aca44bf..8ec2382 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -97,6 +97,27 @@ docker run -p 8080:8080 \ 4. Watch the depth camera output on **Image Receiver** (`/image-receiver-server`, rendered heatmap) or **Matrix Receiver** (`/matrix-receiver-server`, interpolated 32×32 grid). 5. Review history on **Measurement Charts** (`/measurement-charts`) — pick a date range to plot temperature, distance, acceleration, and rotation. +### Control reference + +The joystick supports proportional differential steering across its full circular +range. Move it vertically for straight driving, diagonally for a progressively tighter +turn, or horizontally to run the motors in opposite directions and rotate around the +robot's own axis. Returning it to the center or releasing the pointer stops the robot. + +To use the keyboard, focus the control panel and use: + +| Key | Result | +|---|---| +| `↑` | Full-speed forward | +| `↓` | Full-speed reverse | +| `←` | Rotate left in place | +| `→` | Rotate right in place | +| Release the key | Stop | + +The speedometer gauges show the actual `-255…+255` PWM command for each motor. The +joystick is proportional; the keyboard mapping is intentionally discrete and uses +full power. + ## Troubleshooting | Symptom | Likely cause / fix | diff --git a/docs/server.md b/docs/server.md index 0527686..0f8f5a6 100644 --- a/docs/server.md +++ b/docs/server.md @@ -19,6 +19,25 @@ The web application lives in `src/server/` and consists of two projects: | `/weather` | `Weather.razor` | Server (stream rendering) | Air-quality data pulled from an external GreenCity/InPost sensor API | | `/Account/*` | Identity pages | Server | Registration, login, 2FA, password reset, account management | +## Robot Control Input + +The control panel in `ImageReceiver_server.razor` accepts pointer events from mouse +or touch and keyboard events from the arrow keys: + +- `Components/RobotMovementInput/JoystickInputHandler.cs` normalizes the two joystick + axes, applies stop/straight-driving dead zones, and mixes them as `left = y + x` + and `right = y - x`. This provides proportional straight motion, curved motion, + and rotation around the robot's own axis from one joystick. +- `Components/RobotMovementInput/keyboardInputHandler.cs` maps the arrow keys to + full-range `-255`, `0`, or `255` motor commands. Up/down drive both motors in the + same direction; left/right drive them in opposite directions for rotation in place. +- `ImageReceiver_server.razor` sends commands through `SendMovementCommand`, updates + both speed gauges, limits normal command transmission to one message per 250 ms, + and sends `(0, 0)` when pointer or keyboard input is released. + +See [architecture.md](architecture.md#motion-control) for the equations, exact key +mapping, and the interaction with the firmware dead-man timer. + ## Services ### `Hubs/SignalHub.cs` — SignalR hub (`/signalhub`) diff --git a/src/server/SmartBotBlazorApp.Tests/RobotMovementInputTests.cs b/src/server/SmartBotBlazorApp.Tests/RobotMovementInputTests.cs new file mode 100644 index 0000000..7e67baa --- /dev/null +++ b/src/server/SmartBotBlazorApp.Tests/RobotMovementInputTests.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Components.Web; +using SmartBotBlazorApp.Components.RobotMovementInput; + +namespace SmartBotBlazorApp.Tests; + +public class RobotMovementInputTests +{ + private const double Center = 100; + private const double Radius = 100; + + [Fact] + public void JoystickCenterDeadZoneStopsBothMotors() + { + var handler = MoveJoystick(105, 105); + + Assert.Equal((0, 0), handler.GetRobotEngineValues()); + } + + [Fact] + public void JoystickStraightDrivingDeadZoneIgnoresSmallHorizontalOffset() + { + var handler = MoveJoystick(115, 50); + + Assert.Equal((255, 255), handler.GetRobotEngineValues()); + } + + [Fact] + public void DiagonalJoystickInputMixesMotorsForAProportionalArc() + { + var handler = MoveJoystick(125, 50); + + Assert.Equal((127, 255), handler.GetRobotEngineValues()); + } + + [Fact] + public void HorizontalJoystickInputRotatesRobotAroundItsAxis() + { + var handler = MoveJoystick(150, 100); + + Assert.Equal((-255, 255), handler.GetRobotEngineValues()); + } + + [Fact] + public void ReleasingJoystickResetsPositionAndStopsBothMotors() + { + var handler = MoveJoystick(100, 50); + + handler.OnPointerUp(); + + Assert.Equal((0, 0), handler.GetRobotEngineValues()); + Assert.Equal(Center, handler.KnobPosX); + Assert.Equal(Center, handler.KnobPosY); + Assert.Equal("Center", handler.joystickDirection); + } + + [Theory] + [InlineData("ArrowUp", 255, 255)] + [InlineData("ArrowDown", -255, -255)] + [InlineData("ArrowLeft", 255, -255)] + [InlineData("ArrowRight", -255, 255)] + public void ArrowKeysMapToExpectedMotorCommands(string key, int left, int right) + { + var handler = new keyboardInputHandler(); + + handler.onKeyDown(new KeyboardEventArgs { Key = key }); + + Assert.True(handler.validInput); + Assert.Equal((left, right), handler.GetRobotEngineValues()); + } + + [Fact] + public void UnsupportedKeyIsRejectedAndStopsBothMotors() + { + var handler = new keyboardInputHandler(); + + handler.onKeyDown(new KeyboardEventArgs { Key = "Space" }); + + Assert.False(handler.validInput); + Assert.Equal((0, 0), handler.GetRobotEngineValues()); + } + + private static JoystickInputHandler MoveJoystick(double x, double y) + { + var handler = new JoystickInputHandler(Center, Center, Radius); + handler.OnPointerDown(Center, Center); + handler.OnPointerMove(x, y); + return handler; + } +}