ESP32 antenna positioner MCP server: extracted from mcnanovna
5 MCP tools for positioner control (status, move, home, stop, config) and 3 guided workflow prompts (home_positioner, configure_positioner, measure_pattern_grid). The measure_pattern_grid prompt orchestrates cross-server 3D pattern measurement with mcnanovna's VNA scan tools. httpx HTTP client communicates with ESP32 firmware over WiFi. Firmware and KiCad hardware schematics moved from mcnanovna.
This commit is contained in:
commit
ec3f75e47b
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.pio/
|
||||
*.kicad_prl
|
||||
hardware/positioner-backups/
|
||||
2
firmware/.gitignore
vendored
Normal file
2
firmware/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.pio/
|
||||
.vscode/
|
||||
49
firmware/include/config.h
Normal file
49
firmware/include/config.h
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
// ── WiFi credentials (set via build flags or edit here) ──────────
|
||||
#ifndef WIFI_SSID
|
||||
#define WIFI_SSID "YOUR_SSID"
|
||||
#endif
|
||||
#ifndef WIFI_PASS
|
||||
#define WIFI_PASS "YOUR_PASS"
|
||||
#endif
|
||||
|
||||
// ── Pin assignments ──────────────────────────────────────────────
|
||||
#define THETA_STEP_PIN 25
|
||||
#define THETA_DIR_PIN 26
|
||||
#define THETA_EN_PIN 27
|
||||
|
||||
#define PHI_STEP_PIN 32
|
||||
#define PHI_DIR_PIN 33
|
||||
#define PHI_EN_PIN 14
|
||||
|
||||
// ── TMC2209 UART ─────────────────────────────────────────────────
|
||||
#define TMC_RX_PIN 16
|
||||
#define TMC_TX_PIN 17
|
||||
#define THETA_TMC_ADDR 0
|
||||
#define PHI_TMC_ADDR 1
|
||||
#define TMC_R_SENSE 0.11f // sense resistor value (ohms)
|
||||
#define TMC_RMS_CURRENT 800 // motor current in mA
|
||||
|
||||
// ── Motor constants ──────────────────────────────────────────────
|
||||
#define STEPS_PER_REV 200
|
||||
#define DEFAULT_MICROSTEPS 16 // 0.1125 deg per microstep
|
||||
// Steps per degree = STEPS_PER_REV * MICROSTEPS / 360.0
|
||||
// At 16 microsteps: 200 * 16 / 360 = 8.888...
|
||||
|
||||
// ── Motion defaults ──────────────────────────────────────────────
|
||||
#define DEFAULT_MAX_SPEED 2000.0f // steps/sec (~225 deg/sec)
|
||||
#define DEFAULT_ACCEL 1000.0f // steps/sec^2 (~112 deg/sec^2)
|
||||
#define SETTLE_MS 200 // ms after move before measurement
|
||||
|
||||
// ── Homing ───────────────────────────────────────────────────────
|
||||
#define HOME_SPEED 500.0f // steps/sec (slower for stall detect)
|
||||
#define STALL_THRESHOLD 50 // TMC2209 StallGuard threshold (tune per setup)
|
||||
#define HOME_BACKOFF_STEPS 100 // steps to back off after stall detection
|
||||
|
||||
// ── Safety ───────────────────────────────────────────────────────
|
||||
#define IDLE_DISABLE_MS 30000 // disable motors after 30s idle
|
||||
#define WATCHDOG_TIMEOUT_S 60 // WDT reset if no command while moving
|
||||
|
||||
// ── mDNS ─────────────────────────────────────────────────────────
|
||||
#define MDNS_HOSTNAME "positioner"
|
||||
13
firmware/platformio.ini
Normal file
13
firmware/platformio.ini
Normal file
@ -0,0 +1,13 @@
|
||||
[env:esp32]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
mathieucarbou/ESPAsyncWebServer @ ^3.6.0
|
||||
teemuatlut/TMCStepper @ ^0.7.3
|
||||
waspinator/AccelStepper @ ^1.64
|
||||
bblanchon/ArduinoJson @ ^7.3.0
|
||||
monitor_speed = 115200
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
-DBOARD_HAS_PSRAM=0
|
||||
507
firmware/src/main.cpp
Normal file
507
firmware/src/main.cpp
Normal file
@ -0,0 +1,507 @@
|
||||
/*
|
||||
* ESP32 Antenna Positioner — dual-axis stepper control with HTTP API.
|
||||
*
|
||||
* Hardware: 2x NEMA 17 + TMC2209 (UART) + ESP32 DevKit
|
||||
* Axes: theta (polar, 0-180 deg) and phi (azimuth, 0-360 deg)
|
||||
* Discovery: mDNS at positioner.local
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <AccelStepper.h>
|
||||
#include <TMCStepper.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
// ── Globals ──────────────────────────────────────────────────────
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
// TMC2209 drivers on shared HardwareSerial bus (different addresses)
|
||||
// ESP32 requires HardwareSerial& — pin assignment happens in Serial2.begin()
|
||||
static TMC2209Stepper tmc_theta(&Serial2, TMC_R_SENSE, THETA_TMC_ADDR);
|
||||
static TMC2209Stepper tmc_phi(&Serial2, TMC_R_SENSE, PHI_TMC_ADDR);
|
||||
|
||||
// AccelStepper instances (DRIVER mode = step + dir pins)
|
||||
static AccelStepper stepper_theta(AccelStepper::DRIVER, THETA_STEP_PIN, THETA_DIR_PIN);
|
||||
static AccelStepper stepper_phi(AccelStepper::DRIVER, PHI_STEP_PIN, PHI_DIR_PIN);
|
||||
|
||||
// State
|
||||
static volatile bool homed_theta = false;
|
||||
static volatile bool homed_phi = false;
|
||||
static volatile bool emergency_stop = false;
|
||||
static volatile bool stall_detected = false;
|
||||
static uint16_t microsteps = DEFAULT_MICROSTEPS;
|
||||
static unsigned long last_command_ms = 0;
|
||||
static unsigned long last_move_ms = 0;
|
||||
static bool motors_enabled = true;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
static float steps_per_deg() {
|
||||
return (float)STEPS_PER_REV * microsteps / 360.0f;
|
||||
}
|
||||
|
||||
static float current_theta_deg() {
|
||||
return stepper_theta.currentPosition() / steps_per_deg();
|
||||
}
|
||||
|
||||
static float current_phi_deg() {
|
||||
float deg = fmod(stepper_phi.currentPosition() / steps_per_deg(), 360.0f);
|
||||
if (deg < 0) deg += 360.0f;
|
||||
return deg;
|
||||
}
|
||||
|
||||
static bool is_moving() {
|
||||
return stepper_theta.isRunning() || stepper_phi.isRunning();
|
||||
}
|
||||
|
||||
static void enable_motors() {
|
||||
digitalWrite(THETA_EN_PIN, LOW); // TMC2209: LOW = enabled
|
||||
digitalWrite(PHI_EN_PIN, LOW);
|
||||
motors_enabled = true;
|
||||
}
|
||||
|
||||
static void disable_motors() {
|
||||
digitalWrite(THETA_EN_PIN, HIGH);
|
||||
digitalWrite(PHI_EN_PIN, HIGH);
|
||||
motors_enabled = false;
|
||||
}
|
||||
|
||||
static void stop_motors() {
|
||||
stepper_theta.stop();
|
||||
stepper_phi.stop();
|
||||
// Run deceleration to zero
|
||||
while (stepper_theta.isRunning() || stepper_phi.isRunning()) {
|
||||
stepper_theta.run();
|
||||
stepper_phi.run();
|
||||
}
|
||||
}
|
||||
|
||||
// ── JSON response helpers ────────────────────────────────────────
|
||||
|
||||
static String status_json() {
|
||||
JsonDocument doc;
|
||||
doc["theta_deg"] = round(current_theta_deg() * 100.0f) / 100.0f;
|
||||
doc["phi_deg"] = round(current_phi_deg() * 100.0f) / 100.0f;
|
||||
doc["moving"] = is_moving();
|
||||
doc["homed"] = homed_theta && homed_phi;
|
||||
doc["homed_theta"] = homed_theta;
|
||||
doc["homed_phi"] = homed_phi;
|
||||
doc["stall_detected"] = stall_detected;
|
||||
doc["motors_enabled"] = motors_enabled;
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static String config_json() {
|
||||
JsonDocument doc;
|
||||
doc["steps_per_deg_theta"] = steps_per_deg();
|
||||
doc["steps_per_deg_phi"] = steps_per_deg();
|
||||
doc["speed"] = stepper_theta.maxSpeed();
|
||||
doc["accel"] = stepper_theta.acceleration();
|
||||
doc["microstepping"] = microsteps;
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static String ok_json(const char* extra_key = nullptr, float extra_val = 0) {
|
||||
JsonDocument doc;
|
||||
doc["ok"] = true;
|
||||
doc["theta_deg"] = round(current_theta_deg() * 100.0f) / 100.0f;
|
||||
doc["phi_deg"] = round(current_phi_deg() * 100.0f) / 100.0f;
|
||||
if (extra_key) doc[extra_key] = extra_val;
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static String error_json(const char* message) {
|
||||
JsonDocument doc;
|
||||
doc["ok"] = false;
|
||||
doc["error"] = message;
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── TMC2209 init ─────────────────────────────────────────────────
|
||||
|
||||
static void init_tmc(TMC2209Stepper& tmc, const char* label) {
|
||||
tmc.begin();
|
||||
tmc.toff(4); // enable driver
|
||||
tmc.rms_current(TMC_RMS_CURRENT); // motor current
|
||||
tmc.microsteps(microsteps);
|
||||
tmc.en_spreadCycle(false); // StealthChop for quiet operation
|
||||
tmc.pwm_autoscale(true); // auto-tune PWM
|
||||
tmc.SGTHRS(STALL_THRESHOLD); // StallGuard threshold
|
||||
|
||||
// Verify communication
|
||||
uint8_t result = tmc.test_connection();
|
||||
if (result == 0) {
|
||||
Serial.printf("[TMC] %s: OK\n", label);
|
||||
} else {
|
||||
Serial.printf("[TMC] %s: FAILED (result=%d)\n", label, result);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Homing (StallGuard sensorless) ──────────────────────────────
|
||||
|
||||
static bool home_axis(AccelStepper& stepper, TMC2209Stepper& tmc, const char* label) {
|
||||
Serial.printf("[Home] %s: starting sensorless home...\n", label);
|
||||
|
||||
enable_motors();
|
||||
|
||||
// Switch to SpreadCycle for reliable stall detection
|
||||
tmc.en_spreadCycle(true);
|
||||
tmc.TCOOLTHRS(0xFFFFF); // enable StallGuard at all speeds
|
||||
tmc.SGTHRS(STALL_THRESHOLD);
|
||||
delay(100);
|
||||
|
||||
// Move in negative direction until stall
|
||||
float prev_speed = stepper.maxSpeed();
|
||||
stepper.setMaxSpeed(HOME_SPEED);
|
||||
stepper.move(-999999); // move far negative
|
||||
|
||||
stall_detected = false;
|
||||
unsigned long start = millis();
|
||||
while (!stall_detected && (millis() - start < 30000)) {
|
||||
stepper.run();
|
||||
|
||||
// Check StallGuard via DIAG pin or SG_RESULT register
|
||||
if (tmc.SG_RESULT() < 10) {
|
||||
stall_detected = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (emergency_stop) {
|
||||
stepper.stop();
|
||||
stepper.setMaxSpeed(prev_speed);
|
||||
tmc.en_spreadCycle(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
stepper.stop();
|
||||
while (stepper.isRunning()) stepper.run();
|
||||
|
||||
if (!stall_detected) {
|
||||
Serial.printf("[Home] %s: timeout — no stall detected\n", label);
|
||||
stepper.setMaxSpeed(prev_speed);
|
||||
tmc.en_spreadCycle(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Back off from the stall point
|
||||
stepper.move(HOME_BACKOFF_STEPS);
|
||||
while (stepper.isRunning()) stepper.run();
|
||||
|
||||
// Set this position as zero
|
||||
stepper.setCurrentPosition(0);
|
||||
stepper.setMaxSpeed(prev_speed);
|
||||
stall_detected = false;
|
||||
|
||||
// Return to StealthChop for quiet operation
|
||||
tmc.en_spreadCycle(false);
|
||||
|
||||
Serial.printf("[Home] %s: homed OK\n", label);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── HTTP handlers ────────────────────────────────────────────────
|
||||
|
||||
static void setup_routes() {
|
||||
|
||||
// GET /status
|
||||
server.on("/status", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
last_command_ms = millis();
|
||||
request->send(200, "application/json", status_json());
|
||||
});
|
||||
|
||||
// POST /move — absolute position
|
||||
server.on("/move", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL,
|
||||
[](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t, size_t) {
|
||||
last_command_ms = millis();
|
||||
emergency_stop = false;
|
||||
|
||||
JsonDocument doc;
|
||||
if (deserializeJson(doc, data, len)) {
|
||||
request->send(400, "application/json", error_json("Invalid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc["theta_deg"].is<float>() || !doc["phi_deg"].is<float>()) {
|
||||
request->send(400, "application/json", error_json("Missing theta_deg or phi_deg"));
|
||||
return;
|
||||
}
|
||||
|
||||
float theta = doc["theta_deg"].as<float>();
|
||||
float phi = doc["phi_deg"].as<float>();
|
||||
|
||||
// Clamp theta to 0-180
|
||||
if (theta < 0) theta = 0;
|
||||
if (theta > 180) theta = 180;
|
||||
|
||||
// Normalize phi to 0-360
|
||||
phi = fmod(phi, 360.0f);
|
||||
if (phi < 0) phi += 360.0f;
|
||||
|
||||
enable_motors();
|
||||
stepper_theta.moveTo((long)(theta * steps_per_deg()));
|
||||
stepper_phi.moveTo((long)(phi * steps_per_deg()));
|
||||
last_move_ms = millis();
|
||||
|
||||
request->send(200, "application/json", ok_json());
|
||||
});
|
||||
|
||||
// POST /move/relative — relative move
|
||||
server.on("/move/relative", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL,
|
||||
[](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t, size_t) {
|
||||
last_command_ms = millis();
|
||||
emergency_stop = false;
|
||||
|
||||
JsonDocument doc;
|
||||
if (deserializeJson(doc, data, len)) {
|
||||
request->send(400, "application/json", error_json("Invalid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
float d_theta = doc["d_theta"] | 0.0f;
|
||||
float d_phi = doc["d_phi"] | 0.0f;
|
||||
|
||||
float new_theta = current_theta_deg() + d_theta;
|
||||
if (new_theta < 0) new_theta = 0;
|
||||
if (new_theta > 180) new_theta = 180;
|
||||
|
||||
float new_phi = current_phi_deg() + d_phi;
|
||||
new_phi = fmod(new_phi, 360.0f);
|
||||
if (new_phi < 0) new_phi += 360.0f;
|
||||
|
||||
enable_motors();
|
||||
stepper_theta.moveTo((long)(new_theta * steps_per_deg()));
|
||||
stepper_phi.moveTo((long)(new_phi * steps_per_deg()));
|
||||
last_move_ms = millis();
|
||||
|
||||
request->send(200, "application/json", ok_json());
|
||||
});
|
||||
|
||||
// POST /home
|
||||
server.on("/home", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL,
|
||||
[](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t, size_t) {
|
||||
last_command_ms = millis();
|
||||
emergency_stop = false;
|
||||
|
||||
String axis = "both";
|
||||
if (len > 0) {
|
||||
JsonDocument doc;
|
||||
if (!deserializeJson(doc, data, len)) {
|
||||
axis = doc["axis"] | "both";
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
if (axis == "both" || axis == "theta") {
|
||||
homed_theta = home_axis(stepper_theta, tmc_theta, "theta");
|
||||
ok = ok && homed_theta;
|
||||
}
|
||||
if (axis == "both" || axis == "phi") {
|
||||
homed_phi = home_axis(stepper_phi, tmc_phi, "phi");
|
||||
ok = ok && homed_phi;
|
||||
}
|
||||
|
||||
JsonDocument resp;
|
||||
resp["ok"] = ok;
|
||||
resp["homed"] = homed_theta && homed_phi;
|
||||
resp["homed_theta"] = homed_theta;
|
||||
resp["homed_phi"] = homed_phi;
|
||||
String out;
|
||||
serializeJson(resp, out);
|
||||
request->send(ok ? 200 : 500, "application/json", out);
|
||||
});
|
||||
|
||||
// POST /stop — emergency stop
|
||||
server.on("/stop", HTTP_POST, [](AsyncWebServerRequest* request) {
|
||||
last_command_ms = millis();
|
||||
emergency_stop = true;
|
||||
stop_motors();
|
||||
|
||||
JsonDocument doc;
|
||||
doc["ok"] = true;
|
||||
doc["stopped"] = true;
|
||||
doc["theta_deg"] = round(current_theta_deg() * 100.0f) / 100.0f;
|
||||
doc["phi_deg"] = round(current_phi_deg() * 100.0f) / 100.0f;
|
||||
String out;
|
||||
serializeJson(doc, out);
|
||||
request->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
// GET /config
|
||||
server.on("/config", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
last_command_ms = millis();
|
||||
request->send(200, "application/json", config_json());
|
||||
});
|
||||
|
||||
// POST /config — update speed, accel, microstepping
|
||||
server.on("/config", HTTP_POST, [](AsyncWebServerRequest* request) {},
|
||||
NULL,
|
||||
[](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t, size_t) {
|
||||
last_command_ms = millis();
|
||||
|
||||
JsonDocument doc;
|
||||
if (deserializeJson(doc, data, len)) {
|
||||
request->send(400, "application/json", error_json("Invalid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc["speed"].is<float>()) {
|
||||
float speed = doc["speed"].as<float>();
|
||||
stepper_theta.setMaxSpeed(speed);
|
||||
stepper_phi.setMaxSpeed(speed);
|
||||
}
|
||||
if (doc["accel"].is<float>()) {
|
||||
float accel = doc["accel"].as<float>();
|
||||
stepper_theta.setAcceleration(accel);
|
||||
stepper_phi.setAcceleration(accel);
|
||||
}
|
||||
if (doc["microstepping"].is<unsigned>()) {
|
||||
uint16_t ms = doc["microstepping"].as<uint16_t>();
|
||||
if (ms == 1 || ms == 2 || ms == 4 || ms == 8 || ms == 16 ||
|
||||
ms == 32 || ms == 64 || ms == 128 || ms == 256) {
|
||||
// Recalculate positions for new microstepping
|
||||
float theta_deg_now = current_theta_deg();
|
||||
float phi_deg_now = current_phi_deg();
|
||||
|
||||
microsteps = ms;
|
||||
tmc_theta.microsteps(microsteps);
|
||||
tmc_phi.microsteps(microsteps);
|
||||
|
||||
// Restore positions in new step scale
|
||||
stepper_theta.setCurrentPosition((long)(theta_deg_now * steps_per_deg()));
|
||||
stepper_phi.setCurrentPosition((long)(phi_deg_now * steps_per_deg()));
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument resp;
|
||||
resp["ok"] = true;
|
||||
JsonObject cfg = resp["config"].to<JsonObject>();
|
||||
cfg["speed"] = stepper_theta.maxSpeed();
|
||||
cfg["accel"] = stepper_theta.acceleration();
|
||||
cfg["microstepping"] = microsteps;
|
||||
cfg["steps_per_deg"] = steps_per_deg();
|
||||
String out;
|
||||
serializeJson(resp, out);
|
||||
request->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
// CORS preflight
|
||||
server.on("/*", HTTP_OPTIONS, [](AsyncWebServerRequest* request) {
|
||||
AsyncWebServerResponse* response = request->beginResponse(204);
|
||||
response->addHeader("Access-Control-Allow-Origin", "*");
|
||||
response->addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
response->addHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
// Add CORS headers to all responses
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
}
|
||||
|
||||
// ── Setup ────────────────────────────────────────────────────────
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("\n[Positioner] Starting...");
|
||||
|
||||
// Motor enable pins
|
||||
pinMode(THETA_EN_PIN, OUTPUT);
|
||||
pinMode(PHI_EN_PIN, OUTPUT);
|
||||
disable_motors(); // start disabled
|
||||
|
||||
// TMC2209 UART
|
||||
Serial2.begin(115200, SERIAL_8N1, TMC_RX_PIN, TMC_TX_PIN);
|
||||
delay(100);
|
||||
init_tmc(tmc_theta, "theta");
|
||||
init_tmc(tmc_phi, "phi");
|
||||
|
||||
// AccelStepper config
|
||||
stepper_theta.setMaxSpeed(DEFAULT_MAX_SPEED);
|
||||
stepper_theta.setAcceleration(DEFAULT_ACCEL);
|
||||
stepper_phi.setMaxSpeed(DEFAULT_MAX_SPEED);
|
||||
stepper_phi.setAcceleration(DEFAULT_ACCEL);
|
||||
|
||||
// WiFi
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
|
||||
unsigned long wifi_start = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - wifi_start < 15000)) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("\n[WiFi] Connected: %s\n", WiFi.localIP().toString().c_str());
|
||||
} else {
|
||||
Serial.println("\n[WiFi] FAILED — running without network");
|
||||
}
|
||||
|
||||
// mDNS
|
||||
if (MDNS.begin(MDNS_HOSTNAME)) {
|
||||
MDNS.addService("http", "tcp", 80);
|
||||
Serial.printf("[mDNS] %s.local\n", MDNS_HOSTNAME);
|
||||
}
|
||||
|
||||
// HTTP server
|
||||
setup_routes();
|
||||
server.begin();
|
||||
Serial.println("[HTTP] Server started on port 80");
|
||||
|
||||
// Watchdog (resets if main loop hangs)
|
||||
const esp_task_wdt_config_t wdt_cfg = {
|
||||
.timeout_ms = WATCHDOG_TIMEOUT_S * 1000,
|
||||
.idle_core_mask = 0,
|
||||
.trigger_panic = true,
|
||||
};
|
||||
esp_task_wdt_init(&wdt_cfg);
|
||||
esp_task_wdt_add(NULL);
|
||||
|
||||
last_command_ms = millis();
|
||||
last_move_ms = millis();
|
||||
|
||||
Serial.println("[Positioner] Ready");
|
||||
}
|
||||
|
||||
// ── Loop ─────────────────────────────────────────────────────────
|
||||
|
||||
void loop() {
|
||||
// Feed watchdog
|
||||
esp_task_wdt_reset();
|
||||
|
||||
// Run stepper motion
|
||||
if (!emergency_stop) {
|
||||
stepper_theta.run();
|
||||
stepper_phi.run();
|
||||
}
|
||||
|
||||
// Track when last move finished
|
||||
if (is_moving()) {
|
||||
last_move_ms = millis();
|
||||
}
|
||||
|
||||
// Idle motor disable — prevent overheating
|
||||
if (motors_enabled && !is_moving() &&
|
||||
(millis() - last_move_ms > IDLE_DISABLE_MS)) {
|
||||
disable_motors();
|
||||
Serial.println("[Motors] Disabled (idle timeout)");
|
||||
}
|
||||
}
|
||||
899
hardware/generate_kicad.py
Normal file
899
hardware/generate_kicad.py
Normal file
@ -0,0 +1,899 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate KiCad 9 project files for ESP32 + TMC2209 antenna positioner wiring.
|
||||
|
||||
Pin assignments sourced from firmware/include/config.h:
|
||||
Theta: STEP=25, DIR=26, EN=27 (TMC addr 0)
|
||||
Phi: STEP=32, DIR=33, EN=14 (TMC addr 1)
|
||||
UART: TX=17, RX=16
|
||||
|
||||
Run: python hardware/generate_kicad.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
|
||||
# Deterministic UUIDs seeded from names for reproducible output
|
||||
_UUID_NS = uuid.UUID("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
|
||||
|
||||
|
||||
def uid(name=""):
|
||||
if name:
|
||||
return str(uuid.uuid5(_UUID_NS, name))
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
# ── S-expression formatting helpers ──────────────────────────────────
|
||||
|
||||
|
||||
def _pin(num, name, ptype, x, y, angle, length=2.54):
|
||||
"""KiCad symbol pin S-expression."""
|
||||
return (
|
||||
f" (pin {ptype} line\n"
|
||||
f" (at {x:.2f} {y:.2f} {angle})\n"
|
||||
f" (length {length:.2f})\n"
|
||||
f' (name "{name}" (effects (font (size 1.27 1.27))))\n'
|
||||
f' (number "{num}" (effects (font (size 1.27 1.27))))\n'
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
def _rect(x1, y1, x2, y2, width=0.254, fill="background"):
|
||||
return (
|
||||
f" (rectangle\n"
|
||||
f" (start {x1:.2f} {y1:.2f})\n"
|
||||
f" (end {x2:.2f} {y2:.2f})\n"
|
||||
f" (stroke (width {width}) (type default))\n"
|
||||
f" (fill (type {fill}))\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
def _polyline(pts, width=0.254, fill="none"):
|
||||
pts_str = " ".join(f"(xy {x:.2f} {y:.2f})" for x, y in pts)
|
||||
return (
|
||||
f" (polyline\n"
|
||||
f" (pts {pts_str})\n"
|
||||
f" (stroke (width {width}) (type default))\n"
|
||||
f" (fill (type {fill}))\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
def _arc(start, mid, end, width=0.254):
|
||||
return (
|
||||
f" (arc\n"
|
||||
f" (start {start[0]:.2f} {start[1]:.2f})\n"
|
||||
f" (mid {mid[0]:.2f} {mid[1]:.2f})\n"
|
||||
f" (end {end[0]:.2f} {end[1]:.2f})\n"
|
||||
f" (stroke (width {width}) (type default))\n"
|
||||
f" (fill (type none))\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
def _text(txt, x, y, size=1.27):
|
||||
return (
|
||||
f' (text "{txt}"\n'
|
||||
f" (at {x:.2f} {y:.2f} 0)\n"
|
||||
f" (effects (font (size {size} {size})))\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
# ── Symbol builders ──────────────────────────────────────────────────
|
||||
|
||||
# Each returns the full (symbol "Name" ...) S-expression string.
|
||||
# prefix="" for .kicad_sym, prefix="positioner:" for embedded lib_symbols.
|
||||
|
||||
|
||||
def _build_dip_symbol(name, ref_prefix, description, left_pins, right_pins,
|
||||
body_w=10.16, pin_len=2.54, extra_graphics="", prefix=""):
|
||||
"""Generic DIP-style symbol with pins on left and right."""
|
||||
n_left = len(left_pins)
|
||||
n_right = len(right_pins)
|
||||
n_max = max(n_left, n_right)
|
||||
body_h = (n_max - 1) * 2.54 + 5.08 # padding top+bottom
|
||||
|
||||
hw = body_w / 2
|
||||
hh = body_h / 2
|
||||
|
||||
# Pin Y positions (symbol coords: Y up)
|
||||
def pin_ys(count):
|
||||
top = (count - 1) * 2.54 / 2
|
||||
return [top - i * 2.54 for i in range(count)]
|
||||
|
||||
left_ys = pin_ys(n_left)
|
||||
right_ys = pin_ys(n_right)
|
||||
|
||||
pins_sexp = []
|
||||
for i, (num, pname, ptype) in enumerate(left_pins):
|
||||
pins_sexp.append(_pin(num, pname, ptype, -(hw + pin_len), left_ys[i], 0, pin_len))
|
||||
for i, (num, pname, ptype) in enumerate(right_pins):
|
||||
pins_sexp.append(_pin(num, pname, ptype, (hw + pin_len), right_ys[i], 180, pin_len))
|
||||
|
||||
full_name = f"{prefix}{name}"
|
||||
# Sub-symbol names use just the symbol name, never the library prefix
|
||||
return (
|
||||
f' (symbol "{full_name}"\n'
|
||||
f" (exclude_from_sim no)\n"
|
||||
f" (in_bom yes)\n"
|
||||
f" (on_board yes)\n"
|
||||
f' (property "Reference" "{ref_prefix}" (at 0 {hh + 2.54:.2f} 0)\n'
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Value" "{name}" (at 0 {-(hh + 2.54):.2f} 0)\n'
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Footprint" "" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (property "Datasheet" "" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (property "Description" "{description}" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (symbol "{name}_0_1"\n'
|
||||
f"{_rect(-hw, hh, hw, -hh)}\n"
|
||||
f"{extra_graphics}"
|
||||
f" )\n"
|
||||
f' (symbol "{name}_1_1"\n'
|
||||
f"{chr(10).join(pins_sexp)}\n"
|
||||
f" )\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
def sym_esp32(prefix=""):
|
||||
"""ESP32 DevKit V1 38-pin module symbol.
|
||||
|
||||
Left side matches physical left header (top to bottom).
|
||||
Right side matches physical right header (top to bottom).
|
||||
Used pins have function annotations.
|
||||
"""
|
||||
left = [
|
||||
("1", "3V3", "power_out"),
|
||||
("2", "EN", "input"),
|
||||
("3", "VP/IO36", "passive"),
|
||||
("4", "VN/IO39", "passive"),
|
||||
("5", "IO34", "passive"),
|
||||
("6", "IO35", "passive"),
|
||||
("7", "IO32/\u03c6_STEP", "output"),
|
||||
("8", "IO33/\u03c6_DIR", "output"),
|
||||
("9", "IO25/\u03b8_STEP", "output"),
|
||||
("10", "IO26/\u03b8_DIR", "output"),
|
||||
("11", "IO27/\u03b8_EN", "output"),
|
||||
("12", "IO14/\u03c6_EN", "output"),
|
||||
("13", "IO12", "passive"),
|
||||
("14", "GND", "power_in"),
|
||||
("15", "IO13", "passive"),
|
||||
("16", "SD2", "passive"),
|
||||
("17", "SD3", "passive"),
|
||||
("18", "CMD", "passive"),
|
||||
("19", "5V", "power_in"),
|
||||
]
|
||||
right = [
|
||||
("38", "GND", "power_in"),
|
||||
("37", "IO23", "passive"),
|
||||
("36", "IO22", "passive"),
|
||||
("35", "TX0/IO1", "passive"),
|
||||
("34", "RX0/IO3", "passive"),
|
||||
("33", "IO21", "passive"),
|
||||
("32", "GND", "power_in"),
|
||||
("31", "IO19", "passive"),
|
||||
("30", "IO18", "passive"),
|
||||
("29", "IO5", "passive"),
|
||||
("28", "IO17/TMC_TX", "output"),
|
||||
("27", "IO16/TMC_RX", "input"),
|
||||
("26", "IO4", "passive"),
|
||||
("25", "IO0", "passive"),
|
||||
("24", "IO2", "passive"),
|
||||
("23", "IO15", "passive"),
|
||||
("22", "SD1", "passive"),
|
||||
("21", "SD0", "passive"),
|
||||
("20", "CLK", "passive"),
|
||||
]
|
||||
return _build_dip_symbol(
|
||||
"ESP32_DevKit_38pin", "U",
|
||||
"ESP32 DevKit V1 38-pin module",
|
||||
left, right, body_w=30.0, pin_len=3.81, prefix=prefix,
|
||||
)
|
||||
|
||||
|
||||
def sym_tmc2209(prefix=""):
|
||||
"""TMC2209 SilentStepStick breakout board symbol."""
|
||||
left = [
|
||||
("1", "VM", "power_in"),
|
||||
("2", "GND", "power_in"),
|
||||
("3", "2B", "output"),
|
||||
("4", "2A", "output"),
|
||||
("5", "1A", "output"),
|
||||
("6", "1B", "output"),
|
||||
("7", "VIO", "power_in"),
|
||||
("8", "GND", "power_in"),
|
||||
]
|
||||
right = [
|
||||
("9", "EN", "input"),
|
||||
("10", "MS1", "input"),
|
||||
("11", "MS2", "input"),
|
||||
("12", "PDN_UART", "bidirectional"),
|
||||
("13", "STEP", "input"),
|
||||
("14", "DIR", "input"),
|
||||
("15", "DIAG", "output"),
|
||||
("16", "CLK", "input"),
|
||||
]
|
||||
return _build_dip_symbol(
|
||||
"TMC2209_SilentStepStick", "U",
|
||||
"TMC2209 stepper driver breakout board",
|
||||
left, right, body_w=18.0, prefix=prefix,
|
||||
)
|
||||
|
||||
|
||||
def sym_nema17(prefix=""):
|
||||
"""NEMA 17 stepper motor — 4 pins with coil graphic."""
|
||||
left = [
|
||||
("1", "A1", "passive"),
|
||||
("2", "A2", "passive"),
|
||||
("3", "B1", "passive"),
|
||||
("4", "B2", "passive"),
|
||||
]
|
||||
# Coil graphics inside body: two zigzag coils
|
||||
coil_a = _polyline([(-2, 3.0), (-2, 1.5), (-1, 1.2), (-3, 0.6), (-1, 0.0), (-2, -0.3)], width=0.254)
|
||||
coil_b = _polyline([(2, 3.0), (2, 1.5), (1, 1.2), (3, 0.6), (1, 0.0), (2, -0.3)], width=0.254)
|
||||
label = _text("M", 0, -3.5, 2.0)
|
||||
gfx = f"{coil_a}\n{coil_b}\n{label}\n"
|
||||
return _build_dip_symbol(
|
||||
"NEMA17_Motor", "J",
|
||||
"NEMA 17 stepper motor connector",
|
||||
left, [], body_w=12.0, pin_len=2.54, extra_graphics=gfx, prefix=prefix,
|
||||
)
|
||||
|
||||
|
||||
def sym_barrel_jack(prefix=""):
|
||||
"""DC barrel jack — 3 pins."""
|
||||
left = [
|
||||
("1", "+12V", "passive"),
|
||||
("2", "GND", "passive"),
|
||||
("3", "Shield", "passive"),
|
||||
]
|
||||
return _build_dip_symbol(
|
||||
"Barrel_Jack_DC", "J",
|
||||
"DC barrel jack power input",
|
||||
left, [], body_w=10.0, pin_len=2.54, prefix=prefix,
|
||||
)
|
||||
|
||||
|
||||
def sym_resistor(prefix=""):
|
||||
"""Simple 2-pin resistor (vertical, pins top/bottom)."""
|
||||
name = "R"
|
||||
full_name = f"{prefix}{name}"
|
||||
return (
|
||||
f' (symbol "{full_name}"\n'
|
||||
f" (exclude_from_sim no)\n"
|
||||
f" (in_bom yes)\n"
|
||||
f" (on_board yes)\n"
|
||||
f' (property "Reference" "R" (at 2.54 0 90)\n'
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Value" "R" (at -2.54 0 90)\n'
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Footprint" "" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (property "Datasheet" "" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (symbol "{name}_0_1"\n'
|
||||
f"{_rect(-1.016, 3.81, 1.016, -3.81, width=0.254)}\n"
|
||||
f" )\n"
|
||||
f' (symbol "{name}_1_1"\n'
|
||||
f"{_pin('1', '~', 'passive', 0, 6.35, 270, 2.54)}\n"
|
||||
f"{_pin('2', '~', 'passive', 0, -6.35, 90, 2.54)}\n"
|
||||
f" )\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
def sym_cap_pol(prefix=""):
|
||||
"""Polarized capacitor (vertical, pin 1 = +)."""
|
||||
name = "C_Polarized"
|
||||
full_name = f"{prefix}{name}"
|
||||
# Two horizontal lines for plates, + marker
|
||||
plate_top = _polyline([(-2.0, 1.0), (2.0, 1.0)], width=0.508)
|
||||
plate_bot = _polyline([(-2.0, -1.0), (2.0, -1.0)], width=0.508)
|
||||
plus_h = _polyline([(-1.0, 2.5), (1.0, 2.5)], width=0.254)
|
||||
plus_v = _polyline([(0.0, 1.5), (0.0, 3.5)], width=0.254)
|
||||
# Curved bottom plate
|
||||
arc_gfx = _arc((-2.0, -1.0), (0.0, -2.0), (2.0, -1.0), width=0.508)
|
||||
return (
|
||||
f' (symbol "{full_name}"\n'
|
||||
f" (exclude_from_sim no)\n"
|
||||
f" (in_bom yes)\n"
|
||||
f" (on_board yes)\n"
|
||||
f' (property "Reference" "C" (at 2.54 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Value" "C_Polarized" (at -2.54 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Footprint" "" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (property "Datasheet" "" (at 0 0 0)\n'
|
||||
f" (effects (font (size 1.27 1.27)) hide))\n"
|
||||
f' (symbol "{name}_0_1"\n'
|
||||
f"{plate_top}\n"
|
||||
f"{arc_gfx}\n"
|
||||
f"{plus_h}\n"
|
||||
f"{plus_v}\n"
|
||||
f" )\n"
|
||||
f' (symbol "{name}_1_1"\n'
|
||||
f"{_pin('1', '+', 'passive', 0, 3.81, 270, 2.54)}\n"
|
||||
f"{_pin('2', '-', 'passive', 0, -3.81, 90, 2.54)}\n"
|
||||
f" )\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
|
||||
ALL_SYMBOLS = [sym_esp32, sym_tmc2209, sym_nema17, sym_barrel_jack, sym_resistor, sym_cap_pol]
|
||||
|
||||
|
||||
# ── Symbol library (.kicad_sym) ──────────────────────────────────────
|
||||
|
||||
|
||||
def gen_sym_lib():
|
||||
syms = "\n".join(fn(prefix="") for fn in ALL_SYMBOLS)
|
||||
return (
|
||||
"(kicad_symbol_lib\n"
|
||||
" (version 20231120)\n"
|
||||
' (generator "mcnanovna_gen")\n'
|
||||
' (generator_version "1.0")\n'
|
||||
f"{syms}\n"
|
||||
")\n"
|
||||
)
|
||||
|
||||
|
||||
# ── Schematic (.kicad_sch) ───────────────────────────────────────────
|
||||
|
||||
|
||||
def gen_schematic():
|
||||
root_uuid = uid("root")
|
||||
|
||||
# Embedded lib_symbols (with "positioner:" prefix)
|
||||
lib_syms = "\n".join(fn(prefix="positioner:") for fn in ALL_SYMBOLS)
|
||||
|
||||
# ── Component placement ──
|
||||
# Schematic coords: X right, Y down. All on 2.54mm grid.
|
||||
#
|
||||
# Layout (left to right):
|
||||
# J1(barrel) C1(cap) ... U1(ESP32) ... R1 ... U2(TMC θ) J2(motor θ)
|
||||
# ... U3(TMC φ) J3(motor φ)
|
||||
|
||||
components = [] # (symbol ...) blocks
|
||||
wires = [] # (wire ...) blocks
|
||||
labels = [] # (label ...) blocks
|
||||
no_connects = [] # (no_connect ...) blocks
|
||||
texts = [] # (text ...) blocks
|
||||
sym_instances = [] # for symbol_instances section
|
||||
|
||||
def add_symbol(lib_name, ref, value, x, y, angle, pin_numbers, extra_props=""):
|
||||
"""Add a schematic symbol instance."""
|
||||
inst_uuid = uid(f"inst-{ref}")
|
||||
pin_lines = "\n".join(
|
||||
f' (pin "{p}" (uuid "{uid(f"pin-{ref}-{p}")}"))'
|
||||
for p in pin_numbers
|
||||
)
|
||||
prop_ref_x = x
|
||||
# Place reference above, value below (adjust for symbol size)
|
||||
components.append(
|
||||
f" (symbol\n"
|
||||
f' (lib_id "positioner:{lib_name}")\n'
|
||||
f" (at {x:.2f} {y:.2f} {angle})\n"
|
||||
f" (unit 1)\n"
|
||||
f" (exclude_from_sim no)\n"
|
||||
f" (in_bom yes)\n"
|
||||
f" (on_board yes)\n"
|
||||
f" (dnp no)\n"
|
||||
f' (uuid "{inst_uuid}")\n'
|
||||
f' (property "Reference" "{ref}"\n'
|
||||
f" (at {prop_ref_x:.2f} {y - 3:.2f} 0)\n"
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f' (property "Value" "{value}"\n'
|
||||
f" (at {prop_ref_x:.2f} {y + 3:.2f} 0)\n"
|
||||
f" (effects (font (size 1.27 1.27))))\n"
|
||||
f"{extra_props}"
|
||||
f"{pin_lines}\n"
|
||||
f" )"
|
||||
)
|
||||
sym_instances.append(
|
||||
f' (path "/{inst_uuid}"\n'
|
||||
f' (reference "{ref}")\n'
|
||||
f" (unit 1)\n"
|
||||
f' (value "{value}")\n'
|
||||
f' (footprint ""))'
|
||||
)
|
||||
return inst_uuid
|
||||
|
||||
def add_wire(x1, y1, x2, y2):
|
||||
wires.append(
|
||||
f" (wire\n"
|
||||
f" (pts (xy {x1:.2f} {y1:.2f}) (xy {x2:.2f} {y2:.2f}))\n"
|
||||
f" (stroke (width 0) (type default))\n"
|
||||
f' (uuid "{uid()}")\n'
|
||||
f" )"
|
||||
)
|
||||
|
||||
def add_label(name, x, y, angle=0):
|
||||
labels.append(
|
||||
f' (label "{name}"\n'
|
||||
f" (at {x:.2f} {y:.2f} {angle})\n"
|
||||
f" (effects (font (size 1.27 1.27)) (justify left))\n"
|
||||
f' (uuid "{uid()}")\n'
|
||||
f" )"
|
||||
)
|
||||
|
||||
def add_no_connect(x, y):
|
||||
no_connects.append(
|
||||
f' (no_connect (at {x:.2f} {y:.2f}) (uuid "{uid()}"))'
|
||||
)
|
||||
|
||||
def add_text(txt, x, y, size=2.54):
|
||||
texts.append(
|
||||
f' (text "{txt}"\n'
|
||||
f" (at {x:.2f} {y:.2f} 0)\n"
|
||||
f" (effects (font (size {size} {size})))\n"
|
||||
f' (uuid "{uid()}")\n'
|
||||
f" )"
|
||||
)
|
||||
|
||||
junctions = [] # (junction ...) blocks
|
||||
|
||||
def add_junction(x, y):
|
||||
junctions.append(
|
||||
f' (junction (at {x:.2f} {y:.2f}) (diameter 0) (color 0 0 0 0)\n'
|
||||
f' (uuid "{uid()}")\n'
|
||||
f" )"
|
||||
)
|
||||
|
||||
# ── Pin position calculators ──
|
||||
# For DIP symbols placed at (cx, cy) with body_w and n pins per side:
|
||||
# Left pin i wire-connect at: (cx - body_w/2 - pin_len, cy - top_offset + i*2.54)
|
||||
# Right pin i wire-connect at: (cx + body_w/2 + pin_len, cy - top_offset + i*2.54)
|
||||
# NOTE: In schematic coords, Y is inverted from symbol coords.
|
||||
# Symbol pin at local (x, y) → schematic (cx + x, cy - y)
|
||||
|
||||
def dip_pin_pos(cx, cy, side, index, n_pins, body_w=10.16, pin_len=2.54):
|
||||
"""Get schematic (x, y) of pin wire-connect point."""
|
||||
# Symbol-local Y of pin i (0-indexed): top_y - i * 2.54
|
||||
# where top_y = (n_pins - 1) * 2.54 / 2
|
||||
top_y = (n_pins - 1) * 2.54 / 2.0
|
||||
local_y = top_y - index * 2.54
|
||||
# Transform to schematic coords (Y inverted)
|
||||
if side == "left":
|
||||
sx = cx - (body_w / 2.0 + pin_len)
|
||||
sy = cy - local_y
|
||||
else: # right
|
||||
sx = cx + (body_w / 2.0 + pin_len)
|
||||
sy = cy - local_y
|
||||
return sx, sy
|
||||
|
||||
# ── Place ESP32 (U1) ──
|
||||
U1_X, U1_Y = 101.6, 104.14
|
||||
esp32_left_pins = [str(i) for i in range(1, 20)]
|
||||
esp32_right_pins = [str(i) for i in range(38, 19, -1)]
|
||||
add_symbol("ESP32_DevKit_38pin", "U1", "ESP32 DevKit",
|
||||
U1_X, U1_Y, 0,
|
||||
esp32_left_pins + esp32_right_pins)
|
||||
|
||||
# ESP32 pin positions helper
|
||||
def esp_pin(side, index):
|
||||
return dip_pin_pos(U1_X, U1_Y, side, index, 19, body_w=30.0, pin_len=3.81)
|
||||
|
||||
# ── Place TMC2209 Theta (U2) ──
|
||||
U2_X, U2_Y = 190.5, 68.58
|
||||
tmc_pins = [str(i) for i in range(1, 17)]
|
||||
add_symbol("TMC2209_SilentStepStick", "U2", "TMC2209 \u03b8",
|
||||
U2_X, U2_Y, 0, tmc_pins)
|
||||
|
||||
def tmc_theta_pin(side, index):
|
||||
return dip_pin_pos(U2_X, U2_Y, side, index, 8, body_w=24.0)
|
||||
|
||||
# ── Place TMC2209 Phi (U3) ──
|
||||
U3_X, U3_Y = 190.5, 139.7
|
||||
add_symbol("TMC2209_SilentStepStick", "U3", "TMC2209 \u03c6",
|
||||
U3_X, U3_Y, 0, tmc_pins)
|
||||
|
||||
def tmc_phi_pin(side, index):
|
||||
return dip_pin_pos(U3_X, U3_Y, side, index, 8, body_w=24.0)
|
||||
|
||||
# ── Place NEMA17 Theta Motor (J2) ──
|
||||
J2_X, J2_Y = 243.84, 68.58
|
||||
motor_pins = ["1", "2", "3", "4"]
|
||||
add_symbol("NEMA17_Motor", "J2", "Motor \u03b8",
|
||||
J2_X, J2_Y, 0, motor_pins)
|
||||
|
||||
def motor_theta_pin(index):
|
||||
return dip_pin_pos(J2_X, J2_Y, "left", index, 4, body_w=18.0)
|
||||
|
||||
# ── Place NEMA17 Phi Motor (J3) ──
|
||||
J3_X, J3_Y = 243.84, 139.7
|
||||
add_symbol("NEMA17_Motor", "J3", "Motor \u03c6",
|
||||
J3_X, J3_Y, 0, motor_pins)
|
||||
|
||||
def motor_phi_pin(index):
|
||||
return dip_pin_pos(J3_X, J3_Y, "left", index, 4, body_w=18.0)
|
||||
|
||||
# ── Place Barrel Jack (J1) ──
|
||||
J1_X, J1_Y = 30.48, 104.14
|
||||
add_symbol("Barrel_Jack_DC", "J1", "12V DC",
|
||||
J1_X, J1_Y, 0, ["1", "2", "3"])
|
||||
|
||||
def jack_pin(index):
|
||||
return dip_pin_pos(J1_X, J1_Y, "left", index, 3, body_w=14.0)
|
||||
|
||||
# ── Place Resistor R1 (1k UART) — horizontal ──
|
||||
# Placed between ESP32 TX and TMC UART bus
|
||||
# Rotated 90 degrees (angle=90): pin 1 on left, pin 2 on right
|
||||
R1_X, R1_Y = 154.94, 109.22
|
||||
add_symbol("R", "R1", "1k\u03a9",
|
||||
R1_X, R1_Y, 90, ["1", "2"])
|
||||
# When rotated 90: pin 1 at (x, y-6.35) → (x-6.35, y) in schematic? No...
|
||||
# With angle=90: symbol rotated CCW 90 deg
|
||||
# Pin 1 (at 0, 6.35, 270 in symbol) → after 90 CCW rotation → (6.35, 0, 0)
|
||||
# So pin 1 wire-connect at (R1_X + 6.35, R1_Y) and pin 2 at (R1_X - 6.35, R1_Y)
|
||||
# Wait, the rotation transforms (x,y) → (-y, x) for CCW 90
|
||||
# Pin 1 symbol pos: (0, 6.35) → (-6.35, 0) → schematic (R1_X - 6.35, R1_Y - 0)
|
||||
# Pin 2 symbol pos: (0, -6.35) → (6.35, 0) → schematic (R1_X + 6.35, R1_Y)
|
||||
R1_PIN1 = (R1_X - 6.35, R1_Y) # left end
|
||||
R1_PIN2 = (R1_X + 6.35, R1_Y) # right end
|
||||
|
||||
# ── Place Capacitor C1 (100uF) — vertical ──
|
||||
C1_X, C1_Y = 48.26, 104.14
|
||||
add_symbol("C_Polarized", "C1", "100\u00b5F",
|
||||
C1_X, C1_Y, 0, ["1", "2"])
|
||||
# Pin 1 (+) at top: (C1_X, C1_Y - 3.81)
|
||||
# Pin 2 (-) at bottom: (C1_X, C1_Y + 3.81)
|
||||
C1_PIN1 = (C1_X, C1_Y - 3.81)
|
||||
C1_PIN2 = (C1_X, C1_Y + 3.81)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# WIRING — using net labels for all connections
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
STUB = 5.08 # wire stub length for net labels
|
||||
|
||||
# --- Power: +12V rail ---
|
||||
# Barrel jack pin 1 (+12V) → stub left → label "+12V"
|
||||
jx, jy = jack_pin(0)
|
||||
add_wire(jx, jy, jx - STUB, jy)
|
||||
add_label("+12V", jx - STUB, jy, 180)
|
||||
|
||||
# C1 pin 1 (+) → label "+12V"
|
||||
add_wire(C1_PIN1[0], C1_PIN1[1], C1_PIN1[0], C1_PIN1[1] - STUB)
|
||||
add_label("+12V", C1_PIN1[0], C1_PIN1[1] - STUB, 90)
|
||||
|
||||
# TMC theta VM (left pin 0) → label "+12V"
|
||||
tx, ty = tmc_theta_pin("left", 0)
|
||||
add_wire(tx, ty, tx - STUB, ty)
|
||||
add_label("+12V", tx - STUB, ty, 180)
|
||||
|
||||
# TMC phi VM (left pin 0) → label "+12V"
|
||||
px, py = tmc_phi_pin("left", 0)
|
||||
add_wire(px, py, px - STUB, py)
|
||||
add_label("+12V", px - STUB, py, 180)
|
||||
|
||||
# --- Power: GND rail ---
|
||||
# Barrel jack pin 2 (GND)
|
||||
jx, jy = jack_pin(1)
|
||||
add_wire(jx, jy, jx - STUB, jy)
|
||||
add_label("GND", jx - STUB, jy, 180)
|
||||
|
||||
# C1 pin 2 (-) → GND
|
||||
add_wire(C1_PIN2[0], C1_PIN2[1], C1_PIN2[0], C1_PIN2[1] + STUB)
|
||||
add_label("GND", C1_PIN2[0], C1_PIN2[1] + STUB, 270)
|
||||
|
||||
# ESP32 pin 14 (GND, left index 13) and pin 38 (GND, right index 0)
|
||||
ex, ey = esp_pin("left", 13)
|
||||
add_wire(ex, ey, ex - STUB, ey)
|
||||
add_label("GND", ex - STUB, ey, 180)
|
||||
|
||||
ex, ey = esp_pin("right", 0)
|
||||
add_wire(ex, ey, ex + STUB, ey)
|
||||
add_label("GND", ex + STUB, ey, 0)
|
||||
|
||||
# TMC theta GND (left pins 1 and 7)
|
||||
for idx in [1, 7]:
|
||||
tx, ty = tmc_theta_pin("left", idx)
|
||||
add_wire(tx, ty, tx - STUB, ty)
|
||||
add_label("GND", tx - STUB, ty, 180)
|
||||
|
||||
# TMC phi GND
|
||||
for idx in [1, 7]:
|
||||
px, py = tmc_phi_pin("left", idx)
|
||||
add_wire(px, py, px - STUB, py)
|
||||
add_label("GND", px - STUB, py, 180)
|
||||
|
||||
# --- Power: +5V rail (ESP32 5V → VIO on both TMC2209s) ---
|
||||
# ESP32 pin 19 (5V, left index 18)
|
||||
ex, ey = esp_pin("left", 18)
|
||||
add_wire(ex, ey, ex - STUB, ey)
|
||||
add_label("+5V", ex - STUB, ey, 180)
|
||||
|
||||
# TMC theta VIO (left pin 6)
|
||||
tx, ty = tmc_theta_pin("left", 6)
|
||||
add_wire(tx, ty, tx - STUB, ty)
|
||||
add_label("+5V", tx - STUB, ty, 180)
|
||||
|
||||
# TMC phi VIO (left pin 6)
|
||||
px, py = tmc_phi_pin("left", 6)
|
||||
add_wire(px, py, px - STUB, py)
|
||||
add_label("+5V", px - STUB, py, 180)
|
||||
|
||||
# --- Theta axis: ESP32 → TMC2209 θ ---
|
||||
# Stagger ESP32 left-side labels: alternating short/long stubs to prevent overlap
|
||||
# Labels are ~15mm wide, so we need >15mm separation between columns
|
||||
STUB_SHORT = STUB
|
||||
STUB_LONG = STUB * 5 # ~25mm stub creates clear two-column layout
|
||||
|
||||
# GPIO25 (θ_STEP) — ESP32 left pin index 8
|
||||
ex, ey = esp_pin("left", 8)
|
||||
add_wire(ex, ey, ex - STUB_LONG, ey)
|
||||
add_label("\u03b8_STEP", ex - STUB_LONG, ey, 180)
|
||||
|
||||
# TMC theta STEP (right pin index 4)
|
||||
tx, ty = tmc_theta_pin("right", 4)
|
||||
add_wire(tx, ty, tx + STUB, ty)
|
||||
add_label("\u03b8_STEP", tx + STUB, ty, 0)
|
||||
|
||||
# GPIO26 (θ_DIR) — ESP32 left pin index 9
|
||||
ex, ey = esp_pin("left", 9)
|
||||
add_wire(ex, ey, ex - STUB_SHORT, ey)
|
||||
add_label("\u03b8_DIR", ex - STUB_SHORT, ey, 180)
|
||||
|
||||
tx, ty = tmc_theta_pin("right", 5)
|
||||
add_wire(tx, ty, tx + STUB, ty)
|
||||
add_label("\u03b8_DIR", tx + STUB, ty, 0)
|
||||
|
||||
# GPIO27 (θ_EN) — ESP32 left pin index 10
|
||||
ex, ey = esp_pin("left", 10)
|
||||
add_wire(ex, ey, ex - STUB_LONG, ey)
|
||||
add_label("\u03b8_EN", ex - STUB_LONG, ey, 180)
|
||||
|
||||
tx, ty = tmc_theta_pin("right", 0)
|
||||
add_wire(tx, ty, tx + STUB, ty)
|
||||
add_label("\u03b8_EN", tx + STUB, ty, 0)
|
||||
|
||||
# --- Phi axis: ESP32 → TMC2209 φ ---
|
||||
# GPIO32 (φ_STEP) — ESP32 left pin index 6
|
||||
ex, ey = esp_pin("left", 6)
|
||||
add_wire(ex, ey, ex - STUB_LONG, ey)
|
||||
add_label("\u03c6_STEP", ex - STUB_LONG, ey, 180)
|
||||
|
||||
px, py = tmc_phi_pin("right", 4)
|
||||
add_wire(px, py, px + STUB, py)
|
||||
add_label("\u03c6_STEP", px + STUB, py, 0)
|
||||
|
||||
# GPIO33 (φ_DIR) — ESP32 left pin index 7
|
||||
ex, ey = esp_pin("left", 7)
|
||||
add_wire(ex, ey, ex - STUB_SHORT, ey)
|
||||
add_label("\u03c6_DIR", ex - STUB_SHORT, ey, 180)
|
||||
|
||||
px, py = tmc_phi_pin("right", 5)
|
||||
add_wire(px, py, px + STUB, py)
|
||||
add_label("\u03c6_DIR", px + STUB, py, 0)
|
||||
|
||||
# GPIO14 (φ_EN) — ESP32 left pin index 11
|
||||
ex, ey = esp_pin("left", 11)
|
||||
add_wire(ex, ey, ex - STUB_SHORT, ey)
|
||||
add_label("\u03c6_EN", ex - STUB_SHORT, ey, 180)
|
||||
|
||||
px, py = tmc_phi_pin("right", 0)
|
||||
add_wire(px, py, px + STUB, py)
|
||||
add_label("\u03c6_EN", px + STUB, py, 0)
|
||||
|
||||
# --- UART bus (half-duplex) ---
|
||||
# GPIO17 (TMC_TX, ESP32 right pin index 10) → R1 pin 1
|
||||
# Route with L-shape: horizontal to R1's X, then vertical to R1's Y
|
||||
ex, ey = esp_pin("right", 10)
|
||||
corner_x = R1_PIN1[0]
|
||||
add_wire(ex, ey, corner_x, ey) # horizontal segment
|
||||
add_wire(corner_x, ey, corner_x, R1_PIN1[1]) # vertical segment
|
||||
add_junction(corner_x, ey) # junction at corner
|
||||
|
||||
# R1 pin 2 → net label "PDN_UART"
|
||||
add_wire(R1_PIN2[0], R1_PIN2[1], R1_PIN2[0] + STUB, R1_PIN2[1])
|
||||
add_label("PDN_UART", R1_PIN2[0] + STUB, R1_PIN2[1], 0)
|
||||
|
||||
# GPIO16 (TMC_RX, ESP32 right pin index 11) → "PDN_UART"
|
||||
ex, ey = esp_pin("right", 11)
|
||||
add_wire(ex, ey, ex + STUB, ey)
|
||||
add_label("PDN_UART", ex + STUB, ey, 0)
|
||||
|
||||
# TMC theta PDN_UART (right pin index 3)
|
||||
tx, ty = tmc_theta_pin("right", 3)
|
||||
add_wire(tx, ty, tx + STUB, ty)
|
||||
add_label("PDN_UART", tx + STUB, ty, 0)
|
||||
|
||||
# TMC phi PDN_UART (right pin index 3)
|
||||
px, py = tmc_phi_pin("right", 3)
|
||||
add_wire(px, py, px + STUB, py)
|
||||
add_label("PDN_UART", px + STUB, py, 0)
|
||||
|
||||
# --- TMC2209 address selection ---
|
||||
# Theta (addr 0): MS1=GND, MS2=GND
|
||||
tx, ty = tmc_theta_pin("right", 1) # MS1
|
||||
add_wire(tx, ty, tx + STUB, ty)
|
||||
add_label("GND", tx + STUB, ty, 0)
|
||||
|
||||
tx, ty = tmc_theta_pin("right", 2) # MS2
|
||||
add_wire(tx, ty, tx + STUB, ty)
|
||||
add_label("GND", tx + STUB, ty, 0)
|
||||
|
||||
# Phi (addr 1): MS1=VIO(+5V), MS2=GND
|
||||
px, py = tmc_phi_pin("right", 1) # MS1
|
||||
add_wire(px, py, px + STUB, py)
|
||||
add_label("+5V", px + STUB, py, 0)
|
||||
|
||||
px, py = tmc_phi_pin("right", 2) # MS2
|
||||
add_wire(px, py, px + STUB, py)
|
||||
add_label("GND", px + STUB, py, 0)
|
||||
|
||||
# --- Motor wiring: TMC2209 → NEMA17 ---
|
||||
# TMC outputs (left side: 2B=2, 2A=3, 1A=4, 1B=5) → Motor (A1, A2, B1, B2)
|
||||
# Standard mapping: 1A→A1, 1B→A2, 2A→B1, 2B→B2
|
||||
motor_nets_theta = ["M\u03b8_2B", "M\u03b8_2A", "M\u03b8_1A", "M\u03b8_1B"]
|
||||
motor_nets_phi = ["M\u03c6_2B", "M\u03c6_2A", "M\u03c6_1A", "M\u03c6_1B"]
|
||||
|
||||
# Theta motor: TMC left pins 2-5 → J2 pins 0-3
|
||||
for i, net in enumerate(motor_nets_theta):
|
||||
tx, ty = tmc_theta_pin("left", i + 2)
|
||||
add_wire(tx, ty, tx - STUB, ty)
|
||||
add_label(net, tx - STUB, ty, 180)
|
||||
|
||||
mx, my = motor_theta_pin(i)
|
||||
add_wire(mx, my, mx - STUB, my)
|
||||
add_label(net, mx - STUB, my, 180)
|
||||
|
||||
# Phi motor
|
||||
for i, net in enumerate(motor_nets_phi):
|
||||
px, py = tmc_phi_pin("left", i + 2)
|
||||
add_wire(px, py, px - STUB, py)
|
||||
add_label(net, px - STUB, py, 180)
|
||||
|
||||
mx, my = motor_phi_pin(i)
|
||||
add_wire(mx, my, mx - STUB, my)
|
||||
add_label(net, mx - STUB, my, 180)
|
||||
|
||||
# --- No-connect flags on unused TMC2209 pins ---
|
||||
# DIAG (index 6) and CLK (index 7) on both TMCs
|
||||
for pin_fn in [tmc_theta_pin, tmc_phi_pin]:
|
||||
for idx in [6, 7]: # DIAG, CLK
|
||||
nx, ny = pin_fn("right", idx)
|
||||
add_no_connect(nx, ny)
|
||||
|
||||
# --- No-connect flags on unused ESP32 pins ---
|
||||
# Left side: skip connected power pins (13=GND, 18=5V)
|
||||
# and used signal pins (6=IO32, 7=IO33, 8=IO25, 9=IO26, 10=IO27, 11=IO14)
|
||||
# Include 0=3V3 (unused power output) to silence ERC
|
||||
unused_left_idx = [0, 1, 2, 3, 4, 5, 12, 14, 15, 16, 17]
|
||||
for idx in unused_left_idx:
|
||||
nx, ny = esp_pin("left", idx)
|
||||
add_no_connect(nx, ny)
|
||||
|
||||
# Right side: skip GND connected at 0 (pin 38), used (10=IO17, 11=IO16)
|
||||
# Include index 6 (pin 32, second GND) for no_connect to silence ERC
|
||||
unused_right_idx = [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 16, 17, 18]
|
||||
for idx in unused_right_idx:
|
||||
nx, ny = esp_pin("right", idx)
|
||||
add_no_connect(nx, ny)
|
||||
|
||||
# Barrel jack shield pin
|
||||
sx, sy = jack_pin(2)
|
||||
add_no_connect(sx, sy)
|
||||
|
||||
# --- Text annotations ---
|
||||
add_text("12V DC\\nMotor Supply", J1_X, J1_Y - 15, 1.5)
|
||||
add_text("100\u00b5F\\nBulk Decoupling", C1_X, C1_Y - 12, 1.27)
|
||||
add_text("1k\u03a9 prevents\\nUART bus contention", R1_X, R1_Y - 8, 1.27)
|
||||
add_text("TMC addr 0\\nMS1=GND MS2=GND", U2_X, U2_Y - 18, 1.27)
|
||||
add_text("TMC addr 1\\nMS1=VIO MS2=GND", U3_X, U3_Y - 18, 1.27)
|
||||
add_text("Half-duplex UART\\nshared bus", 155, R1_Y + 6, 1.27)
|
||||
|
||||
# ── Assemble schematic ──
|
||||
all_components = "\n".join(components)
|
||||
all_wires = "\n".join(wires)
|
||||
all_junctions = "\n".join(junctions)
|
||||
all_labels = "\n".join(labels)
|
||||
all_nc = "\n".join(no_connects)
|
||||
all_texts = "\n".join(texts)
|
||||
all_sym_inst = "\n".join(sym_instances)
|
||||
|
||||
return (
|
||||
"(kicad_sch\n"
|
||||
" (version 20231120)\n"
|
||||
' (generator "mcnanovna_gen")\n'
|
||||
' (generator_version "1.0")\n'
|
||||
f' (uuid "{root_uuid}")\n'
|
||||
' (paper "A3")\n'
|
||||
" (title_block\n"
|
||||
' (title "ESP32 + TMC2209 Antenna Positioner Wiring")\n'
|
||||
' (date "2026-02-01")\n'
|
||||
' (rev "1")\n'
|
||||
' (company "mcnanovna")\n'
|
||||
' (comment 1 "Pin assignments from firmware/include/config.h")\n'
|
||||
' (comment 2 "Module-level wiring diagram for breadboard/perfboard")\n'
|
||||
" )\n"
|
||||
" (lib_symbols\n"
|
||||
f"{lib_syms}\n"
|
||||
" )\n"
|
||||
f"{all_components}\n"
|
||||
f"{all_wires}\n"
|
||||
f"{all_junctions}\n"
|
||||
f"{all_labels}\n"
|
||||
f"{all_nc}\n"
|
||||
f"{all_texts}\n"
|
||||
" (sheet_instances\n"
|
||||
' (path "/"\n'
|
||||
' (page "1")\n'
|
||||
" )\n"
|
||||
" )\n"
|
||||
" (symbol_instances\n"
|
||||
f"{all_sym_inst}\n"
|
||||
" )\n"
|
||||
")\n"
|
||||
)
|
||||
|
||||
|
||||
# ── Project file (.kicad_pro) ────────────────────────────────────────
|
||||
|
||||
|
||||
def gen_project():
|
||||
return json.dumps(
|
||||
{
|
||||
"meta": {
|
||||
"filename": "positioner.kicad_pro",
|
||||
"version": 1,
|
||||
},
|
||||
"schematic": {
|
||||
"meta": {"version": 1},
|
||||
"drawing": {"default_line_thickness": 6.0},
|
||||
"page_layout_descr_file": "",
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_symbol_libs": [],
|
||||
"pinned_footprint_libs": [],
|
||||
},
|
||||
"text_variables": {},
|
||||
},
|
||||
indent=2,
|
||||
) + "\n"
|
||||
|
||||
|
||||
# ── Symbol library table (sym-lib-table) ─────────────────────────────
|
||||
|
||||
|
||||
def gen_sym_lib_table():
|
||||
return (
|
||||
"(sym_lib_table\n"
|
||||
" (version 7)\n"
|
||||
' (lib (name "positioner")(type "KiCad")'
|
||||
'(uri "${KIPRJMOD}/positioner.kicad_sym")'
|
||||
'(options "")(descr "Module symbols for ESP32 antenna positioner"))\n'
|
||||
")\n"
|
||||
)
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def main():
|
||||
outdir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
files = [
|
||||
("positioner.kicad_sym", gen_sym_lib()),
|
||||
("positioner.kicad_sch", gen_schematic()),
|
||||
("positioner.kicad_pro", gen_project()),
|
||||
("sym-lib-table", gen_sym_lib_table()),
|
||||
]
|
||||
|
||||
for name, content in files:
|
||||
path = os.path.join(outdir, name)
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
print(f" wrote {name} ({len(content)} bytes)")
|
||||
|
||||
print(f"\nDone. Open {os.path.join(outdir, 'positioner.kicad_pro')} in KiCad 9.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
20
hardware/positioner.kicad_pro
Normal file
20
hardware/positioner.kicad_pro
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"meta": {
|
||||
"filename": "positioner.kicad_pro",
|
||||
"version": 1
|
||||
},
|
||||
"schematic": {
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"drawing": {
|
||||
"default_line_thickness": 6.0
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_symbol_libs": [],
|
||||
"pinned_footprint_libs": []
|
||||
},
|
||||
"text_variables": {}
|
||||
}
|
||||
1428
hardware/positioner.kicad_sch
Normal file
1428
hardware/positioner.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
571
hardware/positioner.kicad_sym
Normal file
571
hardware/positioner.kicad_sym
Normal file
@ -0,0 +1,571 @@
|
||||
(kicad_symbol_lib
|
||||
(version 20231120)
|
||||
(generator "mcnanovna_gen")
|
||||
(generator_version "1.0")
|
||||
(symbol "ESP32_DevKit_38pin"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "U" (at 0 27.94 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "ESP32_DevKit_38pin" (at 0 -27.94 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Description" "ESP32 DevKit V1 38-pin module" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "ESP32_DevKit_38pin_0_1"
|
||||
(rectangle
|
||||
(start -15.00 25.40)
|
||||
(end 15.00 -25.40)
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type background))
|
||||
)
|
||||
)
|
||||
(symbol "ESP32_DevKit_38pin_1_1"
|
||||
(pin power_out line
|
||||
(at -18.81 22.86 0)
|
||||
(length 3.81)
|
||||
(name "3V3" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at -18.81 20.32 0)
|
||||
(length 3.81)
|
||||
(name "EN" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 17.78 0)
|
||||
(length 3.81)
|
||||
(name "VP/IO36" (effects (font (size 1.27 1.27))))
|
||||
(number "3" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 15.24 0)
|
||||
(length 3.81)
|
||||
(name "VN/IO39" (effects (font (size 1.27 1.27))))
|
||||
(number "4" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 12.70 0)
|
||||
(length 3.81)
|
||||
(name "IO34" (effects (font (size 1.27 1.27))))
|
||||
(number "5" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 10.16 0)
|
||||
(length 3.81)
|
||||
(name "IO35" (effects (font (size 1.27 1.27))))
|
||||
(number "6" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -18.81 7.62 0)
|
||||
(length 3.81)
|
||||
(name "IO32/φ_STEP" (effects (font (size 1.27 1.27))))
|
||||
(number "7" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -18.81 5.08 0)
|
||||
(length 3.81)
|
||||
(name "IO33/φ_DIR" (effects (font (size 1.27 1.27))))
|
||||
(number "8" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -18.81 2.54 0)
|
||||
(length 3.81)
|
||||
(name "IO25/θ_STEP" (effects (font (size 1.27 1.27))))
|
||||
(number "9" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -18.81 0.00 0)
|
||||
(length 3.81)
|
||||
(name "IO26/θ_DIR" (effects (font (size 1.27 1.27))))
|
||||
(number "10" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -18.81 -2.54 0)
|
||||
(length 3.81)
|
||||
(name "IO27/θ_EN" (effects (font (size 1.27 1.27))))
|
||||
(number "11" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -18.81 -5.08 0)
|
||||
(length 3.81)
|
||||
(name "IO14/φ_EN" (effects (font (size 1.27 1.27))))
|
||||
(number "12" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 -7.62 0)
|
||||
(length 3.81)
|
||||
(name "IO12" (effects (font (size 1.27 1.27))))
|
||||
(number "13" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at -18.81 -10.16 0)
|
||||
(length 3.81)
|
||||
(name "GND" (effects (font (size 1.27 1.27))))
|
||||
(number "14" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 -12.70 0)
|
||||
(length 3.81)
|
||||
(name "IO13" (effects (font (size 1.27 1.27))))
|
||||
(number "15" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 -15.24 0)
|
||||
(length 3.81)
|
||||
(name "SD2" (effects (font (size 1.27 1.27))))
|
||||
(number "16" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 -17.78 0)
|
||||
(length 3.81)
|
||||
(name "SD3" (effects (font (size 1.27 1.27))))
|
||||
(number "17" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -18.81 -20.32 0)
|
||||
(length 3.81)
|
||||
(name "CMD" (effects (font (size 1.27 1.27))))
|
||||
(number "18" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at -18.81 -22.86 0)
|
||||
(length 3.81)
|
||||
(name "5V" (effects (font (size 1.27 1.27))))
|
||||
(number "19" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at 18.81 22.86 180)
|
||||
(length 3.81)
|
||||
(name "GND" (effects (font (size 1.27 1.27))))
|
||||
(number "38" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 20.32 180)
|
||||
(length 3.81)
|
||||
(name "IO23" (effects (font (size 1.27 1.27))))
|
||||
(number "37" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 17.78 180)
|
||||
(length 3.81)
|
||||
(name "IO22" (effects (font (size 1.27 1.27))))
|
||||
(number "36" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 15.24 180)
|
||||
(length 3.81)
|
||||
(name "TX0/IO1" (effects (font (size 1.27 1.27))))
|
||||
(number "35" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 12.70 180)
|
||||
(length 3.81)
|
||||
(name "RX0/IO3" (effects (font (size 1.27 1.27))))
|
||||
(number "34" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 10.16 180)
|
||||
(length 3.81)
|
||||
(name "IO21" (effects (font (size 1.27 1.27))))
|
||||
(number "33" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at 18.81 7.62 180)
|
||||
(length 3.81)
|
||||
(name "GND" (effects (font (size 1.27 1.27))))
|
||||
(number "32" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 5.08 180)
|
||||
(length 3.81)
|
||||
(name "IO19" (effects (font (size 1.27 1.27))))
|
||||
(number "31" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 2.54 180)
|
||||
(length 3.81)
|
||||
(name "IO18" (effects (font (size 1.27 1.27))))
|
||||
(number "30" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 0.00 180)
|
||||
(length 3.81)
|
||||
(name "IO5" (effects (font (size 1.27 1.27))))
|
||||
(number "29" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at 18.81 -2.54 180)
|
||||
(length 3.81)
|
||||
(name "IO17/TMC_TX" (effects (font (size 1.27 1.27))))
|
||||
(number "28" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 18.81 -5.08 180)
|
||||
(length 3.81)
|
||||
(name "IO16/TMC_RX" (effects (font (size 1.27 1.27))))
|
||||
(number "27" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -7.62 180)
|
||||
(length 3.81)
|
||||
(name "IO4" (effects (font (size 1.27 1.27))))
|
||||
(number "26" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -10.16 180)
|
||||
(length 3.81)
|
||||
(name "IO0" (effects (font (size 1.27 1.27))))
|
||||
(number "25" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -12.70 180)
|
||||
(length 3.81)
|
||||
(name "IO2" (effects (font (size 1.27 1.27))))
|
||||
(number "24" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -15.24 180)
|
||||
(length 3.81)
|
||||
(name "IO15" (effects (font (size 1.27 1.27))))
|
||||
(number "23" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -17.78 180)
|
||||
(length 3.81)
|
||||
(name "SD1" (effects (font (size 1.27 1.27))))
|
||||
(number "22" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -20.32 180)
|
||||
(length 3.81)
|
||||
(name "SD0" (effects (font (size 1.27 1.27))))
|
||||
(number "21" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 18.81 -22.86 180)
|
||||
(length 3.81)
|
||||
(name "CLK" (effects (font (size 1.27 1.27))))
|
||||
(number "20" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "TMC2209_SilentStepStick"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "U" (at 0 13.97 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "TMC2209_SilentStepStick" (at 0 -13.97 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Description" "TMC2209 stepper driver breakout board" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "TMC2209_SilentStepStick_0_1"
|
||||
(rectangle
|
||||
(start -9.00 11.43)
|
||||
(end 9.00 -11.43)
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type background))
|
||||
)
|
||||
)
|
||||
(symbol "TMC2209_SilentStepStick_1_1"
|
||||
(pin power_in line
|
||||
(at -11.54 8.89 0)
|
||||
(length 2.54)
|
||||
(name "VM" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at -11.54 6.35 0)
|
||||
(length 2.54)
|
||||
(name "GND" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -11.54 3.81 0)
|
||||
(length 2.54)
|
||||
(name "2B" (effects (font (size 1.27 1.27))))
|
||||
(number "3" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -11.54 1.27 0)
|
||||
(length 2.54)
|
||||
(name "2A" (effects (font (size 1.27 1.27))))
|
||||
(number "4" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -11.54 -1.27 0)
|
||||
(length 2.54)
|
||||
(name "1A" (effects (font (size 1.27 1.27))))
|
||||
(number "5" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at -11.54 -3.81 0)
|
||||
(length 2.54)
|
||||
(name "1B" (effects (font (size 1.27 1.27))))
|
||||
(number "6" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at -11.54 -6.35 0)
|
||||
(length 2.54)
|
||||
(name "VIO" (effects (font (size 1.27 1.27))))
|
||||
(number "7" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin power_in line
|
||||
(at -11.54 -8.89 0)
|
||||
(length 2.54)
|
||||
(name "GND" (effects (font (size 1.27 1.27))))
|
||||
(number "8" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 11.54 8.89 180)
|
||||
(length 2.54)
|
||||
(name "EN" (effects (font (size 1.27 1.27))))
|
||||
(number "9" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 11.54 6.35 180)
|
||||
(length 2.54)
|
||||
(name "MS1" (effects (font (size 1.27 1.27))))
|
||||
(number "10" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 11.54 3.81 180)
|
||||
(length 2.54)
|
||||
(name "MS2" (effects (font (size 1.27 1.27))))
|
||||
(number "11" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin bidirectional line
|
||||
(at 11.54 1.27 180)
|
||||
(length 2.54)
|
||||
(name "PDN_UART" (effects (font (size 1.27 1.27))))
|
||||
(number "12" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 11.54 -1.27 180)
|
||||
(length 2.54)
|
||||
(name "STEP" (effects (font (size 1.27 1.27))))
|
||||
(number "13" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 11.54 -3.81 180)
|
||||
(length 2.54)
|
||||
(name "DIR" (effects (font (size 1.27 1.27))))
|
||||
(number "14" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin output line
|
||||
(at 11.54 -6.35 180)
|
||||
(length 2.54)
|
||||
(name "DIAG" (effects (font (size 1.27 1.27))))
|
||||
(number "15" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin input line
|
||||
(at 11.54 -8.89 180)
|
||||
(length 2.54)
|
||||
(name "CLK" (effects (font (size 1.27 1.27))))
|
||||
(number "16" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "NEMA17_Motor"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "J" (at 0 8.89 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "NEMA17_Motor" (at 0 -8.89 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Description" "NEMA 17 stepper motor connector" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "NEMA17_Motor_0_1"
|
||||
(rectangle
|
||||
(start -6.00 6.35)
|
||||
(end 6.00 -6.35)
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type background))
|
||||
)
|
||||
(polyline
|
||||
(pts (xy -2.00 3.00) (xy -2.00 1.50) (xy -1.00 1.20) (xy -3.00 0.60) (xy -1.00 0.00) (xy -2.00 -0.30))
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type none))
|
||||
)
|
||||
(polyline
|
||||
(pts (xy 2.00 3.00) (xy 2.00 1.50) (xy 1.00 1.20) (xy 3.00 0.60) (xy 1.00 0.00) (xy 2.00 -0.30))
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type none))
|
||||
)
|
||||
(text "M"
|
||||
(at 0.00 -3.50 0)
|
||||
(effects (font (size 2.0 2.0)))
|
||||
)
|
||||
)
|
||||
(symbol "NEMA17_Motor_1_1"
|
||||
(pin passive line
|
||||
(at -8.54 3.81 0)
|
||||
(length 2.54)
|
||||
(name "A1" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -8.54 1.27 0)
|
||||
(length 2.54)
|
||||
(name "A2" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -8.54 -1.27 0)
|
||||
(length 2.54)
|
||||
(name "B1" (effects (font (size 1.27 1.27))))
|
||||
(number "3" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -8.54 -3.81 0)
|
||||
(length 2.54)
|
||||
(name "B2" (effects (font (size 1.27 1.27))))
|
||||
(number "4" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "Barrel_Jack_DC"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "J" (at 0 7.62 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "Barrel_Jack_DC" (at 0 -7.62 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Description" "DC barrel jack power input" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "Barrel_Jack_DC_0_1"
|
||||
(rectangle
|
||||
(start -5.00 5.08)
|
||||
(end 5.00 -5.08)
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type background))
|
||||
)
|
||||
)
|
||||
(symbol "Barrel_Jack_DC_1_1"
|
||||
(pin passive line
|
||||
(at -7.54 2.54 0)
|
||||
(length 2.54)
|
||||
(name "+12V" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -7.54 0.00 0)
|
||||
(length 2.54)
|
||||
(name "GND" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at -7.54 -2.54 0)
|
||||
(length 2.54)
|
||||
(name "Shield" (effects (font (size 1.27 1.27))))
|
||||
(number "3" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "R"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "R" (at 2.54 0 90)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "R" (at -2.54 0 90)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "R_0_1"
|
||||
(rectangle
|
||||
(start -1.02 3.81)
|
||||
(end 1.02 -3.81)
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type background))
|
||||
)
|
||||
)
|
||||
(symbol "R_1_1"
|
||||
(pin passive line
|
||||
(at 0.00 6.35 270)
|
||||
(length 2.54)
|
||||
(name "~" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 0.00 -6.35 90)
|
||||
(length 2.54)
|
||||
(name "~" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol "C_Polarized"
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(property "Reference" "C" (at 2.54 0 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Value" "C_Polarized" (at -2.54 0 0)
|
||||
(effects (font (size 1.27 1.27))))
|
||||
(property "Footprint" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(property "Datasheet" "" (at 0 0 0)
|
||||
(effects (font (size 1.27 1.27)) hide))
|
||||
(symbol "C_Polarized_0_1"
|
||||
(polyline
|
||||
(pts (xy -2.00 1.00) (xy 2.00 1.00))
|
||||
(stroke (width 0.508) (type default))
|
||||
(fill (type none))
|
||||
)
|
||||
(arc
|
||||
(start -2.00 -1.00)
|
||||
(mid 0.00 -2.00)
|
||||
(end 2.00 -1.00)
|
||||
(stroke (width 0.508) (type default))
|
||||
(fill (type none))
|
||||
)
|
||||
(polyline
|
||||
(pts (xy -1.00 2.50) (xy 1.00 2.50))
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type none))
|
||||
)
|
||||
(polyline
|
||||
(pts (xy 0.00 1.50) (xy 0.00 3.50))
|
||||
(stroke (width 0.254) (type default))
|
||||
(fill (type none))
|
||||
)
|
||||
)
|
||||
(symbol "C_Polarized_1_1"
|
||||
(pin passive line
|
||||
(at 0.00 3.81 270)
|
||||
(length 2.54)
|
||||
(name "+" (effects (font (size 1.27 1.27))))
|
||||
(number "1" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
(pin passive line
|
||||
(at 0.00 -3.81 90)
|
||||
(length 2.54)
|
||||
(name "-" (effects (font (size 1.27 1.27))))
|
||||
(number "2" (effects (font (size 1.27 1.27))))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
4
hardware/sym-lib-table
Normal file
4
hardware/sym-lib-table
Normal file
@ -0,0 +1,4 @@
|
||||
(sym_lib_table
|
||||
(version 7)
|
||||
(lib (name "positioner")(type "KiCad")(uri "${KIPRJMOD}/positioner.kicad_sym")(options "")(descr "Module symbols for ESP32 antenna positioner"))
|
||||
)
|
||||
24
pyproject.toml
Normal file
24
pyproject.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[project]
|
||||
name = "mcpositioner"
|
||||
version = "2026.02.02"
|
||||
description = "MCP server for ESP32 dual-axis antenna positioner control"
|
||||
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastmcp>=2.14.0",
|
||||
"httpx>=0.28.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mcpositioner = "mcpositioner.server:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/mcpositioner"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
line-length = 120
|
||||
1
src/mcpositioner/__init__.py
Normal file
1
src/mcpositioner/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""mcpositioner -- MCP server for ESP32 dual-axis antenna positioner control."""
|
||||
5
src/mcpositioner/__main__.py
Normal file
5
src/mcpositioner/__main__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Allow running as: python -m mcpositioner"""
|
||||
|
||||
from mcpositioner.server import main
|
||||
|
||||
main()
|
||||
102
src/mcpositioner/positioner.py
Normal file
102
src/mcpositioner/positioner.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""HTTP client for ESP32 antenna positioner.
|
||||
|
||||
Communicates with the ESP32 over WiFi to control dual-axis stepper
|
||||
motors for automated antenna pattern measurement. Uses mDNS discovery
|
||||
(positioner.local) with fallback to MCPOSITIONER_HOST env var.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class PositionerError(Exception):
|
||||
"""Raised when positioner communication fails."""
|
||||
|
||||
|
||||
class Positioner:
|
||||
"""Async HTTP client for the ESP32 antenna positioner."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str | None = None,
|
||||
port: int = 80,
|
||||
timeout: float = 30.0,
|
||||
) -> None:
|
||||
if host is None:
|
||||
host = os.environ.get("MCPOSITIONER_HOST", "positioner.local")
|
||||
self.base_url = f"http://{host}:{port}"
|
||||
self._timeout = timeout
|
||||
|
||||
async def _request(self, method: str, path: str, json: dict | None = None) -> dict:
|
||||
"""Send an HTTP request and return the parsed JSON response."""
|
||||
url = f"{self.base_url}{path}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
if method == "GET":
|
||||
resp = await client.get(url)
|
||||
else:
|
||||
resp = await client.post(url, json=json or {})
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except httpx.ConnectError as exc:
|
||||
raise PositionerError(
|
||||
f"Cannot connect to positioner at {self.base_url}. "
|
||||
f"Check that the ESP32 is powered on and connected to WiFi. "
|
||||
f"Set MCPOSITIONER_HOST if not using mDNS. Error: {exc}"
|
||||
) from exc
|
||||
except httpx.TimeoutException as exc:
|
||||
raise PositionerError(f"Positioner request timed out ({self._timeout}s): {path}") from exc
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise PositionerError(f"Positioner returned {exc.response.status_code}: {exc.response.text}") from exc
|
||||
except Exception as exc:
|
||||
raise PositionerError(f"Positioner request failed: {exc}") from exc
|
||||
|
||||
async def status(self) -> dict:
|
||||
"""Get current position, moving state, and homed state."""
|
||||
return await self._request("GET", "/status")
|
||||
|
||||
async def move(self, theta_deg: float, phi_deg: float) -> dict:
|
||||
"""Move to absolute position (theta=polar, phi=azimuth)."""
|
||||
return await self._request("POST", "/move", {"theta_deg": theta_deg, "phi_deg": phi_deg})
|
||||
|
||||
async def move_relative(self, d_theta: float = 0.0, d_phi: float = 0.0) -> dict:
|
||||
"""Move by a relative offset from current position."""
|
||||
return await self._request("POST", "/move/relative", {"d_theta": d_theta, "d_phi": d_phi})
|
||||
|
||||
async def home(self, axis: str = "both") -> dict:
|
||||
"""Home one or both axes using StallGuard sensorless detection.
|
||||
|
||||
Args:
|
||||
axis: 'both', 'theta', or 'phi'
|
||||
"""
|
||||
return await self._request("POST", "/home", {"axis": axis})
|
||||
|
||||
async def stop(self) -> dict:
|
||||
"""Emergency stop all motors immediately."""
|
||||
return await self._request("POST", "/stop")
|
||||
|
||||
async def get_config(self) -> dict:
|
||||
"""Get current motion parameters (speed, accel, microstepping)."""
|
||||
return await self._request("GET", "/config")
|
||||
|
||||
async def set_config(self, **kwargs: float | int) -> dict:
|
||||
"""Update motion parameters.
|
||||
|
||||
Args:
|
||||
speed: Max speed in steps/sec
|
||||
accel: Acceleration in steps/sec^2
|
||||
microstepping: Microstep divisor (1, 2, 4, 8, 16, 32, 64, 128, 256)
|
||||
"""
|
||||
return await self._request("POST", "/config", kwargs)
|
||||
|
||||
async def wait_until_stopped(self, poll_interval: float = 0.1) -> dict:
|
||||
"""Poll /status until motors stop moving. Returns final status."""
|
||||
while True:
|
||||
st = await self.status()
|
||||
if not st.get("moving", False):
|
||||
return st
|
||||
await asyncio.sleep(poll_interval)
|
||||
301
src/mcpositioner/prompts.py
Normal file
301
src/mcpositioner/prompts.py
Normal file
@ -0,0 +1,301 @@
|
||||
"""FastMCP prompts for guided positioner workflows.
|
||||
|
||||
Prompts guide the LLM through multi-step procedures: homing,
|
||||
configuration tuning, and the full 3D pattern measurement workflow
|
||||
that orchestrates both mcpositioner and mcnanovna tools.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
|
||||
def register_prompts(mcp: FastMCP) -> None:
|
||||
"""Register all positioner workflow prompts on the FastMCP server."""
|
||||
|
||||
@mcp.prompt
|
||||
def home_positioner(
|
||||
axis: str = "both",
|
||||
) -> list[Message]:
|
||||
"""Guide through safe positioner homing with pre-flight checks.
|
||||
|
||||
Walks through verifying connectivity, checking for obstructions,
|
||||
and running sensorless StallGuard homing on one or both axes.
|
||||
|
||||
Args:
|
||||
axis: Which axis to home -- 'both', 'theta', or 'phi'
|
||||
"""
|
||||
axis_label = {"both": "both axes", "theta": "theta (polar) axis", "phi": "phi (azimuth) axis"}.get(
|
||||
axis, "both axes"
|
||||
)
|
||||
|
||||
return [
|
||||
Message(
|
||||
role="user",
|
||||
content=f"Home the positioner on {axis_label}.",
|
||||
),
|
||||
Message(
|
||||
role="assistant",
|
||||
content=f"""I'll guide you through homing {axis_label} safely.
|
||||
|
||||
**How StallGuard homing works:**
|
||||
The TMC2209 drivers detect motor stall by monitoring back-EMF. During homing,
|
||||
each axis moves slowly in the negative direction until the motor stalls against
|
||||
its mechanical stop, then backs off slightly and zeroes the position counter.
|
||||
|
||||
**Pre-flight checklist:**
|
||||
1. Verify the positioner is reachable: `positioner_status`
|
||||
2. Check that nothing is blocking the antenna's path to the home position
|
||||
3. Ensure the antenna under test is securely mounted
|
||||
4. Confirm cables have enough slack for the full range of motion
|
||||
|
||||
**Homing procedure:**
|
||||
1. Call `positioner_status` to verify connectivity and current state
|
||||
2. If already homed and you just need to re-home, that's fine -- it's non-destructive
|
||||
3. Call `positioner_home` with axis='{axis}'
|
||||
4. The ESP32 will move motors slowly until stall is detected
|
||||
5. Call `positioner_status` to confirm homed=true and position is near 0,0
|
||||
|
||||
**After homing:**
|
||||
- theta=0 is zenith (straight up), 180 is nadir (straight down)
|
||||
- phi=0 is the reference azimuth direction (typically North or toward tx antenna)
|
||||
- The positioner is ready for moves within theta=[0, 180] and phi=[0, 360]
|
||||
|
||||
**Troubleshooting:**
|
||||
- If homing fails, try reducing speed: `positioner_config` with speed=500
|
||||
- StallGuard sensitivity is set in firmware (config.h STALL_VALUE)
|
||||
- Ensure motors are not overheating (TMC2209 has thermal protection)
|
||||
|
||||
Let me start by checking the positioner status.""",
|
||||
),
|
||||
]
|
||||
|
||||
@mcp.prompt
|
||||
def configure_positioner(
|
||||
speed: float | None = None,
|
||||
accel: float | None = None,
|
||||
microstepping: int | None = None,
|
||||
) -> list[Message]:
|
||||
"""Guide through positioner motion parameter tuning.
|
||||
|
||||
Helps set optimal speed, acceleration, and microstepping for
|
||||
the antenna under test and measurement requirements.
|
||||
|
||||
Args:
|
||||
speed: Target max speed in steps/sec (None to keep current)
|
||||
accel: Target acceleration in steps/sec^2 (None to keep current)
|
||||
microstepping: Microstep divisor (None to keep current)
|
||||
"""
|
||||
changes = []
|
||||
if speed is not None:
|
||||
changes.append(f"speed={speed} steps/sec")
|
||||
if accel is not None:
|
||||
changes.append(f"accel={accel} steps/sec^2")
|
||||
if microstepping is not None:
|
||||
changes.append(f"microstepping=1/{microstepping}")
|
||||
|
||||
change_desc = ", ".join(changes) if changes else "review current settings"
|
||||
|
||||
return [
|
||||
Message(
|
||||
role="user",
|
||||
content=f"Configure positioner motion parameters: {change_desc}.",
|
||||
),
|
||||
Message(
|
||||
role="assistant",
|
||||
content=f"""I'll help tune the positioner's motion parameters.
|
||||
|
||||
**Current request:** {change_desc}
|
||||
|
||||
**Parameter reference:**
|
||||
|
||||
| Parameter | Default | Range | Effect |
|
||||
|-----------|---------|-------|--------|
|
||||
| speed | ~2000 steps/sec | 100-5000 | Higher = faster moves, more vibration |
|
||||
| accel | ~1000 steps/sec^2 | 100-3000 | Higher = snappier starts/stops |
|
||||
| microstepping | 16 | 1-256 | Higher = smoother motion, lower torque |
|
||||
|
||||
**Speed vs. measurement quality tradeoffs:**
|
||||
- **Fast survey** (speed=3000, accel=2000): Quick positioning, more mechanical vibration.
|
||||
Use settle_ms=300+ in measurement grid to compensate.
|
||||
- **Precision measurement** (speed=1000, accel=500): Slower but less vibration and
|
||||
overshoot. settle_ms=100 is usually sufficient.
|
||||
- **Heavy antenna** (speed=500, accel=300): For large antennas that create more
|
||||
inertia. Prevents missed steps.
|
||||
|
||||
**Microstepping guide:**
|
||||
- 1/16: Good default balance of resolution and torque
|
||||
- 1/32 or 1/64: Smoother motion for precision measurements
|
||||
- 1/8 or lower: More torque for heavy loads, but audibly rougher
|
||||
|
||||
**Procedure:**
|
||||
1. Read current config with `positioner_config` (no arguments)
|
||||
2. Apply changes with `positioner_config` passing the new values
|
||||
3. Test with a small move: `positioner_move` to theta=45, phi=45
|
||||
4. Observe motion quality and adjust if needed
|
||||
|
||||
Let me check the current configuration first.""",
|
||||
),
|
||||
]
|
||||
|
||||
@mcp.prompt
|
||||
def measure_pattern_grid(
|
||||
antenna_type: str = "dipole",
|
||||
band: str = "2m",
|
||||
theta_step: float = 5.0,
|
||||
phi_step: float = 10.0,
|
||||
points: int = 51,
|
||||
settle_ms: int = 200,
|
||||
) -> list[Message]:
|
||||
"""Guide through automated 3D antenna pattern measurement using positioner + VNA.
|
||||
|
||||
This is a cross-server workflow: mcpositioner controls the physical rotation,
|
||||
mcnanovna (separate MCP server) controls the VNA for S21 measurement at each
|
||||
grid point. The LLM orchestrates both.
|
||||
|
||||
Args:
|
||||
antenna_type: Label for pattern metadata (e.g., 'dipole', 'yagi', 'measured')
|
||||
band: Ham band name for frequency range (e.g., '2m', '70cm', '20m')
|
||||
theta_step: Polar angle step in degrees
|
||||
phi_step: Azimuth step in degrees
|
||||
points: Number of frequency points per VNA S21 scan
|
||||
settle_ms: Milliseconds to wait after each move before measuring
|
||||
"""
|
||||
ham_bands = {
|
||||
"160m": (1_800_000, 2_000_000),
|
||||
"80m": (3_500_000, 4_000_000),
|
||||
"60m": (5_330_500, 5_403_500),
|
||||
"40m": (7_000_000, 7_300_000),
|
||||
"30m": (10_100_000, 10_150_000),
|
||||
"20m": (14_000_000, 14_350_000),
|
||||
"17m": (18_068_000, 18_168_000),
|
||||
"15m": (21_000_000, 21_450_000),
|
||||
"12m": (24_890_000, 24_990_000),
|
||||
"10m": (28_000_000, 29_700_000),
|
||||
"6m": (50_000_000, 54_000_000),
|
||||
"2m": (144_000_000, 148_000_000),
|
||||
"70cm": (420_000_000, 450_000_000),
|
||||
"23cm": (1_240_000_000, 1_300_000_000),
|
||||
}
|
||||
|
||||
f_start, f_stop = ham_bands.get(band, ham_bands["2m"])
|
||||
n_theta = int(180 / theta_step) + 1
|
||||
n_phi = int(360 / phi_step)
|
||||
total = n_theta * n_phi
|
||||
est_minutes = total * (settle_ms / 1000.0 + 1.0) / 60
|
||||
|
||||
def fmt_freq(hz: int) -> str:
|
||||
if hz >= 1_000_000:
|
||||
return f"{hz / 1e6:.3f} MHz"
|
||||
return f"{hz / 1e3:.1f} kHz"
|
||||
|
||||
return [
|
||||
Message(
|
||||
role="user",
|
||||
content=(
|
||||
f"Measure the 3D radiation pattern of my {antenna_type} antenna "
|
||||
f"on the {band.upper()} band using the positioner and VNA."
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role="assistant",
|
||||
content=f"""I'll guide you through an automated 3D radiation pattern measurement.
|
||||
|
||||
This workflow uses **two MCP servers working together**:
|
||||
- **mcpositioner** -- controls the antenna positioner (this server)
|
||||
- **mcnanovna** -- controls the NanoVNA for S21 measurements
|
||||
|
||||
**Grid setup:**
|
||||
- Band: {band.upper()} ({fmt_freq(f_start)} -- {fmt_freq(f_stop)})
|
||||
- Grid: {theta_step} deg theta x {phi_step} deg phi = {n_theta} x {n_phi} = **{total} points**
|
||||
- VNA scan: {points} frequency points per position
|
||||
- Settle time: {settle_ms} ms after each move
|
||||
- Estimated time: ~{est_minutes:.0f} minutes
|
||||
|
||||
**Hardware setup:**
|
||||
1. ESP32 positioner powered and on WiFi (positioner.local)
|
||||
2. NanoVNA-H connected via USB
|
||||
3. Transmit antenna on NanoVNA Port 1 (stationary, aimed at positioner)
|
||||
4. Antenna under test mounted on positioner, connected to NanoVNA Port 2
|
||||
|
||||
---
|
||||
|
||||
**Step 1: Pre-flight checks**
|
||||
|
||||
Call these tools to verify both systems are ready:
|
||||
- `positioner_status` (mcpositioner) -- verify ESP32 is reachable
|
||||
- `info` (mcnanovna) -- verify VNA is connected
|
||||
|
||||
**Step 2: Home the positioner**
|
||||
|
||||
Call `positioner_home` (mcpositioner) with axis='both'.
|
||||
Wait for it to complete, then verify with `positioner_status`.
|
||||
|
||||
**Step 3: Calibrate VNA (if not already done)**
|
||||
|
||||
Run a SOLT calibration on the mcnanovna server covering {fmt_freq(f_start)} to {fmt_freq(f_stop)}.
|
||||
|
||||
**Step 4: Measure the grid**
|
||||
|
||||
For each point in the theta/phi grid, execute this sequence:
|
||||
|
||||
```
|
||||
for theta in [0, {theta_step}, {2 * theta_step}, ..., 180]:
|
||||
for phi in [0, {phi_step}, {2 * phi_step}, ..., {360 - phi_step}]:
|
||||
1. positioner_move(theta, phi, wait=True) # mcpositioner
|
||||
2. wait {settle_ms}ms for mechanical settling
|
||||
3. scan({f_start}, {f_stop}, {points}, s21=True) # mcnanovna
|
||||
4. extract peak S21 magnitude in dB from scan result
|
||||
5. record: {{theta_deg: theta, phi_deg: phi, s21_peak_db: peak_db}}
|
||||
```
|
||||
|
||||
**Serpentine optimization:** On odd-numbered theta rows, reverse the phi direction
|
||||
to minimize motor travel:
|
||||
- Row 0 (theta=0): phi goes 0 -> {360 - phi_step}
|
||||
- Row 1 (theta={theta_step}): phi goes {360 - phi_step} -> 0
|
||||
- Row 2 (theta={2 * theta_step}): phi goes 0 -> {360 - phi_step}
|
||||
- etc.
|
||||
|
||||
**Extracting peak S21 from scan result:**
|
||||
The scan returns data points with s21 complex values (real + imag).
|
||||
For each point: magnitude_db = 20 * log10(sqrt(real^2 + imag^2)).
|
||||
Take the maximum (least negative) dB value across all frequency points.
|
||||
|
||||
**Step 5: Build the pattern dict**
|
||||
|
||||
Assemble the measurements into the standard pattern format:
|
||||
```python
|
||||
pattern = {{
|
||||
"antenna_type": "{antenna_type}",
|
||||
"frequency_hz": {(f_start + f_stop) / 2},
|
||||
"theta_deg": [list of theta values],
|
||||
"phi_deg": [list of phi values],
|
||||
"gain_dbi": [list of gain values],
|
||||
"peak_gain_dbi": max(gain_values),
|
||||
"num_points": {total},
|
||||
}}
|
||||
```
|
||||
|
||||
**Relative vs. absolute calibration:**
|
||||
- **Relative** (default): Normalize so peak gain = 0 dBi. Shows pattern shape only.
|
||||
gain_dbi[i] = s21_peak_db[i] - max(all s21_peak_db values)
|
||||
- **Absolute**: First measure a known-gain reference antenna at bore-sight.
|
||||
Record that S21 as reference_db. Then:
|
||||
gain_dbi[i] = s21_peak_db[i] - reference_db + reference_antenna_gain_dbi
|
||||
|
||||
**Step 6: Visualize**
|
||||
|
||||
If mcnanovna's web UI is running (MCNANOVNA_WEB_PORT), the pattern dict can be
|
||||
sent to the /api/pattern/compute endpoint for 3D rendering.
|
||||
|
||||
**Resolution tradeoffs:**
|
||||
| Step size | Grid points | Est. time | Use case |
|
||||
|-----------|------------|-----------|----------|
|
||||
| 10 deg x 20 deg | 19 x 18 = 342 | ~9 min | Quick survey |
|
||||
| 5 deg x 10 deg | 37 x 36 = 1332 | ~33 min | Standard |
|
||||
| 2 deg x 5 deg | 91 x 72 = 6552 | ~164 min | High-res |
|
||||
|
||||
Ready to start? I'll begin with the pre-flight checks.""",
|
||||
),
|
||||
]
|
||||
72
src/mcpositioner/server.py
Normal file
72
src/mcpositioner/server.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""FastMCP server for ESP32 antenna positioner control.
|
||||
|
||||
Registers positioner tools and prompts, then starts the MCP server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.tools.tool import FunctionTool
|
||||
|
||||
from mcpositioner.prompts import register_prompts
|
||||
from mcpositioner.tools import PositionerTools
|
||||
|
||||
_TOOL_METHODS = [
|
||||
"positioner_status",
|
||||
"positioner_move",
|
||||
"positioner_home",
|
||||
"positioner_stop",
|
||||
"positioner_config",
|
||||
]
|
||||
|
||||
|
||||
def create_server() -> FastMCP:
|
||||
mcp = FastMCP(
|
||||
name="mcpositioner",
|
||||
instructions=(
|
||||
"MCP server for controlling an ESP32 dual-axis antenna positioner over WiFi. "
|
||||
"The positioner drives two NEMA 17 stepper motors via TMC2209 drivers for "
|
||||
"theta (polar, 0-180 deg) and phi (azimuth, 0-360 deg) rotation.\n\n"
|
||||
"Tools:\n"
|
||||
"- positioner_status: read current theta/phi position, moving/homed state\n"
|
||||
"- positioner_move: absolute move to theta/phi with optional wait-for-completion\n"
|
||||
"- positioner_home: sensorless StallGuard homing on one or both axes\n"
|
||||
"- positioner_stop: emergency stop all motors\n"
|
||||
"- positioner_config: get/set speed, acceleration, microstepping\n\n"
|
||||
"The positioner communicates via HTTP to an ESP32 (default: positioner.local). "
|
||||
"Set MCPOSITIONER_HOST to override the hostname.\n\n"
|
||||
"For automated 3D antenna pattern measurement, use this server together with "
|
||||
"mcnanovna (NanoVNA MCP server). The workflow: move positioner to each grid point "
|
||||
"(positioner_move), then measure S21 with the VNA (mcnanovna's scan tool), "
|
||||
"and repeat across the theta/phi grid. See the measure_pattern_grid prompt "
|
||||
"for a step-by-step guide."
|
||||
),
|
||||
)
|
||||
|
||||
tools = PositionerTools()
|
||||
for method_name in _TOOL_METHODS:
|
||||
bound_method = getattr(tools, method_name)
|
||||
tool = FunctionTool.from_function(bound_method)
|
||||
mcp.add_tool(tool)
|
||||
|
||||
register_prompts(mcp)
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
|
||||
package_version = version("mcpositioner")
|
||||
except Exception:
|
||||
package_version = "dev"
|
||||
|
||||
print(f"mcpositioner v{package_version} -- ESP32 antenna positioner MCP server")
|
||||
|
||||
server = create_server()
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
95
src/mcpositioner/tools.py
Normal file
95
src/mcpositioner/tools.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""MCP tool implementations for the ESP32 antenna positioner.
|
||||
|
||||
Five tools for direct positioner control. The positioner_move, positioner_home,
|
||||
positioner_stop, positioner_config, and positioner_status tools map 1:1 to the
|
||||
ESP32 HTTP API endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from mcpositioner.positioner import Positioner
|
||||
|
||||
|
||||
class PositionerTools:
|
||||
"""Holds one Positioner HTTP client and exposes the 5 MCP tool methods."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._positioner = Positioner()
|
||||
|
||||
async def positioner_status(self) -> dict:
|
||||
"""Get current positioner state: position, moving, homed.
|
||||
|
||||
Returns theta/phi angles in degrees, whether motors are moving,
|
||||
and whether the axes have been homed.
|
||||
"""
|
||||
return await self._positioner.status()
|
||||
|
||||
async def positioner_move(
|
||||
self,
|
||||
theta_deg: float,
|
||||
phi_deg: float,
|
||||
wait: bool = True,
|
||||
) -> dict:
|
||||
"""Move the antenna positioner to an absolute theta/phi position.
|
||||
|
||||
Theta is the polar angle (0=zenith, 90=horizon, 180=nadir).
|
||||
Phi is the azimuth angle (0=North, 90=East, etc.).
|
||||
|
||||
Args:
|
||||
theta_deg: Polar angle in degrees (0 to 180)
|
||||
phi_deg: Azimuth angle in degrees (0 to 360)
|
||||
wait: If True, block until the move completes (default True)
|
||||
"""
|
||||
result = await self._positioner.move(theta_deg, phi_deg)
|
||||
if wait:
|
||||
result = await self._positioner.wait_until_stopped()
|
||||
return result
|
||||
|
||||
async def positioner_home(
|
||||
self,
|
||||
axis: str = "both",
|
||||
) -> dict:
|
||||
"""Home one or both positioner axes using sensorless StallGuard detection.
|
||||
|
||||
The motors move slowly in the negative direction until a mechanical stall
|
||||
is detected, then back off and set the position to zero.
|
||||
|
||||
Args:
|
||||
axis: Which axis to home -- 'both', 'theta', or 'phi'
|
||||
"""
|
||||
return await self._positioner.home(axis)
|
||||
|
||||
async def positioner_stop(self) -> dict:
|
||||
"""Emergency stop all positioner motors immediately.
|
||||
|
||||
Motors decelerate to a stop. Use this if the positioner is heading
|
||||
toward an obstacle or behaving unexpectedly.
|
||||
"""
|
||||
return await self._positioner.stop()
|
||||
|
||||
async def positioner_config(
|
||||
self,
|
||||
speed: float | None = None,
|
||||
accel: float | None = None,
|
||||
microstepping: int | None = None,
|
||||
) -> dict:
|
||||
"""Get or set positioner motion parameters.
|
||||
|
||||
With no arguments, returns current config. With arguments, updates
|
||||
the specified parameters.
|
||||
|
||||
Args:
|
||||
speed: Max speed in steps/sec (default ~2000 = 225 deg/sec)
|
||||
accel: Acceleration in steps/sec^2 (default ~1000 = 112 deg/sec^2)
|
||||
microstepping: Microstep divisor (1, 2, 4, 8, 16, 32, 64, 128, 256)
|
||||
"""
|
||||
if speed is None and accel is None and microstepping is None:
|
||||
return await self._positioner.get_config()
|
||||
kwargs = {}
|
||||
if speed is not None:
|
||||
kwargs["speed"] = speed
|
||||
if accel is not None:
|
||||
kwargs["accel"] = accel
|
||||
if microstepping is not None:
|
||||
kwargs["microstepping"] = microstepping
|
||||
return await self._positioner.set_config(**kwargs)
|
||||
Loading…
x
Reference in New Issue
Block a user