
Update: This class file has been replaced with the Yukon Database Control Library and migrated to github. Please click here and check it out, thanks!
Random records and ruminations by Damon V. Caskey.

Update: This class file has been replaced with the Yukon Database Control Library and migrated to github. Please click here and check it out, thanks!
<?php
/*
Configuration file. This should be added to all PHP scripts to set up commonly used includes, functions, objects, variables and so on.
*/
$iReqTime = $_SERVER['REQUEST_TIME_FLOAT'];
$cDocroot = NULL; //Document root.
$oDB = NULL; //Database class object.
$oErr = NULL; //Error class object.
$oMail = NULL; //E-Mail handler class object.
$oSes = NULL; //Session handler class object.
$oUtl = NULL; //Utility class object.
$oFrm = NULL; //Forms class object.
/* Get needed includes. */
require_once("access.php"); //Account based access.
require_once("constants.php"); //Global constants.
require_once("database.php"); //Database handler.
require_once("forms.php"); //Forms handler.
require_once("error.php"); //Error handler.
require_once("mail.php"); //Mail handler.
require_once("sessions.php"); //Session handler.
require_once("tables.php"); //Table handler.
require_once("utility.php"); //Utility functions.
/* Initialize class objects */
$oUtl = new class_utility(); //Utility functions.
$oMail = new class_mail(); //E-Mail handler.
$oErr = new class_error(array("Mail" => $oMail, "Utl" => $oUtl)); //Error handler.
$oDB = new class_db(array("Utl" => $oUtl, "Err" => $oErr)); //Database handler.
$oSes = new class_sessions(array("DB" => $oDB)); //Session handler.
$oAcc = new class_access(array("DB" => $oDB, "Ses" => $oSes, "Utl" => $oUtl)); //Account based access.
$oTbl = new class_tables(array("Utl"=>$oUtl)); //Tables handler.
$oFrm = new class_forms(array("DB"=>$oDB)); //Forms handler.
$cDocroot = $oUtl->utl_get_server_value('DOCUMENT_ROOT')."/";
/* Replace default session handler. */
session_set_save_handler($oSes, true);
/* If session not started, initialize. */
if(session_status()==PHP_SESSION_NONE)
{
session_start();
}
?>
Enables and executes jump control identical to Mario Brothers style platform games (i.e. length of Jump key press controls jumping height). Also allows optional scripted X and Z movement mid jump with directional key press.
Replaced with Hansburg Library.
This is the license agreement for my source code and other distributions (don’t worry, you’re allowed to use it in your own products, commercial or otherwise).
Note: If you need a non-attribution license for any of my code (for a fee), please see the Using without attribution section below.
The license text is further down this page, and you should only download and use the source code if you agree to the terms in that text. For convenience, though, I’ve put together a human-readable (as opposed to lawyer-readable) non-authoritative interpretation of the license which will hopefully answer any questions you have. Basically, the license says that:
This license is really just a version of the new BSD license, and should be compatible with that license. If you’d like special permission to use any of my code under another license (like BSD, GPL, etc.), just contact me – I’ll be happy to help.
My source code license agreement requires attribution, but I’m aware that some people need a license agreement which does not require attribution. If you’re in that situation, please feel free to contact me and we can make arrangements.
The license requires that you give credit to me, Damon Caskey, as the original author of any of the code that you use. The placement and format of the credit is up to you, but I prefer the credit to be in the software’s “About” window. Alternatively, you could put the credit in the software’s documentation, or on the web page for the product. The suggested format for the attribution is:
Includes “Name of Code” code by Damon Caskey.
where “Name of Code” would obviously be replaced by the name of the specific source-code package you made use of. Where possible, please link the text “Damon Caskey” to the main page of this website, or include the site’s URL (https://www.caskeys.com/dc).
License Agreement for Source Code provided by Damon Caskey
This software is supplied to you by Damon Caskey in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this software.
In consideration of your agreement to abide by the following terms, and subject to these terms, Damon Caskey grants you a personal, non-exclusive license, to use, reproduce, modify and redistribute the software, with or without modifications, in source and/or binary forms; provided that if you redistribute the software in its entirety and without modifications, you must retain a copy of or link to this notice and the following text and disclaimers in all such redistributions of the software, and that in all cases attribution of Damon Caskey as the original author of the source code shall be included in all such resulting software products or distributions. Neither the name, trademarks, service marks or logos of Damon Caskey may be used to endorse or promote products derived from the software without specific prior written permission from Damon Caskey. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Damon Caskey herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the software may be incorporated.
The software is provided by Damon Caskey on an “AS IS” basis. Damon Caskey MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL Damon Caskey BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF Damon Caskey HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
If you’ve got any questions about the license, or if you want to discuss any special requirements or licensing, feel free get in touch – you can find my contact details here.
Enjoy using the source!
DC
E-mailing is one of the more common and versatile ways for webmasters and their customers to keep abreast of events. Naturally databases and logs are best practice, but as a quick and dirty way to get form submissions, error alerts and so on E-mail is tough to beat. Even with an established database E-mail still has its place as an alert mechanism.
Given the above and the ubiquitous presence of E-mail in general, it only makes sense to simplify the process of sending as much as possible. To that end, this PHP class wraps the PHP Mail function to extend capability in such a way that oft repeated coding and formatting is eliminated. Specific goals are:
Copy the following into your class file.
class class_mail
{
/*
class_mail - https://www.caskeys.com/dc/?p=5031
Damon Vaughn Caskey
2012_12_10
Mail handler.
*/
const c_bWMAlert = TRUE; //Send webmaster a blind copy?
const c_cEDefMsg = "..."; //Default message.
const c_cEHead = "MIME-Version: 1.0 \r\nContent-type: text/html; charset=iso-8859-1\r\n"; //Default E-mail headers.
const c_cESubject = "From EHS Web"; //Default outgoing E-mail subject.
const c_cEWMIn = "dvcask2@uky.edu"; //Default webmaster's incoming E-mail address.
const c_cEWMOut = "ehs_noreply@uky.edu"; //Default address when server sends mail.
public function mail_send($cMsg=self::c_cEDefMsg, $cSubject=self::c_cESubject, $cTo=self::c_cEWMIn, $cFrom=self::c_cEWMOut, $cBcc=NULL, $bWMAlert=self::c_bWMAlert, $cHeader=self::c_cEHead, $cParams=NULL)
{
/*
mail_send
Damon Vaughn Caskey
2012_12_28
Send HTML mail with standard defaults.
$cMsg: Body of E-mail.
$cSubject: Subject line.
$cTo: Outgoing address list.
$cFrom: Return address.
$cBcc: Blind carbon copy address list.
$bWMAlert: Send Bcc to webmaster.
$cHeader: Header information.
$cParams: Optional parameters.
*/
$cBody = NULL; //Final sting for message body.
/*
Insert From address to header.
*/
$cHeader .= "From: ".$cFrom. "\r\n";
/*
If Webmaster alert is on, insert address into Bcc and add to header. Otherwise just add Bcc to header as is.
*/
if($bWMAlert===TRUE)
{
$cHeader .= "Bcc: ".self::c_cEWMIn. ", ".$cBcc."\r\n";
}
else
{
$cHeader .= "Bcc: ".$cBcc."\r\n";
}
$cHeader .="\r\n";
/*
If message passed as a key array, break into list and output as table layout.
*/
if (is_array($cMsg))
{
/*
Initial html and table markup.
*/
$cBody = "<html>
<head>
<title>".$cSubject."</title>
</head>
<body>
<h1>".$cSubject."</h1>
<table cellpadding='3'>";
/*
Get each item in array and place into two column table row.
*/
foreach($cMsg as $key => $value)
{
$cBody .= "<tr><th>".$key.":</th><td>".$value."</td></tr>";
}
/*
Add closing markup.
*/
$cBody .= "</table>
</body>
</html>";
}
else
{
/*
Output message as is.
*/
$cBody = $cMsg;
}
/*
Run mail function.
*/
return mail($cTo, $cSubject, $cBody, $cHeader, $cParams);
}
}
Now include the file in your script and declare a class object. You are ready to go. Personally I prefer to place common class declarations in a single config file and use injected dependency to avoid multiple objects.
require_once('mail.php'); //Path to mail class.
$oMail = NULL; //Initialize class object.
$oMail = new class_mail(); //Class object.
Adjust the constant settings to suit your needs.
const c_bWMAlert = TRUE; //Send webmaster a blind copy? const c_cEDefMsg = "..."; //Default message. const c_cEHead = "MIME-Version: 1.0 \r\nContent-type: text/html; charset=iso-8859-1\r\n"; //Default E-mail headers. const c_cESubject = "From EHS Web"; //Default outgoing E-mail subject. const c_cEWMIn = "dvcask2@uky.edu"; //Default webmaster's incoming E-mail address. const c_cEWMOut = "ehs_noreply@uky.edu"; //Default address when server sends mail.
Primary (and as of this writing the only) function. Sends an E-mail; includes an optional tabled layout.
public function mail_send($cMsg=self::c_cEDefMsg, $cSubject=self::c_cESubject, $cTo=self::c_cEWMIn, $cFrom=self::c_cEWMOut, $cBcc=NULL, $bWMAlert=self::c_bWMAlert, $cHeader=self::c_cEHead, $cParams=NULL)
{
/*
mail_send
Damon Vaughn Caskey
2012_12_28
Send HTML mail with standard defaults.
$cMsg: Body of E-mail.
$cSubject: Subject line.
$cTo: Outgoing address list.
$cFrom: Return address.
$cBcc: Blind carbon copy address list.
$bWMAlert: Send Bcc to webmaster.
$cHeader: Header information.
$cParams: Optional parameters.
*/
$cBody = NULL; //Final sting for message body.
/*
Insert From address to header.
*/
$cHeader .= "From: ".$cFrom. "\r\n";
/*
If Webmaster alert is on, insert address into Bcc and add to header. Otherwise just add Bcc to header as is.
*/
if($bWMAlert===TRUE)
{
$cHeader .= "Bcc: ".self::c_cEWMIn. ", ".$cBcc."\r\n";
}
else
{
$cHeader .= "Bcc: ".$cBcc."\r\n";
}
$cHeader .="\r\n";
/*
If message passed as a key array, break into list and output as table layout.
*/
if (is_array($cMsg))
{
/*
Initial html and table markup.
*/
$cBody = "<html>
<head>
<title>".$cSubject."</title>
</head>
<body>
<h1>".$cSubject."</h1>
<table cellpadding='3'>";
/*
Get each item in array and place into two column table row.
*/
foreach($cMsg as $key => $value)
{
$cBody .= "<tr><th>".$key.":</th><td>".$value."</td></tr>";
}
/*
Add closing markup.
*/
$cBody .= "</table>
</body>
</html>";
}
else
{
/*
Output message as is.
*/
$cBody = $cMsg;
}
/*
Run mail function.
*/
return mail($cTo, $cSubject, $cBody, $cHeader, $cParams);
}
Unless noted, parameters are not required. Class defaults will be used for any missing.
This call will send a common E-mail to the designated recipients with the automatic webmaster Bcc turned off:
$cMsg = "Hello world!"; $cSubject = "Hi"; $cTo = "someone@somewhere.com, someoneelse@somewhereelse.com"; $cFrom = "me@myplace.com"; $oMail->mail_send($cMsg, $cSubject, $cTo, $cFrom, NULL, FALSE);
A common task for E-mail is to document errors, alerts, form submissions and other types of keyed data that is far easier to read in table format. It’s certainly possible to do this by manually coding table markup into the message, but also repetitive and time consuming.
Instead, you can pass the message as a keyed array; a simple but elegant table from your data will be created and sent to the recipients. The E-mail’s subject will be added as an attention grabbing header just above the table.
$cMsg = NULL; $cMsg = array( "Time" => $this->cErrTOE, "Type" => $this->cErrType, "IP" => $this->cIP, "Def. Source File" => $this->cSource, "Source File" => $this->cErrFile, "Line" => $this->cErrLine, "State" => $this->cErrState, "Code" => $this->cErrCode, "Message" => $this->cErrMsg, "Details" => $this->cErrDetail ); $this->oMail->mail_send($cMsg, "Error Report");
This set of global constants is a dependency for nearly all other functions and classes I have created with PHP. For obvious security reasons some of the actual values may be removed.
class constants
{
/*
constants
Damon Vaughn Caskey
2012_12_18
~2012_12_30: Local constants moved back to individual class files.
Global constants.
*/
/* Basic values */
const c_cAdmin = ""; //Default "all access" user accounts.
const c_cDateF = "Y-m-d H:i:s"; //Default date format.
const c_cWMName = "Damon V. Caskey"; //Default webmaster's name.
const c_cWMEmailIn = "dvcask2@uky.edu"; //Default webmaster's incoming email address.
const c_cVFGuid = "00000000-0000-0000-0000-000000000000"; //False guid.
/* Media & Icons */
const c_cIconExcel = "<img src='/media/image/icon_excel.png' alt='MS Excel document.' title='MS Excel document.' class='iconS'/>"; //MS Excel icon.
const c_cIconPDF = "<img src='/media/image/icon_pdf.png' alt='PDF document.' title='PDF document.' class='iconS' />"; //PDF icon.
const c_cIconPPT = "<img src='/media/image/icon_powerpoint.png' alt='MS Powerpoint document.' title='MS Powerpoint document.' class='iconS' />"; //MS Powerpoint icon.
const c_cIconWord = "<img src='/media/image/icon_word.png' alt='MS Word document.' title='MS Word document.' class='iconS' />"; //MS Word icon.
const c_cPathImage = "/media/image/"; //Default path for image files.
}

This PHP class file is designed to take care of day to day database operations for the rare but not unheard of combination of PHP with IIS and MSSQL.
I’m not a fan of reinventing the wheel, but after extensive searching had been unable to find any existing classes that suited the needs of a WIMP combo. Not much of a surprise considering the majority of PHP use falls under the LAMP model, but it is certainly frustrating.
I hope anyone in a similar situation can find this of use. The goals are simple and straight forward:
The following values are used from global constants list.
class constants
{
/*
constants
Damon Vaughn Caskey
2012_12_18
Global constants.
*/
/* Basic values */
const c_cAdmin = ""; //Default "all access" user accounts.
const c_cDateF = "Y-m-d H:i:s"; //Default date format.
const c_cWMName = "Damon V. Caskey"; //Default webmaster name.
const c_cWMEmailIn = ""; //Default webmaster email address (sending mail to webmaster)
const c_cWMEmailOut = ""; //Default address when server sends mail.
const c_cVFGuid = "00000000-0000-0000-0000-000000000000"; //False guid.
/* Database */
const c_cDBHost = ""; //Default DB Host.
const c_cDBLName = ""; //Default DB logical name.
const c_cDBUser = ""; //Default DB user.
const c_cDBPword = ""; //Default DB password.
const c_cDBEHost = ""; //Error log DB host.
const c_cDBELName = ""; //Error log DB logical name.
const c_cDBEUser = ""; //Error log DB user.
const c_cEDBPword = ""; //Error log DB password.
const c_cDBEEmailSub = "Database Failure"; //Error email subject line.
const c_iDBFetchType = SQLSRV_FETCH_BOTH; //Default row array fetch type.
/* Media & Icons */
const c_cIconExcel = ""; //MS Excel icon.
const c_cIconPDF = ""; //PDF icon.
const c_cIconPPT = ""; //MS Powerpoint icon.
const c_cIconWord = ""; //MS Word icon.
const c_cPathImage = ""; //Default path for image files.
}
Next, you will need to initialize the database class in whatever script you would like to use it in. This is fairly simple:
public $cDBLine = NULL; //Line array. public $cDBFMeta = NULL; //Line metadata array. public $iDBFCount = NULL; //Line field count. public $iDBRowCount = NULL; //Table row count. public $iDBRowsAffect = NULL; //Number of rows affected by an action query. public $rDBConn = NULL; //Database connection ID resource. public $rDBResult = NULL; //Database query result ID resource. public $rDBStatement = NULL; //Database prepared query statement ID resource.
The following public variables are made available upon initialization. These are set by the various function calls and will be the main means of actually extracting values returned from your database.
function db_basic_action($cQuery, $cParams, $cHost = constants::c_cDBHost, $cDB = constants::c_cDBLName, $cUser = constants::c_cDBUser, $cPword = constants::c_cDBPword)
{
/*
db_basic_action
Damon Vaughn Caskey
2012_11_13
Connect and execute an action query with single call.
$cQuery: SQL string.
$cParams: Parameter array.
$cHost: DB Host.
$cDB: DB logical name.
$cUser: DB user.
$cPword: DB password.
*/
/* Connect to DB */
$this->db_connect($cHost, $cDB, $cUser, $cPword);
/* Execute query. */
$this->db_query($cQuery, $cParams);
/* Set rows affected. */
$this->iDBRowsAffect = sqlsrv_rows_affected($this->rDBResult);
return $this->iDBRowsAffect;
}
This function is designed to run simple action queries with a single call. There is no need to run a separate call to connect, prepare, and query execute. Simply pass the query, parameters and (if necessary) connection values.
Sets and returns iDBRowsAffect.
function db_basic_select($cQuery, $cParams, $iLine=FALSE, $cHost = constants::c_cDBHost, $cDB = constants::c_cDBLName, $cUser = constants::c_cDBUser, $cPword = constants::c_cDBPword)
{
/*
db_basic_select
Damon Vaughn Caskey
2012_11_13
Connect, query and populate common variables from database with a single call.
$cQuery: SQL string.
$cParams: Parameter array.
$iLine: Populate line array with first row?
$cHost: DB Host.
$cDB: DB logical name.
$cUser: DB user.
$cPword: DB password.
*/
/* Connect to DB */
$this->db_connect($cHost, $cDB, $cUser, $cPword);
/* Execute query. */
$this->db_query($cQuery, $cParams);
/* Get row count */
$this->db_count();
/* Get field count. */
$this->db_field_count();
/* Get metadata. */
$this->db_field_metadata();
if($iLine===TRUE)
{
$this->db_line();
}
return $this->cDBLine;
}
This function is designed to run simple select queries with a single call. There is no need to run a separate call to connect, prepare, and query execute. Simply pass the query, parameters and (if necessary) connection values.
Note the iLine parameter. If this is passed as TRUE, cDBLine will be populated with the first row from your query result. This option is very useful for times when you expect and only need a single row result; one call to this function is all you need. However, you will want to be careful with multiple rows as the row cursor will now be at the next record. This means a subsequent call to db_line() will start at the second record, NOT the first.
Returns cDBLine.
function db_close()
{
/*
db_close
Damon Vaughn Caskey
2012_11_13
Close current conneciton. Normally not needed.
*/
/* Close DB conneciton. */
sqlsrv_close($this->rDBConn);
/* Return TRUE. */
return TRUE;
}
Closes connection with rDBConn.
function db_connect($cHost = constants::c_cDBHost, $cDB = constants::c_cDBLName, $cUser = constants::c_cDBUser, $cPword = constants::c_cDBPword)
{
/*
db_connect
Damon Vaughn Caskey
2012_11_13
Connect to database host and store reference to public variable.
$cHost: DB Host.
$cDB: DB logical name.
$cUser: DB user.
$cPword: DB password.
*/
$db_cred = NULL; //Credentials array.
/* Only bother connecting to DB host if no previous connection is established. */
if(!$this->rDBConn)
{
/* Set up credential array. */
$db_cred = array("Database"=>$cDB, "UID"=>$cUser, "PWD"=>$cPword);
/* Establish database connection. */
$this->rDBConn = sqlsrv_connect($cHost, $db_cred);
}
/* False returned. Database connection has failed. */
if($this->rDBConn === FALSE)
{
/* Stop script and log error. */
$this->db_error("db_connect", NULL, NULL, TRUE);
}
}
Opens a connection to database host; defaults to primary host and database using connection pooling.
function db_count()
{
/*
db_line
Damon Vaughn Caskey
2012_11_13
Return number of records from query result.
*/
/* Get row count. */
$this->iDBRowCount = sqlsrv_num_rows($this->rDBResult);
$this->db_error("db_count");
/* Return count. */
return $this->iDBRowCount;
}
Set and return iDBRowCount with record count from last query (rDBResult).
function db_error($cLocation = NULL, $cSql = NULL, $cParams = NULL, $bFatal = FALSE)
{
/*
db_error
Damon Vaughn Caskey
2012_06_08
Wrapper for sqlsrv_errors(). Record any errors into an alternate database and send email to webmaster.
$cLocation: Code location where error trap was called. Aids in dubugging.
$cSql: SQL string passed at time of error.
$cParams: Parameter array passed at time of error.
$bFatal: If error triggers, should process be terminated?
*/
$cMBody = NULL; //Mail message.
$aErrors = NULL; //Errors list array.
$aError = NULL; //Error output array.
$mysqli = NULL; //Connection reference to DB error log.
$query = NULL; //Error query string.
$stmt = NULL; //Prepared query reference.
$val = NULL; //Array of error values.
$cParam = NULL; //Individual item from parameter array.
/* Get error collection */
$aErrors = sqlsrv_errors();
/* Any errors found? */
if($aErrors)
{
/* Connect to error log database (obviously this should be an alternate DB from the one that failed). */
$mysqli = new mysqli(constants::c_cDBEHost, constants::c_cDBELName, constants::c_cDBEUser, constants::c_cEDBPword);
/* Loop through error collection. */
foreach($aErrors as $aError)
{
/*
Ignore these codes; they are informational only:
0: Cursor type changed.
5701: Changed database context.
5703: Changed language setting.
*/
if($aError['code'] != 0 && $aError['code'] != 5701 && $aError['code'] != 5703)
{
$val[0] = $cLocation; //Function & Line location. Manually set in execution code to aid debugging; not part of error routine.
$val[1] = $aError['SQLSTATE']; //Status of DB host.
$val[2] = $aError['code']; //Error code.
$val[3] = $aError['message']; //Error message.
$val[4] = $cSql; //SQL string (if any) attempting to be executed.
$val[5] = NULL; //Parameter array.
$val[6] = $_SERVER["PHP_SELF"]; //Calling PHP file.
$val[7] = $_SERVER['REMOTE_ADDR']; //Client IP address.
/* Dump parameter array into single string. */
if(isset($cParams)) //Parameter array passed?
{
foreach($cParams as $cParam) //Loop array collection.
{
$val[5] .= $cParam .", "; //Add to error parameter string.
}
}
/* Build mail body string */
$cMBody .=
"\n Code Location: ". $val[0].
"\n SQLSTATE: ". $val[1].
"\n Code: ". $val[2].
"\n Message: ". $val[3].
"\n SQL String: ". $val[4].
"\n Parameters: ". $val[5].
"\n File: ". $val[6].
"\n Client IP: ". $val[7].
"\n";
/* If the error log database connection was successful, insert each error to table. */
if (!$mysqli->connect_error)
{
/* Build query string. */
$query = "INSERT INTO tbl_query_errors (codel, state, code, msg, query, params, source, ip) VALUES (?,?,?,?,?,?,?,?)";
$stmt = $mysqli->prepare($query);
/* Bind parameters. */
$stmt->bind_param("ssssssss", $val[0], $val[1], $val[2], $val[3], $val[4], $val[5], $val[6], $val[7]);
/* Execute and close query. */
if($stmt != false)
{
/* Execute and close query. */
$stmt->execute();
$stmt->close();
}
}
}
}
/* Close DB connection. */
$mysqli->close();
/* Send EMail alert. */
mail(constants::c_cWMEmail, constants::c_cDBEEmailSub, $cMBody, constants::c_cWMEmailOut);
/* If error is fatal, stop PHP execution immediately. */
if($bFatal)
{
die("A fatal database error has occurred. Please contact the webmaster immediately.");
}
return true;
}
return false;
}
Capture, log and return any errors encountered by the various other functions. If errors are found, they will be sent to webmaster via e-mail Additionally an attempt is made to log the errors to another database.
My method is to use the MySQL database in my personal web portal to log various activities from production sites I am responsible for using an INSERT only account. This ensures I am always aware of potential issues and have organized debugging info for problems that arise. For this reason, the error function is written primarily for MySQL. It would be easy enough to alter this for your own needs.
Note the bFatal parameter. If this is set to TRUE, the entire script (i.e. the page being generated) will fail and be replaced with an alert. This is less than user friendly, but in some cases may be a preferable result.
function db_execute()
{
/*
db_execute
Damon Vaughn Caskey
2012_11_13
Execute prepared query.
*/
/* Execute statement. */
$this->rDBResult = sqlsrv_execute($this->rDBStatement);
/* Error trapping. */
$this->db_error("db_execute");
/* Set rows affected. */
$this->iDBRowsAffect;
/* Return ID resource. */
return $this->rDBResult;
}
Executes rDBStatement, then populates and returns rDBResult with the results. Also sets iDBRowsAffect with the number of rows affected by execution. Useful for efficiently running a single action query where only the parameters (if anything) change.
function db_field_count()
{
/*
db_field_count
Damon Vaughn Caskey
2012_11_13
Get number of fields from query result.
*/
/* Get field count. */
$this->iDBFCount = sqlsrv_num_fields($this->rDBResult);
/* Error trapping. */
$this->db_error("db_field_count");
/* Return field count. */
return $this->iDBFCount;
}
Sets and returns iDBFCount with the number of fields returned by rDBResult.
function db_field_metadata()
{
/*
db_field_metadata
Damon Vaughn Caskey
2012_11_13
Fetch table rows metadata array (column names, types, etc.).
*/
/* Get metadata array. */
$this->cDBMeta = sqlsrv_field_metadata($this->rDBResult);
/* Error trapping. */
$this->db_error("db_field_metadata");
/* Return metadata array. */
return $this->cDBMeta;
}
Sets and returns cDBMeta with field meta data from rDBResult.
function db_line($iFetchType = constants::c_iDBFetchType)
{
/*
db_line
Damon Vaughn Caskey
2012_11_13
Fetch line array from table rows.
$iFetchType: Row key fetch type
*/
/* Get line array. */
$this->cDBLine = sqlsrv_fetch_array($this->rDBResult, $iFetchType);
/* Error trapping. */
$this->db_error("db_line");
/* Return line array. */
return $this->cDBLine;
}
Sets and returns cDBLine with row array data from rDBResult. This is the function you would place inside of a while loop to fetch the entire row list from a record set.
function db_prepare($cQuery, $cParams, $cOptions = array("Scrollable" => SQLSRV_CURSOR_STATIC))
{
/*
db_prepare
Damon Vaughn Caskey
2012_11_13
Prepare query statement.
$cQuery: Basic SQL statement to execute.
$cParams: Parameters to pass with query (prevents SQL injection).
$cOptions: Options for cursor array, etc.
*/
/* Execute query. */
$this->rDBStatement = sqlsrv_prepare($this->rDBConn, $cQuery, $cParams, $cOptions);
/* Error trapping. */
$this->db_error("db_prepare", $cQuery, $cParams);
/* Return query ID resource. */
return $this->rDBStatement;
}
Prepare a query statement from SQL string and parameters, then place into rDBStatement. Use this function to prepare action queries you need to execute repeatedly.
function db_query($cQuery, $cParams, $cOptions = array("Scrollable" => SQLSRV_CURSOR_STATIC))
{
/*
db_query
Damon Vaughn Caskey
2012_11_13
Prepare and execute query.
$cQuery: Basic SQL statement to execute.
$cParams: Parameters to pass with query (prevents SQL injection).
$cOptions: Options for cursor array, etc.
*/
/* Execute query. */
$this->rDBResult = sqlsrv_query($this->rDBConn, $cQuery, $cParams, $cOptions);
/* Error trapping. */
$this->db_error("db_query", $cQuery, $cParams);
/* Return query ID resource. */
return $this->rDBResult;
}
Prepare a query statement from SQL string and parameters, then execute immediately. Populates and returns rDBResult with result. This function is useful when you want to run a single query with no functional add-ons.

This super simple function will output alternating values as the input numeric increments. Handy for server side table styling, progress bars, and other minor elements where an alternating visual style is desirable.
During any loop statement where row outputs are created, pass the loop or row count to this function. The resulting output should then be used as your style argument.
// Damon Vaughn Caskey
// 2012-10-18
// Output alternating values for even/odd input.
//
// $i Number to evaluate.
// $even: Output if $i is an even number.
// $odd: Output if $i is an odd number.
function wig_wag($i, $even = "#DDDDFF", $odd = "#CECEFF")
{
$result = NULL; // Final result.
if($i%2) //Even number?
{
$result = $even; // Set even value.
}
else
{
$result = $odd; // Set odd value.
}
// Output result.
return $result;
}

I’ve always loved fitness, exploration and outdoor adventure. Even so, I haven’t strapped on numbers and participated in an organized sporting event since high school. It was high time to change that, and The Inaugural Bluegrass Mud Run was a perfect opportunity.
In my usual fashion I lost track of dates and almost missed it until a friend reminded me they too were participating – I just managed to get signed up a day before the event. It was a blast from start to finish, and the entry fee going to support local charities makes the whole thing even better.
Naturally, the GoPro camera came along, and with it a full POV. Want to see what it’s like to run a military inspired obstacle course without getting yourself dirty? Well, here you are…
Finally finished Harley Quinn’s Revenge for Batman Arkham City. Finding the steel mill entrance kept me stuck for a while. What an anticlimactic ending though. I think Harley’s real revenge was suckering people into downloading and playing it. 😛
It’s been a while since I logged any adventure in my adventure section hasn’t it? Far too long I think. Of course, it’s a bit difficult to record things on a broken blog, and boy was it. That is now fixed. The next step is to actually have something to add. Problem is I’m not really all that great at getting “backlogged” adventures up, and for a couple of weeks I haven’t had anything new to contribute.
As it happens some wackiness a few weeks ago (that I’ll hopefully get posted, but probably not :P) resulted in someone asking me nicely to “be more careful with myself” as it were, which in turn really puts a damper on random exploration. Believe it or not I didn’t mind a bit; it was probably the first time anyone had shown me honest concern instead of some smarmy “don’t go die!” derision. The whole thing came from WAY out of left field, but I still found it heart warming and so was willing to comply. Ultimately though it turns out said concern (along with many other things) was a complete farce. Obviously when I figured that out any agreement was immediately null and void. Depressing to say the least, and not something I want to divulge all the details about publicly. But hey, at least I’m free to do my thing again right? Edit: Or not? Time will tell, but perhaps I made a hasty assumption. Communication is key.
Sooooo, blog fixed, freedom restored. Nothing left but to recover my full strength and find something to do with it. 🙂
That something took the form of a second trip up the Dix River to get better photographs of the dam apparatus. On my previous trip I did not have the necessary camera equipment needed to get worthwhile photos. A little high def GoPro video augmented by some 8MP still shots would fix that right up. Or at least, it would have if I could learn to leave on time. I didn’t get to the dam until well after dark, and as anyone else who uses a GoPro can tell you, the one flaw they have is needing an abundance of light. Ahh well, at least I still got to see it for myself. As an added bonus, the darkness gave me an opportunity to climb around for some up close & personal looks at the pen-stocks, dam structure and a few other nick knacks.
Sadly I did not run into Dan this time, making it just a Dam, & Dix trip. Would have liked to chat with him again, but since I still need photos I’ll be back soon enough. Maybe next time.
Here is the footage I did take, along with a route plot. The video runs from put in to just about the first rapid, at which point I turned the camera off since it was obviously not going to record much else from that point. Just for extra fun, the sensors in my GPS went awry a bit (just a tiny bit) sending my plot on a 21,000+ mile detour; averaging over 5000kph! Now THAT’S some paddling!

Here is a somewhat self-pitying but still quite insightful article on the experience of ADHD by one Dr. Edward M. Hallowell. This was originally linked by Ty Brown on Facebook, and quickly caught my attention (no pun intended).
As my peers are aware, I was diagnosed in 7th grade, before calling every other kid ADHD was in vogue. For a short while I was even on the then new drug Ritalin, and it was quite effective (interesting note, Ritalin itself makes a workable if somewhat scary diagnostic tool. It has the intended calming effect on actual ADHD subjects, while simply giving “normal” individuals a variable euphoric high).
Due to side effects on my digestive system and the simple fact neither me nor the family liked the idea of my being on a pill my dosage was discontinued. Bad grades and (in some people’s eyes) a minor thrill seeking complex aside I have gotten along just fine without it.
Here is the actual article, also block quoted below should the link go dead.
Enjoy,
DC
What is it like to have ADHD? What is the feel of the syndrome? Attention Deficit Hyperactivity Disorder. First of all, I resent the term. As far as I’m concerned, most people have Attention Surplus Disorder. I mean, life being what it is, who can pay attention to anything for very long? Is it really a sign of mental health to be able to balance your checkbook, sit still in your chair, and never speak out of turn? But anyway, be that as it may, there is this syndrome called ADD or ADHD, depending on what book you read. So what’s it like to have it?
Some people say the so-called syndrome doesn’t even exist, but believe me, it does. Many metaphors come to mind to describe it. It’s like driving in the rain with bad windshield wipers. Everything is smudged and blurred and you’re speeding along, and it’s really frustrating not being able to see very well. Or it’s like listening to a radio station with a lot of static and you have to strain to hear what’s going on. Or, it’s like trying to build a house of cards in a dust storm. You have to build a structure to protect yourself from the wind before you can even start on the cards.
In other ways it’s like being supercharged all the time. You get one idea and you have to act on it, and then, what do you know, but you’ve got another idea before you’ve finished up with the first one, and so you go for that one, but of course a third idea intercepts the second, and you just have to follow that one, and pretty soon people are calling you disorganized and impulsive and all sorts of impolite words that miss the point completely. Because you’re trying really hard. It’s just that you have all these invisible vectors pulling you this way and that, which makes it really hard to stay on task. Plus, you’re spilling over all the time. You’re drumming your fingers, tapping your feet, humming a song, looking here, looking there, stretching, doodling, and people think you’re not paying attention or that you’re not interested, but all you’re doing is spilling over so that you can pay attention. I can pay a lot better attention when I’m taking a walk or listening to music or even when I’m in a crowded, noisy room than when I’m still and surrounded by silence.
What is it like to have ADHD? Buzzing. Being here and there and everywhere. Someone once said, “Time is the thing that keeps everything from happening all at once.” Time parcels moments out into separate bits so that we can do one thing at a time. In ADHD, this does not happen. In ADHD, time collapses. Time becomes a black hole. To the person with ADHD it feels as if everything is happening all at once. This creates a sense of inner turmoil or even panic. The individual loses perspective and the ability to prioritize. He or she is always on the go, trying to keep the world from caving in on top.
Lines. I’m almost incapable of waiting in lines. I just can’t wait, you see. That’s the agony of it. Impulse leads to action. I’m very short on what you might call the intermediate reflective step between impulse and action. That’s why I, like so many people with ADHD, lack tact. Tact is entirely dependent on the ability to consider one’s words before uttering them. We ADHD types don’t do this so well.
Many of us with ADHD crave high-stimulus situations. In my case, I love casinos, the high-intensity crucible of doing psychotherapy and having lots of people around. High stim situations can get you into trouble, which is why ADHD is high among criminals and self-destructive risk-takers. It is also high among so-called Type A personalities, as well as among manic-depressives, sociopaths and drug users. But it is also high among creative and intuitive people in all fields, and among highly-energetic, highly-productive people. Which is to say there is a positive side to all this. Usually the positive doesn’t get mentioned when people speak about ADHD because there is a natural tendency to focus on what goes wrong, or at least on what has to be somehow controlled. But often once the ADHD has been diagnosed, and the individual, with the help of teachers, parents and colleagues, has learned how to cope with it, an untapped realm of the brain swims into view. Suddenly the radio station is tuned in, the windshield is clear, the sand storm has died down. And the child or adult, who had been such a problem, such a nudge, such a general pain in the neck, starts doing things he’d never been able to do before. He surprises everyone around him, and he surprises himself. I use the male pronoun, but it could just as easily be she, as we are seeing more and more ADHD among females as we are looking for it.
Often these people are highly imaginative and intuitive. They have a “feel” for things, a way of seeing right into the heart of matters while others have to reason their way along methodically. This is the person who can’t explain how he thought of the solution, or where the idea for the story came from, or why suddenly he produced such a painting, or how he knew the short cut to the answer, but all he can say is he just knew it, he could feel it. This is the man or woman who makes million-dollar deals in a catnap and pulls them off the next day. This is the child who, having been reprimanded for blurting something out, is then praised for having blurted out something brilliant. These are the people who learn and know and do and go by touch and feel.
These people can feel a lot. In places where most of us are blind they can, if not see the light, at least feel the light, and they can produce answers apparently out of the dark. It is important for others to be sensitive to this “sixth sense” many ADHD people have, and to nurture it. If the environment insists on rational, linear thinking and “good” behavior from these people all the time, then they may never develop their intuitive style to the point where they can use it profitably. It can be exasperating to listen to people talk. They can sound so vague or rambling. But if you take them seriously and grope along with them, often you will find they are on the brink of startling conclusions or surprising solutions.
What I am saying is that their cognitive style is qualitatively different from most people’s, and what may seem impaired, with patience and encouragement may become gifted. The thing to remember is that if the diagnosis can be made, then most of the bad stuff associated with ADHD can be avoided or contained. The diagnosis can be liberating, particularly for people who have been stuck with labels like “lazy,” “stubborn,” “willful,” “disruptive,” “impossible,” “tyrannical,” “a space shot,” “brain damaged,” “stupid,” or just plain “bad.” Making the diagnosis of ADHD can take the case from the court of moral judgment to the clinic of neuropsychiatric treatment.
What is the treatment all about? Anything that turns down the noise. Just making the diagnosis helps turn down the noise of guilt and self-recrimination. Building certain kinds of structure into one’s life can help a lot. Working in small spurts rather than long hauls. Breaking tasks down into smaller tasks. Making lists. Getting help where you need it. Maybe applying external limits on your impulses. Or getting enough exercise to work off some of the noise inside. Finding support. Getting someone in your corner to coach you, to keep you on track. Medication can help a great deal too, but it is far from the whole solution. The good news is that treatment can really help.
We who have ADHD need your help and understanding. We may make mess-piles wherever we go, but with your help, those mess-piles can be turned into realms of reason and art. So, if you know someone like me who’s acting up and daydreaming and forgetting this or that and just not getting with the program, consider ADHD before he starts believing all the bad things people are saying about him and it’s too late.

After thoroughly breaking DC Current with a botched bit of custom code to the point my database was in need of a rebuild, I had put off fixing everything for quite a while. But today I finally buckled down and got everything back online.
Unfortunately it meant blowing away all the customization and feature adds I had done prior, so now it’s time to get started reinstalling the plug-ins, themes, widgets, etc. Ugh…
DC
Holy nerdgasm Batman!
On Friday 2012/04/13 the original 1966 series Batmobile was parked in front of UK’s Joe Craft Center, just outside my office. How’s that for a Friday the 13th surprise? Way too awesome to banter on about, that’s what! Let’s get right to the pictures instead. See you next time. 🙂
DC
As I’m sure most of you are aware, several towns in Eastern Kentucky suffered severe damage from storms Friday evening. West Liberty was hit particularly hard, having been almost completely destroyed by an EF3 tornado. After assisting there on Saturday I can personally attest the situation is absolutely horrific and the people can use all the help they can get.
To aid in the relief efforts, Caskey’s Inc. will be sending our box truck with supplies on as many runs as necessary. We are currently soliciting donations for any of the following:
If you are interested, drop offs can be made in my office at 252 East Maxwell Street Lexington KY, or contact Cindy Caskey at 606-784-2782.
In addition, I will be returning Saturday to offer direct assistance with clean-up efforts. If anyone would like to come along and get a good day’s work in, please contact me at 859-257-3241 or 606-776-4914.
Thanks,
DC

This has to be one of the worst winters I can remember. Not once has there been a reasonable snow, and aside from some some very short lived exceptions the temperature has hovered at 50′ or more since late November. So when a small clipper system dropped a bit of the white stuff in Eastern Kentucky this weekend, I just had to get out and enjoy it. And where else to enjoy a fresh snowfall than Red River Gorge?
Going out meant I had to miss the Packers playing New York, but I already knew how that one was going to pan out (though I had no idea just how bad). It also meant delaying the Cumberland River section of my mapping project, but I’m pretty sure those rivers won’t be going anywhere for a while.
In short, a no brainier. Strap on the field kit, grab Cammy and away we went for a brisk day of winter walking. Surprisingly enough dogs can’t solo climb, so I had to leave some of my favorite places off the list – Half Moon arch in particular. Still, we hit quite a few landmarks before the day was through. Chimney Rock, Half Moon (just not to the top), Sky Bridge, Angel Windows, Eagles Nest, and of course the Nada Tunnel.
Cammy seemed to love it, and I for one was happy to finally have some company!
One of my personal projects is to map out and survey the major rivers in Eastern & Central Kentucky, in particular, their headwaters. Eastern Kentucky is drained by three main rivers; Big Sandy, Licking, and the Cumberland. Each of these rivers runs a course quite distinct from the others, though their headwaters reside within a few dozen miles and all are tributaries of the Ohio. Central Kentucky is in turn drained by the Kentucky River, whose headwaters are also in the same vicinity.
The Licking river runs through my hometown of Morehead and beyond forming Cave Run Lake it attracts little attention. It has a powerful flow year round, but few rapids and runs mostly through private farms. Its headwaters are located in Magoffin County in a difficult to access area around route 7. Unlike the others in this project, the Licking River retains the same identity end to end. It does have two “forks”, but these are in name only. Both are simply downstream tributaries, although locals along the South Fork often mistake it to be the main stem. The Licking is personally notorious to me because every time I go near it, mishaps surly follow. One in particular probably should have killed me, but instead just left a minor scar. Sometimes it’s better to be lucky than good. Rivers have a personality, and as I am learning, the Licking doesn’t seem to like me very much. I’ll probably do this one next.
Second to the Cumberland in historical significance, the Big Sandy proper is barley 30 miles in length, otherwise divided into Tug Fork and the dangerous Levisa Fork. Because of the distance involved, I will likely save this river for last, and conduct most of the mapping during kayaking trips.
The Kentucky itself is easily assessable, but its three forks are somewhat dispersed. I plan to take this after finishing up the Licking.
Of these four rivers, the Cumberland is by far the largest, most unique in course, and historically significant. It also has the most physically accessible headwaters areas, though perhaps the most isolated. For these reasons it was my first choice. Like many large rivers, there is some debate to this day what exactly constitutes the Cumberland headwaters. In name the Cumberland begins in Harlan Kentucky, where the Martin’s Fork, Clover Fork, and Poor Fork converge. This spot is quite easily accessible; it literally forms the back boundary of a Dairy Queen parking lot. From a hydrological standpoint, each river contributes about an equal share of water volume, so in effect there really isn’t a single headwaters, not on a human scale anyway – it’s more accurate to say the Cumberland’s source is Harlan County itself.
That all said, the very definition of “headwaters” is the most remote source of a given watercourse, even if insignificant by volume. Since the Poor Fork is the longest contributing stream, that means its source is also that of the Cumberland. All there is to be done is trace the Poor Fork until it’s nothing more than a ditch. To do this I left for Harlan KY on 2011/12/27. Apparently I wasn’t supposed to go. Out of the whole week, I picked the one day with blowing rain, sleet and fog. Then I didn’t even make it out of the house before I took a nail through my foot, but if I let that stop me I wouldn’t deserve to lace up my explorer’s gloves. Anyway, the plan was simple enough. Get to the confluence in Harlan, follow Poor Fork to its source, draw out a mental map, and take lots of pictures or video along the way. Naturally there were distractions, but that’s the point of an exploration trip, yes? These are the results:
Poor Fork’s ultimate source is a slight bowl shaped depression at the top of Pine Mountain along hwy 932 that collects water from numerous gulleys and puddles into two small streams. These streams converge roadside, forming the Poor Fork, which continues for some 30 miles back to the confluence at Harlan. Both streams are on private property. I was unable to get permission to explore the larger one (no one answered), but the other was in a front lawn and easily photographed. I wonder if the owner has any idea that a mud puddle under his VW Bug is the primary water source for a 680+ mile river that discharges over 225,000 gallons of per second at its terminus. I would like to Map the Martin’s Fork and Clover fork as well, but for now, mission accomplished.
Checking out the culvert where Watts Creek passes under Highway 119 along Seven Sisters Road in Bell County. Nice scenery at the far end. Also saw an interesting little cave, but it was water filled. I’ll have to come back with swimming gear and underwater lights to have a better look. Part of a personal project to survey the headwaters of Cumberland River.
Accidentally left the camera on while scouting this boat ramp access path along the Cumberland River. Figured what the heck and tossed it up.
Scouting for a picture, this where Clover, Martin’s and Poor Fork converge to form the Cumberland.
Various pictures taken along the way:
Old Slate was a road side park park located in Bath County; it’s one of the many places my grandfather would take me as part of trying to instill a sense of curiosity and adventure. Unfortunately the park is a now shadow of its former self, clearly abandoned to the elements and teenage revelers.
The furnace itself is now a graffiti muse, and all that remains of a once well kept playground/park area are some rotting picnic tables. Even so the sense of antiquity and isolation is still there, in some strange way perhaps more so. Only an occasionally passing car and soft rippling from nearby Slate Creek break the silence, both oblivious to this small piece of history they continually witness.
Running this function in a given page will verify via session variables if the user is currently logged in using their Active Directory account (referred to as Link Blue by UK), and part of an authorized group. At the time of writing, I do not have access to the necessary ou containers, so groups are passed through an array parameter in the function call. Not exactly Fort Knox security, but is enough to squash spammers and stop curious onlookers. More importantly, internal customers are no longer able to skip or fudge registration information on various forms and training materials.
Project notes for a hovercraft/moving floor design:
It is common knowledge that almost any household appliance can perform seemingly amazing feats when properly harnessed. Who here doesn’t remember the pictures in our elementary science books of a car lift consisting of nothing more than an Electrolux ‘sausage’ vacuum and some trash bags? It’s eye opening at first, but mathematically speaking, really not hard to conceive at all. The principal is very similar to common torque multiplication; or in simpler terms, spreading out the load. So when I found myself in need of a moving floor, that very text book came to mind (Heath Science, 3rd grade, for anyone who didn’t attend Tilden Hogge in the 80’s). Grab some bags and a vacuum, plug it in and go, right? Wrong. Right off the bat I ran into some issues:
As far as I know, the “car lifter” experiment deals with none of those issues, and if it does there is (or rather was) certainly no explanation. Google to the rescue!! Except, not so much. I couldn’t find anything on the specific experiment, and while there were some designs here and there, none of them suit my needs. In the end, I was left to come up with a design of my own. The biggest problem was still a check valve system. After some thought, I more or less landed on an epiphany; why not allow the air to escape from a series of holes in the bladder, using an O ring to center them in a dome shape? As the dome inflated, the holes would be sealed against the ground, giving maximum lifting strength. Afterward the whole unit would sit on a cushion of air, allowing (and requiring) a constant inflow to maintain pressure balance. It would be fully self-checking and self-resetting, just like I needed. It was only afterward I realized the design is literally a hovercraft. Duh! As an extra bonus, I had never even noticed my old shop-vac converts to a blower, not only giving me slightly more power than a common house vacuum, but also less worry about cooling, since the shop-vac’s motor has a separate cooling fan (though the hovercraft design mostly resolves cooling anyway). All that was left was to test the concept, get a design, gather the parts together and start building.
The first model is intended to and will only lift; it has no means of locomotive propulsion. It will serve as a prototype for a self propelled iteration in the future. Absolute lifting capacity is a given; what I am worried about is speed. The idea is to lift a human adult about 12cm within a few seconds of start-up. For this experiment I used the ballast bladders from my Kayak, a simple piece of wood, my own weight, and my hands as a check valve. It worked beautifully. Even with the tiny volume of air that could get through the bladder’s filler hose and nothing but my hands acting as a seal, the bag filled to capacity under 10 seconds, lifting me well off the ground in the process.
Plans Drawn, parts on the way. I should be able to get started in the next week or so. I guess it needs a name. I’ll call it the DCHC Model 1. 🙂
DCHC Model 1 constructed, tested and… absolute failure. The skirt attachment (staples + tape) simply cannot withstand enough air pressure to lift significant weight. The center spire underneath also failed. Not such a big thing and easily fixed, but the skirt failure is an obvious deal breaker.
Improved design with following modifications:
Testing of this second design (DCHC Model 2) proved far more successful. A minor flaw is a bit too much air escaping toward the back of the craft compared to other areas. I apparently mis-measured and cut the skirt a bit short in that area. Still, it works as intended. The craft itself weighs around 18kg and will easily lift my 90kg body weight. I’d estimate the maximum total operating capacity is around 135kg.
More to come…
DC