B4J Library An SSH library courtesy of MS Copilot - B4JSSH - now the Ferrari version

PREAMBLE

For some time now I have had a B4J app that manages a number of Netonix (any) and Ubiquiti (USW-FLEX) WISP switches that turn on/off a fleet of high end Hikvision cameras in a remote location.

This app has used the SSHJ library which has worked flawlessly.

I have decided to replace the Netonix switches with Planet (WGS-5225-8UP2SV) switches.

Unfortunately I discovered that SSHJ can't communicate with Planet switches, Copilot's explanation follows:

===========================================
Planet Switch SSH Compatibility — Technical Limitation Summary
Overview

Planet‑brand Ethernet switches require an interactive, PTY‑allocated SSH shell for authentication and CLI access. Their SSH server does not support non‑interactive exec channels for login or command execution.
Root Cause
The stock B4J SSHJ library (as distributed for B4J) only exposes non‑interactive exec‑style channels and does not provide a mechanism to open a PTY‑based interactive shell.
Planet switches rely on PTY allocation to present their username/password prompts and to maintain an authenticated session.
Failure Mode
When connecting without a PTY:
  • Planet still emits a Username: prompt
  • The switch rejects all submitted credentials
  • The login state machine resets
  • The session loops indefinitely with repeated Username: prompts
  • No CLI prompt (> or #) is ever reached
This behaviour is consistent across all Planet models that use the same SSH daemon.
Conclusion
Planet switches cannot be automated using the stock B4J SSHJ library because the library lacks PTY‑shell support, which Planet requires for interactive authentication and command execution.

===========================================

After a fruitless search for an existing solution I decided to have a crack at developing an SSH library with the assistance of Copilot.

This was a two-fold exercise - see what I could really do with Copilot and (if lucky) find a solution.

I FOUND IT!!!!!

EDIT - Check out the
Ferrari Version 1.1
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
CODE

I won't go thru the somewhat torturous conversations I had with Copilot but the net result was a folder in

N:\B4A\SimpleLibraryCompiler\B4JSSH with sub folders:

src
....B4JSSH.java
libs
....jsch-0.1.55.jar​

B4JSSH.java was entirely supplied by Copilot:
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;

import com.jcraft.jsch.*;

import java.io.InputStream;
import java.io.OutputStream;

@ShortName("B4JSSH")
@Version(1.00f)
public class B4JSSH {

    private JSch jsch;
    private Session session;
    private ChannelShell shell;
    private InputStream in;
    private OutputStream out;

    public void Initialize() {
        jsch = new JSch();
    }

    public void Connect(String host, int port, String user, String pass, int timeout) throws Exception {
        session = jsch.getSession(user, host, port);
        session.setPassword(pass);
        session.setConfig("StrictHostKeyChecking", "no");
        session.connect(timeout);
    }

    public void OpenShell() throws Exception {
        shell = (ChannelShell) session.openChannel("shell");
        shell.setPty(true);
        in = shell.getInputStream();
        out = shell.getOutputStream();
        shell.connect();
    }

    public void Write(String cmd) throws Exception {
        out.write((cmd + "\n").getBytes());
        out.flush();
    }

    public String Read() throws Exception {
        StringBuilder sb = new StringBuilder();
        while (in.available() > 0) {
            sb.append((char) in.read());
            Thread.sleep(10);
        }
        return sb.toString();
    }

    public void Disconnect() {
        try { if (shell != null) shell.disconnect(); } catch (Exception ignored) {}
        try { if (session != null) session.disconnect(); } catch (Exception ignored) {}
    }
}
The only real hassles with this was that initially the statement

@ShortName("B4JSSH")

was left out - eventually sorted by referring to an unrelated bit of java for another library that I had,

jsch-0.1.55.jar was sourced by Copilot at: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55

and then the compilation steps (after a bit of confusion) were:

N:\B4A\SimpleLibraryCompiler > B4J_LibraryCompiler.exe
Java 8 Compiler: C:\Program Files\Eclipse Adoptium\jdk-8.0.472.8-hotspot\bin\javac.exe
Project Fo;der: N:\B4A\SimpleLibraryCompiler\B4JSSH
Library Name: B4JSSH
Compile

Builds: N:\B4J\Additional Libraries\B4JSSH.xml
N:\B4J\Additional Libraries\B4JSSH.jar

Then all that needed to be done was copy jsch-0.1.55.jar to N:\B4J\Additional Libraries and add this to my B4J project's Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region

Then, obviously, make sure the B4JSSH library is ticked in the B4J project.

Sounds easy but took me a weekend...
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
INSTALLATION

Load the attached files into your B4J Additional Libraries folder.

Download jsch-0.1.55.jar from: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55 and place it into your B4J Additional Libraries folder.

In your B4J project select the B4JSSH library and add this to the Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region
 

Attachments

  • B4JSSH.jar
    1.4 KB · Views: 37
  • B4JSSH.xml
    1.9 KB · Views: 32
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
EXAMPLE

Here is a bit of the code from my new B4J wisp switch manager that shows how you can interact with Netonix, Planet and Ubiquiti switches:
B4X:
                    Try

                        'Always start with a fresh engine
                        Obj_ssh.Initialize

                        'Connect
                        Obj_ssh.Connect(wrk_switch_ip, wrk_switch_ssh_port, wrk_camera.switch_user_name, wrk_camera.switch_password, Gen_timeout * DateTime.TicksPerSecond)

                        'Open interactive PTY shell
                        Obj_ssh.OpenShell

                        'Flush camera's switch connect count
                        Gen_switch_connect_count.Remove(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)

                    Catch
                  
                        'If camera's switch timeout count exists...
                        If Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) Then
                      
                            'Increment it
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) + 1)
                  
                        'Otherwise...
                        Else
                      
                            'Set it to 1
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, 1)
                  
                        End If
                  
                        Error_msg.Text = "Switch connect failure 1 " & Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) & " of " & Gen_max_retries & " - " & wrk_camera.switch_type & ":" & wrk_switch_name & ":"  & wrk_switch_ip & ":" &  wrk_switch_ssh_port & "(" & NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) & ")"
                  
                        'Close failed session
                        Obj_ssh.Disconnect

                        'If camera's switch connection has failed Gen_max_retries times...
                        If Gen_max_retries = NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) Then
                  
                            'Save camera's switch error state
                            Gen_switch_error_state.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, Error_msg.Text)
                      
                            'Whatever reporting here
                      
                        End If
                  
                    End Try
              
                    'If switch connected...
                    If Not(Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)) Then

                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'Ask switch for current configuration details
                            wrk_commands_sent = "terminal length 0" & CRLF & "show config" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see Netonix prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, wrk_switch_name & "# exit")

                            'If got Netonix prompt...
                            If wrk_result.Status = "Marker" Then
                      
                                'Extract JSON which is configuration details
                                wrk_json_start = wrk_result.Output.IndexOf("{")
                                wrk_json_end = wrk_result.Output.IndexOf(wrk_switch_name & "# exit")
                                wrk_json_str = wrk_result.Output.SubString2(wrk_json_start, wrk_json_end)

                                'If switch has not had its Revert Timer setting set to 0...
                                If wrk_json_str.IndexOf("""Switch_Revert_Timer"": ""0"",") = -1 Then
                  
                                    'Update Config log on disk
                                    Logger("Config", Gen_crlf & "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port)
                      
                                    Error_msg.Text = "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port
                          
                                    'As no timer is enabled this will cause app to hang
                                    Return
                      
                                End If
                  
                                'Extract PoE Ports configuration details
                                wrk_ports_start = wrk_json_str.IndexOf("""Ports"": [")
                                wrk_ports_end = wrk_json_str.IndexOf2("],", wrk_ports_start)
                                wrk_ports_str = wrk_json_str.SubString2(wrk_ports_start, wrk_ports_end)
                                wrk_port_number_end = 0
                  
                                wrk_port_poe.Clear

                                'Loop forever...
                                Do While True
              
                                    'Locate start of next PoE Port number field
                                    wrk_port_number_start = wrk_ports_str.IndexOf2("""Number"":", wrk_port_number_end) + 10
              
                                    'Quit loop if no more
                                    If wrk_port_number_start = 9 Then Exit
              
                                    'Locate end of PoE Port number field
                                    wrk_port_number_end = wrk_ports_str.IndexOf2(",", wrk_port_number_start)
              
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_ports_str.SubString2(wrk_port_number_start, wrk_port_number_end)
              
                                    'Locate start of next PoE Port state field
                                    wrk_port_poe_start = wrk_ports_str.IndexOf2("""PoE"":", wrk_port_number_end) + 8
              
                                    'Locate end of PoE Port state field
                                    wrk_port_poe_end = wrk_ports_str.IndexOf2(""",", wrk_port_poe_start)
              
                                    'Extract PoE Port current state
                                    wrk_port_poe.Put(wrk_port_number, wrk_ports_str.SubString2(wrk_port_poe_start, wrk_port_poe_end))
                  
                                Loop
                  
                            'Otherwise, must have timed out...
                            Else
                  
                                Error_msg.Text = "Netonix switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
              
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                  
                                'Whatever reporting here

                            End If

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'Read until we see Planet Username: prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "Username:")
                      
                            'If got Planet Username: prompt...
                            If wrk_result.Status = "Marker" Then
                      
                                'Send Planet switch username
                                wrk_commands_sent = wrk_camera.switch_user_name
                                Obj_ssh.Write(wrk_commands_sent)

                                'Read until we see Planet Password: prompt or timeout
                                wrk_result = SSH_Read_Until(Obj_ssh, "Password:")
                              
                                'If got Planet Password: prompt...
                                If wrk_result.Status = "Marker" Then
                                  
                                    'Send Planet switch password
                                    wrk_commands_sent = wrk_camera.switch_password
                                    Obj_ssh.Write(wrk_commands_sent)
                                  
                                    'Read until we see Planet # prompt or timeout
                                    wrk_result = SSH_Read_Until(Obj_ssh, "#")
                                  
                                    'If got Planet # prompt...
                                    If wrk_result.Status = "Marker" Then
                                  
                                        'Send Planet command to show PoE status of all PoE ports
                                        wrk_commands_sent = "show poe"
                                        Obj_ssh.Write(wrk_commands_sent)

                                        'Read until we see Planet prompt or timeout
                                        wrk_result = SSH_Read_Until(Obj_ssh, "#")

                                        'If got Planet # prompt...
                                        If wrk_result.Status = "Marker" Then

                                            'If got to here wrk_result.Output should look like this:
                                            '
                                            'PoE                     PD     Port               Power     Current
                                            'Interface               Class  Status             Used [W]  Used [mA]
                                            '----------------------  -----  -----------------  --------  ---------
                                            'GigabitEthernet 1/1     6      PoE ON             2 .9      62
                                            'GigabitEthernet 1/2     4      PoE ON             6 .1      129
                                            'GigabitEthernet 1/3     ---    PoE disabled       0 .0      0
                                            'GigabitEthernet 1/4     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/5     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/6     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/7     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/8     ---    PoE Search         0 .0      0
                                            '----------------------  -----  -----------------  --------  ---------
                                            '
                                            'Current Power Consumption    9. 0[W] (2%)
                                            'PoE Voltage                 47. 5[V]
                                            'PoE Version==> 3.55(  0)
                                            '
                                            'WGS-5225-8UP2SV#
                                            '
                                            'Notes 1. all Planet Wisp switches denote port numbers as 1/whatever
                                            '         with 1/ being common across all models e.g. "1/3" means port 3
                                            '      2. PD Class 6 = 802.3bt device (a.k.a. PoE++ e.g. Ubiquiti port 1)
                                            '      3. PD CLass 4 = 802.3at device (a.k.a. POE+ e.g. Hikvision camera)
                                            '      4. Normally only a Ubiquiti port 1 would be connected to any
                                            '         WGS-5225-8UP2SV port
                                            '      5. PoE ON = power on, automatically negotiated as per PD class
                                            '      6. PoE disabled = power off
                                            '      7. PoE Search = no device attached

                                            wrk_port_lines = Regex.Split(Gen_crlf, wrk_result.Output)
                          
                                            wrk_port_poe.Clear

                                            For Each wrk_port_line As String In wrk_port_lines

                                                'If a port line...
                                                If wrk_port_line.StartsWith("GigabitEthernet 1/") Then
                                  
                                                    'Strip out "GigabitEthernet 1/"
                                                    wrk_port_line = wrk_port_line.Replace("GigabitEthernet 1/", "")
                                  
                                                    'Extract PoE Port number
                                                    wrk_port_number = wrk_port_line.SubString2(0, 1)

                                                    'Extract PoE Port current state
                                                    If wrk_port_line.Contains("PoE ON") Then
                                                        wrk_port_poe.Put(wrk_port_number, "48V/H")
                                                    Else
                                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                                    End If

                                                End If

                                            Next
                                      
                                        End If

                                    End If
                      
                                End If

                            End If

                            'If timed out somewhere above...
                            If wrk_result.Status = "Timeout" Then
              
                                Error_msg.Text = "Planet switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
          
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
              
                                'Whatever reporting here

                            End If

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'Send Ubiquiti command to show PoE status of all ports
                            wrk_commands_sent = "ubntbox swctrl poe show" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see marker or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "# exit")

                            'If got marker...
                            If wrk_result.Status = "Marker" Then

                                'If got to here wrk_result.Output should look like this:
                                '
                                '~l04262279:
                                '~l04262279:
                                'BusyBox v1.25.1 () built-in shell (ash)
                                '~l04262279:
                                '~l04262279:
                                '  ___ ___      .__________.__
                                ' |   |   |____ |__\_  ____/__|
                                ' |   |   /    \|  ||  __) |  |   (c) 2010-2023
                                ' |   |  |   |  \  ||  \   |  |   Ubiquiti Inc.
                                ' |______|___|  /__||__/   |__|
                                '            |_/                  https://www.ui.com
                                '~l04262279:
                                '      Welcome to UniFi USW-Flex!
                                '~l04262279:
                                '********************************* NOTICE **********************************
                                '* By logging in to, accessing, or using any Ubiquiti product, you are     *
                                '* signifying that you have read our Terms of Service (ToS) and End User   *
                                '* License Agreement (EULA), understand their terms, and agree to be       *
                                '* fully bound to them. The use of SSH (Secure Shell) can potentially      *
                                '* harm Ubiquiti devices and result in lost access to them and their data. *
                                '* By proceeding, you acknowledge that the use of SSH to modify device(s)  *
                                '* outside of their normal operational scope, or in any manner             *
                                '* inconsistent with the ToS or EULA, will permanently and irrevocably     *
                                '* void any applicable warranty.                                           *
                                '***************************************************************************
                                '~l04262279:
                                'USW-Flex-US.6.5.32# Total Power Limit(mW): 46000
                                '~l04262279:
                                'Port  OpMode      HpMode    PwrLimit   Class   PoEPwr  PwrGood  Power(W)  Voltage(V)  Current(mA)
                                '                              (mW)
                                '----  ------  ------------  --------  -------  ------  -------  --------  ----------  -----------
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00
                                'USW-Flex-US.6.5.32# exit
                              
                                'Extract block of configuration details
                                wrk_block_start = wrk_result.Output.IndexOf("----  ------  ------------")
                                wrk_block_start = wrk_result.Output.IndexOf2("   2", wrk_block_start)
                                wrk_block_end = wrk_result.Output.IndexOf("# exit")
                                wrk_block_end = wrk_result.Output.LastIndexOf2(Gen_crlf, wrk_block_end)
                                wrk_block_str = wrk_result.Output.SubString2(wrk_block_start, wrk_block_end + 2)

                                'wrk_block_str should look like this:
                                '
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00

                                wrk_port_lines = Regex.Split(Gen_crlf, wrk_block_str)

                                wrk_port_poe.Clear

                                For Each wrk_port_line As String In wrk_port_lines

                                    'wrk_port_line should look like this:
                                    '
                                    '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                      
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_port_line.SubString2(0, 4)
                          
                                    'Extract PoE Port current state
                                    If wrk_port_line.IndexOf(" Off ") = -1 Then
                                        wrk_port_poe.Put(wrk_port_number, "48V")
                                    Else
                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                    End If

                                Next

                            'Otherwise, must have timed out...
                            Else
                                                          
                                Error_msg.Text = "Ubiquiti switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
                              
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                                  
                                'Whatever reporting here
                          
                            End If

                        End If
                              
                        Obj_ssh.Disconnect
              
                    End If

                End If

and the SSH_Read_Until sub looks like this:
B4X:
Private Sub SSH_Read_Until(SSH_object As B4JSSH, Marker As String) As SSH_Read_Result_Type

    Private wrk_result As SSH_Read_Result_Type
    Private wrk_chunk, wrk_str As String
    Private wrk_end As Long = DateTime.Now + Gen_timeout * DateTime.tickspersecond

    Private wrk_jo As JavaObject
    wrk_jo.InitializeStatic("java.lang.Thread")

    Do While DateTime.Now  < wrk_end
        wrk_chunk = SSH_object.Read
        If wrk_chunk.Length > 0 Then
            wrk_str = wrk_str & wrk_chunk

            'Marker detection
            If wrk_str.Contains(Marker) Then
                wrk_result.Status = "Marker"
                wrk_result.Output = wrk_str
                Return wrk_result
            End If

        End If

        '5 ms pause, stays inside loop      
        Private wrk_5 As Long = 5
        wrk_jo.RunMethodJO("sleep", Array As Object(wrk_5))
      
    Loop

    'Iimeout detection
    wrk_result.Status = "Timeout"
    wrk_result.Output = wrk_str
    Return wrk_result
  
End Sub
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
USER DOCUMENTATION

For what it is worth here is some documentation I got Copilot to do:

# 📘 **B4JSSH Library — Method Summary**

The **B4JSSH** library provides a minimal, deterministic SSH client wrapper for B4J, designed specifically for devices that require **interactive PTY‑based shell sessions** (e.g., Planet, Netonix, Ubiquiti, Cisco).
It exposes a small, predictable API surface suitable for automation, scripting, and prompt‑driven CLI interaction.


## 🔧 **Initialize**

### **`Initialize()`**
Creates the internal JSch instance and prepares the library for use.

**Purpose:**
Sets up the SSH engine. Must be called before any other method.

---

## 🔌 **Connect**

### **`Connect(host As String, port As Int, user As String, pass As String, timeout As Int)`**
Establishes an SSH session with the target device.

**Parameters:**
- `host` — IP address or hostname
- `port` — SSH port (typically 22)
- `user` — username
- `pass` — password
- `timeout` — connection timeout in milliseconds

**Behaviour:**
- Creates a new SSH session
- Disables strict host key checking
- Authenticates using username/password
- Applies the specified timeout

**Notes:**
This method does **not** open a shell. Use `OpenShell` after connecting.

---

## 🖥️ **OpenShell**

### **`OpenShell()`**
Opens an interactive **PTY‑enabled shell channel**.

**Purpose:**
Provides a persistent, interactive CLI session required by Planet and similar switches.

**Behaviour:**
- Allocates a PTY
- Opens a shell channel
- Exposes raw input/output streams
- Enables real‑time command execution

**Notes:**
This is essential for devices that do not support exec channels.

---

## ✏️ **Write**

### **`Write(cmd As String)`**
Sends a command or raw text to the open shell session.

**Behaviour:**
- Writes the string followed by a newline
- Flushes immediately
- Does not wait for output

**Usage:**
Typically paired with `Read` or a custom `ReadUntil` loop in B4J.

---

## 📥 **Read**

### **`Read() As String`**
Returns all currently available output from the shell buffer.

**Behaviour:**
- Non‑blocking
- Reads whatever bytes are available at the moment
- Returns partial or complete CLI output
- Does not wait for prompts

**Notes:**
This method is intentionally low‑level to allow deterministic prompt handling in B4J.

---

## 🔌 **Disconnect**

### **`Disconnect()`**
Closes the shell channel and SSH session.

**Behaviour:**
- Safely disconnects the shell
- Safely disconnects the session
- Ignores exceptions during cleanup

**Notes:**
Always call this when finished to release resources.

---

# 🧩 **Design Philosophy**

The B4JSSH library intentionally exposes only the **minimal primitives** required for reliable automation:

- Connect
- OpenShell
- Write
- Read
- Disconnect

All higher‑level logic (prompt detection, command wrappers, parsing, retries, paging control) is implemented in B4J, not inside the library.
This keeps the library deterministic, predictable, and compatible with a wide range of network devices.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
KNOWN BUGS
 

JackKirk

Well-Known Member
Licensed User
Longtime User
ADDITIONAL NOTES

I don't do java, xml or build libraries but Copilot can - with a lot of guidance in the form of progressively less dumb questions from me....
 
Last edited:

MicroDrie

Well-Known Member
Licensed User
Longtime User
Thank you for sharing this information.
 

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - PREAMBLE

I don't know if anyone is actually interested in the functionality of this library but on the off chance...

I have done some extensive testing on the 1.0 version described above and found performance to be very ordinary - for example compared to Windows .bat files that I have had for some time that allows me to manually fiddle with Ubiquiti switches.

These .bat files use plink.

I queried Copilot on this and. after several goes around the merry-go-round it came up with a revised version.

This version has 2 optional boolean settings:

Obj_ssh.SetPTY
Obj_ssh.SetInitialNewline

and a revised Read method, which seems to be the major improvement.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - CODE

I have left the original up so anyone who is interested can see the changes made.

The new B4JSSH.java is:
B4X:
package b4j.ssh;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;

import com.jcraft.jsch.*;

import java.io.InputStream;
import java.io.OutputStream;

@ShortName("B4JSSH")
@Version(1.10f)
public class B4JSSH {

    private JSch jsch;
    private Session session;
    private ChannelShell shell;
    private InputStream in;
    private OutputStream out;

    //Defaults - match version 1.0 functionality
    private boolean usePTY = true;          
    private boolean sendInitialNewline = false;

    public void Initialize() {
        jsch = new JSch();
    }

    //Allow B4J to set PTY mode before opening shell
    public void SetPTY(boolean enabled) {
        this.usePTY = enabled;
    }

    //Allow B4J to toggle initial newline before opening shell
    public void SetInitialNewline(boolean enabled) {
        this.sendInitialNewline = enabled;
    }

    public void Connect(String host, int port, String user, String pass, int timeout) throws Exception {
        session = jsch.getSession(user, host, port);
        session.setPassword(pass);

        //Safe automation defaults
        session.setConfig("StrictHostKeyChecking", "no");
        session.setConfig("UserKnownHostsFile", "/dev/null");
        session.setConfig("PreferredAuthentications", "password");

        session.connect(timeout);
    }

    public void OpenShell() throws Exception {
 
        //Add a guard for session == null
        if (session == null || !session.isConnected()) {
            throw new IllegalStateException("Session not connected.");
        }
   
        shell = (ChannelShell) session.openChannel("shell");

        //Apply PTY mode chosen by user
        shell.setPty(usePTY);

        in = shell.getInputStream();
        out = shell.getOutputStream();
        shell.connect();

        //Some devices (Cisco, Juniper) need 20–40ms before sending data
        Thread.sleep(20);

        //Optional banner bypass
        if (sendInitialNewline) {

            //Adaptive banner detection - wait until remote side actually starts
            //sending data - this avoids guessing delays (50ms, 150ms, etc.)
            long start = System.currentTimeMillis();
            //2 seconds max wait for banner
            long timeout = 2000;

            while (System.currentTimeMillis() - start < timeout) {
                if (in.available() > 0) {
                    //Banner has begun arriving - safe to send newline
                    break;
                }
                Thread.sleep(10);
            }

            out.write("\n".getBytes());
            out.flush();
        }
    }

    public void Write(String cmd) throws Exception {
        out.write((cmd + "\n").getBytes());
        out.flush();
    }

    public String Read() throws Exception {
        StringBuilder sb = new StringBuilder();
        long start = System.currentTimeMillis();

        byte[] buffer = new byte[1024];

        //Active read loop for deterministic output
        while (System.currentTimeMillis() - start < 300) {
            //Non-blocking read - only read when data is available
            while (in.available() > 0) {
                int len = in.read(buffer);
                if (len > 0) {
                    sb.append(new String(buffer, 0, len, "UTF-8"));
                }
            }
            Thread.sleep(10);
        }
        return sb.toString();
    }

    public void Disconnect() {
        try { if (shell != null) shell.disconnect(); } catch (Exception ignored) {}
        try { if (session != null) session.disconnect(); } catch (Exception ignored) {}
    }
}
and then the compilation steps (as before) are:

N:\B4A\SimpleLibraryCompiler > B4J_LibraryCompiler.exe
Java 8 Compiler: C:\Program Files\Eclipse Adoptium\jdk-8.0.472.8-hotspot\bin\javac.exe
Project Fo;der: N:\B4A\SimpleLibraryCompiler\B4JSSH
Library Name: B4JSSH
Compile

Builds: N:\B4J\Additional Libraries\B4JSSH.xml
N:\B4J\Additional Libraries\B4JSSH.jar

- now 1.1

And as before, all that needs to be done is copy jsch-0.1.55.jar to N:\B4J\Additional Libraries and add this to the B4J project's Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region

Then, obviously, make sure the B4JSSH library is ticked in the B4J project.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - INSTALLATION

Load the attached files into your B4J Additional Libraries folder.

Download jsch-0.1.55.jar from: https://mvnrepository.com/artifact/com.jcraft/jsch/0.1.55 and place it into your B4J Additional Libraries folder.

In your B4J project select the B4JSSH library (make sure it is 1.1) and add this to the Main module:

#Region Project Attributes
...
'For B4JSSH library:
#AdditionalJar: jsch-0.1.55.jar
...
#End Region
 

Attachments

  • B4JSSH.xml
    2.4 KB · Views: 9
  • B4JSSH.jar
    1.9 KB · Views: 9

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - EXAMPLE

You could use the original example code - this would just use defaults for the 2 optional boolean settings:

Obj_ssh.SetPTY(True)
Obj_ssh.SetInitialNewline(False)

If you are using Netonix, Planet or Ubiquiti switches you might consider revising this code as follows:
B4X:
                    Try

                        'Always start with a fresh engine
                        Obj_ssh.Initialize

                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'No PTY needed (faster)
                            Obj_ssh.SetPTY(False)

                            'No banner .: no newline
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline breaks login
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline skips banner
                            Obj_ssh.SetInitialNewline(True)

                        End If

                        'Connect
                        Obj_ssh.Connect(wrk_switch_ip, wrk_switch_ssh_port, wrk_camera.switch_user_name, wrk_camera.switch_password, Gen_timeout * DateTime.TicksPerSecond)

                        'Open interactive PTY shell
                        Obj_ssh.OpenShell

                        'Flush camera's switch connect count
                        Gen_switch_connect_count.Remove(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)

                    Catch
                 
                        'If camera's switch timeout count exists...
                        If Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) Then
                     
                            'Increment it
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) + 1)
                 
                        'Otherwise...
                        Else
                     
                            'Set it to 1
                            Gen_switch_connect_count.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, 1)
                 
                        End If
                 
                        Error_msg.Text = "Switch connect failure 1 " & Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port) & " of " & Gen_max_retries & " - " & wrk_camera.switch_type & ":" & wrk_switch_name & ":"  & wrk_switch_ip & ":" &  wrk_switch_ssh_port & "(" & NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) & ")"
                 
                        'Close failed session
                        Obj_ssh.Disconnect

                        'If camera's switch connection has failed Gen_max_retries times...
                        If Gen_max_retries = NumberFormat(Gen_switch_connect_count.Get(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port), 1, 0) Then
                 
                            'Save camera's switch error state
                            Gen_switch_error_state.Put(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port, Error_msg.Text)
                     
                            'Whatever reporting here
                     
                        End If
                 
                    End Try
             
                    'If switch connected...
                    If Not(Gen_switch_connect_count.ContainsKey(wrk_camera.switch_type & ":" & wrk_camera.switch_name & ":" & wrk_camera.switch_ip & ":" & wrk_camera.switch_ssh_port)) Then

                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'Ask switch for current configuration details
                            wrk_commands_sent = "terminal length 0" & CRLF & "show config" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see Netonix prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, wrk_switch_name & "# exit")

                            'If got Netonix prompt...
                            If wrk_result.Status = "Marker" Then
                     
                                'Extract JSON which is configuration details
                                wrk_json_start = wrk_result.Output.IndexOf("{")
                                wrk_json_end = wrk_result.Output.IndexOf(wrk_switch_name & "# exit")
                                wrk_json_str = wrk_result.Output.SubString2(wrk_json_start, wrk_json_end)

                                'If switch has not had its Revert Timer setting set to 0...
                                If wrk_json_str.IndexOf("""Switch_Revert_Timer"": ""0"",") = -1 Then
                 
                                    'Update Config log on disk
                                    Logger("Config", Gen_crlf & "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port)
                     
                                    Error_msg.Text = "Netonix switch non-zero revert timer - " & wrk_switch_type & ":"  & wrk_switch_name & ":" & wrk_switch_ip & ":" &  wrk_switch_ssh_port
                         
                                    'As no timer is enabled this will cause app to hang
                                    Return
                     
                                End If
                 
                                'Extract PoE Ports configuration details
                                wrk_ports_start = wrk_json_str.IndexOf("""Ports"": [")
                                wrk_ports_end = wrk_json_str.IndexOf2("],", wrk_ports_start)
                                wrk_ports_str = wrk_json_str.SubString2(wrk_ports_start, wrk_ports_end)
                                wrk_port_number_end = 0
                 
                                wrk_port_poe.Clear

                                'Loop forever...
                                Do While True
             
                                    'Locate start of next PoE Port number field
                                    wrk_port_number_start = wrk_ports_str.IndexOf2("""Number"":", wrk_port_number_end) + 10
             
                                    'Quit loop if no more
                                    If wrk_port_number_start = 9 Then Exit
             
                                    'Locate end of PoE Port number field
                                    wrk_port_number_end = wrk_ports_str.IndexOf2(",", wrk_port_number_start)
             
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_ports_str.SubString2(wrk_port_number_start, wrk_port_number_end)
             
                                    'Locate start of next PoE Port state field
                                    wrk_port_poe_start = wrk_ports_str.IndexOf2("""PoE"":", wrk_port_number_end) + 8
             
                                    'Locate end of PoE Port state field
                                    wrk_port_poe_end = wrk_ports_str.IndexOf2(""",", wrk_port_poe_start)
             
                                    'Extract PoE Port current state
                                    wrk_port_poe.Put(wrk_port_number, wrk_ports_str.SubString2(wrk_port_poe_start, wrk_port_poe_end))
                 
                                Loop
                 
                            'Otherwise, must have timed out...
                            Else
                 
                                Error_msg.Text = "Netonix switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
             
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                 
                                'Whatever reporting here

                            End If

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'Read until we see Planet Username: prompt or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "Username:")
                     
                            'If got Planet Username: prompt...
                            If wrk_result.Status = "Marker" Then
                     
                                'Send Planet switch username
                                wrk_commands_sent = wrk_camera.switch_user_name
                                Obj_ssh.Write(wrk_commands_sent)

                                'Read until we see Planet Password: prompt or timeout
                                wrk_result = SSH_Read_Until(Obj_ssh, "Password:")
                             
                                'If got Planet Password: prompt...
                                If wrk_result.Status = "Marker" Then
                                 
                                    'Send Planet switch password
                                    wrk_commands_sent = wrk_camera.switch_password
                                    Obj_ssh.Write(wrk_commands_sent)
                                 
                                    'Read until we see Planet # prompt or timeout
                                    wrk_result = SSH_Read_Until(Obj_ssh, "#")
                                 
                                    'If got Planet # prompt...
                                    If wrk_result.Status = "Marker" Then
                                 
                                        'Send Planet command to show PoE status of all PoE ports
                                        wrk_commands_sent = "show poe"
                                        Obj_ssh.Write(wrk_commands_sent)

                                        'Read until we see Planet prompt or timeout
                                        wrk_result = SSH_Read_Until(Obj_ssh, "#")

                                        'If got Planet # prompt...
                                        If wrk_result.Status = "Marker" Then

                                            'If got to here wrk_result.Output should look like this:
                                            '
                                            'PoE                     PD     Port               Power     Current
                                            'Interface               Class  Status             Used [W]  Used [mA]
                                            '----------------------  -----  -----------------  --------  ---------
                                            'GigabitEthernet 1/1     6      PoE ON             2 .9      62
                                            'GigabitEthernet 1/2     4      PoE ON             6 .1      129
                                            'GigabitEthernet 1/3     ---    PoE disabled       0 .0      0
                                            'GigabitEthernet 1/4     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/5     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/6     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/7     ---    PoE Search         0 .0      0
                                            'GigabitEthernet 1/8     ---    PoE Search         0 .0      0
                                            '----------------------  -----  -----------------  --------  ---------
                                            '
                                            'Current Power Consumption    9. 0[W] (2%)
                                            'PoE Voltage                 47. 5[V]
                                            'PoE Version==> 3.55(  0)
                                            '
                                            'WGS-5225-8UP2SV#
                                            '
                                            'Notes 1. all Planet Wisp switches denote port numbers as 1/whatever
                                            '         with 1/ being common across all models e.g. "1/3" means port 3
                                            '      2. PD Class 6 = 802.3bt device (a.k.a. PoE++ e.g. Ubiquiti port 1)
                                            '      3. PD CLass 4 = 802.3at device (a.k.a. POE+ e.g. Hikvision camera)
                                            '      4. Normally only a Ubiquiti port 1 would be connected to any
                                            '         WGS-5225-8UP2SV port
                                            '      5. PoE ON = power on, automatically negotiated as per PD class
                                            '      6. PoE disabled = power off
                                            '      7. PoE Search = no device attached

                                            wrk_port_lines = Regex.Split(Gen_crlf, wrk_result.Output)
                         
                                            wrk_port_poe.Clear

                                            For Each wrk_port_line As String In wrk_port_lines

                                                'If a port line...
                                                If wrk_port_line.StartsWith("GigabitEthernet 1/") Then
                                 
                                                    'Strip out "GigabitEthernet 1/"
                                                    wrk_port_line = wrk_port_line.Replace("GigabitEthernet 1/", "")
                                 
                                                    'Extract PoE Port number
                                                    wrk_port_number = wrk_port_line.SubString2(0, 1)

                                                    'Extract PoE Port current state
                                                    If wrk_port_line.Contains("PoE ON") Then
                                                        wrk_port_poe.Put(wrk_port_number, "48V/H")
                                                    Else
                                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                                    End If

                                                End If

                                            Next
                                     
                                        End If

                                    End If
                     
                                End If

                            End If

                            'If timed out somewhere above...
                            If wrk_result.Status = "Timeout" Then
             
                                Error_msg.Text = "Planet switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
         
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
             
                                'Whatever reporting here

                            End If

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'Send Ubiquiti command to show PoE status of all ports
                            wrk_commands_sent = "ubntbox swctrl poe show" & CRLF & "exit"
                            Obj_ssh.Write(wrk_commands_sent)

                            'Read until we see marker or timeout
                            wrk_result = SSH_Read_Until(Obj_ssh, "# exit")

                            'If got marker...
                            If wrk_result.Status = "Marker" Then

                                'If got to here wrk_result.Output should look like this:
                                '
                                '~l04262279:
                                '~l04262279:
                                'BusyBox v1.25.1 () built-in shell (ash)
                                '~l04262279:
                                '~l04262279:
                                '  ___ ___      .__________.__
                                ' |   |   |____ |__\_  ____/__|
                                ' |   |   /    \|  ||  __) |  |   (c) 2010-2023
                                ' |   |  |   |  \  ||  \   |  |   Ubiquiti Inc.
                                ' |______|___|  /__||__/   |__|
                                '            |_/                  https://www.ui.com
                                '~l04262279:
                                '      Welcome to UniFi USW-Flex!
                                '~l04262279:
                                '********************************* NOTICE **********************************
                                '* By logging in to, accessing, or using any Ubiquiti product, you are     *
                                '* signifying that you have read our Terms of Service (ToS) and End User   *
                                '* License Agreement (EULA), understand their terms, and agree to be       *
                                '* fully bound to them. The use of SSH (Secure Shell) can potentially      *
                                '* harm Ubiquiti devices and result in lost access to them and their data. *
                                '* By proceeding, you acknowledge that the use of SSH to modify device(s)  *
                                '* outside of their normal operational scope, or in any manner             *
                                '* inconsistent with the ToS or EULA, will permanently and irrevocably     *
                                '* void any applicable warranty.                                           *
                                '***************************************************************************
                                '~l04262279:
                                'USW-Flex-US.6.5.32# Total Power Limit(mW): 46000
                                '~l04262279:
                                'Port  OpMode      HpMode    PwrLimit   Class   PoEPwr  PwrGood  Power(W)  Voltage(V)  Current(mA)
                                '                              (mW)
                                '----  ------  ------------  --------  -------  ------  -------  --------  ----------  -----------
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00
                                'USW-Flex-US.6.5.32# exit
                             
                                'Extract block of configuration details
                                wrk_block_start = wrk_result.Output.IndexOf("----  ------  ------------")
                                wrk_block_start = wrk_result.Output.IndexOf2("   2", wrk_block_start)
                                wrk_block_end = wrk_result.Output.IndexOf("# exit")
                                wrk_block_end = wrk_result.Output.LastIndexOf2(Gen_crlf, wrk_block_end)
                                wrk_block_str = wrk_result.Output.SubString2(wrk_block_start, wrk_block_end + 2)

                                'wrk_block_str should look like this:
                                '
                                '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   3    Auto        Dot3at     31507  Class 4      On     Good      2.01       48.60         0.00
                                '   4    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                                '   5    Auto        Dot3at     31507  Class 4      On     Good      2.15       48.81        44.00

                                wrk_port_lines = Regex.Split(Gen_crlf, wrk_block_str)

                                wrk_port_poe.Clear

                                For Each wrk_port_line As String In wrk_port_lines

                                    'wrk_port_line should look like this:
                                    '
                                    '   2    Auto        Dot3at     31507  Unknown     Off      Bad      0.00        0.00         0.00
                     
                                    'Extract PoE Port number
                                    wrk_port_number = wrk_port_line.SubString2(0, 4)
                         
                                    'Extract PoE Port current state
                                    If wrk_port_line.IndexOf(" Off ") = -1 Then
                                        wrk_port_poe.Put(wrk_port_number, "48V")
                                    Else
                                        wrk_port_poe.Put(wrk_port_number, "Off")
                                    End If

                                Next

                            'Otherwise, must have timed out...
                            Else
                                                         
                                Error_msg.Text = "Ubiquiti switch show timeout - " & wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port
                             
                                'Save error state of switch
                                Gen_switch_error_state.Put(wrk_switch_type & ":" & wrk_switch_name & ":" & wrk_switch_ip & ":" & wrk_switch_ssh_port, Error_msg.Text)
                                 
                                'Whatever reporting here
                         
                            End If

                        End If
                             
                        Obj_ssh.Disconnect
             
                    End If

                End If
the only change is:
B4X:
                        'If Netonix switch...
                        If wrk_camera.switch_type = "Netonix" Then

                            'No PTY needed (faster)
                            Obj_ssh.SetPTY(False)

                            'No banner .: no newline
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, if Planet switch...
                        Else If wrk_camera.switch_type = "Planet" Then

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline breaks login
                            Obj_ssh.SetInitialNewline(False)

                        'Otherwise, must be Ubiquiti switch...
                        Else

                            'PTY required for prompt
                            Obj_ssh.SetPTY(True)

                            'Newline skips banner
                            Obj_ssh.SetInitialNewline(True)

                        End If
placed between the Initialize and Connect

These settings are as recommended by Copilot for each switch and have tested OK.

If you were using a different switch (Cisco, ...) you might need to experiment (there are only 4 possible combinations).

The SSH_Read_Until sub is as before.
 
Last edited:

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - DOCUMENTATION

# 📘 **B4JSSH Library — Method Summary**

The **B4JSSH** library provides a minimal, deterministic SSH client wrapper for B4J, designed specifically for devices that require **interactive PTY‑based shell sessions** (e.g., Planet, Netonix, Ubiquiti, Cisco).
It exposes a small, predictable API surface suitable for automation, scripting, and prompt‑driven CLI interaction.

The library intentionally avoids high‑level abstractions.
All prompt detection, parsing, retries, and automation logic remain in B4J for full operator control.

---

## 🔧 **Initialize**

### **`Initialize()`**
Creates the internal JSch instance and prepares the library for use.

**Purpose:**
Initialises the SSH engine. Must be called before any other method.

---

## ⚙️ **SetPTY**

### **`SetPTY(enabled As Boolean)`**
Configures whether the shell channel will request a PTY (pseudo‑terminal) during `OpenShell()`.

**Purpose:**
Controls terminal emulation behaviour. PTY affects how remote devices present banners, prompts, and output formatting.

**Behaviour:**
- Value is stored until `OpenShell()` is called
- No effect after the shell is already open
- Default is `true` (matching v1.0 behaviour)

---

## ⚙️ **SetInitialNewline**

### **`SetInitialNewline(enabled As Boolean)`**
Enables or disables transmission of a single newline immediately after the shell channel connects.

**Purpose:**
Some devices do not present a prompt until a keypress is received.
When enabled, the library sends a newline **only after** detecting that the remote side has begun sending data.

**Behaviour:**
- Adaptive: waits for incoming bytes before sending newline
- Avoids fixed delays or timing guesses
- Default is `false` (matching v1.0 behaviour)

---

## 🔌 **Connect**

### **`Connect(host, port, user, pass, timeout)`**
Establishes an SSH session with the target device.

**Parameters:**
- `host` — IP address or hostname
- `port` — SSH port
- `user` — username
- `pass` — password
- `timeout` — connection timeout in milliseconds

**Behaviour:**
- Creates a new SSH session
- Disables strict host key checking
- Uses password authentication
- Applies the specified timeout
- Does **not** open a shell

**Notes:**
`OpenShell()` must be called after a successful connection.

---

## 🖥️ **OpenShell**

### **`OpenShell()`**
Opens an interactive shell channel and prepares it for real‑time command execution.

**Purpose:**
Provides a persistent, interactive CLI session required by devices that do not support exec channels.

**Behaviour:**
- Validates that the session is connected
- Opens a `ChannelShell`
- Applies PTY mode based on `SetPTY()`
- Retrieves input/output streams
- Connects the shell
- Performs a short stabilisation delay
- Optionally sends an adaptive initial newline

**Notes:**
This method exposes raw streams.
All higher‑level automation is performed in B4J.

---

## ✏️ **Write**

### **`Write(cmd As String)`**
Sends a command or raw text to the open shell session.

**Behaviour:**
- Appends a newline
- Flushes immediately
- Does not wait for output
- Does not perform prompt detection

**Notes:**
Designed for deterministic, low‑level control.

---

## 📥 **Read**

### **`Read() As String`**
Returns all currently available output from the shell buffer within a fixed time window.

**Purpose:**
Provides a deterministic, non‑blocking read suitable for prompt‑driven automation.

**Behaviour:**
- Uses a 300 ms read window
- Reads only when data is available (`in.available() > 0`)
- Never blocks on `in.read()`
- Returns whatever output arrived during the window
- UTF‑8 safe

**Notes:**
This method intentionally returns partial output.
Prompt detection and command‑completion logic belong in B4J.

---

## 🔌 **Disconnect**

### **`Disconnect()`**
Closes the shell channel and SSH session.

**Behaviour:**
- Safely disconnects the shell
- Safely disconnects the session
- Ignores cleanup exceptions
- Idempotent (safe to call multiple times)

---

# 🧩 **Design Philosophy**

The B4JSSH library intentionally exposes only the **minimal primitives** required for reliable automation:

- `Connect`
- `OpenShell`
- `Write`
- `Read`
- `Disconnect`

All higher‑level logic—prompt detection, command wrappers, parsing, retries, paging control—is implemented in B4J, not inside the library.
This keeps the library deterministic, predictable, and compatible with a wide range of network devices.
 

JackKirk

Well-Known Member
Licensed User
Longtime User
Version 1.1 (Ferrari) - ADDITIONAL NOTES

I believe this exercise demonstrates the strong need for guidance of MLs like Copilot - I (by default) assumed that it would give me the most refined solution possible when I originally queried it on the subject.

But, in hindsight, it initially only gave the bare minimum that worked.

It was only after I badgered it for a better performing version that it delivered.

Which suggests the human element is still required for quality results (for now at least).

That said, I am really quite chuffed that I now have the ability to do things like this that are way outside my historic knowledge envelope.
 
Last edited:
Top