simple-pid-controller
v2.0.1
Published
A simple application to demonstrate the usage of a PID Controller with examples
Readme
simple-pid-controller
A feature-complete, industrial-grade PID controller for Node.js.
Built for IIoT gateway applications, SCADA dashboards, and process automation — with MQTT, InfluxDB, and OPC UA integration in mind.
Author: Harshad Joshi
Organisation: Bufferstack.IO Analytics Technology LLP, Pune
License: Apache 2.0
Table of Contents
- Features
- Installation
- Quick Start
- API Reference
- Options Reference
- Examples
- Included Applications
- Changelog
- License
Features
| Feature | v1.x (Original) | v2.x (Current) |
|---|---|---|
| Proportional / Integral / Derivative control | ✅ | ✅ |
| Configurable gains (k_p, k_i, k_d) | ✅ | ✅ |
| Individual P, I, D getters | ✅ | ✅ |
| Input validation (type checks) | ✅ | ✅ |
| Fixed dt at construction | ✅ | ✅ (fallback only) |
| Dynamic dt via real timestamps | ❌ | ✅ |
| Anti-windup (integral clamping) | ❌ | ✅ |
| Output clamping (min/max) | ❌ | ✅ |
| Deadband support | ❌ | ✅ |
| Derivative on measurement (no kick) | ❌ | ✅ |
| setTarget() resets integral state | ❌ | ✅ |
| reset() method | ❌ | ✅ |
| updateGains() runtime tuning | ❌ | ✅ |
| Manual / Auto mode | ❌ | ✅ |
| Bumpless manual → auto transfer | ❌ | ✅ |
| getStatus() telemetry snapshot | ❌ | ✅ |
| EventEmitter (update, settled) | ❌ | ✅ |
Installation
npm install simple-pid-controllerQuick Start
const PIDController = require('simple-pid-controller');
const controller = new PIDController(1.2, 1.0, 0.01, 1.0, {
outputMin: -10,
outputMax: 10,
integralMin: -50,
integralMax: 50,
deadband: 0.02,
settledTolerance: 0.05,
});
controller.setTarget(100);
controller.on('update', (status) => console.log(status));
controller.on('settled', (status) => console.log('Settled!', status));
setInterval(() => {
const pv = readSensor(); // replace with your actual sensor read
controller.update(pv);
}, 1000);API Reference
Constructor
new PIDController(k_p, k_i, k_d, dt, options)| Parameter | Type | Default | Description |
|---|---|---|---|
| k_p | number | 1.0 | Proportional gain |
| k_i | number | 0.0 | Integral gain |
| k_d | number | 0.0 | Derivative gain |
| dt | number | 1.0 | Fallback time interval in seconds (overridden by dynamic timestamps) |
| options | object | {} | Configuration object — see Options Reference |
Methods
setTarget(target)
Sets the controller setpoint (SV). Also resets the integral accumulator, derivative memory, and settled flag to prevent carryover from the previous setpoint.
controller.setTarget(100);update(currentValue)
Runs one PID cycle with the given process variable (PV). Computes the true elapsed dt from the last call using Date.now(), applies deadband, anti-windup, output clamping, and fires update / settled events.
Returns the clamped controller output as a number.
const output = controller.update(pv);In manual mode, returns the manually set output without any computation.
updateGains(k_p, k_i, k_d)
Updates PID gains at runtime without recreating the controller instance. Integral and derivative state are preserved — call reset() first if a clean start is needed.
controller.updateGains(2.0, 0.5, 0.05);setMode(mode)
Switches between 'auto' (PID active) and 'manual' (fixed output) modes.
When switching manual → auto, a bumpless transfer is performed: the integral term is pre-loaded so the first auto output matches the last manual output, preventing sudden actuator jumps.
controller.setMode('manual');
controller.setMode('auto'); // bumpless transfer applied automaticallysetManualOutput(value)
Sets the fixed output value used in 'manual' mode. Also seeds the bumpless transfer when switching back to 'auto'.
controller.setManualOutput(5.0);reset()
Clears all internal state: integral accumulator, derivative memory, PV history, and timestamp. Call after fault recovery, process restarts, or when switching modes.
controller.reset();getStatus()
Returns a plain object snapshot of the current controller state. Suitable for direct publishing to MQTT, InfluxDB, or a SCADA dashboard.
const status = controller.getStatus();
// {
// sv: 100,
// pv: 97.4,
// error: 2.6,
// p: 3.12,
// i: 0.44,
// d: -0.02,
// output: 3.54,
// mode: 'auto'
// }Properties (Getters)
These read-only getters reflect values from the most recent update() cycle.
| Property | Description |
|---|---|
| controller.p | Current proportional term: k_p × error |
| controller.i | Current integral term: k_i × sumError |
| controller.d | Current derivative term: k_d × (-dPV/dt) (derivative on measurement) |
Note on Derivative: The D term uses derivative on measurement (
-dPV/dt) rather thand(error)/dt. This prevents a derivative spike (kick) when the setpoint changes suddenly.
Events
The controller extends Node.js EventEmitter.
'update'
Fired every update() cycle (in auto mode). Passes the getStatus() snapshot as the argument.
controller.on('update', (status) => {
mqttClient.publish('pid/status', JSON.stringify(status));
});'settled'
Fired once when |error| <= settledTolerance. Re-arms automatically if the process drifts outside tolerance again. Only active when settledTolerance > 0.
controller.on('settled', (status) => {
console.log('Process reached setpoint.', status);
});Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
| outputMin | number | -Infinity | Minimum clamped controller output |
| outputMax | number | Infinity | Maximum clamped controller output |
| integralMin | number | -Infinity | Anti-windup: lower bound on integral accumulator |
| integralMax | number | Infinity | Anti-windup: upper bound on integral accumulator |
| deadband | number | 0 | If |error| <= deadband, output is forced to 0 (disabled when 0) |
| settledTolerance | number | 0 | Threshold for 'settled' event emission (disabled when 0) |
Examples
Basic Usage
const PIDController = require('simple-pid-controller');
const controller = new PIDController(1.0, 0.0, 0.0);
controller.setTarget(50);
setInterval(() => {
const pv = readSensor();
const output = controller.update(pv);
applyOutput(output);
}, 1000);With Anti-windup and Output Clamping
const PIDController = require('simple-pid-controller');
const controller = new PIDController(1.2, 1.0, 0.01, 1.0, {
outputMin: -100,
outputMax: 100,
integralMin: -500,
integralMax: 500,
deadband: 0.5,
});
controller.setTarget(75);
controller.on('update', (status) => {
console.log(JSON.stringify(status));
});
setInterval(() => controller.update(readSensor()), 1000);Manual / Auto Mode with Bumpless Transfer
const PIDController = require('simple-pid-controller');
const controller = new PIDController(1.2, 1.0, 0.01);
controller.setTarget(100);
// Place controller in manual mode with a fixed output
controller.setManualOutput(30);
controller.setMode('manual');
// Later, transfer to auto — integral is pre-loaded to match output of 30
// so the actuator does not jump
setTimeout(() => {
controller.setMode('auto');
}, 5000);
setInterval(() => controller.update(readSensor()), 1000);Runtime Gain Tuning
const PIDController = require('simple-pid-controller');
const controller = new PIDController(1.0, 0.5, 0.01);
controller.setTarget(80);
// Tune gains live without stopping the control loop
setTimeout(() => {
controller.updateGains(1.5, 0.8, 0.02);
console.log('Gains updated');
}, 10000);
setInterval(() => controller.update(readSensor()), 1000);MQTT Integration
See mqtt-pid.js for a full working example.
const PIDController = require('simple-pid-controller');
const mqtt = require('mqtt');
const client = mqtt.connect('mqtt://localhost');
const controller = new PIDController(1.2, 1.0, 0.01, 1.0, {
outputMin: -10, outputMax: 10,
integralMin: -100, integralMax: 100,
deadband: 0.02, settledTolerance: 0.05,
});
// Publish full telemetry on every cycle
controller.on('update', (status) => {
client.publish('pid/status', JSON.stringify(status));
});
// Publish settled notification and stop loop
controller.on('settled', (status) => {
client.publish('pid/settled', JSON.stringify(status));
});
// Remote gain tuning via MQTT
client.on('message', (topic, message) => {
if (topic === 'gains') {
const g = JSON.parse(message.toString());
controller.updateGains(g.k_p, g.k_i, g.k_d);
}
if (topic === 'sv') {
controller.setTarget(parseFloat(message.toString()));
}
});Included Applications
| File | Description |
|---|---|
| tolerance.js | Interactive CLI demo — enter SV and PV, watch the controller converge |
| mqtt-pid.js | Full MQTT integration — subscribe to SV/PV topics, publish telemetry, support remote gain tuning and mode switching |
| sample-application-template.js | Minimal boilerplate for building your own application on top of this library |
Changelog
v2.0.0 — 2026-03-12
Core Library (index.js)
Bug Fixes
- Fixed derivative term calculation — Original formula
k_d * (target - lastError) / dtwas incorrect. Now uses derivative on measurement:k_d * (-dPV/dt), which correctly measures rate of change and eliminates derivative kick on setpoint steps. setTarget()now resets controller state — Previously, the integral accumulator (sumError) andlastErrorpersisted across setpoint changes, causing integral carryover and derivative spikes. Both are now cleared on everysetTarget()call.- Dynamic
dtvia timestamps —dtis no longer assumed to be the fixed constructor value. Eachupdate()call computes actual elapsed time usingDate.now(), making the controller accurate under irregular loop timing.
New Features
- Anti-windup —
integralMin/integralMaxoptions clamp the integral accumulator to prevent runaway windup during actuator saturation. - Output clamping —
outputMin/outputMaxoptions clamp the final controller output, replacing the ad-hoc clamping that was done externally intolerance.jsandmqtt-pid.js. - Deadband —
deadbandoption forces output to0when|error| <= deadband, reducing unnecessary actuator activity near the setpoint. reset()method — Clears integral, derivative state, PV history, timestamp, and settled flag. Use after fault recovery or process restarts.updateGains(k_p, k_i, k_d)— Update PID gains at runtime without recreating the controller instance.- Manual / Auto mode (
setMode()) — Switch between PID-controlled and fixed-output modes. setManualOutput(value)— Set the fixed output value for manual mode.- Bumpless transfer — When switching
manual → auto, the integral term is pre-loaded to match the last manual output, preventing sudden actuator jumps. getStatus()— Returns a plain{ sv, pv, error, p, i, d, output, mode }object for telemetry publishing.- EventEmitter — Class now extends
EventEmitterand emits:'update'— on everyupdate()cycle with the full status snapshot'settled'— once when|error| <= settledTolerance; re-arms if process drifts back out
Applications
tolerance.js
- Removed manual
if (Math.abs(pv - sv) <= tolerance)check; replaced withcontroller.on('settled', ...)event listener - Added
controller.on('update', ...)for structured JSON telemetry output includingerror,output, andmodefields - Constructor updated to use
optionsobject (outputMin/Max,integralMin/Max,deadband,settledTolerance) - Output from
update()used directly — no external clamping needed
mqtt-pid.js
- Constructor updated to use
optionsobject - Replaced inline
console.login interval withcontroller.on('update', ...)publishing topid/statusMQTT topic 'settled'event stops the interval and publishes topid/settledMQTT topic- Added
gainstopic handler — accepts{ k_p, k_i, k_d }JSON for remote gain tuning viaupdateGains() - Added
modetopic handler — accepts'auto'or'manual'string viasetMode() - Added
manual_outputtopic handler — accepts numeric string viasetManualOutput() setTarget(sv)on SV message resets integral and derivative stateprocess.on('exit')callscontroller.reset()andclient.end()for clean shutdown
sample-application-template.js
- Updated to use full
optionsobject in constructor - Added
controller.on('update', ...)andcontroller.on('settled', ...)with inline comments showing MQTT/InfluxDB/OPC UA integration points - Telemetry logging moved from inside the loop body to the
updateevent listener - Stub functions
readCurrentProcessValue()andsendControlSignalToActuator()documented with PLC/sensor code examples - Commented-out blocks for
setMode(),updateGains(), andreset()show full API surface without cluttering the active loop process.on('exit')calls bothclearIntervalandcontroller.reset()
v1.0.0 — 2023
- Initial release
- Basic PID controller with configurable
k_p,k_i,k_d, anddt setTarget(target)to set the desired setpointupdate(currentValue)to compute and return the controller output- Read-only getters for individual
p,i,dcomponents - Type validation in constructor,
setTarget(), andupdate() tolerance.js— interactive CLI demo with manual tolerance checkmqtt-pid.js— MQTT integration subscribing tosvandpvtopicssample-application-template.js— minimal boilerplate for custom applications
License
Copyright 2023–2026, Harshad Joshi and Bufferstack.IO Analytics Technology LLP, Pune.
Licensed under the Apache License, Version 2.0. See LICENSE for full terms.
