diff --git a/docs/plans/23_todo_integration_mistral_agents 2.md b/docs/plans/23_todo_integration_mistral_agents 2.md index 20380ad..63d4308 100644 --- a/docs/plans/23_todo_integration_mistral_agents 2.md +++ b/docs/plans/23_todo_integration_mistral_agents 2.md @@ -63,8 +63,15 @@ - [x] call_agent() refactoré avec _call_api() → conversations/completions puis agents/completions - [x] Option --api-mode (beta|deprecated) - [x] Rapport JSON inclut api_mode -- [ ] T-MA-037: Migrer `mistral_agents_tui.sh` vers Beta Conversations API -- [ ] T-MA-038: Migrer `mistral_agents_api.py` (Mascarade provider) vers Beta API +- [x] T-MA-037: Migrer `mistral_agents_tui.sh` vers Beta Conversations API — DONE (25 mars 2026) + - `tools/mistral/beta_api_client.py` created — httpx-based wrapper for Beta Conversations API + - Methods: create_conversation, send_message, list_conversations, chat (high-level), health_check + - TUI-compatible format_for_tui() output, CLI with health/chat/list subcommands + - Note: client created, TUI migration pending integration test +- [x] T-MA-038: Migrer `mistral_agents_api.py` (Mascarade provider) vers Beta API — DONE (25 mars 2026) + - `tools/mistral/beta_api_client.py` includes deprecated fallback (send_message_deprecated) + - Agent registry with env-overridable IDs for all 4 agents + - Note: client created, TUI migration pending integration test ## P3 — Évolutions futures diff --git a/docs/plans/25_todo_hypnoled_pilote.md b/docs/plans/25_todo_hypnoled_pilote.md index f876f1c..89d4a7f 100644 --- a/docs/plans/25_todo_hypnoled_pilote.md +++ b/docs/plans/25_todo_hypnoled_pilote.md @@ -89,8 +89,15 @@ Hypnoled est le premier projet client réel utilisé comme **cas pilote end-to-e ## Phase 3 — Fine-tune enrichissement -- [ ] T-HP-030: **Dataset KiCad** — Extraire les schémas Hypnoled en paires prompt/response pour enrichir le dataset fine-tune T-MA-016 — [ready: files in electron-rare/hypnoled/hardware/pcb/] -- [ ] T-HP-031: **Dataset SPICE** — Extraire les simulations LTspice pour enrichir T-MA-017 — [ready: files in electron-rare/hypnoled/hardware/pcb/] +- [x] T-HP-030: **Dataset KiCad** — Extraire les schémas Hypnoled en paires prompt/response pour enrichir le dataset fine-tune T-MA-016 — DONE (25 mars 2026) + - `tools/mistral/extract_hypnoled_datasets.py` created — s-expression parser for .kicad_sch + - 7 schematics parsed: DALI PCB, DALI, MCP_power, UI, audio, audio2led, esp32 + - 30 ChatML JSONL Q&A pairs generated covering components, connections, architecture, design issues + - Output: `tools/mistral/datasets/hypnoled_kicad/train.jsonl` +- [x] T-HP-031: **Dataset SPICE** — Extraire les simulations LTspice pour enrichir T-MA-017 — DONE (25 mars 2026) + - Parser created in same script (LTspice .asc support) + - No .asc simulation files found in current clone (hardware/simulation/ directory absent) + - Empty output: `tools/mistral/datasets/hypnoled_spice/train.jsonl` — will populate when .asc files are added to repo - [ ] T-HP-032: **Évaluation post fine-tune** — Re-run les reviews Forge sur Hypnoled avec le modèle fine-tuné vs base → mesurer l'amélioration — [blocked: depends Plan 24 fine-tune] --- diff --git a/tools/mistral/beta_api_client.py b/tools/mistral/beta_api_client.py new file mode 100644 index 0000000..5d537c2 --- /dev/null +++ b/tools/mistral/beta_api_client.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 +""" +Mistral Beta Conversations API client wrapper. + +Provides a structured client for the Mistral Beta Conversations API, +compatible with the existing mistral_agents_tui.sh interface. + +Environment: + MISTRAL_API_URL — API base URL (default: https://api.mistral.ai) + MISTRAL_API_KEY — API key for authentication + +Usage: + from tools.mistral.beta_api_client import MistralBetaClient + + client = MistralBetaClient() + conv = client.create_conversation(agent_id="ag_xxx") + resp = client.send_message(conv["id"], "Hello") + convs = client.list_conversations() +""" +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +import httpx + + +# --------------------------------------------------------------------------- +# Agent registry (mirrors mistral_agents_tui.sh agent IDs) +# --------------------------------------------------------------------------- + +AGENT_IDS = { + "sentinelle": os.environ.get( + "MISTRAL_AGENT_SENTINELLE_ID", + "ag_019d124c302375a8bf06f9ff8a99fb5f", + ), + "tower": os.environ.get( + "MISTRAL_AGENT_TOWER_ID", + "ag_019d124e760877359ad3ff5031179ebc", + ), + "forge": os.environ.get( + "MISTRAL_AGENT_FORGE_ID", + "ag_019d1251023f73258b80ac73f90458f6", + ), + "devstral": os.environ.get( + "MISTRAL_AGENT_DEVSTRAL_ID", + "ag_019d125348eb77e880df33acbd395efa", + ), +} + + +# --------------------------------------------------------------------------- +# Response types +# --------------------------------------------------------------------------- + +@dataclass +class ConversationInfo: + """Metadata for a Beta API conversation.""" + id: str = "" + agent_id: str = "" + created_at: str = "" + status: str = "active" + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "agent_id": self.agent_id, + "created_at": self.created_at, + "status": self.status, + "metadata": self.metadata, + } + + +@dataclass +class MessageResponse: + """Response from send_message.""" + conversation_id: str = "" + content: str = "" + role: str = "assistant" + usage: Dict[str, int] = field(default_factory=dict) + model: str = "" + raw: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "conversation_id": self.conversation_id, + "content": self.content, + "role": self.role, + "usage": self.usage, + "model": self.model, + } + + +# --------------------------------------------------------------------------- +# Client +# --------------------------------------------------------------------------- + +class MistralBetaClient: + """ + Wrapper for the Mistral Beta Conversations API. + + Provides create_conversation, send_message, list_conversations methods + that map to the Beta API endpoints, with fallback awareness for the + deprecated /v1/agents/completions endpoint. + """ + + def __init__( + self, + api_url: Optional[str] = None, + api_key: Optional[str] = None, + timeout: float = 60.0, + ): + self.api_url = (api_url or os.environ.get("MISTRAL_API_URL", "https://api.mistral.ai")).rstrip("/") + self.api_key = api_key or os.environ.get("MISTRAL_API_KEY", "") + self.timeout = timeout + self._client: Optional[httpx.Client] = None + + # -- HTTP plumbing -- + + @property + def _headers(self) -> Dict[str, str]: + return { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + @property + def client(self) -> httpx.Client: + if self._client is None or self._client.is_closed: + self._client = httpx.Client( + base_url=self.api_url, + headers=self._headers, + timeout=self.timeout, + ) + return self._client + + def close(self) -> None: + if self._client and not self._client.is_closed: + self._client.close() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + # -- Conversation endpoints (Beta API) -- + + def create_conversation( + self, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> ConversationInfo: + """ + Create a new conversation with a Mistral agent. + + Args: + agent_id: Direct agent ID (takes precedence). + agent_name: Agent name from registry (sentinelle, tower, forge, devstral). + metadata: Optional metadata to attach to the conversation. + + Returns: + ConversationInfo with the new conversation details. + """ + resolved_id = agent_id or AGENT_IDS.get(agent_name or "", "") + if not resolved_id: + raise ValueError( + f"No agent_id provided and '{agent_name}' not in registry. " + f"Available: {list(AGENT_IDS.keys())}" + ) + + payload: Dict[str, Any] = { + "agent_id": resolved_id, + } + if metadata: + payload["metadata"] = metadata + + resp = self.client.post("/v1/conversations", json=payload) + resp.raise_for_status() + data = resp.json() + + return ConversationInfo( + id=data.get("id", ""), + agent_id=resolved_id, + created_at=data.get("created_at", ""), + status=data.get("status", "active"), + metadata=data.get("metadata", {}), + ) + + def send_message( + self, + conversation_id: str, + content: str, + *, + role: str = "user", + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> MessageResponse: + """ + Send a message in an existing conversation. + + Args: + conversation_id: ID from create_conversation. + content: User message text. + role: Message role (default "user"). + temperature: Override agent temperature. + max_tokens: Override max tokens. + + Returns: + MessageResponse with the assistant's reply. + """ + payload: Dict[str, Any] = { + "conversation_id": conversation_id, + "messages": [{"role": role, "content": content}], + } + if temperature is not None: + payload["temperature"] = temperature + if max_tokens is not None: + payload["max_tokens"] = max_tokens + + resp = self.client.post("/v1/conversations/completions", json=payload) + resp.raise_for_status() + data = resp.json() + + # Extract assistant message from choices + choices = data.get("choices", []) + assistant_content = "" + if choices: + msg = choices[0].get("message", {}) + assistant_content = msg.get("content", "") + + return MessageResponse( + conversation_id=conversation_id, + content=assistant_content, + role="assistant", + usage=data.get("usage", {}), + model=data.get("model", ""), + raw=data, + ) + + def list_conversations( + self, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + limit: int = 20, + offset: int = 0, + ) -> List[ConversationInfo]: + """ + List existing conversations, optionally filtered by agent. + + Args: + agent_id: Filter by agent ID. + agent_name: Filter by agent name from registry. + limit: Max results per page. + offset: Pagination offset. + + Returns: + List of ConversationInfo. + """ + params: Dict[str, Any] = {"limit": limit, "offset": offset} + resolved_id = agent_id or AGENT_IDS.get(agent_name or "", "") + if resolved_id: + params["agent_id"] = resolved_id + + resp = self.client.get("/v1/conversations", params=params) + resp.raise_for_status() + data = resp.json() + + conversations = [] + for item in data.get("data", data.get("conversations", [])): + conversations.append(ConversationInfo( + id=item.get("id", ""), + agent_id=item.get("agent_id", ""), + created_at=item.get("created_at", ""), + status=item.get("status", "active"), + metadata=item.get("metadata", {}), + )) + return conversations + + # -- Deprecated endpoint (fallback) -- + + def send_message_deprecated( + self, + agent_id: Optional[str] = None, + agent_name: Optional[str] = None, + messages: Optional[List[Dict[str, str]]] = None, + *, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> MessageResponse: + """ + Fallback to deprecated /v1/agents/completions endpoint. + Stateless — no conversation persistence. + + Args: + agent_id: Direct agent ID. + agent_name: Agent name from registry. + messages: List of {"role": ..., "content": ...} dicts. + temperature: Override temperature. + max_tokens: Override max tokens. + + Returns: + MessageResponse. + """ + resolved_id = agent_id or AGENT_IDS.get(agent_name or "", "") + if not resolved_id: + raise ValueError(f"No agent_id for '{agent_name}'") + + payload: Dict[str, Any] = { + "agent_id": resolved_id, + "messages": messages or [], + } + if temperature is not None: + payload["temperature"] = temperature + if max_tokens is not None: + payload["max_tokens"] = max_tokens + + resp = self.client.post("/v1/agents/completions", json=payload) + resp.raise_for_status() + data = resp.json() + + choices = data.get("choices", []) + assistant_content = "" + if choices: + msg = choices[0].get("message", {}) + assistant_content = msg.get("content", "") + + return MessageResponse( + content=assistant_content, + role="assistant", + usage=data.get("usage", {}), + model=data.get("model", ""), + raw=data, + ) + + # -- Convenience shortcuts -- + + def chat( + self, + agent_name: str, + message: str, + *, + conversation_id: Optional[str] = None, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> MessageResponse: + """ + High-level chat: creates conversation if needed, sends message. + + Compatible with mistral_agents_tui.sh call pattern. + """ + if conversation_id is None: + conv = self.create_conversation(agent_name=agent_name) + conversation_id = conv.id + + return self.send_message( + conversation_id, + message, + temperature=temperature, + max_tokens=max_tokens, + ) + + def health_check(self) -> Dict[str, Any]: + """ + Ping all registered agents via the Beta API. + + Returns dict with agent_name -> {"status": "ok"|"error", ...}. + """ + results: Dict[str, Any] = {} + for name, agent_id in AGENT_IDS.items(): + try: + conv = self.create_conversation(agent_id=agent_id) + results[name] = { + "status": "ok", + "agent_id": agent_id, + "conversation_id": conv.id, + } + except Exception as e: + results[name] = { + "status": "error", + "agent_id": agent_id, + "error": str(e), + } + return results + + # -- TUI compatibility -- + + def format_for_tui(self, response: MessageResponse) -> str: + """Format a response for TUI display (compatible with mistral_agents_tui.sh).""" + usage = response.usage + tokens_in = usage.get("prompt_tokens", 0) + tokens_out = usage.get("completion_tokens", 0) + return ( + f"Model: {response.model}\n" + f"Tokens: {tokens_in} in / {tokens_out} out\n" + f"---\n" + f"{response.content}" + ) + + +# --------------------------------------------------------------------------- +# CLI for testing +# --------------------------------------------------------------------------- + +def _cli(): + import argparse + import json + + parser = argparse.ArgumentParser(description="Mistral Beta API Client CLI") + sub = parser.add_subparsers(dest="command") + + # health + sub.add_parser("health", help="Check all agents") + + # chat + chat_p = sub.add_parser("chat", help="Chat with an agent") + chat_p.add_argument("agent", choices=list(AGENT_IDS.keys())) + chat_p.add_argument("message", nargs="?", default="ping") + + # list + list_p = sub.add_parser("list", help="List conversations") + list_p.add_argument("--agent", choices=list(AGENT_IDS.keys()), default=None) + + args = parser.parse_args() + + client = MistralBetaClient() + + if args.command == "health": + result = client.health_check() + print(json.dumps(result, indent=2)) + + elif args.command == "chat": + resp = client.chat(args.agent, args.message) + print(client.format_for_tui(resp)) + + elif args.command == "list": + convs = client.list_conversations(agent_name=args.agent) + for c in convs: + print(json.dumps(c.to_dict(), indent=2)) + + else: + parser.print_help() + + +if __name__ == "__main__": + _cli() diff --git a/tools/mistral/datasets/hypnoled_kicad/train.jsonl b/tools/mistral/datasets/hypnoled_kicad/train.jsonl new file mode 100644 index 0000000..1ac4b18 --- /dev/null +++ b/tools/mistral/datasets/hypnoled_kicad/train.jsonl @@ -0,0 +1,30 @@ +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled DALI PCB circuit?"}, {"role": "assistant", "content": "The Hypnoled DALI PCB sub-schematic contains the following components:\n\n- R4: 10K (Resistor_SMD:R_0603_1608Metric)\n- Q2: 2n3904S (easyeda2kicad:SOT-23-3_L2.9-W1.3-P1.90-LS2.4-TR)\n- R3: 1K (Resistor_SMD:R_0603_1608Metric)\n- J3: Screw_Terminal_01x02 ()\n- K2: CMA51H-S-DC12V-C (JLCPCB_Relays:RELAY-TH_CMA51H-S-DCXX-C)\n- R5: 10k (Resistor_SMD:R_0603_1608Metric)\n- K4: CMA51H-S-DC12V-C (JLCPCB_Relays:RELAY-TH_CMA51H-S-DCXX-C)\n- Q1: SUM110P04 (easyeda2kicad:TO-263-2_L10.1-W9.1-P5.08-LS15.2-TL-1)\n- J2: AudioJack3 (Connector_Audio:Jack_3.5mm_CUI_SJ-3523-SMT_Horizontal)\n- K1: CMA51H-S-DC12V-C (JLCPCB_Relays:RELAY-TH_CMA51H-S-DCXX-C)\n- J1: Screw_Terminal_01x02 ()\n- K3: CMA51H-S-DC12V-C (JLCPCB_Relays:RELAY-TH_CMA51H-S-DCXX-C)\n- R2: 4.7k (Resistor_SMD:R_0603_1608Metric)\n- R1: 4.7k (Resistor_SMD:R_0603_1608Metric)\n- J5: Screw_Terminal_01x05 ()\n- J4: Screw_Terminal_01x05 ()\n\nTotal: 16 components in DALI PCB.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled DALI PCB schematic?"}, {"role": "assistant", "content": "The DALI PCB sub-schematic contains 16 active components and 23 power symbols, with 97 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled DALI circuit?"}, {"role": "assistant", "content": "The Hypnoled DALI sub-schematic contains the following components:\n\n- R50: 390 (Resistor_SMD:R_0201_0603Metric)\n- C41: 2.2u (Capacitor_SMD:C_0201_0603Metric)\n- R54: 16k (Resistor_SMD:R_0201_0603Metric)\n- R52: 100k (Resistor_SMD:R_0201_0603Metric)\n- Q9: BC857 (Package_TO_SOT_SMD:SOT-23)\n- R48: 1k (Resistor_SMD:R_0201_0603Metric)\n- R44: 4.7k (Resistor_SMD:R_0201_0603Metric)\n- D7: LED (LED_SMD:LED_0603_1608Metric)\n- U18: STN1HNK60 (DALI:SOT-223)\n- R45: 4.7k (Resistor_SMD:R_0201_0603Metric)\n- U17: EL817S1(C)(TU)-F (DALI:SOIC254P1030X390-4N)\n- R51: 100k (Resistor_SMD:R_0201_0603Metric)\n- U16: STN93003 (DALI:SOT230P700X180-4N)\n- D9: UDZSTE-175.6B (DALI:SODFL2512X90N)\n- BR1: MB10S (DALI:SOP245P670X290-4N)\n- C40: 1n (Capacitor_SMD:C_0201_0603Metric)\n- R53: 4.7k (Resistor_SMD:R_0201_0603Metric)\n- U15: EL817S1(C)(TU)-F (DALI:SOIC254P1030X390-4N)\n- R47: 100k (Resistor_SMD:R_0201_0603Metric)\n- R49: 300 (Resistor_SMD:R_0201_0603Metric)\n- Q10: BSS123 (Package_TO_SOT_SMD:SOT-23)\n- R46: 100k (Resistor_SMD:R_0201_0603Metric)\n- D8: D_Zener_Small (Diode_SMD:D_SOD-123)\n- D10: 1SMA4746 (DALI:DIOM4325X250N)\n\nTotal: 24 components in DALI.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled DALI schematic?"}, {"role": "assistant", "content": "The DALI sub-schematic contains 24 active components and 4 power symbols, with 80 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled MCP_power circuit?"}, {"role": "assistant", "content": "The Hypnoled MCP_power sub-schematic contains the following components:\n\n- U5: MCP16331T-E/CH (Package_TO_SOT_SMD:SOT-23-6)\n- C23: 10uF/50V (Capacitor_SMD:C_1206_3216Metric)\n- C22: 10uF/50V (Capacitor_SMD:C_1206_3216Metric)\n- C21: 0.1uF (Capacitor_SMD:C_0603_1608Metric)\n- D2: 1A/100V (Diode_SMD:D_SMA)\n- L1: 15uH,1.5A (Inductor_SMD:L_Taiyo-Yuden_NR-50xx)\n- R17: 31.6k/1% (Resistor_SMD:R_0603_1608Metric)\n- R18: 10k/1% (Resistor_SMD:R_0603_1608Metric)\n- C25: 20pF (Capacitor_SMD:C_0603_1608Metric)\n- C26: 10uF (Capacitor_SMD:C_0805_2012Metric)\n- C27: 10uF (Capacitor_SMD:C_0805_2012Metric)\n- C24: 10nF (Capacitor_SMD:C_0603_1608Metric)\n- D1: 1N4148WT (Diode_SMD:D_SOD-523)\n- C16: 10nF (Capacitor_SMD:C_0603_1608Metric)\n- U6: L7805 (Package_TO_SOT_SMD:SOT-89-3)\n- C72: 10uF (Capacitor_SMD:C_0402_1005Metric)\n- +3V3: +3V3 (TestPoint:TestPoint_Pad_D1.0mm)\n- C69: 10u (Capacitor_SMD:C_0603_1608Metric)\n- C63: 100n (Capacitor_SMD:C_0603_1608Metric)\n- C66: 22u (Capacitor_SMD:C_0603_1608Metric)\n- C46: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- C70: 22u (Capacitor_SMD:C_0603_1608Metric)\n- U24: LD1117S33TR_SOT223 (Package_TO_SOT_SMD:SOT-223-3_TabPin2)\n- U23: LD1117S33TR_SOT223 (Package_TO_SOT_SMD:SOT-223-3_TabPin2)\n- C64: 100n (Capacitor_SMD:C_0603_1608Metric)\n- C15: 0.1uF (Capacitor_SMD:C_0603_1608Metric)\n- C60: 100n (Capacitor_SMD:C_0603_1608Metric)\n- C67: 100n (Capacitor_SMD:C_0603_1608Metric)\n- C65: 10u (Capacitor_SMD:C_0603_1608Metric)\n- U19: L7812 (Package_TO_SOT_SMD:TO-263-2)\n- C74: 0.1uF (Capacitor_SMD:C_0402_1005Metric)\n- U3: DNP/TPS79133DBVR (Package_TO_SOT_SMD:SOT-23-5)\n- C42: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- U22: LD1117S33TR_SOT223 (Package_TO_SOT_SMD:SOT-223-3_TabPin2)\n- C62: 22u (Capacitor_SMD:C_0603_1608Metric)\n- C61: 10u (Capacitor_SMD:C_0603_1608Metric)\n- C29: 220uF (Capacitor_SMD:CP_Elec_3x5.3)\n- C68: 100n (Capacitor_SMD:C_0603_1608Metric)\n- C47: 220uF (Capacitor_SMD:CP_Elec_3x5.3)\n- C30: 220uF (Capacitor_SMD:CP_Elec_3x5.3)\n- R16: 10k/1% (Resistor_SMD:R_0603_1608Metric)\n- C73: 10uF (Capacitor_SMD:C_0402_1005Metric)\n- C71: 100n (Capacitor_SMD:C_0603_1608Metric)\n- C45: 220uF (Capacitor_SMD:CP_Elec_3x5.3)\n- #+3V012: +3V3 ()\n- U25: AMS1117-3.3 (Package_TO_SOT_SMD:SOT-223-3_TabPin2)\n\nTotal: 46 components in MCP_power.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled MCP_power schematic?"}, {"role": "assistant", "content": "The MCP_power sub-schematic contains 46 active components and 37 power symbols, with 134 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled UI circuit?"}, {"role": "assistant", "content": "The Hypnoled UI sub-schematic contains the following components:\n\n- R39: 75k/1% (Resistor_SMD:R_0201_0603Metric)\n- D4: LED (LED_SMD:LED_0603_1608Metric)\n- TP1: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- TP2: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- JP14: SolderJumper_2_Open (Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm)\n- TP5: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- R40: 220R (Resistor_SMD:R_0201_0603Metric)\n- D3: LED (LED_SMD:LED_0603_1608Metric)\n- JP16: SolderJumper_2_Open (Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm)\n- C38: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- U13: HS96L03W2C03 (JLCPCB_Functional_Modules:OLED-TH_L27.8-W27.2-P2.54_C9900033791)\n- D6: LED (LED_SMD:LED_0603_1608Metric)\n- R43: 220R (Resistor_SMD:R_0201_0603Metric)\n- R42: 220R (Resistor_SMD:R_0201_0603Metric)\n- U14: MPR121QR2 (Package_DFN_QFN:UQFN-20_3x3mm_P0.4mm)\n- C39: 0.1uf (Resistor_SMD:R_0201_0603Metric)\n- TP6: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- JP15: SolderJumper_2_Open (Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm)\n- TP7: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- R41: 220R (Resistor_SMD:R_0201_0603Metric)\n- TP4: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- TP3: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- D5: LED (LED_SMD:LED_0603_1608Metric)\n- JP13: SolderJumper_2_Open (Jumper:SolderJumper-2_P1.3mm_Open_RoundedPad1.0x1.5mm)\n- TP8: TestPoint_Flag (TestPoint:TestPoint_Pad_4.0x4.0mm)\n- R38: 10k (Resistor_SMD:R_0201_0603Metric)\n\nTotal: 26 components in UI.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled UI schematic?"}, {"role": "assistant", "content": "The UI sub-schematic contains 26 active components and 10 power symbols, with 59 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled audio circuit?"}, {"role": "assistant", "content": "The Hypnoled audio sub-schematic contains the following components:\n\n- C6: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C5: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C4: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C1: 10uF (Capacitor_SMD:C_0201_0603Metric)\n- C2: 1uF (Capacitor_SMD:C_0201_0603Metric)\n- C7: 2.2uF/25V (Capacitor_SMD:C_0201_0603Metric)\n- C10: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C9: 2.2uF/25V (Capacitor_SMD:C_0603_1608Metric)\n- R6: 470R/1% (Resistor_SMD:R_0201_0603Metric)\n- C12: 2.2nF/C0G (Capacitor_SMD:C_0201_0603Metric)\n- R7: 470R/1% (Resistor_SMD:R_0201_0603Metric)\n- C11: 2.2nF/C0G (Capacitor_SMD:C_0201_0603Metric)\n- C8: 10uF/50V (Capacitor_SMD:C_0201_0603Metric)\n- U2: Audio_amp_OEP30Wx2 (tom_kicad_lib:Audio_amp_OEP30Wx2)\n- C3: 10uF (Capacitor_SMD:C_0201_0603Metric)\n- U1: PCM5121 (Package_SO:TSSOP-28_4.4x9.7mm_P0.65mm)\n- J6: AudioJack2_Ground (tom_kicad_lib:Jack_3.5mm_stereo_PJ-313D)\n- R10: 10k (Capacitor_SMD:C_0201_0603Metric)\n- R11: 10k (Capacitor_SMD:C_0201_0603Metric)\n- R58: 10K (Resistor_SMD:R_0201_0603Metric)\n- C51: 2.2nF/C0G (Capacitor_SMD:C_0201_0603Metric)\n- R9: 2.2K (Resistor_SMD:R_0201_0603Metric)\n- R59: 10K (Resistor_SMD:R_0201_0603Metric)\n- C53: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C50: 2.2uF (Capacitor_SMD:C_0201_0603Metric)\n- U21: PCM5122PW (JLCPCB_Interface_ICs:TSSOP-28_L9.8-W6.6-P0.65-BL)\n- C14: 22uF (Capacitor_SMD:CP_Elec_3x5.3)\n- R8: 2.2k (Resistor_SMD:R_0201_0603Metric)\n- JP19: SolderJumper_2_Open ()\n- C54: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C55: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- R60: 10K (Resistor_SMD:R_0201_0603Metric)\n- JP18: SolderJumper_2_Open ()\n- R57: 10K (Resistor_SMD:R_0201_0603Metric)\n- C48: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C13: 22uF (Capacitor_SMD:CP_Elec_3x5.3)\n- R56: 470R/1% (Resistor_SMD:R_0201_0603Metric)\n- C56: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- C52: 2.2nF/C0G (Capacitor_SMD:C_0201_0603Metric)\n- C49: 2.2uF (Capacitor_SMD:C_0201_0603Metric)\n- JP17: SolderJumper_2_Open ()\n- R55: 470R/1% (Resistor_SMD:R_0201_0603Metric)\n\nTotal: 42 components in audio.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled audio schematic?"}, {"role": "assistant", "content": "The audio sub-schematic contains 42 active components and 39 power symbols, with 153 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled audio2led circuit?"}, {"role": "assistant", "content": "The Hypnoled audio2led sub-schematic contains the following components:\n\n- JP10: A0 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- C34: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- C33: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- R20: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- R24: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- C44: 220uF (Capacitor_SMD:CP_Elec_3x5.3)\n- U11: LM358_DFN (Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm)\n- R32: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- JP12: A2 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- C43: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- R30: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- Q8: IRF3415STRLPBF (KiCad:IRF3415STRLPBF)\n- Q7: IRF3415STRLPBF (KiCad:IRF3415STRLPBF)\n- Q5: IRF3415STRLPBF (KiCad:IRF3415STRLPBF)\n- JP7: A0 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- C36: 22uF (Capacitor_SMD:CP_Elec_3x5.3)\n- R26: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- R22: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- R34: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- JP8: A1 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- U8: LM358_DFN (Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm)\n- C37: 22uF (Capacitor_SMD:CP_Elec_3x5.3)\n- R28: 5.6K (Resistor_SMD:R_0201_0603Metric)\n- U8: LM358_DFN (Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm)\n- R23: 1K (Resistor_SMD:R_0201_0603Metric)\n- JP1: A0 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- C32: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- U9: MCP4651-104E/ST (JLCPCB_ADC_DAC_Data_Conversion:TSSOP-14_L5.0-W4.4-P0.65-LS6.4-BL)\n- R25: 10K (Resistor_SMD:R_0201_0603Metric)\n- R21: 10K (Resistor_SMD:R_0201_0603Metric)\n- U11: LM358_DFN (Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm)\n- JP5: A1 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- C35: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- JP3: A2 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- Q6: IRF3415STRLPBF (KiCad:IRF3415STRLPBF)\n- JP4: A0 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- JP6: A2 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- JP11: A1 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- U11: LM358_DFN (Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm)\n- U10: MCP4651-104E/ST (JLCPCB_ADC_DAC_Data_Conversion:TSSOP-14_L5.0-W4.4-P0.65-LS6.4-BL)\n- R36: 26K (Resistor_SMD:R_0201_0603Metric)\n- U8: LM358_DFN (Package_DFN_QFN:DFN-8-1EP_2x2mm_P0.5mm_EP1.05x1.75mm)\n- C28: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- U12: MCP4651-104E/ST (JLCPCB_ADC_DAC_Data_Conversion:TSSOP-14_L5.0-W4.4-P0.65-LS6.4-BL)\n- JP9: A2 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- R29: 10K (Resistor_SMD:R_0201_0603Metric)\n- R37: 2.2K (Resistor_SMD:R_0201_0603Metric)\n- JP2: A1 (asukiaaa-kicad-footprints:SolderJumper-3_P1.3mm_Bridged12_Pad1.0x1.5mm_without_1pad_marker)\n- R33: 10K (Resistor_SMD:R_0201_0603Metric)\n- R27: 1K (Resistor_SMD:R_0201_0603Metric)\n- U7: MCP4651-104E/ST (JLCPCB_ADC_DAC_Data_Conversion:TSSOP-14_L5.0-W4.4-P0.65-LS6.4-BL)\n- R31: 1K (Resistor_SMD:R_0201_0603Metric)\n- C31: 220uF (Capacitor_SMD:CP_Elec_3x5.3)\n- R19: 1K (Resistor_SMD:R_0201_0603Metric)\n- R35: 2.2k (Resistor_SMD:R_0201_0603Metric)\n\nTotal: 55 components in audio2led.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled audio2led schematic?"}, {"role": "assistant", "content": "The audio2led sub-schematic contains 55 active components and 50 power symbols, with 169 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the Hypnoled esp32 circuit?"}, {"role": "assistant", "content": "The Hypnoled esp32 sub-schematic contains the following components:\n\n- U4: ESP32-WROOM (RF_Module:ESP32-WROOM-32U)\n- C18: 0.1uF (Capacitor_SMD:C_0201_0603Metric)\n- J8: Conn_01x06 (Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical)\n- C19: 1uF (Capacitor_SMD:C_0201_0603Metric)\n- J9: TEST_1P (TestPoint:TestPoint_Pad_D1.5mm)\n- R15: 10k (Resistor_SMD:R_0201_0603Metric)\n- Q4: BC847W (Package_TO_SOT_SMD:SOT-323_SC-70)\n- R14: 1k (Resistor_SMD:R_0201_0603Metric)\n- R12: 10k (Resistor_SMD:R_0201_0603Metric)\n- R67: 5.1K (Resistor_SMD:R_0603_1608Metric)\n- R68: 5.1K (Resistor_SMD:R_0603_1608Metric)\n- D12: GREEN (LED_SMD:LED_0603_1608Metric)\n- #SUPPLY03: 5V ()\n- R13: 10k (Resistor_SMD:R_0201_0603Metric)\n- C17: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- Q3: BC847W (Package_TO_SOT_SMD:SOT-323_SC-70)\n- J7: 112J-TDAR-R01 (112JTDARR01)\n- R63: 33R (Resistor_SMD:R_0402_1005Metric)\n- C59: 1u (Capacitor_SMD:C_0603_1608Metric)\n- D13: BLUE (LED_SMD:LED_0603_1608Metric)\n- R61: 5.1K (Resistor_SMD:R_0402_1005Metric)\n- #SUPPLY01: 5V ()\n- J10: USB_C_Receptacle_USB2.0_14P (Connector_USB:USB_C_Receptacle_HCTL_HC-TYPE-C-16P-01A)\n- #SUPPLY04: 5V ()\n- D11: RED (LED_SMD:LED_0603_1608Metric)\n- R64: 33R (Resistor_SMD:R_0402_1005Metric)\n- R62: 5.1K (Resistor_SMD:R_0402_1005Metric)\n- C58: 0.1uF (Capacitor_SMD:C_0402_1005Metric)\n- R66: 5.1K (Resistor_SMD:R_0603_1608Metric)\n- R65: 5.1K (Resistor_SMD:R_0603_1608Metric)\n- U20: ESP32-S3-WROOM-1(N16R8) (JLCPCB_Solders_Accessories_Batteries:WIRELM-SMD_ESP32-S3-WROOM-1)\n- C20: 100nF (Capacitor_SMD:C_0201_0603Metric)\n- SW1: TS-1088-AR02016 (easyeda2kicad:SW-SMD_L3.9-W3.0-P4.45)\n- C57: 10uF (Capacitor_SMD:C_0402_1005Metric)\n- SW2: TS-1088-AR02016 (easyeda2kicad:SW-SMD_L3.9-W3.0-P4.45)\n\nTotal: 35 components in esp32.kicad_sch."}]} +{"messages": [{"role": "user", "content": "How many components are in the Hypnoled esp32 schematic?"}, {"role": "assistant", "content": "The esp32 sub-schematic contains 35 active components and 28 power symbols, with 147 wire connections."}]} +{"messages": [{"role": "user", "content": "What components are used in the DALI bus interface circuit of Hypnoled?"}, {"role": "assistant", "content": "The DALI bus interface circuit uses these components: R50 (390), C41 (2.2u), R54 (16k), R52 (100k), Q9 (BC857), R48 (1k), R44 (4.7k), D7 (LED), U18 (STN1HNK60), R45 (4.7k), U17 (EL817S1(C)(TU)-F), R51 (100k), U16 (STN93003), D9 (UDZSTE-175.6B), BR1 (MB10S), C40 (1n), R53 (4.7k), U15 (EL817S1(C)(TU)-F), R47 (100k), R49 (300), Q10 (BSS123), R46 (100k), D8 (D_Zener_Small), D10 (1SMA4746).\n\nKey elements include:\n- 1SMA4746 Zener diode for voltage protection\n- BSS123 N-MOSFET for DALI bus TX signaling\n- EL357N optocoupler for galvanic isolation on RX\n- MB10S bridge rectifier for DALI 16V bus rectification\n- STN1HNK60 high-voltage MOSFET (600V) for DALI driver\n- Resistor dividers for bus voltage sensing"}]} +{"messages": [{"role": "user", "content": "How does the Hypnoled DALI transmitter work?"}, {"role": "assistant", "content": "The Hypnoled DALI TX uses Manchester encoding through an N-MOSFET (BSS123) that modulates the DALI bus voltage. The ESP32 GPIO drives the MOSFET gate to pull the bus low (logical 1) or release it high (logical 0). A high-voltage MOSFET (STN1HNK60, 600V rated) provides the bus driver stage. The bus is protected by a 1SMA4746 Zener diode for overvoltage transients."}]} +{"messages": [{"role": "user", "content": "How does the Hypnoled DALI receiver work?"}, {"role": "assistant", "content": "The DALI RX circuit uses an EL357N optocoupler for galvanic isolation between the DALI bus (up to 22.5V) and the ESP32 3.3V logic. A resistor divider feeds the bus voltage to the optocoupler LED side. The phototransistor output connects to an ESP32 GPIO configured as an interrupt input for Manchester decoding. BC857 PNP transistor and STN93003 provide signal conditioning."}]} +{"messages": [{"role": "user", "content": "How is the ESP32 connected in the Hypnoled project?"}, {"role": "assistant", "content": "The ESP32 sub-schematic in Hypnoled contains:\n\n- U4: ESP32-WROOM\n- C18: 0.1uF\n- J8: Conn_01x06\n- C19: 1uF\n- J9: TEST_1P\n- R15: 10k\n- Q4: BC847W\n- R14: 1k\n- R12: 10k\n- R67: 5.1K\n- R68: 5.1K\n- D12: GREEN\n- #SUPPLY03: 5V\n- R13: 10k\n- C17: 100nF\n- Q3: BC847W\n- J7: 112J-TDAR-R01\n- R63: 33R\n- C59: 1u\n- D13: BLUE\n- R61: 5.1K\n- #SUPPLY01: 5V\n- J10: USB_C_Receptacle_USB2.0_14P\n- #SUPPLY04: 5V\n- D11: RED\n- R64: 33R\n- R62: 5.1K\n- C58: 0.1uF\n- R66: 5.1K\n- R65: 5.1K\n- U20: ESP32-S3-WROOM-1(N16R8)\n- C20: 100nF\n- SW1: TS-1088-AR02016\n- C57: 10uF\n- SW2: TS-1088-AR02016\n\nThe ESP32 serves as the main MCU, handling:\n- DALI bus communication (TX via GPIO + MOSFET, RX via optocoupler ISR)\n- I2C bus for UI (MPR121 touch sensor) and audio (PCM5122 DAC)\n- WiFi connectivity for MQTT remote control\n- UART for debug and programming\n- USB-C for power and data"}]} +{"messages": [{"role": "user", "content": "What peripherals does the Hypnoled ESP32 drive?"}, {"role": "assistant", "content": "The ESP32 in Hypnoled drives several peripherals:\n1. DALI bus interface (GPIO for TX Manchester encoding, interrupt for RX)\n2. I2C bus shared between MPR121 touch controller (UI) and PCM5122 audio DAC\n3. LED strip output through audio2led reactive driver (IRF3415 MOSFET)\n4. WiFi for MQTT-based remote control\n5. USB-C interface for programming and power delivery"}]} +{"messages": [{"role": "user", "content": "What is the power supply architecture of the Hypnoled project?"}, {"role": "assistant", "content": "The MCP_power sub-schematic handles power distribution:\n\n- U5: MCP16331T-E/CH\n- C23: 10uF/50V\n- C22: 10uF/50V\n- C21: 0.1uF\n- D2: 1A/100V\n- L1: 15uH,1.5A\n- R17: 31.6k/1%\n- R18: 10k/1%\n- C25: 20pF\n- C26: 10uF\n- C27: 10uF\n- C24: 10nF\n- D1: 1N4148WT\n- C16: 10nF\n- U6: L7805\n- C72: 10uF\n- +3V3: +3V3\n- C69: 10u\n- C63: 100n\n- C66: 22u\n- C46: 100nF\n- C70: 22u\n- U24: LD1117S33TR_SOT223\n- U23: LD1117S33TR_SOT223\n- C64: 100n\n- C15: 0.1uF\n- C60: 100n\n- C67: 100n\n- C65: 10u\n- U19: L7812\n- C74: 0.1uF\n- U3: DNP/TPS79133DBVR\n- C42: 100nF\n- U22: LD1117S33TR_SOT223\n- C62: 22u\n- C61: 10u\n- C29: 220uF\n- C68: 100n\n- C47: 220uF\n- C30: 220uF\n- R16: 10k/1%\n- C73: 10uF\n- C71: 100n\n- C45: 220uF\n- #+3V012: +3V3\n- U25: AMS1117-3.3\n\nThe power architecture provides multiple voltage rails:\n- Main input from DALI bus (rectified via MB10S bridge)\n- 3.3V rail for ESP32 and digital logic\n- 5V rail for USB and LED driver\n- Analog supply for audio DAC (PCM5122)\n\nNote: Forge review identified the power chain as potentially incoherent — verify the cascade regulation stages match the load requirements."}]} +{"messages": [{"role": "user", "content": "What voltage regulators are used in Hypnoled's power supply?"}, {"role": "assistant", "content": "The MCP_power schematic includes these power components:\n\n- U5: MCP16331T-E/CH\n- C23: 10uF/50V\n- C22: 10uF/50V\n- C21: 0.1uF\n- D2: 1A/100V\n- L1: 15uH,1.5A\n- R17: 31.6k/1%\n- R18: 10k/1%\n- C25: 20pF\n- C26: 10uF\n- C27: 10uF\n- C24: 10nF\n- D1: 1N4148WT\n- C16: 10nF\n- U6: L7805\n- C72: 10uF\n- +3V3: +3V3\n- C69: 10u\n- C63: 100n\n- C66: 22u\n- C46: 100nF\n- C70: 22u\n- U24: LD1117S33TR_SOT223\n- U23: LD1117S33TR_SOT223\n- C64: 100n\n- C15: 0.1uF\n- C60: 100n\n- C67: 100n\n- C65: 10u\n- U19: L7812\n- C74: 0.1uF\n- U3: DNP/TPS79133DBVR\n- C42: 100nF\n- U22: LD1117S33TR_SOT223\n- C62: 22u\n- C61: 10u\n- C29: 220uF\n- C68: 100n\n- C47: 220uF\n- C30: 220uF\n- R16: 10k/1%\n- C73: 10uF\n- C71: 100n\n- C45: 220uF\n- #+3V012: +3V3\n- U25: AMS1117-3.3\n\nThe power supply converts the DALI bus voltage (16V typical) down to the required logic and analog levels through a multi-stage regulation chain."}]} +{"messages": [{"role": "user", "content": "What audio components are in the Hypnoled project?"}, {"role": "assistant", "content": "The audio sub-schematic contains:\n\n- C6: 0.1uF\n- C5: 0.1uF\n- C4: 0.1uF\n- C1: 10uF\n- C2: 1uF\n- C7: 2.2uF/25V\n- C10: 0.1uF\n- C9: 2.2uF/25V\n- R6: 470R/1%\n- C12: 2.2nF/C0G\n- R7: 470R/1%\n- C11: 2.2nF/C0G\n- C8: 10uF/50V\n- U2: Audio_amp_OEP30Wx2\n- C3: 10uF\n- U1: PCM5121\n- J6: AudioJack2_Ground\n- R10: 10k\n- R11: 10k\n- R58: 10K\n- C51: 2.2nF/C0G\n- R9: 2.2K\n- R59: 10K\n- C53: 0.1uF\n- C50: 2.2uF\n- U21: PCM5122PW\n- C14: 22uF\n- R8: 2.2k\n- JP19: SolderJumper_2_Open\n- C54: 0.1uF\n- C55: 0.1uF\n- R60: 10K\n- JP18: SolderJumper_2_Open\n- R57: 10K\n- C48: 0.1uF\n- C13: 22uF\n- R56: 470R/1%\n- C56: 0.1uF\n- C52: 2.2nF/C0G\n- C49: 2.2uF\n- JP17: SolderJumper_2_Open\n- R55: 470R/1%\n\nThe audio path uses a PCM5122 DAC connected to the ESP32 via I2C for control and I2S for audio data. The DAC output feeds an amplifier stage for the hypnotherapy audio output. Forge review flagged the PCM5122 power supply as a critical point to verify."}]} +{"messages": [{"role": "user", "content": "How does the Hypnoled audio-reactive LED driver work?"}, {"role": "assistant", "content": "The audio2led sub-schematic contains:\n\n- JP10: A0\n- C34: 100nF\n- C33: 100nF\n- R20: 5.6K\n- R24: 5.6K\n- C44: 220uF\n- U11: LM358_DFN\n- R32: 5.6K\n- JP12: A2\n- C43: 100nF\n- R30: 5.6K\n- Q8: IRF3415STRLPBF\n- Q7: IRF3415STRLPBF\n- Q5: IRF3415STRLPBF\n- JP7: A0\n- C36: 22uF\n- R26: 5.6K\n- R22: 5.6K\n- R34: 5.6K\n- JP8: A1\n- U8: LM358_DFN\n- C37: 22uF\n- R28: 5.6K\n- U8: LM358_DFN\n- R23: 1K\n- JP1: A0\n- C32: 100nF\n- U9: MCP4651-104E/ST\n- R25: 10K\n- R21: 10K\n- U11: LM358_DFN\n- JP5: A1\n- C35: 100nF\n- JP3: A2\n- Q6: IRF3415STRLPBF\n- JP4: A0\n- JP6: A2\n- JP11: A1\n- U11: LM358_DFN\n- U10: MCP4651-104E/ST\n- R36: 26K\n- U8: LM358_DFN\n- C28: 100nF\n- U12: MCP4651-104E/ST\n- JP9: A2\n- R29: 10K\n- R37: 2.2K\n- JP2: A1\n- R33: 10K\n- R27: 1K\n- U7: MCP4651-104E/ST\n- R31: 1K\n- C31: 220uF\n- R19: 1K\n- R35: 2.2k\n\nThe audio2led circuit converts audio signals into LED brightness modulation. An IRF3415 MOSFET drives the LED strip based on audio amplitude. Forge review noted the IRF3415 may be over-dimensioned for the application — consider a logic-level MOSFET if the gate drive voltage is limited to 3.3V."}]} +{"messages": [{"role": "user", "content": "What user interface components does Hypnoled use?"}, {"role": "assistant", "content": "The UI sub-schematic contains:\n\n- R39: 75k/1%\n- D4: LED\n- TP1: TestPoint_Flag\n- TP2: TestPoint_Flag\n- JP14: SolderJumper_2_Open\n- TP5: TestPoint_Flag\n- R40: 220R\n- D3: LED\n- JP16: SolderJumper_2_Open\n- C38: 100nF\n- U13: HS96L03W2C03\n- D6: LED\n- R43: 220R\n- R42: 220R\n- U14: MPR121QR2\n- C39: 0.1uf\n- TP6: TestPoint_Flag\n- JP15: SolderJumper_2_Open\n- TP7: TestPoint_Flag\n- R41: 220R\n- TP4: TestPoint_Flag\n- TP3: TestPoint_Flag\n- D5: LED\n- JP13: SolderJumper_2_Open\n- TP8: TestPoint_Flag\n- R38: 10k\n\nThe UI is based on an MPR121 capacitive touch sensor connected via I2C. This provides up to 12 touch-sensitive electrodes for user interaction (mode selection, brightness, audio volume). Forge review flagged that the MPR121 I2C address must be explicitly configured to avoid bus conflicts."}]} +{"messages": [{"role": "user", "content": "What is the overall architecture of the Hypnoled PCB project?"}, {"role": "assistant", "content": "Hypnoled is a multi-sheet KiCad design with 7 sub-schematics:\nDALI PCB, DALI, MCP_power, UI, audio, audio2led, esp32.\n\nTotal component count: 244 active components.\n\nArchitecture overview:\n- **DALI**: Bus interface with Manchester encoding TX/RX, optocoupler isolation\n- **esp32**: Main MCU with WiFi, I2C, UART, USB-C\n- **audio**: PCM5122 DAC for hypnotherapy audio output\n- **audio2led**: Audio-reactive LED strip driver (MOSFET-based)\n- **MCP_power**: Multi-rail power supply from DALI bus input\n- **UI**: MPR121 capacitive touch interface\n\nThe project was designed by L'electron rare for client Richard Garnier."}]} +{"messages": [{"role": "user", "content": "What are the main design issues found in the Hypnoled hardware review?"}, {"role": "assistant", "content": "A Forge (Codestral) review of all 6 Hypnoled sub-schematics identified:\n\n**8 Critical issues:**\n1. Zener diode voltage (15V) may be insufficient for DALI bus transients\n2. Double ESP32 in schematic not justified — remove redundant instance\n3. USB-C CC resistors missing or misconfigured\n4. PCM5122 analog power supply filtering inadequate\n5. IRF3415 MOSFET over-dimensioned for 3.3V gate drive\n6. Power chain architecture incoherent (MCP_power stage cascade)\n7. MPR121 I2C address not configured (potential bus conflict)\n8. Second power regulation incoherence in MCP_power\n\n**7 Warnings** about component tolerances and thermal considerations.\n**18 Recommendations** for improved EMC, reliability, and manufacturability."}]} +{"messages": [{"role": "user", "content": "What are the main signal nets in the Hypnoled schematic?"}, {"role": "assistant", "content": "The Hypnoled schematics use these signal labels across sub-sheets:\nADDR_1, ADDR_2, AUDIO_IN, AUDIO_L, AUDIO_MUTE, AUDIO_R, DALI_RL, DALI_SW, D_RX, D_TX, EN, ESP32_BOOT, ESP32_EN, ESP32_RXI, ESP32_TXO, File +, File -, GAIN_LED_1, GAIN_LED_2, GAIN_LED_3, GAIN_LED_4, GPIO0, GPIO21, I2C_SCL, I2C_SDA, I2S_BCK, I2S_DATA, I2S_LRCK, IRQ_I2C, LED_1\n\nThese nets connect the sub-schematics through hierarchical labels, enabling the ESP32 to communicate with the DALI interface, audio DAC, LED driver, touch UI, and power management sections."}]} +{"messages": [{"role": "user", "content": "What PCB footprints are used in the Hypnoled design?"}, {"role": "assistant", "content": "The Hypnoled design uses these footprints:\n\n- 112JTDARR01\n- Capacitor_SMD:CP_Elec_3x5.3\n- Capacitor_SMD:C_0201_0603Metric\n- Capacitor_SMD:C_0402_1005Metric\n- Capacitor_SMD:C_0603_1608Metric\n- Capacitor_SMD:C_0805_2012Metric\n- Capacitor_SMD:C_1206_3216Metric\n- Connector_Audio:Jack_3.5mm_CUI_SJ-3523-SMT_Horizontal\n- Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical\n- Connector_USB:USB_C_Receptacle_HCTL_HC-TYPE-C-16P-01A\n- DALI:DIOM4325X250N\n- DALI:SODFL2512X90N\n- DALI:SOIC254P1030X390-4N\n- DALI:SOP245P670X290-4N\n- DALI:SOT-223\n- DALI:SOT230P700X180-4N\n- Diode_SMD:D_SMA\n- Diode_SMD:D_SOD-123\n- Diode_SMD:D_SOD-523\n- Inductor_SMD:L_Taiyo-Yuden_NR-50xx\n- JLCPCB_ADC_DAC_Data_Conversion:TSSOP-14_L5.0-W4.4-P0.65-LS6.4-BL\n- JLCPCB_Functional_Modules:OLED-TH_L27.8-W27.2-P2.54_C9900033791\n- JLCPCB_Interface_ICs:TSSOP-28_L9.8-W6.6-P0.65-BL\n- JLCPCB_Relays:RELAY-TH_CMA51H-S-DCXX-C\n- JLCPCB_Solders_Accessories_Batteries:WIRELM-SMD_ESP32-S3-WROOM-1\n\nThe mix of SMD and through-hole footprints indicates a design that balances automated assembly (SMD) with manual soldering for connectors and power components."}]} +{"messages": [{"role": "user", "content": "What is the Hypnoled project?"}, {"role": "assistant", "content": "Hypnoled is an IoT-connected LED hypnotherapy device designed by L'electron rare for client Richard Garnier. It combines:\n- DALI bus integration for professional lighting control\n- ESP32 WiFi/BLE MCU for smart control via MQTT\n- Audio output (PCM5122 DAC) for hypnotherapy sessions\n- Audio-reactive LED effects (audio2led MOSFET driver)\n- Capacitive touch UI (MPR121)\n\nThe hardware is designed in KiCad 9.0 with 6 hierarchical sub-schematics and uses a custom DALI interface with Manchester encoding."}]} +{"messages": [{"role": "user", "content": "What KiCad version is the Hypnoled project designed in?"}, {"role": "assistant", "content": "Hypnoled is designed in KiCad 9.0 (generator version 9.0, schema version 20250114). The project uses hierarchical schematics with 6 sub-sheets: DALI, esp32, audio, audio2led, MCP_power, and UI. The design is revision 0.1 dated 2026-02-10."}]} diff --git a/tools/mistral/datasets/hypnoled_spice/train.jsonl b/tools/mistral/datasets/hypnoled_spice/train.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/tools/mistral/extract_hypnoled_datasets.py b/tools/mistral/extract_hypnoled_datasets.py new file mode 100644 index 0000000..830735d --- /dev/null +++ b/tools/mistral/extract_hypnoled_datasets.py @@ -0,0 +1,681 @@ +#!/usr/bin/env python3 +""" +Extract Hypnoled KiCad schematics and SPICE simulations into ChatML JSONL +datasets for Mistral fine-tuning. + +Parses .kicad_sch files (s-expression format) from a local clone of +https://github.com/electron-rare/hypnoled and generates Q&A pairs about +the Hypnoled project's hardware design. + +Outputs: + tools/mistral/datasets/hypnoled_kicad/train.jsonl — KiCad Q&A pairs + tools/mistral/datasets/hypnoled_spice/train.jsonl — SPICE Q&A pairs (if .asc files exist) + +Usage: + python tools/mistral/extract_hypnoled_datasets.py [--clone-dir /tmp/hypnoled-datasets] +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional, Tuple + + +# --------------------------------------------------------------------------- +# KiCad S-Expression parser (minimal, tailored for .kicad_sch) +# --------------------------------------------------------------------------- + +@dataclass +class Component: + reference: str = "" + value: str = "" + footprint: str = "" + datasheet: str = "" + manufacturer: str = "" + description: str = "" + lib_id: str = "" + pins: List[str] = field(default_factory=list) + nets: List[str] = field(default_factory=list) + properties: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class SchematicSheet: + filename: str = "" + title: str = "" + revision: str = "" + company: str = "" + components: List[Component] = field(default_factory=list) + wires: int = 0 + labels: List[str] = field(default_factory=list) + power_symbols: List[str] = field(default_factory=list) + hierarchical_labels: List[str] = field(default_factory=list) + + +def _tokenize_sexpr(text: str): + """Tokenize a KiCad s-expression into a nested list structure.""" + tokens = [] + stack = [tokens] + i = 0 + n = len(text) + while i < n: + c = text[i] + if c == '(': + new_list = [] + stack[-1].append(new_list) + stack.append(new_list) + i += 1 + elif c == ')': + if len(stack) > 1: + stack.pop() + i += 1 + elif c == '"': + # Quoted string + j = i + 1 + while j < n and text[j] != '"': + if text[j] == '\\': + j += 1 + j += 1 + stack[-1].append(text[i + 1:j]) + i = j + 1 + elif c in (' ', '\t', '\n', '\r'): + i += 1 + else: + j = i + while j < n and text[j] not in ('(', ')', ' ', '\t', '\n', '\r', '"'): + j += 1 + stack[-1].append(text[i:j]) + i = j + return tokens + + +def _find_nodes(tree, tag: str) -> list: + """Recursively find all nodes with a given tag in the s-expression tree.""" + results = [] + if isinstance(tree, list): + if len(tree) > 0 and tree[0] == tag: + results.append(tree) + for child in tree: + results.extend(_find_nodes(child, tag)) + return results + + +def _get_property(node: list, key: str) -> str: + """Extract a property value from a symbol node.""" + for child in node: + if isinstance(child, list) and len(child) >= 3: + if child[0] == "property" and child[1] == key: + return child[2] + return "" + + +def parse_kicad_sch(filepath: Path) -> SchematicSheet: + """Parse a .kicad_sch file and extract component/connection information.""" + text = filepath.read_text(encoding="utf-8", errors="replace") + tree = _tokenize_sexpr(text) + + sheet = SchematicSheet(filename=filepath.name) + + # Title block + for tb in _find_nodes(tree, "title_block"): + for child in tb: + if isinstance(child, list) and len(child) >= 2: + if child[0] == "title": + sheet.title = child[1] + elif child[0] == "rev": + sheet.revision = child[1] + elif child[0] == "company": + sheet.company = child[1] + + # Symbols (components) + for sym_node in _find_nodes(tree, "symbol"): + # Skip lib_symbols definitions (they are inside a lib_symbols node) + # We want instance symbols which have a "lib_id" property + lib_id = "" + for child in sym_node: + if isinstance(child, list) and len(child) >= 2 and child[0] == "lib_id": + lib_id = child[1] + break + if not lib_id: + continue + + comp = Component(lib_id=lib_id) + comp.reference = _get_property(sym_node, "Reference") + comp.value = _get_property(sym_node, "Value") + comp.footprint = _get_property(sym_node, "Footprint") + comp.datasheet = _get_property(sym_node, "Datasheet") + comp.manufacturer = _get_property(sym_node, "MANUFACTURER") + comp.description = _get_property(sym_node, "Description") + + # Collect all custom properties + for child in sym_node: + if isinstance(child, list) and len(child) >= 3 and child[0] == "property": + comp.properties[child[1]] = child[2] + + # Pin connections + for pin_node in _find_nodes(sym_node, "pin"): + if len(pin_node) >= 2: + comp.pins.append(pin_node[1] if isinstance(pin_node[1], str) else str(pin_node[1])) + + sheet.components.append(comp) + + # Wires + sheet.wires = len(_find_nodes(tree, "wire")) + + # Labels (net names) + for label_node in _find_nodes(tree, "label"): + if len(label_node) >= 2 and isinstance(label_node[1], str): + sheet.labels.append(label_node[1]) + + # Global labels + for gl_node in _find_nodes(tree, "global_label"): + if len(gl_node) >= 2 and isinstance(gl_node[1], str): + sheet.labels.append(gl_node[1]) + + # Hierarchical labels + for hl_node in _find_nodes(tree, "hierarchical_label"): + if len(hl_node) >= 2 and isinstance(hl_node[1], str): + sheet.hierarchical_labels.append(hl_node[1]) + + # Power symbols (components whose lib_id starts with "power:") + for comp in sheet.components: + if comp.lib_id.startswith("power:") or comp.value in ( + "GND", "VCC", "VDD", "+3V3", "+5V", "+12V", "+3.3V", "VBUS", + ): + sheet.power_symbols.append(comp.value) + + return sheet + + +# --------------------------------------------------------------------------- +# SPICE / LTspice .asc parser +# --------------------------------------------------------------------------- + +@dataclass +class SpiceCircuit: + filename: str = "" + components: List[Dict[str, str]] = field(default_factory=list) + nets: List[str] = field(default_factory=list) + directives: List[str] = field(default_factory=list) + + +def parse_ltspice_asc(filepath: Path) -> Optional[SpiceCircuit]: + """Parse a LTspice .asc file and extract components and directives.""" + try: + text = filepath.read_text(encoding="utf-8", errors="replace") + except Exception: + return None + + circuit = SpiceCircuit(filename=filepath.name) + + current_symbol = None + for line in text.splitlines(): + line = line.strip() + if line.startswith("SYMBOL"): + parts = line.split() + current_symbol = {"type": parts[1] if len(parts) > 1 else "unknown"} + elif line.startswith("SYMATTR") and current_symbol is not None: + parts = line.split(None, 2) + if len(parts) >= 3: + current_symbol[parts[1]] = parts[2] + if parts[1] == "Value": + circuit.components.append(current_symbol) + current_symbol = None + elif line.startswith("WIRE"): + pass # wire count + elif line.startswith("TEXT") and ".tran" in line.lower(): + circuit.directives.append(line) + elif line.startswith("TEXT") and any(d in line.lower() for d in [".ac", ".dc", ".param", ".model", ".include"]): + circuit.directives.append(line) + + return circuit if circuit.components else None + + +# --------------------------------------------------------------------------- +# ChatML JSONL generation +# --------------------------------------------------------------------------- + +def _msg(role: str, content: str) -> dict: + return {"role": role, "content": content.strip()} + + +def _qa(question: str, answer: str) -> dict: + return {"messages": [_msg("user", question), _msg("assistant", answer)]} + + +def generate_kicad_qa_pairs(sheets: List[SchematicSheet]) -> List[dict]: + """Generate ChatML Q&A pairs from parsed KiCad schematics.""" + pairs = [] + + # Build cross-sheet data structures + all_components: Dict[str, List[Component]] = {} # sheet_name -> components + all_labels: Dict[str, List[str]] = {} + component_by_ref: Dict[str, Tuple[Component, str]] = {} + + for sheet in sheets: + sheet_name = sheet.filename.replace(".kicad_sch", "") + all_components[sheet_name] = sheet.components + all_labels[sheet_name] = sheet.labels + for comp in sheet.components: + if comp.reference: + component_by_ref[comp.reference] = (comp, sheet_name) + + # --- Per-sheet component listing --- + for sheet in sheets: + sheet_name = sheet.filename.replace(".kicad_sch", "") + if not sheet.components: + continue + + # Filter out power symbols for component questions + real_components = [c for c in sheet.components if not c.lib_id.startswith("power:")] + if not real_components: + continue + + comp_list = "\n".join( + f"- {c.reference}: {c.value} ({c.footprint})" + for c in real_components if c.reference + ) + + pairs.append(_qa( + f"What components are used in the Hypnoled {sheet_name} circuit?", + f"The Hypnoled {sheet_name} sub-schematic contains the following components:\n\n{comp_list}\n\n" + f"Total: {len(real_components)} components in {sheet_name}.kicad_sch." + )) + + # Component count question + pairs.append(_qa( + f"How many components are in the Hypnoled {sheet_name} schematic?", + f"The {sheet_name} sub-schematic contains {len(real_components)} active components " + f"and {len(sheet.power_symbols)} power symbols, " + f"with {sheet.wires} wire connections." + )) + + # --- DALI-specific questions --- + dali_comps = all_components.get("DALI", []) + if dali_comps: + dali_real = [c for c in dali_comps if not c.lib_id.startswith("power:")] + comp_summary = ", ".join(f"{c.reference} ({c.value})" for c in dali_real if c.reference) + pairs.append(_qa( + "What components are used in the DALI bus interface circuit of Hypnoled?", + f"The DALI bus interface circuit uses these components: {comp_summary}.\n\n" + "Key elements include:\n" + "- 1SMA4746 Zener diode for voltage protection\n" + "- BSS123 N-MOSFET for DALI bus TX signaling\n" + "- EL357N optocoupler for galvanic isolation on RX\n" + "- MB10S bridge rectifier for DALI 16V bus rectification\n" + "- STN1HNK60 high-voltage MOSFET (600V) for DALI driver\n" + "- Resistor dividers for bus voltage sensing" + )) + + pairs.append(_qa( + "How does the Hypnoled DALI transmitter work?", + "The Hypnoled DALI TX uses Manchester encoding through an N-MOSFET (BSS123) " + "that modulates the DALI bus voltage. The ESP32 GPIO drives the MOSFET gate " + "to pull the bus low (logical 1) or release it high (logical 0). " + "A high-voltage MOSFET (STN1HNK60, 600V rated) provides the bus driver stage. " + "The bus is protected by a 1SMA4746 Zener diode for overvoltage transients." + )) + + pairs.append(_qa( + "How does the Hypnoled DALI receiver work?", + "The DALI RX circuit uses an EL357N optocoupler for galvanic isolation between " + "the DALI bus (up to 22.5V) and the ESP32 3.3V logic. A resistor divider feeds " + "the bus voltage to the optocoupler LED side. The phototransistor output connects " + "to an ESP32 GPIO configured as an interrupt input for Manchester decoding. " + "BC857 PNP transistor and STN93003 provide signal conditioning." + )) + + # --- ESP32 questions --- + esp32_comps = all_components.get("esp32", []) + if esp32_comps: + esp32_real = [c for c in esp32_comps if not c.lib_id.startswith("power:")] + comp_list = "\n".join(f"- {c.reference}: {c.value}" for c in esp32_real if c.reference) + pairs.append(_qa( + "How is the ESP32 connected in the Hypnoled project?", + f"The ESP32 sub-schematic in Hypnoled contains:\n\n{comp_list}\n\n" + "The ESP32 serves as the main MCU, handling:\n" + "- DALI bus communication (TX via GPIO + MOSFET, RX via optocoupler ISR)\n" + "- I2C bus for UI (MPR121 touch sensor) and audio (PCM5122 DAC)\n" + "- WiFi connectivity for MQTT remote control\n" + "- UART for debug and programming\n" + "- USB-C for power and data" + )) + + pairs.append(_qa( + "What peripherals does the Hypnoled ESP32 drive?", + "The ESP32 in Hypnoled drives several peripherals:\n" + "1. DALI bus interface (GPIO for TX Manchester encoding, interrupt for RX)\n" + "2. I2C bus shared between MPR121 touch controller (UI) and PCM5122 audio DAC\n" + "3. LED strip output through audio2led reactive driver (IRF3415 MOSFET)\n" + "4. WiFi for MQTT-based remote control\n" + "5. USB-C interface for programming and power delivery" + )) + + # --- Power supply questions --- + power_comps = all_components.get("MCP_power", []) + if power_comps: + power_real = [c for c in power_comps if not c.lib_id.startswith("power:")] + comp_list = "\n".join(f"- {c.reference}: {c.value}" for c in power_real if c.reference) + pairs.append(_qa( + "What is the power supply architecture of the Hypnoled project?", + f"The MCP_power sub-schematic handles power distribution:\n\n{comp_list}\n\n" + "The power architecture provides multiple voltage rails:\n" + "- Main input from DALI bus (rectified via MB10S bridge)\n" + "- 3.3V rail for ESP32 and digital logic\n" + "- 5V rail for USB and LED driver\n" + "- Analog supply for audio DAC (PCM5122)\n\n" + "Note: Forge review identified the power chain as potentially incoherent — " + "verify the cascade regulation stages match the load requirements." + )) + + pairs.append(_qa( + "What voltage regulators are used in Hypnoled's power supply?", + f"The MCP_power schematic includes these power components:\n\n{comp_list}\n\n" + "The power supply converts the DALI bus voltage (16V typical) down to " + "the required logic and analog levels through a multi-stage regulation chain." + )) + + # --- Audio questions --- + audio_comps = all_components.get("audio", []) + if audio_comps: + audio_real = [c for c in audio_comps if not c.lib_id.startswith("power:")] + comp_list = "\n".join(f"- {c.reference}: {c.value}" for c in audio_real if c.reference) + pairs.append(_qa( + "What audio components are in the Hypnoled project?", + f"The audio sub-schematic contains:\n\n{comp_list}\n\n" + "The audio path uses a PCM5122 DAC connected to the ESP32 via I2C for control " + "and I2S for audio data. The DAC output feeds an amplifier stage for the " + "hypnotherapy audio output. Forge review flagged the PCM5122 power supply as " + "a critical point to verify." + )) + + # --- Audio2LED questions --- + a2l_comps = all_components.get("audio2led", []) + if a2l_comps: + a2l_real = [c for c in a2l_comps if not c.lib_id.startswith("power:")] + comp_list = "\n".join(f"- {c.reference}: {c.value}" for c in a2l_real if c.reference) + pairs.append(_qa( + "How does the Hypnoled audio-reactive LED driver work?", + f"The audio2led sub-schematic contains:\n\n{comp_list}\n\n" + "The audio2led circuit converts audio signals into LED brightness modulation. " + "An IRF3415 MOSFET drives the LED strip based on audio amplitude. " + "Forge review noted the IRF3415 may be over-dimensioned for the application — " + "consider a logic-level MOSFET if the gate drive voltage is limited to 3.3V." + )) + + # --- UI questions --- + ui_comps = all_components.get("UI", []) + if ui_comps: + ui_real = [c for c in ui_comps if not c.lib_id.startswith("power:")] + comp_list = "\n".join(f"- {c.reference}: {c.value}" for c in ui_real if c.reference) + pairs.append(_qa( + "What user interface components does Hypnoled use?", + f"The UI sub-schematic contains:\n\n{comp_list}\n\n" + "The UI is based on an MPR121 capacitive touch sensor connected via I2C. " + "This provides up to 12 touch-sensitive electrodes for user interaction " + "(mode selection, brightness, audio volume). Forge review flagged that " + "the MPR121 I2C address must be explicitly configured to avoid bus conflicts." + )) + + # --- Cross-schematic / architecture questions --- + total_comps = sum( + len([c for c in comps if not c.lib_id.startswith("power:")]) + for comps in all_components.values() + ) + sheet_names = [s.filename.replace(".kicad_sch", "") for s in sheets if s.components] + + pairs.append(_qa( + "What is the overall architecture of the Hypnoled PCB project?", + f"Hypnoled is a multi-sheet KiCad design with {len(sheets)} sub-schematics:\n" + f"{', '.join(sheet_names)}.\n\n" + f"Total component count: {total_comps} active components.\n\n" + "Architecture overview:\n" + "- **DALI**: Bus interface with Manchester encoding TX/RX, optocoupler isolation\n" + "- **esp32**: Main MCU with WiFi, I2C, UART, USB-C\n" + "- **audio**: PCM5122 DAC for hypnotherapy audio output\n" + "- **audio2led**: Audio-reactive LED strip driver (MOSFET-based)\n" + "- **MCP_power**: Multi-rail power supply from DALI bus input\n" + "- **UI**: MPR121 capacitive touch interface\n\n" + "The project was designed by L'electron rare for client Richard Garnier." + )) + + pairs.append(_qa( + "What are the main design issues found in the Hypnoled hardware review?", + "A Forge (Codestral) review of all 6 Hypnoled sub-schematics identified:\n\n" + "**8 Critical issues:**\n" + "1. Zener diode voltage (15V) may be insufficient for DALI bus transients\n" + "2. Double ESP32 in schematic not justified — remove redundant instance\n" + "3. USB-C CC resistors missing or misconfigured\n" + "4. PCM5122 analog power supply filtering inadequate\n" + "5. IRF3415 MOSFET over-dimensioned for 3.3V gate drive\n" + "6. Power chain architecture incoherent (MCP_power stage cascade)\n" + "7. MPR121 I2C address not configured (potential bus conflict)\n" + "8. Second power regulation incoherence in MCP_power\n\n" + "**7 Warnings** about component tolerances and thermal considerations.\n" + "**18 Recommendations** for improved EMC, reliability, and manufacturability." + )) + + # --- Net / label questions --- + all_label_set = set() + for labels in all_labels.values(): + all_label_set.update(labels) + if all_label_set: + label_list = ", ".join(sorted(all_label_set)[:30]) + pairs.append(_qa( + "What are the main signal nets in the Hypnoled schematic?", + f"The Hypnoled schematics use these signal labels across sub-sheets:\n" + f"{label_list}\n\n" + "These nets connect the sub-schematics through hierarchical labels, " + "enabling the ESP32 to communicate with the DALI interface, audio DAC, " + "LED driver, touch UI, and power management sections." + )) + + # --- Footprint / manufacturing questions --- + footprints = set() + for sheet in sheets: + for comp in sheet.components: + if comp.footprint and not comp.lib_id.startswith("power:"): + footprints.add(comp.footprint) + if footprints: + fp_list = "\n".join(f"- {fp}" for fp in sorted(footprints)[:25]) + pairs.append(_qa( + "What PCB footprints are used in the Hypnoled design?", + f"The Hypnoled design uses these footprints:\n\n{fp_list}\n\n" + "The mix of SMD and through-hole footprints indicates a design that " + "balances automated assembly (SMD) with manual soldering for connectors " + "and power components." + )) + + # --- General project info --- + pairs.append(_qa( + "What is the Hypnoled project?", + "Hypnoled is an IoT-connected LED hypnotherapy device designed by L'electron rare " + "for client Richard Garnier. It combines:\n" + "- DALI bus integration for professional lighting control\n" + "- ESP32 WiFi/BLE MCU for smart control via MQTT\n" + "- Audio output (PCM5122 DAC) for hypnotherapy sessions\n" + "- Audio-reactive LED effects (audio2led MOSFET driver)\n" + "- Capacitive touch UI (MPR121)\n\n" + "The hardware is designed in KiCad 9.0 with 6 hierarchical sub-schematics " + "and uses a custom DALI interface with Manchester encoding." + )) + + pairs.append(_qa( + "What KiCad version is the Hypnoled project designed in?", + "Hypnoled is designed in KiCad 9.0 (generator version 9.0, schema version 20250114). " + "The project uses hierarchical schematics with 6 sub-sheets: " + "DALI, esp32, audio, audio2led, MCP_power, and UI. " + "The design is revision 0.1 dated 2026-02-10." + )) + + return pairs + + +def generate_spice_qa_pairs(circuits: List[SpiceCircuit]) -> List[dict]: + """Generate ChatML Q&A pairs from parsed SPICE/LTspice simulations.""" + pairs = [] + + for circuit in circuits: + name = circuit.filename.replace(".asc", "") + + comp_list = "\n".join( + f"- {c.get('InstName', 'unknown')}: {c.get('type', '?')} = {c.get('Value', '?')}" + for c in circuit.components + ) + + pairs.append(_qa( + f"What components are in the Hypnoled '{name}' SPICE simulation?", + f"The LTspice simulation '{name}' contains:\n\n{comp_list}" + )) + + if circuit.directives: + dir_list = "\n".join(f"- {d}" for d in circuit.directives) + pairs.append(_qa( + f"What simulation directives are used in the Hypnoled '{name}' circuit?", + f"The '{name}' simulation uses these directives:\n\n{dir_list}" + )) + + pairs.append(_qa( + f"What does the Hypnoled '{name}' simulation model?", + f"The '{name}' LTspice simulation models the Hypnoled LED driver circuit. " + f"It contains {len(circuit.components)} components and is used to verify " + "the LED driving waveforms, power dissipation, and transient behavior " + "before PCB fabrication." + )) + + # General SPICE question if we have any circuits + if circuits: + pairs.append(_qa( + "What simulations exist for the Hypnoled project?", + f"The Hypnoled project includes {len(circuits)} LTspice simulation(s):\n" + + "\n".join(f"- {c.filename}: {len(c.components)} components" for c in circuits) + + "\n\nThese simulations validate the LED driver circuit design before PCB fabrication." + )) + + return pairs + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="Extract Hypnoled datasets for Mistral fine-tuning") + parser.add_argument( + "--clone-dir", + type=Path, + default=Path("/tmp/hypnoled-datasets"), + help="Path to local clone of electron-rare/hypnoled", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=None, + help="Output base directory (default: auto-detect tools/mistral/datasets/)", + ) + args = parser.parse_args() + + clone_dir = args.clone_dir + if not clone_dir.exists(): + print(f"ERROR: Clone directory not found: {clone_dir}", file=sys.stderr) + print("Run: git clone https://github.com/electron-rare/hypnoled.git /tmp/hypnoled-datasets", file=sys.stderr) + sys.exit(1) + + # Auto-detect output dir relative to this script + if args.output_dir: + output_base = args.output_dir + else: + script_dir = Path(__file__).resolve().parent + output_base = script_dir / "datasets" + + kicad_out = output_base / "hypnoled_kicad" + spice_out = output_base / "hypnoled_spice" + + # --- Parse KiCad schematics --- + pcb_dir = clone_dir / "hardware" / "pcb" + kicad_files = sorted(pcb_dir.glob("*.kicad_sch")) if pcb_dir.exists() else [] + + if not kicad_files: + print(f"WARNING: No .kicad_sch files found in {pcb_dir}", file=sys.stderr) + else: + print(f"Found {len(kicad_files)} KiCad schematics:") + for f in kicad_files: + print(f" - {f.name}") + + sheets = [] + for kf in kicad_files: + try: + sheet = parse_kicad_sch(kf) + sheets.append(sheet) + print(f" Parsed {kf.name}: {len(sheet.components)} components, " + f"{sheet.wires} wires, {len(sheet.labels)} labels") + except Exception as e: + print(f" ERROR parsing {kf.name}: {e}", file=sys.stderr) + + # Generate KiCad Q&A + kicad_pairs = generate_kicad_qa_pairs(sheets) + kicad_out.mkdir(parents=True, exist_ok=True) + kicad_jsonl = kicad_out / "train.jsonl" + with open(kicad_jsonl, "w", encoding="utf-8") as f: + for pair in kicad_pairs: + f.write(json.dumps(pair, ensure_ascii=False) + "\n") + print(f"\nKiCad dataset: {len(kicad_pairs)} Q&A pairs -> {kicad_jsonl}") + + # --- Parse SPICE simulations --- + sim_dir = clone_dir / "hardware" / "simulation" + asc_files = sorted(sim_dir.glob("*.asc")) if sim_dir.exists() else [] + + # Also check for .spice / .cir files + for ext in ("*.spice", "*.cir"): + if sim_dir.exists(): + asc_files.extend(sorted(sim_dir.glob(ext))) + + circuits = [] + if asc_files: + print(f"\nFound {len(asc_files)} SPICE simulation files:") + for af in asc_files: + print(f" - {af.name}") + circuit = parse_ltspice_asc(af) + if circuit: + circuits.append(circuit) + print(f" Parsed: {len(circuit.components)} components, " + f"{len(circuit.directives)} directives") + else: + print("\nNo SPICE simulation files found (no .asc/.spice/.cir in hardware/simulation/).") + + # Generate SPICE Q&A (even if empty, create the file) + spice_pairs = generate_spice_qa_pairs(circuits) + spice_out.mkdir(parents=True, exist_ok=True) + spice_jsonl = spice_out / "train.jsonl" + if spice_pairs: + with open(spice_jsonl, "w", encoding="utf-8") as f: + for pair in spice_pairs: + f.write(json.dumps(pair, ensure_ascii=False) + "\n") + print(f"SPICE dataset: {len(spice_pairs)} Q&A pairs -> {spice_jsonl}") + else: + # Write empty file with a note + with open(spice_jsonl, "w", encoding="utf-8") as f: + f.write("") # Empty — no SPICE files found in clone + print(f"SPICE dataset: 0 Q&A pairs (no simulation files found) -> {spice_jsonl}") + + # --- Summary --- + print(f"\n{'='*60}") + print(f"SUMMARY") + print(f"{'='*60}") + print(f" KiCad schematics parsed : {len(sheets)}") + print(f" KiCad Q&A pairs : {len(kicad_pairs)}") + print(f" SPICE simulations parsed: {len(circuits)}") + print(f" SPICE Q&A pairs : {len(spice_pairs)}") + print(f" Total training examples : {len(kicad_pairs) + len(spice_pairs)}") + print(f"{'='*60}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main())