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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
9 changes: 9 additions & 0 deletions README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
41 changes: 41 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
19 changes: 19 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
89 changes: 89 additions & 0 deletions src/server/SmartBotBlazorApp.Tests/RobotMovementInputTests.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading