Skip to content

Commit 575b73c

Browse files
authored
Merge pull request #17 from FRC-7525/state-visualization-CI
[CI] State Machine Visualization
2 parents 56a8244 + a91b025 commit 575b73c

File tree

3 files changed

+171
-3
lines changed

3 files changed

+171
-3
lines changed

.github/scripts/visualizeStates.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import sys
2+
import re
3+
import networkx as nx
4+
from pathlib import Path
5+
import matplotlib.pyplot as plt
6+
7+
# States that do not need to have a trigger to idle
8+
EXCLUDED_STATES = []
9+
10+
def get_states(manager_states_path):
11+
file = Path(manager_states_path).read_text(encoding='utf8')
12+
states = re.findall(r'\s*(?:\w+)\s*\((?:[\s\S]*?)\)[,;]', file, re.MULTILINE | re.IGNORECASE)
13+
return states
14+
15+
def parse_states(states):
16+
parsed_states = []
17+
for state in states:
18+
state_name_match = re.match(r'^\s*(\w+)\s*\(', state, re.IGNORECASE)
19+
sub_states = re.findall(r'\b(?:\w+\.)+\w+\b', state)
20+
parsed_states.append({
21+
'stateName': state_name_match[1] if state_name_match else None,
22+
'subStates': sub_states
23+
})
24+
return parsed_states
25+
26+
def get_triggers(manager_path):
27+
file = Path(manager_path).read_text(encoding='utf8')
28+
triggers = re.findall(r'addTrigger\(.+?\)', file, re.MULTILINE | re.IGNORECASE)
29+
return triggers
30+
31+
def parse_triggers(triggers):
32+
parsed_triggers = []
33+
for trigger in triggers:
34+
matched = re.search(r'\s*(\w+)\s*,\s*(\w+)\s*,\s*((?:[\w:]+)|(?:\(\)\s*->\s*[\w.]+\(\)))\s*\)', trigger)
35+
if matched:
36+
parsed_triggers.append({
37+
'from': matched[1],
38+
'to': matched[2],
39+
'condition': matched[3]
40+
})
41+
return parsed_triggers
42+
43+
def create_state_map(states, triggers):
44+
state_map = {state['stateName']: state for state in states}
45+
46+
for trigger in triggers:
47+
state = state_map.get(trigger['from'])
48+
if state is not None:
49+
if 'connectionsTo' not in state:
50+
state['connectionsTo'] = []
51+
state['connectionsTo'].append({
52+
'to': trigger['to'],
53+
'condition': trigger['condition']
54+
})
55+
return state_map
56+
57+
def generate_graph(state_map):
58+
graph = nx.DiGraph()
59+
for (state_name, state_info) in state_map.items():
60+
graph.add_node(state_name, label=state_name)
61+
for connection in state_info.get('connectionsTo', []):
62+
graph.add_edge(state_name, connection['to'], weight=1)
63+
64+
65+
pos = nx.shell_layout(graph)
66+
pos["IDLE"] = (0, 0)
67+
68+
fixed_nodes = ["IDLE"]
69+
pos = nx.spring_layout(graph, pos=pos, fixed=fixed_nodes, k=0.2)
70+
71+
edge_labels = nx.get_edge_attributes(graph, 'label')
72+
73+
nx.draw(graph, pos, with_labels=True, node_color='lightblue', node_size=1000, font_size=6, arrows=True)
74+
nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels, font_size=6)
75+
76+
plt.title("State Machine Visualization")
77+
plt.tight_layout()
78+
plt.savefig("state_machine.png", format='png', dpi=300)
79+
80+
81+
def checkConnectedToIdle(state_map):
82+
notToIdle = []
83+
for state_name, state in state_map.items():
84+
if state_name in EXCLUDED_STATES: continue
85+
for connections in state["connectionsTo"]:
86+
if connections.to == "IDLE": continue
87+
else: notToIdle.append(state_name)
88+
return notToIdle
89+
90+
91+
def main(managerStatesPath, managerPath):
92+
states = get_states(managerStatesPath)
93+
states = parse_states(states)
94+
95+
triggers = get_triggers(managerPath)
96+
triggers = parse_triggers(triggers)
97+
98+
state_map = create_state_map(states, triggers)
99+
100+
generate_graph(state_map)
101+
notToIdle = checkConnectedToIdle(state_map)
102+
if notToIdle:
103+
print(f"ERROR: The following states are not connected to IDLE: {', '.join(notToIdle)}")
104+
return 1
105+
else:
106+
print("Success: All states are connected to IDLE.")
107+
return 0
108+
if __name__ == '__main__':
109+
main(sys.argv[1], sys.argv[2])
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Visualize States
2+
3+
on:
4+
pull_request:
5+
types: [opened]
6+
7+
jobs:
8+
generate-graph:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repo
12+
uses: actions/checkout@v3
13+
14+
- name: Set up Python
15+
uses: actions/setup-python@v4
16+
with:
17+
python-version: '3.x'
18+
19+
- name: Install dependencies
20+
run: |
21+
pip install networkx matplotlib
22+
23+
- name: Run graph generation and check IDLE connections
24+
id: run-script
25+
run: |
26+
python3 .github/scripts/visualizeStates.py ./src/main/java/frc/robot/Manager/ManagerStates.java ./src/main/java/frc/robot/Manager/Manager.java
27+
echo "exit_code=$?" >> $GITHUB_OUTPUT
28+
continue-on-error: true
29+
30+
- name: Upload graph as artifact
31+
uses: actions/upload-artifact@v4
32+
with:
33+
name: state-machine-graph
34+
path: state_machine.png
35+
36+
- name: Comment on PR
37+
uses: actions/github-script@v6
38+
with:
39+
script: |
40+
const prNumber = context.payload.pull_request.number;
41+
const exitCode = Number(process.env.EXIT_CODE);
42+
const commentBody = exitCode === 0
43+
? "✅ State machine graph generated and uploaded.\n\nAll states connect to IDLE."
44+
: "⚠️ State machine graph generated, but some states do not connect to IDLE. Check the workflow logs and the generated graph for details.";
45+
46+
github.rest.issues.createComment({
47+
issue_number: prNumber,
48+
owner: context.repo.owner,
49+
repo: context.repo.repo,
50+
body: commentBody
51+
})
52+
env:
53+
EXIT_CODE: ${{ steps.run-script.outputs.exit_code }}
54+
55+
- name: Fail if states not connected to IDLE
56+
if: steps.run-script.outputs.exit_code != '0'
57+
run: |
58+
echo "States not connected to IDLE."
59+
exit 1
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"fileName": "yagsl-2025.8.0.json",
2+
"fileName": "yagsl-2025.7.2.json",
33
"name": "YAGSL",
4-
"version": "2025.8.0",
4+
"version": "2025.7.2",
55
"frcYear": "2025",
66
"uuid": "1ccce5a4-acd2-4d18-bca3-4b8047188400",
77
"mavenUrls": [
@@ -12,7 +12,7 @@
1212
{
1313
"groupId": "swervelib",
1414
"artifactId": "YAGSL-java",
15-
"version": "2025.8.0"
15+
"version": "2025.7.2"
1616
}
1717
],
1818
"requires": [

0 commit comments

Comments
 (0)