Saturday, November 13, 2021

X32 edit PC Transport Control

 What the heck is that setting?


Tells the X32 to record on the USB port on the Behringer vs the LIVE card (if one is installed)




Friday, October 29, 2021

Insert an EQ into a bus on an x32

 Its easy.  Here is what they don't tell you, or what isn't really clear, especially if you are running a rack on X32 Mix.

FX1-4 are sidechain/insert  FX 5-8 are inserts


XMIX screen


So all you have to do is pick your 31 band eq, and put them into FX 5-8

Since each one is a dual 31 band, you set the BUS to which ever one you want to go to what EQ.

So in the above example, you'll see I have bus 1&2 going to FX slot 5 (insert) and they FX is selected (purple) to activate it.  Now bus 1 & 2 have a 31 band inserted.  You can EQ your wedges.

You'll see FX slot 6 has bus #4 going to it, an i have channel 5 with a 31 band inserted.


Tuesday, September 14, 2021

Origin missing MSVCP140.dll on Windows 10

 Origin not installing on Windows 10 with a missing MSVCP140.dll ?

This should fix you up.

https://support.microsoft.com/en-us/topic/the-latest-supported-visual-c-downloads-2647da03-1eea-4433-9aff-95f26a218cc0

Go to that MS site and download the X86 version of this "x86: vc_redist.x86.exe" 

Actually install both.

Install that and should be good to go.

Wednesday, July 21, 2021

CUSTOMIZATION of PTZoptics OBS controls

 CUSTOMIZATION of PTZoptics OBS controls


This post is about how to customize those same controls they offer to, perhaps, make them more conducive to your environment.

I found the majority of the buttons too big, or too much information being displayed for users that wasn't necessary.  Since most of their code is in HTML, you could make some modifications relatively easy.

Instead of one big controller, I wanted smaller ones to fit into my OBS screen.


Here's some additional things that might make your PTZoptics cameras in OBS a little easier to control.

You can see here I've modified just the HTML code that they provide for the buttons to alter them to fit within the UI view that I have.  This is to make it easier for operators and not to have to scroll around a large PTZ dispaly
I'm using these controls in conjunction with a plug-in called "Source Dock" Source Dock | OBS Forums (obsproject.com) to give me dedicated preview shots above the controls for that camera. 


The operator to can select tabs at the bottom of each camera view to make it easier to select a variety of preset positions, PTZ buttons 

You may notice that I've removed the "FOCUS" button from my PTZ control.  I also removed the "Auto Pan" feature as these are things we never use, so I prevent users from accessing them accidently.

I separated the preferences and preset assignments to a separate tab in OBS.  So these can be still fully accessible, but don't clutter up the normal day-day usage of the PTZ system.





This was all done by ONLY changing the HTML code provided by PTZ optics.  Was not hard at all and works perfectly.

Its easy to customize the buttons...here's how you would change the code text name and the camera preset you want to have it activate when you click it.  You can alter this in NOTEPAD.  Make a backup of the file in case you make some HTML mistakes.

By default it looks kind of like this:

<a class="call_preset preset_button preset-1" data-preset="1" href="#preset-1">Button1</a>

Change "data-preset" and "#preset-1" to be the preset saved position you would have programmed into your camera.  In this case, the value is 1, but if preset position is 22, change these values both to "22"

Then "Button1" is the descriptive name you see in OBS.  Make it whatever you want, i think the max i 8 characters though

In the example below, I've made the first 3 buttons do presets 1-3,
however buttons 4-6 activate presets 10-12 and 7-9 do presets 13-15

<form role="form" class="form-horizontal">
<fieldset class="presets">
<div class="row">
<a class="call_preset preset_button preset-1" data-preset="1" href="#preset-1">Pulpit</a>
<a class="call_preset preset_button preset-2" data-preset="2" href="#preset-2">Communion Table</a>
<a class="call_preset preset_button preset-3" data-preset="3" href="#preset-3">Lecturn</a>
</div>
<div class="row">
<a class="call_preset preset_button preset-10" data-preset="10" href="#preset-10">Organ</a>
<a class="call_preset preset_button preset-11" data-preset="11" href="#preset-11">Stairs</a>
<a class="call_preset preset_button preset-12" data-preset="12" href="#preset-12">12</a>
</div>
<div class="row">
<a class="call_preset preset_button preset-13" data-preset="13" href="#preset-13">13</a>
<a class="call_preset preset_button preset-14" data-preset="14" href="#preset-14">14</a>
<a class="call_preset preset_button preset-15" data-preset="15" href="#preset-15">Control</a>
</div>
</fieldset>

Here's a google drive link to some of these files to help give you some ideas perhaps.

https://drive.google.com/drive/folders/1n-G4Vk4Lkfzp6Nl7pGJy1UwYpF37OPz2?usp=sharing

Drop them into the appropriate "Camera X" folder(s) that is created when you extract the PTZOptics download, and then install them using this method :

You should know have some similar examples available in your OBS as is pictured above.

Wednesday, July 14, 2021

Control PTZ optics using their OBS "PTZOptics Open Source Control Software"

 This will let you control the PTZ optics camera using buttons that you can place inside your OBS Studio program.

First you want to download the PTZ optics package for their cameras to use with OBS

PTZOptics Open Source Software.zip

This is the most recent version (as of July 14, 2021).  This one fixes a problem where you could only control 1 camera, despite claims that you could control 4 (or more)

Next, you unzip this file, and you can unzip it anywhere in your machine, doesn't need to be in the OBS folder.  Just put it in a spot where you aren't going to accidently delete it later on.

Once you unzip it, you'll have a folder like this:

Here's the whole process you should know.  The above example controls 4 cameras, you just use use a different folder for each camera you have.  You could have a whole bunch, just copy/paste the Camera "X" folder and give it a new name.  PTZOptics makes it very easy.

STEPS:

Go into the folder for a camera.  I'll use Camera 1 as example

You will see a few pre-built layouts



Lets pick "Medium Controller (6) Preset Images".  So, simply double click that file, and it will open in a web browser

Now select the entire URL location and copy it.  We are going to paste this into OBS, and obs will use this file location and file URL to display the image you see now into OBS.


Go into OBS and select VIEW -> DOCKS -> CUSTOM BROWSER DOCKS


You'll see this:


Give it a "DOCK NAME" of your choice, then in the "URL" window paste the URL you copied above


Then press "APPLY"

You should now see a window pop up with the title you put in "Dock Name"

Now click drag it an you can place it somewhere in OBS that you like.


Now click on "PREFERENCES" in the Camera window at the bottom


Now you can assign hotkeys 1-9 on your PTZOptics camera and by pressing one of these buttons 1-9, the camera will move.


Change the IP Address to be the IP of the camera you are trying to control, then press "RELOAD CAMERA"  (which means "save IP address"

Click on DONE to get out of this screen.

You may need to restart OBS after to reload the camera you want to control.  
Youll notice there are other functions that you have access to just in case.


Now in that same folder that you unzipped, you'll notice some other pre-built controls.  You can have them all in OBS if you want.  I like the one called "Small Controller".  This will let me move the camera around

Repeat the above steps by copy/pasting the file URL of the subseqent URL files you want to have in OBS.
I place the "Small Controller" window ontop of the camera window, and it creates a small tab at the bottom that lets me toggle back and forth between the preset view and the PTZ view.





Automatically move PTZoptics Camera OBS Studio based on scene

Have your PTZoptics camera move to a pre-programmed x/y/z location automatically

You can have your PTZoptics camera move to a preset position that you've stored automatically when you select a scene.  Basically, we will use the "BROWSER" function in OBS to send an HTML command to the specific camera, that is formatted to trigger one of the pre-stored positions.

First, save a preset location.  In this example, Position #25 has been saved.  I'm doing this through the GUI of the PTZoptics camera.  




Now, in OBS, lets create a scene


Now in "Sources" we'll create a new one


Select "BROWSER"


Give it a name and click OK

You'll see the following

In the "URL" section paste in the following code syntax, but with changes for your situation.

http://IPofCAMERA/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&PositionNumber

So as example, the camera I have uses IP 192.168.2.211 and we are using saved position # 25 so my code would look like 

      http://192.168.2.211/cgi-bin/ptzctrl.cgi?ptzcmd&poscall&25


Make sure you now check "Refresh browser when scene becomes active"

Then press "OK"

Now, add in the Camera input for the PTZoptics camera you are trying to control into the SOURCES window,  In my example the camera with IP of 192.168.2.211 is on Input #4

Now when you select the scene, your camera will move to that preset position #25 automatically.





Sunday, July 11, 2021

Ping a range of IPs from a windows command prompt

Need a way to ping a range of IPs from a windows command prompt?  

This command will ping every ip from 1-255 in the subnet of 192.168.2.X

for /l %i in (1,1,255) do ping -w 20 -n 1 192.168.2.%i | find /i "Reply"


EXAMPLE:  This command will ping, and you'll notice that .1, .3, .4 and .8 return pings

C:>for /l %i in (1,1,255) do ping -w 20 -n 1 192.168.2.%i | find /i "Reply"

C:>ping -w 20 -n 1 192.168.2.1   | find /i "Reply"

Reply from 192.168.2.1: bytes=32 time<1ms TTL=64

C:>ping -w 20 -n 1 192.168.2.2   | find /i "Reply"

C:>ping -w 20 -n 1 192.168.2.3   | find /i "Reply"

Reply from 192.168.2.3: bytes=32 time<1ms TTL=64

C:>ping -w 20 -n 1 192.168.2.4   | find /i "Reply"

Reply from 192.168.2.4: bytes=32 time<1ms TTL=64

C:>ping -w 20 -n 1 192.168.2.5   | find /i "Reply"

C:>ping -w 20 -n 1 192.168.2.6   | find /i "Reply"

C:>ping -w 20 -n 1 192.168.2.7   | find /i "Reply"

C:>ping -w 20 -n 1 192.168.2.8   | find /i "Reply"

Reply from 192.168.2.8: bytes=32 time<1ms TTL=64


Here's how the command works:

for /l %i in (1,1,255) do ping -w 20 -n 1 192.168.2.%i | find /i "Reply"

Do a loop of pings from 1, counting up 1 each time, until you reach 255, while waiting for only 20 ms and for only 1 response on the specified network of 192.168.0.XXX.

To specify From A to B is in the (1,1,255) translates to (A,X,B)/  the W indicates how many milliseconds to wait for a response before you continue.  I keep it low, usually since i'm pinging on a local lan, but if you find results aren't accurate, turn up the rate to 100 (or eliminate the -w 20 alltogether to wait longer.

A= starting point  B= Ending point X= Increment value (normally would be 1)


Range 1-100:

for /l %i in (1,1,100) do ping -w 20 -n 1 192.168.2.%i | find /i "Reply" 


Range 100-200:

for /l %i in (100,1,200) do ping -w 20 -n 1 192.168.2.%i | find /i "Reply"


Range 1 to 200, but ping every second value

for /l %i in (1,2,200) do ping -w 20 -n 1 192.168.2.%i | find /i "Reply"

Thursday, July 1, 2021

How to record Aux Inputs in your DAW on a X32

Hey you want to record the AUX inputs on your DAW in an X32

Good news!  You can do it!

Bad news, as of firmware 4.06 and X32 edit 4.6 in order to record them, you have to sacrifice 8 channels on your outputs to your DAW

But here's all you have to do

Goto "ROUTING"

Click on "CARD"

Select the OUTPUT group that you want to assign to your "AUX IN"


You'll see that I've assigned AUXIN to Outputs "25-32" on the "card"

Now in my DAW you'll see Aux input #2 is appearing on input #26 of my DAW software





Wednesday, June 30, 2021

Update Bitlocker recovery password

 CMD prompt (as administrator)

1>

First, get the current password ID (assuming C: drive)

manage-bde c: -protectors -get -type RecoveryPassword


System comes back with a value, in this example it is {C18DE14-177B-4BF2-AEC-798C7B888F5}  

2>

Select/Copy that key and include the parenthesis

3>

Now we delete the current password.  Enter the command below, then paste in the ID key you copied above.

manage-bde c: -protectors -delete -id {YourKeySelectedAbove}


System should come back with a “key protector with ID XXX deleted”

4>

Now create a new key.  This command will generate a new random key

manage-bde c: -protectors -add -rp


System has generated a new numerical password ID and recovery password.  

If on AD, this will update automatically.


Tuesday, June 29, 2021

Ubuntu Live "CD" creation

If you just want to run UBUNTU on a computer boot the downloaded image on a machine via a USB key, Ubutun will prompt if you want to "TRY IT" or "INSTALL IT".   By selecting "TRY IT" it will run from the USB key and let you see the features, doesn't touch the currently installed OS on the computer.

Download and Ubuntu flavor, this one uses Ubuntu Desktop 20.04.2.0 LTS

Now you will want to use an app to install this to a USB key.  I use "RUFUS"

With RUFUS, I've selected the "DEVICE" which is the USB-key in my system.  you'll need an 8 gig one.  This will delete everything on the USB key.
I"ve selected the Ubuntu ISO, and for my example, the partition scheme i've selected is MBR.


Now select START


Rufus application may prompt you about the type of image you want to write, I go with the recommended.


A warning just in case you don't already know that this will delete everything on the USB Key/devcie you selected.




Put the key into a computer and boot from it.
Select "TRY IT" and give it a go.

Wednesday, June 16, 2021

some Powershell commands not all working

 Ran into a problem when running some powershell commands, these were specifically for Microsoft Teams

I kept getting errors running this command

Get-CsOnlineTelephoneNumber -TelephoneNumber 15552129191

Get-CsOnlineSession : Connecting to remote server api.interfaces.records.teams.microsoft.com failed with the following error message : The WinRM client cannot process the request. Basic authentication is currently disabled in the client configuration. Change the client

configuration and try the request again. For more information, see the about_Remote_Troubleshooting Help topic.

At C:\Program Files\WindowsPowerShell\Modules\microsoftteams\2.3.1\net472\SfBORemotePowershellModule.psm1:63 char:22

+     $remoteSession = & (Get-CsOnlineSessionCommand)

+                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    + CategoryInfo          : NotSpecified: (:) [Get-CsOnlineSession], PSRemotingTransportException

    + FullyQualifiedErrorId : PSRemotingTransportException,Microsoft.Teams.ConfigApi.Cmdlets.GetCsOnlineSession

Despite authenticating to 365 (via powershell)

PS C:\WINDOWS\system32> install-module microsoftteams

PS C:\WINDOWS\system32> import-module microsoftteams

PS C:\WINDOWS\system32> connect-microsoftteams

Account                            Environment Tenant                               TenantId

-------                            ----------- ------                               --------

adminuser@companyinc.onmicrosoft.com AzureCloud  27151f1b-5172-41f3-a416-21146a11d221 1715cf1b-5571-41f3-a166-21146a1fd120

PS C:\WINDOWS\system32> Get-CsOnlineTelephoneNumber -TelephoneNumber 15552129191

ERROR

My coworker told me they had same problem and had to change a registry entry

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client

DWORD "AllowBasic" = 1



Was able to run successful commands that were failing prior

Thursday, April 15, 2021

PowerShell Scripts

 Get a list of running applications on windows 10

From Powershell

Get-Process | Where-Object { $_.MainWindowTitle } | Format-Table Name,Mainwindowtitle -AutoSize

or from windows CMD prompt

powershell "Get-Process | Where-Object { $_.MainWindowTitle } | Format-Table Name,Mainwindowtitle -AutoSize"

Tuesday, April 6, 2021

Asterisk Post Call Recording Script configuration

This code shows how to alter a file with the Post Call Recording script.  But in general shows a bit of how the process of making this asterisk feature work

I did worked on trying to get that AGENT ID variable via dialplan, but I just couldn't figure it out in Issabel. The Callcenter system in that is very tied in and I just wasn't able to figure it out. If someone else can show me, that would be fantastic.

I know that is value is stored in the QUEUE. LOG file as each call is sent to an agent. So using the "Post Call Recording Script", at the end of each call, I use a shell script that is automatically activated. This script searches the queue.log file for the unqiue callID and the value "COMPLETEAGENT". This indicates the end of a queue call. The shell parses the entry and pulls out the agent id, Then it renames the sound recording by adding on the AGENTXXXX ID.

So a call that went to queue Agent ID "9989", the recording changes from this:

rg-600-1000-20210331-213521-1617237321.105.wav

and becomes this:

rg-600-1000-20210331-213521-1617237321.105-Agent9989.wav

Here's how i did it:

You don't need any modifications to the Issabel diaplan. The only thing you have to do in Issabel is activate "Post Call Recording Script"

PBX -> "Advanced Settings"

HiddenMenu

You will need to activate "Display Readonly Settings"

and "Override Readonly Settings" Set them both to "TRUE" then click on the Green checkbox on the right side.

You wlll get prompted that you will need to "REFRESH THE PAGE" which you will have to.
Once both boxes are set to "TRUE" and you have pressed the checkbox, click on "ADVANCED SETTINGS" again to refresh.

Now scroll down and you will see

"Post Call Recording Script"

In that box put in this. The quotes around the path and script are required. Asterisk $variables are substituted with a "hat" symbol

HiddenMenu

"/etc/asterisk/updateagent.sh" ^{UNIQUEID} ^{MIXMONITOR_FILENAME}

This is the code that will run after the caller hangsup and the call recording is completed processing.
After each call, the "updateagent.sh" shell script is activated and it passes the UniqueID of the call as well as the current file location

Select the green check mark to the right of this box. Then press APPLY SETTINGS

Now we need to create a shell script.

In /etc/asterisk create a file called "updateagent.sh" (you can of course put this file in your preferential own directory, just make sure the path in the config in Issabel reflects it. You can also change the name.

In side this file you will want to copy in the following into that file and then save it.

#!/bin/bash

#This line finds the agent number in the queue_log file.  It then parses out any extra data to get the number value
#It will find thise 1617240353|1617234166.70|NONE|Agent/9989|AGENTLOGOFF|SIP/1000-00000028|6182 and finish with
#this "9989"
agentnumber=$(grep $1.*COMPLETEAGENT /var/log/asterisk/queue_log | awk -F "|" '{ print $4 }' | awk -F "/" '{ print $2 }')

#this line finds the file type you are using.  They are usually .WAV, but in case its something else we want to account for that
filetype=$(echo $2 | awk -F "." '{ print $3 }')

#This creates a new file name based on the AGENTID and the FileType you are using
newfilename=$(echo $2 | awk -F "." '{ print $1"."$2}')-Agent$agentnumber.$filetype

mv $2 $newfilename
#This rg-600-1000-20210331-213521-1617237321.105.wav
#becomes
#rg-600-1000-20210331-213521-1617237321.105-Agent-9989.wav

Now you will need to make the file executable

chmod u+x updateagent.sh

And now you need to change ownership to allow asterisk PBX to run it

chown asterisk:asterisk updateagent.sh

And that should be it. Make a test call and see if it works. If you have problems, just take out the code in "Post Call Recording Script" and will let you troubleshoot.

MODIFICATION TO PASS OTHER VARIABLES

You can pass additional variables into the script with two modifications

In the Call Record Script you can add more entries by adding them after the MixMonitor entry. Every variable that would be in asterisk needs to have the $ changed to a ? (hat) symbol. So if you wanted to pass $'CONTEXT' and $'EPOCH' data, you can add it. Very important to have "quotes" around the shellscript path. Many examples on internet forget to include this requirement

"/etc/asterisk/updateagent.sh" ^{UNIQUEID} ^{MIXMONITOR_FILENAME} ^{CONTEXT} ^{EPOCH}

THEN in the updateagent.sh script, you just need to alter the "newfilename" entry by adding a $3 and then a $4 like the example below. These would represent the 2 additional values.

newfilename=$(echo $2 | awk -F "." '{ print $1"."$2}')-Agent$agentnumber-$3-$4.$filetype

Save and you should be able to run the file. If the variables are passed correctly, you'll see them appear in the filename, something like this: (DialPlanVar1 and Var2)

rg-600-1000-20210331-230350-1617242630.111-Agent9989-DialPlanVar1-DialPlanVar2.wav

If the data is NOT available, they will just look like this: (you'll see the - that are delimiters)

rg-600-1000-20210331-230350-1617242630.111-Agent--.wav

    Monday, April 5, 2021

    Check GMAIL with a bashscript

    The code below will check a GMAIL account and if it receives a new email, it will trigger the Issabel PBX to make a phone call. This might be useful to some people. This code will check the GMAIL inbox for email.

    I setup a GMAIL account, but I had to turn down the security

    in the Gmail account to let a shell script access it. This script doesn't have much intelligence, so it can't really tell the difference between an alarm email or a regular email, so use a dedicated GMAIL account.

    gmailsecurity

    Now create a shell script called " check_email.sh " on the PBX


    #!/bin/bash username="Gmail-Name" password="Gmail-Password"

    curl -u $username:$password --silent "https://mail.google.com/mail/feed/atom" > emailresult
    
    cat emailresult | grep -oPm1 "(?<=<title>)[^<]+" | sed '1d' > title
    cat emailresult | grep -oPm1 "(?<=<modified>)[^<]+" | sed '1d' >  date
    sed -e 's/^/|/' -i date
    paste title date > merge
    
    diff merge lastmerge
    if [ $? -ne 0 ]; then
        echo "Channel: IAX2/T28_Y02_Y07/5551234" > /etc/asterisk/alarm.call
        echo "MaxRetries: 2" >> /etc/asterisk/alarm.call
        echo "RetryTime: 60" >> /etc/asterisk/alarm.call
        echo "WaitTime: 30" >> /etc/asterisk/alarm.call
        echo "application: Playback" >> /etc/asterisk/alarm.call
        echo "data: /path/to/sound/file.wav" >> /etc/asterisk/alarm.call
        chmod 777 /etc/asterisk/alarm.call
        chown asterisk:asterisk /etc/asterisk/alarm.call
        mv /etc/asterisk/alarm.call /var/spool/asterisk/outgoing/
    
    else
        echo "No New Email"
    
    fi
    
    mv merge -f lastmerge

    change "IAX2/T28_Y02_Y07/5551234" to be the outbound trunk and the phone number you want the system to call.
    

    data: /path/to/sound/file.wav - this is the recording you want the system to play when the person answers. "THIS IS AN ALARM, PLEASE CHECK SYSTEM"

    Make the file executable

    chmod u+x check_email.sh

    Now run the file

     ./check_email.sh

    The system

    checks GMAIL It looks at the list of emails in the

    INBOX and compares it to the list that was there previous If the list is the same, it doesn't do anything. If its different, it will generate a call file and ring the number in the script.

    Now create a CRON job and run this over a period of time. I wouldn't do it more than eleven every 5 mins, I'm not sure if GMAIL will throttle you or not

    The script finds the emails and the dates of each email, merges them together. Perhaps someone more proficient at parsing HTML code can make it do more. I might work on it more myself later. But something to try.

    The "GREP" statement I got from https://linuxconfig.org/check-your-gmail-inbox-for-new-emails-with-bash-script, that site might give you more information how to make things work.

    Friday, March 19, 2021

    Autohotkey FTP code (reference)

    This is reference location.   https://www.autohotkey.com/boards/viewtopic.php?t=79142

    Written by user jNizM (github )

    Mirror: Release on GitHub

    Code: Select all - Collapse View - Toggle Line numbers

    ; ===============================================================================================================================
    ; AutoHotkey wrapper for FTP Sessions
    ;
    ; Author ....: jNizM
    ; Released ..: 2020-07-26
    ; Modified ..: 2020-07-31
    ; Github ....: https://github.com/jNizM/Class_FTP
    ; Forum .....: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=79142
    ; ===============================================================================================================================
    
    
    class FTP
    {
    
    	static hWININET := DllCall("LoadLibrary", "str", "wininet.dll", "ptr")
    
    
    	; ===== PUBLIC METHODS ======================================================================================================
    
    	Close(hInternet)
    	{
    		if (hInternet)
    			if (this.InternetCloseHandle(hInternet))
    				return true
    		return false
    	}
    
    
    	Connect(hInternet, ServerName, Port := 21, UserName := "", Password := "", FTP_PASV := 1)
    	{
    		if (hConnect := this.InternetConnect(hInternet, ServerName, Port, UserName, Password, FTP_PASV))
    			return hConnect
    		return false
    	}
    
    
    	CreateDirectory(hConnect, Directory)
    	{
    		if (DllCall("wininet\FtpCreateDirectory", "ptr", hConnect, "ptr", &Directory))
    			return true
    		return false
    	}
    
    
    	DeleteFile(hConnect, FileName)
    	{
    		if (DllCall("wininet\FtpDeleteFile", "ptr", hConnect, "ptr", &FileName))
    			return true
    		return false
    	}
    
    
    	Disconnect(hConnect)
    	{
    		if (hConnect)
    			if (this.InternetCloseHandle(hConnect))
    				return true
    		return false
    	}
    
    
    	FindFiles(hConnect, SearchFile := "*.*")
    	{
    		static FILE_ATTRIBUTE_DIRECTORY := 0x10
    
    		Files := []
    		find := this.FindFirstFile(hConnect, hEnum, SearchFile)
    		if !(find.FileAttr & FILE_ATTRIBUTE_DIRECTORY)
    			Files.Push(find)
    
    		while (find := this.FindNextFile(hEnum))
    			if !(find.FileAttr & FILE_ATTRIBUTE_DIRECTORY)
    				Files.Push(find)
    		this.Close(hEnum)
    		return Files
    	}
    
    
    	FindFolders(hConnect, SubDirectories := "*.*")
    	{
    		static FILE_ATTRIBUTE_DIRECTORY := 0x10
    
    		Folders := []
    		find := this.FindFirstFile(hConnect, hEnum, SubDirectories)
    		if (find.FileAttr & FILE_ATTRIBUTE_DIRECTORY)
    			Folders.Push(find)
    		while (find := this.FindNextFile(hEnum))
    			if (find.FileAttr & FILE_ATTRIBUTE_DIRECTORY)
    				Folders.Push(find)
    		this.Close(hEnum)
    		return Folders
    	}
    
    
    	GetCurrentDirectory(hConnect)
    	{
    		static MAX_PATH := 260 + 8
    
    		BUFFER_SIZE := VarSetCapacity(CurrentDirectory, MAX_PATH, 0)
    		if (DllCall("wininet\FtpGetCurrentDirectory", "ptr", hConnect, "ptr", &CurrentDirectory, "uint*", BUFFER_SIZE))
    			return StrGet(&CurrentDirectory)
    		return false
    	}
    
    
    	GetFile(hConnect, RemoteFile, NewFile, OverWrite := 0, Flags := 0)
    	{
    		if (DllCall("wininet\FtpGetFile", "ptr", hConnect, "ptr", &RemoteFile, "ptr", &NewFile, "int", !OverWrite, "uint", 0, "uint", Flags, "uptr", 0))
    			return true
    		return false
    	}
    
    
    	GetFileSize(hConnect, FileName, SizeFormat := "auto", SizeSuffix := false)
    	{
    		static GENERIC_READ := 0x80000000
    
    		if (hFile := this.OpenFile(hConnect, FileName, GENERIC_READ))
    		{
    			VarSetCapacity(FileSizeHigh, 8)
    			if (FileSizeLow := DllCall("wininet\FtpGetFileSize", "ptr", hFile, "uint*", FileSizeHigh, "uint"))
    			{
    				this.InternetCloseHandle(hFile)
    				return this.FormatBytes(FileSizeLow + (FileSizeHigh << 32), SizeFormat, SizeSuffix)
    			}
    			this.InternetCloseHandle(hFile)
    		}
    		return false
    	}
    
    
    	Open(Agent, Proxy := "", ProxyBypass := "")
    	{
    		if (hInternet := this.InternetOpen(Agent, Proxy, ProxyBypass))
    			return hInternet
    		return false
    	}
    
    
    	PutFile(hConnect, LocaleFile, RemoteFile, Flags := 0)
    	{
    		if (DllCall("wininet\FtpPutFile", "ptr", hConnect, "ptr", &LocaleFile, "ptr", &RemoteFile, "uint", Flags, "uptr", 0))
    			return true
    		return false
    	}
    
    
    	RemoveDirectory(hConnect, Directory)
    	{
    		if (DllCall("wininet\FtpRemoveDirectory", "ptr", hConnect, "ptr", &Directory))
    			return true
    		return false
    	}
    
    
    	RenameFile(hConnect, ExistingFile, NewFile)
    	{
    		if (DllCall("wininet\FtpRenameFile", "ptr", hConnect, "ptr", &ExistingFile, "ptr", &NewFile))
    			return true
    		return false
    	}
    
    
    	SetCurrentDirectory(hconnect, Directory)
    	{
    		if (DllCall("wininet\FtpSetCurrentDirectory", "ptr", hConnect, "ptr", &Directory))
    			return true
    		return false
    	}
    
    
    	; ===== PRIVATE METHODS =====================================================================================================
    
    	FileAttributes(Attributes)
    	{
    		static FILE_ATTRIBUTE := { 0x1: "READONLY", 0x2: "HIDDEN", 0x4: "SYSTEM", 0x10: "DIRECTORY", 0x20: "ARCHIVE", 0x40: "DEVICE", 0x80: "NORMAL"
    						, 0x100: "TEMPORARY", 0x200: "SPARSE_FILE", 0x400: "REPARSE_POINT", 0x800: "COMPRESSED", 0x1000: "OFFLINE"
    						, 0x2000: "NOT_CONTENT_INDEXED", 0x4000: "ENCRYPTED", 0x8000: "INTEGRITY_STREAM", 0x10000: "VIRTUAL"
    						, 0x20000: "NO_SCRUB_DATA", 0x40000: "RECALL_ON_OPEN", 0x400000: "RECALL_ON_DATA_ACCESS" }
    		GetFileAttributes := []
    		for k, v in FILE_ATTRIBUTE
    			if (k & Attributes)
    				GetFileAttributes.Push(v)
    		return GetFileAttributes
    	}
    
    
    	FindData(ByRef WIN32_FIND_DATA, SizeFormat := "auto", SizeSuffix := false)
    	{
    		static MAX_PATH := 260
    		static MAXDWORD := 0xffffffff
    
    		addr := &WIN32_FIND_DATA
    		FIND_DATA := []
    		FIND_DATA["FileAttr"]          := NumGet(addr + 0, "uint")
    		FIND_DATA["FileAttributes"]    := this.FileAttributes(NumGet(addr + 0, "uint"))
    		FIND_DATA["CreationTime"]      := this.FileTime(NumGet(addr +  4, "uint64"))
    		FIND_DATA["LastAccessTime"]    := this.FileTime(NumGet(addr + 12, "uint64"))
    		FIND_DATA["LastWriteTime"]     := this.FileTime(NumGet(addr + 20, "uint64"))
    		FIND_DATA["FileSize"]          := this.FormatBytes((NumGet(addr + 28, "uint") * (MAXDWORD + 1)) + NumGet(addr + 32, "uint"), SizeFormat, SizeSuffix)
    		FIND_DATA["FileName"]          := StrGet(addr + 44, "utf-16")
    		FIND_DATA["AlternateFileName"] := StrGet(addr + 44 + MAX_PATH * (A_IsUnicode ? 2 : 1), "utf-16")
    		return FIND_DATA
    	}
    
    
    	FindFirstFile(hConnect, ByRef hFind, SearchFile := "*.*", SizeFormat := "auto", SizeSuffix := false)
    	{
    		VarSetCapacity(WIN32_FIND_DATA, (A_IsUnicode ? 592 : 320), 0)
    		if (hFind := DllCall("wininet\FtpFindFirstFile", "ptr", hConnect, "str", SearchFile, "ptr", &WIN32_FIND_DATA, "uint", 0, "uint*", 0))
    			return this.FindData(WIN32_FIND_DATA, SizeFormat, SizeSuffix)
    		VarSetCapacity(WIN32_FIND_DATA, 0)
    		return false
    	}
    
    
    	FindNextFile(hFind, SearchFile := "*.*", SizeFormat := "auto", SizeSuffix := false)
    	{
    		VarSetCapacity(WIN32_FIND_DATA, (A_IsUnicode ? 592 : 320), 0)
    		if (DllCall("wininet\InternetFindNextFile", "ptr", hFind, "ptr", &WIN32_FIND_DATA))
    			return this.FindData(WIN32_FIND_DATA, SizeFormat, SizeSuffix)
    		VarSetCapacity(WIN32_FIND_DATA, 0)
    		return false
    	}
    
    
    	FileTime(addr)
    	{
    		this.FileTimeToSystemTime(addr, SystemTime)
    		this.SystemTimeToTzSpecificLocalTime(&SystemTime, LocalTime)
    		return Format("{:04}{:02}{:02}{:02}{:02}{:02}"
    					, NumGet(LocalTime,  0, "ushort")
    					, NumGet(LocalTime,  2, "ushort")
    					, NumGet(LocalTime,  6, "ushort")
    					, NumGet(LocalTime,  8, "ushort")
    					, NumGet(LocalTime, 10, "ushort")
    					, NumGet(LocalTime, 12, "ushort"))
    	}
    
    
    	FileTimeToSystemTime(FileTime, ByRef SystemTime)
    	{
    		VarSetCapacity(SystemTime, 16, 0)
    		if (DllCall("FileTimeToSystemTime", "int64*", FileTime, "ptr", &SystemTime))
    			return true
    		return false
    	}
    
    
    	FormatBytes(bytes, SizeFormat := "auto", suffix := false)
    	{
    		static SFBS_FLAGS_ROUND_TO_NEAREST_DISPLAYED_DIGIT    := 0x0001
    		static SFBS_FLAGS_TRUNCATE_UNDISPLAYED_DECIMAL_DIGITS := 0x0002
    		static S_OK := 0
    
    		if (SizeFormat = "auto")
    		{
    			size := VarSetCapacity(buf, 1024, 0)
    			if (DllCall("shlwapi\StrFormatByteSizeEx", "int64", bytes, "int", SFBS_FLAGS_ROUND_TO_NEAREST_DISPLAYED_DIGIT, "str", buf, "uint", size) = S_OK)
    				output := buf
    		}
    		else if (SizeFormat = "kilobytes" || SizeFormat = "kb")
    			output := Round(bytes / 1024, 2) . (suffix ? " KB" : "")
    		else if (SizeFormat = "megabytes" || SizeFormat = "mb")
    			output := Round(bytes / 1024**2, 2) . (suffix ? " MB" : "")
    		else if (SizeFormat = "gigabytes" || SizeFormat = "gb")
    			output := Round(bytes / 1024**3, 2) . (suffix ? " GB" : "")
    		else if (SizeFormat = "terabytes" || SizeFormat = "tb")
    			output := Round(bytes / 1024**4, 2) . (suffix ? " TB" : "")
    		else
    			output := Round(bytes, 2) . (suffix ? " Bytes" : "")
    		return output
    	}
    
    
    	InternetCloseHandle(hInternet)
    	{
    		if (DllCall("wininet\InternetCloseHandle", "ptr", hInternet))
    			return true
    		return false
    	}
    
    
    	InternetConnect(hInternet, ServerName, Port := 21, UserName := "", Password := "", FTP_PASV := 1)
    	{
    		static INTERNET_DEFAULT_FTP_PORT := 21
    		static INTERNET_SERVICE_FTP      := 1
    		static INTERNET_FLAG_PASSIVE     := 0x08000000
    
    		if (hConnect := DllCall("wininet\InternetConnect", "ptr",    hInternet
    														 , "ptr",    &ServerName
    														 , "ushort", (Port = 21 ? INTERNET_DEFAULT_FTP_PORT : Port)
    														 , "ptr",    (UserName ? &UserName : 0)
    														 , "ptr",    (Password ? &Password : 0)
    														 , "uint",   INTERNET_SERVICE_FTP
    														 , "uint",   (FTP_PASV ? INTERNET_FLAG_PASSIVE : 0)
    														 , "uptr",   0
    														 , "ptr"))
    			return hConnect
    		return false
    	}
    
    
    	InternetOpen(Agent, Proxy := "", ProxyBypass := "")
    	{
    		static INTERNET_OPEN_TYPE_DIRECT := 1
    		static INTERNET_OPEN_TYPE_PROXY  := 3
    
    		if (hInternet := DllCall("wininet\InternetOpen", "ptr",  &Agent
    													   , "uint", (Proxy ? INTERNET_OPEN_TYPE_PROXY : INTERNET_OPEN_TYPE_DIRECT)
    													   , "ptr",  (Proxy ? &Proxy : 0)
    													   , "ptr",  (ProxyBypass ? &ProxyBypass : 0)
    													   , "uint", 0
    													   , "ptr"))
    			return hInternet
    		return false
    	}
    
    
    	OpenFile(hConnect, FileName, Access)
    	{
    		static FTP_TRANSFER_TYPE_BINARY := 2
    
    		if (hFTPSESSION := DllCall("wininet\FtpOpenFile", "ptr", hConnect, "ptr", &FileName, "uint", Access, "uint", FTP_TRANSFER_TYPE_BINARY, "uptr", 0))
    			return hFTPSESSION
    		return false
    	}
    
    
    	SystemTimeToTzSpecificLocalTime(SystemTime, ByRef LocalTime)
    	{
    		VarSetCapacity(LocalTime, 16, 0)
    		if (DllCall("SystemTimeToTzSpecificLocalTime", "ptr", 0, "ptr", SystemTime, "ptr", &LocalTime))
    			return true
    		return false
    	}
    
    }
    
    ; ===============================================================================================================================

    Then us this code to run it

    Writes a file to the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    FTP.PutFile(hSession, "C:\Temp\testfile.txt", "testfile.txt")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Retrieves a file from the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    FTP.GetFile(hSession, "testfile.txt", "C:\Temp\testfile.txt")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Retrieves the file size of the requested FTP resource.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    MsgBox % FTP.GetFileSize(hSession, "testfile.txt")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Deletes a file from the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    FTP.DeleteFile(hSession, "testfile.txt")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Creates a new directory on the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    FTP.CreateDirectory(hSession, "Test_Folder")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Changes the client's current directory on the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    FTP.SetCurrentDirectory(hSession, "Test_Folder")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Returns the client's current directory on the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    MsgBox % FTP.GetCurrentDirectory(hSession)
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Deletes a directory on the server.

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    FTP.RemoveDirectory(hSession, "Test_Folder")
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Enumerate all Files in root directory. (!!! EXPERIMENTAL !!!)

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    for k, File in FTP.FindFiles(hSession)
    	MsgBox % File.FileName
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Enumerate all Files in a subdirectory. (!!! EXPERIMENTAL !!!)

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    for k, File in FTP.FindFiles(hSession, "/Folder 2")
    	MsgBox % File.FileName
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Enumerate all Folders in root directory. (!!! EXPERIMENTAL !!!)

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    for k, Folder in FTP.FindFolders(hSession)
    	MsgBox % Folder.FileName
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)

    Enumerate all Folders in a subdirectory. (!!! EXPERIMENTAL !!!)

    Code: Select all - Toggle Line numbers

    hFTP := FTP.Open("AHK-FTP")
    hSession := FTP.Connect(hFTP, "ftp.example.com", 21, "user", "passwd")
    for k, Folder in FTP.FindFolders(hSession, "/Folder 2")
    	MsgBox % Folder.FileName
    FTP.Disconnect(hSession)
    FTP.Close(hFTP)