add pudu robot control client; drop global.json SDK pin
This commit is contained in:
118
EpsonPrintService/PuduRobotClient.cs
Normal file
118
EpsonPrintService/PuduRobotClient.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
EpsonPrintService/robot_control.md
Normal file
138
EpsonPrintService/robot_control.md
Normal file
@@ -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 <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <ROBOT_SN> <TABLE_NAME>`
|
||||||
|
|
||||||
|
## 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 <token>` header.
|
||||||
29
EpsonPrintService/send_robot.sh
Normal file
29
EpsonPrintService/send_robot.sh
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
echo "Usage: $0 <ROBOT_SN> <TABLE_NAME>" >&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
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"sdk": {
|
|
||||||
"version": "8.0.419",
|
|
||||||
"rollForward": "latestPatch"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user