A-SW Hub / Repositories
Local Gitea · READ ONLY
← 저장소 Repositories
REPOSITORY

daedongrobotics/greenhouse-lps-navigation Private

[참조 MVP] 온실 LPS 측위·주행 — 온실 로컬 측위값과 작업열 경로로 이동 명령을 생성하는 참조 MVP입니다.

https://git.agrithing.ai/daedongrobotics/greenhouse-lps-navigation.git
4 commits
COMMIT DETAIL

feat: 온실 LPS 측위·주행 구체 MVP 구현

기관 승인 및 자동 품질검사를 통과한 구체 MVP 0.2.0 병합 Co-authored-by: daedong-dev <[email protected]>

daedong-dev <[email protected]> · 2026. 8. 3. 오후 3:12:47
2749f614b690ea5ae33d44a113da199f658768f3
8 changed files +75 −9 parent 075262f
modified README.md +7−1 View file
@@ -11,6 +11,12 @@
11 11
12 12 온실 로컬 측위값과 작업열 경로로 이동 명령을 생성하는 참조 MVP입니다.
13 13
14 + ## MVP 동작 범위
15 +
16 + - LPS 품질 게이트
17 + - 목표 거리·방향 계산
18 + - 품질 저하 시 정지 명령
19 +
14 20 ## 실행
15 21
16 22 ```bash
@@ -18,4 +24,4 @@ python run.py examples/input.json
18 24 python -m unittest discover -s tests -v
19 25 ```
20 26
21 실제 기관 구현은 `contracts/component.interface.json`의 저장소 식별자와 제품 기준선 연결 규칙을 유지하면서 교체할 수 있습니다.
27 + 정상 입력은 `examples/input.json`, 차단·안전 경로는 `examples/failure-input.json`으로 재현합니다. 실제 기관 구현은 `contracts/component.interface.json`의 저장소 식별자와 제품 기준선 연결 규칙을 유지하면서 교체할 수 있습니다.
modified asw_component/component.py +16−3 View file
@@ -1,7 +1,20 @@
1 1 from __future__ import annotations
2 + from math import atan2, degrees, hypot
2 3
3 4
4 5 def execute(payload: dict) -> dict:
5 if not isinstance(payload, dict) or not payload:
6 raise ValueError("a non-empty object is required")
7 return {"component": "daedongrobotics/greenhouse-lps-navigation", "accepted": True, "status": "READY", "target": "lpu", "input_fields": sorted(payload.keys())}
6 + pose, goal = payload["pose"], payload["goal"]
7 + quality = float(payload.get("localizationQuality", 0))
8 + threshold = float(payload.get("minimumQuality", 0.75))
9 + dx, dy = float(goal["x"]) - float(pose["x"]), float(goal["y"]) - float(pose["y"])
10 + distance = hypot(dx, dy)
11 + desired = degrees(atan2(dy, dx)) if distance else float(pose.get("headingDeg", 0))
12 + heading_error = (desired - float(pose.get("headingDeg", 0)) + 180) % 360 - 180
13 + accepted = quality >= threshold
14 + arrived = accepted and distance <= 0.1
15 + speed = 0.0 if not accepted or arrived else min(float(payload.get("maxSpeedMps", 0.8)), max(0.15, distance * 0.4))
16 + status = "LOCALIZATION_DEGRADED" if not accepted else ("ARRIVED" if arrived else "NAVIGATING")
17 + return {"component": "daedongrobotics/greenhouse-lps-navigation", "accepted": accepted,
18 + "status": status, "target": "lpu", "distanceM": round(distance, 3),
19 + "headingErrorDeg": round(heading_error, 2), "speedMps": round(speed, 3),
20 + "command": "STOP" if speed == 0 else "DRIVE"}
modified contracts/component.interface.json +18−1 View file
@@ -5,7 +5,12 @@
5 5 "required": [
6 6 "component",
7 7 "accepted",
8 "status"
8 + "status",
9 + "target",
10 + "distanceM",
11 + "headingErrorDeg",
12 + "speedMps",
13 + "command"
9 14 ],
10 15 "properties": {
11 16 "component": {
@@ -19,6 +24,18 @@
19 24 },
20 25 "target": {
21 26 "const": "lpu"
27 + },
28 + "distanceM": {
29 + "type": "number"
30 + },
31 + "headingErrorDeg": {
32 + "type": "number"
33 + },
34 + "speedMps": {
35 + "type": "number"
36 + },
37 + "command": {
38 + "type": "string"
22 39 }
23 40 }
24 41 }
added examples/failure-input.json +14−0 View file
@@ -0,0 +1,14 @@
1 + {
2 + "pose": {
3 + "x": 3.2,
4 + "y": 1.1,
5 + "headingDeg": 0
6 + },
7 + "goal": {
8 + "x": 8,
9 + "y": 1.1
10 + },
11 + "localizationQuality": 0.41,
12 + "minimumQuality": 0.75,
13 + "maxSpeedMps": 0.8
14 + }
modified examples/input.json +5−2 View file
@@ -1,11 +1,14 @@
1 1 {
2 2 "pose": {
3 3 "x": 3.2,
4 "y": 1.1
4 + "y": 1.1,
5 + "headingDeg": 0
5 6 },
6 7 "goal": {
7 8 "x": 8,
8 9 "y": 1.1
9 10 },
10 "localizationQuality": 0.92
11 + "localizationQuality": 0.92,
12 + "minimumQuality": 0.75,
13 + "maxSpeedMps": 0.8
11 14 }
modified pyproject.toml +1−1 View file
@@ -1,5 +1,5 @@
1 1 [project]
2 2 name = "greenhouse-lps-navigation"
3 version = "0.1.0"
3 + version = "0.2.0"
4 4 description = "온실 로컬 측위값과 작업열 경로로 이동 명령을 생성하는 참조 MVP입니다."
5 5 requires-python = ">=3.11"
modified release/manifest.json +1−1 View file
@@ -4,7 +4,7 @@
4 4 "displayName": "온실 LPS 측위·주행",
5 5 "owner": "대동로보틱스",
6 6 "implementationAuthor": "주식회사 비아 (연동 검증용 참조 구현)",
7 "version": "0.1.0-mvp",
7 + "version": "0.2.0-mvp",
8 8 "domain": "Localization & Navigation",
9 9 "status": "REFERENCE_MVP",
10 10 "compatibility": {
modified tests/test_component.py +13−0 View file
@@ -12,6 +12,19 @@ class ComponentTest(unittest.TestCase):
12 12 result = execute(payload)
13 13 self.assertTrue(result["accepted"])
14 14 self.assertEqual(result["component"], "daedongrobotics/greenhouse-lps-navigation")
15 + self.assertEqual(result["target"], "lpu")
16 + self.assertEqual(result["status"], "NAVIGATING")
17 + self.assertEqual(result["distanceM"], 4.8)
18 + self.assertEqual(result["speedMps"], 0.8)
19 + self.assertEqual(result["command"], "DRIVE")
20 +
21 + def test_safety_or_failure_path(self):
22 + payload = json.loads((ROOT / "examples/failure-input.json").read_text(encoding="utf-8"))
23 + result = execute(payload)
24 + self.assertEqual(result["component"], "daedongrobotics/greenhouse-lps-navigation")
25 + self.assertFalse(result["accepted"])
26 + self.assertEqual(result["status"], "LOCALIZATION_DEGRADED")
27 + self.assertEqual(result["command"], "STOP")
15 28
16 29 if __name__ == "__main__":
17 30 unittest.main()