Tim's DC Dual Curve Tracer

by Palingenesis in Circuits > Tools

136 Views, 1 Favorites, 0 Comments

Tim's DC Dual Curve Tracer

Tims Curve Tracer 001.png

🧪 Types of Curve Tracers

There are several kinds of curve tracers, each suited to different tasks:

  1. DC Curve Tracers: Use a ramp or sawtooth voltage sweep to test static I-V characteristics. Ideal for diodes, BJTs, and basic silicon junctions.
  2. AC Curve Tracers: Use sine wave or triangle waveforms to analyze reactive components like capacitors and inductors.
  3. Pulse-Based Tracers: Designed for high-speed switching devices (MOSFETs, IGBTs), simulating real-world load conditions.
  4. Oscilloscope-Driven Tracers: Use X-Y plotting to visualize live waveforms, often with external signal generators.

Each type has its strengths — but many require complex setups, expensive gear, or deep technical knowledge.

🔧 What This Project Is

This build is a Basic (Limited) Dual-Probe Curve Tracer, designed to be:

  1. Simple: Uses a basic circuit with an ADC and microcontroller
  2. Accessible: Runs entirely in-browser via a Wi-Fi-enabled microcontroller
  3. Teachable: Includes ghost trace overlays, save/load/export logic, and a clean UI
  4. Limited: Focuses on DC analysis of small components like diodes and BJTs — not designed for reactive parts or high-voltage breakdown

It’s powered by an ESP8266, uses an ADS1115 ADC, and communicates via a built-in web server. The goal is to make curve tracing approachable for makers, students, and hobbyists — without needing an oscilloscope or lab bench.

🧠 Why I Built It This Way

I wanted a tool that could:

  1. Run on low-cost hardware
  2. Be used wirelessly from any browser
  3. Teach core diagnostic principles without overwhelming complexity

This version is intentionally limited — but it’s a solid foundation for future extensions like bipolar sweeps, ghost trace comparison, and batch validation.

🔧 Upgradable by Design

While this version focuses on basic DC analysis, the setup is intentionally designed to be expandable. For example, future upgrades could include:

  1. H-Bridge integration for automatic polarity switching
  2. Bipolar sweep logic to capture full I-V curves in both directions
  3. Reverse breakdown testing with current limiting
  4. Batch trace comparison for anomaly detection

These enhancements build on the same core architecture — a microcontroller, ADC, and browser-native UI — making it easy to evolve the tool without starting from scratch.

Supplies

ESP8266_2.png
ADS1115.png
Resistors.png
Capacitor Kit.png
830 Solderless Breadboard.png
Cable Breadboard.png
Tims Curve Tracer 003.png

Supplies for Tim’s Basic (Limited) Dual-Probe Curve Tracer


Core Components

  1. ESP8266 microcontroller (NodeMCU Module) → Handles Wi-Fi, web server, and UI logic.
  2. ADS1115 16-bit ADC module → Measures voltage drop across the shunt with high precision.


Measurement Circuit

  1. 2 × Shunt resistors (10Ω) → One for each probe, enabling current sensing via voltage drop.
  2. 2 × 1kΩ resistors → Isolate each probe from the sweep voltage supply.
  3. 2 × Test probes or jumper wires → Connect to the DUT (Device Under Test).
  4. 1 × Power supply (Power will be from the 3.3v Linear regulator on the NodeMCU Module) → Drives the sweep voltage across the DUT.


Signal Conditioning & Filtering

  1. 1 × NPN transistor (e.g., 2N3904 or BC547) → For sweep control.
  2. 1 × 1kΩ resistor → Current limiter from the NPN transistor.
  3. 1 × RC filter:
  4. 1 × resistor (1kΩ)
  5. 1 × capacitor (1µF ceramic) → Smooths PWM or sweep signal before reaching the DUT
  6. 2 × 4.7kΩ resistors → I²C pull-up resistors for SDA and SCL lines (required for stable communication with ADS1115).


Breadboarding & Connectivity

  1. 1 × Solderless breadboard → For prototyping the entire circuit.
  2. Jumper wires (male-male and male-female) → For flexible connections between modules.
  3. USB cable (for ESP8266 power and programming)


️ ️Software & UI

  1. Web browser (Chrome, Firefox, etc.) → Displays the curve tracer UI.
  2. Serial monitor or uploader (Arduino IDE) → For flashing the ESP8266 and debugging.

🧭 Schematic

Tims Curve Tracer_bb.png
Tims Curve Tracer_schem.png

I’ve created the circuit using Fritzing, so you can easily build this project on a solderless breadboard. The schematic shows all connections between the ESP8266, ADS1115, shunt resistors, RC filter, and I²C pull-ups — making it simple to follow and replicate.

You’ll find the following files attached:

  1. .fzz — editable Fritzing project file
  2. .png — breadboard layout image for quick reference
  3. .pdf — printable schematic for offline use or documentation

This layout is designed to be:

  1. Compact — fits on a standard breadboard
  2. Modular — easy to upgrade later (e.g., H-bridge, bipolar sweep)
  3. Teachable — with clear labeling and intuitive signal flow

If you're new to Fritzing, you can open the .fzz file in the Fritzing app to explore the circuit interactively or modify it for your own experiments.

Arduino IDE

Tims Curve Tracer 003.png

I would have attached all the source code files directly to this Instructable, but not all file types are accepted due to upload restrictions on the site (yes, I’ve asked!). So instead, I’ve included the full source code by creating a dedicated step for each file. This way, you can view and copy the code directly without needing external downloads.

The only exception is the favicon.ico file, which isn’t shown due to file type restrictions.

To use the code:

  1. Just create a new file in your project folder
  2. Name it the same as the step title
  3. Paste the code from that step into the file
  4. ⚠️ You may need to enable “View file extensions” in your operating system to ensure the file gets the correct extension (e.g., .html, .css, .js, .ino). This is especially important if your system hides extensions by default — otherwise, you might accidentally save files as .txt instead.


📦 What Is LittleFS?

LittleFS is a lightweight filesystem for ESP8266 and ESP32 boards. It allows you to store and serve files (like HTML, CSS, JavaScript, and trace data) directly from the microcontroller’s flash memory — perfect for browser-based UI projects like this curve tracer.

🛠️ How to Install LittleFS in Arduino IDE

  1. Open the Arduino IDE
  2. Go to Tools → Board → Boards Manager
  3. Search for ESP8266 and install the platform (if not already installed)
  4. Once installed, go to Tools → ESP8266 Sketch Data Upload
  5. If you don’t see this option, install the ESP8266FS tool:
  6. Download it from GitHub
  7. Place it in your Arduino IDE’s tools folder
  8. Restart the IDE

📁 What Gets Uploaded

When you compile and upload the project, two things happen:

1. Sketch Upload

  1. Your main .ino file (ESP8266 logic, ADC sampling, sweep control, etc.)

2. LittleFS Upload

  1. All files inside the /data folder:
  2. index.html → The browser-based UI
  3. style.css → UI styling
  4. script.js → Sweep logic, ghost trace overlays, and user interaction
  5. Any trace data or configuration files you want to preload

To upload these files:

  1. Go to Tools → ESP8266 Sketch Data Upload
  2. This flashes the /data folder contents into the ESP8266’s flash memory using LittleFS

Tims_Curve_Tracer.ino

Code_ino.png

📄 Tims_Curve_Tracer.ino

Create a new file named Tims_Curve_Tracer.ino, then paste the following code into it and save.

💡 Important: All project files must be placed inside a folder named exactly Tims_Curve_Tracer — this matches the .ino filename and is required by the Arduino IDE.

Inside this folder, create a sub-folder named data. Place all the LittleFS files (like index.html, style.css, script.js) inside the data folder. This ensures they’re correctly uploaded to the ESP8266’s flash memory using the ESP8266 Sketch Data Upload tool.

💡 Tip: Make sure your system is set to “View file extensions” so you can confirm each file is saved with the correct extension (e.g., .ino, .html, .css, .js).

This .ino file contains the core logic for:

  1. Sweep generation
  2. ADC sampling
  3. Serial and web communication
  4. UI status feedback

Once uploaded, it coordinates the entire curve tracing workflow.



/*
Tims_Curve_Tracer.ino
--------------------
An ESP8266-based curve tracer for semiconductors.
Uses an ADS1115 ADC to measure voltage and current while sweeping a PWM signal.
Serves a web interface with real-time plotting via WebSockets.
--------------------
Created by Tim jackso.1960, June 2025.

This is the firmware code for the ESP8266 microcontroller.
Dependencies:
index.html, styles.css, scripts.js (served via LittleFS)

*/

#include <Wire.h> /* credit: Arduino Team. https://github.com/arduino/ArduinoCore-avr */
#include <ADS1115.h> /* credit: Baruch Even. https://github.com/baruch/ADS1115 */
#include <LittleFS.h> /* credit: ESP8266 Arduino Core Team. https://github.com/esp8266/Arduino/tree/master/libraries/LittleFS */
#include <ESP8266WiFi.h> /* credit: ESP8266 Arduino Core Team. https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi */
#include <ESP8266WebServer.h> /* credit: ESP8266 Arduino Core Team. https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WebServer */
#include <WebSocketsServer.h> /* credit: Markus Sattler. https://github.com/Links2004/arduinoWebSockets */
#include "Credentials.h" /* credit: Local network login details (private) */

#define PWM_PIN 12 /* D6 = GPIO 12 */
#define ADS_SCL 5 /* D1 = GPIO 5 */
#define ADS_SDA 4 /* D2 = GPIO 4 */

#define PWM_RESOLUTION 1024 /* 10-bit PWM range (0�1023) */
#define PWM_STEP 20 /* Step size per sweep increment */
#define SWEEP_SAMPLES (PWM_RESOLUTION / PWM_STEP + 1) /* Calculate Total samples */

ADS1115 ads;
ESP8266WebServer server(80);
WebSocketsServer webSocket(81);

struct SweepSample {
int16_t sweep;
int16_t a;
int16_t b;
int16_t sweepVoltage;
};

SweepSample sweepData[SWEEP_SAMPLES];
int sweepIndex = 0;
bool sweepComplete = false;

void setupWiFi() {
#ifdef ACCESS_POINT
WiFi.mode(WIFI_AP);
WiFi.softAP(ESP8266_SSID_AP);
#ifdef ENABLE_SERIAL
Serial.println("AP mode (no password)");
Serial.println(WiFi.softAPIP());
#endif
#endif

#ifdef ACCESS_POINT_PW
WiFi.mode(WIFI_AP);
WiFi.softAP(ESP8266_SSID_AP, ESP8266_PASSWORD_AP);
#ifdef ENABLE_SERIAL
Serial.println("AP mode (with password)");
Serial.println(WiFi.softAPIP());
#endif
#endif

#ifdef ACCESS_POINT_STA
WiFi.mode(WIFI_STA);
WiFi.begin(LOCAL_SSID_STA, LOCAL_PASSWORD_STA);
#ifdef ENABLE_SERIAL
Serial.print("Connecting to Wi-Fi");
#endif
while (WiFi.status() != WL_CONNECTED) {
delay(500);
#ifdef ENABLE_SERIAL
Serial.print(".");
#endif
}
#ifdef ENABLE_SERIAL
Serial.println();
Serial.print("Connected! IP: ");
Serial.println(WiFi.localIP());
#endif
#endif
}

void setup() {

#ifdef ENABLE_SERIAL
/* Serial */
Serial.begin(115200);
#endif

setupWiFi();
/* Wire */
Wire.begin(ADS_SDA, ADS_SCL);

/* ADS */
ads.begin();
ads.set_data_rate(ADS1115_DATA_RATE_860_SPS);
ads.set_mode(ADS1115_MODE_SINGLE_SHOT);
ads.set_mux(ADS1115_MUX_GND_AIN1);
ads.set_pga(ADS1115_PGA_TWO);

/* PWM */
pinMode(PWM_PIN, OUTPUT);
analogWriteRange(PWM_RESOLUTION - 1);
analogWriteFreq(1000);
analogWrite(PWM_PIN, 255);

/* LittleFS */
LittleFS.begin();

/* Web Server */
server.serveStatic("/", LittleFS, "/index.html", "text/html");
server.serveStatic("/styles.css", LittleFS, "/styles.css", "text/css");
server.serveStatic("/scripts.js", LittleFS, "/scripts.js", "text/javascript");
server.serveStatic("/favicon.ico", LittleFS, "/favicon.ico", "image/x-icon");

server.begin();

/* WebSocket */
webSocket.begin();
webSocket.onEvent([](uint8_t client, WStype_t type, uint8_t* payload, size_t length) {
if (type == WStype_CONNECTED) {
#ifdef ENABLE_SERIAL
Serial.println("WebSocket client connected");
#endif
}
});
}


void loop() {

static int sweepVal = 0;

if (sweepVal >= 1023) {
sweepVal = 1023;
sweepComplete = true;
}

analogWrite(PWM_PIN, sweepVal);

int16_t sweepVoltage = readADSChannel(ADS1115_MUX_GND_AIN0);
int16_t a = readADSChannel(ADS1115_MUX_GND_AIN1);
int16_t b = readADSChannel(ADS1115_MUX_GND_AIN2);

if (sweepIndex < SWEEP_SAMPLES) {
sweepData[sweepIndex].sweep = sweepVal;
sweepData[sweepIndex].a = a;
sweepData[sweepIndex].b = b;
sweepData[sweepIndex].sweepVoltage = sweepVoltage;
sweepIndex++;
}
sweepVal += PWM_STEP;

if (sweepComplete && sweepIndex >= SWEEP_SAMPLES) {

int16_t sweepVoltageMax = INT16_MIN;
for (int16_t i = 0; i < sweepIndex; i++) {
if (sweepData[i].sweepVoltage > sweepVoltageMax) {
sweepVoltageMax = sweepData[i].sweepVoltage;
}
}

String json = "{\"samples\":[";
for (int i = 0; i < sweepIndex; i++) {
json += "{\"sweep\":" + String(sweepData[i].sweep) +
",\"a\":" + String(sweepData[i].a) +
",\"b\":" + String(sweepData[i].b) + "}";
if (i < sweepIndex - 1) json += ",";
}
json += "],\"sweepVoltageMax\":" + String(sweepVoltageMax) + "}";
webSocket.broadcastTXT(json);

sweepIndex = 0;
sweepVal = 0;
sweepComplete = false;
}

server.handleClient();
webSocket.loop();
}

int16_t readADSChannel(ads1115_mux mux) {
ads.set_mux(mux);
if (ads.trigger_sample() != 0) {
/* Optional: handle error */
return 0;
}
while (ads.is_sample_in_progress()) {
delayMicroseconds(50); /* wait for conversion */
}
return ads.read_sample(); /* raw int16_t result */
}

Credentials.h

Code_h.png

📄 Credentials.h

Create a new file named Credentials.h, then paste the following code into it and save. You’ll then need to edit this file depending on your preferred Wi-Fi connection mode. It controls how the ESP8266 connects to a network — either by creating its own (Access Point mode) or joining your existing Wi-Fi (Station mode).

There are three options, but you should only enable one at a time by uncommenting the corresponding line:

🔹 Access Point (AP) Mode

  1. ESP8266 creates its own Wi-Fi network
  2. You connect directly to it (e.g., from your phone)
  3. No internet required — great for portable use
  4. Set #define ACCESS_POINT

🔹 Access Point with Password

  1. Same as AP mode, but requires a password
  2. Set #define ACCESS_POINT_PW

🔹 Station (STA) Mode — Recommended

  1. ESP8266 connects to your existing home Wi-Fi
  2. You access the curve tracer from any device on your network
  3. Set #define ACCESS_POINT_STA


✏️ What to Edit

If you choose Station (STA) mode, you must enter your Wi-Fi credentials:

#define LOCAL_SSID_STA "Your Network Name Here"
#define LOCAL_PASSWORD_STA "Your Network Password Here"

Replace the placeholder text with your actual Wi-Fi name and password.


🖥️ Serial Monitor Tip

Make sure #define ENABLE_SERIAL stays defined. This allows the ESP8266 to print its assigned IP address to the serial monitor when it boots — especially useful in STA mode, where the IP is assigned by your router.



#pragma once

/*
Type of connection?
"Access Point", "Access Point with password" or "Station"

"Access Point (AP) mode"
This is not connected to your local network, you connect to the Network of the ESP8266.
This means you can connect to it with your Moble Phone anywhare you are.
You change the WiFi your Phone is connected to, to the ESP8266 WiFi.
Then open browser to the IP of the ESP8266 control page. (we will be setting it to 192.168.50.11)
Have your serial monitor connected when you re-set the module to confirm correct IP Address.

"Access Point with Pasword (AP) mode"
This is same as above, but you need a password to connect to the WiFi. (it currently is: 12345678)

"Station (STA) mode"
In STA mode, the ESP8266 connects to an existing WiFi network created by your wireless router.
You will need to set the access credentials for your local network.
This is the recommended setting as it will enable you to use a web browser any device currently connected to your network.

Un-Comment which way you want to connect. (1 choice only)
*/

#define ACCESS_POINT_STA
// #define ACCESS_POINT
// #define ACCESS_POINT_PW

/*
These are made up for the websocket.
*/
#define ESP8266_SSID_AP "CurveTracer"
#define ESP8266_PASSWORD_AP "tracer123"

/*
The name of your local network.
The Password.

These are required if using the "Station (STA) mode".
*/
#define LOCAL_SSID_STA "Your Network Name Here" /* Your local network name. */
#define LOCAL_PASSWORD_STA "Your Network Password Here" /* Your local network password. */

#define ENABLE_SERIAL

Index.html

Code_html.png

📄 index.html

Create a new file named index.html, then paste the following code into it and save.

📁 Important: This file must be placed inside the data folder, which lives inside your main project folder (Tims_Curve_Tracer/data). This ensures it gets uploaded correctly to the ESP8266’s flash memory using LittleFS.

This file is the heart of the browser-based UI — it defines the layout, buttons, and structure of the curve tracer interface.



<!DOCTYPE html>
<html>
<head>
<title>Curve Tracer</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<h1 id="Title">
Tim's Basic (Limited)<br />
Dual-Probe Curve Tracer
</h1>
<div id="canvasContainer">
<img id="curveImage" width="600" height="600" />
<img id="ghostImage" width="600" height="600" />
</div>

<div class="buttonRow">
<button id="ghostPlus">Create<br />Ghost</button>
<button id="ghostMinus">Remove<br />Ghost</button>
<button id="ghostSave">Save<br />Ghost</button>
<button id="ghostLoad">Load<br />Ghost</button>
<button id="downLoad">Down<br />Load</button>
<button id="upLoad">Up<br />Load</button>
</div>

<div id="statusLabel"></div>

<script src="/scripts.js"></script>
</body>

</html>
<footer style="font-size:0.8em; text-align:center; padding:10px; color:#888;">
Curve Tracer UI © 2025 Tim<br>
Powered by open-source libraries:<br>
<a href="https://github.com/baruch/ADS1115"
target="_blank">
ADS1115 by Baruch Even
</a> |
<a href="https://github.com/Links2004/arduinoWebSockets"
target="_blank">
WebSockets by Markus Sattler
</a> |
<a href="https://github.com/esp8266/Arduino/tree/master/libraries"
target="_blank">
ESP8266WiFi, ESP8266WebServer, LittleFS
</a>
</footer>


Styles.css

Code_css.png

📄 style.css

Create a new file named style.css, then paste the following code into it and save.

📁 Important: This file must be placed inside the data folder, located within your main project folder (Tims_Curve_Tracer/data). This ensures it gets uploaded correctly to the ESP8266’s flash memory using LittleFS.

This file controls the visual styling of the curve tracer UI — including layout, colors, fonts, and spacing — to make the interface clean and user-friendly.


body {
font-family: sans-serif;
background-color: #f8f8f8;
text-align: center;
margin: 20px;
background-color: #000;
}

h1 {
margin-bottom: 10px;
}

#Title {
color: #0f0;
}

#canvasContainer {
position: relative;
width: 600px;
height: 600px;
margin: 0 auto;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 0 20px #0f0;
background-color: #000;
}

canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
border-radius: 20px;
}
#curveImage {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}

#ghostImage {
display: none;
position: absolute;
top: 0;
left: 0;
z-index: 2;
pointer-events: none;
}
#statusLabel {
color: goldenrod;
opacity: 0.6;
margin-top: 10px;
font-weight: bold;
text-align: center;
margin-top: 10px;
}

.buttonRow {
display: flex;
justify-content: space-between;
width: 600px;
margin: 20px auto 0;
}

.buttonRow button {
flex: 1;
margin: 0 4px;
aspect-ratio: 5 / 1;
background: #222;
color: #0f0;
border: 2px solid #0f0;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
background: rgba(182, 255, 0, 0.4);
box-shadow: 2px 2px 8px #0f0;
}

.buttonRow button:hover {
background: #0f0;
color: #000;
}

Scripts.js

Code_js.png

📄 script.js

Create a new file named script.js, then paste the following code into it and save.

📁 Important: This file must be placed inside the data folder, located within your main project folder (Tims_Curve_Tracer/data). This ensures it gets uploaded correctly to the ESP8266’s flash memory using LittleFS.

This file handles the interactive logic of the curve tracer UI — including sweep control, ghost trace overlays, and user interactions with the browser interface.



/* Curve Display */
const curveImage = document.getElementById('curveImage');
const canvasB = document.createElement('canvas');
canvasB.width = curveImage.width;
canvasB.height = curveImage.height;
const ctxB = canvasB.getContext('2d');

/* Ghost Canvas for offscreen rendering */
const ghostCanvas = document.createElement('canvas');
ghostCanvas.width = canvasB.width;
ghostCanvas.height = canvasB.height;
const ghostCtx = ghostCanvas.getContext('2d');
const ghostImage = document.getElementById('ghostImage');


let sweepBuffer = [];
let lastSweep = 0;
let PlotPadding = 20; /* Padding factor for min/max plotting. Larger value = smaller margin. */

const sweepMin = -(1024 + (1023 / PlotPadding)); /* 1023 = 10 bit value */
const sweepMax = (1024 + (1023 / PlotPadding)); /* 1023 = 10 bit value */
let probeMin = -(65535 + (65535 / PlotPadding)); /* 65535 = 16 bit value */
let probeMax = (65535 + (65535 / PlotPadding)); /* 65535 = 16 bit value */
const sweepOffset = 2; /* Number of initial samples to skip and center from */

/* Setup WebSocket connection */
let socket = new WebSocket("ws://" + location.hostname + ":81");


/* Draw grid lines on canvas */
function drawGrid(ctx) {
const centerX = canvasB.width / 2;
const centerY = canvasB.height / 2;

/* Vertical lines */
for (let x = 0; x <= canvasB.width; x += 50) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvasB.height);
ctx.strokeStyle = 'rgba(0,255,0,0.4)';
ctx.lineWidth = (x === centerX) ? 2 : 1;
ctx.stroke();
}

/* Horizontal lines */
for (let y = 0; y <= canvasB.height; y += 50) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvasB.width, y);
ctx.strokeStyle = 'rgba(0,255,0,0.4)';
ctx.lineWidth = (y === centerY) ? 2 : 1;
ctx.stroke();
}
}

/* Draw the curve on canvas from data */
function drawCurve(ctx, data, ghost) {
if (data.length <= sweepOffset) return;

ctx.clearRect(0, 0, canvasB.width, canvasB.height);

if (!ghost) {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvasB.width, canvasB.height);
}

const sweepZero = data[sweepOffset - 1].sweep;

// Set alpha depending on ghost mode
const alpha = ghost ? 0.4 : 0.8;

/* Draw .a in cyan */
ctx.beginPath();
ctx.strokeStyle = `rgba(0,255,255,${alpha})`;
ctx.lineWidth = 4;
for (let i = sweepOffset; i < data.length; i++) {
let sweep = data[i].sweep - sweepZero;
let a = data[i].a;
let x = (sweep - sweepMin) / (sweepMax - sweepMin) * canvasB.width;
let y = canvasB.height - (a - probeMin) / (probeMax - probeMin) * canvasB.height;
if (i === sweepOffset) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();

/* Draw .b in yellow */
ctx.beginPath();
ctx.strokeStyle = `rgba(255,255,0,${alpha})`;
ctx.lineWidth = 4;
for (let i = sweepOffset; i < data.length; i++) {
let sweep = data[i].sweep - sweepZero;
let b = data[i].b;
let x = (sweep - sweepMin) / (sweepMax - sweepMin) * canvasB.width;
let y = canvasB.height - (b - probeMin) / (probeMax - probeMin) * canvasB.height;
if (i === sweepOffset) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}

/* Handle "Ghost" button click */
document.getElementById("ghostPlus").addEventListener("click", () => {

ghostCtx.clearRect(0, 0, ghostCanvas.width, ghostCanvas.height);
drawCurve(ghostCtx, sweepBuffer, true); /* Draw only the curve, no background or grid */
ghostImage.src = ghostCanvas.toDataURL("image/png"); /* Convert to transparent PNG */
ghostImage.style.display = "block"; /* Show the ghost image */
});

/* Handle "Remove Ghost" button click */
document.getElementById("ghostMinus").addEventListener("click", () => {

ghostImage.src = "";
ghostImage.style.display = "none"; /* Hide the ghost image */
});

/* Handle "Save Ghost" button click */
document.getElementById("ghostSave").addEventListener("click", () => {

const id = prompt("Enter ID to save this ghost trace:");
if (id) {
const dataURL = ghostCanvas.toDataURL("image/png");
localStorage.setItem(`ghost_${id}`, dataURL);
document.getElementById("statusLabel").textContent = `Ghost saved as "${id}"`;
}
});

/* Handle "Load Ghost" button click */
document.getElementById("ghostLoad").addEventListener("click", () => {

const ghostIDs = [];

/* Find all ghost_ keys */
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith("ghost_")) {
ghostIDs.push(key.replace("ghost_", ""));
}
}

if (ghostIDs.length === 0) {
alert("No saved ghost traces found.");
return;
}

/* Build a simple selection prompt */
const selection = prompt(
"Entre a ghost ID (1, 2, 3 tetc.) to load:\n" + ghostIDs.map((id, i) => `${i + 1}: ${id}`).join("\n")
);

const index = parseInt(selection) - 1;
if (!isNaN(index) && ghostIDs[index]) {
const id = ghostIDs[index];
const dataURL = localStorage.getItem(`ghost_${id}`);
ghostImage.src = dataURL;
ghostImage.style.display = "block";
document.getElementById("statusLabel").textContent = `Ghost "${id}" loaded`;
} else {
document.getElementById("statusLabel").textContent = `Invalid selection`;
}
});


/* Update the displayed image from the offscreen canvas */
function updateImageFromCanvas() {
requestAnimationFrame(() => {
const dataURL = canvasB.toDataURL('image/png');
curveImage.src = dataURL;
});
}

/* SaveGhost Image */
function saveGhostImage(id) {
const dataURL = ghostCanvas.toDataURL("image/png");
localStorage.setItem(`ghost_${id}`, dataURL);
}

/* Load GhostImage */
function loadGhostImage(id) {

const dataURL = localStorage.getItem(`ghost_${id}`);
if (dataURL) {
ghostImage.src = dataURL;
ghostImage.style.display = "block";
}
}

/* Down Load Goast Image */
document.getElementById("downLoad").addEventListener("click", () => {

const link = document.createElement("a");
link.download = "Ghost Image.png"; /* Default filename */
link.href = ghostImage.src; /* Use the displayed image */
link.click(); /* Opens Save As dialog */
document.getElementById("statusLabel").textContent = `Ghost image download triggered`;
});

/* Up Load Goast Image */
document.getElementById("upLoad").addEventListener("click", () => {

const input = document.createElement("input");
input.type = "file";
input.accept = "image/png";

input.onchange = () => {
const file = input.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
ghostImage.src = reader.result;
ghostImage.style.display = "block";
document.getElementById("statusLabel").textContent = `Ghost image loaded from file`;
};
reader.readAsDataURL(file);
}
};

input.click(); /* Opens file picker */
});




/* WebSocket message handler */
socket.onmessage = (event) => {
let obj = JSON.parse(event.data);

/* If it's the full sweep object */
if (obj.samples && obj.sweepVoltageMax !== undefined) {
let samples = obj.samples;
let sweepVoltageMax = obj.sweepVoltageMax;

probeMin = -(sweepVoltageMax + (sweepVoltageMax / PlotPadding));
probeMax = (sweepVoltageMax + (sweepVoltageMax / PlotPadding));

sweepBuffer = samples; /* Update the Ghoast Image sweep buffer */
drawCurve(ctxB, samples, false);
drawGrid(ctxB);
updateImageFromCanvas();
}
};

Favicon.ico

Tims Curve Tracer 001.png

📄 favicon.ico

Web browsers use a favicon to display a small icon in the tab next to your page title — it’s a nice touch for polish and branding.

Above is an image you can use to create your own favicon. To do this:

  1. Save the image above as favicon.png
  2. Go to any online favicon generator (e.g., favicon.io, realfavicongenerator.net, or similar)
  3. Upload the saved image
  4. Download the generated .ico file
  5. Rename it (if needed) to favicon.ico
  6. Place it inside the data folder (Tims_Curve_Tracer/data)

📁 Once uploaded using the ESP8266 Sketch Data Upload tool, the favicon will be served by the web server and shown in your browser’s tab when you access the curve tracer UI.

🚀Getting Started

Tims Curve Tracer 004.png
Tims Curve Tracer 005.png
Tims Curve Tracer 006.png
Tims Curve Tracer 007.png
Tims Curve Tracer 008.png

Now that you’ve created all the required files and placed them in the correct folders, it’s time to upload everything to your ESP8266 and launch the curve tracer UI.


🧭 Step-by-Step

  1. Open the Arduino IDE
  2. Make sure your board is set to ESP8266 and your project folder is named Tims_Curve_Tracer.
  3. Upload the Firmware
  4. Click the Upload button to flash the Tims_Curve_Tracer.ino sketch to your ESP8266 (see image with arrow pointing to the Upload button)
  5. Upload the Filesystem (LittleFS)
  6. Go to Tools → ESP8266 LittleFS Data Upload to flash the contents of the /data folder (see image with arrow pointing to the LittleFS button)
  7. Open the Serial Monitor
  8. Set the baud rate to 115200
  9. Reset the ESP8266 and watch for the IP address assigned by your router (see image showing the Serial Monitor output)


🌐 Launch the Curve Tracer UI

Once you have the IP address, open a web browser on any device connected to the same network and enter the IP in the address bar (e.g., http://192.168.50.11 or whatever your router assigned).

You should now see the curve tracer interface in action! (see browser screenshots showing the UI)


🖥️ Serial Monitor Tip

Depending on the USB-to-Serial chip used in your ESP8266 module, the Serial Monitor may not show any output when you press the reset button manually. If that happens, the best workaround is to:

  1. Keep the Serial Monitor open while uploading the firmware
  2. The ESP8266 will perform a hard reset during upload and reconnect to the network
  3. You should then see the assigned IP address printed in the Serial Monitor as part of the boot sequence

This IP address is what you’ll use to access the curve tracer UI in your browser.

🎥Video

Tim's Basic Curve Tracer [Part 1?]

I’ve recorded a video where I walk through the circuit and the code in detail.

It’s a bit long, so feel free to skip around and focus on the parts you’re less familiar with — whether that’s the wiring, the firmware, or how the browser-based UI ties it all together.

📈How Does a Curve Tracer Work (My Version)

Tims Curve Tracer 001.png

A curve tracer helps you visualize how a component (like a diode or transistor) responds to different voltages — by plotting its current vs. voltage characteristics in real time.

Here’s how my version works, step by step:

⚡ 1. Sweep Voltage Is Generated

The ESP8266 creates a ramp or sweep voltage using PWM (Pulse Width Modulation) and filtering. This voltage is applied across the component under test — typically between the collector and emitter of a transistor or across a diode.

  1. The sweep goes from 0V up to a set maximum (e.g., 3.3V), then resets.
  2. This simulates a range of operating conditions.

🔍 2. Current Is Measured

A shunt resistor is placed in series with the component. As current flows, a small voltage drop appears across the shunt.

  1. The ESP8266 reads this voltage using its ADC (Analog-to-Digital Converter).
  2. This lets us calculate the current flowing through the component at each sweep point.

🧠 3. Data Is Captured and Sent

The ESP8266 samples both:

  1. The sweep voltage (what’s being applied)
  2. The shunt voltage (which tells us the current)

It packages this data and sends it via WebSocket to the browser-based UI.

🌐 4. Browser UI Plots the Curve

Your browser receives the data and plots it in real time:

  1. X-axis: Sweep voltage
  2. Y-axis: Calculated current

You’ll see the curve update live as the sweep runs — just like classic Tektronix curve tracers, but all in-browser!

👻 5. Ghost Traces and Overlays

You can save a trace as a ghost overlay to compare multiple components or see how a part behaves under different conditions.

  1. Ghost traces are stored in the browser and can be exported or reloaded.
  2. This makes it easy to spot anomalies or validate consistency across batches.

🧰 Summary

  1. The ESP8266 generates a sweep
  2. Measures current via a shunt resistor
  3. Sends data to the browser
  4. The browser plots the curve in real time
  5. You can save and compare traces