localty-system-protocol 0.1.0__tar.gz

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.
@@ -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,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,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,14 @@
1
+ LICENSE
2
+ pyproject.toml
3
+ readme.md
4
+ localty_protocol/__init__.py
5
+ localty_protocol/command.py
6
+ localty_protocol/telemetry.py
7
+ localty_protocol/version.py
8
+ localty_protocol/video.py
9
+ localty_system_protocol.egg-info/PKG-INFO
10
+ localty_system_protocol.egg-info/SOURCES.txt
11
+ localty_system_protocol.egg-info/dependency_links.txt
12
+ localty_system_protocol.egg-info/top_level.txt
13
+ tests/test_docs.py
14
+ tests/test_telemetry.py
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "localty-system-protocol"
7
+ version = "0.1.0"
8
+ description = "Shared protocol definitions for the Localty system"
9
+ readme = "readme.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [
14
+ { name = "Localty Project" },
15
+ ]
16
+ keywords = ["localty", "robotics", "protocol", "udp", "telemetry"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Libraries",
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/inabako/localty-system-protocol"
31
+ Issues = "https://github.com/inabako/localty-system-protocol/issues"
32
+
33
+ [tool.setuptools]
34
+ packages = ["localty_protocol"]
@@ -0,0 +1,141 @@
1
+ # localty-system-protocol
2
+
3
+ Localty システムにおけるロボット・GUI 間通信プロトコル仕様。
4
+
5
+
6
+
7
+ ## 概要
8
+
9
+ `localty-system-protocol` は、Localty ロボット制御システムにおける
10
+ GUI ↔ ロボット間の通信仕様を定義するプロトコルリポジトリです。
11
+
12
+ 本リポジトリは以下を目的とします:
13
+
14
+ - 通信仕様の単一ソース化
15
+ - 実装と仕様の乖離防止
16
+ - デバッグおよび拡張時の設計基準の提供
17
+
18
+
19
+
20
+ 本READMEは Localty 通信仕様の正規ドキュメントであり、実装は本仕様に従って構築される。
21
+
22
+
23
+
24
+ ## 構成
25
+
26
+ ```text
27
+ localty_protocol/
28
+ ├─ command.py
29
+ ├─ telemetry.py
30
+ ├─ video.py
31
+ └─ version.py
32
+
33
+ ```
34
+
35
+ `localty_protocol.telemetry` は、UDPポートとTelemetry JSONの共通定義を管理する。
36
+
37
+ - `UDP_PORTS`: control / announce / video / telemetry_fast / telemetry_slow の一覧
38
+ - `TELEMETRY_CHANNELS`: FAST / SLOW channelと対応message typeの一覧
39
+ - `*_payload()`: Robot / Simulator が送信する既存JSON形状の生成
40
+ - `encode_payload()` / `decode_payload()`: JSON over UDP の共通encode/decode
41
+
42
+ ## UDPポート
43
+
44
+ UDPポートの正規定義は `localty_protocol.telemetry.UDP_PORTS` です。
45
+ README、各repoのdocs、Docker設定はこの一覧に合わせます。
46
+
47
+ | name | direction | port |
48
+ | --- | --- | ---: |
49
+ | `control` | GUI -> robot/simulator | `5005/udp` |
50
+ | `announce` | robot/simulator -> GUI | `5006/udp` |
51
+ | `video` | robot/simulator -> GUI | `5000/udp` |
52
+ | `telemetry_fast` | robot/simulator -> GUI | `5010/udp` |
53
+ | `telemetry_slow` | robot/simulator -> GUI | `5011/udp` |
54
+
55
+ Telemetry channelの正規定義は `localty_protocol.telemetry.TELEMETRY_CHANNELS` です。
56
+
57
+ | channel | port | message types |
58
+ | --- | ---: | --- |
59
+ | `fast` | `5010/udp` | `heartbeat`, `distance`, `imu` |
60
+ | `slow` | `5011/udp` | `gps`, `battery` |
61
+
62
+ SimulatorのFault API `8080/tcp` は、シミュレーター管理用であり共通UDP protocolには含めません。
63
+
64
+
65
+
66
+ ## 通信モデル
67
+
68
+ ```
69
+ GUI ⇄ UDP/TCP ⇄ Robot
70
+
71
+
72
+ ```
73
+
74
+ - GUI: 操作・可視化
75
+ - Robot: 実機制御・センサ管理
76
+ - Protocol: データ構造・コマンド定義
77
+
78
+
79
+
80
+ ## パケット構造
81
+
82
+ | フィールド | 型 | 説明 |
83
+ | ---------- | ------ | ------------ |
84
+ | header | uint8 | パケット種別 |
85
+ | length | uint16 | データ長 |
86
+ | payload | bytes | 本体データ |
87
+ | checksum | uint16 | 整合性検証 |
88
+
89
+
90
+
91
+
92
+
93
+ ## コマンド一覧
94
+
95
+
96
+
97
+ ### Drive Commands
98
+
99
+ | コマンド | 説明 |
100
+ | -------- | -------- |
101
+ | MOVE | 移動制御 |
102
+ | STOP | 停止 |
103
+ | TURN | 旋回 |
104
+ | | |
105
+
106
+
107
+
108
+ ### Camera Commands
109
+
110
+ | コマンド | 説明 |
111
+ | -------- | -------- |
112
+ | START | 映像開始 |
113
+ | STOP | 映像停止 |
114
+ | | |
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+ ## バージョニング方針
124
+
125
+ プロトコルは後方互換性を重視して管理する。
126
+
127
+ - Major: 非互換変更
128
+
129
+ - Minor: 互換追加
130
+
131
+ - Patch: バグ修正
132
+
133
+
134
+
135
+
136
+ ## 開発ポリシー
137
+
138
+ - 実装と仕様は常に一致させる
139
+ - 仕様変更は必ず Issue / PR で管理
140
+
141
+ - README を最上位の仕様書とする
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from pathlib import Path
2
+
3
+ from localty_protocol.telemetry import TELEMETRY_CHANNELS, UDP_PORTS
4
+
5
+
6
+ def test_readme_lists_udp_ports() -> None:
7
+ readme = Path("readme.md").read_text(encoding="utf-8")
8
+ for item in UDP_PORTS:
9
+ assert f"`{item.name}`" in readme
10
+ assert f"`{item.port}/udp`" in readme
11
+
12
+
13
+ def test_readme_lists_telemetry_channels() -> None:
14
+ readme = Path("readme.md").read_text(encoding="utf-8")
15
+ for channel in TELEMETRY_CHANNELS:
16
+ assert f"`{channel.name}`" in readme
17
+ assert f"`{channel.port}/udp`" in readme
18
+ for message_type in channel.message_types:
19
+ assert f"`{message_type}`" in readme
@@ -0,0 +1,33 @@
1
+ from localty_protocol.telemetry import (
2
+ ANNOUNCE_PORT,
3
+ CONTROL_PORT,
4
+ TELEMETRY_CHANNELS,
5
+ TELEMETRY_FAST_PORT,
6
+ TELEMETRY_SLOW_PORT,
7
+ VIDEO_PORT,
8
+ decode_payload,
9
+ distance_payload,
10
+ encode_payload,
11
+ telemetry_port_for_type,
12
+ telemetry_ports,
13
+ udp_port,
14
+ )
15
+
16
+
17
+ def test_udp_ports_are_named_source_of_truth() -> None:
18
+ assert udp_port("control") == CONTROL_PORT == 5005
19
+ assert udp_port("announce") == ANNOUNCE_PORT == 5006
20
+ assert udp_port("video") == VIDEO_PORT == 5000
21
+
22
+
23
+ def test_telemetry_channels_are_list_based() -> None:
24
+ assert [channel.name for channel in TELEMETRY_CHANNELS] == ["fast", "slow"]
25
+ assert telemetry_ports() == [TELEMETRY_FAST_PORT, TELEMETRY_SLOW_PORT]
26
+ assert telemetry_port_for_type("distance") == TELEMETRY_FAST_PORT
27
+ assert telemetry_port_for_type("battery") == TELEMETRY_SLOW_PORT
28
+
29
+
30
+ def test_payload_helpers_keep_json_shape() -> None:
31
+ payload = distance_payload(12.5, 123.0)
32
+ assert payload == {"type": "distance", "cm": 12.5, "timestamp": 123.0}
33
+ assert decode_payload(encode_payload(payload)) == payload