CLI + Web-GUI controller for Eltek Flatpack2 48V/2000W HE rectifiers via CAN bus
Tested on real hardware with Waveshare USB-CAN-A adapter (STM32, CH341).
This project is tested and works on real hardware, but it is experimental software based on reverse engineering of an undocumented CAN protocol. It is provided as-is, with no warranty, and no liability for any damage resulting from its use – including damage to your PSU, battery, or other connected equipment. Not affiliated with Eltek. You use this software entirely at your own risk.
Full text: DISCLAIMER.md
- CAN communication via Waveshare USB-CAN-A (native binary protocol, not slcan)
- Auto-detection of adapter by USB VID:PID
1a86:7523 - Virtual PTY terminal – connect with
screen /tmp/flatpack2.ptyfrom any SSH session- Top status bar for warnings/errors (PSU ALARM, CAN loss/reconnect, charger error stops) – color-coded, timestamped, drawn on row 1 via ANSI scroll region so async events never corrupt the prompt
- Web-GUI dashboard – mobile-first dark theme, live SSE updates every 3 s (configurable
sse_interval)- Live Vout·Iout in the header at all times; Ah·Wh added in charger mode
- PSU status (Vout, Iout, Vin, temperatures, mode)
- Set voltage / current with confirmation dialog
- Standby button – sets PSU to 48.0 V / 0.1 A safe idle state (values shown inline)
- LiFePO4 charger control (start / stop / change current / reset / SOC progress bar / stop reason)
- Battery parameters card – static config overview (cells, voltages, currents, limits)
- Live graphs: Vout+Iout, Delivered capacity [Ah], Delivered energy [Wh] – time window 15 min / 1 h / 12 h
- CC→CV transition annotation in graph
- Live log tail
- CSV history export (
/api/history)
- CSV history persistence (optional) – samples written to disk (buffered), last 12 h reloaded into the graphs on restart, automatic retention trim
- LiFePO4 CC/CV charger with soft-start and safe battery detection
- DETECT phase: PSU holds low
detect_voltageat minimaldetect_current; battery detected via voltage rise on output (no inrush current) - RAMP phase: voltage stepped up from V_bat to target in configurable steps; avoids inrush spikes on connection
- CC phase: constant current until target voltage reached
- CV phase: voltage held, current tapers until
charge_current_tail - Battery-disconnect detection in every phase (RAMP/CC/CV) returns to DETECT automatically; Ah/Wh counters are preserved
- Change charge current on the fly and reset the cycle (CLI + Web-GUI)
- Rough SOC estimate – OCV anchor at detection + coulomb counting
- ALARM safety – PSU alarm in any phase aborts the charge and puts the PSU into standby (manual restart required)
- Auto-start on program startup and after CAN reconnect (configurable)
- DETECT phase: PSU holds low
- Standby mode – sets PSU to 48.0 V / 0.1 A; overwrites restore values so reconnect keeps standby
- Value restore on reconnect – last set voltage/current automatically re-applied after CAN bus recovery
- Charge resume on reconnect – if charging was active before CAN loss, it resumes automatically
- Daemonization – double-fork, PID file, systemd service included
- CAN watchdog – auto-reconnect on communication loss
- Per-PSU startup config – serial number mapping, auto-apply on discovery
- Configurable power rating –
power_rating(1800/2000/3000 W) derives enforced current/power limits for your specific PSU variant - Log rotation –
RotatingFileHandler - SIGHUP reloads log level from the config file specified at startup (
--config)
| Component | Details |
|---|---|
| PSU | Eltek Flatpack2 48V / 2000W HE |
| Adapter | Waveshare USB-CAN-A (Model A, STM32 + CH341) |
| CAN speed | 125 kbit/s |
| Serial baudrate | 2 000 000 baud |
| USB VID:PID | 1a86:7523 |
| Frame | Arbitration ID | Notes |
|---|---|---|
| LOGIN TX | 0x05004804 |
Keepalive every 1 s; PSU times out after 15 s |
| STATUS RX | (arb & 0xFFFFFF00) == 0x05014000 |
8-byte status frame |
| SET TX | 0x05FF4004 |
Broadcast – only this address works |
| ALERT | 0x0501BFFC |
Alert request/response |
SET data format: struct.pack("<HHHH", iout_da, vout_cv, vout_cv, ovp_cv)
iout_da= current × 10 (deciamps)vout_cv= voltage × 100 (centivolts)ovp_cv= OVP voltage × 100
Frame format (Waveshare binary protocol):
AA (E0|len) [ID 4 bytes LE] [data 0–8 bytes] 55
- Python 3.8+
- Linux (uses
/dev/ttyUSB*, sysfs, pty) - User must be in
dialoutgroup (or run as root)
pip install -r requirements.txt
# pyserial>=3.5
# flask>=3.0# Install pipx if not already installed
sudo apt install pipx
pipx ensurepath
# Install flatpack2 directly from GitHub
pipx install git+https://github.com/mti-sk/flatpack2.git
# Run
flatpack2 --config /etc/flatpack2/flatpack2.confThe wheel bundles the config templates, systemd unit and install scripts. After installation run:
flatpack2 --filesThis prints the directory with the bundled files (typically
~/.local/share/pipx/venvs/flatpack2/share/flatpack2/) together with
ready-to-paste commands for copying the config to /etc/flatpack2/ and the
service unit to /etc/systemd/system/ (including the correct ExecStart=
path for your pipx binary). You decide what to copy where - nothing is
written outside the pipx venv automatically.
To upgrade to the latest version:
pipx upgrade flatpack2To uninstall:
pipx uninstall flatpack2# Clone the repository
git clone https://github.com/mti-sk/flatpack2.git
cd flatpack2
# Install (PSU controller only)
sudo bash install.sh
# Or install with LiFePO4 charger config
sudo bash install.sh --config flatpack2_charger.confThe script will:
- Create a Python virtual environment at
- Install dependencies (pyserial, flask)
- Copy program files to
- Copy config to (only if not already present)
- Create wrapper
- Install and start systemd service
- Add current user to group
Upgrade – run the script again; existing config is preserved:
sudo bash install.shUninstall:
# Keep config files
sudo bash uninstall.sh
# Remove everything including config
sudo bash uninstall.sh --purgegit clone https://github.com/mti-sk/flatpack2.git
cd flatpack2
# Create venv and install dependencies
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Add user to dialout (logout/login required)
sudo usermod -aG dialout $USER
# Run in foreground
python3 flatpack2.py
screen /tmp/flatpack2.ptypython3 flatpack2.py --config flatpack2_charger.confsudo cp flatpack2.service /etc/systemd/system/
sudo nano /etc/systemd/system/flatpack2.service # adjust paths if needed
sudo systemctl daemon-reload
sudo systemctl enable flatpack2
sudo systemctl start flatpack2
sudo systemctl status flatpack2
journalctl -u flatpack2 -fTwo config files are provided:
| File | Purpose |
|---|---|
flatpack2.conf |
PSU controller only, charger section commented out |
flatpack2_charger.conf |
Same as above + LiFePO4 charger enabled |
Specify config file with --config:
python3 flatpack2.py --config flatpack2_charger.conf[can]
channel = /dev/ttyUSB0 # tried first; autodetect fallback
bitrate = 125000 # Flatpack2 CAN speed
serial_baudrate = 2000000 # Waveshare adapter serial speed
autodetect = true # find adapter by USB VID:PID
[psu]
ovp_voltage = 60.0 # over-voltage protection (V)
discovery_timeout = 10 # seconds to wait for PSU on startup
power_rating = 2000 # PSU power variant (W): 1800 / 2000 / 3000
# derives enforced I_MAX = power_rating / 48.0V
[webgui]
enabled = true
host = 0.0.0.0 # 127.0.0.1 for localhost only
port = 8080
log_access = true
[daemon]
enabled = false
pidfile = /var/run/flatpack2.pid
[terminal]
type = pty
pty_symlink = /tmp/flatpack2.pty[PSU_1]
serial = 173350049492 # serial number (from 'map' command)
voltage = 54.0 # V
current = 20.0 # A
apply_on_start = true # apply after stable status detected
# ignored when charger auto_start = trueSerial number is matched against the hex bytes reported by the PSU hello frame.
If serial is omitted, mapping is by discovery order (first seen = PSU_1).
[charger]
cell_count = 16 # cells in series
cell_voltage_max = 3.60 # V per cell (CV target = cell_count × cell_voltage_max)
capacity = 400 # Ah (informational)
charge_current = 35.0 # A (CC phase)
charge_current_tail = 4.0 # A (end-of-charge detection in CV phase)
safety_time_limit = 1200 # minutes (20 h) – counted from RAMP start
detect_voltage = 48.0 # V – PSU output during DETECT phase
detect_current = 0.2 # A – PSU current during DETECT phase
detect_threshold = 1.0 # V – vout must exceed detect_voltage + detect_threshold
min_current_detect = 0.5 # A – detection confirmation AND "current collapsed"
# threshold for battery-disconnect detection in CC/CV
disconnect_detect_time = 5.0 # s – iout must stay below min_current_detect this long
# in CC before the battery is deemed disconnected
ramp_step_voltage = 0.1 # V per ramp step
ramp_step_interval = 5.0 # seconds between ramp steps
voltage_tolerance = 0.1 # V – CC→CV transition threshold
monitor_interval = 5.0 # seconds between monitor refreshes
auto_start = true # start charging automatically on program start
# and after CAN reconnect (default: true)| Phase | Description |
|---|---|
| detect | PSU holds detect_voltage at detect_current. Battery detected when vout > detect_voltage + detect_threshold. No inrush current. |
| ramp | Voltage steps up from V_bat by ramp_step_voltage every ramp_step_interval s. Transitions to CC when iout >= charge_current, or to CV when target reached. Battery disconnect returns to detect. |
| CC | Full charge_current applied (PSU in current limit). The voltage setpoint keeps stepping by ramp_step_voltage up to target_voltage – it is never jumped in one step, which would make the PSU's current limiter overshoot and overload the unit. Ends when vout >= target_voltage - voltage_tolerance. If current collapses to ≤ min_current_detect for disconnect_detect_time s, the battery is deemed disconnected and the charger returns to detect (Ah/Wh preserved). |
| CV | Target voltage held. A gradual taper to charge_current_tail ends the charge (done); a sudden current collapse is treated as a disconnect and returns to detect. |
| done | Charging finished when iout <= charge_current_tail via a normal taper. |
| error | Stopped due to PSU ALARM (any phase), high temp, or safety timeout. Requires a manual charge start / charge reset to clear. |
When auto_start = true (default):
- Charging starts automatically after PSU discovery on program startup.
- After a CAN bus reconnect, charging resumes automatically only if it was active before the disconnection. If the user stopped charging manually before the outage, it will not restart.
- When auto-start is active,
apply_on_startin[PSU_x]sections is effectively superseded by the charger.
The charge current can be changed at any time without stopping the cycle:
charge current <A> (CLI) or the Set I button in the Web-GUI. In RAMP/CC/CV
the new current is sent to the PSU immediately; in DETECT it is stored and used
once the battery is detected.
charge reset (CLI) / Reset button clears the Ah/Wh counters, restores the
charge current to the config default and restarts the cycle from DETECT. Note
that a battery disconnect during charging does not clear Ah/Wh – only an
explicit reset does.
The reported state-of-charge is a rough estimate, always labelled as such. LiFePO4 has a very flat voltage curve and the voltage is elevated under charge current, so voltage alone is unreliable. Instead:
- at battery detection the current is near zero, so
vout ≈ OCV; a per-cell OCV→SOC table (linear interpolation) gives the starting SOC, - from there the estimate tracks by coulomb counting (
charged Ah / capacity), - in CV it is pulled toward 100 % as the current tapers to
charge_current_tail.
Accuracy depends on a correct capacity value and on the battery resting near
OCV at detection; treat it as indicative, not a fuel gauge.
By default the measurement history lives only in RAM (12 h ring buffer). Enable persistence to keep it across restarts:
[history]
persist = true # off by default
file = /var/log/flatpack2_history.csv
flush_interval = 60 # s – buffered writes to spare flash
retention_days = 30 # rows older than this trimmed at startup- Columns:
timestamp_ms,vout,iout,ah,wh. - Writes are buffered and flushed every
flush_intervalseconds (and once on shutdown) to avoid hammering an SD card / flash storage. - On startup the last 12 h are loaded back into the graphs, and rows older than
retention_daysare dropped (atomic rewrite via a temp file). - The bundled
flatpack2_charger.confenables persistence;flatpack2.confships it enabled too. Setpersist = falseto keep the previous RAM-only behaviour.
Standby sets the PSU to a safe low-power idle state:
| Parameter | Value |
|---|---|
| Voltage | 48.0 V |
| Current | 0.1 A (PSU minimum) |
Standby overwrites the restore values, so after a CAN bus outage the PSU will return to standby rather than to a previously set charging voltage. This is intentional – use charge start or set to resume normal operation after standby.
# Terminal 1 – start program
python3 flatpack2.py
# Terminal 2 – connect to PTY
screen /tmp/flatpack2.ptypython3 flatpack2.py --daemon
screen /tmp/flatpack2.pty # connect to running daemonOpen in browser: http://<host>:8080
Default is 0.0.0.0:8080 – accessible from any device on the local network.
For localhost-only access set host = 127.0.0.1 in [webgui].
Connect via PTY (screen /tmp/flatpack2.pty) or use stdio mode.
help Show help
get Show status of all PSUs
get <id> Show status of PSU with given ID
set <id|all> <V> <I> Set voltage (V) and current (A)
standby Set ALL PSUs to standby (48.0 V / 0.1 A)
standby <id> Set PSU <id> to standby
map Show serial → ID mapping
charge start [I] Start LiFePO4 charging (optional current override)
charge stop Stop charging
charge current <A> Change charge current while charging
charge reset Clear Ah/Wh, restore default current, restart from DETECT
charge status Show charge status (incl. SOC estimate)
charge monitor Continuous monitor (Enter to stop)
charge config Show charger configuration
charge battery Show battery parameters (static config)
shutdown Stop program
Examples:
get
get 1
set all 54.0 20.0
set 1 48.0 10.0
standby
standby 1
map
charge start
charge start 15.0
charge current 20.0
charge reset
charge status
charge battery
charge monitor
All endpoints return JSON unless noted.
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
Dashboard HTML |
| GET | /events |
SSE stream (interval = sse_interval, default 3 s) |
| GET | /api/status |
Single status snapshot (JSON) |
| POST | /api/set |
Set voltage/current |
| POST | /api/standby |
Set PSU to standby (48.0 V / 0.1 A) |
| POST | /api/charge/start |
Start charging |
| POST | /api/charge/stop |
Stop charging |
| POST | /api/charge/current |
Change charge current while charging |
| POST | /api/charge/reset |
Clear Ah/Wh, restore default current, restart from DETECT |
| GET | /api/history |
Download history as CSV |
POST /api/set
{ "voltage": 54.0, "current": 20.0 }POST /api/charge/start
{ "current": 15.0 } // optional; omit to use config defaultPOST /api/charge/current
{ "current": 20.0 } // required; must be within the PSU current limitPOST /api/charge/reset
{} // no body requiredPOST /api/standby
{} // no body requiredGET /api/history
?window=900 // seconds; omit for full 12h history
Returns CSV: timestamp_ms,vout,iout,ah,wh
The Eltek Flatpack2 48V rectifier is produced in several power variants.
The voltage range and CAN protocol are identical across variants; only the
max current/power differs. Set power_rating in [psu] to match your
hardware – this drives the software-enforced current/power limits.
power_rating |
Voltage | Current (I_MAX) | Power |
|---|---|---|---|
| 1800 W | 43.5 – 57.6 V | ~37.5 A | 1800 W |
| 2000 W (default) | 43.5 – 57.6 V | ~41.7 A | 2000 W |
| 3000 W | 43.5 – 57.6 V | ~62.5 A | 3000 W |
I_MAX is derived as power_rating / 48.0 V (nominal). Current is
automatically limited if V × I would exceed the configured power.
Note: setting power_rating only changes the software limit check – it
does not change what your physical unit is actually rated for. Make sure
the value matches the label on your PSU; see DISCLAIMER.md.
| Signal | Action |
|---|---|
SIGTERM |
Graceful shutdown |
SIGHUP |
Reload log level from config (uses --config path from startup) |
flatpack2.py Main program
flatpack2.conf Configuration – PSU controller only
flatpack2_charger.conf Configuration – PSU controller + LiFePO4 charger
flatpack2.service systemd service unit
requirements.txt Python dependencies
README.md This file
DISCLAIMER.md Legal disclaimer (no warranty, no liability, experimental software)
- Single PSU only. The internal ID/serial mapping infrastructure is
prepared for multiple rectifiers, but
_handle_status()currently attributes all STATUS frames to PSU 1 and login uses a fixed arbitration ID for every unit. With more than one Flatpack2 on the bus, readings from different units would be mixed into a single record. Proper multi-PSU support requires per-unit login IDs and STATUS dispatching, which cannot be implemented safely without hardware testing with multiple rectifiers. - Assumption:
voutin the STATUS frame reflects the actual output terminal voltage even when the PSU is passive (battery backfeed). This matches observed behaviour but has not been exhaustively verified. power_ratingvariants 1800 W / 3000 W are validated logically (limit math only); only the 2000 W variant has been confirmed on real hardware so far.- The Web-GUI has no authentication and uses the Flask development
server. Keep it on a trusted LAN (
host = 0.0.0.0) or bind to127.0.0.1and use an SSH tunnel / reverse proxy if exposure is a concern.
Hardware confirmed working (real Flatpack2 48V/2000W HE + Waveshare USB-CAN-A): CAN frame format, LOGIN keepalive, broadcast SET, STATUS parsing, serial-number mapping, startup apply, value restore after CAN reconnect, and the full DETECT → RAMP → CC → CV charger flow.
For per-version changes see CHANGELOG.md.