Silex USB Device Server - Geräte neu verbinden

Ich setze einen Silex USB Device Server ein, um USB-Geräte an meinen virtuellen Windows 10 Host anzubinden. Das funktioniert in der Regel ganz gut, aber bei den virtuellen COM-Ports gibt es leider einige Kandidaten die sporadisch in Fehlerzustände geraten. Das äußert sich dann so, dass der Port im Gerätemanager mit X geflaggt ist und in IPS die entsprechende IO-Instanz sich nicht verbinden lässt („Pfad nicht gefunden“).

Abhilfe schafft dann, im Silex-USB-Tool auf der Maschine per GUI das Gerät einmal zu trennen und wieder zu verbinden.

Das hat mich auf Dauer aber genervt und zum Glück habe ich ein Kommandozeilen-Tool gefunden, mit dem sich das ganze automatisiert durchführen lässt:

http://www.silexamerica.com/uploads/common/sx-virtual-link-win-cui-120-sample.zip

Dazu habe ich ein Skript geschrieben, das die Behebung solcher Probleme automatisiert. Wenn eine der dort eingetragenen Serial Port Instanzen in einen Fehlerstatus gerät, wird automatisch folgendes unternommen:
-Port geschlossen
-Gerät getrennt
-Gerät verbunden
-Port geöffnet
Abschließend wird geprüft, ob die Instanz nun wieder funktioniert.

Man kann eine Mailer-Instanz angeben, die anschließend einen kurzen Bericht absendet.

Bleibt das Problem bestehen so wird es in 15 Minuten erneut versucht.

<?
// Setup instructions:
// - Download DSCon.exe from
//   http://www.silexamerica.com/uploads/common/sx-virtual-link-win-cui-120-sample.zip
// - Unzip the files and copy them all to the SX Virtual Link program folder
//   (Usually under C:\Program Files\silex technology).
// - Fill in the IP address of your USB host(s) in the table below
// - Execute this script once. A list of device names should be displayed.
// - Fill in the rest of the table (device name => serial port instance ID):
// - Execute this script again to check for configuration errors.
// - Probably should send $mailer_id to a SMTP mailer instance so you get
//   notified whenever the script does something on its own.

$device_mappings = array(
	'192.168.178.123' /* <-- IP Address of your USB Host goes here!*/ => array(
		// below, replace with your own device ID => instance ID mappings!
		'FTDI FT232R USB UART' => 0,
		'ELV AG ELV FHZ 1300 PC' => 0,
		'Arduino (www.arduino.cc) PID [0x0042]' => 0
	)
);

$mailer_id = 0;

IPS_SetScriptTimer($_IPS['SELF'], 15 * 60);

// if the script was called by an instance status event, check only the specified
// instance
if($_IPS['SENDER'] == 'StatusEvent') {
	$specific_inst_id = $_IPS['INSTANCE'];
} else {
	$specific_inst_id = false;
}

$body_short = ''; // will contain a short report only listing PROBLEMS
$body_detailed = ''; // will contain a more detailed report listing ALL devices

if(IPS_SemaphoreEnter("Silex USB Host", 30 * 1000)) {
	// try to solve problems if any are detected
	$report = fix_problems_all($device_mappings, $specific_inst_id);
	
	// create report
	if($report !== false) {
		foreach($report as $entry) {
			$inst_name = @IPS_GetName($entry['instance_id']);
			if($inst_name === false) {
				$inst_name = '? (ID ' . $entry['instance_id'] . ')';
			}
			$cfg_json = @IPS_GetConfiguration($entry['instance_id']);
			$port = '?';
			if($cfg_json !== false) {
				$cfg = json_decode($cfg_json, true);
				$port = $cfg['Port'];
			}
			
			$ip = $entry['host_ip'];
			$dev_full_name = $inst_name . ' (' . $port . ', ' .
				$entry['device_name'] . ') @ ' . $ip;
			
			$this_body_entry = $dev_full_name . "\r";
			
			if($entry['problem_found']) {
				$this_body_entry .= "	Problem found! Status: " . $entry['status_before'] .
					"\r	Attempting to solve...\r";
				
				foreach($entry['steps'] as $step) {
					$this_body_entry .= "		Measure \"" . $step['measure'] . "\" => ";
					if($step['success']) {
						$this_body_entry .= 'SUCCESS';
					} else {
						$this_body_entry .= 'FAILED';
					}
					$this_body_entry .= "\r";
				}
				
				if($entry['problem_solved']) {
					$this_body_entry .= "	Problem solved. Status: " . $entry['status_after'] .
						"\r";
				} else {
					$this_body_entry .= "	Problem unsolved! Status: " . $entry['status_after'] .
						"\r";
				}
			} else if($entry['success']) {
				$this_body_entry .= "	Status: " . $entry['status_before'] . "\r";
			} else {
				IPS_LogMessage(IPS_GetName($_IPS['SELF']), 'Configuration error! Execute script for details.');
			}
			
			$this_body_entry .= "	Result: " . $entry['result'] . "\r\r";
			
			$body_detailed .= $this_body_entry;
			if($entry['problem_found']) {
				$body_short .= $this_body_entry;
			}
		}
	}
	IPS_SemaphoreLeave("Silex USB Host");
} else {
	$body_detailed = "Timeout waiting for other instance to finish!";
}

// if executed in console, set up, display detailed report
if($_IPS['SENDER'] == 'Execute') {
	if($body_detailed == "") $body_detailed = "No instances configured.\r\r";
	echo $body_detailed;
	
	// get event control's id and check whether a mapping for the supllied serial
	// port instance(s) exists on it
	$ec_id = IPS_GetInstanceListByModuleID('{ED573B53-8991-4866-B28C-CBE44C59A2DA}')[0];
	$ec_cfg_json = IPS_GetConfiguration($ec_id);
	$ec_cfg = json_decode($ec_cfg_json, true);
	$status_events_list = json_decode($ec_cfg['StatusEvents'], true);
	
	foreach($device_mappings as $ip => $instances) { // for all usb hosts
		try{
			$devices = get_device_list($ip);
		} catch(Exception $e) {
			echo $e->getMessage();
			continue;
		}
		echo "//Device names of devices attached to Host " . $ip . ":\r" .
			"\"" . $ip . "\" => array(\r";
		foreach($devices as $dev_name => $dev_ids) {
			echo "	\"" . $dev_name . "\" => 0, /* <-- replace with instance ID! */\r";
		}
		echo ")\r";
		
		$status_events_modified = false;
		foreach($instances as $dev_name => $inst_id) { // for each device on this host
			$has_status_event = false;
			foreach($status_events_list as $status_event_entry) {
				if($inst_id == $status_event_entry['DeviceID']) {
					if($status_event_entry['ScriptID'] == $_IPS['SELF']) {
						$has_status_event = true;
						break;
					}
				}
			}
			if(!$has_status_event) { // if this instance is not yet in the list, append it
				echo "Füge Status-Event für " . IPS_GetName($inst_id) . " hinzu!\r";
				$status_events_list[] = array(
					'DeviceID' => $inst_id,
					'ScriptID' => $_IPS['SELF']
				);
				$status_events_modified = true; // flag as modified so that new list gets saved
			}
		}
		
		// if we modified the status events list, save it to the event control's config
		if($status_events_modified) {
			$status_events_json = json_encode($status_events_list);
			IPS_SetProperty($ec_id, 'StatusEvents', $status_events_json);
			IPS_ApplyChanges($ec_id);
		}
	}
} else if($body_short != '') { // if executed by event control or timer
	// only send mail when mailer is specified and there is a problem to be reported
	if($mailer_id != 0) {
		SMTP_SendMail(
			$mailer_id,
			IPS_GetName($_IPS['SELF']),
			$body_short
		);
	}
	IPS_LogMessage(IPS_GetName($_IPS['SELF']), $body_short);
}

// attempts to fix problems for serial port instances connected on all supplied hosts
function fix_problems_all($device_mappings, $specific_inst_id = false) {
	$result = array();
	foreach($device_mappings as $ip => $instances) {
		$result = fix_problems_for_server($ip, $instances, $specific_inst_id, $result);
	}
	return $result;
}

// attempts to fix problems for serial port instances connected to specified host
function fix_problems_for_server($ip, $instances, $specific_inst_id = false, $result = false) {
	try {
		$devices = get_device_list($ip);
	} catch(Exception $e) {
		return false;
	}
	
	if($result === false) $result = array();
	
	foreach($instances as $dev_name => $inst_id) {
		$this_result = array(
			'host_ip' => $ip,
			'device_name' => $dev_name,
			'instance_id' => $inst_id
		);
		
		if($inst_id == 0) continue;
		
		// if a specific instance id has been supplied, only try to fix problems for
		// the specified instance!
		if($specific_inst_id !== false) {
			if($specific_inst_id != $inst_id) {
				continue;
			}
		}
		
		if(!IPS_InstanceExists($inst_id)) {
			$this_result['result'] = 'instance not found';
			$this_result['problem_found'] = false;
			$this_result['problem_solved'] = false;
			$this_result['success'] = false;
			$result[] = $this_result;
			continue;
		}
		
		// check whether the device name specified as key in the instances array
		// exists within the USB host's device list.
		if(!array_key_exists($dev_name, $devices)) {
			$this_result['result'] = 'device name not found';
			$this_result['problem_found'] = false;
			$this_result['problem_solved'] = false;
			$this_result['success'] = false;
			$result[] = $this_result;
			continue;
		}
		
		$inst = IPS_GetInstance($inst_id);
		$this_result['status_before'] = $inst['InstanceStatus'];
		
		// if instance does not have an error status
		if($inst['InstanceStatus'] < 200) {
			$this_result['result'] = 'status ok';
			$this_result['problem_found'] = false;
			$this_result['problem_solved'] = false;
			$this_result['success'] = true;
			$result[] = $this_result;
			continue;
		}

		$this_result['problem_found'] = true;
		
		$res_steps = array();
		
		// close port
		$res_steps[] = array(
			'measure' => 'close port',
			'success' => (
				@IPS_SetProperty($inst_id, 'Open', false) &&
				@IPS_ApplyChanges($inst_id)
			)
		);
		
		$dev_ids = $devices[$dev_name];
		foreach($dev_ids as $dev_id) {
			// disconnect virtual usb device
			$res_steps[] = array(
				'measure' => 'disconnect device',
				'device_id' => $dev_id,
				'success' => disconnect($ip, $dev_id)
			);
			
			// pause
			IPS_Sleep(1 * 1000);
						
			// connect virtual usb device
			$res_steps[] = array(
				'measure' => 'connect device',
				'device_id' => $dev_id,
				'success' => connect($ip, $dev_id)
			);
			
			// pause
			IPS_Sleep(3 * 1000);
		} // foreach
		
		// open port
		$res_steps[] = array(
			'measure' => 'open port',
			'success' => (
				@IPS_SetProperty($inst_id, 'Open', true) &&
				@IPS_ApplyChanges($inst_id)
			)
		);
		
		// pause
		IPS_Sleep(1 * 1000);
		
		$inst = IPS_GetInstance($inst_id);
		$this_result['status_after'] = $inst['InstanceStatus'];
		$this_result['steps'] = $res_steps;
		
		// if instance does not have an error status
		if($inst['InstanceStatus'] >= 200) {
			$this_result['problem_solved'] = false;
			$this_result['success'] = false;
			$this_result['result'] = 'problem unsolved';
		} else {
			$this_result['problem_solved'] = true;
			$this_result['success'] = true;
			$this_result['result'] = 'problem solved';
		}
		
		$result[] = $this_result;
	} // foreach
	
	return $result;
} // fix_problems

// Connect device attached to USB Host to local system, return true if successful
function connect($ip, $dev_id) {
	return dscon($ip, "C", $dev_id);
} // connect

// Disconnect device attached to USB Host from local system, return true on success
function disconnect($ip, $dev_id) {
	return dscon($ip, "D", $dev_id);
} // disconnect

// Returns an array of device names => array(device ids). The device ids are
// strings used to reference a device in subsequent calls to dscon().
function get_device_list($ip) {
	static $device_list;
	
	if(!isset($device_list)) $device_list = dscon($ip, "L");
	return $device_list;
} // get_device_list

// Wrapper function that calls the DSCon.exe utility in order to enumerate
// connected devices, connect or disconnect them from the local system.
function dscon($ip, $cmd, $dev_id = false) {
	$shell_script_path = IPS_GetKernelDirEx() . "DSCon.cmd";
	$outfile_path = IPS_GetKernelDirEx() . "DSCon_out.txt";
	
	// create wrapper shell script if it doesn't already exist.
	if(!file_exists($shell_script_path)) {
		$content = "@echo off
" .
			"if not exist \"%ProgramFiles%\\silex technology\\SX Virtual Link\\DSCon.exe\" goto error_dscon_missing
" .
			"cd \"%ProgramFiles%\\silex technology\\SX Virtual Link\"
" .
			"\"%ProgramFiles%\\silex technology\\SX Virtual Link\\DSCon.exe\" %*>" .
			$outfile_path . " 2>&1
" .
			"goto end
" .
			":error_dscon_missing
" .
			"echo DSCon.exe is missing!>" . $outfile_path . "
" .
			":end
";
		file_put_contents($shell_script_path, $content);
	}
	
	// remove output file so that we don't accidentally get old results if the
	// script call fails
	if(file_exists($outfile_path)) unlink($outfile_path);
	
	// call the wrapper shell script
	IPS_ExecuteEx(
		$shell_script_path,
		"/" . $cmd . ' /I' . $ip . ($dev_id !== false ? ' /P'.$dev_id : ''),
		false,
		true, // wait for it
		-1); // run in user session
	
	// get output
	if(file_exists($outfile_path)) {
		$result = file_get_contents($outfile_path);
		
		// message generated by shell script if DSCon.exe is missing
		if($result == "DSCon.exe is missing!
") {
			throw new Exception('DSCon.exe is missing. Must be in the SX Virtual Link folder!');
		}
	} else { // no output file means something went wrong with the shell script
		throw new Exception('Error launching shell script "' . $shell_script_path . '"!');
	}
	
	// interpret the output from DSCon.exe depending on which command was used
	switch($cmd) {
		case 'L':
			// return an array of $dev_name => array($dev_id, ...)
			if($result == "This device server doesn't exist on the network.
") {
				throw new Exception('Unable to connect to USB Host.');
			}
			
			$lines = explode("
", $result);
			$result = array();
			foreach($lines as $line) {
				if($line != '') {
					$fields = explode("	", $line);
					$dev_id = trim($fields[0], " 	[]");
					$dev_name = $fields[1];
					if(array_key_exists($dev_name, $result)) {
						$result[$dev_name][] = $dev_id;
					} else {
						$result[$dev_name] = array($dev_id);
					}
				}
			}
			break;
			
		case 'D':
			// return true if disconnecting succeeded
			$result = ($result == "Disconnection succeeded.
");
			break;
			
		case 'C':
			// return true if connecting succeeded
			$result = (
				($result == "Connection succeeded.
") ||
				($result == "One or more devices are already connected.
Please check the input command.
")
			);
			break;
	}
	
	return $result;
} // dscon
?>

klasse, it works ! Danke !
ab und zu hängt sich mal der Silex ab bei mir

Ich hatte mehrere Jahre eine FHZ1300 und die Basisstation der Oregonwetterstation über den Silex mit USB verbunden. Die Oregon hatte alle paar Wochen die Verbindung verloren, die FHZ1300. Das war so nervig, dass ich den Silex raus geworfen habe.

Mit dem Script könnte ich aber mal wieder einen neuen Anlauf wagen. Gerade Funkkomponenten auf USB Basis (auch Z-Wave) kann man mit LAN-Verlängerung dann dort hin bringen, wo sich funktechnisch am besten aufgehoben sind.

Gruss
Bernd

Freut mich!

Interessehalber: Wie hast du es getestet?

einfach mal die Devices abgehängt und Skript gestartet. obwohl die serPorts auf meinem System öfter mal durcheinander purzeln wenn neu connected wird

Das ist ja obernervig (und typische Windows-Krankheit). Prinzipiell könnte / sollte sich das auch automatisch beheben lassen. Bei mir tritt es zum Glück bisher nicht auf, daher habe ich mich noch nicht damit befasst wie es sich lösen lässt :wink: