Skip to main content

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

PyPI Python License: MIT

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

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.

  1. Click New session in the left sidebar (or right-click the sidebar → New session…).
  2. Pick SSH as the connection type.
  3. Fill in Host, User, Password. Tick Connect through a jump host if you need one (its fields pre-fill from Settings).
  4. Tick Enable legacy algorithms for old ECUs, or leave Ignore host key on for lab boxes.
  5. 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:

  1. Pick a preset from the dropdown (slog2info, journalctl -f, …) or type your own command.
  2. Add a regex filter if you only care about some lines.
  3. 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 BaudOK. 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.

  1. New session → Serial.
  2. 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.
  3. Click Scan remote to list that machine's COM ports, and pick yours.
  4. Set the Baud and click OK.
  5. 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:

  1. Pick a SourceLocal (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).
  2. Pick a Camera, a resolution and FPS, then Start.
  3. 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, .ok
  • TransferResult.size_bytes, .duration, .human_speed, .files
  • OperationResult — 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

turbossh-1.2.14-py3-none-any.whl (90.5 MB view details)

Uploaded Python 3

File details

Details for the file turbossh-1.2.14-py3-none-any.whl.

File metadata

  • Download URL: turbossh-1.2.14-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

Hashes for turbossh-1.2.14-py3-none-any.whl
Algorithm Hash digest
SHA256 9b849321c64cf42c88eecebddd33a732098a7d0ac9662b2ac9d6f7528b54e3aa
MD5 6ec5027d3354ebc26d4d281bc1f0b642
BLAKE2b-256 037c998a26a0964e53848cea1c9d7f653f56dcb2cc63e0f20a7d4a91a61e3d01

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page