diff --git a/EpsonPrintService/PuduRobotClient.cs b/EpsonPrintService/PuduRobotClient.cs new file mode 100644 index 0000000..fabc2a1 --- /dev/null +++ b/EpsonPrintService/PuduRobotClient.cs @@ -0,0 +1,118 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace EpsonPrintService; + +public sealed class PuduRobotClient +{ + private readonly HttpClient _http; + private readonly string _baseUrl; + private readonly string _username; + private readonly string _password; + private string? _token; + + public PuduRobotClient(string baseUrl, string username, string password, HttpClient? http = null) + { + _baseUrl = baseUrl.TrimEnd('/'); + _username = username; + _password = password; + _http = http ?? new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + } + + public async Task SendToTableAsync(string robotSn, string targetPoint, CancellationToken ct = default) + { + await EnsureTokenAsync(ct); + try + { + await PostAddCommandAsync(robotSn, targetPoint, ct); + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) + { + // Token expired — refresh once and retry. + _token = null; + await EnsureTokenAsync(ct); + await PostAddCommandAsync(robotSn, targetPoint, ct); + } + } + + private async Task EnsureTokenAsync(CancellationToken ct) + { + if (_token is not null) return; + + using var resp = await _http.PostAsJsonAsync( + $"{_baseUrl}/api/Auth/login", + new { username = _username, password = _password }, + ct); + resp.EnsureSuccessStatusCode(); + + var bodyText = await resp.Content.ReadAsStringAsync(ct); + var token = ExtractToken(bodyText); + if (string.IsNullOrEmpty(token)) + throw new InvalidOperationException($"Pudu login: no token returned. Body: {bodyText}"); + + _token = token; + } + + private async Task PostAddCommandAsync(string robotSn, string targetPoint, CancellationToken ct) + { + using var req = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/api/Robots/add-command") + { + Content = JsonContent.Create(new { Sn = robotSn, TargetPoint = targetPoint }) + }; + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); + + using var resp = await _http.SendAsync(req, ct); + resp.EnsureSuccessStatusCode(); + } + + // Recursive search so we match the token regardless of wrapping (e.g. {"data":{"token":"..."}}). + // Mirrors the behavior of `sed -n 's/.*"token":"\([^"]*\)".*/\1/p'` in send_robot.sh. + private static string? ExtractToken(string json) + { + try + { + using var doc = JsonDocument.Parse(json); + return FindTokenProperty(doc.RootElement); + } + catch (JsonException) + { + return null; + } + } + + private static string? FindTokenProperty(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + if ((prop.Name.Equals("token", StringComparison.OrdinalIgnoreCase) + || prop.Name.Equals("accessToken", StringComparison.OrdinalIgnoreCase)) + && prop.Value.ValueKind == JsonValueKind.String) + { + return prop.Value.GetString(); + } + } + foreach (var prop in element.EnumerateObject()) + { + var nested = FindTokenProperty(prop.Value); + if (nested is not null) return nested; + } + return null; + + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + var nested = FindTokenProperty(item); + if (nested is not null) return nested; + } + return null; + + default: + return null; + } + } +} diff --git a/EpsonPrintService/robot_control.md b/EpsonPrintService/robot_control.md new file mode 100644 index 0000000..bccb909 --- /dev/null +++ b/EpsonPrintService/robot_control.md @@ -0,0 +1,138 @@ +# Robot Control — Send a Robot to a Table + +Step-by-step guide to authorize against the `PuduControl.DataSync.API` and dispatch a robot (by serial number) to a specific table (by table name). + +## Prerequisites + +- API running locally at `http://localhost:5022` (see `Properties/launchSettings.json`). +- Credentials: `admin` / `admin`. +- Robot serial number (`Sn`) and the exact table name configured as a destination point (`TargetPoint`) in the robot's shop. +- A REST client (`curl`, Postman, HTTPie, etc.). + +All endpoints return a uniform envelope: + +```json +{ "success": true, "data": { ... }, "errorMessage": null } +``` + +## Step 1 — Authorize (obtain a JWT) + +`POST /api/Auth/login` + +Request body: + +```json +{ "username": "admin", "password": "admin" } +``` + +`curl` example: + +```bash +curl -s -X POST http://localhost:5022/api/Auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' +``` + +Sample response: + +```json +{ + "success": true, + "data": { + "message": "Login successful", + "username": "admin", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expiresIn": 86400 + }, + "errorMessage": null +} +``` + +Copy `data.token` — every subsequent call must include it as: + +``` +Authorization: Bearer +``` + +The token is valid for 24 hours. + +## Step 2 — Send the robot to a table + +`POST /api/Robots/add-command` (requires `Authorization` header) + +Request body: + +| Field | Type | Description | +|---------------|--------|--------------------------------------------------| +| `Sn` | string | Robot serial number | +| `TargetPoint` | string | Table name (must exist in the robot's shop map) | + +```json +{ "Sn": "ROBOT_SERIAL_HERE", "TargetPoint": "TABLE_NAME_HERE" } +``` + +`curl` example: + +```bash +TOKEN="eyJhbGciOi..." # token from Step 1 + +curl -s -X POST http://localhost:5022/api/Robots/add-command \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"Sn":"ROBOT_SERIAL_HERE","TargetPoint":"TABLE_NAME_HERE"}' +``` + +Success response: + +```json +{ + "success": true, + "data": { "message": "Command added successfully." }, + "errorMessage": null +} +``` + +The command is persisted and picked up by the `DataSyncHosted` background service, which forwards it to the robot through the Pudu API. + +## One-shot script + +```bash +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="http://localhost:5022" +ROBOT_SN="$1" +TABLE_NAME="$2" + +TOKEN=$(curl -s -X POST "$BASE_URL/api/Auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' \ + | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + +curl -s -X POST "$BASE_URL/api/Robots/add-command" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"Sn\":\"$ROBOT_SN\",\"TargetPoint\":\"$TABLE_NAME\"}" +``` + +Usage: `./send_robot.sh ` + +## Troubleshooting + +- **401 Unauthorized** — token missing, expired, or malformed. Repeat Step 1. +- **`You do not have permission to control this robot.`** — the logged-in user does not own the shop this robot belongs to. Assign the shop to the user via `POST /api/Robots/assign-shop` (admin only). +- **Target point not reached / ignored** — verify `TargetPoint` matches a point name on the robot's map exactly (case-sensitive). Use `GET /api/Robots/shops/{shopId}` to list available points. +- **Listing available robots** — `GET /api/Robots/shops/{shopId}/robots` returns robots (and their `Sn`) in a shop; `GET /api/Robots/shops` lists shops the user can access. + +## Helpful companion endpoints + +| Purpose | Method & Path | +|-------------------------------|---------------------------------------------------| +| List accessible shops | `GET /api/Robots/shops` | +| List robots in a shop | `GET /api/Robots/shops/{shopId}/robots` | +| Get shop layout / points | `GET /api/Robots/shops/{shopId}` | +| Get robot status | `GET /api/Robots/shops/{shopId}/status` | +| List queued commands | `POST /api/Robots/list-commands` | +| Cancel a queued command | `POST /api/Robots/delete-command` | + +All of the above require the `Authorization: Bearer ` header. diff --git a/EpsonPrintService/send_robot.sh b/EpsonPrintService/send_robot.sh new file mode 100644 index 0000000..f96ff83 --- /dev/null +++ b/EpsonPrintService/send_robot.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +BASE_URL="${BASE_URL:-https://pudu-api.tes.gd}" +USERNAME="${USERNAME:-admin}" +PASSWORD="${PASSWORD:-admin}" +ROBOT_SN="$1" +TABLE_NAME="$2" + +TOKEN=$(curl -fsS -X POST "$BASE_URL/api/Auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \ + | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + +if [[ -z "$TOKEN" ]]; then + echo "Login failed: could not extract token." >&2 + exit 1 +fi + +curl -fsS -X POST "$BASE_URL/api/Robots/add-command" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "{\"Sn\":\"$ROBOT_SN\",\"TargetPoint\":\"$TABLE_NAME\"}" +echo diff --git a/global.json b/global.json deleted file mode 100644 index 74ce97c..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.419", - "rollForward": "latestPatch" - } -}