Initial commit
This commit is contained in:
33
.editorconfig
Normal file
33
.editorconfig
Normal file
@@ -0,0 +1,33 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.java]
|
||||
indent_size = 4
|
||||
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
|
||||
[*.html]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
45
.gitattributes
vendored
Normal file
45
.gitattributes
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Auto-detect text files and normalize line endings to LF on commit
|
||||
* text=auto eol=lf
|
||||
|
||||
# Java source
|
||||
*.java text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.properties text eol=lf
|
||||
|
||||
# Web
|
||||
*.html text eol=lf
|
||||
*.css text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
|
||||
# Python
|
||||
*.py text eol=lf
|
||||
|
||||
# Docs
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
|
||||
# Shell scripts
|
||||
*.sh text eol=lf
|
||||
|
||||
# Windows scripts (keep CRLF)
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Binary - do not diff or merge
|
||||
*.class binary
|
||||
*.jar binary
|
||||
*.war binary
|
||||
*.ear binary
|
||||
*.db binary
|
||||
*.sqlite binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
*.docx binary
|
||||
*.xlsx binary
|
||||
*.zip binary
|
||||
63
.gitignore
vendored
Normal file
63
.gitignore
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Compiled output
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# IDE - IntelliJ IDEA
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# IDE - Eclipse
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
bin/
|
||||
|
||||
# IDE - VS Code
|
||||
.vscode/
|
||||
|
||||
# IDE - NetBeans
|
||||
nbproject/
|
||||
nbbuild/
|
||||
nbdist/
|
||||
.nb-gradle/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Nexus Workshops LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
BIN
docs/Robot_Telemetry_Complete_Field_Guide.docx
Executable file
BIN
docs/Robot_Telemetry_Complete_Field_Guide.docx
Executable file
Binary file not shown.
BIN
docs/Robot_Telemetry_Complete_Field_Guide.pdf
Normal file
BIN
docs/Robot_Telemetry_Complete_Field_Guide.pdf
Normal file
Binary file not shown.
61
pom.xml
Executable file
61
pom.xml
Executable file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.nexusworkshops</groupId>
|
||||
<artifactId>udp-telemetry</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<mainClass>com.nexusworkshops.telemetry.Main</mainClass>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- JSON serialization - no other deps needed -->
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.nexusworkshops.telemetry.Main</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals><goal>shade</goal></goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
56
src/main/java/com/nexusworkshops/telemetry/Main.java
Executable file
56
src/main/java/com/nexusworkshops/telemetry/Main.java
Executable file
@@ -0,0 +1,56 @@
|
||||
package com.nexusworkshops.telemetry;
|
||||
|
||||
/**
|
||||
* Demo main -- simulates robot telemetry from your desktop.
|
||||
* Run telelog.py in one terminal, this in another.
|
||||
*
|
||||
* Usage:
|
||||
* java -jar target/udp-telemetry-1.0.0.jar
|
||||
* java -jar target/udp-telemetry-1.0.0.jar 127.0.0.1 3000
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
String host = args.length > 0 ? args[0] : "127.0.0.1";
|
||||
int port = args.length > 1 ? Integer.parseInt(args[1]) : 3000;
|
||||
int packets = 20;
|
||||
|
||||
System.out.println("[Main] Sending " + packets + " telemetry packets to " + host + ":" + port);
|
||||
|
||||
UdpTelemetry t = new UdpTelemetry(host, port);
|
||||
|
||||
for (int i = 0; i < packets; i++) {
|
||||
double t_sec = i * 0.05;
|
||||
|
||||
// Simulate motor state
|
||||
double leftPower = Math.sin(t_sec) * 0.8;
|
||||
double rightPower = Math.sin(t_sec + 0.1) * 0.8;
|
||||
|
||||
// Simulate chute hood sweeping open
|
||||
double hoodPos = Math.min(1.0, i / (double) packets);
|
||||
|
||||
// Simulate IMU
|
||||
double yaw = t_sec * 15.0; // degrees
|
||||
double steering = leftPower - rightPower;
|
||||
|
||||
t.put("left_motor", "power", leftPower)
|
||||
.put("left_motor", "temp", 35.0 + i * 0.2)
|
||||
.put("right_motor", "power", rightPower)
|
||||
.put("right_motor", "temp", 34.5 + i * 0.15)
|
||||
.put("chute_hood", "position", hoodPos)
|
||||
.put("imu", "yaw", yaw)
|
||||
.put("imu", "steering", steering)
|
||||
.put("loop_time_ms", i * 50); // flat scalar, no explicit subject
|
||||
|
||||
t.send();
|
||||
|
||||
System.out.printf("[Main] packet %02d yaw=%.1f left=%.2f right=%.2f hood=%.2f%n",
|
||||
i + 1, yaw, leftPower, rightPower, hoodPos);
|
||||
|
||||
Thread.sleep(50); // 50ms loop, ~20Hz
|
||||
}
|
||||
|
||||
t.close();
|
||||
System.out.println("[Main] Done.");
|
||||
}
|
||||
}
|
||||
154
src/main/java/com/nexusworkshops/telemetry/UdpTelemetry.java
Executable file
154
src/main/java/com/nexusworkshops/telemetry/UdpTelemetry.java
Executable file
@@ -0,0 +1,154 @@
|
||||
package com.nexusworkshops.telemetry;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* UdpTelemetry -- sends structured telemetry packets over UDP to telelog.py.
|
||||
*
|
||||
* Packets are JSON objects where top-level keys are subjects and their
|
||||
* values are objects of predicate/value pairs. Flat scalars are allowed
|
||||
* too and will be stored by telelog with the source IP as subject.
|
||||
*
|
||||
* Example packet sent:
|
||||
* {
|
||||
* "left_motor": {"power": 0.8, "temp": 42.1},
|
||||
* "right_motor": {"power": 0.6, "temp": 38.5},
|
||||
* "chute_hood": {"position": 0.45}
|
||||
* }
|
||||
*
|
||||
* Usage:
|
||||
* UdpTelemetry t = new UdpTelemetry("192.168.43.100", 3000);
|
||||
* t.put("left_motor", "power", 0.8);
|
||||
* t.put("left_motor", "temp", 42.1);
|
||||
* t.put("right_motor", "power", 0.6);
|
||||
* t.send(); // transmits and clears the buffer
|
||||
* t.close();
|
||||
*/
|
||||
public class UdpTelemetry {
|
||||
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final Gson gson;
|
||||
private DatagramSocket socket;
|
||||
|
||||
// subject -> (predicate -> value)
|
||||
private final Map<String, Map<String, Object>> buffer;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public UdpTelemetry(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.gson = new Gson();
|
||||
this.buffer = new LinkedHashMap<>();
|
||||
try {
|
||||
this.socket = new DatagramSocket();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to open UDP socket: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Building a packet
|
||||
|
||||
/**
|
||||
* Add a subject/predicate/value triple to the outgoing buffer.
|
||||
* Call this multiple times before send() to batch related data.
|
||||
*/
|
||||
public UdpTelemetry put(String subject, String predicate, Object value) {
|
||||
buffer.computeIfAbsent(subject, k -> new LinkedHashMap<>())
|
||||
.put(predicate, value);
|
||||
return this; // fluent
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: flat key/value with no explicit subject.
|
||||
* telelog will assign the source IP as the subject.
|
||||
*/
|
||||
public UdpTelemetry put(String predicate, Object value) {
|
||||
// Store under a sentinel key that becomes a top-level scalar in JSON
|
||||
buffer.computeIfAbsent("", k -> new LinkedHashMap<>())
|
||||
.put(predicate, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Sending
|
||||
|
||||
/**
|
||||
* Serialize the current buffer to JSON and transmit it, then clear.
|
||||
* Safe to call even if buffer is empty (sends an empty object).
|
||||
*/
|
||||
public void send() {
|
||||
JsonObject json = new JsonObject();
|
||||
|
||||
for (Map.Entry<String, Map<String, Object>> subjectEntry : buffer.entrySet()) {
|
||||
String subject = subjectEntry.getKey();
|
||||
Map<String, Object> predicates = subjectEntry.getValue();
|
||||
|
||||
if (subject.isEmpty()) {
|
||||
// Flat scalars go directly at the top level
|
||||
for (Map.Entry<String, Object> e : predicates.entrySet()) {
|
||||
addValue(json, e.getKey(), e.getValue());
|
||||
}
|
||||
} else {
|
||||
JsonObject inner = new JsonObject();
|
||||
for (Map.Entry<String, Object> e : predicates.entrySet()) {
|
||||
addValue(inner, e.getKey(), e.getValue());
|
||||
}
|
||||
json.add(subject, inner);
|
||||
}
|
||||
}
|
||||
|
||||
transmit(gson.toJson(json));
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a raw JSON string directly, bypassing the buffer.
|
||||
* Useful for sending pre-built payloads.
|
||||
*/
|
||||
public void sendRaw(String json) {
|
||||
transmit(json);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Cleanup
|
||||
|
||||
public void close() {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
|
||||
private void transmit(String payload) {
|
||||
try {
|
||||
byte[] bytes = payload.getBytes("UTF-8");
|
||||
InetAddress addr = InetAddress.getByName(host);
|
||||
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, addr, port);
|
||||
socket.send(packet);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[UdpTelemetry] Send failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void addValue(JsonObject obj, String key, Object value) {
|
||||
if (value instanceof Number) {
|
||||
obj.addProperty(key, (Number) value);
|
||||
} else if (value instanceof Boolean) {
|
||||
obj.addProperty(key, (Boolean) value);
|
||||
} else {
|
||||
obj.addProperty(key, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
238
telelog.py
Executable file
238
telelog.py
Executable file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
telelog.py -- UDP telemetry logger using a triplestore SQLite backend
|
||||
|
||||
Usage:
|
||||
python3 telelog.py --init-db # create/reset schema and exit
|
||||
python3 telelog.py # listen on default port, write to db
|
||||
python3 telelog.py --port 3001 # specify port
|
||||
python3 telelog.py --db mydata.db # specify db file
|
||||
python3 telelog.py --quiet # no console output
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import socket
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
DEFAULT_PORT = 3000
|
||||
DEFAULT_DB = "telemetry.db"
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
BUFFER_SIZE = 65535
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema
|
||||
# ---------------------------------------------------------------------------
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS packets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts TEXT NOT NULL,
|
||||
source TEXT,
|
||||
tag TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_packets_tag ON packets(tag);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS triples (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
packet_id INTEGER NOT NULL REFERENCES packets(id),
|
||||
subject TEXT NOT NULL,
|
||||
predicate TEXT NOT NULL,
|
||||
object TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_triples_subject ON triples(subject);
|
||||
CREATE INDEX IF NOT EXISTS idx_triples_predicate ON triples(predicate);
|
||||
CREATE INDEX IF NOT EXISTS idx_triples_packet ON triples(packet_id);
|
||||
"""
|
||||
|
||||
|
||||
def init_db(db_path):
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.executescript(SCHEMA)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"[telelog] Schema initialized: {db_path}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ingest
|
||||
# ---------------------------------------------------------------------------
|
||||
def insert_packet(conn, ts, source, tag, triples):
|
||||
"""
|
||||
Insert one packet and its subject-predicate-object triples.
|
||||
triples is a list of (subject, predicate, object) tuples.
|
||||
Returns the new packet_id.
|
||||
"""
|
||||
cur = conn.execute(
|
||||
"INSERT INTO packets (ts, source, tag) VALUES (?, ?, ?)",
|
||||
(ts, source, tag)
|
||||
)
|
||||
packet_id = cur.lastrowid
|
||||
|
||||
rows = [(packet_id, s, p, o) for s, p, o in triples]
|
||||
conn.executemany(
|
||||
"INSERT INTO triples (packet_id, subject, predicate, object) VALUES (?, ?, ?, ?)",
|
||||
rows
|
||||
)
|
||||
conn.commit()
|
||||
return packet_id
|
||||
|
||||
|
||||
def extract_triples(obj, fallback_subject):
|
||||
"""
|
||||
Convert a parsed JSON payload into a list of (subject, predicate, object) tuples.
|
||||
|
||||
Rules:
|
||||
- If the top-level value is a dict of dicts, the top-level key is the subject
|
||||
and its child keys are predicates:
|
||||
{"left_motor": {"power": 0.8, "temp": 42}}
|
||||
-> ("left_motor", "power", "0.8"), ("left_motor", "temp", "42")
|
||||
|
||||
- If the top-level value is a scalar or mixed, the top-level key is the
|
||||
predicate and fallback_subject (source IP) is the subject:
|
||||
{"heading": 1.57}
|
||||
-> (fallback_subject, "heading", "1.57")
|
||||
|
||||
- Deeper nesting collapses predicates with dot notation:
|
||||
{"left_motor": {"pid": {"p": 1.0}}}
|
||||
-> ("left_motor", "pid.p", "1.0")
|
||||
|
||||
- Lists are stored as JSON strings.
|
||||
- Non-dict top-level payload falls back to (fallback_subject, "value", raw).
|
||||
"""
|
||||
triples = []
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
triples.append((fallback_subject, "value", str(obj)))
|
||||
return triples
|
||||
|
||||
for top_key, top_val in obj.items():
|
||||
if isinstance(top_val, dict):
|
||||
# top_key is the subject; flatten children as predicates
|
||||
subject = top_key
|
||||
for pred, obj_val in _flatten_predicates(top_val).items():
|
||||
triples.append((subject, pred, str(obj_val)))
|
||||
else:
|
||||
# top_key is the predicate; use fallback subject
|
||||
if isinstance(top_val, list):
|
||||
triples.append((fallback_subject, top_key, json.dumps(top_val)))
|
||||
else:
|
||||
triples.append((fallback_subject, top_key, str(top_val)))
|
||||
|
||||
return triples
|
||||
|
||||
|
||||
def _flatten_predicates(obj, prefix=""):
|
||||
"""Recursively flatten a dict into dot-separated predicate keys."""
|
||||
out = {}
|
||||
for k, v in obj.items():
|
||||
key = f"{prefix}.{k}" if prefix else k
|
||||
if isinstance(v, dict):
|
||||
out.update(_flatten_predicates(v, key))
|
||||
elif isinstance(v, list):
|
||||
out[key] = json.dumps(v)
|
||||
else:
|
||||
out[key] = v
|
||||
return out
|
||||
|
||||
|
||||
def parse_payload(raw_bytes, fallback_subject):
|
||||
"""
|
||||
Attempt to parse payload as JSON. Fall back to raw string.
|
||||
Returns a list of (subject, predicate, object) tuples.
|
||||
"""
|
||||
try:
|
||||
text = raw_bytes.decode("utf-8").strip()
|
||||
except UnicodeDecodeError:
|
||||
return [(fallback_subject, "_raw_hex", raw_bytes.hex())]
|
||||
|
||||
try:
|
||||
data = json.loads(text)
|
||||
return extract_triples(data, fallback_subject)
|
||||
except json.JSONDecodeError:
|
||||
return [(fallback_subject, "_raw", text)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop
|
||||
# ---------------------------------------------------------------------------
|
||||
def run(db_path, host, port, tag, quiet):
|
||||
conn = sqlite3.connect(db_path)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((host, port))
|
||||
sock.settimeout(1.0) # allows clean shutdown on Ctrl-C
|
||||
|
||||
tag_display = tag if tag else "(untagged)"
|
||||
print(f"[telelog] Listening on {host}:{port} db={db_path} tag={tag_display}")
|
||||
print(f"[telelog] Ctrl-C to stop")
|
||||
|
||||
running = True
|
||||
|
||||
def handle_signal(sig, frame):
|
||||
nonlocal running
|
||||
running = False
|
||||
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
|
||||
packet_count = 0
|
||||
|
||||
while running:
|
||||
try:
|
||||
data, addr = sock.recvfrom(BUFFER_SIZE)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
|
||||
ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
|
||||
source = f"{addr[0]}:{addr[1]}"
|
||||
triples = parse_payload(data, source)
|
||||
|
||||
packet_id = insert_packet(conn, ts, source, tag, triples)
|
||||
packet_count += 1
|
||||
|
||||
if not quiet:
|
||||
subjects = sorted(set(s for s, p, o in triples))
|
||||
print(f"[{ts}] src={source} id={packet_id} subjects={subjects}")
|
||||
|
||||
sock.close()
|
||||
conn.close()
|
||||
print(f"\n[telelog] Stopped. {packet_count} packets written.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="UDP telemetry triplestore logger")
|
||||
parser.add_argument("--db", default=DEFAULT_DB, help=f"SQLite db file (default: {DEFAULT_DB})")
|
||||
parser.add_argument("--port", default=DEFAULT_PORT, type=int, help=f"UDP listen port (default: {DEFAULT_PORT})")
|
||||
parser.add_argument("--host", default=DEFAULT_HOST, help=f"Bind address (default: {DEFAULT_HOST})")
|
||||
parser.add_argument("--init-db", action="store_true", help="Initialize/reset schema and exit")
|
||||
parser.add_argument("--tag", default=None, help="Label for this session (e.g. 'auto_match_3')")
|
||||
parser.add_argument("--quiet", action="store_true", help="Suppress per-packet console output")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.init_db:
|
||||
init_db(args.db)
|
||||
sys.exit(0)
|
||||
|
||||
# Ensure schema exists before running (idempotent)
|
||||
init_db(args.db)
|
||||
run(args.db, args.host, args.port, args.tag, args.quiet)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user