Install
npx skillscat add deluale/framework-parser/code-generation-device-analysis-skill Install via the SkillsCat registry.
SKILL.md
Device Analysis Skill v1.0.2
Purpose
Analyze SCL Function Blocks to extract:
- Device patterns (command/status, state machine, alarms)
- UDT contracts (input/output/inout)
- Anti-patterns (portability violations)
- Configuration templates
Input Requirements
FB_PATH: Path to .scl Function Block fileUDT_PATH: Path to UDT folder (.xml files) - optionalOUTPUT_PATH: Output directory (default: current dir)
Output
device_pattern_{FB_NAME}.json- Machine-readable pattern (schema v1.0.2)DEVICE_{FB_NAME}.md- Human-readable documentation
Execution Instructions for Claude Code
When user provides an SCL file for analysis, execute these steps in order.
STEP 1: Setup and Read Files
import os
import re
import json
from datetime import datetime
# Get paths from user or use defaults
FB_PATH = "{{FB_PATH}}" # User must provide
UDT_PATH = "{{UDT_PATH}}" # Optional
OUTPUT_PATH = "{{OUTPUT_PATH}}" # Default: /home/claude/output
# Read FB content
with open(FB_PATH, 'r', encoding='utf-8') as f:
fb_content = f.read()
# Extract metadata
fb_name_match = re.search(r'FUNCTION_BLOCK\s+"([^"]+)"', fb_content)
FB_NAME = fb_name_match.group(1) if fb_name_match else os.path.basename(FB_PATH).replace('.scl', '')
version_match = re.search(r'VERSION\s*:\s*([\d.]+)', fb_content)
VERSION = version_match.group(1) if version_match else "1.0"
author_match = re.search(r'AUTHOR\s*:\s*([^\n;]+)', fb_content)
AUTHOR = author_match.group(1).strip() if author_match else "Unknown"
print(f"Analyzing: {FB_NAME} v{VERSION} by {AUTHOR}")STEP 2: Device Taxonomy Detection
def detect_device_taxonomy(content, fb_name):
"""Detect device_family and device_type from code patterns."""
device_family = "generic"
device_type = "actuator_generic"
# Motion family detection
if re.search(r'MC_MoveAbsolute|MC_Home|MC_MoveRelative|MC_Power', content):
device_family = "motion"
if re.search(r'Rot|Turn|Rotation', fb_name, re.I):
device_type = "rotary_servo"
else:
device_type = "linear_servo"
elif re.search(r'TAx_DriveInterface', content):
device_family = "motion"
device_type = "servo_drive"
elif re.search(r'PosFbk_ITF', content) and not re.search(r'MC_', content):
device_family = "motion"
device_type = "linear_onoff"
# Pneumatic family detection
elif re.search(r'Extend.*Retract|Retract.*Extend', content, re.I | re.S):
device_family = "pneumatic"
device_type = "cylinder_double"
elif re.search(r'Open.*Close.*Grip|Grip.*Open.*Close', content, re.I | re.S):
device_family = "pneumatic"
device_type = "gripper"
elif re.search(r'Open.*Close|Close.*Open', content, re.I | re.S):
device_family = "pneumatic"
device_type = "valve"
return device_family, device_type
DEVICE_FAMILY, DEVICE_TYPE = detect_device_taxonomy(fb_content, FB_NAME)
print(f"Device: {DEVICE_FAMILY}/{DEVICE_TYPE}")STEP 3: Portability Gate Analysis (CRITICAL)
def analyze_portability(content):
"""
Detect CRITICAL portability violations.
Returns: (status, violations_list, anti_patterns_list)
"""
anti_patterns = []
violations = []
ap_counter = 1
lines = content.split('\n')
for line_num, line in enumerate(lines, 1):
# Skip comments
if re.match(r'^\s*//', line):
continue
# PG-01: Physical I/O (%I, %Q, %M)
io_matches = re.finditer(r'%[IMQ][WDB]?[0-9]+(\.[0-9]+)?', line)
for match in io_matches:
# Skip if inside string literal
if "'" in line and line.index("'") < match.start():
continue
ap_id = f"AP{ap_counter:03d}"
anti_patterns.append({
"id": ap_id,
"type": "io_physical",
"severity": "CRITICAL",
"line": line_num,
"code": line.strip()[:80],
"rule": "portability_no_physical_io",
"impact": "Breaks portability - project-specific address",
"suggested_fix": "Pass via VAR_INPUT parameter"
})
violations.append(ap_id)
ap_counter += 1
# PG-02: AT mapping
if re.search(r'\bAT\s+%[IMQ]', line):
ap_id = f"AP{ap_counter:03d}"
anti_patterns.append({
"id": ap_id,
"type": "io_at_mapping",
"severity": "CRITICAL",
"line": line_num,
"code": line.strip()[:80],
"rule": "portability_no_at_mapping",
"impact": "AT mapping is project-specific",
"suggested_fix": "Remove AT, use symbolic access"
})
violations.append(ap_id)
ap_counter += 1
# PG-03: Hardcoded global DB
db_match = re.search(r'"DB_[A-Za-z0-9_]+"', line)
if db_match:
# Skip if inside string literal or comment
if "'" in line and line.index("'") < db_match.start():
continue
ap_id = f"AP{ap_counter:03d}"
anti_patterns.append({
"id": ap_id,
"type": "hardcoded_db",
"severity": "CRITICAL",
"line": line_num,
"code": line.strip()[:80],
"rule": "portability_no_global_db",
"impact": "Global DB is project-specific",
"suggested_fix": "Pass data via VAR_IN_OUT or UDT parameter"
})
violations.append(ap_id)
ap_counter += 1
# Non-critical: Manual edge detection (MEDIUM)
for line_num, line in enumerate(lines, 1):
if re.match(r'^\s*//', line):
continue
if re.search(r'\bAND\s+NOT\s+\w+_(old|prev|last)\b', line, re.I):
ap_id = f"AP{ap_counter:03d}"
anti_patterns.append({
"id": ap_id,
"type": "manual_edge_detection",
"severity": "MEDIUM",
"line": line_num,
"code": line.strip()[:80],
"rule": "use_native_triggers",
"impact": "Code verbosity, non-standard",
"suggested_fix": "Use R_TRIG or F_TRIG from standard library"
})
ap_counter += 1
# Non-critical: Magic numbers (LOW) - limit to 5 examples
magic_count = 0
for line_num, line in enumerate(lines, 1):
if magic_count >= 5:
break
if re.match(r'^\s*//', line):
continue
if re.search(r'VAR_CONSTANT|Config\.', line):
continue
magic_match = re.search(r'[^a-zA-Z_][0-9]+\.[0-9]+', line)
if magic_match:
ap_id = f"AP{ap_counter:03d}"
anti_patterns.append({
"id": ap_id,
"type": "magic_number",
"severity": "LOW",
"line": line_num,
"code": line.strip()[:80],
"rule": "use_named_constants",
"impact": "Hard to maintain, not configurable",
"suggested_fix": "Define in Config UDT or VAR_CONSTANT"
})
ap_counter += 1
magic_count += 1
status = "FAIL" if violations else "PASS"
return status, violations, anti_patterns
PORTABILITY_STATUS, PORTABILITY_VIOLATIONS, ANTI_PATTERNS = analyze_portability(fb_content)
print(f"Portability: {PORTABILITY_STATUS} ({len(PORTABILITY_VIOLATIONS)} violations)")STEP 4: Pattern Detection
def detect_patterns(content):
"""Detect command/status patterns, state machine, timers, edge detection."""
# Command/Status pattern
pattern_cmd = "unknown"
confidence = "LOW"
if re.search(r'\.Permitted\s*:=.*NOT.*AND', content) and re.search(r'\.Request\s*:=.*\.Permitted', content):
pattern_cmd = "Permitted→Request→Cmd→Done"
confidence = "HIGH"
elif re.search(r'Request.*Command|Command.*Request', content):
pattern_cmd = "Request→Cmd"
confidence = "MEDIUM"
# State machine type
state_type = "implicit"
if re.search(r'CASE\s+\w+\s+OF', content):
state_type = "explicit_case"
elif re.search(r'\w+\.Request.*\.Permitted', content):
state_type = "struct_pattern"
# Extract states from REGION
states = re.findall(r'REGION\s+(\w+)', content)
if not states:
states = ["Main"]
# Timers
timers = re.findall(r'(\w+)\s*:\s*(?:IEC_TIMER|TON|TOF|TP)', content)
# Edge detection
edge = "none"
if re.search(r'R_TRIG|F_TRIG', content):
edge = "R_TRIG"
elif re.search(r'\bAND\s+NOT\s+\w+_(old|prev)', content):
edge = "manual"
# Math functions
math_funcs = []
for func in ['ABS', 'MAX', 'MIN', 'LIMIT', 'SEL', 'SQRT', 'SIN', 'COS']:
if re.search(rf'\b{func}\s*\(', content):
math_funcs.append(func)
return {
"command_status": {"pattern": pattern_cmd, "confidence": confidence},
"state_machine": {"type": state_type, "states": states},
"timers": timers,
"edge_detection": edge,
"math": math_funcs
}
PATTERNS = detect_patterns(fb_content)
print(f"Pattern: {PATTERNS['command_status']['pattern']} ({PATTERNS['command_status']['confidence']})")STEP 5: UDT Classification (if UDT_PATH provided)
def classify_udts(udt_path):
"""Classify UDTs by naming convention."""
command_udts = []
config_udts = []
status_udts = []
if not udt_path or not os.path.exists(udt_path):
return command_udts, config_udts, status_udts
for filename in os.listdir(udt_path):
if not filename.endswith('.xml'):
continue
udt_name = filename.replace('.xml', '')
if re.search(r'_Ctrl$|_Command$', udt_name):
command_udts.append({"name": udt_name, "category": "command_udt"})
elif re.search(r'_Config$|_Par', udt_name):
config_udts.append({"name": udt_name, "category": "config_udt"})
elif re.search(r'_Sts$|_Status$', udt_name):
status_udts.append({"name": udt_name, "category": "status_udt"})
return command_udts, config_udts, status_udts
COMMAND_UDTS, CONFIG_UDTS, STATUS_UDTS = classify_udts(UDT_PATH if 'UDT_PATH' in dir() else None)STEP 6: Generate JSON Output
def generate_pattern_json(fb_name, version, author, device_family, device_type,
portability_status, portability_violations, anti_patterns,
patterns, command_udts, config_udts, status_udts):
"""Generate device_pattern JSON according to schema v1.0.2."""
return {
"schema_version": "1.0.2",
"metadata": {
"fb_name": fb_name,
"version": version,
"author": author,
"device_family": device_family,
"device_type": device_type,
"analyzed_at": datetime.now().isoformat(),
"source_files": {
"fb_scl": FB_PATH
},
"portability_gate": {
"status": portability_status,
"violations": portability_violations
}
},
"contract": {
"input": {
"command_udt": command_udts,
"config_udt": config_udts,
"logical_inputs": []
},
"output": {
"status_udt": status_udts,
"logical_outputs": []
},
"inout": {
"interfaces": []
}
},
"logic": {
"state_machine": {
"type": patterns["state_machine"]["type"],
"states": patterns["state_machine"]["states"]
},
"key_algorithms": []
},
"patterns": {
"command_status": patterns["command_status"],
"alarm_handling": {"pattern": "unknown", "confidence": "LOW"},
"native_functions": {
"timers": patterns["timers"],
"edge_detection": patterns["edge_detection"],
"motion_control": [],
"math": patterns["math"]
}
},
"anti_patterns": anti_patterns,
"dependencies": {
"fb_called": [],
"custom_udts": [],
"external_devices": []
},
"constraints": {
"portability_compliant": portability_status == "PASS",
"multi_instance_safe": portability_status == "PASS",
"notes": ""
},
"configuration": {
"example_config": {},
"validation_rules": []
}
}
# Generate JSON
pattern_data = generate_pattern_json(
FB_NAME, VERSION, AUTHOR, DEVICE_FAMILY, DEVICE_TYPE,
PORTABILITY_STATUS, PORTABILITY_VIOLATIONS, ANTI_PATTERNS,
PATTERNS, COMMAND_UDTS, CONFIG_UDTS, STATUS_UDTS
)
# Save JSON
os.makedirs(OUTPUT_PATH, exist_ok=True)
json_path = os.path.join(OUTPUT_PATH, f"device_pattern_{FB_NAME}.json")
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(pattern_data, f, indent=2, ensure_ascii=False)
print(f"✅ JSON saved: {json_path}")STEP 7: Generate Markdown Report
def generate_markdown_report(pattern_data):
"""Generate human-readable markdown report."""
md = pattern_data["metadata"]
gate = md["portability_gate"]
patterns = pattern_data["patterns"]
anti = pattern_data["anti_patterns"]
report = f"""# Device Analysis: {md['fb_name']}
**Version:** {md['version']}
**Family:** {md['device_family']}
**Type:** {md['device_type']}
**Analyzed:** {md['analyzed_at'][:10]}
---
## Portability Gate: {gate['status']}
"""
if gate['status'] == "FAIL":
report += "### ⌠CRITICAL Violations\n\n"
for v in gate['violations']:
report += f"- {v}\n"
report += "\n**This FB cannot be reused across projects without modifications.**\n"
else:
report += "### ✅ Portable\n\nNo physical I/O, AT mapping, or hardcoded DBs detected.\n"
report += f"""
---
## Pattern Recognition
**Command/Status:** {patterns['command_status']['pattern']} ({patterns['command_status']['confidence']})
**State Machine:** {pattern_data['logic']['state_machine']['type']}
**Edge Detection:** {patterns['native_functions']['edge_detection']}
---
## Anti-Patterns Detected
Total: {len(anti)}
"""
for ap in anti:
report += f"### {ap['id']} - {ap['type']} ({ap['severity']})\n"
report += f"Line {ap['line']}: `{ap['code']}`\n\n"
report += f"""---
## Next Steps
"""
if gate['status'] == "FAIL":
report += """1. Fix CRITICAL anti-patterns
2. Re-analyze to verify PASS
3. Use as template for code generation
"""
else:
report += """1. Review LOW/MEDIUM anti-patterns
2. Ready for code generation
"""
report += f"\n---\n\nSee: `device_pattern_{md['fb_name']}.json` for full details\n"
return report
# Generate MD
md_content = generate_markdown_report(pattern_data)
md_path = os.path.join(OUTPUT_PATH, f"DEVICE_{FB_NAME}.md")
with open(md_path, 'w', encoding='utf-8') as f:
f.write(md_content)
print(f"✅ Markdown saved: {md_path}")STEP 8: Final Report
print(f"""
========================================
DEVICE ANALYSIS v1.0.2 COMPLETE
========================================
FB Name: {FB_NAME}
Family: {DEVICE_FAMILY}
Type: {DEVICE_TYPE}
Pattern: {PATTERNS['command_status']['pattern']}
Confidence: {PATTERNS['command_status']['confidence']}
Portability: {PORTABILITY_STATUS}
Anti-Patterns: {len(ANTI_PATTERNS)} total
CRITICAL: {len(PORTABILITY_VIOLATIONS)}
Output:
JSON: {json_path}
MD: {md_path}
{"⚠️ FIX CRITICAL violations before using as template" if PORTABILITY_STATUS == "FAIL" else "✅ Ready for reuse across projects"}
========================================
""")Usage Examples
Example 1: Analyze Single FB
User: Analyze /path/to/OnOffAxis.scl
Claude: [Executes STEP 1-8 with FB_PATH="/path/to/OnOffAxis.scl"]Example 2: With UDT Path
User: Analyze FB at /code/MyDevice.scl with UDTs in /code/UDT/
Claude: [Sets FB_PATH="/code/MyDevice.scl", UDT_PATH="/code/UDT/", executes workflow]Example 3: Quick Check Only
User: Check portability of /code/LegacyFB.scl
Claude: [Executes only STEP 1-3, reports PASS/FAIL with violations]Schema Validation
After generating JSON, validate against schema:
python scripts/validate_pattern.py device_pattern_X.jsonNotes
- Portability Gate is CRITICAL: FAIL blocks code generation
- Anti-patterns are the single source of truth for violations
- Constraints are derived from anti-patterns (not independent)
- This skill uses Python (not bash) for reliable cross-platform execution