SSH / serial / SFTP toolkit for automotive & embedded work — a Python library, a CLI, and a PyQt5 GUI with a real VT100 terminal. Native serial-over-RDP and offline OpenSSH install included.
Project description
TurboSSH
SSH, serial, and SFTP for people who work on embedded and automotive boxes all
day. One toolkit, three ways to drive it: a Python library for your test
framework, a command-line tool for scripts, and a desktop GUI with a
real VT100 terminal. The GUI ships as a self-contained Windows .exe with PyQt5
baked in, so you don't have to fight a PyQt install to use it.
It came out of a specific headache: getting a shell on a QNX target that's only reachable through a Windows RDP jump box, reading a debug board hanging off a COM port on that same box, and pulling logs off the target — without juggling three tools to do it.
pip install turbossh # library + CLI + the GUI launcher
turbossh-gui # launch the GUI
Every feature below shows you all three ways to use it: in a script, in the GUI, and on the command line.
Contents
- Install
- The three interfaces, in 30 seconds
- Connecting · Running commands · Live logs · Files (SFTP / SCP)
- Serial ports · Serial over RDP · Scanning remote ports · Camera
- Port forwarding · Installing an SSH server · Credentials
- The GUI tour · CLI reference · Results & errors
- Architecture · Changelog
Install
pip install turbossh
That gives you the turbossh library, the turbossh CLI, and turbossh-gui.
On Windows the GUI runs from a bundled executable, so PyQt5 doesn't need to
install. To run the GUI from source on a platform with PyQt5 wheels instead:
pip install "turbossh[gui]"
Python 3.8 or newer. Paramiko, scp, pyserial, keyring, pywinrm and pyte come along automatically.
The three interfaces, in 30 seconds
The library is one object, SSHHandler. By default it raises typed exceptions
(what you want in a test); pass safe=True and every call returns a result
object instead (what the GUI uses so a dropped socket logs a line instead of
crashing the window).
from turbossh import SSHHandler, SSHConfig
with SSHHandler(SSHConfig(host="192.168.1.50", username="root", password="…")) as ssh:
print(ssh.run("uname -a").text)
The CLI wraps the same calls behind turbossh <command> --host … --user …. The
GUI wraps them behind tabs and buttons. Nothing is GUI-only.
Connecting
The starting point for everything. Direct, through a jump host, or to old gear that needs deprecated crypto.
In a script. A connection is an SSHConfig. Nest one in jump_host to hop
through an RDP/Windows box first.
from turbossh import SSHHandler, SSHConfig
# direct
cfg = SSHConfig(host="10.0.0.5", username="root", password="…")
# through a jump host: laptop -> RDP box -> target
cfg = SSHConfig(
host="adelegg-mopf", username="root",
jump_host=SSHConfig(host="10.232.9.120", username="EU\\nkennedy", password="…"),
)
# an old ECU that only speaks legacy ciphers/kex
cfg = SSHConfig(host="10.0.0.9", username="root", enable_legacy_algorithms=True)
# lab gear that gets re-imaged constantly: skip host-key checking
cfg = SSHConfig(host="10.0.0.9", username="root", host_key_policy="ignore")
with SSHHandler(cfg) as ssh:
...
In the GUI.
- Click New session in the left sidebar (or right-click the sidebar → New session…).
- Pick SSH as the connection type.
- Fill in Host, User, Password. Tick Connect through a jump host if you need one (its fields pre-fill from Settings).
- Tick Enable legacy algorithms for old ECUs, or leave Ignore host key on for lab boxes.
- Click OK — it connects and opens a terminal tab.
The jump host you use most often goes in Settings → Jump host once, and every session reuses it.
On the command line. The connection flags are shared by every command:
turbossh run --host 10.0.0.5 --user root --password uname -a
# --domain CORP for CORP\user logins
# --key id_rsa key-based auth
# --use-stored pull the password from the OS vault (see Credentials)
Running commands
Get an exit code and output back as a structured object — no parsing exit codes out of stdout.
In a script.
res = ssh.run("systemctl is-active sshd")
res.ok # True if exit code == 0
res.text # stdout, stripped
res.stderr # stderr
res.exit_code # the actual number
res.duration # seconds it took
ssh.run("reboot", check=True) # raise SSHCommandError on a non-zero exit
ssh.sudo("mount -o remount,rw /") # sudo, password fed on stdin
ssh.run_many(["sync", "reboot"]) # several commands over one channel
In the GUI. Open the session's Terminal tab and just type — it's a real terminal, so arrow keys, Tab-completion and Ctrl-C all work. The Quick buttons (top of the tab) fire common commands in one click; Ctrl-C sends an interrupt; Clear wipes the screen.
On the command line.
turbossh run --host H --user U uname -a
turbossh run --host H --user U --json systemctl status sshd # machine-readable
The CLI exits with the remote command's exit code, so it drops into shell scripts cleanly.
Live logs
Follow a command that never ends — slog2info -w, journalctl -f, dmesg -w,
tail -f — and react to it.
In a script. iter_lines() yields output as it arrives; stream() adds
regex matching and a tee-to-file.
# print every line, stop the instant one matches
ssh.stream("slog2info -w", on_line=print, match=r"E/.*panic", stop_on_match=True)
# follow a log into a local file while watching for a string
result = ssh.stream("journalctl -f", save_to="boot.log", match="Started Target")
print(result["matched"], result["matches"])
In the GUI. Each SSH session has a Logs tab:
- Pick a preset from the dropdown (
slog2info,journalctl -f, …) or type your own command. - Add a regex filter if you only care about some lines.
- Click Start. Use Pause, Clear, and Save… as needed.
The log runs on a pseudo-terminal with the login PATH, so QNX tools like
slog2info actually stream instead of buffering or coming back "not found".
On the command line.
turbossh stream --host H --user U slog2info -w
turbossh stream --host H --user U --match "panic" --save boot.log journalctl -f
Ctrl-C stops it.
Files (SFTP / SCP)
A full remote filesystem, not just put and get.
In a script.
ssh.push("dist/", "/opt/app", recursive=True) # upload
ssh.pull("/var/log/messages", "logs/") # download
ssh.listdir("/etc")
ssh.exists("/tmp/lock")
ssh.read_text("/proc/version")
ssh.write_text("/tmp/flag", "1")
ssh.makedirs("/opt/app/cache")
ssh.remove("/tmp/old")
for root, dirs, files in ssh.walk("/etc/network"):
...
SCP is there too — ssh.scp_push(...) / ssh.scp_pull(...) — for servers where
it's faster or SFTP is disabled.
In the GUI. Every SSH session has a SFTP tab: a two-pane browser. Double- click folders to navigate, use the buttons to upload, download, make a directory, rename, or delete. Transfers run on their own channel, so a big copy never freezes the terminal.
On the command line.
turbossh push --host H --user U ./build /opt/app --recursive
turbossh pull --host H --user U /var/log ./logs --recursive
Serial ports
A serial port on the machine you're sitting at.
In a script.
from turbossh import SerialHandler, list_serial_ports
print(list_serial_ports()) # what's plugged in
with SerialHandler("COM5", baudrate=115200) as ser:
ser.write("version\n")
ser.stream(on_line=print, match="login:") # follow it
In the GUI. New session → Serial → pick the Device and Baud → OK. The console is a native terminal: type straight into it.
On the command line.
turbossh list-serial # list local ports
turbossh serial-monitor --port COM5 --baud 115200 --match "login:"
Serial over RDP
The automotive bread-and-butter: the debug board is plugged into the Windows RDP box, not your laptop. TurboSSH runs the serial bridge on that box and pipes it back over SSH, as a native terminal.
In a script. Read the remote port with serial_stream (auto-detects COM vs
/dev), write to it with serial_write.
# stream a COM port on the RDP machine, save the console to a file
ssh.serial_stream("COM4", baudrate=115200, on_line=print, save_to="console.log")
# send a line to it
ssh.serial_write("COM4", "reboot\n", baudrate=115200)
For a fully interactive, character-by-character session there's serial_bridge()
(it returns a raw channel you read/write), with serial_in_use() and
serial_release() around it — that's what the GUI drives.
In the GUI.
- New session → Serial.
- Tick Port is on the RDP machine (connect to it remotely). The section above relabels to the RDP machine — fill in its IP, your Windows login, and password.
- Click Scan remote to list that machine's COM ports, and pick yours.
- Set the Baud and click OK.
- You get a native terminal — type into it directly, with Tab-completion and Ctrl-C. If the port's already in use it asks before taking it, and it releases the port cleanly when you close the tab or the app.
On the command line.
turbossh serial-ssh --host 10.232.9.120 --user "EU\\nkennedy" \
--device COM4 --baud 115200 --save console.log
# --send "reboot" write a line first
# --match "login:" flag matching lines
Scanning remote ports
Ask the box you're about to connect to which COM ports it actually has.
In a script.
for p in ssh.remote_serial_ports():
print(p["device"], "-", p["description"])
# COM4 - Silicon Labs CP210x USB to UART Bridge (COM4)
In the GUI. It's the Scan remote button in the serial session dialog — it fills the device dropdown for you.
On the command line.
turbossh scan-ports --host H --user U
Camera
Watch any camera — on your own machine or on a remote machine — live in TurboSSH. It's opt-in: turn it on in Settings → Enable camera and a 📷 Camera button appears in the ribbon (it stays hidden otherwise).
In the GUI. The Camera button opens a panel with the view front-and-centre:
- Pick a Source — Local (this PC) (the default, no connecting) or Remote (RDP / Windows machine), where you enter the machine's host/login (pre-filled from Settings → Jump host).
- Pick a Camera, a resolution and FPS, then Start.
- Use Snapshot, Record, Pause; saved files show an open folder link. Files save on your laptop.
Local needs nothing but ffmpeg. For a remote source, it connects over that saved machine's own SSH connection on its own threads (so it never slows the terminal/serial work), runs ffmpeg there, and streams MJPEG back; closing the tab releases the remote camera. If a remote camera is busy it offers to take it.
ffmpeg isn't bundled in the pip wheel (it would blow past PyPI's size limit), so
the first time you use the camera it's fetched once from a public GitHub build,
cached, and (for a remote source) pushed to that machine. For a fully offline
setup, point Settings → ffmpeg path at a local ffmpeg.exe.
In a script (the remote path):
for cam in ssh.list_cameras(ffmpeg=r"C:\ffmpeg\ffmpeg.exe"):
print(cam)
chan = ssh.webcam_channel("Integrated Camera", ffmpeg=r"C:\ffmpeg\ffmpeg.exe",
width=640, height=480, fps=15)
# chan.recv() yields MJPEG (concatenated JPEG frames); chan.close() to stop
ssh.webcam_release()
(There's no CLI subcommand for the camera — it's a GUI/library feature.)
Port forwarding
ssh -L and ssh -R tunnels through the same connection or jump host.
In a script.
fwd = ssh.forward_local("10.0.0.9", 80, local_port=8080) # -L
print(fwd.local_port) # browse localhost:8080
# ... use it ...
fwd.close()
ssh.forward_remote(9000, "127.0.0.1", 3000) # -R
On the command line. Runs until you Ctrl-C:
turbossh forward --host H --user U --local-port 8080 --to-host 10.0.0.9 --to-port 80
(The GUI doesn't expose forwarding yet — use the library or CLI.)
Installing an SSH server
The catch with the RDP-jump-box workflow is that the box often doesn't have SSH
yet, and corporate networks block the Windows-Update path that would install it.
So TurboSSH bundles the OpenSSH binaries (ARM64 / x64 / x86) and installs them
with zero downloads. It's a one-time install — sshd is registered as an
automatic service, so it's back on every reboot and listening at the login
screen. You don't re-run it.
On the machine itself.
turbossh-setup # self-elevates, installs + starts sshd,
# opens the firewall, fixes host keys
turbossh-setup --install-pip --port 22 # also pip-install turbossh; custom port
Or in the GUI: the SSH server button → This PC. A window shows progress,
and the GUI tells you when sshd is listening.
On a remote machine, from your laptop (needs WinRM reachable on the target and a local-admin account on it) — pushes the bundled OpenSSH over WinRM and installs it there:
turbossh install-ssh-remote --host 10.232.9.120 --user "EU\\nkennedy"
Or in the GUI: the SSH server button → A remote machine (WinRM).
In a script (the remote, WinRM path):
from turbossh import enable_openssh_via_winrm_offline
enable_openssh_via_winrm_offline(
"10.232.9.120", "EU\\nkennedy", "password",
openssh_dir="…/turbossh/openssh", log=print)
Credentials
Passwords belong in the OS vault, not in a script.
In a script.
from turbossh import CredentialStore, SSHHandler, SSHConfig
CredentialStore("my_lab").set("EU\\nkennedy", "the-password") # store once
pw = CredentialStore("my_lab").get("EU\\nkennedy") # later
ssh = SSHHandler(SSHConfig(host="…", username="nkennedy", password=pw))
Passwords are wrapped in a Secret that masks itself in logs and reprs, so they
don't bleed into your test output.
In the GUI. Saved-session passwords go straight into the OS keyring, never plaintext on disk. Show/hide them with the eye icon in the dialog.
On the command line.
turbossh store-credential --user nkennedy --domain EU --service my_lab
turbossh run --host H --user nkennedy --domain EU --use-stored --service my_lab uname -a
The GUI tour
turbossh-gui
- Sidebar of saved sessions with a quick-connect filter. Right-click for new / open / edit / duplicate / delete.
- Tabbed sessions, each with Terminal, SFTP, and Logs tabs.
- Real VT100 terminal — htop, vim, less render correctly.
- 10,000 lines of scrollback (configurable in Settings), and Save all output, which writes the entire session to disk, not just what's on screen — it scales to millions of lines.
- Split view tiles several sessions at once.
- Menu bar (File / Edit / View / Session / Help) and a ribbon with the same actions plus SSH server, Check updates, and Settings.
- Check updates pulls the latest from PyPI and reinstalls in place, then offers to refresh the bundled OpenSSH on the machines you use.
- Dark and light themes; a desktop and Start-menu shortcut on first run.
CLI reference
Connection flags (--host --user [--domain] [--key] [--password|--use-stored])
are shared.
turbossh run --host H --user U uname -a
turbossh stream --host H --user U --match "panic" slog2info -w
turbossh push --host H --user U ./build /opt/app --recursive
turbossh pull --host H --user U /var/log ./logs --recursive
turbossh info --host H --user U --json
turbossh serial-ssh --host H --user U --device COM4 --baud 115200 --save log.txt
turbossh scan-ports --host H --user U
turbossh forward --host H --user U --local-port 8080 --to-host 10.0.0.9 --to-port 80
turbossh list-serial
turbossh serial-monitor --port COM5 --baud 115200
turbossh install-ssh-remote --host H --user "DOMAIN\\admin"
turbossh store-credential --user U --domain CORP --service my_lab
turbossh-gui # the GUI
turbossh-setup # install OpenSSH Server on THIS machine (offline)
turbossh-shortcut # (re)create the desktop / Start-menu shortcut
turbossh-docs # open the documentation
Results & errors
CommandResult—.exit_code,.stdout,.stderr,.text,.duration,.okTransferResult—.size_bytes,.duration,.human_speed,.filesOperationResult— the safe-mode wrapper:bool(res),.value,.error,.unwrap()
Raise mode (the default) gives you typed exceptions to catch precisely —
SSHConnectionError, SSHAuthenticationError, SSHTimeoutError,
SSHCommandError, SSHTransferError, SerialError, WinRMError,
CredentialError, all under SSHError. Safe mode (SSHHandler(cfg, safe=True))
turns every call into an OperationResult instead.
A real-world example
A hardware-in-the-loop check that reboots a target through the RDP box's serial port and waits for it to come back:
from turbossh import SSHHandler, SSHConfig
def test_target_boots_clean(rdp_box, target):
jump = SSHConfig(host=rdp_box.ip, username=rdp_box.user, password=rdp_box.pw)
cfg = SSHConfig(host=target.ip, username="root", jump_host=jump)
with SSHHandler(cfg, safe=True) as ssh:
ssh.serial_write("COM4", "reboot\n")
res = ssh.serial_stream("COM4", match=r"login:", stop_on_match=True,
timeout=120, save_to="boot.log")
assert res["matched"], "target never reached the login prompt"
assert ssh.run("slog2info | grep -c FATAL").text.strip() == "0"
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file turbossh-1.2.23-py3-none-any.whl.
File metadata
- Download URL: turbossh-1.2.23-py3-none-any.whl
- Upload date:
- Size: 90.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d86b2e14cc17dba731a7e794e68d1acd39a067743ddcd02f59ee9c089af2a28
|
|
| MD5 |
3440951db3aae3832cc58868f500e0f0
|
|
| BLAKE2b-256 |
fd220491abd1aa2dd2b6f3fc25f8a84c8e1742aec8116d25bd1ab9c2197d4f97
|