localty-system-protocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- localty_protocol/__init__.py +47 -0
- localty_protocol/command.py +62 -0
- localty_protocol/telemetry.py +213 -0
- localty_protocol/version.py +1 -0
- localty_protocol/video.py +91 -0
- localty_system_protocol-0.1.0.dist-info/METADATA +163 -0
- localty_system_protocol-0.1.0.dist-info/RECORD +10 -0
- localty_system_protocol-0.1.0.dist-info/WHEEL +5 -0
- localty_system_protocol-0.1.0.dist-info/licenses/LICENSE +21 -0
- localty_system_protocol-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .command import (
|
|
2
|
+
# formats / sizes
|
|
3
|
+
HDR_FMT,
|
|
4
|
+
HDR_SIZE,
|
|
5
|
+
DRIVE_FMT,
|
|
6
|
+
DRIVE_SIZE,
|
|
7
|
+
CMD_DRIVE,
|
|
8
|
+
CMD_STOP,
|
|
9
|
+
|
|
10
|
+
# types
|
|
11
|
+
Header,
|
|
12
|
+
|
|
13
|
+
# functions
|
|
14
|
+
pack_drive,
|
|
15
|
+
pack_stop,
|
|
16
|
+
decode_packet,
|
|
17
|
+
)
|
|
18
|
+
from .telemetry import (
|
|
19
|
+
ANNOUNCE_PORT,
|
|
20
|
+
CONTROL_PORT,
|
|
21
|
+
TELEMETRY_CHANNELS,
|
|
22
|
+
TELEMETRY_FAST_PORT,
|
|
23
|
+
TELEMETRY_SLOW_PORT,
|
|
24
|
+
UDP_PORTS,
|
|
25
|
+
VIDEO_PORT,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"HDR_FMT",
|
|
30
|
+
"HDR_SIZE",
|
|
31
|
+
"DRIVE_FMT",
|
|
32
|
+
"DRIVE_SIZE",
|
|
33
|
+
"CMD_DRIVE",
|
|
34
|
+
"CMD_STOP",
|
|
35
|
+
"Header",
|
|
36
|
+
"pack_drive",
|
|
37
|
+
"pack_stop",
|
|
38
|
+
"decode_packet",
|
|
39
|
+
"ANNOUNCE_PORT",
|
|
40
|
+
"CONTROL_PORT",
|
|
41
|
+
"TELEMETRY_CHANNELS",
|
|
42
|
+
"TELEMETRY_FAST_PORT",
|
|
43
|
+
"TELEMETRY_SLOW_PORT",
|
|
44
|
+
"UDP_PORTS",
|
|
45
|
+
"VIDEO_PORT",
|
|
46
|
+
]
|
|
47
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import struct
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
# Header: seq, ts_ms, cmd_id, flags(reserved)
|
|
7
|
+
HDR_FMT = "!IIHH"
|
|
8
|
+
HDR_SIZE = struct.calcsize(HDR_FMT)
|
|
9
|
+
|
|
10
|
+
DRIVE_FMT = "!ff"
|
|
11
|
+
DRIVE_SIZE = struct.calcsize(DRIVE_FMT)
|
|
12
|
+
|
|
13
|
+
# Command IDs
|
|
14
|
+
CMD_DRIVE: int = 1
|
|
15
|
+
CMD_STOP: int = 2 # 例
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def now_ms_u32() -> int:
|
|
19
|
+
# u32で回す(wrapするがOK)
|
|
20
|
+
return int(time.time() * 1000) & 0xFFFFFFFF
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Header:
|
|
25
|
+
seq: int
|
|
26
|
+
ts_ms: int
|
|
27
|
+
cmd_id: int
|
|
28
|
+
flags: int = 0
|
|
29
|
+
|
|
30
|
+
def pack(self) -> bytes:
|
|
31
|
+
return struct.pack(HDR_FMT, self.seq, self.ts_ms, self.cmd_id, self.flags)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def unpack(data: bytes) -> "Header":
|
|
35
|
+
seq, ts_ms, cmd_id, flags = struct.unpack(HDR_FMT, data[:HDR_SIZE])
|
|
36
|
+
return Header(seq=seq, ts_ms=ts_ms, cmd_id=cmd_id, flags=flags)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pack_drive(seq: int, vx: float, wz: float, ts_ms: int | None = None) -> bytes:
|
|
40
|
+
if ts_ms is None:
|
|
41
|
+
ts_ms = now_ms_u32()
|
|
42
|
+
hdr = Header(seq=seq, ts_ms=ts_ms, cmd_id=CMD_DRIVE, flags=0).pack()
|
|
43
|
+
body = struct.pack(DRIVE_FMT, float(vx), float(wz))
|
|
44
|
+
return hdr + body
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def pack_stop(seq: int, ts_ms: int | None = None) -> bytes:
|
|
48
|
+
if ts_ms is None:
|
|
49
|
+
ts_ms = now_ms_u32()
|
|
50
|
+
hdr = Header(seq=seq, ts_ms=ts_ms, cmd_id=CMD_STOP, flags=0).pack()
|
|
51
|
+
# stopはボディなしにしてもいい(拡張性のためflagsで表現してもいい)
|
|
52
|
+
return hdr
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def decode_packet(data: bytes) -> tuple[Header, bytes]:
|
|
56
|
+
"""
|
|
57
|
+
戻り値: (Header, body_bytes)
|
|
58
|
+
"""
|
|
59
|
+
if len(data) < HDR_SIZE:
|
|
60
|
+
raise ValueError(f"packet too short: {len(data)} < {HDR_SIZE}")
|
|
61
|
+
hdr = Header.unpack(data)
|
|
62
|
+
return hdr, data[HDR_SIZE:]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ENCODING = "utf-8"
|
|
9
|
+
|
|
10
|
+
CONTROL_PORT = 5005
|
|
11
|
+
ANNOUNCE_PORT = 5006
|
|
12
|
+
VIDEO_PORT = 5000
|
|
13
|
+
TELEMETRY_FAST_PORT = 5010
|
|
14
|
+
TELEMETRY_SLOW_PORT = 5011
|
|
15
|
+
|
|
16
|
+
TYPE_HEARTBEAT = "heartbeat"
|
|
17
|
+
TYPE_DISTANCE = "distance"
|
|
18
|
+
TYPE_IMU = "imu"
|
|
19
|
+
TYPE_GPS = "gps"
|
|
20
|
+
TYPE_BATTERY = "battery"
|
|
21
|
+
|
|
22
|
+
KEY_TYPE = "type"
|
|
23
|
+
KEY_TIMESTAMP = "timestamp"
|
|
24
|
+
KEY_CM = "cm"
|
|
25
|
+
KEY_AX = "ax"
|
|
26
|
+
KEY_AY = "ay"
|
|
27
|
+
KEY_AZ = "az"
|
|
28
|
+
KEY_GX = "gx"
|
|
29
|
+
KEY_GY = "gy"
|
|
30
|
+
KEY_GZ = "gz"
|
|
31
|
+
KEY_FIX = "fix"
|
|
32
|
+
KEY_SAT = "sat"
|
|
33
|
+
KEY_HDOP = "hdop"
|
|
34
|
+
KEY_ALT_M = "alt_m"
|
|
35
|
+
KEY_LAT = "lat"
|
|
36
|
+
KEY_LON = "lon"
|
|
37
|
+
KEY_SPEED = "speed"
|
|
38
|
+
KEY_COURSE = "course"
|
|
39
|
+
KEY_PERCENT = "percent"
|
|
40
|
+
KEY_VOLTAGE = "voltage"
|
|
41
|
+
KEY_DEVICE_ID = "device_id"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class UdpPort:
|
|
46
|
+
name: str
|
|
47
|
+
port: int
|
|
48
|
+
direction: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class TelemetryChannel:
|
|
53
|
+
name: str
|
|
54
|
+
port: int
|
|
55
|
+
message_types: list[str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class TelemetryDistance:
|
|
60
|
+
type: str
|
|
61
|
+
cm: float
|
|
62
|
+
timestamp: float
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class TelemetryImu:
|
|
67
|
+
type: str
|
|
68
|
+
ax: float
|
|
69
|
+
ay: float
|
|
70
|
+
az: float
|
|
71
|
+
gx: float
|
|
72
|
+
gy: float
|
|
73
|
+
gz: float
|
|
74
|
+
timestamp: float
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class TelemetryGps:
|
|
79
|
+
type: str
|
|
80
|
+
fix: int
|
|
81
|
+
sat: int
|
|
82
|
+
hdop: float
|
|
83
|
+
alt_m: float | None
|
|
84
|
+
lat: float | None
|
|
85
|
+
lon: float | None
|
|
86
|
+
speed: float
|
|
87
|
+
course: float
|
|
88
|
+
timestamp: float
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class TelemetryBattery:
|
|
93
|
+
type: str
|
|
94
|
+
percent: float
|
|
95
|
+
voltage: float
|
|
96
|
+
timestamp: float
|
|
97
|
+
device_id: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
UDP_PORTS: list[UdpPort] = [
|
|
101
|
+
UdpPort("control", CONTROL_PORT, "gui-to-robot"),
|
|
102
|
+
UdpPort("announce", ANNOUNCE_PORT, "robot-to-gui"),
|
|
103
|
+
UdpPort("video", VIDEO_PORT, "robot-to-gui"),
|
|
104
|
+
UdpPort("telemetry_fast", TELEMETRY_FAST_PORT, "robot-to-gui"),
|
|
105
|
+
UdpPort("telemetry_slow", TELEMETRY_SLOW_PORT, "robot-to-gui"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
TELEMETRY_CHANNELS: list[TelemetryChannel] = [
|
|
109
|
+
TelemetryChannel("fast", TELEMETRY_FAST_PORT, [TYPE_HEARTBEAT, TYPE_DISTANCE, TYPE_IMU]),
|
|
110
|
+
TelemetryChannel("slow", TELEMETRY_SLOW_PORT, [TYPE_GPS, TYPE_BATTERY]),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def udp_port(name: str) -> int:
|
|
115
|
+
for item in UDP_PORTS:
|
|
116
|
+
if item.name == name:
|
|
117
|
+
return item.port
|
|
118
|
+
raise KeyError(f"unknown UDP port: {name}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def telemetry_ports() -> list[int]:
|
|
122
|
+
return [channel.port for channel in TELEMETRY_CHANNELS]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def telemetry_channel_for_type(message_type: str) -> TelemetryChannel | None:
|
|
126
|
+
for channel in TELEMETRY_CHANNELS:
|
|
127
|
+
if message_type in channel.message_types:
|
|
128
|
+
return channel
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def telemetry_port_for_type(message_type: str) -> int | None:
|
|
133
|
+
channel = telemetry_channel_for_type(message_type)
|
|
134
|
+
return channel.port if channel else None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def heartbeat_payload(timestamp: float) -> dict[str, Any]:
|
|
138
|
+
return {KEY_TYPE: TYPE_HEARTBEAT, KEY_TIMESTAMP: float(timestamp)}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def distance_payload(cm: float, timestamp: float) -> dict[str, Any]:
|
|
142
|
+
return asdict(TelemetryDistance(TYPE_DISTANCE, float(cm), float(timestamp)))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def imu_payload(
|
|
146
|
+
ax: float,
|
|
147
|
+
ay: float,
|
|
148
|
+
az: float,
|
|
149
|
+
gx: float,
|
|
150
|
+
gy: float,
|
|
151
|
+
gz: float,
|
|
152
|
+
timestamp: float,
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
return asdict(
|
|
155
|
+
TelemetryImu(
|
|
156
|
+
TYPE_IMU,
|
|
157
|
+
float(ax),
|
|
158
|
+
float(ay),
|
|
159
|
+
float(az),
|
|
160
|
+
float(gx),
|
|
161
|
+
float(gy),
|
|
162
|
+
float(gz),
|
|
163
|
+
float(timestamp),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def gps_payload(
|
|
169
|
+
fix: int,
|
|
170
|
+
sat: int,
|
|
171
|
+
hdop: float,
|
|
172
|
+
alt_m: float | None,
|
|
173
|
+
lat: float | None,
|
|
174
|
+
lon: float | None,
|
|
175
|
+
speed: float,
|
|
176
|
+
course: float,
|
|
177
|
+
timestamp: float,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
return asdict(
|
|
180
|
+
TelemetryGps(
|
|
181
|
+
TYPE_GPS,
|
|
182
|
+
int(fix),
|
|
183
|
+
int(sat),
|
|
184
|
+
float(hdop),
|
|
185
|
+
None if alt_m is None else float(alt_m),
|
|
186
|
+
None if lat is None else float(lat),
|
|
187
|
+
None if lon is None else float(lon),
|
|
188
|
+
float(speed),
|
|
189
|
+
float(course),
|
|
190
|
+
float(timestamp),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def battery_payload(percent: float, voltage: float, timestamp: float, device_id: str) -> dict[str, Any]:
|
|
196
|
+
return asdict(
|
|
197
|
+
TelemetryBattery(TYPE_BATTERY, float(percent), float(voltage), float(timestamp), str(device_id))
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def encode_payload(payload: dict[str, Any]) -> bytes:
|
|
202
|
+
return json.dumps(payload).encode(ENCODING)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def decode_payload(data: bytes) -> dict[str, Any] | None:
|
|
206
|
+
try:
|
|
207
|
+
text = data.decode(ENCODING).strip()
|
|
208
|
+
if not text:
|
|
209
|
+
return None
|
|
210
|
+
payload = json.loads(text)
|
|
211
|
+
except Exception:
|
|
212
|
+
return None
|
|
213
|
+
return payload if isinstance(payload, dict) else None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PROTOCOL_VERSION = "0.1.0"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import struct
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
# header: frame_id(uint32), chunk_idx(uint16), total_chunks(uint16), payload_len(uint16)
|
|
8
|
+
HDR_FMT: str = "!IHHH"
|
|
9
|
+
HDR_SIZE: int = struct.calcsize(HDR_FMT)
|
|
10
|
+
|
|
11
|
+
# UDPではMTU超えしないよう余裕を見る(LANでも安全側)
|
|
12
|
+
DEFAULT_MTU_SAFE: int = 1200
|
|
13
|
+
DEFAULT_CHUNK_PAYLOAD: int = DEFAULT_MTU_SAFE - HDR_SIZE
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class VideoChunk:
|
|
18
|
+
frame_id: int
|
|
19
|
+
chunk_idx: int
|
|
20
|
+
total_chunks: int
|
|
21
|
+
payload: bytes
|
|
22
|
+
|
|
23
|
+
def encode(self) -> bytes:
|
|
24
|
+
plen = len(self.payload)
|
|
25
|
+
header = struct.pack(HDR_FMT, self.frame_id & 0xFFFFFFFF,
|
|
26
|
+
self.chunk_idx & 0xFFFF,
|
|
27
|
+
self.total_chunks & 0xFFFF,
|
|
28
|
+
plen & 0xFFFF)
|
|
29
|
+
return header + self.payload
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def decode(cls, data: bytes) -> "VideoChunk":
|
|
33
|
+
if len(data) < HDR_SIZE:
|
|
34
|
+
raise ValueError("VideoChunk too short for header")
|
|
35
|
+
frame_id, idx, total, plen = struct.unpack(HDR_FMT, data[:HDR_SIZE])
|
|
36
|
+
payload = data[HDR_SIZE:HDR_SIZE + plen]
|
|
37
|
+
if len(payload) != plen:
|
|
38
|
+
raise ValueError("VideoChunk payload length mismatch")
|
|
39
|
+
return cls(frame_id=frame_id, chunk_idx=idx, total_chunks=total, payload=payload)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def split_frame(frame_id: int, frame_bytes: bytes, chunk_payload: int = DEFAULT_CHUNK_PAYLOAD) -> List[VideoChunk]:
|
|
43
|
+
if chunk_payload <= 0:
|
|
44
|
+
raise ValueError("chunk_payload must be > 0")
|
|
45
|
+
|
|
46
|
+
total = (len(frame_bytes) + chunk_payload - 1) // chunk_payload
|
|
47
|
+
total = max(1, total)
|
|
48
|
+
|
|
49
|
+
chunks: List[VideoChunk] = []
|
|
50
|
+
for idx in range(total):
|
|
51
|
+
start = idx * chunk_payload
|
|
52
|
+
end = min(len(frame_bytes), (idx + 1) * chunk_payload)
|
|
53
|
+
chunks.append(VideoChunk(frame_id=frame_id, chunk_idx=idx, total_chunks=total, payload=frame_bytes[start:end]))
|
|
54
|
+
return chunks
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FrameReassembler:
|
|
58
|
+
"""Collect VideoChunk and reconstruct a complete frame.
|
|
59
|
+
Incomplete frames can be dropped by caller based on timeout.
|
|
60
|
+
"""
|
|
61
|
+
def __init__(self):
|
|
62
|
+
self._frame_id: Optional[int] = None
|
|
63
|
+
self._total: int = 0
|
|
64
|
+
self._chunks: Dict[int, bytes] = {}
|
|
65
|
+
|
|
66
|
+
def reset(self) -> None:
|
|
67
|
+
self._frame_id = None
|
|
68
|
+
self._total = 0
|
|
69
|
+
self._chunks = {}
|
|
70
|
+
|
|
71
|
+
def push(self, chunk: VideoChunk) -> Tuple[Optional[int], Optional[bytes]]:
|
|
72
|
+
"""Returns (frame_id, frame_bytes) when complete, else (None, None)."""
|
|
73
|
+
if self._frame_id is None or chunk.frame_id != self._frame_id:
|
|
74
|
+
# new frame -> reset buffer
|
|
75
|
+
self._frame_id = chunk.frame_id
|
|
76
|
+
self._total = chunk.total_chunks
|
|
77
|
+
self._chunks = {}
|
|
78
|
+
|
|
79
|
+
if chunk.total_chunks != self._total:
|
|
80
|
+
return (None, None)
|
|
81
|
+
|
|
82
|
+
self._chunks[chunk.chunk_idx] = chunk.payload
|
|
83
|
+
|
|
84
|
+
if len(self._chunks) == self._total:
|
|
85
|
+
# ensure order
|
|
86
|
+
payload = b"".join(self._chunks[i] for i in range(self._total) if i in self._chunks)
|
|
87
|
+
fid = self._frame_id
|
|
88
|
+
self.reset()
|
|
89
|
+
return (fid, payload)
|
|
90
|
+
|
|
91
|
+
return (None, None)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: localty-system-protocol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared protocol definitions for the Localty system
|
|
5
|
+
Author: Localty Project
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/inabako/localty-system-protocol
|
|
8
|
+
Project-URL: Issues, https://github.com/inabako/localty-system-protocol/issues
|
|
9
|
+
Keywords: localty,robotics,protocol,udp,telemetry
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# localty-system-protocol
|
|
24
|
+
|
|
25
|
+
Localty システムにおけるロボット・GUI 間通信プロトコル仕様。
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## 概要
|
|
30
|
+
|
|
31
|
+
`localty-system-protocol` は、Localty ロボット制御システムにおける
|
|
32
|
+
GUI ↔ ロボット間の通信仕様を定義するプロトコルリポジトリです。
|
|
33
|
+
|
|
34
|
+
本リポジトリは以下を目的とします:
|
|
35
|
+
|
|
36
|
+
- 通信仕様の単一ソース化
|
|
37
|
+
- 実装と仕様の乖離防止
|
|
38
|
+
- デバッグおよび拡張時の設計基準の提供
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
本READMEは Localty 通信仕様の正規ドキュメントであり、実装は本仕様に従って構築される。
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## 構成
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
localty_protocol/
|
|
50
|
+
├─ command.py
|
|
51
|
+
├─ telemetry.py
|
|
52
|
+
├─ video.py
|
|
53
|
+
└─ version.py
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`localty_protocol.telemetry` は、UDPポートとTelemetry JSONの共通定義を管理する。
|
|
58
|
+
|
|
59
|
+
- `UDP_PORTS`: control / announce / video / telemetry_fast / telemetry_slow の一覧
|
|
60
|
+
- `TELEMETRY_CHANNELS`: FAST / SLOW channelと対応message typeの一覧
|
|
61
|
+
- `*_payload()`: Robot / Simulator が送信する既存JSON形状の生成
|
|
62
|
+
- `encode_payload()` / `decode_payload()`: JSON over UDP の共通encode/decode
|
|
63
|
+
|
|
64
|
+
## UDPポート
|
|
65
|
+
|
|
66
|
+
UDPポートの正規定義は `localty_protocol.telemetry.UDP_PORTS` です。
|
|
67
|
+
README、各repoのdocs、Docker設定はこの一覧に合わせます。
|
|
68
|
+
|
|
69
|
+
| name | direction | port |
|
|
70
|
+
| --- | --- | ---: |
|
|
71
|
+
| `control` | GUI -> robot/simulator | `5005/udp` |
|
|
72
|
+
| `announce` | robot/simulator -> GUI | `5006/udp` |
|
|
73
|
+
| `video` | robot/simulator -> GUI | `5000/udp` |
|
|
74
|
+
| `telemetry_fast` | robot/simulator -> GUI | `5010/udp` |
|
|
75
|
+
| `telemetry_slow` | robot/simulator -> GUI | `5011/udp` |
|
|
76
|
+
|
|
77
|
+
Telemetry channelの正規定義は `localty_protocol.telemetry.TELEMETRY_CHANNELS` です。
|
|
78
|
+
|
|
79
|
+
| channel | port | message types |
|
|
80
|
+
| --- | ---: | --- |
|
|
81
|
+
| `fast` | `5010/udp` | `heartbeat`, `distance`, `imu` |
|
|
82
|
+
| `slow` | `5011/udp` | `gps`, `battery` |
|
|
83
|
+
|
|
84
|
+
SimulatorのFault API `8080/tcp` は、シミュレーター管理用であり共通UDP protocolには含めません。
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## 通信モデル
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
GUI ⇄ UDP/TCP ⇄ Robot
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- GUI: 操作・可視化
|
|
97
|
+
- Robot: 実機制御・センサ管理
|
|
98
|
+
- Protocol: データ構造・コマンド定義
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## パケット構造
|
|
103
|
+
|
|
104
|
+
| フィールド | 型 | 説明 |
|
|
105
|
+
| ---------- | ------ | ------------ |
|
|
106
|
+
| header | uint8 | パケット種別 |
|
|
107
|
+
| length | uint16 | データ長 |
|
|
108
|
+
| payload | bytes | 本体データ |
|
|
109
|
+
| checksum | uint16 | 整合性検証 |
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## コマンド一覧
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
### Drive Commands
|
|
120
|
+
|
|
121
|
+
| コマンド | 説明 |
|
|
122
|
+
| -------- | -------- |
|
|
123
|
+
| MOVE | 移動制御 |
|
|
124
|
+
| STOP | 停止 |
|
|
125
|
+
| TURN | 旋回 |
|
|
126
|
+
| | |
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
### Camera Commands
|
|
131
|
+
|
|
132
|
+
| コマンド | 説明 |
|
|
133
|
+
| -------- | -------- |
|
|
134
|
+
| START | 映像開始 |
|
|
135
|
+
| STOP | 映像停止 |
|
|
136
|
+
| | |
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## バージョニング方針
|
|
146
|
+
|
|
147
|
+
プロトコルは後方互換性を重視して管理する。
|
|
148
|
+
|
|
149
|
+
- Major: 非互換変更
|
|
150
|
+
|
|
151
|
+
- Minor: 互換追加
|
|
152
|
+
|
|
153
|
+
- Patch: バグ修正
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## 開発ポリシー
|
|
159
|
+
|
|
160
|
+
- 実装と仕様は常に一致させる
|
|
161
|
+
- 仕様変更は必ず Issue / PR で管理
|
|
162
|
+
|
|
163
|
+
- README を最上位の仕様書とする
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
localty_protocol/__init__.py,sha256=WIdbuD-SUZRgBnjkUQsL-GxYUryN630Z0Uj-QxuMf90,736
|
|
2
|
+
localty_protocol/command.py,sha256=RlEIaWpUE1Jnx3NgIrHplp9sFyN7JU-Ec9LddEoDI_o,1713
|
|
3
|
+
localty_protocol/telemetry.py,sha256=c9q3LjJUN1fxDHDANio6uaw8Iizq1gQ-jOhmPFzSxx0,4664
|
|
4
|
+
localty_protocol/version.py,sha256=FMnBSibErrspy-YtiLHU9pQM7UR929eGwEJBSbyt3HA,26
|
|
5
|
+
localty_protocol/video.py,sha256=dPFTnK8Wfe9qXUnGROMp26wpHAiA1cdl-1uuAzA-UW8,3195
|
|
6
|
+
localty_system_protocol-0.1.0.dist-info/licenses/LICENSE,sha256=Rv97knUyBkeGPp24B6hys-7brLIswb7-tY7OHy5rZOQ,1064
|
|
7
|
+
localty_system_protocol-0.1.0.dist-info/METADATA,sha256=tHV-fAUwMxQDM8LwNyRFv21sYx0kY_cmBvL6bC-HF3w,4065
|
|
8
|
+
localty_system_protocol-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
localty_system_protocol-0.1.0.dist-info/top_level.txt,sha256=VRVaYUnp64_MLkMQ2NeRpzJpBOFX3mqpGvlwEOxa7dY,17
|
|
10
|
+
localty_system_protocol-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 inabako
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
localty_protocol
|