WIP
DirectOutput framework for virtual pinball cabinets WIP
Go to:
Overview 
Pinscape.cs
Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Runtime.InteropServices;
5 using System.Threading;
6 using Microsoft.Win32.SafeHandles;
7 using System.IO;
8 using System.Text.RegularExpressions;
9 
10 namespace DirectOutput.Cab.Out.PS
11 {
41  {
42  #region Number
43 
44 
45  private object NumberUpdateLocker = new object();
46  private int _Number = -1;
47 
59  public int Number
60  {
61  get { return _Number; }
62  set
63  {
64  if (!value.IsBetween(1, 16))
65  {
66  throw new Exception("Pinscape Unit Numbers must be between 1-16. The supplied number {0} is out of range.".Build(value));
67  }
68  lock (NumberUpdateLocker)
69  {
70  // if the unit number changed, update it and attach to the new unit
71  if (_Number != value)
72  {
73  // if we used a default name for the old unit number, change to the default
74  // name for the new unit number
75  if (Name.IsNullOrWhiteSpace() || Name == "Pinscape Controller {0:00}".Build(_Number))
76  {
77  Name = "Pinscape Controller {0:00}".Build(value);
78  }
79 
80  // remember the new unit number
81  _Number = value;
82 
83  // attach to the new device record for this unit number, updating the output list to match
84  this.Dev = Devices.First(D => D.UnitNo() == Number);
85  this.NumberOfOutputs = this.Dev.NumOutputs();
86  this.OldOutputValues = Enumerable.Repeat((byte)255, this.NumberOfOutputs).ToArray();
87  }
88  }
89  }
90  }
91 
92  #endregion
93 
94 
95  private int _MinCommandIntervalMs = 1;
96  private bool MinCommandIntervalMsSet = false;
97 
116  public int MinCommandIntervalMs
117  {
118  get { return _MinCommandIntervalMs; }
119  set
120  {
121  _MinCommandIntervalMs = value.Limit(0, 1000);
122  MinCommandIntervalMsSet = true;
123  }
124  }
125 
126  #region IOutputcontroller implementation
127 
134  public override void Init(Cabinet Cabinet)
135  {
136  // get the minimum update interval from the global config
137  if (!MinCommandIntervalMsSet && Cabinet.Owner.ConfigurationSettings.ContainsKey("PinscapeDefaultMinCommandIntervalMs") && Cabinet.Owner.ConfigurationSettings["PinscapeDefaultMinCommandIntervalMs"] is int)
138  MinCommandIntervalMs = (int)Cabinet.Owner.ConfigurationSettings["PinscapeDefaultMinCommandIntervalMs"];
139 
140  // do the base class work
141  base.Init(Cabinet);
142  }
143 
148  public override void Finish()
149  {
150  Dev.AllOff();
151  base.Finish();
152  }
153  #endregion
154 
155 
156  #region OutputControllerFlexCompleteBase implementation
157 
158 
163  protected override bool VerifySettings()
164  {
165  return true;
166  }
167 
171  protected override void UpdateOutputs(byte[] NewOutputValues)
172  {
173  // The extended protocol lets us update outputs in blocks of 7.
174  // Run through our output list and send an update for each bank
175  // that's changed. The extended protocol message starts with
176  // a byte set to 200+B, where B is the bank number - B=0 for
177  // outputs 1-7, B=1 for outputs 8-14, etc.
178  //
179  // Note that, unlike the LedWiz protocol, the extended protocol
180  // uses ONLY the brightness value to control each output. There's
181  // no separate on/off state. "Off" is simply a brightness of 0.
182  byte pfx = 200;
183  for (int i = 0; i < NumberOfOutputs; i += 7, ++pfx)
184  {
185  // look for a change among this bank's 7 outputs
186  int lim = Math.Min(i + 7, NumberOfOutputs);
187  for (int j = i; j < lim; ++j)
188  {
189  // if this output has changed, flush the bank
190  if (NewOutputValues[j] != OldOutputValues[j])
191  {
192  // found a change - send the bank
193  UpdateDelay();
194  byte[] buf = new byte[9];
195  buf[0] = 0; // USB report ID - always 0
196  buf[1] = pfx; // message prefix
197  Array.Copy(NewOutputValues, i, buf, 2, lim - i);
198  Dev.WriteUSB(buf);
199 
200  // the new values are now the current values on the device
201  Array.Copy(NewOutputValues, i, OldOutputValues, i, lim - i);
202  }
203  }
204  }
205  }
206 
207  byte[] OldOutputValues;
208 
209  private DateTime LastUpdate = DateTime.Now;
210  private void UpdateDelay()
211  {
212  int Ms = (int)DateTime.Now.Subtract(LastUpdate).TotalMilliseconds;
213  if (Ms < MinCommandIntervalMs)
214  Thread.Sleep((MinCommandIntervalMs - Ms).Limit(0, MinCommandIntervalMs));
215  LastUpdate = DateTime.Now;
216  }
217 
221  protected override void ConnectToController()
222  {
223  }
224 
228  protected override void DisconnectFromController()
229  {
230  Dev.AllOff();
231  }
232 
233  #endregion
234 
235  #region USB Communications
236 
237  public class Device
238  {
239  public override string ToString()
240  {
241  return name + " (unit " + UnitNo() + ")";
242  }
243 
244  public int UnitNo()
245  {
246  return unitNo;
247  }
248 
249  public int NumOutputs()
250  {
251  return numOutputs;
252  }
253 
254  public Device(IntPtr fp, string path, string name, short vendorID, short productID, short version)
255  {
256  // remember the settings
257  this.fp = fp;
258  this.path = path;
259  this.name = name;
260  this.vendorID = vendorID;
261  this.productID = productID;
262  this.version = version;
263  this.plungerEnabled = true;
264 
265  // presume we have the standard LedWiz-compatible complement of 32 outputs
266  this.numOutputs = 32;
267 
268  // If we're using the LedWiz vendor/product ID, the unit number is encoded in the product
269  // ID (it's the bottom 4 bits of the ID value - this is zero-based, so add one to get our
270  // 1-based value for UI reporting). If we're using our private vendor/product ID, the unit
271  // number must be obtained from the configuration report below.
272  if ((ushort)vendorID == 0xFAFA && (productID & 0xFFF0) == 0x00F0)
273  this.unitNo = (short)((productID & 0x000f) + 1);
274  else
275  this.unitNo = 1;
276 
277  // read a status report
278  byte[] buf = ReadUSB();
279  if (buf != null)
280  {
281  // parse the reponse
282  this.plungerEnabled = (buf[1] & 0x01) != 0;
283  }
284 
285  // Request a configuration report (special request type 4)
286  SpecialRequest(4);
287  for (int i = 0; i < 8; ++i)
288  {
289  // read a report - if it's our configuration reply, parse it, otherwise
290  // skip it (there might be one or more joystick status reports buffered)
291  buf = ReadUSB();
292  if (buf != null && (buf[2] & 0xF8) == 0x88)
293  {
294  // get the number of outputs configured on the hardware side
295  this.numOutputs = (int)buf[3] | (((int)buf[4]) << 8);
296 
297  // get the unit number
298  unitNo = (short)(((ushort)buf[5] | (((ushort)buf[6]) << 8)) + 1);
299  break;
300  }
301  }
302  }
303 
304  ~Device()
305  {
306  if (fp.ToInt32() != 0 && fp.ToInt32() != -1)
308  }
309 
310  private System.Threading.NativeOverlapped ov;
311  public byte[] ReadUSB()
312  {
313  for (int tries = 0; tries < 3; ++tries)
314  {
315  const int rptLen = 15;
316  byte[] buf = new byte[rptLen];
317  buf[0] = 0x00;
318  uint actual;
319  if (HIDImports.ReadFile(fp, buf, rptLen, out actual, ref ov) == 0)
320  {
321  // if the error is 6 ("invalid handle"), try re-opening the device
322  if (TryReopenHandle())
323  continue;
324 
325  Log.Write("Pinscape Controller USB error reading from device: " + GetLastWin32ErrMsg());
326  return null;
327  }
328  else if (actual != rptLen)
329  {
330  Log.Write("Pinscape Controller USB error reading from device: not all bytes received");
331  return null;
332  }
333  else
334  return buf;
335  }
336 
337  // don't retry more than a few times
338  return null;
339  }
340 
341  private IntPtr OpenFile()
342  {
343  return HIDImports.CreateFile(
345  IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
346  }
347 
348  private bool TryReopenHandle()
349  {
350  // if the last error is 6 ("invalid handle"), try re-opening it
351  if (Marshal.GetLastWin32Error() == 6)
352  {
353  // try opening a new handle on the device path
354  Console.WriteLine("invalid handle on read - trying to reopen handle");
355  IntPtr fp2 = OpenFile();
356 
357  // if that succeeded, replace the old handle with the new one and retry the read
358  if (fp2 != null)
359  {
360  // replace the handle
361  fp = fp2;
362 
363  // tell the caller to try again
364  return true;
365  }
366  }
367 
368  // we didn't successfully reopen the handle
369  return false;
370  }
371 
372  public String GetLastWin32ErrMsg()
373  {
374  int errno = Marshal.GetLastWin32Error();
375  return String.Format("{0} (Win32 error {1})",
376  new System.ComponentModel.Win32Exception(errno).Message, errno);
377  }
378 
379  public void AllOff()
380  {
381  // send the All Outputs Off request (special request 5)
382  SpecialRequest(5);
383  }
384 
385  public bool SpecialRequest(byte id)
386  {
387  byte[] buf = new byte[9];
388  buf[0] = 0x00; // report ID - always 0
389  buf[1] = 0x41; // 0x41 -> Pinscape special request
390  buf[2] = id; // special request type
391  return WriteUSB(buf);
392  }
393 
394  public bool WriteUSB(byte[] buf)
395  {
396  for (int tries = 0; tries < 3; ++tries)
397  {
398  UInt32 actual;
399  if (HIDImports.WriteFile(fp, buf, 9, out actual, ref ov) == 0)
400  {
401  // try re-opening the handle, if it's an "invalid handle" error
402  if (TryReopenHandle())
403  continue;
404 
405  Log.Write("Pinscape Controller USB error sending request to device: " + GetLastWin32ErrMsg());
406  return false;
407  }
408  else if (actual != 9)
409  {
410  Log.Write("Pinscape Controller USB error sending request: not all bytes sent");
411  return false;
412  }
413  else
414  return true;
415  }
416 
417  // maximum retries exceeded - return failure
418  return false;
419  }
420 
421  public IntPtr fp;
422  public string path;
423  public string name;
424  public short vendorID;
425  public short productID;
426  public short version;
427  public short unitNo;
428  public bool plungerEnabled;
429  public int numOutputs;
430  }
431 
432  #endregion
433 
434 
435  #region Device enumeration
436 
440  public static List<Device> AllDevices()
441  {
442  return Devices;
443  }
444 
445  // Search the Windows USB HID device set for Pinscape controllers
446  private static List<Device> FindDevices()
447  {
448  // set up an empty return list
449  List<Device> devices = new List<Device>();
450 
451  // get the list of devices matching the HID class GUID
452  Guid guid;
453  HIDImports.HidD_GetHidGuid(out guid);
454  IntPtr hdev = HIDImports.SetupDiGetClassDevs(ref guid, null, IntPtr.Zero, HIDImports.DIGCF_DEVICEINTERFACE);
455 
456  // set up the attribute structure buffer
458  diData.cbSize = Marshal.SizeOf(diData);
459 
460  // read the devices in our list
461  for (uint i = 0;
462  HIDImports.SetupDiEnumDeviceInterfaces(hdev, IntPtr.Zero, ref guid, i, ref diData);
463  ++i)
464  {
465  // get the size of the detail data structure
466  UInt32 size = 0;
467  HIDImports.SetupDiGetDeviceInterfaceDetail(hdev, ref diData, IntPtr.Zero, 0, out size, IntPtr.Zero);
468 
469  // now actually read the detail data structure
471  diDetail.cbSize = (IntPtr.Size == 8) ? (uint)8 : (uint)5;
472  if (HIDImports.SetupDiGetDeviceInterfaceDetail(hdev, ref diData, ref diDetail, size, out size, IntPtr.Zero))
473  {
474  // create a file handle to access the device
475  IntPtr fp = HIDImports.CreateFile(
477  IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
478 
479  // read the attributes
481  attrs.Size = Marshal.SizeOf(attrs);
482  if (HIDImports.HidD_GetAttributes(fp, ref attrs))
483  {
484  // presume this is a Pinscape Controller, then look for reasons it's not
485  bool ok = true;
486 
487  // read the product name string
488  String name = "<not available>";
489  byte[] nameBuf = new byte[128];
490  if (HIDImports.HidD_GetProductString(fp, nameBuf, 128))
491  name = System.Text.Encoding.Unicode.GetString(nameBuf).TrimEnd('\0');
492 
493  // If the vendor and product ID match an LedWiz OR our private ID, and the
494  // product name contains "pinscape", and it's product version 7 or higher,
495  // it's a Pinscape controller with the extended protocol features.
496  bool isLW = ((ushort)attrs.VendorID == 0xFAFA && (attrs.ProductID >= 0x00F0 && attrs.ProductID <= 0x00FF));
497  bool isPS = ((ushort)attrs.VendorID == 0x1209 && ((ushort)attrs.ProductID == 0xEAEA));
498  ok &= ((isLW || isPS)
499  && Regex.IsMatch(name, @"\b(?i)pinscape\b")
500  && attrs.VersionNumber >= 7);
501 
502  // Newer versions of the device software can present multiple USB HID
503  // interfaces, including Keyboard (usage page 1, usage 6) and Media
504  // Control (volume up/down/mute buttons) (usage page 12, usage 1).
505  // The output controller is always part of the Joystick interface
506  // (usage page 1, usage 4). HidP_GetCaps() returns the USB usage
507  // information for the first HID report descriptor associated with
508  // the interface, so we can determine which interface we're looking
509  // at by checking this information. Start by getting the preparsed
510  // data from the Windows HID driver.
511  IntPtr ppdata;
512  if (ok && HIDImports.HidD_GetPreparsedData(fp, out ppdata))
513  {
514  // get the device caps
516  HIDImports.HidP_GetCaps(ppdata, ref caps);
517 
518  // This Pinscape interface accepts output controller commands only
519  // if it's the joystick type (usage page 1 == generic desktop, usage
520  // 4 == joystick). If it doesn't match, it must be a secondary HID
521  // interface on the same device, such as the keyboard or media
522  // controller interface. Skip those interfaces, as they don't
523  // accept the output controller commands.
524  ok &= (caps.UsagePage == 1 && caps.Usage == 4);
525 
526  // done with the preparsed data
527  HIDImports.HidD_FreePreparsedData(ppdata);
528  }
529 
530  // If we passed all tests, this is the output controller interface for
531  // a Pinscape controller device, so add the device to our list.
532  if (ok)
533  {
534  // add the device to our list
535  devices.Add(new Device(fp, diDetail.DevicePath, name, attrs.VendorID, attrs.ProductID, attrs.VersionNumber));
536 
537  // the device list object owns the handle nwo
538  fp = System.IntPtr.Zero;
539  }
540  }
541 
542  // done with the file handle
543  if (fp.ToInt32() != 0 && fp.ToInt32() != -1)
544  HIDImports.CloseHandle(fp);
545  }
546  }
547 
548  // return the device list
549  return devices;
550  }
551 
552  #endregion
553 
554  // list of Pinscape controller devices discovered in Windows USB HID scan
555  private static List<Device> Devices;
556 
557  // my device
558  private Device Dev;
559 
560  #region Constructor
561 
565  static Pinscape()
566  {
567  // scan the Windows USB HID device set for installed Pinscape Controller devices,
568  // and save the list statically in the class
569  Devices = FindDevices();
570  }
571 
576  public Pinscape()
577  {
578  }
579 
584  public Pinscape(int Number)
585  {
586  this.Number = Number;
587  }
588 
589  #endregion
590  }
591 }
The Cabinet object describes the parts of a pinball cabinet (toys, outputcontrollers, outputs and more).
Definition: Cabinet.cs:17
Pinscape(int Number)
Initializes a new instance of the Pinscape class with a given unit number.
Definition: Pinscape.cs:584
ICabinetOwner Owner
Gets or sets the owner or the cabinet.
Definition: Cabinet.cs:67
static void Write(string Message)
Writes the specified message to the logfile.
Definition: Log.cs:99
Device(IntPtr fp, string path, string name, short vendorID, short productID, short version)
Definition: Pinscape.cs:254
A simple logger used to record important events and exceptions.
Definition: Log.cs:14
override void Init(Cabinet Cabinet)
Initializes the Pinscape object. This method does also start the workerthread which does the actual ...
Definition: Pinscape.cs:134
override void DisconnectFromController()
Disconnect from the controller.
Definition: Pinscape.cs:228
override void UpdateOutputs(byte[] NewOutputValues)
Send updated outputs to the physical device.
Definition: Pinscape.cs:171
static bool CloseHandle(IntPtr hObject)
Dictionary< string, object > ConfigurationSettings
Gets the configuration settings. This dict can contain settings which are used by the output controll...
override void ConnectToController()
Connect to the controller.
Definition: Pinscape.cs:221
static List< Device > AllDevices()
Get the list of all Pinscape devices discovered in the system from the Windows USB device scan...
Definition: Pinscape.cs:440
Pinscape()
Initializes a new instance of the Pinscape class with no unit number set. The unit number must be set...
Definition: Pinscape.cs:576
override void Finish()
Finishes the Pinscape object. Finish does also terminate the workerthread for updates.
Definition: Pinscape.cs:148
The Pinscape Controller is an open-source software/hardware project based on the inexpensive and powe...
Definition: Pinscape.cs:40
override bool VerifySettings()
Verify settings. Returns true if settings are valid, false otherwise. In the current implementation...
Definition: Pinscape.cs:163