|
Motion Master 6.0.0-alpha.86
Next-generation motion control software
|
HTTP-transport glue shared by the built-in server and route plug-in libs. More...
Classes | |
| class | Request |
| A request, snapshotted on the loop thread so a handler can outlive it. More... | |
| struct | Response |
| A complete response, produced off the loop and written by the framework. More... | |
| struct | RouteContext |
| The live collaborators a route plug-in needs, handed to it at registration time. More... | |
| class | Router |
| Registers routes whose handlers run off the event loop. More... | |
Typedefs | |
| using | Handler = std::function< Response(const Request &)> |
| What a route does: a pure function from a snapshotted request to a response. | |
| using | RegisterRoutesFn = std::function< void(Router &router, const RouteContext &ctx)> |
| A route plug-in: registers its routes on the HTTP app. | |
Functions | |
| std::string | percentDecode (std::string_view text) |
Percent-decodes a URL path component — %20 to a space, %2F to a slash. | |
| Response | json (const nlohmann::json &body) |
A 200 response carrying body as JSON. | |
| Response | bytes (std::string contentType, std::string body) |
A 200 response carrying body verbatim under contentType. | |
| Response | error (std::string status, std::string_view message) |
A status response carrying a {"error": message} body. | |
| Response | badRequest (std::string_view message) |
A 400 with message — the most common failure, so it gets a name. | |
| Response | notFound (std::string_view message) |
A 404 with message. | |
| Response | statusOnly (std::string status) |
A bare status response with no body — a 202 that means "under way", a 204 delete. | |
| Response | withWireTime (Response response, std::chrono::microseconds wireUs) |
Attaches the server-measured device time (X-Wire-Us) to response and returns it. | |
| std::vector< std::string > | parameterNames (std::string_view pattern) |
The :name tokens of a route pattern, in the order uWS will index them. | |
| template<typename Op > | |
| Response | timed (Op &&op, std::string errorStatus="500 Internal Server Error") |
Runs a device operation, times it, and turns its std::expected into a timed response. | |
| template<typename Res > | |
| Res * | setCorsOrigin (Res *res, std::string_view corsOrigin) |
Writes the Access-Control-Allow-Origin header and returns res for chaining. | |
| template<typename Res > | |
| Res * | setWireTime (Res *res, std::chrono::microseconds wireUs) |
Attaches the server-measured wire-time header (X-Wire-Us, microseconds) to a response. | |
| template<typename Res > | |
| void | sendJson (Res *res, std::string_view corsOrigin, const nlohmann::json &body) |
Writes body as a 200 application/json response with the CORS header. | |
| template<typename Res > | |
| void | sendBytes (Res *res, std::string_view corsOrigin, std::string_view contentType, std::string_view body) |
Writes body verbatim as a 200 response with the given contentType and the CORS header. | |
| template<typename Res > | |
| void | sendError (Res *res, std::string_view status, std::string_view corsOrigin, std::string_view message, std::optional< std::chrono::microseconds > wireUs=std::nullopt) |
Writes a status response carrying a {"error": message} JSON body and the CORS header. | |
| template<typename Res > | |
| void | sendStatus (Res *res, std::string_view status, std::string_view corsOrigin, std::optional< std::chrono::microseconds > wireUs=std::nullopt) |
Writes a bare status response (no body) with the CORS header. | |
HTTP-transport glue shared by the built-in server and route plug-in libs.
The response helpers here write directly to a uWS::HttpResponse, which is only valid on the event-loop thread — so they are for the handful of framework-level responses that genuinely run there (the OPTIONS preflight, the HTML index, the catch-all 404). **Everything that serves the API goes through mm::api::Router instead**, whose handlers return a Response value and run off the loop. A timed variant of these once existed and was used by every device endpoint; it was removed with the last of them, because a shared header offering the blocking shape is how a fixed bug comes back.
This layer sits above mm::node (the transport-agnostic domain layer) and below the app: it is the one place that knows about uWebSockets. mm::node must never depend on it. It exists so a route plug-in lib (e.g. mm::example) can register endpoints with the exact same response shape (content type + CORS) as the built-in routes without depending on the app.
| using mm::api::Handler = typedef std::function<Response(const Request&)> |
What a route does: a pure function from a snapshotted request to a response.
| using mm::api::RegisterRoutesFn = typedef std::function<void(Router& router, const RouteContext& ctx)> |
A route plug-in: registers its routes on the HTTP app.
Called once, on the HTTP server's event-loop thread, after the built-in routes and before the CORS preflight, the catch-all 404, and listen(). A plug-in should register only its own specific paths (e.g. /api/example/...); the server owns the OPTIONS /api/* preflight and the /* fallthrough. Registration order does not affect matching — uWS routes by specificity — but a plug-in must not claim /api/* or /* wildcards.
Wire one up in the composition root with HttpServer::addRoutes (before start()). A plug-in is handed the Router rather than the raw app, so its handlers run off the event loop like every built-in route. That is the point of passing it: a plug-in that took the app could register a handler doing bus I/O on the loop thread and stall the whole API, which is the bug the Router exists to make unrepresentable — and a shared header offering the unsafe path is how it would come back.
| Response mm::api::badRequest | ( | std::string_view | message | ) |
A 400 with message — the most common failure, so it gets a name.
| Response mm::api::bytes | ( | std::string | contentType, |
| std::string | body | ||
| ) |
A 200 response carrying body verbatim under contentType.
| Response mm::api::error | ( | std::string | status, |
| std::string_view | message | ||
| ) |
A status response carrying a {"error": message} body.
| Response mm::api::json | ( | const nlohmann::json & | body | ) |
A 200 response carrying body as JSON.
Serialised with the replace error handler so a string value carrying non-UTF-8 bytes (a garbage VISIBLE_STRING from a misbehaving device) renders as U+FFFD rather than throwing — and an uncaught throw here would take the server down.
| Response mm::api::notFound | ( | std::string_view | message | ) |
A 404 with message.
| std::vector< std::string > mm::api::parameterNames | ( | std::string_view | pattern | ) |
The :name tokens of a route pattern, in the order uWS will index them.
uWS addresses path parameters positionally — req->getParameter(0); naming them is this layer's doing, so a handler asks for parameter("slavePosition") and stays correct when a route gains a segment ahead of it. A name runs to the next / or to the end of the pattern, so a trailing parameter needs no terminator, and a pattern with none (/api/user-cache/*) yields nothing.
Declared here rather than kept file-local so the mapping can be tested directly: it is the one piece of pattern parsing this layer does itself, and a route whose names come out shifted by one would still compile, still serve, and answer with another parameter's value.
| std::string mm::api::percentDecode | ( | std::string_view | text | ) |
Percent-decodes a URL path component — %20 to a space, %2F to a slash.
RFC 3986 §2.1 calls the mechanism percent-encoding and its unit a percent-encoded octet (a triplet: % and two hex digits); it names the inverse only as "decoding" those octets. The name here follows the WHATWG URL Standard, which calls the operation percent-decode. Not urlDecode, deliberately — that reads as though it folds + into a space, which is exactly what this must not do.
For path components, and deliberately not shared with query decoding: a query decoder also maps + to a space (application/x-www-form-urlencoded), which in a path is a literal +, so decoding a+b.zip that way would look up a b.zip. Request::query handles queries; this handles paths, which uWS hands over still encoded.
An invalid escape (%4Z, a trailing %, %4) is left verbatim rather than dropped, so a name containing one arrives intact at whatever resolves it and either names something real or fails cleanly — instead of silently becoming a different name.
The result may contain a NUL, from %00, and its length is authoritative: never treat it as a C string, or such a name truncates into a different one.
| void mm::api::sendBytes | ( | Res * | res, |
| std::string_view | corsOrigin, | ||
| std::string_view | contentType, | ||
| std::string_view | body | ||
| ) |
Writes body verbatim as a 200 response with the given contentType and the CORS header.
The raw-bytes analogue of sendJson: for a response whose body is already serialized (an octet-stream dump, a verbatim on-disk file, a text/yaml spec) rather than a nlohmann::json value — body is sent as-is. For anything richer — an extra header (e.g. Content-Disposition), a non-200 status, or backpressure-aware streaming via tryEnd — drop to setCorsOrigin + writeHeader directly instead.
| void mm::api::sendError | ( | Res * | res, |
| std::string_view | status, | ||
| std::string_view | corsOrigin, | ||
| std::string_view | message, | ||
| std::optional< std::chrono::microseconds > | wireUs = std::nullopt |
||
| ) |
Writes a status response carrying a {"error": message} JSON body and the CORS header.
Pass wireUs to attach the X-Wire-Us timing header to the failure the same way a success carries it — a failed device transaction still consumed wire time (an SDO read that waits out the mailbox timeout, a partial FoE transfer), and the client renders it identically. The header is written after writeStatus() so uWS keeps the non-200 status (headers-before-status would force a default 200).
| void mm::api::sendJson | ( | Res * | res, |
| std::string_view | corsOrigin, | ||
| const nlohmann::json & | body | ||
| ) |
Writes body as a 200 application/json response with the CORS header.
Uses the replace error handler so a string-typed value carrying non-UTF-8 bytes (e.g. a garbage VISIBLE_STRING from a misbehaving device) is rendered with U+FFFD instead of throwing — an uncaught throw on the uWS loop terminates the whole server.
| void mm::api::sendStatus | ( | Res * | res, |
| std::string_view | status, | ||
| std::string_view | corsOrigin, | ||
| std::optional< std::chrono::microseconds > | wireUs = std::nullopt |
||
| ) |
Writes a bare status response (no body) with the CORS header.
wireUs optionally attaches the server-measured operation time, exactly as sendError does — an endpoint whose success case carries no body (a 204 No Content delete) still spent time doing the work, and the client shows it the same way as for a body-bearing response.
| Res * mm::api::setCorsOrigin | ( | Res * | res, |
| std::string_view | corsOrigin | ||
| ) |
Writes the Access-Control-Allow-Origin header and returns res for chaining.
The single home for the origin-policy header. sendJson/sendError/sendStatus emit it for the common JSON paths; call this directly for a hand-rolled response that can't use them — a raw octet-stream body, a text/yaml dump, or a bespoke status line — so the header name and policy live in exactly one place. Order-independent among headers; call before end().
| Res * mm::api::setWireTime | ( | Res * | res, |
| std::chrono::microseconds | wireUs | ||
| ) |
Attaches the server-measured wire-time header (X-Wire-Us, microseconds) to a response.
wireUs is the server-measured time spent on the device-side operation itself — control-plane lock acquire plus the mailbox/ESC wire transaction(s) — not the end-to-end HTTP round-trip, which a cross-origin browser client observes as much larger (TLS + transport overhead). Reporting it lets the client attribute the device cost to the device and the remainder to the browser/transport. For a single-transaction endpoint (one SDO/FoE/register access) it is essentially the pure wire round-trip; for a multi-transaction one (object-dictionary enumeration, PDO-mapping read/write) it is the total across all of the operation's transactions. Because the value rides a header rather than the body, it is the one uniform timing channel that works for any response shape (JSON or raw octet-stream) without touching each endpoint's body schema. The header is CORS-exposed so the PWA can read it cross-origin. Emitted on both success and failure — a failed operation still consumed device time (e.g. an SDO read that waits out the mailbox timeout), and the client shows it the same way; failures route through sendError()'s wireUs parameter so the header lands after writeStatus(). Call before the body/end() (uWS requires all headers written before the body). Returns res for chaining, mirroring setCorsOrigin.
| Response mm::api::statusOnly | ( | std::string | status | ) |
A bare status response with no body — a 202 that means "under way", a 204 delete.
Named for what it produces rather than the plain status, which collides with the parameter name the neighbouring send* helpers already use for a status line.
| Response mm::api::timed | ( | Op && | op, |
| std::string | errorStatus = "500 Internal Server Error" |
||
| ) |
Runs a device operation, times it, and turns its std::expected into a timed response.
The shape of nearly every device endpoint, in one place: time the call, send the value as JSON with X-Wire-Us on success, or errorStatus with the same header on failure. Only op is timed, so body serialisation stays out of the figure.
Attaches the server-measured device time (X-Wire-Us) to response and returns it.
The uniform timing channel: it rides a header rather than the body, so it works for any response shape, and it is emitted on failure as well as success because a failed device operation still consumed wire time. See the header's own documentation in web_api.h.