1
2"""
3server.py
4
5FastAPI server for managing Arduino projects with arduino-cli.
6Exposes granular endpoints for OpenAI GPT integration.
7
8@author Ripred
9@date 2023-12-10
10@version 1.1
11"""
12
13import os
14import subprocess
15import logging
16import platform
17from fastapi import FastAPI, HTTPException
18from pydantic import BaseModel
19from typing import Optional, Dict, List
20from pathlib import Path
21
22
23logging.basicConfig(
24 level=logging.INFO,
25 format="%(asctime)s - %(levelname)s - %(message)s",
26 handlers=[
27 logging.FileHandler("server.log"),
28 logging.StreamHandler()
29 ]
30)
31logger = logging.getLogger(__name__)
32
33app = FastAPI(
34 title="Arduino Project Manager",
35 description="API for managing Arduino projects with arduino-cli",
36 version="1.1.0"
37)
38
39
40OS_TYPE = platform.system()
41
42if OS_TYPE == "Darwin":
43 ARDUINO_DIR = Path.home() / "Documents" / "Arduino"
44elif OS_TYPE == "Windows":
45 ARDUINO_DIR = Path(os.environ["USERPROFILE"]) / "Documents" / "Arduino"
46elif OS_TYPE == "Linux":
47 ARDUINO_DIR = Path.home() / "Arduino"
48else:
49 raise RuntimeError(f"Unsupported operating system: {OS_TYPE}")
50
51
52ARDUINO_DIR.mkdir(parents=True, exist_ok=True)
53logger.info(f"Arduino projects directory set to: {ARDUINO_DIR}")
54
55
56class ProjectRequest(BaseModel):
57 project_name: str
58
59class SketchRequest(BaseModel):
60 project_name: str
61 sketch_content: str
62
63class UploadRequest(BaseModel):
64 project_name: str
65 port: str
66
67
68def run_command(command: List[str], cwd: Optional[Path] = None) -> Dict[str, str]:
69 try:
70 result = subprocess.run(
71 command,
72 cwd=cwd,
73 capture_output=True,
74 text=True,
75 check=True
76 )
77 return {"status": "success", "output": result.stdout, "error": ""}
78 except subprocess.CalledProcessError as e:
79 logger.error(f"Command failed: {command}, Error: {e.stderr}")
80 return {"status": "error", "output": "", "error": e.stderr}
81 except Exception as e:
82 logger.error(f"Unexpected error: {str(e)}")
83 return {"status": "error", "output": "", "error": str(e)}
84
85@app.post("/check_folder", summary="Check if project folder exists")
86async def check_folder(request: ProjectRequest):
87 """Check if the specified project folder exists."""
88 project_dir = ARDUINO_DIR / request.project_name
89 exists = project_dir.exists() and project_dir.is_dir()
90 logger.info(f"Checked folder {project_dir}: {'exists' if exists else 'does not exist'}")
91 return {"exists": exists}
92
93@app.post("/read_files", summary="Read all files in project folder")
94async def read_files(request: ProjectRequest):
95 """Read all files in the specified project folder."""
96 project_dir = ARDUINO_DIR / request.project_name
97 if not project_dir.exists() or not project_dir.is_dir():
98 logger.error(f"Project folder not found: {project_dir}")
99 raise HTTPException(status_code=404, detail="Project folder not found")
100
101 files_content = {}
102 for file_path in project_dir.glob("*"):
103 try:
104 with open(file_path, "r") as f:
105 files_content[file_path.name] = f.read()
106 except Exception as e:
107 logger.error(f"Failed to read file {file_path}: {str(e)}")
108 raise HTTPException(status_code=500, detail=f"Failed to read file {file_path.name}: {str(e)}")
109
110 logger.info(f"Read files from {project_dir}: {list(files_content.keys())}")
111 return {"files": files_content}
112
113@app.post("/create_project", summary="Create project folder and write sketch")
114async def create_project(request: SketchRequest):
115 """Create a project folder and write the sketch file if it doesn't exist."""
116 project_dir = ARDUINO_DIR / request.project_name
117 sketch_file = project_dir / f"{request.project_name}.ino"
118
119 if project_dir.exists() and sketch_file.exists():
120 logger.error(f"Project already exists: {project_dir}")
121 raise HTTPException(status_code=400, detail="Project already exists")
122
123 try:
124 project_dir.mkdir(parents=True, exist_ok=True)
125 with open(sketch_file, "w") as f:
126 f.write(request.sketch_content)
127 logger.info(f"Created project {project_dir} with sketch {sketch_file}")
128 return {"status": "success", "message": f"Created project {request.project_name}"}
129 except Exception as e:
130 logger.error(f"Failed to create project {project_dir}: {str(e)}")
131 raise HTTPException(status_code=500, detail=f"Failed to create project: {str(e)}")
132
133@app.post("/update_sketch", summary="Update sketch file contents")
134async def update_sketch(request: SketchRequest):
135 """Update the contents of the sketch file in the project folder."""
136 project_dir = ARDUINO_DIR / request.project_name
137 sketch_file = project_dir / f"{request.project_name}.ino"
138
139 if not project_dir.exists() or not sketch_file.exists():
140 logger.error(f"Project or sketch not found: {sketch_file}")
141 raise HTTPException(status_code=404, detail="Project or sketch file not found")
142
143 try:
144 with open(sketch_file, "w") as f:
145 f.write(request.sketch_content)
146 logger.info(f"Updated sketch {sketch_file}")
147 return {"status": "success", "message": f"Updated sketch {request.project_name}.ino"}
148 except Exception as e:
149 logger.error(f"Failed to update sketch {sketch_file}: {str(e)}")
150 raise HTTPException(status_code=500, detail=f"Failed to update sketch: {str(e)}")
151
152@app.post("/compile_project", summary="Compile project using arduino-cli")
153async def compile_project(request: ProjectRequest):
154 """Compile the specified project using arduino-cli."""
155 project_dir = ARDUINO_DIR / request.project_name
156 if not project_dir.exists() or not (project_dir / f"{request.project_name}.ino").exists():
157 logger.error(f"Project or sketch not found: {project_dir}")
158 raise HTTPException(status_code=404, detail="Project or sketch file not found")
159
160 command = ["arduino-cli", "compile", "--fqbn", "arduino:avr:nano:cpu=atmega328old", str(project_dir)]
161 result = run_command(command, cwd=ARDUINO_DIR)
162 logger.info(f"Compilation result for {project_dir}: {result['status']}")
163 if result["status"] == "error":
164 raise HTTPException(status_code=500, detail=f"Compilation failed: {result['error']}")
165 return result
166
167@app.post("/upload_project", summary="Upload compiled project to Arduino")
168async def upload_project(request: UploadRequest):
169 """Upload the compiled project to the specified Arduino port."""
170 project_dir = ARDUINO_DIR / request.project_name
171 if not project_dir.exists() or not (project_dir / f"{request.project_name}.ino").exists():
172 logger.error(f"Project or sketch not found: {project_dir}")
173 raise HTTPException(status_code=404, detail="Project or sketch file not found")
174
175 command = ["arduino-cli", "upload", "-p", request.port, "--fqbn", "arduino:avr:nano:cpu=atmega328old", str(project_dir)]
176 result = run_command(command, cwd=ARDUINO_DIR)
177 logger.info(f"Upload result for {project_dir} to {request.port}: {result['status']}")
178 if result["status"] == "error":
179 raise HTTPException(status_code=500, detail=f"Upload failed: {result['error']}")
180 return result