Reverse-engineered from the official FNIRSI Windows app (
FNIRSI Power supply.exe, .NET assembly) and cross-referenced against 4 independent open-source implementations: cho45/fnirsi-dps-150 (JS), svenk123/dps150tool (C), KochC/DPS-150-python-library (Python), huiminghao/fnirsi-dps-150-rs (Rust).
| Parameter | Value |
|---|---|
| Interface | USB CDC (Virtual COM Port) |
| USB VID:PID | 0x2E3C:0x5740 |
| Baud rates | 9600, 19200, 38400, 57600, 115200 (default) |
| Data bits | 8 |
| Parity | None |
| Stop bits | 1 |
| Flow control | RTS asserted (see note) |
| Read timeout | 500ms (official app) |
Baud rate index mapping:
| Index | Baud Rate |
|---|---|
| 0 | AUTO (reserved, not used by official app) |
| 1 | 9600 |
| 2 | 19200 |
| 3 | 38400 |
| 4 | 57600 |
| 5 | 115200 |
Note on RTS: The official app sets
RtsEnable = truewhich asserts the RTS line but does NOT enable full RTS/CTS hardware flow control. The device may require RTS to be asserted before it will send data. cho45's WebSerial implementation usesflowControl: 'hardware'(full RTS/CTS) which also works. When in doubt, assert RTS.
Note: The index sent over the wire is 1-based. Some implementations use a 0-based array internally and add 1 before sending (e.g. array index 4 + 1 = wire value 5 for 115200). The svenk123 C tool sends index 4 directly — this may be a bug in that implementation, or the device may accept both. Use index 5 for 115200 to match the official app.
Every packet (TX and RX) follows the same format:
┌────────┬─────────┬──────────┬────────┬──────────────┬──────────┐
│ Header │ Command │ Register │ Length │ Data (0..N) │ Checksum │
│ 1 byte │ 1 byte │ 1 byte │ 1 byte │ N bytes │ 1 byte │
└────────┴─────────┴──────────┴────────┴──────────────┴──────────┘
| Field | Size | Description |
|---|---|---|
| Header (c1) | 1 | 0xF1 for host→device, 0xF0 for device→host |
| Command (c2) | 1 | Command category (see below) |
| Register (c3) | 1 | Target register address |
| Length (c4) | 1 | Number of data bytes that follow |
| Data (c5) | N | Payload (variable length, can be 0) |
| Checksum (c6) | 1 | (c3 + c4 + sum(c5[])) & 0xFF |
Total packet size = 5 + Length
checksum = register + length
for each byte in data:
checksum = (checksum + byte) & 0xFF
Note: The header (c1) and command (c2) bytes are NOT included in the checksum.
| Byte | Hex | Direction | Purpose |
|---|---|---|---|
| 161 | 0xA1 | Read | Read register value / response from device |
| 176 | 0xB0 | Write | Set baud rate |
| 177 | 0xB1 | Write | Write register value |
| 192 | 0xC0 | Write | Enter firmware upgrade (DFU) mode — not a normal restart (see §12) |
| 193 | 0xC1 | Write | Session control (connect/disconnect) |
TX: F1 C1 00 01 01 02
After opening the serial port, send this handshake.
The official app then reads register 0xE1 to verify the device address:
TX: F1 A1 E1 01 00 E2 (read device address register)
The device responds with its address byte (configurable, range 1-255). The official app compares this against the user-selected device address ("设备地址"). If they match, IsSpecific is set to true and initialization proceeds. The app retries up to 2 times with a 1-second delay between attempts before giving up.
Most third-party implementations skip this verification step entirely and it works fine.
After successful handshake, send these commands in order:
- Set baud rate:
F1 B0 00 01 05 06(05 = 115200) - Read model name:
F1 A1 DE 01 00 DF - Read firmware version:
F1 A1 E0 01 00 E1 - Read hardware version:
F1 A1 DF 01 00 E0 - Read all registers:
F1 A1 FF 01 00 00
Note: The official app sends firmware version (0xE0) before hardware version (0xDF). The order doesn't matter functionally — they are independent reads. A 1-second timer retries the model name and firmware version reads until responses are received.
TX: F1 C1 00 01 00 01
Once connected, the device automatically pushes telemetry data. There are two categories:
Periodic telemetry (sent continuously, even when output is stopped):
| Register | Hex | Description |
|---|---|---|
| 192 | 0xC0 | Input voltage |
| 195 | 0xC3 | Output V/I/P (3×float32, ~every 500ms) |
| 196 | 0xC4 | Temperature |
| 226 | 0xE2 | Max voltage capability |
| 227 | 0xE3 | Max current capability |
On-change telemetry (sent when state changes):
| Register | Hex | Description |
|---|---|---|
| 219 | 0xDB | Output state changed |
| 220 | 0xDC | Protection status changed |
| 221 | 0xDD | CV/CC mode changed |
Only during RUN (sent only when output is enabled):
| Register | Hex | Description |
|---|---|---|
| 217 | 0xD9 | Ah counter (capacity) |
| 218 | 0xDA | Wh counter (energy) |
You can also poll explicitly by sending read-all (F1 A1 FF 01 00 00).
A ~50ms delay between consecutive commands is recommended for reliable communication. The svenk123 C implementation uses usleep(50000) after each write.
| Register | Hex | Type | Description |
|---|---|---|---|
| 192 | 0xC0 | float32 | Input voltage reading |
| 193 | 0xC1 | float32 | Voltage set-point |
| 194 | 0xC2 | float32 | Current set-point |
| 195 | 0xC3 | 3×float32 | Output voltage, current, power (12 bytes) |
| 196 | 0xC4 | float32 | Internal temperature (°C) |
| 197-208 | 0xC5-0xD0 | float32 | Preset M1-M6 voltage/current (see §5.3) |
| 214 | 0xD6 | uint8 | Display brightness |
| 215 | 0xD7 | uint8 | Volume (beep level) |
| 217 | 0xD9 | float32 | Ah counter (amp-hours) |
| 218 | 0xDA | float32 | Wh counter (watt-hours) |
| 219 | 0xDB | uint8 | Output state (0=off, 1=on) |
| 220 | 0xDC | uint8 | Protection status (see §6) |
| 221 | 0xDD | uint8 | Regulation mode (0=CC, 1=CV) |
| 222 | 0xDE | string | Model name |
| 223 | 0xDF | string | Hardware version |
| 224 | 0xE0 | string | Firmware version |
| 225 | 0xE1 | uint8 | Device address (1-255, for multi-device verification) |
| 226 | 0xE2 | float32 | Max voltage capability |
| 227 | 0xE3 | float32 | Max current capability |
| 255 | 0xFF | block | Full state dump (see §7) |
| Register | Hex | Type | Description |
|---|---|---|---|
| 193 | 0xC1 | float32 | Set voltage |
| 194 | 0xC2 | float32 | Set current limit |
| 197 | 0xC5 | float32 | Preset M1 voltage |
| 198 | 0xC6 | float32 | Preset M1 current |
| 199 | 0xC7 | float32 | Preset M2 voltage |
| 200 | 0xC8 | float32 | Preset M2 current |
| 201 | 0xC9 | float32 | Preset M3 voltage |
| 202 | 0xCA | float32 | Preset M3 current |
| 203 | 0xCB | float32 | Preset M4 voltage |
| 204 | 0xCC | float32 | Preset M4 current |
| 205 | 0xCD | float32 | Preset M5 voltage |
| 206 | 0xCE | float32 | Preset M5 current |
| 207 | 0xCF | float32 | Preset M6 voltage |
| 208 | 0xD0 | float32 | Preset M6 current |
| 209 | 0xD1 | float32 | OVP threshold (volts) |
| 210 | 0xD2 | float32 | OCP threshold (amps) |
| 211 | 0xD3 | float32 | OPP threshold (watts) |
| 212 | 0xD4 | float32 | OTP threshold (°C) |
| 213 | 0xD5 | float32 | LVP threshold (volts) |
| 214 | 0xD6 | uint8 | Display brightness |
| 215 | 0xD7 | uint8 | Volume (beep level) |
| 216 | 0xD8 | uint8 | Metering enable (0=stop, 1=start) |
| 219 | 0xDB | uint8 | Output enable (0=off, 1=on) |
Note on brightness range: The official app writes 0-255, but the Python library enforces 0-10. The actual hardware range needs verification on-device.
For preset Mn (n = 1..6):
- Voltage register =
0xC3 + 2*n=0xC5, 0xC7, 0xC9, 0xCB, 0xCD, 0xCF - Current register =
0xC3 + 2*n + 1=0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0
| Value | Status | Description |
|---|---|---|
| 0 | OK | Normal operation |
| 1 | OVP | Over-voltage protection triggered |
| 2 | OCP | Over-current protection triggered |
| 3 | OPP | Over-power protection triggered |
| 4 | OTP | Over-temperature protection triggered |
| 5 | LVP | Low/under-voltage protection triggered |
| 6 | REP | Reverse polarity/connection protection triggered |
Request: F1 A1 FF 01 00 00
Response payload is ~139 bytes. All float values are IEEE 754 single-precision, little-endian.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 (0) | 4 | d1 | Input voltage (V) |
| 0x04 (4) | 4 | d2 | Voltage set-point (V) |
| 0x08 (8) | 4 | d3 | Current set-point (A) |
| 0x0C (12) | 4 | d4 | Measured output voltage (V) |
| 0x10 (16) | 4 | d5 | Measured output current (A) |
| 0x14 (20) | 4 | d6 | Measured output power (W) |
| 0x18 (24) | 4 | d7 | Temperature (°C) |
| 0x1C (28) | 4 | d8 | Preset M1 voltage |
| 0x20 (32) | 4 | d9 | Preset M1 current |
| 0x24 (36) | 4 | d10 | Preset M2 voltage |
| 0x28 (40) | 4 | d11 | Preset M2 current |
| 0x2C (44) | 4 | d12 | Preset M3 voltage |
| 0x30 (48) | 4 | d13 | Preset M3 current |
| 0x34 (52) | 4 | d14 | Preset M4 voltage |
| 0x38 (56) | 4 | d15 | Preset M4 current |
| 0x3C (60) | 4 | d16 | Preset M5 voltage |
| 0x40 (64) | 4 | d17 | Preset M5 current |
| 0x44 (68) | 4 | d18 | Preset M6 voltage |
| 0x48 (72) | 4 | d19 | Preset M6 current |
| 0x4C (76) | 4 | d20 | OVP set-point (V) |
| 0x50 (80) | 4 | d21 | OCP set-point (A) |
| 0x54 (84) | 4 | d22 | OPP set-point (W) |
| 0x58 (88) | 4 | d23 | OTP set-point (°C) |
| 0x5C (92) | 4 | d24 | LVP set-point (V) |
| 0x60 (96) | 1 | d25 | Brightness (0-255) |
| 0x61 (97) | 1 | d26 | Volume (beep level) |
| 0x62 (98) | 1 | d27 | Metering state (0=open/running, 1=closed/stopped) |
| 0x63 (99) | 4 | d28 | Ah counter (amp-hours) |
| 0x67 (103) | 4 | d29 | Wh counter (watt-hours) |
| 0x6B (107) | 1 | d30 | Output state (0=off, 1=on) |
| 0x6C (108) | 1 | d31 | Protection status (0-6, see §6) |
| 0x6D (109) | 1 | d32 | CV/CC mode (0=CC, 1=CV) |
| 0x6E (110) | 1 | d33 | Unknown (reads as a byte, purpose unclear) |
| 0x6F (111) | 4 | d34 | Max voltage capability (V) |
| 0x73 (115) | 4 | d35 | Max current capability (A) |
| 0x77 (119) | 4 | d36 | OVP max ceiling (V) |
| 0x7B (123) | 4 | d37 | OCP max ceiling (A) |
| 0x7F (127) | 4 | d38 | OPP max ceiling (W) |
| 0x83 (131) | 4 | d39 | OTP max ceiling (°C) |
| 0x87 (135) | 4 | d40 | LVP max ceiling (V) |
d36-d40 (offsets 0x77-0x87): These are the maximum configurable ceilings for each protection threshold. cho45 reads them but marks them as unknown. The .NET decompilation confirms they correspond to OVP/OCP/OPP/OTP/LVP maximums, in that order.
Default capabilities (before device reports actual values):
- Max voltage: 24.0V
- Max current: 5.0A
All numeric values transmitted over the wire use:
| Type | Size | Encoding |
|---|---|---|
| float32 | 4 bytes | IEEE 754 single-precision, little-endian |
| uint8 | 1 byte | Unsigned byte |
| string | variable | ASCII (null-terminated or length-delimited by packet) |
TX: F1 C1 00 01 01 02
│ │ │ │ │ └─ checksum: (0x00+0x01+0x01) & 0xFF = 0x02
│ │ │ │ └──── data: 0x01 (enable)
│ │ │ └─────── length: 1
│ │ └────────── register: 0x00
│ └───────────── command: 0xC1 (session control)
└──────────────── header: 0xF1 (host→device)
TX: F1 C1 00 01 00 01
TX: F1 A1 FF 01 00 00
TX: F1 B1 DB 01 01 DD
TX: F1 B1 DB 01 00 DC
TX: F1 B1 C1 04 00 00 A0 40 A5
└──────────┘
IEEE 754 LE: 5.0
checksum: (0xC1+0x04+0x00+0x00+0xA0+0x40) & 0xFF = 0xA5
TX: F1 B1 C2 04 00 00 80 3F 85
└──────────┘
IEEE 754 LE: 1.0
TX: F1 B0 00 01 05 06
└── baud index 5 = 115200
TX: F1 B1 D7 01 09 E1
TX: F1 B1 D6 01 05 DC
TX: F1 B1 D1 04 00 00 C8 41 4E
TX: F1 B1 D8 01 01 DA
TX: F1 A1 DE 01 00 DF
RX: F0 A1 DE XX [string bytes...] [checksum]
TX: F1 A1 E0 01 00 E1
RX: F0 A1 E0 XX [string bytes...] [checksum]
TX: F1 C0 00 01 01 02
WARNING: This does NOT perform a normal restart. The device enters its bootloader (firmware upgrade mode) and the USB CDC port disappears. A physical USB unplug/replug is required to return to normal operation. No reference implementation uses this command. It was identified from the .NET decompilation's
CMD_192handler.
Responses from the device use header byte 0xF0 instead of 0xF1. The rest of the structure is identical:
F0 [command] [register] [length] [data...] [checksum]
To parse a response buffer that may contain multiple packets:
- Find packet boundary: total size =
5 + buffer[offset + 3] - Minimum valid response is 5 bytes (header + command + register + length + checksum)
- Extract data payload: bytes from offset+4 to offset+4+length (exclusive of checksum)
- Dispatch based on register byte (offset+2)
These features are implemented entirely in the PC software, not in device firmware. They work by sending repeated set-voltage/set-current commands with timing delays.
| Parameter | Type | Description |
|---|---|---|
| V | float | Fixed voltage |
| startA | float | Starting current |
| stopA | float | Ending current |
| stepA | float | Current increment per step |
| sleepTime | float | Delay between steps (seconds) |
| Parameter | Type | Description |
|---|---|---|
| A | float | Fixed current |
| startV | float | Starting voltage |
| stopV | float | Ending voltage |
| stepV | float | Voltage increment per step |
| sleepTime | float | Delay between steps (seconds) |
| Parameter | Type | Description |
|---|---|---|
| sn | int | Step sequence number |
| V | float | Voltage for this step |
| A | float | Current for this step |
| sleepTime | float | Dwell time (seconds) |
Up to 10 steps, loopable N times with configurable start/stop rows.
- The device defaults to CV mode (d32=1) on initialization.
- After writing preset values, send a full read-all (0xFF) to refresh state — the device does not echo individual register writes.
- Temperature is reported in Celsius by the device.
- Offset 0x6E (byte 110) in the full dump is not read by the official app. The gap between offset 109 (CV/CC mode) and 111 (max voltage) is simply skipped. Treat as reserved.
- Protection naming: register 0xD5 and protection code 5 are referred to as both "UVP" (under-voltage) and "LVP" (low-voltage) across implementations. They are the same thing.
- Protection code 6 (REP): documented by cho45, Python, and Rust implementations but not handled by the official Windows app. The device likely sends it for reverse polarity events, but the official app ignores it.
- Read requests: the official app sends reads with length=1 and a zero data byte (e.g.
F1 A1 DE 01 00 DF). Some third-party implementations send reads with length=0 and no data byte (e.g.F1 A1 DE 00 DE). Both formats are accepted by the device. - Checksum validation may be lenient: The official app has a bug in
CmdMoudle.setContent()where modifying a command's payload does not recalculate the checksum. This means the baud rate command (CMD_13) is sent with an incorrect checksum, yet the device accepts it. Implementations should still compute correct checksums, but be aware the device may not strictly validate them on the RX side. - Volume (offset 0x61 / register 0xD7): present in the full state dump and writable as a register, but the official Windows app never exposes it in the UI. Third-party implementations (cho45, Python, Rust) all document and use it.
- Command 0xC0 is NOT a restart — it enters firmware upgrade (DFU/bootloader) mode. The USB CDC port disappears and a physical unplug/replug is required to recover. None of the 4 reference implementations use this command. It was originally labeled "System Restart" based on the .NET decompilation, but on-device testing confirms it enters the bootloader. There is no known command for a normal soft restart.
| Feature | Official App | cho45 (JS) | svenk123 (C) | KochC (Python) | Rust |
|---|---|---|---|---|---|
| Packet structure | Y | Y | Y | Y | Y |
| Checksum algorithm | Y | Y | Y | Y | Y |
| Session connect/disconnect | Y | Y | Y | Y | partial |
| Voltage/current set (float32) | Y | Y | N (byte only) | Y | N (byte only) |
| Preset registers (M1-M6) | Y | Y | - | Y | Y |
| Protection thresholds (float32) | Y | Y | N (byte only) | Y | Y |
| Volume register (0xD7) | Y* | Y | - | Y | Y |
| Metering enable (0xD8) | Y | Y | - | Y | - |
| Full state dump (0xFF) | Y | Y | - | Y | partial |
| Protection ceiling fields (d36-d40) | Y | - | - | - | - |
| REP protection (code 6) | - | Y | - | Y | Y |
| Auto-pushed telemetry | Y | Y | - | Y | - |
| USB VID:PID | - | Y | - | - | - |
Y = implemented/documented, - = not covered, N = different approach, * = byte exists in dump but not exposed in UI