A finished signal recording goes to an outbox and is sent gzipped with
the car's token (PUT /v1/logs/{name}); the service retries every 2
minutes while the car is offline. The server keeps the newest 200 on a
new volume (LOGS_DIR); reading takes a token of its own (LOGS_TOKEN).
fetch-logs in the dev shell fetches new ones into logs/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
105 lines
4.2 KiB
Java
105 lines
4.2 KiB
Java
package com.evsuite.abrp;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
/**
|
|
* One JSON request to the car server, shared by telemetry, consumption sync and pairing (and,
|
|
* through {@link #send}, the direct ABRP upload).
|
|
* Blocking, with short timeouts — call it off the main thread.
|
|
*
|
|
* The token goes in the Authorization header, never the URL, and is never logged. The answer
|
|
* body is read for every status (error bodies name the problem) and capped, so a wrong server
|
|
* cannot fill memory.
|
|
*/
|
|
final class ServerHttp {
|
|
|
|
static final int TIMEOUT_MS = 8_000;
|
|
private static final int MAX_BODY_BYTES = 64 * 1024;
|
|
|
|
/** Status and body of an answer. */
|
|
static final class Response {
|
|
final int status;
|
|
final String body;
|
|
|
|
Response(int status, String body) {
|
|
this.status = status;
|
|
this.body = body;
|
|
}
|
|
|
|
boolean ok() { return status >= 200 && status < 300; }
|
|
|
|
/** The body for a log line: short, on one line. */
|
|
String excerpt() {
|
|
String text = body.replace('\n', ' ').trim();
|
|
return text.length() > 300 ? text.substring(0, 300) + "…" : text;
|
|
}
|
|
}
|
|
|
|
private ServerHttp() { }
|
|
|
|
/**
|
|
* Sends {@code json} (or nothing, when null) with {@code method} to {@code url}.
|
|
* {@code token} may be null for the unauthenticated pairing calls.
|
|
*/
|
|
static Response request(String method, String url, String token, JSONObject json) throws Exception {
|
|
byte[] body = json == null ? null : json.toString().getBytes(StandardCharsets.UTF_8);
|
|
return send(method, url, token == null ? null : "Bearer " + token, "application/json", body);
|
|
}
|
|
|
|
/**
|
|
* The request underneath, also used for ABRP ({@link AbrpApi}): {@code authorization} is the
|
|
* whole header value or null, {@code body} null for none.
|
|
*/
|
|
static Response send(String method, String url, String authorization, String contentType, byte[] body)
|
|
throws Exception {
|
|
return send(method, url, authorization, contentType, null, body);
|
|
}
|
|
|
|
/** As above, with a {@code Content-Encoding} ({@code "gzip"}) for the body, or null. */
|
|
static Response send(String method, String url, String authorization, String contentType,
|
|
String contentEncoding, byte[] body) throws Exception {
|
|
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
try {
|
|
connection.setRequestMethod(method);
|
|
connection.setConnectTimeout(TIMEOUT_MS);
|
|
connection.setReadTimeout(TIMEOUT_MS);
|
|
if (authorization != null) connection.setRequestProperty("Authorization", authorization);
|
|
if (body != null) {
|
|
connection.setDoOutput(true);
|
|
connection.setFixedLengthStreamingMode(body.length);
|
|
connection.setRequestProperty("Content-Type", contentType);
|
|
if (contentEncoding != null) connection.setRequestProperty("Content-Encoding", contentEncoding);
|
|
try (OutputStream out = connection.getOutputStream()) { out.write(body); }
|
|
}
|
|
int status = connection.getResponseCode();
|
|
return new Response(status, readBody(connection, status));
|
|
} finally {
|
|
connection.disconnect();
|
|
}
|
|
}
|
|
|
|
private static String readBody(HttpURLConnection connection, int status) {
|
|
try {
|
|
InputStream in = status < 400 ? connection.getInputStream() : connection.getErrorStream();
|
|
if (in == null) return "";
|
|
try (InputStream stream = in) {
|
|
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
byte[] chunk = new byte[4096];
|
|
int n;
|
|
while ((n = stream.read(chunk)) != -1 && buf.size() < MAX_BODY_BYTES) buf.write(chunk, 0, n);
|
|
return buf.toString(StandardCharsets.UTF_8.name());
|
|
}
|
|
} catch (Exception e) {
|
|
// The status is what matters; an unreadable body is just empty.
|
|
return "";
|
|
}
|
|
}
|
|
}
|