npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fdp3-modbus

v0.1.1

Published

Node.js TypeScript client for the RealTime Control Systems FDP3-Modbus interface (Toshiba TCC-NET VRF/Split air-conditioning bridge)

Downloads

44

Readme

fdp3-modbus

Node.js / TypeScript client for the RealTime Control Systems FDP3-Modbus interface.

The FDP3-Modbus bridges a Toshiba TCC-NET VRF/Split air-conditioning remote-controller bus to Modbus RTU over RS-485. Each FDP3 taps into one indoor unit's wired-remote-controller line (its "TCC-NET" A/B terminals) — in VRF systems where several indoor units share one remote controller (a "twin"/"triple" group), a single FDP3 can see up to 8 linked units. This library gives you a typed, promise-based API to read status and send control commands from Node.js.

Note: despite the similar acronym, TCC-NET (the AB remote-controller bus) is a different, physically separate bus from TCC-Link (the U1/U2 outdoor-to-indoor line) — this library has no visibility into the outdoor unit or refrigerant-circuit communication at all.


Requirements

  • Node.js 18+
  • RS-485 USB adapter wired to the FDP3's DB(+)/DA(-)/GND terminals
  • FDP3 Modbus address set via DIP switches SW1.3–SW1.8 (range 0–63)

Installation

npm install fdp3-modbus

Quick start

import { FdpNetwork, RunMode, FanSpeed } from 'fdp3-modbus';

const network = new FdpNetwork({ port: '/dev/ttyUSB0' }); // or 'COM3' on Windows
await network.connect();

// Get the device at Modbus address 1 (matches this FDP3's DIP switch setting)
const device = network.getDevice(1);

// Common case: one remote controller + one indoor unit behind this FDP3
const unit = await device.getPrimaryUnitStatus();
console.log('Unit present:', unit.exists);
console.log('Return air temp:', unit.returnAirTemperatureC, '°C');
console.log('Fault:', unit.faultCode.code ?? 'none');

// Control this device's remote-controller group
await device.control({
  onOff: true,
  mode: RunMode.Cool,
  fanSpeed: FanSpeed.High,
  setpointC: 22,
});

await network.disconnect();

Connection settings

Fixed per the FDP3-Modbus manual — defaults require no configuration:

| Parameter | Value | |-----------|-------| | Network | 3-wire RS485 | | Mode | Modbus RTU Slave | | Baud rate | 9600 | | Parity | None | | Stop bits | 1 | | Register base | 0 | | Max devices per bus | 62 (addresses 0–63) |

The manual notes baud rate and parity can be reconfigured on the device if required — override via ModbusConfig if your setup differs:

const network = new FdpNetwork({
  port:              '/dev/ttyUSB0',
  baudRate:          9600,   // default
  parity:            'none', // default
  interFrameDelayMs: 0,      // default; not required by this manual, raise if your adapter needs it
  timeoutMs:         2000,
});

Note there is no slaveAddress here — one FdpNetwork manages a shared serial connection, and each FDP3 device's address is supplied separately via network.getDevice(address).


Architecture: one network, many devices, linked units

Unlike single-module interface cards, up to 62 FDP3 devices can share one physical RS-485 bus. FdpNetwork owns the shared connection; network.getDevice(address) returns an FdpDevice for one physical card. Each FdpDevice represents one TCC-NET remote-controller group, which can contain 1–8 linked indoor units:

const network = new FdpNetwork({ port: '/dev/ttyUSB0' });
await network.connect();

const deviceA = network.getDevice(0); // FDP3 wired to indoor unit A's remote controller
const deviceB = network.getDevice(1); // a second, independent FDP3 on the same bus

const statusA = await deviceA.getPrimaryUnitStatus();
const statusB = await deviceB.getPrimaryUnitStatus();

Control (H0001H0005) is always group-wide — it affects every linked unit behind one FDP3's remote controller together, since they share a single remote controller. Readback of individual linked units (getUnitStatus(1..8)) is per-unit diagnostics only.


API reference

FdpNetwork

new FdpNetwork(config: ModbusConfig)
new FdpNetwork(config: ModbusConfig, transport: IModbusTransport) // inject for testing

await network.connect()
await network.disconnect()
network.isConnected // boolean

network.getDevice(address: number): FdpDevice // address 0–63

FdpDevice

device.address // the Modbus slave address this device answers to

// Control — group-wide, affects every linked unit
await device.control({
  onOff:      true,
  mode:       RunMode.Heat,     // Auto | Heat | Fan | Cool | Dry
  fanSpeed:   FanSpeed.Medium,  // Auto | Low | Medium | High
  louvre:     Louvre.Deg45,     // Swing | Deg0 | Deg20 | Deg45 | Deg70 | Deg90
  setpointC:  21,               // plain integer, 10–40 °C
})

// Control limiting (restrict what the remote controller can set)
await device.setControlLimits({
  setpointMinC:    18,
  setpointMaxC:    28,
  fanSpeedInhibit: [FanSpeed.Auto],
  modeInhibit:     [RunMode.Dry],
  louvreInhibit:   [],
})

// Update mode (keypad lock + write-propagation behaviour)
await device.setUpdateMode({ global: UpdateMode.OnChange })
await device.setUpdateMode({ onOff: UpdateMode.Central }) // override just one field

// Read back current control / limits / update-mode state — H0001-H0024 are
// Holding Registers (Read/Write per the manual), so they reflect whatever is
// currently set, whether it got there via this library, the remote
// controller, or another Modbus master on the bus.
const control: DeviceControlState   = await device.getControl()
const limits:  ControlLimitsState   = await device.getControlLimits()
const modes:   UpdateModeState      = await device.getUpdateMode() // H0011-H0015 only, H0010 is write-only

// Group summary of all linked units
const group: GroupStatus = await device.getGroupStatus()

// Wired remote controller readback
const rc: RemoteControllerStatus = await device.getRemoteControllerStatus()

// Individual linked unit (1–8)
const unit: UnitStatus = await device.getUnitStatus(3)

// Convenience for the common single-unit wiring case — same as getUnitStatus(1)
const primary: UnitStatus = await device.getPrimaryUnitStatus()

// All 8 slots — units with `exists: false` simply aren't wired
const all: UnitStatus[] = await device.getAllUnitStatuses()

UnitStatus — key fields

| Field | Type | Notes | |-------|------|-------| | unitIndex | number | 1–8 | | exists | boolean | false if no unit is wired at this index | | isFault / faultCode | boolean / FaultCode | faultCode.code is e.g. 'E04', or null if no fault | | returnAirTemperatureC | number | °C | | filterAlarm | boolean | | | thermoState | ThermoState | IdleFan / Heating / Cooling | | coilTcTemperatureC / coilTcjTemperatureC | number | °C | | duty / dutyPercent | number | 0–15 raw, 0–100 converted | | defrostActive | boolean | | | lineAddress / unitAddress | number | 1–32, TCC-NET addressing |


Custom transport

Replace the serial port with any object implementing IModbusTransport — useful for testing or wrapping a TCP-to-serial bridge. Note every method takes an explicit slaveAddress, since one transport can serve many devices on a shared bus:

import { FdpNetwork, IModbusTransport } from 'fdp3-modbus';

class MockTransport implements IModbusTransport {
  isConnected = false;
  async connect() { this.isConnected = true; }
  async disconnect() { this.isConnected = false; }
  async readHoldingRegisters(_slave: number, _addr: number, count: number) { return Array(count).fill(0); }
  async readInputRegisters(_slave: number, _addr: number, count: number) { return Array(count).fill(0); }
  async writeSingleRegister(_slave: number, _addr: number, _val: number) {}
  async writeMultipleRegisters(_slave: number, _addr: number, _vals: number[]) {}
}

const network = new FdpNetwork({ port: '' }, new MockTransport());

Examples

Two runnable examples live in examples/:

  • examples/cli — terminal dashboard (tsx, no build step) that polls one or more FDP3 devices on a shared bus and prints group summary, remote controller, and linked-unit diagnostics. Read-only.
  • examples/ui — Express + Vue 3 + PrimeVue web dashboard for a single FDP3 device, including a --mock demo transport (no hardware needed) that seeds a two-linked-unit "twin" group. Includes device-wide control (power/mode/fan/louvre/setpoint).

Building

npm run build        # tsdown → dist/index.mjs + dist/index.js + .d.ts
npm run build:watch  # rebuild on source changes
npm run typecheck    # tsc --noEmit (no bundling, fast)

modbus-serial is kept external and must be installed separately by consumers.


Testing

npm test             # jest + ts-jest, no hardware needed
npm run test:watch
npm run test:coverage

All tests use an in-memory MockTransport — no RS-485 adapter required. There's also a standalone runner that works without npm install: npx tsx test/run.ts.


Project layout

src/
  index.ts               Public entry point
  types/                 Config, enums, status shapes, control shapes
  registers/             Modbus PDU address constants and unitAddress() formula
  codec/                 Temperature (x100), fault code, duty, and inhibit-bitmask helpers
  transport/             IModbusTransport interface + RS-485 SerialTransport
  client/                FdpNetwork, FdpDevice

test/                    Jest tests (MockTransport, no hardware)
tsdown.config.ts         Build config (ESM + CJS, external: modbus-serial)
tsconfig.json            Type-check config (module: Preserve, moduleResolution: Bundler)
tsconfig.test.json       Jest config override (module: CommonJS)
AGENTS.md                Guidance for AI coding agents
CHANGELOG.md             Release history (Keep a Changelog format)

Publishing / releasing

Releases are published manually to the public npm registry — there is no CI automation.

  1. Make sure main is clean and npm test / npm run typecheck pass.
  2. Update CHANGELOG.md — move [Unreleased] items under a new version heading.
  3. Bump the version and tag it:
    npm version patch   # or minor / major
  4. Publish. prepublishOnly automatically re-runs typecheck, test, and build first, so a broken build can't ship:
    npm publish
    Use npm publish --dry-run beforehand to preview exactly what will be included (per the files field: dist/ + src/) without actually publishing.
  5. Push the version commit and tag:
    git push && git push --tags

License

BSD-2-Clause — see LICENSE.