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

via/alice-sdv-demo Private

A-SW 코드-배포 파이프라인 검증용 ALICE SDV 예제

https://git.agrithing.ai/via/alice-sdv-demo.git
13 commits
COMMIT DETAIL

feat: add ALICE SDV pipeline sample

A-SW Pipeline <[email protected]> · 2026. 8. 2. 오전 3:14:11
6feeeadc7e9ba80030d11323022686e958a7b9eb
9 changed files +177 −0
added .gitea/workflows/asw-pipeline.yml +32−0 View file
@@ -0,0 +1,32 @@
1 + name: A-SW ALICE pipeline
2 +
3 + on:
4 + push:
5 + branches: [main, "feature/**"]
6 + pull_request:
7 +
8 + jobs:
9 + contracts:
10 + runs-on: ubuntu-latest
11 + steps:
12 + - uses: actions/checkout@v4
13 + - name: Validate machine-readable contracts
14 + run: python -m json.tool contracts/telemetry.schema.json >/dev/null
15 +
16 + unit-test:
17 + runs-on: ubuntu-latest
18 + steps:
19 + - uses: actions/checkout@v4
20 + - name: Run ALICE state-machine tests
21 + run: python -m unittest discover -s tests -v
22 +
23 + package:
24 + runs-on: ubuntu-latest
25 + needs: [contracts, unit-test]
26 + steps:
27 + - uses: actions/checkout@v4
28 + - name: Build the teaching artifact
29 + run: |
30 + mkdir -p dist
31 + tar -czf dist/alice-sdv-reference.tar.gz edge contracts profiles routes README.md
32 + shasum -a 256 dist/alice-sdv-reference.tar.gz > dist/checksums.sha256
added .gitignore +3−0 View file
@@ -0,0 +1,3 @@
1 + __pycache__/
2 + *.pyc
3 + dist/
added README.md +25−0 View file
@@ -0,0 +1,25 @@
1 + # ALICE SDV reference module
2 +
3 + This repository is the first executable teaching fixture for the A-SW Hub
4 + code-to-deployment pipeline. It models a small, hardware-independent slice of
5 + the ALICE control stack so that a local runner can validate the same contracts
6 + before a Jetson or MCU is connected.
7 +
8 + ## Included vertical slice
9 +
10 + - explicit `IDLE -> ENTER -> PATROL -> RETURN -> IDLE` state transitions
11 + - `FAULT` entry and reset behavior
12 + - machine-readable MQTT telemetry contract
13 + - ALICE profile and sample route fixtures
14 + - unit tests with no external runtime dependencies
15 + - a Gitea Actions workflow ready for the local runner
16 +
17 + ## Local test
18 +
19 + ```bash
20 + python -m unittest discover -s tests -v
21 + python -m json.tool contracts/telemetry.schema.json >/dev/null
22 + ```
23 +
24 + The next increment adds the simulated MQTT broker, package manifest, artifact
25 + checksums, and a local deployment target.
added contracts/telemetry.schema.json +15−0 View file
@@ -0,0 +1,15 @@
1 + {
2 + "$schema": "https://json-schema.org/draft/2020-12/schema",
3 + "$id": "https://asw.local/contracts/alice/telemetry.schema.json",
4 + "title": "ALICE telemetry",
5 + "type": "object",
6 + "additionalProperties": false,
7 + "required": ["device_id", "state", "battery", "fault", "timestamp"],
8 + "properties": {
9 + "device_id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
10 + "state": { "enum": ["IDLE", "ENTER", "PATROL", "RETURN", "FAULT"] },
11 + "battery": { "type": "number", "minimum": 0, "maximum": 100 },
12 + "fault": { "type": ["string", "null"] },
13 + "timestamp": { "type": "string", "format": "date-time" }
14 + }
15 + }
added edge/__init__.py +1−0 View file
@@ -0,0 +1 @@
1 + """Hardware-independent ALICE edge-domain examples."""
added edge/state_machine.py +51−0 View file
@@ -0,0 +1,51 @@
1 + from __future__ import annotations
2 +
3 + from dataclasses import dataclass
4 + from enum import Enum
5 +
6 +
7 + class State(str, Enum):
8 + IDLE = "IDLE"
9 + ENTER = "ENTER"
10 + PATROL = "PATROL"
11 + RETURN = "RETURN"
12 + FAULT = "FAULT"
13 +
14 +
15 + class Event(str, Enum):
16 + START_PATROL = "START_PATROL"
17 + ENTERED_ROUTE = "ENTERED_ROUTE"
18 + PATROL_COMPLETED = "PATROL_COMPLETED"
19 + RETURNED_HOME = "RETURNED_HOME"
20 + ABORT = "ABORT"
21 + SAFETY_TIMEOUT = "SAFETY_TIMEOUT"
22 + RESET = "RESET"
23 +
24 +
25 + TRANSITIONS: dict[tuple[State, Event], State] = {
26 + (State.IDLE, Event.START_PATROL): State.ENTER,
27 + (State.ENTER, Event.ENTERED_ROUTE): State.PATROL,
28 + (State.ENTER, Event.ABORT): State.RETURN,
29 + (State.PATROL, Event.PATROL_COMPLETED): State.RETURN,
30 + (State.PATROL, Event.ABORT): State.RETURN,
31 + (State.RETURN, Event.RETURNED_HOME): State.IDLE,
32 + (State.ENTER, Event.SAFETY_TIMEOUT): State.FAULT,
33 + (State.PATROL, Event.SAFETY_TIMEOUT): State.FAULT,
34 + (State.RETURN, Event.SAFETY_TIMEOUT): State.FAULT,
35 + (State.FAULT, Event.RESET): State.IDLE,
36 + }
37 +
38 +
39 + class InvalidTransition(ValueError):
40 + """Raised when an event is not allowed from the current state."""
41 +
42 +
43 + @dataclass(frozen=True)
44 + class PatrolStateMachine:
45 + state: State = State.IDLE
46 +
47 + def dispatch(self, event: Event) -> "PatrolStateMachine":
48 + next_state = TRANSITIONS.get((self.state, event))
49 + if next_state is None:
50 + raise InvalidTransition(f"{event} is not allowed from {self.state}")
51 + return PatrolStateMachine(next_state)
added profiles/alice.yaml +10−0 View file
@@ -0,0 +1,10 @@
1 + device_id: alice-01
2 + control:
3 + tick_seconds: 0.2
4 + watchdog_seconds: 0.5
5 + command_stream_hz: 50
6 + safety:
7 + corridor_stop_m: 0.30
8 + corridor_resume_m: 0.45
9 + fault_after_seconds: 5
10 + route: office-patrol-01
added routes/office-patrol-01.yaml +8−0 View file
@@ -0,0 +1,8 @@
1 + id: office-patrol-01
2 + frame: map
3 + arrival_radius_m: 0.12
4 + waypoints:
5 + - { x: 0.0, y: 0.0, heading: 0.0 }
6 + - { x: 2.0, y: 0.0, heading: 0.0 }
7 + - { x: 2.0, y: 1.5, heading: 1.57 }
8 + - { x: 0.0, y: 1.5, heading: 3.14 }
added tests/test_state_machine.py +32−0 View file
@@ -0,0 +1,32 @@
1 + import unittest
2 +
3 + from edge.state_machine import Event, InvalidTransition, PatrolStateMachine, State
4 +
5 +
6 + class PatrolStateMachineTest(unittest.TestCase):
7 + def test_nominal_patrol_returns_to_idle(self) -> None:
8 + machine = PatrolStateMachine()
9 + for event in (
10 + Event.START_PATROL,
11 + Event.ENTERED_ROUTE,
12 + Event.PATROL_COMPLETED,
13 + Event.RETURNED_HOME,
14 + ):
15 + machine = machine.dispatch(event)
16 + self.assertEqual(machine.state, State.IDLE)
17 +
18 + def test_abort_during_enter_returns_home(self) -> None:
19 + machine = PatrolStateMachine().dispatch(Event.START_PATROL)
20 + self.assertEqual(machine.dispatch(Event.ABORT).state, State.RETURN)
21 +
22 + def test_safety_timeout_enters_fault(self) -> None:
23 + machine = PatrolStateMachine().dispatch(Event.START_PATROL).dispatch(Event.ENTERED_ROUTE)
24 + self.assertEqual(machine.dispatch(Event.SAFETY_TIMEOUT).state, State.FAULT)
25 +
26 + def test_invalid_transition_is_rejected(self) -> None:
27 + with self.assertRaises(InvalidTransition):
28 + PatrolStateMachine().dispatch(Event.RETURNED_HOME)
29 +
30 +
31 + if __name__ == "__main__":
32 + unittest.main()