Skip to main content

ControlForge + Phidgets: Hardware Interface Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.610 | Native pure-Go driver, zero dependencies


1. Architecture Overview

ControlForge treats Phidgets as plug-and-play USB sensor/actuator modules. Each Phidget channel is opened by channel type and index, given a string name, and accessed through typed ST functions. No drivers to install, no libraries to package, no udev rules to configure — plug in the USB device and call PHIDGET_OPEN.

As of ControlForge v1.0.610 the Phidgets driver is a native pure-Go implementation that talks directly to the Linux usbfs kernel interface. It does not link libphidget22, does not depend on libusb, does not require cgo, and leaves zero runtime install footprint. A single-binary ControlForge build running on a fresh Linux machine can discover and drive a 1018/1019 PhidgetInterfaceKit 8/8/8 with nothing else installed.

There are 16 ST functions organized into three groups:

GroupFunctionsPurpose
Device LifecyclePHIDGET_OPEN, CLOSE, IS_ATTACHED, DELETE, LISTConnect, disconnect, enumerate
Reading SensorsPHIDGET_READ, READ_BOOL, VOLTAGE, CURRENT, TEMPERATURE, HUMIDITY, RATIOTyped sensor input
Writing OutputsPHIDGET_WRITE, WRITE_BOOL, SET_MOTOR, SET_RELAYTyped actuator output

All functions use IEC 61131-3 Structured Text in ControlForge's browser-based IDE.

System Diagram

Typical Applications

  • Industrial measurement — High-precision voltage/current monitoring with calibrated sensors
  • Environmental monitoring — Temperature and humidity logging across multiple zones
  • Load cell / strain gauge — Ratiometric bridge input for weighing and force measurement
  • Relay control — Switching AC/DC loads with isolated solid-state or mechanical relays
  • Motor control — Variable-speed DC motor drive with bidirectional speed (-1.0 to 1.0)
  • Lab automation — Plug-and-play USB sensor expansion without custom wiring

Supported Hardware

Phidgets ships a large device catalog. The native driver covers a growing subset today and is architected to expand device class by device class as hardware arrives on the test bench. There are three clear tiers of support:

✅ Verified end-to-end on real hardware

FamilyUSB Product IDsVerified channels
1010 / 1013 / 1018 / 1019 PhidgetInterfaceKit 8/8/80x06C2:0x00458 digital inputs, 8 digital outputs (open-drain), 8 voltage-ratio inputs. Classic USB HID family with SET_REPORT output writes. Live verified 2026-04-14 against a 1019 including ST-code read/write + physical DI↔DO loopback.

🟡 Framing complete, awaiting hardware

FamilyUSB Product IDsStatus
HUB0000 / HUB0001 / HUB0002 / HUB0007 / HUB5000 VINT hubs0x06C2:0x0030..0x0052VINT protocol framing, hub-wrap encoder, packet tracker, USB enumeration, and interface claim all implemented and unit-tested. Needs a physical VINT hub on the bench to confirm the wire format and the hub descriptor read path.

🔴 Not yet implemented — per-device ports pending

Everything else. The ST function surface (PHIDGET_OPEN with channel_type strings like temperature, humidity, dc_motor, voltage_input, current, etc.) is already present because the backend dispatcher understands those types. What's missing is the per-device packet layout for each device class — each Phidgets module has its own VINT packet opcodes defined in libphidget22/src/device/*.c, and they need to be ported one device class at a time. Roughly 30+ device classes to cover, including:

  • Sensing: 1048 / TMP1100 / TMP1200 (temperature), HUM1000 (humidity), DAQ1500 / 1046 (bridge inputs for load cells and strain gauges), 1122 (current), HIN1101 (capacitive touch), distance sensors, light sensors, gyros, magnetometers, accelerometers, GPS.
  • Output: REL1100 (isolated relays), OUT1100 (digital out), VCP1000 (voltage output), DCC1000 / 1063 (DC motor controllers), advanced servos, LED arrays, PH sensors, power guards.
  • Data bridges: RFID readers, IR transmit/receive, LCD displays, SPI/I2C data adapters, encoders, frequency counters.

Each port is a well-bounded task (roughly 50–200 lines of Go per device class, plus a live bench test). The architecture does not need new shared infrastructure to expand — only a new file under drivers/phidgets/vint/ or drivers/phidgets/ifkit/ that implements the device-specific packet encode/decode, plus a dispatcher entry that maps the Phidgets VINTID or classic PID to the new parser.

If a specific device is blocking a project, file an issue with the model number and it jumps to the front of the queue.

Installation

None. The driver is compiled into the main ControlForge binary. There is no package to install, no .so library to ship, and no udev rules to apply.

Permissions: The native driver needs read/write access to /dev/bus/usb/BBB/DDD device nodes. The simplest options in order of preference:

  1. Add the ControlForge user to the plugdev group (most distros already grant usbfs access to plugdev members).

  2. Drop a udev rule targeting the Phidgets USB vendor:

    # /etc/udev/rules.d/99-phidgets.rules
    SUBSYSTEM=="usb", ATTR{idVendor}=="06c2", MODE="0666"
  3. Run controlforge as root (not recommended for production but fine for bench testing).

Existing Phidgets udev rules from a prior libphidget22 install continue to work — nothing in the native driver requires them to be changed.

The driver also auto-detaches any kernel driver that may be bound to the device (typically usbhid on the classic InterfaceKit family). No manual modprobe -r usbhid or rmmod dance is needed.


2. Device Lifecycle

Every Phidget channel follows the same pattern: open, use, close. The string name you assign at open time is the handle used by all subsequent read/write calls.

2.1 PHIDGET_OPEN — Connect to a Channel

(* Open digital input 0 on the first Phidget found *)
ok := PHIDGET_OPEN('di0', 'digital_input', -1, -1, 0);

(* Open digital output 3 on a specific device by serial number *)
ok := PHIDGET_OPEN('relay_main', 'digital_output', 437891, -1, 3);

(* Open a temperature sensor plugged into VINT hub port 2, channel 0 *)
ok := PHIDGET_OPEN('temp1', 'temperature', 561234, 2, 0);
ParamTypeDescription
nameSTRINGUnique handle for this channel (your choice)
channel_typeSTRINGPhidgets channel class — see table below
serial_numberINTDevice serial number, or -1 for "any matching device"
hub_portINTVINT hub port (0..5), or -1 for non-hub devices
channelINTChannel index on the device (0-based)

Supported channel types:

channel_type stringHardwareRead function
digital_inputDI channel on any devicePHIDGET_READ_BOOL
digital_outputDO channel / relayPHIDGET_WRITE_BOOL
voltage_inputAnalog voltage inputPHIDGET_VOLTAGE
voltage_ratioRatiometric bridge (load cell, pressure)PHIDGET_RATIO
temperatureThermocouple / RTD / integrated sensorPHIDGET_TEMPERATURE
humidityHumidity sensorPHIDGET_HUMIDITY
currentCurrent inputPHIDGET_CURRENT
dc_motorDC motor drivePHIDGET_SET_MOTOR
rc_servoHobby servoPHIDGET_WRITE
encoderQuadrature encoderPHIDGET_READ

Finding the serial number: Every Phidgets device has a unique serial number printed on the board. Pass -1 to accept any plugged-in device that matches the requested channel type — useful when you only have one Phidget on the host.

2.2 PHIDGET_CLOSE — Disconnect a Channel

ok := PHIDGET_CLOSE('temp1');
(* Returns: TRUE if closed successfully *)

Releases the channel. The device remains physically connected and can be reopened.

2.3 PHIDGET_IS_ATTACHED — Check Connection

IF PHIDGET_IS_ATTACHED('temp1') THEN
(* Safe to read *)
temp := PHIDGET_TEMPERATURE('temp1');
END_IF;

Returns TRUE if the device is physically connected and the channel is open. Use this to guard reads against USB disconnection.

2.4 PHIDGET_DELETE — Remove a Channel

ok := PHIDGET_DELETE('temp1');
(* Returns: TRUE if the channel was removed from the internal registry *)

Closes the channel (if open) and removes it from ControlForge's internal channel map. Use this for cleanup when a device is permanently removed.

2.5 PHIDGET_LIST — Enumerate Channels

list := PHIDGET_LIST();
(* Returns a string listing all registered channels and their status *)

Returns a string describing all channels that have been opened with PHIDGET_OPEN, including their attachment state. Useful for diagnostics and HMI display.

Example: Startup Initialization

PROGRAM POU_PhidgetInit
VAR
state : INT := 0;
ok : BOOL;
info : STRING;
END_VAR

CASE state OF
0: (* Open all channels *)
ok := PHIDGET_OPEN('temp_ambient', 'temperature', 561234, -1, 0);
ok := PHIDGET_OPEN('humidity1', 'humidity', 561234, -1, 1);
ok := PHIDGET_OPEN('voltage_in', 'voltage_input', 329876, -1, 0);
ok := PHIDGET_OPEN('relay1', 'digital_output', 437891, -1, 0);
ok := PHIDGET_OPEN('relay2', 'digital_output', 437891, -1, 1);
state := 1;

1: (* Wait for all devices to attach *)
IF PHIDGET_IS_ATTACHED('temp_ambient')
AND PHIDGET_IS_ATTACHED('voltage_in')
AND PHIDGET_IS_ATTACHED('relay1') THEN
state := 10;
END_IF;

10: (* Running — list channels for diagnostics *)
info := PHIDGET_LIST();
END_CASE;
END_PROGRAM

3. Reading Sensors

Each sensor type has a dedicated function that returns a value in calibrated engineering units. All read functions take a single name parameter — the handle assigned at PHIDGET_OPEN.

3.1 PHIDGET_READ — Generic Sensor Read

value := PHIDGET_READ('sensor1');
(* Returns: REAL — the primary value of whatever sensor type is attached *)

Returns the primary measurement value for any sensor. The meaning depends on the device type (voltage for voltage inputs, temperature for temperature sensors, etc.). Use the typed functions below when you need explicit clarity about what you're reading.

3.2 PHIDGET_READ_BOOL — Digital Input

door_open := PHIDGET_READ_BOOL('door_sensor');
(* Returns: BOOL — TRUE if the digital input is active *)

For digital input channels (e.g., HIN1101 touch sensor, 1012 digital input). Returns the boolean state of the input.

3.3 PHIDGET_VOLTAGE — Voltage Input

volts := PHIDGET_VOLTAGE('voltage_in');
(* Returns: REAL — voltage in Volts *)

Reads a voltage input channel. Resolution and range depend on the hardware (e.g., 1002 Voltage Input: 0-5V, 12-bit; VINT VoltageInput: -40 to +40V, 16-bit).

3.4 PHIDGET_CURRENT — Current Input

amps := PHIDGET_CURRENT('current_probe');
(* Returns: REAL — current in Amps *)

Reads a current input channel. Typically used with the 1122 30A Current Sensor or VINT current inputs.

3.5 PHIDGET_TEMPERATURE — Temperature Sensor

temp_c := PHIDGET_TEMPERATURE('temp_ambient');
(* Returns: REAL — temperature in degrees Celsius *)

Reads a temperature channel. Works with thermocouple interfaces (1048, TMP1100), RTD interfaces (TMP1200), and integrated temperature/humidity sensors (HUM1000).

3.6 PHIDGET_HUMIDITY — Humidity Sensor

rh := PHIDGET_HUMIDITY('humidity1');
(* Returns: REAL — relative humidity in percent (0.0-100.0) *)

Reads a humidity channel from sensors like the HUM1000 or 1125.

3.7 PHIDGET_RATIO — Ratiometric Sensor

ratio := PHIDGET_RATIO('load_cell');
(* Returns: REAL — voltage ratio (V/V) *)

Reads a ratiometric bridge input. Used with load cells, strain gauges, and pressure sensors connected to a Wheatstone bridge interface (1046 PhidgetBridge, DAQ1500). The returned value is the ratio of the measured differential voltage to the excitation voltage.

Example: Environmental Monitoring

PROGRAM POU_Environment
VAR
temp_c : REAL;
humidity : REAL;
temp_f : REAL;
alarm : BOOL := FALSE;
END_VAR

IF PHIDGET_IS_ATTACHED('temp_ambient') THEN
temp_c := PHIDGET_TEMPERATURE('temp_ambient');
temp_f := temp_c * 9.0 / 5.0 + 32.0;
END_IF;

IF PHIDGET_IS_ATTACHED('humidity1') THEN
humidity := PHIDGET_HUMIDITY('humidity1');
END_IF;

(* High-temperature alarm *)
IF temp_c > 40.0 OR humidity > 85.0 THEN
alarm := TRUE;
ELSE
alarm := FALSE;
END_IF;
END_PROGRAM

Example: Load Cell Measurement

PROGRAM POU_LoadCell
VAR
raw_ratio : REAL;
weight_kg : REAL;
tare_offset : REAL := 0.0;
scale_factor : REAL := 1000.0; (* kg per V/V — calibrate per cell *)
END_VAR

IF PHIDGET_IS_ATTACHED('load_cell') THEN
raw_ratio := PHIDGET_RATIO('load_cell');
weight_kg := (raw_ratio - tare_offset) * scale_factor;
END_IF;
END_PROGRAM

Example: Voltage and Current Measurement

PROGRAM POU_PowerMonitor
VAR
voltage : REAL;
current : REAL;
power_w : REAL;
END_VAR

IF PHIDGET_IS_ATTACHED('voltage_in') AND PHIDGET_IS_ATTACHED('current_probe') THEN
voltage := PHIDGET_VOLTAGE('voltage_in');
current := PHIDGET_CURRENT('current_probe');
power_w := voltage * current;
END_IF;
END_PROGRAM

4. Writing Outputs

Output functions return BOOLTRUE on success, FALSE on failure (device detached, invalid value, etc.).

4.1 PHIDGET_WRITE — Generic Output Write

ok := PHIDGET_WRITE('analog_out', 2.5);
(* Writes 2.5 to the output channel *)
ParamTypeDescription
nameSTRINGChannel handle
valueREALOutput value (meaning depends on device type)

Generic write for any output channel. The interpretation of value depends on the device.

4.2 PHIDGET_WRITE_BOOL — Digital Output

ok := PHIDGET_WRITE_BOOL('indicator_led', TRUE);
(* Turns on a digital output *)

ok := PHIDGET_WRITE_BOOL('indicator_led', FALSE);
(* Turns it off *)

For digital output channels (e.g., REL1101 isolated digital output, OUT1100 digital output).

4.3 PHIDGET_SET_MOTOR — Motor Speed Control

(* Full speed forward *)
ok := PHIDGET_SET_MOTOR('drive_motor', 1.0);

(* Half speed reverse *)
ok := PHIDGET_SET_MOTOR('drive_motor', -0.5);

(* Stop *)
ok := PHIDGET_SET_MOTOR('drive_motor', 0.0);
ParamTypeDescription
nameSTRINGChannel handle
valueREALSpeed: -1.0 (full reverse) to 1.0 (full forward)

Controls DC motor controllers (DCC1000, DCC1001, DCC1002, 1060, 1064). The value is a duty cycle ratio — negative values reverse direction.

4.4 PHIDGET_SET_RELAY — Relay Control

(* Energize relay *)
ok := PHIDGET_SET_RELAY('relay1', TRUE);

(* De-energize relay *)
ok := PHIDGET_SET_RELAY('relay1', FALSE);
ParamTypeDescription
nameSTRINGChannel handle
valueBOOLTRUE = energized, FALSE = de-energized

Controls relay channels on relay boards (REL1100, REL1101, 1014, 1017). Functionally equivalent to PHIDGET_WRITE_BOOL but provides semantic clarity for relay applications.

Example: Motor with Safety Interlock

PROGRAM POU_MotorControl
VAR
speed_setpoint : REAL := 0.0;
e_stop : BOOL;
ok : BOOL;
END_VAR

(* Read e-stop digital input *)
e_stop := PHIDGET_READ_BOOL('e_stop_input');

IF e_stop THEN
(* Emergency stop — kill motor immediately *)
ok := PHIDGET_SET_MOTOR('drive_motor', 0.0);
ok := PHIDGET_SET_RELAY('motor_contactor', FALSE);
ELSE
(* Normal operation *)
ok := PHIDGET_SET_RELAY('motor_contactor', TRUE);
ok := PHIDGET_SET_MOTOR('drive_motor', speed_setpoint);
END_IF;
END_PROGRAM

Example: Relay Sequencer

PROGRAM POU_RelaySequence
VAR
state : INT := 0;
scan_count : DINT := 0;
delay_scans : DINT := 50; (* ~5 sec at 100ms scan *)
ok : BOOL;
END_VAR

scan_count := scan_count + 1;

CASE state OF
0: (* Step 1: Energize pump relay *)
ok := PHIDGET_SET_RELAY('pump', TRUE);
scan_count := 0;
state := 1;

1: (* Wait for pressure to stabilize *)
IF scan_count >= delay_scans THEN
state := 2;
END_IF;

2: (* Step 2: Open valve *)
ok := PHIDGET_SET_RELAY('valve', TRUE);
scan_count := 0;
state := 3;

3: (* Wait for flow *)
IF scan_count >= delay_scans THEN
state := 4;
END_IF;

4: (* Running — monitor and hold *)
(* Control logic here *)
END_CASE;
END_PROGRAM

5. Complete Program Example

This example ties together lifecycle, sensor reads, and output control in a single temperature-controlled relay system.

PROGRAM POU_TempControl
VAR
state : INT := 0;
temp_c : REAL;
humidity : REAL;
setpoint : REAL := 25.0;
deadband : REAL := 1.0;
cooling_on : BOOL := FALSE;
ok : BOOL;
END_VAR

CASE state OF
0: (* Initialize — open all channels *)
ok := PHIDGET_OPEN('temp1', 'temperature', 561234, -1, 0);
ok := PHIDGET_OPEN('hum1', 'humidity', 561234, -1, 1);
ok := PHIDGET_OPEN('fan_relay', 'digital_output', 437891, -1, 0);
ok := PHIDGET_OPEN('alarm_relay', 'digital_output', 437891, -1, 1);
state := 1;

1: (* Wait for attach *)
IF PHIDGET_IS_ATTACHED('temp1')
AND PHIDGET_IS_ATTACHED('fan_relay') THEN
state := 10;
END_IF;

10: (* Running — temperature control loop *)
temp_c := PHIDGET_TEMPERATURE('temp1');
humidity := PHIDGET_HUMIDITY('hum1');

(* Deadband control *)
IF temp_c > (setpoint + deadband) AND NOT cooling_on THEN
ok := PHIDGET_SET_RELAY('fan_relay', TRUE);
cooling_on := TRUE;
ELSIF temp_c < (setpoint - deadband) AND cooling_on THEN
ok := PHIDGET_SET_RELAY('fan_relay', FALSE);
cooling_on := FALSE;
END_IF;

(* Over-temperature alarm *)
IF temp_c > 50.0 THEN
ok := PHIDGET_SET_RELAY('alarm_relay', TRUE);
ELSE
ok := PHIDGET_SET_RELAY('alarm_relay', FALSE);
END_IF;

99: (* Shutdown *)
ok := PHIDGET_SET_RELAY('fan_relay', FALSE);
ok := PHIDGET_SET_RELAY('alarm_relay', FALSE);
ok := PHIDGET_CLOSE('temp1');
ok := PHIDGET_CLOSE('hum1');
ok := PHIDGET_CLOSE('fan_relay');
ok := PHIDGET_CLOSE('alarm_relay');
END_CASE;
END_PROGRAM

6. Function Quick Reference

FunctionReturnsDescription
PHIDGET_OPEN(name, channel_type, serial, hub_port, channel)BOOLOpen a channel by type, serial, hub port, and channel index. Pass -1 for serial or hub_port to accept any match.
PHIDGET_CLOSE(name)BOOLClose a channel
PHIDGET_IS_ATTACHED(name)BOOLCheck if device is physically connected
PHIDGET_DELETE(name)BOOLRemove channel from registry
PHIDGET_LIST()STRINGList all registered channels
PHIDGET_READ(name)REALGeneric sensor read (primary value)
PHIDGET_READ_BOOL(name)BOOLDigital input read
PHIDGET_VOLTAGE(name)REALVoltage input (Volts)
PHIDGET_CURRENT(name)REALCurrent input (Amps)
PHIDGET_TEMPERATURE(name)REALTemperature sensor (degrees C)
PHIDGET_HUMIDITY(name)REALHumidity sensor (% RH)
PHIDGET_RATIO(name)REALRatiometric bridge input (V/V)
PHIDGET_WRITE(name, value)BOOLGeneric output write
PHIDGET_WRITE_BOOL(name, value)BOOLDigital output write
PHIDGET_SET_MOTOR(name, value)BOOLMotor speed (-1.0 to 1.0)
PHIDGET_SET_RELAY(name, value)BOOLRelay control (TRUE/FALSE)

ControlForge v1.0.533 | Phidgets USB Sensor/Actuator Interface

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides