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

sunvote-ars-client

v1.2.0

Published

Unofficial open-source client for SunVote ARS (Audience Response System) hardware. Not affiliated with Changsha SunVote Information Technology Co., Ltd.

Readme

sunvote-ars-client

CI npm version License: Apache 2.0

Unofficial open-source TypeScript client for SunVote ARS (Audience Response System) hardware. Communicates with PVS-2010-433M wireless voting receivers via serial port.

Disclaimer: This project is NOT affiliated with, endorsed by, or sponsored by Changsha SunVote Information Technology Co., Ltd. "SunVote" is a trademark of its respective owner. See NOTICE for details.

Installation

npm install sunvote-ars-client

Note: The serialport package is a dependency and requires native compilation. On some platforms you may need build tools installed (Python, C++ compiler).

Quick Start

import { SunVoteController } from 'sunvote-ars-client';

const ctrl = new SunVoteController();
ctrl.on('keypad:press', (press) => console.log(press));
const config = await ctrl.autoConnect();
await ctrl.startVoting();

API Reference

SunVoteController

The main high-level class. Manages connection, voting sessions, and keypad event handling.

Constructor

new SunVoteController(options?: { pollInterval?: number })
  • pollInterval -- Interval between keypad poll cycles in ms (default: 50).

Properties

| Property | Type | Description | |---|---|---| | currentState | SessionState | Current state: 'idle', 'connected', or 'voting'. | | config | BaseConfig \| null | Base station config after connecting. |

Methods

autoConnect(options?): Promise<BaseConfig>

Find a SunVote receiver automatically via USB and connect to it.

const config = await ctrl.autoConnect({ baudRate: 19200, debug: true });
  • options.baudRate -- Serial baud rate (default: 19200).
  • options.debug -- Log all TX/RX packets as hex to console.debug().
  • Throws if no receiver is found.
connect(options): Promise<BaseConfig>

Connect to a specific serial port.

const config = await ctrl.connect({ path: '/dev/ttyUSB0', debug: true });
  • options.path -- Serial port path (required).
  • options.baudRate -- Baud rate (default: 19200).
  • options.debug -- Enable debug logging.
disconnect(): Promise<void>

Disconnect from the receiver. Stops voting if active.

startVoting(options?): Promise<void>

Start a voting session and begin polling keypads. The controller internally runs a five-packet activation sequence (C1 start → C2 clear → C4 poll → C5×2 wake broadcast with a 50 ms RF gap) so the keypads actually come out of sleep.

// Using the defaults — known to produce raw bitmap button codes on PVS-W00 keypads.
await ctrl.startVoting();

// Explicit:
await ctrl.startVoting({ options: 2, minSelections: 1, maxSelections: 6 });
  • options.mode -- Voting mode (default: 0x05, matches the captured reference session).
  • options.options -- Number of answer options advertised to the keypads (default: 2).
  • options.minSelections -- Minimum selections required (default: 1).
  • options.maxSelections -- Maximum selections allowed (default: 6).

Hardware note: On the PVS-W00 keypad family, maxSelections = 1 has been observed to make the base return an obfuscated single byte instead of the raw bitmap. If you see unexpected button codes, start with the defaults above.

stopVoting(): Promise<void>

Stop the current voting session and halt polling.

writeConfig(config): Promise<void>

Write a new base station configuration. Must be connected and not voting.

writeKeypadId(keypadId): Promise<void>

Assign an ID to a keypad in programming mode.

readKeypadId(): Promise<number | null>

Read the ID from a keypad in programming mode.

getKeypads(): Map<number, KeypadPress | null>

Get a snapshot of all known keypads and their last button press. Keys are keypad IDs; values are the last KeypadPress object, or null if the keypad was detected but hasn't pressed a button yet.

static listPorts(): Promise<PortInfo[]>

List all serial ports on the system.

static findPort(): Promise<string | null>

Find the first FTDI-based (SunVote) serial port.

Events

Subscribe to events with ctrl.on(event, handler).

| Event | Payload | Description | |---|---|---| | keypad:press | KeypadPress | A keypad's selected button changed. Deduplicated — two taps of the same button in a row collapse into one event. Best for "currently selected answer" UIs. | | keypad:click | KeypadPress | Every physical tap reported by the base — no dedup. Best for vote tallies and analytics where each tap should count. | | keypad:new | number (keypadId) | A previously unseen keypad was detected. | | state:change | (newState, oldState) | The session state changed. | | base:config | BaseConfig | Base station config was read or written. | | error | Error | An error occurred (poll failure, disconnect, etc.). |

Types

KeypadPress

interface KeypadPress {
  keypadId: number;
  button: number;       // Raw button byte (0x01, 0x02, 0x04, 0x08, 0x10, 0x20)
  buttonLabel: string;  // Human-readable label ("1/A", "2/B", etc.)
  timestamp: number;    // Date.now() when the press was detected
  counter?: number;     // Raw per-slot byte from the poll response (payload[13]),
                        // exposed for debugging / custom dedup logic.
}

BaseConfig

interface BaseConfig {
  baseId: number;
  keyFrom: number;
  keyTo: number;
  keyMax: number;
  channel: number;
}

SessionState

enum SessionState {
  Idle = 'idle',
  Connected = 'connected',
  Voting = 'voting',
}

Low-Level API

For advanced usage, the SDK also exports:

  • SunVoteReceiver -- Direct serial I/O wrapper.
  • buildShortPacket() / buildLongPacket() -- Packet builders.
  • parsePacket() / extractPackets() -- Packet parsers.
  • PacketAssembler -- Incremental assembler for fragmented serial reads.
  • crc16() -- CRC-16/CCITT implementation.
  • listPorts() / findSunVotePort() -- Port discovery.

Electron Integration

The SDK works in Electron's main process (it requires Node.js serialport).

// main.ts (Electron main process)
import { SunVoteController } from 'sunvote-ars-client';
import { ipcMain, BrowserWindow } from 'electron';

const ctrl = new SunVoteController();

ctrl.on('keypad:press', (press) => {
  BrowserWindow.getAllWindows().forEach((win) => {
    win.webContents.send('keypad-press', press);
  });
});

ctrl.on('error', (err) => {
  console.error('SunVote error:', err.message);
});

ipcMain.handle('sunvote:connect', async () => {
  return ctrl.autoConnect({ debug: true });
});

ipcMain.handle('sunvote:start-voting', async () => {
  await ctrl.startVoting();
});

ipcMain.handle('sunvote:stop-voting', async () => {
  await ctrl.stopVoting();
});

ipcMain.handle('sunvote:disconnect', async () => {
  await ctrl.disconnect();
});

ipcMain.handle('sunvote:keypads', () => {
  return Object.fromEntries(ctrl.getKeypads());
});
// renderer (preload bridge)
const { ipcRenderer } = require('electron');

ipcRenderer.on('keypad-press', (_event, press) => {
  console.log('Keypad pressed:', press);
});

await ipcRenderer.invoke('sunvote:connect');
await ipcRenderer.invoke('sunvote:start-voting');

Debug Mode

Pass debug: true to connect() or autoConnect() to enable verbose logging:

[sunvote] 2025-01-15T10:30:00.123Z Connecting to /dev/ttyUSB0
[sunvote] 2025-01-15T10:30:00.124Z Opening port /dev/ttyUSB0 @ 19200 baud
[sunvote] 2025-01-15T10:30:00.200Z TX: f5 aa aa 0c 11 00 00 0f 0c 00 00 00 00 00 7a d9
[sunvote] 2025-01-15T10:30:00.250Z RX raw: f5 aa aa 0c 91 01 ...
[sunvote] 2025-01-15T10:30:00.251Z State: idle -> connected

All debug output uses console.debug() prefixed with [sunvote] and ISO timestamps.

Cross-Platform Notes

  • macOS: Port paths look like /dev/tty.usbserial-XXXX. FTDI drivers are built into macOS 10.15+.
  • Linux: Port paths look like /dev/ttyUSB0. You may need to add your user to the dialout group: sudo usermod -aG dialout $USER.
  • Windows: Port paths look like COM3. On Windows 10/11, FTDI drivers usually install automatically via Windows Update a few seconds after the receiver is plugged in. See Driver Setup below.

The SDK auto-detects SunVote receivers by looking for FTDI vendor ID 0403.

Driver Setup

The SDK does not bundle FTDI drivers — FTDI's CDM Driver License restricts redistribution to hardware sellers/distributors of devices that contain a Genuine FTDI Component. End-users have the right to download and install the driver themselves directly from FTDI under their own license.

For most users, no manual setup is needed:

  • macOS 10.15+ and Linux ship with a working FTDI driver out of the box.
  • Windows 10/11 auto-installs the FTDI driver via Windows Update the first time the receiver is plugged in.

If automatic install does not happen, the SDK exposes helpers your application can use to guide the end-user through manual setup:

import {
  checkDriver,
  getDriverInstallInfo,
  openDriverDownloadPage,
} from 'sunvote-ars-client';

const status = await checkDriver();
if (!status.installed) {
  const info = getDriverInstallInfo(status);
  console.log(info.instructions);          // human-readable next steps
  console.log(info.downloadUrl);           // official FTDI URL

  // Optional: open the FTDI download page in the user's default browser.
  await openDriverDownloadPage();
}

Bundling Drivers (Hardware Distributors Only)

If you ship your application together with hardware that contains a Genuine FTDI Component, FTDI's license permits you to bundle the CDM driver files inside your app. Place ftdibus.inf, ftdiport.inf, and the supporting files under e.g. drivers/win32/, then call:

import { app } from 'electron';
import { join } from 'path';
import { installDriver } from 'sunvote-ars-client';

await installDriver(join(app.getAppPath(), 'drivers', 'win32'));

installDriver() shells out to pnputil /add-driver ... /install and requires Administrator privileges. On non-Windows platforms it is a no-op.

Do not bundle FTDI drivers if your application is software-only. Distributing them on npm or as part of a generic app violates the FTDI CDM Driver License (§3.1.7).

Demo App

A minimal Electron demo lives in demo/. It exercises every SDK surface — driver detection, auto-connect, config readout, voting start/stop, live keypad table, activity log — and is useful both as a smoke test and as a copy-paste reference for your own Electron integration. The demo is a workspace in this repo and is not published to npm.

npm install
npm run demo

See demo/README.md for details.

Contributing

Contributions are welcome! Please read the contributing guidelines before opening a pull request.

License

Apache-2.0 -- see LICENSE and NOTICE for details.