JasonYANG170 的 LogoYANG Docs
首页
硬件
软件
AI
3D
主站
首页
硬件
软件
AI
3D
主站
OSHWLab

EDA_Info-Helper

本项目是基于嘉立创EDA API扩展开发的EDA信息助手,用于与嘉立创EDA交互的简单信息显示。

开源硬件2025-11-19T01:50:53.145Z★ 18
查看原始项目 ↗
EDA_Info-Helper

项目文档

项目简介🪄

项目介绍

本项目基于ESP8266开发,借助嘉立创EDA API实现实时将嘉立创EDA信息显示至嵌入式设备上,本项目需要适当的C/C++、Html、CSS、JavaScript/TypeScript、Python语言基础,本项目偏软件及物联网方向。

在本项目中我们需要制作三个产物:

  • 硬件设备(C/C++)。
  • 中转转发服务上位机(Python)。
  • 嘉立创EDA插件(Html、CSS、JavaScript/TypeScript)

项目功能

本项目得益于ESP8266的WIFI功能,其实现原理是先通过嘉立创EDA API通过定时器实时监听用户鼠标的点击操作,将点击操作返回的属性信息通过外部请求API发送至本地中转服务,再通过中转服务将数据POST请求至硬件设备IP地址,由ESP8266 WEB服务监听接收POST请求,最后通过显示至屏幕上。

当然,你可能会疑惑为什么不能直接通过外部请求API去POST到硬件设备,这是因为存在安全问题不允许跨IP请求,所以这里只能通过创建一个转发服务来完成跨IP数据转发。

硬件固件功能
  • ✅ WIFI局域网配网
  • ✅ 自动记录及联网
  • ✅ 获取数据并显示至屏幕
转发服务功能
  • ✅ 接收本地5012端口数据,转发至指定IP地址
嘉立创EDA插件功能
  • ✅ 监听鼠标点击事件,获取属性信息
  • ✅ 逻辑处理及信息解析
  • ✅ 数据发送至本地5012端口

项目参数🔮

  • 采用ESP8266作为主控,ESP8266价格低廉且自带WIFI网络功能
  • 屏幕采用ST7789驱动的2.4寸屏幕,240*320分辨率
  • 电池采用单节14500锂电池,满电电压4.2V
  • 充电方案采用TP4056芯片,最大1A充电电流

硬件设计

主控电路
主控选用ESP8266主控,成本低廉,IO接口刚好足够

    image.png

查看数据手册 前往立创商城
屏幕电路
屏幕这里我们选用的是2.4寸240*320LCD屏,该尺寸的屏幕有两种驱动芯片,本项目使用的是ST7789驱动芯片,当然你也可以用ILI9341驱动芯片,需要修改TFT-espid的驱动定义即可。这里推荐购买IPS面板的屏幕,IPS面板相对于TFT面板能获得更好的视觉体验。

(选用屏幕时切记确保屏幕线序与PCB焊盘线序匹配)

    ST7789屏幕模组模组 原理图
    image.png image.png

    (本项目未添加触控功能,无需购买带触控面板的屏幕,当然你可以自己拓展触控功能)

    屏幕信息 引脚定义
    image.png image.png
电池供电电路

电池供电电路中选用TP4056锂电池充电芯片,电池选用14500单节满电电压4.2V锂电池。项目中通过TYPEC接口的5V进行充电。

(项目电路没有添加保护功能,选用电池时确认电池带保护电路板)

    (TP4056选用1A挡位充电时建议添加散热片) image.png

    (为方便没有加热台和风枪的用户焊接TP4056,在焊盘背面做了开窗和插件焊盘孔,你可以在孔内插入锡丝并用烙铁融化即可)

    焊盘正面 焊盘背面
    image.png image.png
    查看数据手册 前往立创商城
稳压电路
稳压芯片选用ME6217C33M5G,将电池电压降至3.3V,提供稳定电压,及电流

    image.png

查看数据手册 前往立创商城
电量检测电路
ESP8266的ADC引脚还没用上,所以这里我们再加一个ADC电量检测电路

    image.png

其他外设电路

其他外设中有烧录接口、总开关及供电接口。

    image.png

    image.png

    image.png

嵌入式软件开发

3c9927e3-cf2b-407d-9c59-3dce8c2121b0.jpg

开发环境
  • 软件环境:VSCode+PlatformIO
  • 开发语言:C/C++
依赖库
通过以下开源库协助本项目开发
  • TFT_eSPI:用于屏幕显示驱动
  • ArduinoJson:解析Json数据
  • ESPAsyncWebServer-esphome:构建异步WEB服务
main 主进程
本项目的嵌入式软件功能比较简单,只需要接收/api路径下数据并解析显示至ST7789屏幕,所以这里直接用裸机开发实现,而不使用OS,UI部分则是由TFT-eSPI实现,逻辑默认仅当数据变化下才更新,避免频繁刷屏导致频闪。

Markdown 语法解析会导致部分内容消失,完整源码建议查看附件

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "image.cpp"
AsyncWebServer server(80);// 设置服务器端口
TFT_eSPI tft = TFT_eSPI();
const char *ssidFile = "/ssid.json"; // 配置存储文件
String ipAddress = "Not Connected";
String lastMode = "";
String lastprimitiveType = "";
String lastIP = "";
String lastname1 = "";
String currentname1 = "--";
String lastdata1 = "";
String currentdata1 = "--";
String lastname2 = "";
String currentname2 = "--";
String lastdata2 = "";
String currentdata2 = "--";
String lastname3 = "";
String currentname3 = "--";
String lastdata3 = "";
String currentdata3 = "--";
String lastname4 = "";
String currentname4 = "--";
String lastdata4 = "";
String currentdata4 = "--";
// AP 默认热点配置
const char *ap_ssid = "EDA-InfoHelper";
const char *ap_password = "";
String currentMode = "--";
String currentprimitiveType = "--";
bool wifiBlink = true;
int temp = 26;
int humi = 45;

// ================== UI 提示函数 ==================
void showMessage(String msg, uint16_t color = TFT_WHITE) {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(color, TFT_BLACK);
  tft.drawCentreString(msg, 160, 120, 4);
}

// ================== 原始 UI 元素 ==================
void drawHeader() {
  tft.fillRect(0, 0, 320, 20, TFT_NAVY);
  tft.setTextColor(TFT_WHITE, TFT_NAVY);
  tft.drawString("EDA-InfoHelper", 5, 5, 2);


  if (WiFi.status() == WL_CONNECTED) {
    tft.setTextColor(TFT_GREEN, TFT_NAVY);
    tft.drawRightString(ipAddress, 230, 5, 2);
  } else {
    tft.setTextColor(TFT_GREEN, TFT_NAVY);
    tft.drawRightString(ipAddress, 230, 5, 2);
    if (wifiBlink) {
      tft.fillRect(290, 5, 5, 14, TFT_CYAN);
      tft.fillRect(300, 9, 5, 10, TFT_CYAN);
      tft.fillRect(310, 13, 5, 6, TFT_CYAN);
    } else {
      tft.fillRect(280, 0, 40, 20, TFT_NAVY);
    }
  }
}

void drawCard(int x, int y,uint16_t color,String mode) {
  tft.fillRoundRect(x, y, 80, 80, 8, color);
  if(mode == "PCB")
  {

    /* code */
    tft.pushImage(x+3, y+3, 75, 75, pcb);
   }
  else if(mode == "SCH")
  {
    /* code */
    tft.pushImage(x+3, y+3, 75, 75, sch);

  }else{
    tft.pushImage(x+3, y+3, 75, 75, eda);
  }
 
}

void drawTextCard(int x, int y, String title, String value, uint16_t color) {
  tft.fillRoundRect(x, y, 210, 35, 8, color);
  tft.drawRoundRect(x, y, 210, 35, 8, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, color);
  tft.drawString(title, x + 8, y + 6, 2);
  tft.setTextColor(TFT_YELLOW, color);
  tft.drawRightString(value, x + 200, y + 5, 4);
}


void drawTextCard2(int x, int y, String title, String value, uint16_t color) {
  tft.fillRoundRect(x, y, 145, 50, 8, color);
  tft.drawRoundRect(x, y, 145, 50, 8, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, color);
  tft.drawString(title, x + 8, y + 6, 2);
  tft.setTextColor(TFT_YELLOW, color);
  tft.drawRightString(value, x + 130, y + 25, 2);
}

// ================== 保存 WiFi 配置 ==================
void saveWiFiConfig(String ssid, String pass) {
  DynamicJsonDocument doc(256);
  doc["ssid"] = ssid;
  doc["pass"] = pass;
  fs::File file = SPIFFS.open(ssidFile, "w");
  if (file) {
    serializeJson(doc, file);
    file.close();
  }
}

// ================== 加载 WiFi 配置 ==================
bool loadWiFiConfig(String &ssid, String &pass) {
  if (!SPIFFS.exists(ssidFile)) return false;
  fs::File file = SPIFFS.open(ssidFile, "r");
  if (!file) return false;
  DynamicJsonDocument doc(256);
  if (deserializeJson(doc, file)) return false;
  ssid = doc["ssid"].as();
  pass = doc["pass"].as();
  return true;
}

// ================== WiFi STA 连接函数 ==================
bool connectSTA(String ssid, String pass, int timeout = 10000) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), pass.c_str());

  showMessage("Wait connect WIFI...", TFT_YELLOW);

  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < timeout) {
    delay(3000);
  }

  if (WiFi.status() == WL_CONNECTED) {
    ipAddress = WiFi.localIP().toString();
    return true;
  } else {
    return false;
  }
}

// ================== AP 模式启动 ==================
void startAP() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ap_ssid, ap_password);
  showMessage("Wait connect WIFI...", TFT_CYAN);
}

// ================== 路由配置 ==================
void handleWiFiConfig() {
    server.on("/api", HTTP_POST, [](AsyncWebServerRequest *request) {
  if (request->hasParam("mode", true) && request->hasParam("primitiveType", true)) {
    currentMode = request->getParam("mode", true)->value();
    currentprimitiveType = request->getParam("primitiveType", true)->value();
    currentname1 = request->getParam("name1", true)->value();
    currentdata1 = request->getParam("data1", true)->value();
    currentname2= request->getParam("name2", true)->value();
    currentdata2 = request->getParam("data2", true)->value();
    currentname3 = request->getParam("name3", true)->value();
    currentdata3 = request->getParam("data3", true)->value();
    currentname4 = request->getParam("name4", true)->value();
    currentdata4 = request->getParam("data4", true)->value();
    request->send(200, "application/json", "{\"status\":\"ok\"}");
  } else {
    request->send(400, "application/json", "{\"error\":\"missing parameters\"}");
  }
});

server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request) {
    // 获取POST参数:ssid、pass、city、api
    String ssid = request->getParam("ssid", true)->value();
    String pass = request->getParam("pass", true)->value();
    // 打印接收到的参数
    Serial.println(ssid);
    Serial.println(pass);

    // 保存WiFi信息到JSON文件
    DynamicJsonDocument doc(1024);
    doc["ssid"] = ssid;
    doc["pass"] = pass;


    fs::File file = SPIFFS.open(ssidFile, "w");  // 打开文件进行写入
    if (file) {
        serializeJson(doc, file);  // 将JSON内容写入文件
        file.close();  // 关闭文件
        Serial.println("WiFi信息保存成功,即将重启...");
    } else {
        Serial.println("无法打开文件保存WiFi信息!");
        request->send(500, "application/json", "{\"error\":\"无法保存WiFi信息\"}");
        return;
    }

    // 发送响应告知用户即将重启
    request->send(200, "text/html; charset=UTF-8", 
        ""
        ""
        ""
        "    "
        "    状态"
        ""
        ""
        "    <h1>WiFi配置已保存,设备正在重启...</h1>"
        ""
        ""
    );

    delay(500); // 确保响应发送出去
    ESP.restart(); // 重启设备
});


    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        // 检查SPIFFS文件系统中是否存在index.html文件
        if (SPIFFS.exists("/index.html")) {
            fs::File file = SPIFFS.open("/index.html", "r");  // 打开index.html文件
            if (file) {
                size_t fileSize = file.size();  // 获取文件大小
                String fileContent;

                // 逐字节读取文件内容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 关闭文件
                // 返回HTML内容
                request-&gt;send(200, "text/html", fileContent);
                return;
            }
        }
        // 如果文件不存在,返回404错误
        request-&gt;send(404, "text/plain", "File Not Found"); });
  
    server.on("/setting.html", HTTP_GET, [](AsyncWebServerRequest *request)
              {
        // 检查SPIFFS文件系统中是否存在index.html文件
        if (SPIFFS.exists("/setting.html")) {
            fs::File file = SPIFFS.open("/setting.html", "r");  // 打开index.html文件
            if (file) {
                size_t fileSize = file.size();  // 获取文件大小
                String fileContent;

                // 逐字节读取文件内容
                while (file.available()) {
                    fileContent += (char)file.read();
                }
                file.close();  // 关闭文件

                // 返回HTML内容
                request-&gt;send(200, "text/html", fileContent);
                return;
            }
        }
        // 如果文件不存在,返回404错误
        request-&gt;send(404, "text/plain", "File Not Found"); });
    // 启动服务器
    server.begin();
}

// ================== 主流程 ==================
void setup() {
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  Serial.begin(115200);
  SPIFFS.begin();

  String ssid, pass;

  if (loadWiFiConfig(ssid, pass)) {
    if (!connectSTA(ssid, pass)) {
      startAP();
    }
  } else {
    startAP();
  }

  handleWiFiConfig();

  if (WiFi.status() == WL_CONNECTED) {
    tft.fillScreen(TFT_BLACK);
    drawHeader();
    
    drawCard(10, 30, TFT_WHITE,"EDA");
    drawTextCard(100, 30, "mode", "JLC_EDA", TFT_GREEN);
    drawTextCard(100, 75, "primitiveType", "ABC", TFT_SKYBLUE);
    drawTextCard2(10, 120, "name1", "data1", TFT_ORANGE);
    drawTextCard2(165, 120, "name2", "data2", TFT_ORANGE);
    drawTextCard2(10, 180, "name3", "data3", TFT_MAGENTA);
    drawTextCard2(165, 180, "name4", "data4", TFT_MAGENTA);
  }
}
void loop() {
  // WiFi 状态刷新(顶部 header)
  static unsigned long lastBlink = 0;
  if (millis() - lastBlink &gt; 500) {
    wifiBlink = !wifiBlink;

    String currentIP = WiFi.localIP().toString();
    // 仅当IP发生变化或wifiBlink状态改变时才刷新header
    if (currentIP != lastIP) {
      ipAddress = currentIP;
      drawHeader();
      lastIP = currentIP;
    } else if (wifiBlink) {
      drawHeader(); // 保持闪烁动画
    }

    lastBlink = millis();
  }

  // 实时更新 mode/data(仅当发生变化时)
  if (WiFi.status() == WL_CONNECTED) {
    if (currentMode != lastMode) {
      drawTextCard(100, 30, "mode", currentMode, TFT_GREEN);
        drawCard(10, 30, TFT_WHITE,currentMode);

      lastMode = currentMode;
    }
    if (currentprimitiveType != lastprimitiveType) {
      drawTextCard(100, 75, "primitiveType", currentprimitiveType, TFT_SKYBLUE);
      lastprimitiveType = currentprimitiveType;
    }
    if (currentdata1 != lastdata1) {
      if (currentdata1.length() &gt; 16) {
      currentdata1 = currentdata1.substring(0, 16);
    }
        drawTextCard2(10, 120, currentname1, currentdata1, TFT_ORANGE);
    
      lastname1 = currentname1;
    
      lastdata1 = currentdata1;
    }
   if (currentdata2 != lastdata2) {
     if (currentdata2.length() &gt; 16) {
      currentdata2 = currentdata2.substring(0, 16);
    }
      drawTextCard2(165, 120,currentname2,currentdata2, TFT_ORANGE);
      lastname2 = currentname2;
   
      lastdata2 = currentdata2;
    }
   if (currentdata3 != lastdata3) {
     if (currentdata3.length() &gt; 16) {
      currentdata3 = currentdata3.substring(0, 16);
    }
      drawTextCard2(10, 180, currentname3, currentdata3, TFT_MAGENTA);
      lastname3 = currentname3;
  
      lastdata3 = currentdata3;
    }
   if (currentdata4 != lastdata4) {
     if (currentdata4.length() &gt; 16) {
      currentdata4 = currentdata4.substring(0, 16);
    }
      drawTextCard2(165, 180, currentname4, currentdata4, TFT_MAGENTA);
      lastname4 = currentname4;
  
      lastdata4 = currentdata4;
    }
  }
  
}

上位机客户端软件开发

image.png

开发环境
  • 软件环境:VSCode
  • 开发语言:Python
依赖库
通过以下开源库协助本项目开发
  • Flask:用来快速搭建 HTTP 接口或 Web 应用
  • requests:用来发送 GET/POST 等请求
  • flask_cors (CORS):用来处理跨域请求问题
  • Pillow (PIL):托盘图标绘制
  • pystray:用于在系统托盘创建图标和菜单
Python主进程
因为只是转发服务,所以不需要什么UI,就选择Python快速开发,应用会在后台运行,可在系统托盘中关闭中转转发服务,通过5012端口/proxy/api路径的数据监听,把数据转发给传入的IP地址/api路径下。
import threading
from flask import Flask, request, jsonify
import requests
from flask_cors import CORS
from PIL import Image, ImageDraw
import pystray
import sys
import tkinter as tk
from tkinter import messagebox
app = Flask(__name__)
CORS(app)
# --- Flask 路由 ---
@app.route("/proxy/api", methods=["POST"])
def proxy_api():
    try:
        ip = request.form.get('ip', '127.0.0.1')
        mode = request.form.get('mode')
        primitiveType = request.form.get('primitiveType')
        name1 = request.form.get('name1')
        data1 = request.form.get('data1')
        name2 = request.form.get('name2')
        data2 = request.form.get('data2')
        name3 = request.form.get('name3')
        data3 = request.form.get('data3')
        name4 = request.form.get('name4')
        data4 = request.form.get('data4')
        if not all([ip, mode, primitiveType,name1,data1,name2,data2,name3,data3,name4,data4]):
            return {"success": False, "error": "缺少必要参数"}, 400
        target_url = f"http://{ip}/api"
        resp = requests.post(target_url,
                             data={"mode": mode, "primitiveType": primitiveType,
                                   "name1":name1,"data1":data1,
                                   "name2":name2,"data2":data2,
                                   "name3":name3,"data3":data3,
                                   "name4":name4,"data4":data4},
                             headers={"Content-Type": "application/x-www-form-urlencoded"},
                             timeout=5)
        resp.raise_for_status()
        return {"success": True, "esp_response": resp.text}
    except Exception as e:
        return {"success": False, "error": str(e)}, 500
# --- 托盘相关 ---
def create_image():
    img = Image.new('RGB', (64, 64), color='white')
    d = ImageDraw.Draw(img)
    d.rectangle([16, 16, 48, 48], fill='black')
    return img
def on_quit(icon, item):
    icon.stop()
    sys.exit(0)
def run_tray():
    icon = pystray.Icon("EDA-InfoHelper")
    icon.icon = create_image()
    icon.title = "EDA-InfoHelper"
    icon.menu = pystray.Menu(pystray.MenuItem("EDA-InfoHelper正在运行",None),pystray.MenuItem("退出", on_quit))
    icon.run()
# --- 弹窗提示 ---
def show_startup_message():
    root = tk.Tk()
    root.withdraw()  # 隐藏主窗口
    messagebox.showinfo("提示", "EDA-InfoHelper 已启动并隐藏到托盘")
    root.destroy()
# --- 启动 Flask 线程 ---
def run_flask():
    print("EDA-InfoHelper 转发服务已启动,端口号5012")
    app.run(host="0.0.0.0", port=5012, debug=False)
if __name__ == "__main__":
    # 弹窗提示
    show_startup_message()
    # 启动 Flask 后台线程
    threading.Thread(target=run_flask, daemon=True).start()
    # 显示托盘图标
    run_tray()


嘉立创EDA插件开发

image.png

开发环境
  • 软件环境:VSCode
  • 开发语言:Html/CSS/TypeScript
依赖库
通过以下开源库协助本项目开发
  • EDA-SDK:用于调用嘉立创EDA API
查看 API 开发文档
index.ts
index.ts是功能入口,创建的状态栏列表下按键通过此文件入口进入,这里我们通过定时器实现实时检测,同时将定时器id记录并存储,方便结束定时器。

import * as extensionConfig from '../extension.json';
import * as func from './function';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function activate(status?: 'onStartupFinished', arg?: string): void {}

export function about(): void {
	eda.sys_Dialog.showInformationMessage(
		eda.sys_I18n.text('EasyEDA extension SDK v', undefined, undefined, extensionConfig.version),
		eda.sys_I18n.text('About'),
	);
}
let timerId: ReturnType | null = null;
// 封装定时器函数

// 启动定时器
export async function start(): Promise {
	const configs = await eda.sys_Storage.getExtensionAllUserConfigs();

	if (configs.timerId) {
		console.log('定时器已存在:', configs.timerId);
		return;
	}

	const id = func.createDocumentTimer();

	// 保存到扩展配置
	await eda.sys_Storage.setExtensionAllUserConfigs({
		...configs,
		timerId: id,
	});

	console.log('start timerId =', id);
}

// 停止定时器
export async function stopa(): Promise {
	console.log('stop');
	const configs = eda.sys_Storage.getExtensionAllUserConfigs();
	const id = configs.timerId;

	console.log('stop timerId =', id);
	if (id !== undefined) {
		clearInterval(id);
		delete configs.timerId;
		await eda.sys_Storage.setExtensionAllUserConfigs(configs);
		console.log('循环已停止');
	}
}
export async function setting(): Promise {
	eda.sys_IFrame.openIFrame('/iframe/index.html', 450, 500);
}



function.ts
function.ts是存放的是功能实现,用于逻辑处理及在index.ts入门中调用实现的功能。
export function createDocumentTimer(): number {
	return setInterval(() =&gt; {
		eda.dmt_SelectControl
			.getCurrentDocumentInfo()
			.then(async (doc) =&gt; {
				if (doc) {
					console.log('用户当前在:' + doc.documentType);
					if (doc.documentType === 3) {
						// PCB
						const items = await eda.pcb_SelectControl.getSelectedPrimitives();
						if (items &amp;&amp; items.length &gt; 0) {
							items.forEach((i) =&gt; {
								const dataValue = i.pcbItemPrimitiveType ?? 0; // 如果 undefined 就用 0
								switch (dataValue) {
									case "Component":
										const name = i.device.name?? 0;
										const part = i.device.attrs["Supplier Part"]?? 0;
										const footprint = i.attrsMap["Footprint"] ?? 0;
										const globalIndex = i.globalIndex ?? 0;
										sendData("PCB", dataValue,"name",name,"part",part,"footprint",footprint,"globalIndex",globalIndex);
										break;
									case "Pad":
										const net = i.net ?? 0;
										const num = i.num ?? 0;
										const layerId = i.layerId ?? 0;
										const globalIndex2 = i.globalIndex ?? 0;
										sendData("PCB", dataValue,"net",net,"num",num,"layerId",layerId,"globalIndex",globalIndex2);
										break;
									case "Text":
										const formulaName = i.formulaName ?? 0;
										const fontSize = i.fontSize ?? 0;
										const key = i.key ?? 0;
										const globalIndex3 = i.globalIndex ?? 0;
										sendData("PCB", dataValue,"formulaName", formulaName,"fontSize",fontSize,"key",key,"globalIndex",globalIndex3);
										break;
									case "Rect":
										const height = i.height ?? 0;
										const width = i.width ?? 0;
										const type = i.type ?? 0;
										const globalIndex4 = i.globalIndex ?? 0;
										sendData("PCB", dataValue,"height",height,"width",width,"type",type,"globalIndex",globalIndex4);
										break;
									case "Via":
										const net2 = i.net ?? 0;
										const holeRadius = i.holeRadius ?? 0;
										const radius = i.radius ?? 0;
										const globalIndex5 = i.globalIndex ?? 0;
										sendData("PCB", dataValue,"net",net2,"holeRadius",holeRadius,"radius",radius,"globalIndex",globalIndex5);
										break;
									case "Polygon":
										const strokeWidth = i.strokeWidth ?? 0;
										const type2 = i.type ?? 0;
										const layerId2 = i.layerId ?? 0;
										const globalIndex6 = i.globalIndex ?? 0;
										sendData("PCB", dataValue,"strokeWidth",strokeWidth,"type",type2,"layerId",layerId2,"globalIndex",globalIndex6);
										break;
									default:
										break;
								}

							});
						}
					} else if (doc.documentType === 1) {
						// SCH
						const items = await eda.sch_SelectControl.getSelectedPrimitives();
						if (items &amp;&amp; items.length &gt; 0) {
							items.forEach((i) =&gt; {
								const dataValue = i.primitiveType ?? 0; // 防止 undefined
								switch (dataValue) {
									case "Component":

										const designator = i.param.designator?? 0;
										const uniqueId = i.param.uniqueIdt?? 0;
										const id = i.id ?? 0;
										sendData("SCH", dataValue,"designator",designator,"uniqueId",uniqueId,"id",id,"---","---");
										break;
									case "Net Port":
									case "Wire":
									case "Net Flag":
										const net = i.param.net ?? 0;
										const id2 = i.id ?? 0;
										sendData("SCH", dataValue,"net",net,"id",id2,"---","---","---","---");
										break;
									case "Property":
									case "Text":
										const id3 = i.id ?? 0;
										sendData("SCH", dataValue,"id",id3,"---","---","---","---","---","---");
										break;
									case "Name":

										const id4 = i.id ?? 0;
										sendData("SCH", dataValue,"id",id4,"---","---","---","---","---","---");
										break;

									default:
										break;
								}
							});
						}
					}
				} else {
					console.log('当前没有选中文档');
				}
			})
			.catch((err) =&gt; {
				console.error('获取文档信息失败:', err);
			});
	}, 1000);
}
function sendData(mode: String, primitiveType: String,name1:String,data1:String,name2:String,data2:String,name3:String,data3:String,name4:String,data4:String) {
	const params = new URLSearchParams();

	// 获取已存储的IP地址
	const configs = eda.sys_Storage.getExtensionAllUserConfigs();
	const ip = configs.ip || '127.0.0.1';
	params.append('ip', ip); // 新增IP参数
	params.append('mode', mode.toString());
	params.append('primitiveType', primitiveType.toString());
	params.append('name1', name1.toString());
	params.append('data1', data1.toString());
	params.append('name2', name2.toString());
	params.append('data2', data2.toString());
	params.append('name3', name3.toString());
	params.append('data3', data3.toString());
	params.append('name4', name4.toString());
	params.append('data4', data4.toString());
	eda.sys_ClientUrl
		.request(
			'http://127.0.0.1:5012/proxy/api',
			'POST',
			params.toString(),
			{
				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
			},
			(res) =&gt; {
				console.log('数据发送成功', res);
			},
		)
		.catch((err) =&gt; {
			console.error('请求失败:', err);
		});
}

declare global {
	interface ExtensionConfigs {
		timerId?: number;
		ip?: string; // 新增IP字段
	}
}

index.html
index.html是设置页面的前端展示,用于用户下载中转服务及输入目标硬件设备的IP,IP会存储至插件内部存储,便于API请求,避免重复配置。

<div>

	<div>
		<h3>EDA-InfoHelperServer中转服务工具</h3>
		<p>在使用本插件的同时,您还需下载并启动 EDA-InfoHelperServer中转服务工具,通过本工具把POST数据传输至硬件设备。</p>
		下载中转服务工具
	</div>
	<div>
		<h3>绑定EDA-InfoHelper的IP地址</h3>
		<p>中转服务工具启动后,还需在此填入您的EDA-InfoHelper设备IP地址,您的硬件联网后硬件设备IP地址会显示在硬件设备的顶部导航栏上。</p>
		<input type="text" id="ipAddress" />
		保存设置
	</div>
</div>

Markdown 语法解析会导致部分内容消失,详细内容建议看附件文件

3D外壳结构

前盖
正面 背面
38bc126c-df86-442e-b26c-fd16aa91861e.png fa3eff3a-ca92-4ac0-a093-7c3be60440b6.png
  • 内壁设置有限位槽,为按键预留空间。
  • 屏幕部分设有隔离层,避免屏幕接触PCB焊盘导致短路
  • 按键及TypeC开槽
后盖
正面 背面
6193ef6f-a4dc-4f39-982b-9582145e407e.png 98492ebc-46b8-4aa9-ab01-8ea4f86be122.png
  • 后盖部分采用异形结构
  • 同样圆角处理
  • 为烧录接口开槽

安装结构

项目采用的是双外壳结构,由前盖、后盖构成

39b639a3-c6d0-4488-95a0-5cce83414d09.png

免费打样
嘉立创提供一站式产业互联智造平台,涵盖电子、机械产业一站式服务🛠️
嘉立创免费PCB打样 嘉立创免费3D打样
实机数据流程演示
    EDA插件配置IP 点击器件
    image.png 企业微信截图_1756188012583.png
    EDA发送数据 中转服务接收及转发
    image.png image.png
    硬件接收PCB数据显示 硬件接收原理图数据显示
    fa0cfce4-c27c-4f76-a8cf-d83b1e5cf296.jpg b780fbf5-e497-4c52-9437-89695746a02d.jpg
    PCB数据显示总流程 原理图数据显示总流程
    3e785398-4c74-44c8-a985-2e53a17bbd3e.jpg 6638940f-fa2a-479e-8260-d4add117bec2.jpg
拓展方向
本项目中提供了嘉立创EDA-API的一种扩展及交互方向,你可以基于本项目扩展一个结合嘉立创EDA的交互设备。硬件中还支持ADC电量检测和两个按键外设,可以按需进行功能扩展。
  • 一键DRC报告
  • 在EDA中显示硬件信息
  • 天气、时钟显示
  • ......
嘉立创EDA扩展广场
嘉立创EDA扩展广场已经上线,在扩展广场中可以发现各种实用好玩的扩展插件,当然如果你自己也开发了一些有趣的插件,欢迎上架至扩展广场,共建嘉立创EDA扩展插件生态,丰富EDA软件功能。
访问扩展广场 下载EDA-InfoHelper扩展
使用说明

附件"EDA-InfoHelper资源包"包含所有编译后产物、3D文件及硬件工程,源码文件则单独打包在附件中

硬件烧录请参考: EDA-Robot机器狗 烧录说明 插件安装请参考: 嘉立创EDA扩展广场_EDA-InfoHelper扩展插件

1.为硬件上电并连接“EDA-InfoHelp” 2.4G热点。
2.连接成功后进入192.168.4.1完成配网(配置2.4G频段网络)
3.设备联网后会在顶部状态栏显示IP地址,记录下这个IP
4.在EDA-设置-扩展-扩展管理器导入eda-info-helper.eext插件,并允许外部交互。
5.进入PCB或原理图页面,点击顶部状态栏InfoHelper-设置页面 6.填入记录的IP地址并下载中转服务工具。 7.打开中转服务工具即可
现在,点击任意器件即可显示信息了。

首页/硬件项目