Skip to main content

jkctl

A cross-platform tool for JK BMS battery management systems on an RS485 bus, with a browser front end and a command for everything it does. It finds the units on the bus, reports their nameplate and live pack data, reads and writes the whole configuration table, drives the one-key presets and the other action registers, sets the port protocols and dry contacts, and validates and flashes a .jkbms firmware file the way JK's own Windows application does.

It is a web interface first. jkctl, with nothing after it, serves a page on this machine and opens a browser on it — that is the default mode of operation and how most people will use it. Everything the page does is also a command you can type, for a script or a terminal.

Everything here was derived from JK BMS Monitor 3.11.0, the encrypted protocol datasource shipped with it, and JK's own RS485 Modbus register-map documents. The reverse-engineering record is in research/windows/windows-findings.md; docs/how-it-works.md is the readable summary.

Install

uv tool install jkctl          # or: pipx install jkctl

Python 3.10 or newer. The only third-party runtime dependency is pyserial; it also uses devicectl-core, this project's shared, pure-standard-library core. Firmware decryption needs no crypto package at all (see AES backends below).

First run

jkctl                          # the web interface, in a browser

With no arguments jkctl is jkctl ui: it serves a page on this machine and opens a browser on it. There is nothing to configure first: with no port chosen it opens on a picker listing the serial ports this machine has, scans all sixteen addresses of whichever you choose, and shows you the bank — every board on the bus, one tile each. Options for it can be given straight after the bare name (jkctl --port /dev/ttyUSB1, jkctl --listen 0.0.0.0:8087 --read-only). See docs/web-ui.md.

The jkctl dashboard: pack state and charge, per-cell voltages against their protection band, board identity, temperatures, the running history, the three main switches, raised protections and the health check

To see the whole interface without a battery anywhere near it:

jkctl ui --simulate            # a bank of fake boards, no serial port opened

Everything it does is also a command:

jkctl scan                     # what is on /dev/ttyUSB0?
jkctl info --id 1              # model, versions, serial number
jkctl status --id 1            # voltage, current, SOC, temperatures, alarms
jkctl cells --id 1             # per-cell voltages and wire resistances
jkctl status --id all          # the whole bank at once

If nothing answers, jkctl probe sweeps the baud rates and addresses and writes a report you can read or send on. It never writes a register.

Commands

command what it does
jkctl ui what jkctl alone does: serve the web interface
jkctl scan sweep addresses 0-15 for units
jkctl info model, hardware and software versions, serial, run time, port protocols
jkctl status pack voltage, current, power, SOC, SOH, temperatures, raised alarms
jkctl cells per-cell voltages, wire resistances, spread, what the balancer is doing
jkctl alarms every protection and warning bit raised, named
jkctl registers every register of every table, with its value
jkctl history the board's own stored fault records, where it maps them
jkctl log-codes what each stored record's code means (no hardware)
jkctl log sample the runtime data into CSV or JSON
jkctl settings show|get|set|list|export|import the configuration table
jkctl charge|discharge|balance [on|off] the three main switches
jkctl switches show|set the sixteen multiplexed on/off settings
jkctl preset lifepo4|li-ion|lto a one-key chemistry preset
jkctl emergency / jkctl shutdown the emergency start; the power-off
jkctl restart / jkctl factory-restore / jkctl erase-data three board actions JK's document does not list
jkctl address show|set the address the board answers on
jkctl time show|sync the unit's real-time clock
jkctl calibrate voltage|current recalibrate a measurement
jkctl protocols list|show|set which protocol each UART and the CAN port speaks
jkctl dry-contact show|set the two dry contacts and the LCD buzzer
jkctl firmware info|check|list|flash a .jkbms file or a directory of them: read, check, flash
jkctl probe characterise a bus that is not answering
jkctl simulate serve a fake BMS on a serial port, for testing
jkctl doctor read a unit over and report what looks wrong
jkctl config show|path|init the optional settings file

--json is available on the commands that report; --watch [S] redraws status and cells until Ctrl+C. Every command takes --port, --id, --baud, --timeout, --retries and --trace. The read commands also take --id 1,2,5, --id 1-4 or --id all, and print one section per board (or one JSON document keyed by address).

$ jkctl status --id 1
BMS 1:

  Pack
    State of charge  95 %
    Voltage          53.27 V
    Current          -2.65 A
    Remaining        296.8 Ah

  Cells
    Average          3.329 V
    Spread           0.003 V

  Temperatures
    MOS              23.5 ℃
    Battery 1        21.8 ℃

The groups and the names are the web dashboard's cards and rows, so a reading is called the same in both.

Changing settings

Settings are typed, scaled and range-checked against JK's own datasource before anything goes out, and a write always shows what it will change:

$ jkctl settings set volCellUV=2.9 balanEn=off --id 1
2 setting(s) to change:

  Cell UVP (volCellUV): 2.800 V -> 2.900 V
  Balancing (balanEn): On -> Off

Wrote 2 setting(s).

--dry-run prints the plan and the exact Modbus frames without sending them. jkctl settings list shows every setting with its unit, its allowed range and its factory default. export/import move a whole configuration between units; an import that would change nothing says so instead of rewriting sixty registers.

Configuration file

jkctl config init writes a commented jk.toml under $XDG_CONFIG_HOME/jk (or ~/.config/jk). Everything in it is optional and a flag always wins:

port = "/dev/ttyUSB0"
id = 1

[devices.shed]
id = 2

Then jkctl status --device shed.

Safety

  • scan, info, status, cells, alarms, log, doctor, probe and every show/get/list are strictly read-only. They never write a register.
  • settings set, the three switches and protocols set write, and say what they wrote. Range checks come from JK's own datasource, so a value the vendor's application would clamp is refused here instead.
  • preset, shutdown, calibrate, address set, restart, factory-restore and erase-data ask first, naming what they will do, because they replace every protection setpoint, turn the board off, move the address you are talking to, or erase what it has stored. -y skips the question; the browser asks for the word typed out instead.
  • The last three of those use action slots that appear in neither revision of JK's register-map document. They were read out of the vendor application's own buttons, a reading whose control is that it gives 0x04 for "Shutdown Board", which the document does list at 0x04. None has been fired at real hardware.
  • Flashing is irreversible and an interrupted transfer can leave a unit with no working application. The compatibility gate enforces model and major version; --force bypasses only the minor-version and expiry checks. Do not flash a battery you cannot afford to lose without a verified, compatible image.
  • Recovering a dead app is a bench job, and how bad depends on readout protection. Re-arming the upgrade over RS485 is done by the application, so a dead app cannot be reflashed over the wire. The fallback is the STM32 ROM bootloader (BOOT0 high, a USB-TTL adapter, stm32flash): if the MCU's readout protection (RDP) is off it can rewrite the application region from the image jkctl extracts, leaving the JK bootloader untouched. If RDP is on, clearing it mass-erases the JK bootloader too, and recovery then needs a full image -- including the 0x08000000–0x08003FFF bootloader that ships in no .jkbms. See research/windows/docs/bootloader-dump-procedure.md.
  • The image sent is the vendor's own, to the byte; the BMS is not known to check it. jkctl decodes a .jkbms exactly as JK's application does, and the bytes it extracts match an independently pulled vendor image to the SHA-256 -- so the risk is not that it sends the wrong bytes. But a .jkbms carries no signature and no whole-image checksum, JK's application validates only the metadata (model, version, expiry) and never the image itself, and on the wire the only guard is XMODEM's weak 8-bit per-block checksum. Whether the bootloader verifies the image before running it is unknown: it ships in no firmware file and could not be read. Assume a wrong or corrupt image can be written and run.
  • The web interface is bound to 127.0.0.1 and needs no token. Off loopback it generates one, refuses a Host header that is a name it was not told to expect, and requires a header on every write that a cross-origin form cannot set. --read-only refuses every write server-side, so a page can be shared without handing over the battery.
  • No write path has yet been exercised against real hardware -- from the browser or from the terminal. See docs/reference.md for the full list of what has and has not been proven.

AES backends

Firmware and datasource files are AES-256-CBC. jkctl needs no crypto package for that: it uses the first available of PyCryptodome, cryptography, the system OpenSSL libcrypto (through stdlib ctypes -- nothing to install), or a bundled pure-Python AES. On a small ARM host, simply having OpenSSL present gives an instant decrypt with no compiler involved.

Development

uv sync                                 # pytest, ruff, ty -- pure Python
uv run pytest
uv run ruff format . && uv run ruff check . && uv run ty check
biome ci src/jkctl/web/static/          # the browser half
uv sync --group browser                 # the V8 and the browser, opt-in

The engine-backed checks are a separate dependency group so that installing and hacking on jkctl stays light on the small hosts it targets.

The tests need no hardware: they wire the Modbus master straight to jkctl.simulator, which reproduces the real device's quirks, and the web tests point the server at the same thing. See CONTRIBUTING.md.

Licence

EUPL-1.2, except the files named in NOTICE: three of JK's own data files, and the vendored Preact/htm bundle the page is built with.

Release files for jkctl 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jkctl 0.1.0
File Size Uploaded
jkctl-0.1.0.tar.gz 930.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jkctl 0.1.0
File Interpreter ABI Platform
jkctl-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.2 MB

Release files / jkctl-0.1.0.tar.gz

Download URL jkctl-0.1.0.tar.gz
Size 930.3 kB
Tags Source
SHA-256 checksum
How to use checksums
1008bf57cbaf925117b61a00df432371737000f3532a72c831dca8e8c62e4744
BLAKE2b-256 checksum
How to use checksums
1ca7b6d339cedc7da8364d8803e7316946575741300a48db583bccc642f805d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / jkctl-0.1.0-py3-none-any.whl

Download URL jkctl-0.1.0-py3-none-any.whl
Size 253.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
376e09f20e8481edd6eb7a355e59ea711caedad898915b985fb88e144b202cd8
BLAKE2b-256 checksum
How to use checksums
c6c136c94033a831842870b3c8b92eb1c3b39751141bec73fc3c96e40d6a67ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page