Open programming software for the TYT TH-2000 amateur radio, also sold as the UV-99: a browser-based programmer that needs nothing installed, a CHIRP driver, a Python library, and the documentation to rebuild any of it.
Built by reverse engineering the manufacturer's own software, then checking every finding against a real radio.
Try TH-2000 CPS in your browser: https://dobrishinov.github.io/TYT-TH-2000/
The stock software is Windows-only, undocumented, and easy to get wrong. This
replaces it with tools that run anywhere, explain what they are doing, and can
be checked. The .icf files are interchangeable with the manufacturer's
software, so you can move between them freely.
Three wires: D+, D− and ground. The radio is a USB device in its own right, so there is no serial adapter and no chip involved — just wire.
Leave pin 7 unconnected. The radio supplies 5 V there, and it sits next to the D+ data line. Step-by-step instructions, the measured voltages and how to test the cable are in docs/CABLE.md.
Open the hosted page above in Chrome or Edge — Web Serial is needed and Firefox does not have it. TH-2000 CPS in your browser: https://dobrishinov.github.io/TYT-TH-2000/
Or run it yourself:
cd site && python3 -m http.server 8000then visit http://localhost:8000. Serving matters: Web Serial needs a
secure context, and a file:// page is not one.
Try it with no radio first — Open .icf and pick
reference/DefaulFile.icf.
Help → Developer Mode, restart, then File → Load Module… and choose
chirp/tyt_th2000.py. The radio appears as TYT → TH-2000.
site/ |
the built single-file tools — this is what gets published |
web/ |
their source, and the test suites |
chirp/ |
CHIRP driver, .icf↔.img converter, protocol loopback test |
docs/ |
cable, specification, derivation, mapping guide, developer notes |
channels/ |
a ready-made Bulgarian repeater list |
reference/ |
the manufacturer's files, extracted |
th2000.py |
standalone Python library, no dependencies |
noop_roundtrip.py |
the cross-implementation invariant check |
Two tools ship as single self-contained HTML files:
site/index.html— the programmersite/compare.html— a byte-level notebook for mapping what is still unknown
Channels — 200 of them, edited inline or in a side panel. Copy, cut, paste and delete from the keyboard. Bulk operations across a range or on one channel. CSV import and export, including CHIRP's own CSV.
VFOs — both of them, one per band, in the same A/B layout the radio uses.
Radio settings — 29 fields: squelch, time-out timer, auto power off, backlight, LED mode, display mode, scan type, key mode and lock, beep, dual watch, monitor, squelch tail, both intro screen lines, the boot-strap password, and all ten programmable key assignments.
FM broadcast — 32 presets and the VFO.
Safety — reading saves a timestamped backup automatically. Before writing, it reads the radio back and shows exactly what would change, in the terms you edit in. After writing, it verifies byte for byte. Nothing outside a named field is ever written.
site/ holds the two built files and nothing else, so it can be published as
it stands.
With the included workflow — .github/workflows/pages.yml
publishes site/ on every push to main. Switch it on under
Settings → Pages → Source → GitHub Actions, push, and the address appears
there. Then replace the placeholder at the top of this file.
Without Actions — GitHub can serve a branch folder, but only the repository
root or /docs. Since docs/ here is documentation, rename site/ to docs/
and move the documentation elsewhere if you prefer that route.
Rebuild site/ after changing anything under web/:
cd web && python3 - <<'EOF'
core = open('th2000-core.js').read()
tag = '<script src="th2000-core.js"></script>'
for src, dst in (('index.html', '../site/index.html'),
('compare.html', '../site/compare.html')):
open(dst, 'w').write(open(src).read().replace(tag, '<script>\n' + core + '\n</script>', 1))
EOFThis project is provided without any guarantee or warranty. Any use of the programmer, CHIRP driver, library, or write functionality is performed entirely at your own risk and responsibility.
The user is responsible for making backups and for verifying their hardware and configuration before writing to a radio. If something goes wrong or the radio's configuration becomes corrupted, there is no guarantee that it can be recovered, and the project authors cannot be held responsible for any damage, loss of configuration, or other consequences resulting from its use.
Read it first and save that file.
The write path is the one part of this that has never run against hardware — everything else has. The protocol was recovered by reading the manufacturer's software rather than by capturing a session. It agrees with the simulators, and three independent implementations agree with each other byte for byte, but that is not the same as proven.
docs/SPECIFICATION.md §10 is an honest ledger of what is confirmed and what is not.
A codeplug write cannot reach the radio's firmware or its calibration — no command in the protocol can. The worst realistic outcome is a scrambled configuration you fix by writing your backup back.
cd web && node th2000-core.test.js # 193 checks, no dependencies
python3 noop_roundtrip.py # every implementation agreesWith jsdom installed, node ui.test.js (475) and node compare.test.js (43)
drive the real pages in a headless DOM. The CHIRP driver passes CHIRP's own
driver suite: 35 passed, 7 skipped.
noop_roundtrip.py is the one that matters. It reads a codeplug, parses every
channel, writes each straight back unchanged, and requires zero bytes to
differ across the Python library, the JavaScript core and the CHIRP driver. Any
bit a field model fails to carry shows up there.
Apache License 2.0 — see LICENSE. Two exceptions, both in NOTICE:
chirp/tyt_th2000.py is GPL-2.0-or-later. It imports from and subclasses
CHIRP, which is GPL, so the driver is a derivative work and has to carry the
same terms. It cannot be Apache-licensed. This is normal for a CHIRP driver and
causes no difficulty in practice: it is a standalone module CHIRP loads, kept
apart from the rest of this repository.
(Apache 2.0 and GPLv2 are incompatible. Because the driver is "v2 or later", anyone combining it with the Apache-licensed parts can do so under GPLv3, which Apache 2.0 is compatible with.)
reference/ is the manufacturer's material, extracted from their
installer. It belongs to TYT and is here for interoperability work. It is not
covered by the Apache licence and must not be shipped with any release you
make.
The channel record layout was cross-checked against
chirp/drivers/th_uv88.py, which supports a
sibling radio and independently confirms most of it. Three fields that driver
marks unknown are named here; one bit-pair remains disputed between them. Both
are documented in the specification.
