An input/output controller for virtual pinball machines, with plunger position tracking, accelerometer-based nudge sensing, button input encoding, and feedback device control.

Dependencies:   USBDevice mbed FastAnalogIn FastIO FastPWM SimpleDMA

/media/uploads/mjr/pinscape_no_background_small_L7Miwr6.jpg

The Pinscape Controller is a special-purpose software project that I wrote for my virtual pinball machine.

New version: V2 is now available! The information below is for version 1, which will continue to be available for people who prefer the original setup.

What exactly is a virtual pinball machine? It's basically a video-game pinball emulator built to look like a real pinball machine. (The picture at right is the one I built.) You start with a standard pinball cabinet, either built from scratch or salvaged from a real machine. Inside, you install a PC motherboard to run the software, and install TVs in place of the playfield and backglass. Several Windows pinball programs can take advantage of this setup, including the open-source project Visual Pinball, which has hundreds of tables available. Building one of these makes a great DIY project, and it's a good way to add to your skills at woodworking, computers, and electronics. Check out the Cabinet Builders' Forum on vpforums.org for lots of examples and advice.

This controller project is a key piece in my setup that helps integrate the video game into the pinball cabinet. It handles several input/output tasks that are unique to virtual pinball machines. First, it lets you connect a mechanical plunger to the software, so you can launch the ball like on a real machine. Second, it sends "nudge" data to the software, based on readings from an accelerometer. This lets you interact with the game physically, which makes the playing experience more realistic and immersive. Third, the software can handle button input (for wiring flipper buttons and other cabinet buttons), and fourth, it can control output devices (for tactile feedback, button lights, flashers, and other special effects).

Documentation

The Hardware Build Guide (PDF) has detailed instructions on how to set up a Pinscape Controller for your own virtual pinball cabinet.

Update notes

December 2015 version: This version fully supports the new Expansion Board project, but it'll also run without it. The default configuration settings haven't changed, so existing setups should continue to work as before.

August 2015 version: Be sure to get the latest version of the Config Tool for windows if you're upgrading from an older version of the firmware. This update adds support for TSL1412R sensors (a version of the 1410 sensor with a slightly larger pixel array), and a config option to set the mounting orientation of the board in the firmware rather than in VP (for better support for FP and other pinball programs that don't have VP's flexibility for setting the rotation).

Feb/March 2015 software versions: If you have a CCD plunger that you've been using with the older versions, and the plunger stops working (or doesn't work as well) after you update to the latest version, you might need to increase the brightness of your light source slightly. Check the CCD exposure with the Windows config tool to see if it looks too dark. The new software reads the CCD much more quickly than the old versions did. This makes the "shutter speed" faster, which might require a little more light to get the same readings. The CCD is actually really tolerant of varying light levels, so you probably won't have to change anything for the update - I didn't. But if you do have any trouble, have a look at the exposure meter and try a slightly brighter light source if the exposure looks too dark.

Downloads

  • Config tool for Windows (.exe and C# source): this is a Windows program that lets you view the raw pixel data from the CCD sensor, trigger plunger calibration mode, and configure some of the software options on the controller.
  • Custom VP builds: I created modified versions of Visual Pinball 9.9 and Physmod5 that you might want to use in combination with this controller. The modified versions have special handling for plunger calibration specific to the Pinscape Controller, as well as some enhancements to the nudge physics. If you're not using the plunger, you might still want it for the nudge improvements. The modified version also works with any other input controller, so you can get the enhanced nudging effects even if you're using a different plunger/nudge kit. The big change in the modified versions is a "filter" for accelerometer input that's designed to make the response to cabinet nudges more realistic. It also makes the response more subdued than in the standard VP, so it's not to everyone's taste. The downloads include both the updated executables and the source code changes, in case you want to merge the changes into your own custom version(s).

    Note! These features are now standard in the official VP 9.9.1 and VP 10 releases, so you don't need my custom builds if you're using 9.9.1 or 10 or later. I don't think there's any reason to use my 9.9 instead of the official 9.9.1, but I'm leaving it here just in case. In the official VP releases, look for the checkbox "Enable Nudge Filter" in the Keys preferences dialog. (There's no checkbox in my custom builds, though; the filter is simply always on in those.)
  • Output circuit shopping list: This is a saved shopping cart at mouser.com with the parts needed for each output driver, if you want to use the LedWiz emulator feature. Note that quantities in the cart are for one output channel, so multiply everything by the number of channels you plan to use, except that you only need one of the ULN2803 transistor array chips for each eight output circuits.
  • Lemming77's potentiometer mounting bracket and shooter rod connecter: Sketchup designs for 3D-printable parts for mounting a slide potentiometer as the plunger sensor. These were designed for a particular slide potentiometer that used to be available from an Aliexpress.com seller but is no longer listed. You can probably use this design as a starting point for other similar devices; just check the dimensions before committing the design to plastic.

Features

  • Plunger position sensing, using a TAOS TSL 1410R CCD linear array sensor. This sensor is a 1280 x 1 pixel array at 400 dpi, which makes it about 3" long - almost exactly the travel distance of a standard pinball plunger. The idea is that you install the sensor just above (within a few mm of) the shooter rod on the inside of the cabinet, with the CCD window facing down, aligned with and centered on the long axis of the shooter rod, and positioned so that the rest position of the tip is about 1/2" from one end of the window. As you pull back the plunger, the tip will travel down the length of the window, and the maximum retraction point will put the tip just about at the far end of the window. Put a light source below, facing the sensor - I'm using two typical 20 mA blue LEDs about 8" away (near the floor of the cabinet) with good results. The principle of operation is that the shooter rod casts a shadow on the CCD, so pixels behind the rod will register lower brightness than pixels that aren't in the shadow. We scan down the length of the sensor for the edge between darker and brighter, and this tells us how far back the rod has been pulled. We can read the CCD at about 25-30 ms intervals, so we can get rapid updates. We pass the readings reports to VP via our USB joystick reports.

    The hardware build guide includes schematics showing how to wire the CCD to the KL25Z. It's pretty straightforward - five wires between the two devices, no external components needed. Two GPIO ports are used as outputs to send signals to the device and one is used as an ADC in to read the pixel brightness inputs. The config tool has a feature that lets you display the raw pixel readings across the array, so you can test that the CCD is working and adjust the light source to get the right exposure level.

    Alternatively, you can use a slide potentiometer as the plunger sensor. This is a cheaper and somewhat simpler option that seems to work quite nicely, as you can see in Lemming77's video of this setup in action. This option is also explained more fully in the build guide.
  • Nudge sensing via the KL25Z's on-board accelerometer. Mounting the board in your cabinet makes it feel the same accelerations the cabinet experiences when you nudge it. Visual Pinball already knows how to interpret accelerometer input as nudging, so we simply feed the acceleration readings to VP via the joystick interface.
  • Cabinet button wiring. Up to 24 pushbuttons and switches can be wired to the controller for input controls (for example, flipper buttons, the Start button, the tilt bob, coin slot switches, and service door buttons). These appear to Windows as joystick buttons. VP can map joystick buttons to pinball inputs via its keyboard preferences dialog. (You can raise the 24-button limit by editing the source code, but since all of the GPIO pins are allocated, you'll have to reassign pins currently used for other functions.)
  • LedWiz emulation (limited). In addition to emulating a joystick, the device emulates the LedWiz USB interface, so controllers on the PC side such as DirectOutput Framework can recognize it and send it commands to control lights, solenoids, and other feedback devices. 22 GPIO ports are assigned by default as feedback device outputs. This feature has some limitations. The big one is that the KL25Z hardware only has 10 PWM channels, which isn't enough for a fully decked-out cabinet. You also need to build some external power driver circuitry to use this feature, because of the paltry 4mA output capacity of the KL25Z GPIO ports. The build guide includes instructions for a simple and robust output circuit, including part numbers for the exact components you need. It's not hard if you know your way around a soldering iron, but just be aware that it'll take a little work.

Warning: This is not replacement software for the VirtuaPin plunger kit. If you bought the VirtuaPin kit, please don't try to install this software. The VP kit happens to use the same microcontroller board, but the rest of its hardware is incompatible. The VP kit uses a different type of sensor for its plunger and has completely different button wiring, so the Pinscape software won't work properly with it.

Committer:
mjr
Date:
Fri Feb 27 04:14:04 2015 +0000
Revision:
17:ab3cec0c8bf4
Parent:
16:c35f905c3311
Child:
18:5e890ebd0023
FastIO and FastAnalogIn; better firing event sensing; potentiometer plunger sensor option; new key debouncing; ZB Launch Ball feature

Who changed what in which revision?

UserRevisionLine numberNew contents of line
mjr 5:a70c0bce770d 1 /* Copyright 2014 M J Roberts, MIT License
mjr 5:a70c0bce770d 2 *
mjr 5:a70c0bce770d 3 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
mjr 5:a70c0bce770d 4 * and associated documentation files (the "Software"), to deal in the Software without
mjr 5:a70c0bce770d 5 * restriction, including without limitation the rights to use, copy, modify, merge, publish,
mjr 5:a70c0bce770d 6 * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
mjr 5:a70c0bce770d 7 * Software is furnished to do so, subject to the following conditions:
mjr 5:a70c0bce770d 8 *
mjr 5:a70c0bce770d 9 * The above copyright notice and this permission notice shall be included in all copies or
mjr 5:a70c0bce770d 10 * substantial portions of the Software.
mjr 5:a70c0bce770d 11 *
mjr 5:a70c0bce770d 12 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
mjr 5:a70c0bce770d 13 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
mjr 5:a70c0bce770d 14 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
mjr 5:a70c0bce770d 15 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
mjr 5:a70c0bce770d 16 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
mjr 5:a70c0bce770d 17 */
mjr 5:a70c0bce770d 18
mjr 5:a70c0bce770d 19 //
mjr 5:a70c0bce770d 20 // Pinscape Controller
mjr 5:a70c0bce770d 21 //
mjr 17:ab3cec0c8bf4 22 // "Pinscape" is the name of my custom-built virtual pinball cabinet, so I call this
mjr 17:ab3cec0c8bf4 23 // software the Pinscape Controller. I wrote it to handle several tasks that I needed
mjr 17:ab3cec0c8bf4 24 // for my cabinet. It runs on a Freescale KL25Z microcontroller, which is a small and
mjr 17:ab3cec0c8bf4 25 // inexpensive device that attaches to the cabinet PC via a USB cable, and can attach
mjr 17:ab3cec0c8bf4 26 // via custom wiring to sensors, buttons, and other devices in the cabinet.
mjr 5:a70c0bce770d 27 //
mjr 17:ab3cec0c8bf4 28 // I designed the software and hardware in this project especially for my own
mjr 17:ab3cec0c8bf4 29 // cabinet, but it uses standard interfaces in Windows and Visual Pinball, so it should
mjr 17:ab3cec0c8bf4 30 // work in any VP-based cabinet, as long as you're using the usual VP software suite.
mjr 17:ab3cec0c8bf4 31 // I've tried to document the hardware in enough detail for anyone else to duplicate
mjr 17:ab3cec0c8bf4 32 // the entire project, and the full software is open source.
mjr 5:a70c0bce770d 33 //
mjr 17:ab3cec0c8bf4 34 // The Freescale board appears to the host PC as a standard USB joystick. This works
mjr 17:ab3cec0c8bf4 35 // with the built-in Windows joystick device drivers, so there's no need to install any
mjr 17:ab3cec0c8bf4 36 // new drivers or other software on the PC. Windows should recognize the Freescale
mjr 17:ab3cec0c8bf4 37 // as a joystick when you plug it into the USB port, and Windows shouldn't ask you to
mjr 17:ab3cec0c8bf4 38 // install any drivers. If you bring up the Windows control panel for USB Game
mjr 17:ab3cec0c8bf4 39 // Controllers, this device will appear as "Pinscape Controller". *Don't* do any
mjr 17:ab3cec0c8bf4 40 // calibration with the Windows control panel or third-part calibration tools. The
mjr 17:ab3cec0c8bf4 41 // software calibrates the accelerometer portion automatically, and has its own special
mjr 17:ab3cec0c8bf4 42 // calibration procedure for the plunger sensor, if you're using that (see below).
mjr 5:a70c0bce770d 43 //
mjr 17:ab3cec0c8bf4 44 // This software provides a whole bunch of separate features. You can use any of these
mjr 17:ab3cec0c8bf4 45 // features individually or all together. If you're not using a particular feature, you
mjr 17:ab3cec0c8bf4 46 // can simply omit the extra wiring and/or hardware for that feature. You can use
mjr 17:ab3cec0c8bf4 47 // the nudging feature by itself without any extra hardware attached, since the
mjr 17:ab3cec0c8bf4 48 // accelerometer is built in to the KL25Z board.
mjr 5:a70c0bce770d 49 //
mjr 17:ab3cec0c8bf4 50 // - Nudge sensing via the KL25Z's on-board accelerometer. Nudging the cabinet
mjr 17:ab3cec0c8bf4 51 // causes small accelerations that the accelerometer can detect; these are sent to
mjr 17:ab3cec0c8bf4 52 // Visual Pinball via the joystick interface so that VP can simulate the effect
mjr 17:ab3cec0c8bf4 53 // of the real physical nudges on its simulated ball. VP has native handling for
mjr 17:ab3cec0c8bf4 54 // this type of input, so all you have to do is set some preferences in VP to tell
mjr 17:ab3cec0c8bf4 55 // it that an accelerometer is attached.
mjr 5:a70c0bce770d 56 //
mjr 5:a70c0bce770d 57 // - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor.
mjr 17:ab3cec0c8bf4 58 // To use this feature, you need to buy the TAOS device (it's not built in to the
mjr 17:ab3cec0c8bf4 59 // KL25Z, obviously), wire it to the KL25Z (5 wire connections between the two
mjr 17:ab3cec0c8bf4 60 // devices are required), and mount the TAOS sensor in your cabinet so that it's
mjr 17:ab3cec0c8bf4 61 // positioned properly to capture images of the physical plunger shooter rod.
mjr 17:ab3cec0c8bf4 62 //
mjr 17:ab3cec0c8bf4 63 // The physical mounting and wiring details are desribed in the project
mjr 17:ab3cec0c8bf4 64 // documentation.
mjr 17:ab3cec0c8bf4 65 //
mjr 17:ab3cec0c8bf4 66 // If the CCD is attached, the software constantly captures images from the CCD
mjr 17:ab3cec0c8bf4 67 // and analyzes them to determine how far back the plunger is pulled. It reports
mjr 17:ab3cec0c8bf4 68 // this to Visual Pinball via the joystick interface. This allows VP to make the
mjr 17:ab3cec0c8bf4 69 // simulated on-screen plunger track the motion of the physical plunger in real
mjr 17:ab3cec0c8bf4 70 // time. As with the nudge data, VP has native handling for the plunger input,
mjr 17:ab3cec0c8bf4 71 // so you just need to set the VP preferences to tell it that an analog plunger
mjr 17:ab3cec0c8bf4 72 // device is attached. One caveat, though: although VP itself has built-in
mjr 17:ab3cec0c8bf4 73 // support for an analog plunger, not all existing tables take advantage of it.
mjr 17:ab3cec0c8bf4 74 // Many existing tables have their own custom plunger scripting that doesn't
mjr 17:ab3cec0c8bf4 75 // cooperate with the VP plunger input. All tables *can* be made to work with
mjr 17:ab3cec0c8bf4 76 // the plunger, and in most cases it only requires some simple script editing,
mjr 17:ab3cec0c8bf4 77 // but in some cases it requires some more extensive surgery.
mjr 5:a70c0bce770d 78 //
mjr 6:cc35eb643e8f 79 // For best results, the plunger sensor should be calibrated. The calibration
mjr 6:cc35eb643e8f 80 // is stored in non-volatile memory on board the KL25Z, so it's only necessary
mjr 6:cc35eb643e8f 81 // to do the calibration once, when you first install everything. (You might
mjr 6:cc35eb643e8f 82 // also want to re-calibrate if you physically remove and reinstall the CCD
mjr 17:ab3cec0c8bf4 83 // sensor or the mechanical plunger, since their alignment shift change slightly
mjr 17:ab3cec0c8bf4 84 // when you put everything back together.) You can optionally install a
mjr 17:ab3cec0c8bf4 85 // dedicated momentary switch or pushbutton to activate the calibration mode;
mjr 17:ab3cec0c8bf4 86 // this is describe in the project documentation. If you don't want to bother
mjr 17:ab3cec0c8bf4 87 // with the extra button, you can also trigger calibration using the Windows
mjr 17:ab3cec0c8bf4 88 // setup software, which you can find on the Pinscape project page.
mjr 6:cc35eb643e8f 89 //
mjr 17:ab3cec0c8bf4 90 // The calibration procedure is described in the project documentation. Briefly,
mjr 17:ab3cec0c8bf4 91 // when you trigger calibration mode, the software will scan the CCD for about
mjr 17:ab3cec0c8bf4 92 // 15 seconds, during which you should simply pull the physical plunger back
mjr 17:ab3cec0c8bf4 93 // all the way, hold it for a moment, and then slowly return it to the rest
mjr 17:ab3cec0c8bf4 94 // position. (DON'T just release it from the retracted position, since that
mjr 17:ab3cec0c8bf4 95 // let it shoot forward too far. We want to measure the range from the park
mjr 17:ab3cec0c8bf4 96 // position to the fully retracted position only.)
mjr 5:a70c0bce770d 97 //
mjr 13:72dda449c3c0 98 // - Button input wiring. 24 of the KL25Z's GPIO ports are mapped as digital inputs
mjr 13:72dda449c3c0 99 // for buttons and switches. The software reports these as joystick buttons when
mjr 13:72dda449c3c0 100 // it sends reports to the PC. These can be used to wire physical pinball-style
mjr 13:72dda449c3c0 101 // buttons in the cabinet (e.g., flipper buttons, the Start button) and miscellaneous
mjr 13:72dda449c3c0 102 // switches (such as a tilt bob) to the PC. Visual Pinball can use joystick buttons
mjr 13:72dda449c3c0 103 // for input - you just have to assign a VP function to each button using VP's
mjr 13:72dda449c3c0 104 // keyboard options dialog. To wire a button physically, connect one terminal of
mjr 13:72dda449c3c0 105 // the button switch to the KL25Z ground, and connect the other terminal to the
mjr 13:72dda449c3c0 106 // the GPIO port you wish to assign to the button. See the buttonMap[] array
mjr 13:72dda449c3c0 107 // below for the available GPIO ports and their assigned joystick button numbers.
mjr 13:72dda449c3c0 108 // If you're not using a GPIO port, you can just leave it unconnected - the digital
mjr 13:72dda449c3c0 109 // inputs have built-in pull-up resistors, so an unconnected port is the same as
mjr 13:72dda449c3c0 110 // an open switch (an "off" state for the button).
mjr 13:72dda449c3c0 111 //
mjr 5:a70c0bce770d 112 // - LedWiz emulation. The KL25Z can appear to the PC as an LedWiz device, and will
mjr 5:a70c0bce770d 113 // accept and process LedWiz commands from the host. The software can turn digital
mjr 5:a70c0bce770d 114 // output ports on and off, and can set varying PWM intensitiy levels on a subset
mjr 5:a70c0bce770d 115 // of ports. (The KL25Z can only provide 6 PWM ports. Intensity level settings on
mjr 5:a70c0bce770d 116 // other ports is ignored, so non-PWM ports can only be used for simple on/off
mjr 5:a70c0bce770d 117 // devices such as contactors and solenoids.) The KL25Z can only supply 4mA on its
mjr 5:a70c0bce770d 118 // output ports, so external hardware is required to take advantage of the LedWiz
mjr 5:a70c0bce770d 119 // emulation. Many different hardware designs are possible, but there's a simple
mjr 5:a70c0bce770d 120 // reference design in the documentation that uses a Darlington array IC to
mjr 5:a70c0bce770d 121 // increase the output from each port to 500mA (the same level as the LedWiz),
mjr 5:a70c0bce770d 122 // plus an extended design that adds an optocoupler and MOSFET to provide very
mjr 5:a70c0bce770d 123 // high power handling, up to about 45A or 150W, with voltages up to 100V.
mjr 5:a70c0bce770d 124 // That will handle just about any DC device directly (wtihout relays or other
mjr 5:a70c0bce770d 125 // amplifiers), and switches fast enough to support PWM devices.
mjr 5:a70c0bce770d 126 //
mjr 5:a70c0bce770d 127 // The device can report any desired LedWiz unit number to the host, which makes
mjr 5:a70c0bce770d 128 // it possible to use the LedWiz emulation on a machine that also has one or more
mjr 5:a70c0bce770d 129 // actual LedWiz devices intalled. The LedWiz design allows for up to 16 units
mjr 5:a70c0bce770d 130 // to be installed in one machine - each one is invidually addressable by its
mjr 5:a70c0bce770d 131 // distinct unit number.
mjr 5:a70c0bce770d 132 //
mjr 5:a70c0bce770d 133 // The LedWiz emulation features are of course optional. There's no need to
mjr 5:a70c0bce770d 134 // build any of the external port hardware (or attach anything to the output
mjr 5:a70c0bce770d 135 // ports at all) if the LedWiz features aren't needed. Most people won't have
mjr 5:a70c0bce770d 136 // any use for the LedWiz features. I built them mostly as a learning exercise,
mjr 5:a70c0bce770d 137 // but with a slight practical need for a handful of extra ports (I'm using the
mjr 5:a70c0bce770d 138 // cutting-edge 10-contactor setup, so my real LedWiz is full!).
mjr 6:cc35eb643e8f 139 //
mjr 6:cc35eb643e8f 140 // The on-board LED on the KL25Z flashes to indicate the current device status:
mjr 6:cc35eb643e8f 141 //
mjr 6:cc35eb643e8f 142 // two short red flashes = the device is powered but hasn't successfully
mjr 6:cc35eb643e8f 143 // connected to the host via USB (either it's not physically connected
mjr 6:cc35eb643e8f 144 // to the USB port, or there was a problem with the software handshake
mjr 6:cc35eb643e8f 145 // with the USB device driver on the computer)
mjr 6:cc35eb643e8f 146 //
mjr 6:cc35eb643e8f 147 // short red flash = the host computer is in sleep/suspend mode
mjr 6:cc35eb643e8f 148 //
mjr 6:cc35eb643e8f 149 // long red/green = the LedWiz unti number has been changed, so a reset
mjr 6:cc35eb643e8f 150 // is needed. You can simply unplug the device and plug it back in,
mjr 6:cc35eb643e8f 151 // or presss and hold the reset button on the device for a few seconds.
mjr 6:cc35eb643e8f 152 //
mjr 6:cc35eb643e8f 153 // long yellow/green = everything's working, but the plunger hasn't
mjr 6:cc35eb643e8f 154 // been calibrated; follow the calibration procedure described above.
mjr 6:cc35eb643e8f 155 // This flash mode won't appear if the CCD has been disabled. Note
mjr 6:cc35eb643e8f 156 // that the device can't tell whether a CCD is physically attached,
mjr 6:cc35eb643e8f 157 // so you should use the config command to disable the CCD software
mjr 6:cc35eb643e8f 158 // features if you won't be attaching a CCD.
mjr 6:cc35eb643e8f 159 //
mjr 6:cc35eb643e8f 160 // alternating blue/green = everything's working
mjr 6:cc35eb643e8f 161 //
mjr 6:cc35eb643e8f 162 // Software configuration: you can change option settings by sending special
mjr 6:cc35eb643e8f 163 // USB commands from the PC. I've provided a Windows program for this purpose;
mjr 6:cc35eb643e8f 164 // refer to the documentation for details. For reference, here's the format
mjr 6:cc35eb643e8f 165 // of the USB command for option changes:
mjr 6:cc35eb643e8f 166 //
mjr 6:cc35eb643e8f 167 // length of report = 8 bytes
mjr 6:cc35eb643e8f 168 // byte 0 = 65 (0x41)
mjr 6:cc35eb643e8f 169 // byte 1 = 1 (0x01)
mjr 6:cc35eb643e8f 170 // byte 2 = new LedWiz unit number, 0x01 to 0x0f
mjr 6:cc35eb643e8f 171 // byte 3 = feature enable bit mask:
mjr 6:cc35eb643e8f 172 // 0x01 = enable CCD (default = on)
mjr 9:fd65b0a94720 173 //
mjr 9:fd65b0a94720 174 // Plunger calibration mode: the host can activate plunger calibration mode
mjr 9:fd65b0a94720 175 // by sending this packet. This has the same effect as pressing and holding
mjr 9:fd65b0a94720 176 // the plunger calibration button for two seconds, to allow activating this
mjr 9:fd65b0a94720 177 // mode without attaching a physical button.
mjr 9:fd65b0a94720 178 //
mjr 9:fd65b0a94720 179 // length = 8 bytes
mjr 9:fd65b0a94720 180 // byte 0 = 65 (0x41)
mjr 9:fd65b0a94720 181 // byte 1 = 2 (0x02)
mjr 9:fd65b0a94720 182 //
mjr 10:976666ffa4ef 183 // Exposure reports: the host can request a report of the full set of pixel
mjr 10:976666ffa4ef 184 // values for the next frame by sending this special packet:
mjr 10:976666ffa4ef 185 //
mjr 10:976666ffa4ef 186 // length = 8 bytes
mjr 10:976666ffa4ef 187 // byte 0 = 65 (0x41)
mjr 10:976666ffa4ef 188 // byte 1 = 3 (0x03)
mjr 10:976666ffa4ef 189 //
mjr 10:976666ffa4ef 190 // We'll respond with a series of special reports giving the exposure status.
mjr 10:976666ffa4ef 191 // Each report has the following structure:
mjr 10:976666ffa4ef 192 //
mjr 10:976666ffa4ef 193 // bytes 0:1 = 11-bit index, with high 5 bits set to 10000. For
mjr 10:976666ffa4ef 194 // example, 0x04 0x80 indicates index 4. This is the
mjr 10:976666ffa4ef 195 // starting pixel number in the report. The first report
mjr 10:976666ffa4ef 196 // will be 0x00 0x80 to indicate pixel #0.
mjr 10:976666ffa4ef 197 // bytes 2:3 = 16-bit unsigned int brightness level of pixel at index
mjr 10:976666ffa4ef 198 // bytes 4:5 = brightness of pixel at index+1
mjr 10:976666ffa4ef 199 // etc for the rest of the packet
mjr 10:976666ffa4ef 200 //
mjr 10:976666ffa4ef 201 // This still has the form of a joystick packet at the USB level, but
mjr 10:976666ffa4ef 202 // can be differentiated by the host via the status bits. It would have
mjr 10:976666ffa4ef 203 // been cleaner to use a different Report ID at the USB level, but this
mjr 10:976666ffa4ef 204 // would have necessitated a different container structure in the report
mjr 10:976666ffa4ef 205 // descriptor, which would have broken LedWiz compatibility. Given that
mjr 10:976666ffa4ef 206 // constraint, we have to re-use the joystick report type, making for
mjr 10:976666ffa4ef 207 // this somewhat kludgey approach.
mjr 6:cc35eb643e8f 208
mjr 0:5acbbe3f4cf4 209 #include "mbed.h"
mjr 6:cc35eb643e8f 210 #include "math.h"
mjr 0:5acbbe3f4cf4 211 #include "USBJoystick.h"
mjr 0:5acbbe3f4cf4 212 #include "MMA8451Q.h"
mjr 1:d913e0afb2ac 213 #include "tsl1410r.h"
mjr 1:d913e0afb2ac 214 #include "FreescaleIAP.h"
mjr 2:c174f9ee414a 215 #include "crc32.h"
mjr 2:c174f9ee414a 216
mjr 17:ab3cec0c8bf4 217 // our local configuration file
mjr 17:ab3cec0c8bf4 218 #include "config.h"
mjr 17:ab3cec0c8bf4 219
mjr 5:a70c0bce770d 220
mjr 5:a70c0bce770d 221 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 222 // utilities
mjr 17:ab3cec0c8bf4 223
mjr 17:ab3cec0c8bf4 224 // number of elements in an array
mjr 17:ab3cec0c8bf4 225 #define countof(x) (sizeof(x)/sizeof((x)[0]))
mjr 17:ab3cec0c8bf4 226
mjr 17:ab3cec0c8bf4 227
mjr 17:ab3cec0c8bf4 228 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 229 // USB device vendor ID, product ID, and version.
mjr 5:a70c0bce770d 230 //
mjr 5:a70c0bce770d 231 // We use the vendor ID for the LedWiz, so that the PC-side software can
mjr 5:a70c0bce770d 232 // identify us as capable of performing LedWiz commands. The LedWiz uses
mjr 5:a70c0bce770d 233 // a product ID value from 0xF0 to 0xFF; the last four bits identify the
mjr 5:a70c0bce770d 234 // unit number (e.g., product ID 0xF7 means unit #7). This allows multiple
mjr 5:a70c0bce770d 235 // LedWiz units to be installed in a single PC; the software on the PC side
mjr 5:a70c0bce770d 236 // uses the unit number to route commands to the devices attached to each
mjr 5:a70c0bce770d 237 // unit. On the real LedWiz, the unit number must be set in the firmware
mjr 5:a70c0bce770d 238 // at the factory; it's not configurable by the end user. Most LedWiz's
mjr 5:a70c0bce770d 239 // ship with the unit number set to 0, but the vendor will set different
mjr 5:a70c0bce770d 240 // unit numbers if requested at the time of purchase. So if you have a
mjr 5:a70c0bce770d 241 // single LedWiz already installed in your cabinet, and you didn't ask for
mjr 5:a70c0bce770d 242 // a non-default unit number, your existing LedWiz will be unit 0.
mjr 5:a70c0bce770d 243 //
mjr 6:cc35eb643e8f 244 // Note that the USB_PRODUCT_ID value set here omits the unit number. We
mjr 6:cc35eb643e8f 245 // take the unit number from the saved configuration. We provide a
mjr 6:cc35eb643e8f 246 // configuration command that can be sent via the USB connection to change
mjr 6:cc35eb643e8f 247 // the unit number, so that users can select the unit number without having
mjr 6:cc35eb643e8f 248 // to install a different version of the software. We'll combine the base
mjr 6:cc35eb643e8f 249 // product ID here with the unit number to get the actual product ID that
mjr 6:cc35eb643e8f 250 // we send to the USB controller.
mjr 5:a70c0bce770d 251 const uint16_t USB_VENDOR_ID = 0xFAFA;
mjr 6:cc35eb643e8f 252 const uint16_t USB_PRODUCT_ID = 0x00F0;
mjr 6:cc35eb643e8f 253 const uint16_t USB_VERSION_NO = 0x0006;
mjr 0:5acbbe3f4cf4 254
mjr 5:a70c0bce770d 255
mjr 6:cc35eb643e8f 256 // Joystick axis report range - we report from -JOYMAX to +JOYMAX
mjr 6:cc35eb643e8f 257 #define JOYMAX 4096
mjr 6:cc35eb643e8f 258
mjr 5:a70c0bce770d 259
mjr 17:ab3cec0c8bf4 260 // --------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 261 //
mjr 17:ab3cec0c8bf4 262 // Potentiometer configuration
mjr 17:ab3cec0c8bf4 263 //
mjr 17:ab3cec0c8bf4 264 #ifdef POT_SENSOR_ENABLED
mjr 17:ab3cec0c8bf4 265 #define IF_POT(x) x
mjr 17:ab3cec0c8bf4 266 #else
mjr 17:ab3cec0c8bf4 267 #define IF_POT(x)
mjr 17:ab3cec0c8bf4 268 #endif
mjr 9:fd65b0a94720 269
mjr 17:ab3cec0c8bf4 270
mjr 17:ab3cec0c8bf4 271 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 272 //
mjr 17:ab3cec0c8bf4 273 // On-board RGB LED elements - we use these for diagnostic displays.
mjr 17:ab3cec0c8bf4 274 //
mjr 17:ab3cec0c8bf4 275 DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);
mjr 17:ab3cec0c8bf4 276
mjr 9:fd65b0a94720 277
mjr 9:fd65b0a94720 278 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 279 //
mjr 5:a70c0bce770d 280 // LedWiz emulation
mjr 5:a70c0bce770d 281 //
mjr 5:a70c0bce770d 282
mjr 0:5acbbe3f4cf4 283 static int pbaIdx = 0;
mjr 0:5acbbe3f4cf4 284
mjr 6:cc35eb643e8f 285 // LedWiz output pin interface. We create a cover class to virtualize
mjr 6:cc35eb643e8f 286 // digital vs PWM outputs and give them a common interface. The KL25Z
mjr 6:cc35eb643e8f 287 // unfortunately doesn't have enough hardware PWM channels to support
mjr 6:cc35eb643e8f 288 // PWM on all 32 LedWiz outputs, so we provide as many PWM channels as
mjr 6:cc35eb643e8f 289 // we can (10), and fill out the rest of the outputs with plain digital
mjr 6:cc35eb643e8f 290 // outs.
mjr 6:cc35eb643e8f 291 class LwOut
mjr 6:cc35eb643e8f 292 {
mjr 6:cc35eb643e8f 293 public:
mjr 6:cc35eb643e8f 294 virtual void set(float val) = 0;
mjr 6:cc35eb643e8f 295 };
mjr 6:cc35eb643e8f 296 class LwPwmOut: public LwOut
mjr 6:cc35eb643e8f 297 {
mjr 6:cc35eb643e8f 298 public:
mjr 13:72dda449c3c0 299 LwPwmOut(PinName pin) : p(pin) { prv = -1; }
mjr 13:72dda449c3c0 300 virtual void set(float val)
mjr 13:72dda449c3c0 301 {
mjr 13:72dda449c3c0 302 if (val != prv)
mjr 13:72dda449c3c0 303 p.write(prv = val);
mjr 13:72dda449c3c0 304 }
mjr 6:cc35eb643e8f 305 PwmOut p;
mjr 13:72dda449c3c0 306 float prv;
mjr 6:cc35eb643e8f 307 };
mjr 6:cc35eb643e8f 308 class LwDigOut: public LwOut
mjr 6:cc35eb643e8f 309 {
mjr 6:cc35eb643e8f 310 public:
mjr 13:72dda449c3c0 311 LwDigOut(PinName pin) : p(pin) { prv = -1; }
mjr 13:72dda449c3c0 312 virtual void set(float val)
mjr 13:72dda449c3c0 313 {
mjr 13:72dda449c3c0 314 if (val != prv)
mjr 13:72dda449c3c0 315 p.write((prv = val) == 0.0 ? 0 : 1);
mjr 13:72dda449c3c0 316 }
mjr 6:cc35eb643e8f 317 DigitalOut p;
mjr 13:72dda449c3c0 318 float prv;
mjr 6:cc35eb643e8f 319 };
mjr 11:bd9da7088e6e 320 class LwUnusedOut: public LwOut
mjr 11:bd9da7088e6e 321 {
mjr 11:bd9da7088e6e 322 public:
mjr 11:bd9da7088e6e 323 LwUnusedOut() { }
mjr 11:bd9da7088e6e 324 virtual void set(float val) { }
mjr 11:bd9da7088e6e 325 };
mjr 6:cc35eb643e8f 326
mjr 6:cc35eb643e8f 327 // output pin array
mjr 6:cc35eb643e8f 328 static LwOut *lwPin[32];
mjr 6:cc35eb643e8f 329
mjr 6:cc35eb643e8f 330 // initialize the output pin array
mjr 6:cc35eb643e8f 331 void initLwOut()
mjr 6:cc35eb643e8f 332 {
mjr 9:fd65b0a94720 333 for (int i = 0 ; i < countof(lwPin) ; ++i)
mjr 6:cc35eb643e8f 334 {
mjr 11:bd9da7088e6e 335 PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC);
mjr 11:bd9da7088e6e 336 if (p == NC)
mjr 11:bd9da7088e6e 337 lwPin[i] = new LwUnusedOut();
mjr 11:bd9da7088e6e 338 else if (ledWizPortMap[i].isPWM)
mjr 11:bd9da7088e6e 339 lwPin[i] = new LwPwmOut(p);
mjr 11:bd9da7088e6e 340 else
mjr 11:bd9da7088e6e 341 lwPin[i] = new LwDigOut(p);
mjr 6:cc35eb643e8f 342 }
mjr 6:cc35eb643e8f 343 }
mjr 6:cc35eb643e8f 344
mjr 0:5acbbe3f4cf4 345 // on/off state for each LedWiz output
mjr 1:d913e0afb2ac 346 static uint8_t wizOn[32];
mjr 0:5acbbe3f4cf4 347
mjr 0:5acbbe3f4cf4 348 // profile (brightness/blink) state for each LedWiz output
mjr 1:d913e0afb2ac 349 static uint8_t wizVal[32] = {
mjr 13:72dda449c3c0 350 48, 48, 48, 48, 48, 48, 48, 48,
mjr 13:72dda449c3c0 351 48, 48, 48, 48, 48, 48, 48, 48,
mjr 13:72dda449c3c0 352 48, 48, 48, 48, 48, 48, 48, 48,
mjr 13:72dda449c3c0 353 48, 48, 48, 48, 48, 48, 48, 48
mjr 0:5acbbe3f4cf4 354 };
mjr 0:5acbbe3f4cf4 355
mjr 1:d913e0afb2ac 356 static float wizState(int idx)
mjr 0:5acbbe3f4cf4 357 {
mjr 13:72dda449c3c0 358 if (wizOn[idx])
mjr 13:72dda449c3c0 359 {
mjr 0:5acbbe3f4cf4 360 // on - map profile brightness state to PWM level
mjr 1:d913e0afb2ac 361 uint8_t val = wizVal[idx];
mjr 13:72dda449c3c0 362 if (val <= 48)
mjr 13:72dda449c3c0 363 {
mjr 15:944bbc29c4dd 364 // PWM brightness/intensity level. Rescale from the LedWiz
mjr 15:944bbc29c4dd 365 // 0..48 integer range to our internal PwmOut 0..1 float range.
mjr 15:944bbc29c4dd 366 // Note that on the actual LedWiz, level 48 is actually about
mjr 15:944bbc29c4dd 367 // 98% on - contrary to the LedWiz documentation, level 49 is
mjr 15:944bbc29c4dd 368 // the true 100% level. (In the documentation, level 49 is
mjr 15:944bbc29c4dd 369 // simply not a valid setting.) Even so, we treat level 48 as
mjr 15:944bbc29c4dd 370 // 100% on to match the documentation. This won't be perfectly
mjr 15:944bbc29c4dd 371 // ocmpatible with the actual LedWiz, but it makes for such a
mjr 15:944bbc29c4dd 372 // small difference in brightness (if the output device is an
mjr 15:944bbc29c4dd 373 // LED, say) that no one should notice. It seems better to
mjr 15:944bbc29c4dd 374 // err in this direction, because while the difference in
mjr 15:944bbc29c4dd 375 // brightness when attached to an LED won't be noticeable, the
mjr 15:944bbc29c4dd 376 // difference in duty cycle when attached to something like a
mjr 15:944bbc29c4dd 377 // contactor *can* be noticeable - anything less than 100%
mjr 15:944bbc29c4dd 378 // can cause a contactor or relay to chatter. There's almost
mjr 15:944bbc29c4dd 379 // never a situation where you'd want values other than 0% and
mjr 15:944bbc29c4dd 380 // 100% for a contactor or relay, so treating level 48 as 100%
mjr 15:944bbc29c4dd 381 // makes us work properly with software that's expecting the
mjr 15:944bbc29c4dd 382 // documented LedWiz behavior and therefore uses level 48 to
mjr 15:944bbc29c4dd 383 // turn a contactor or relay fully on.
mjr 13:72dda449c3c0 384 return val/48.0;
mjr 13:72dda449c3c0 385 }
mjr 13:72dda449c3c0 386 else if (val == 49)
mjr 13:72dda449c3c0 387 {
mjr 15:944bbc29c4dd 388 // 49 is undefined in the LedWiz documentation, but actually
mjr 15:944bbc29c4dd 389 // means 100% on. The documentation says that levels 1-48 are
mjr 15:944bbc29c4dd 390 // the full PWM range, but empirically it appears that the real
mjr 15:944bbc29c4dd 391 // range implemented in the firmware is 1-49. Some software on
mjr 15:944bbc29c4dd 392 // the PC side (notably DOF) is aware of this and uses level 49
mjr 15:944bbc29c4dd 393 // to mean "100% on". To ensure compatibility with existing
mjr 15:944bbc29c4dd 394 // PC-side software, we need to recognize level 49.
mjr 13:72dda449c3c0 395 return 1.0;
mjr 13:72dda449c3c0 396 }
mjr 0:5acbbe3f4cf4 397 else if (val >= 129 && val <= 132)
mjr 13:72dda449c3c0 398 {
mjr 13:72dda449c3c0 399 // Values of 129-132 select different flashing modes. We don't
mjr 13:72dda449c3c0 400 // support any of these. Instead, simply treat them as fully on.
mjr 13:72dda449c3c0 401 // Note that DOF doesn't ever use modes 129-132, as it implements
mjr 13:72dda449c3c0 402 // all flashing modes itself on the host side, so this limitation
mjr 13:72dda449c3c0 403 // won't have any effect on DOF users. You can observe it using
mjr 13:72dda449c3c0 404 // LedBlinky, though.
mjr 13:72dda449c3c0 405 return 1.0;
mjr 13:72dda449c3c0 406 }
mjr 0:5acbbe3f4cf4 407 else
mjr 13:72dda449c3c0 408 {
mjr 13:72dda449c3c0 409 // Other values are undefined in the LedWiz documentation. Hosts
mjr 13:72dda449c3c0 410 // *should* never send undefined values, since whatever behavior an
mjr 13:72dda449c3c0 411 // LedWiz unit exhibits in response is accidental and could change
mjr 13:72dda449c3c0 412 // in a future version. We'll treat all undefined values as equivalent
mjr 13:72dda449c3c0 413 // to 48 (fully on).
mjr 13:72dda449c3c0 414 //
mjr 13:72dda449c3c0 415 // NB: the 49 and 129-132 cases are broken out above for the sake
mjr 13:72dda449c3c0 416 // of documentation. We end up using 1.0 as the return value for
mjr 13:72dda449c3c0 417 // everything outside of the defined 0-48 range, so we could collapse
mjr 13:72dda449c3c0 418 // this whole thing to a single 'else' branch, but I wanted to call
mjr 13:72dda449c3c0 419 // out the specific reasons for handling the settings above as we do.
mjr 0:5acbbe3f4cf4 420 return 1.0;
mjr 13:72dda449c3c0 421 }
mjr 0:5acbbe3f4cf4 422 }
mjr 13:72dda449c3c0 423 else
mjr 13:72dda449c3c0 424 {
mjr 13:72dda449c3c0 425 // off - show at 0 intensity
mjr 13:72dda449c3c0 426 return 0.0;
mjr 0:5acbbe3f4cf4 427 }
mjr 0:5acbbe3f4cf4 428 }
mjr 0:5acbbe3f4cf4 429
mjr 1:d913e0afb2ac 430 static void updateWizOuts()
mjr 1:d913e0afb2ac 431 {
mjr 6:cc35eb643e8f 432 for (int i = 0 ; i < 32 ; ++i)
mjr 6:cc35eb643e8f 433 lwPin[i]->set(wizState(i));
mjr 1:d913e0afb2ac 434 }
mjr 1:d913e0afb2ac 435
mjr 11:bd9da7088e6e 436
mjr 11:bd9da7088e6e 437 // ---------------------------------------------------------------------------
mjr 11:bd9da7088e6e 438 //
mjr 11:bd9da7088e6e 439 // Button input
mjr 11:bd9da7088e6e 440 //
mjr 11:bd9da7088e6e 441
mjr 11:bd9da7088e6e 442 // button input map array
mjr 11:bd9da7088e6e 443 DigitalIn *buttonDigIn[32];
mjr 11:bd9da7088e6e 444
mjr 12:669df364a565 445 // timer for button reports
mjr 12:669df364a565 446 static Timer buttonTimer;
mjr 12:669df364a565 447
mjr 11:bd9da7088e6e 448 // initialize the button inputs
mjr 11:bd9da7088e6e 449 void initButtons()
mjr 11:bd9da7088e6e 450 {
mjr 11:bd9da7088e6e 451 // create the digital inputs
mjr 11:bd9da7088e6e 452 for (int i = 0 ; i < countof(buttonDigIn) ; ++i)
mjr 11:bd9da7088e6e 453 {
mjr 11:bd9da7088e6e 454 if (i < countof(buttonMap) && buttonMap[i] != NC)
mjr 11:bd9da7088e6e 455 buttonDigIn[i] = new DigitalIn(buttonMap[i]);
mjr 11:bd9da7088e6e 456 else
mjr 11:bd9da7088e6e 457 buttonDigIn[i] = 0;
mjr 11:bd9da7088e6e 458 }
mjr 12:669df364a565 459
mjr 12:669df364a565 460 // start the button timer
mjr 12:669df364a565 461 buttonTimer.start();
mjr 11:bd9da7088e6e 462 }
mjr 11:bd9da7088e6e 463
mjr 11:bd9da7088e6e 464
mjr 11:bd9da7088e6e 465 // read the raw button input state
mjr 11:bd9da7088e6e 466 uint32_t readButtonsRaw()
mjr 11:bd9da7088e6e 467 {
mjr 11:bd9da7088e6e 468 // start with all buttons off
mjr 11:bd9da7088e6e 469 uint32_t buttons = 0;
mjr 11:bd9da7088e6e 470
mjr 11:bd9da7088e6e 471 // scan the button list
mjr 11:bd9da7088e6e 472 uint32_t bit = 1;
mjr 11:bd9da7088e6e 473 for (int i = 0 ; i < countof(buttonDigIn) ; ++i, bit <<= 1)
mjr 11:bd9da7088e6e 474 {
mjr 11:bd9da7088e6e 475 if (buttonDigIn[i] != 0 && !buttonDigIn[i]->read())
mjr 11:bd9da7088e6e 476 buttons |= bit;
mjr 11:bd9da7088e6e 477 }
mjr 11:bd9da7088e6e 478
mjr 11:bd9da7088e6e 479 // return the button list
mjr 11:bd9da7088e6e 480 return buttons;
mjr 11:bd9da7088e6e 481 }
mjr 11:bd9da7088e6e 482
mjr 17:ab3cec0c8bf4 483 // Read buttons with debouncing.
mjr 17:ab3cec0c8bf4 484 //
mjr 17:ab3cec0c8bf4 485 // Debouncing is the process of filtering out transients from button
mjr 17:ab3cec0c8bf4 486 // state changes. When an electrical switch is closed or opened, the
mjr 17:ab3cec0c8bf4 487 // signal can have a brief period of instability that makes the switch
mjr 17:ab3cec0c8bf4 488 // appear to turn on and off very rapidly. This is known as "bouncing".
mjr 17:ab3cec0c8bf4 489 //
mjr 17:ab3cec0c8bf4 490 // To remove the transients, we filter the signal by requiring each
mjr 17:ab3cec0c8bf4 491 // change to stick for at least a minimum interval (we use 50ms). We
mjr 17:ab3cec0c8bf4 492 // keep a short recent history of each button's state for this purpose.
mjr 17:ab3cec0c8bf4 493 // If we see a button change state, we ignore the change if we saw the
mjr 17:ab3cec0c8bf4 494 // same button make another change within the same interval.
mjr 11:bd9da7088e6e 495 uint32_t readButtonsDebounced()
mjr 11:bd9da7088e6e 496 {
mjr 11:bd9da7088e6e 497 struct reading {
mjr 17:ab3cec0c8bf4 498 // elapsed time between this reading and the previous reading
mjr 17:ab3cec0c8bf4 499 int dt;
mjr 17:ab3cec0c8bf4 500
mjr 17:ab3cec0c8bf4 501 // Final button state for each button that changed on this
mjr 17:ab3cec0c8bf4 502 // report. OR this with a new report (after applying the
mjr 17:ab3cec0c8bf4 503 // mask 'm') to carry forward the changes that occurred in
mjr 17:ab3cec0c8bf4 504 // this report to the new report.
mjr 17:ab3cec0c8bf4 505 uint32_t b;
mjr 17:ab3cec0c8bf4 506
mjr 17:ab3cec0c8bf4 507 // Change mask at this report. This is a bit mask of the buttons
mjr 17:ab3cec0c8bf4 508 // that *didn't* change on this report. AND this mask with a
mjr 17:ab3cec0c8bf4 509 // new reading to filter buttons out of the new reading that
mjr 17:ab3cec0c8bf4 510 // changed on this report.
mjr 17:ab3cec0c8bf4 511 uint32_t m;
mjr 11:bd9da7088e6e 512 };
mjr 11:bd9da7088e6e 513 static reading readings[8]; // circular buffer of readings
mjr 11:bd9da7088e6e 514 static int ri = 0; // reading buffer index (next write position)
mjr 17:ab3cec0c8bf4 515 static int bPrv = 0; // immediately previous report
mjr 11:bd9da7088e6e 516
mjr 11:bd9da7088e6e 517 // get the write pointer
mjr 11:bd9da7088e6e 518 reading *r = &readings[ri];
mjr 11:bd9da7088e6e 519
mjr 11:bd9da7088e6e 520 // figure the time since the last reading, and read the raw button state
mjr 17:ab3cec0c8bf4 521 int ms = r->dt = buttonTimer.read_ms();
mjr 17:ab3cec0c8bf4 522 uint32_t b = readButtonsRaw();
mjr 11:bd9da7088e6e 523
mjr 11:bd9da7088e6e 524 // start timing the next interval
mjr 12:669df364a565 525 buttonTimer.reset();
mjr 11:bd9da7088e6e 526
mjr 17:ab3cec0c8bf4 527 // mask out changes for any buttons that changed state within the
mjr 17:ab3cec0c8bf4 528 // past 50ms
mjr 17:ab3cec0c8bf4 529 for (int i = 1 ; i < countof(readings) && ms < 50 ; ++i)
mjr 11:bd9da7088e6e 530 {
mjr 11:bd9da7088e6e 531 // find the next prior reading, wrapping in the circular buffer
mjr 11:bd9da7088e6e 532 int j = ri - i;
mjr 11:bd9da7088e6e 533 if (j < 0)
mjr 11:bd9da7088e6e 534 j = countof(readings) - 1;
mjr 11:bd9da7088e6e 535 reading *rj = &readings[j];
mjr 17:ab3cec0c8bf4 536
mjr 17:ab3cec0c8bf4 537 // For any button that changed state in the prior reading 'rj',
mjr 17:ab3cec0c8bf4 538 // remove any new change and restore it to its 'rj' state.
mjr 17:ab3cec0c8bf4 539 b &= rj->m;
mjr 17:ab3cec0c8bf4 540 b |= rj->b;
mjr 17:ab3cec0c8bf4 541
mjr 17:ab3cec0c8bf4 542 // add in the time to the next prior report
mjr 11:bd9da7088e6e 543 ms += rj->dt;
mjr 11:bd9da7088e6e 544 }
mjr 11:bd9da7088e6e 545
mjr 17:ab3cec0c8bf4 546 // figure which buttons changed on this report vs the prior report
mjr 17:ab3cec0c8bf4 547 uint32_t m = b ^ bPrv;
mjr 17:ab3cec0c8bf4 548
mjr 17:ab3cec0c8bf4 549 // save the change mask and changed button vector in our history entry
mjr 17:ab3cec0c8bf4 550 r->m = ~m;
mjr 17:ab3cec0c8bf4 551 r->b = b & m;
mjr 17:ab3cec0c8bf4 552
mjr 17:ab3cec0c8bf4 553 // save this as the prior report
mjr 17:ab3cec0c8bf4 554 bPrv = b;
mjr 17:ab3cec0c8bf4 555
mjr 11:bd9da7088e6e 556 // advance the write position for next time
mjr 11:bd9da7088e6e 557 ri += 1;
mjr 12:669df364a565 558 if (ri >= countof(readings))
mjr 11:bd9da7088e6e 559 ri = 0;
mjr 11:bd9da7088e6e 560
mjr 11:bd9da7088e6e 561 // return the debounced result
mjr 11:bd9da7088e6e 562 return b;
mjr 11:bd9da7088e6e 563 }
mjr 11:bd9da7088e6e 564
mjr 5:a70c0bce770d 565 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 566 //
mjr 5:a70c0bce770d 567 // Customization joystick subbclass
mjr 5:a70c0bce770d 568 //
mjr 5:a70c0bce770d 569
mjr 5:a70c0bce770d 570 class MyUSBJoystick: public USBJoystick
mjr 5:a70c0bce770d 571 {
mjr 5:a70c0bce770d 572 public:
mjr 5:a70c0bce770d 573 MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release)
mjr 5:a70c0bce770d 574 : USBJoystick(vendor_id, product_id, product_release, true)
mjr 5:a70c0bce770d 575 {
mjr 5:a70c0bce770d 576 suspended_ = false;
mjr 5:a70c0bce770d 577 }
mjr 5:a70c0bce770d 578
mjr 5:a70c0bce770d 579 // are we connected?
mjr 5:a70c0bce770d 580 int isConnected() { return configured(); }
mjr 5:a70c0bce770d 581
mjr 5:a70c0bce770d 582 // Are we in suspend mode?
mjr 5:a70c0bce770d 583 int isSuspended() const { return suspended_; }
mjr 5:a70c0bce770d 584
mjr 5:a70c0bce770d 585 protected:
mjr 5:a70c0bce770d 586 virtual void suspendStateChanged(unsigned int suspended)
mjr 5:a70c0bce770d 587 { suspended_ = suspended; }
mjr 5:a70c0bce770d 588
mjr 5:a70c0bce770d 589 // are we suspended?
mjr 5:a70c0bce770d 590 int suspended_;
mjr 5:a70c0bce770d 591 };
mjr 5:a70c0bce770d 592
mjr 5:a70c0bce770d 593 // ---------------------------------------------------------------------------
mjr 6:cc35eb643e8f 594 //
mjr 6:cc35eb643e8f 595 // Some simple math service routines
mjr 6:cc35eb643e8f 596 //
mjr 6:cc35eb643e8f 597
mjr 6:cc35eb643e8f 598 inline float square(float x) { return x*x; }
mjr 6:cc35eb643e8f 599 inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); }
mjr 6:cc35eb643e8f 600
mjr 6:cc35eb643e8f 601 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 602 //
mjr 5:a70c0bce770d 603 // Accelerometer (MMA8451Q)
mjr 5:a70c0bce770d 604 //
mjr 5:a70c0bce770d 605
mjr 5:a70c0bce770d 606 // The MMA8451Q is the KL25Z's on-board 3-axis accelerometer.
mjr 5:a70c0bce770d 607 //
mjr 5:a70c0bce770d 608 // This is a custom wrapper for the library code to interface to the
mjr 6:cc35eb643e8f 609 // MMA8451Q. This class encapsulates an interrupt handler and
mjr 6:cc35eb643e8f 610 // automatic calibration.
mjr 5:a70c0bce770d 611 //
mjr 5:a70c0bce770d 612 // We install an interrupt handler on the accelerometer "data ready"
mjr 6:cc35eb643e8f 613 // interrupt to ensure that we fetch each sample immediately when it
mjr 6:cc35eb643e8f 614 // becomes available. The accelerometer data rate is fiarly high
mjr 6:cc35eb643e8f 615 // (800 Hz), so it's not practical to keep up with it by polling.
mjr 6:cc35eb643e8f 616 // Using an interrupt handler lets us respond quickly and read
mjr 6:cc35eb643e8f 617 // every sample.
mjr 5:a70c0bce770d 618 //
mjr 6:cc35eb643e8f 619 // We automatically calibrate the accelerometer so that it's not
mjr 6:cc35eb643e8f 620 // necessary to get it exactly level when installing it, and so
mjr 6:cc35eb643e8f 621 // that it's also not necessary to calibrate it manually. There's
mjr 6:cc35eb643e8f 622 // lots of experience that tells us that manual calibration is a
mjr 6:cc35eb643e8f 623 // terrible solution, mostly because cabinets tend to shift slightly
mjr 6:cc35eb643e8f 624 // during use, requiring frequent recalibration. Instead, we
mjr 6:cc35eb643e8f 625 // calibrate automatically. We continuously monitor the acceleration
mjr 6:cc35eb643e8f 626 // data, watching for periods of constant (or nearly constant) values.
mjr 6:cc35eb643e8f 627 // Any time it appears that the machine has been at rest for a while
mjr 6:cc35eb643e8f 628 // (about 5 seconds), we'll average the readings during that rest
mjr 6:cc35eb643e8f 629 // period and use the result as the level rest position. This is
mjr 6:cc35eb643e8f 630 // is ongoing, so we'll quickly find the center point again if the
mjr 6:cc35eb643e8f 631 // machine is moved during play (by an especially aggressive bout
mjr 6:cc35eb643e8f 632 // of nudging, say).
mjr 5:a70c0bce770d 633 //
mjr 5:a70c0bce770d 634
mjr 17:ab3cec0c8bf4 635 // I2C address of the accelerometer (this is a constant of the KL25Z)
mjr 17:ab3cec0c8bf4 636 const int MMA8451_I2C_ADDRESS = (0x1d<<1);
mjr 17:ab3cec0c8bf4 637
mjr 17:ab3cec0c8bf4 638 // SCL and SDA pins for the accelerometer (constant for the KL25Z)
mjr 17:ab3cec0c8bf4 639 #define MMA8451_SCL_PIN PTE25
mjr 17:ab3cec0c8bf4 640 #define MMA8451_SDA_PIN PTE24
mjr 17:ab3cec0c8bf4 641
mjr 17:ab3cec0c8bf4 642 // Digital in pin to use for the accelerometer interrupt. For the KL25Z,
mjr 17:ab3cec0c8bf4 643 // this can be either PTA14 or PTA15, since those are the pins physically
mjr 17:ab3cec0c8bf4 644 // wired on this board to the MMA8451 interrupt controller.
mjr 17:ab3cec0c8bf4 645 #define MMA8451_INT_PIN PTA15
mjr 17:ab3cec0c8bf4 646
mjr 17:ab3cec0c8bf4 647
mjr 6:cc35eb643e8f 648 // accelerometer input history item, for gathering calibration data
mjr 6:cc35eb643e8f 649 struct AccHist
mjr 5:a70c0bce770d 650 {
mjr 6:cc35eb643e8f 651 AccHist() { x = y = d = 0.0; xtot = ytot = 0.0; cnt = 0; }
mjr 6:cc35eb643e8f 652 void set(float x, float y, AccHist *prv)
mjr 6:cc35eb643e8f 653 {
mjr 6:cc35eb643e8f 654 // save the raw position
mjr 6:cc35eb643e8f 655 this->x = x;
mjr 6:cc35eb643e8f 656 this->y = y;
mjr 6:cc35eb643e8f 657 this->d = distance(prv);
mjr 6:cc35eb643e8f 658 }
mjr 6:cc35eb643e8f 659
mjr 6:cc35eb643e8f 660 // reading for this entry
mjr 5:a70c0bce770d 661 float x, y;
mjr 5:a70c0bce770d 662
mjr 6:cc35eb643e8f 663 // distance from previous entry
mjr 6:cc35eb643e8f 664 float d;
mjr 5:a70c0bce770d 665
mjr 6:cc35eb643e8f 666 // total and count of samples averaged over this period
mjr 6:cc35eb643e8f 667 float xtot, ytot;
mjr 6:cc35eb643e8f 668 int cnt;
mjr 6:cc35eb643e8f 669
mjr 6:cc35eb643e8f 670 void clearAvg() { xtot = ytot = 0.0; cnt = 0; }
mjr 6:cc35eb643e8f 671 void addAvg(float x, float y) { xtot += x; ytot += y; ++cnt; }
mjr 6:cc35eb643e8f 672 float xAvg() const { return xtot/cnt; }
mjr 6:cc35eb643e8f 673 float yAvg() const { return ytot/cnt; }
mjr 5:a70c0bce770d 674
mjr 6:cc35eb643e8f 675 float distance(AccHist *p)
mjr 6:cc35eb643e8f 676 { return sqrt(square(p->x - x) + square(p->y - y)); }
mjr 5:a70c0bce770d 677 };
mjr 5:a70c0bce770d 678
mjr 5:a70c0bce770d 679 // accelerometer wrapper class
mjr 3:3514575d4f86 680 class Accel
mjr 3:3514575d4f86 681 {
mjr 3:3514575d4f86 682 public:
mjr 3:3514575d4f86 683 Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin)
mjr 3:3514575d4f86 684 : mma_(sda, scl, i2cAddr), intIn_(irqPin)
mjr 3:3514575d4f86 685 {
mjr 5:a70c0bce770d 686 // remember the interrupt pin assignment
mjr 5:a70c0bce770d 687 irqPin_ = irqPin;
mjr 5:a70c0bce770d 688
mjr 5:a70c0bce770d 689 // reset and initialize
mjr 5:a70c0bce770d 690 reset();
mjr 5:a70c0bce770d 691 }
mjr 5:a70c0bce770d 692
mjr 5:a70c0bce770d 693 void reset()
mjr 5:a70c0bce770d 694 {
mjr 6:cc35eb643e8f 695 // clear the center point
mjr 6:cc35eb643e8f 696 cx_ = cy_ = 0.0;
mjr 6:cc35eb643e8f 697
mjr 6:cc35eb643e8f 698 // start the calibration timer
mjr 5:a70c0bce770d 699 tCenter_.start();
mjr 5:a70c0bce770d 700 iAccPrv_ = nAccPrv_ = 0;
mjr 6:cc35eb643e8f 701
mjr 5:a70c0bce770d 702 // reset and initialize the MMA8451Q
mjr 5:a70c0bce770d 703 mma_.init();
mjr 6:cc35eb643e8f 704
mjr 6:cc35eb643e8f 705 // set the initial integrated velocity reading to zero
mjr 6:cc35eb643e8f 706 vx_ = vy_ = 0;
mjr 3:3514575d4f86 707
mjr 6:cc35eb643e8f 708 // set up our accelerometer interrupt handling
mjr 6:cc35eb643e8f 709 intIn_.rise(this, &Accel::isr);
mjr 5:a70c0bce770d 710 mma_.setInterruptMode(irqPin_ == PTA14 ? 1 : 2);
mjr 3:3514575d4f86 711
mjr 3:3514575d4f86 712 // read the current registers to clear the data ready flag
mjr 6:cc35eb643e8f 713 mma_.getAccXYZ(ax_, ay_, az_);
mjr 3:3514575d4f86 714
mjr 3:3514575d4f86 715 // start our timers
mjr 3:3514575d4f86 716 tGet_.start();
mjr 3:3514575d4f86 717 tInt_.start();
mjr 3:3514575d4f86 718 }
mjr 3:3514575d4f86 719
mjr 9:fd65b0a94720 720 void get(int &x, int &y)
mjr 3:3514575d4f86 721 {
mjr 3:3514575d4f86 722 // disable interrupts while manipulating the shared data
mjr 3:3514575d4f86 723 __disable_irq();
mjr 3:3514575d4f86 724
mjr 3:3514575d4f86 725 // read the shared data and store locally for calculations
mjr 6:cc35eb643e8f 726 float ax = ax_, ay = ay_;
mjr 6:cc35eb643e8f 727 float vx = vx_, vy = vy_;
mjr 5:a70c0bce770d 728
mjr 6:cc35eb643e8f 729 // reset the velocity sum for the next run
mjr 6:cc35eb643e8f 730 vx_ = vy_ = 0;
mjr 3:3514575d4f86 731
mjr 3:3514575d4f86 732 // get the time since the last get() sample
mjr 3:3514575d4f86 733 float dt = tGet_.read_us()/1.0e6;
mjr 3:3514575d4f86 734 tGet_.reset();
mjr 3:3514575d4f86 735
mjr 3:3514575d4f86 736 // done manipulating the shared data
mjr 3:3514575d4f86 737 __enable_irq();
mjr 3:3514575d4f86 738
mjr 6:cc35eb643e8f 739 // adjust the readings for the integration time
mjr 6:cc35eb643e8f 740 vx /= dt;
mjr 6:cc35eb643e8f 741 vy /= dt;
mjr 6:cc35eb643e8f 742
mjr 6:cc35eb643e8f 743 // add this sample to the current calibration interval's running total
mjr 6:cc35eb643e8f 744 AccHist *p = accPrv_ + iAccPrv_;
mjr 6:cc35eb643e8f 745 p->addAvg(ax, ay);
mjr 6:cc35eb643e8f 746
mjr 5:a70c0bce770d 747 // check for auto-centering every so often
mjr 5:a70c0bce770d 748 if (tCenter_.read_ms() > 1000)
mjr 5:a70c0bce770d 749 {
mjr 5:a70c0bce770d 750 // add the latest raw sample to the history list
mjr 6:cc35eb643e8f 751 AccHist *prv = p;
mjr 5:a70c0bce770d 752 iAccPrv_ = (iAccPrv_ + 1) % maxAccPrv;
mjr 6:cc35eb643e8f 753 p = accPrv_ + iAccPrv_;
mjr 6:cc35eb643e8f 754 p->set(ax, ay, prv);
mjr 5:a70c0bce770d 755
mjr 5:a70c0bce770d 756 // if we have a full complement, check for stability
mjr 5:a70c0bce770d 757 if (nAccPrv_ >= maxAccPrv)
mjr 5:a70c0bce770d 758 {
mjr 5:a70c0bce770d 759 // check if we've been stable for all recent samples
mjr 6:cc35eb643e8f 760 static const float accTol = .01;
mjr 6:cc35eb643e8f 761 AccHist *p0 = accPrv_;
mjr 6:cc35eb643e8f 762 if (p0[0].d < accTol
mjr 6:cc35eb643e8f 763 && p0[1].d < accTol
mjr 6:cc35eb643e8f 764 && p0[2].d < accTol
mjr 6:cc35eb643e8f 765 && p0[3].d < accTol
mjr 6:cc35eb643e8f 766 && p0[4].d < accTol)
mjr 5:a70c0bce770d 767 {
mjr 6:cc35eb643e8f 768 // Figure the new calibration point as the average of
mjr 6:cc35eb643e8f 769 // the samples over the rest period
mjr 6:cc35eb643e8f 770 cx_ = (p0[0].xAvg() + p0[1].xAvg() + p0[2].xAvg() + p0[3].xAvg() + p0[4].xAvg())/5.0;
mjr 6:cc35eb643e8f 771 cy_ = (p0[0].yAvg() + p0[1].yAvg() + p0[2].yAvg() + p0[3].yAvg() + p0[4].yAvg())/5.0;
mjr 5:a70c0bce770d 772 }
mjr 5:a70c0bce770d 773 }
mjr 5:a70c0bce770d 774 else
mjr 5:a70c0bce770d 775 {
mjr 5:a70c0bce770d 776 // not enough samples yet; just up the count
mjr 5:a70c0bce770d 777 ++nAccPrv_;
mjr 5:a70c0bce770d 778 }
mjr 6:cc35eb643e8f 779
mjr 6:cc35eb643e8f 780 // clear the new item's running totals
mjr 6:cc35eb643e8f 781 p->clearAvg();
mjr 5:a70c0bce770d 782
mjr 5:a70c0bce770d 783 // reset the timer
mjr 5:a70c0bce770d 784 tCenter_.reset();
mjr 5:a70c0bce770d 785 }
mjr 5:a70c0bce770d 786
mjr 6:cc35eb643e8f 787 // report our integrated velocity reading in x,y
mjr 6:cc35eb643e8f 788 x = rawToReport(vx);
mjr 6:cc35eb643e8f 789 y = rawToReport(vy);
mjr 5:a70c0bce770d 790
mjr 6:cc35eb643e8f 791 #ifdef DEBUG_PRINTF
mjr 6:cc35eb643e8f 792 if (x != 0 || y != 0)
mjr 6:cc35eb643e8f 793 printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt);
mjr 6:cc35eb643e8f 794 #endif
mjr 3:3514575d4f86 795 }
mjr 3:3514575d4f86 796
mjr 3:3514575d4f86 797 private:
mjr 6:cc35eb643e8f 798 // adjust a raw acceleration figure to a usb report value
mjr 6:cc35eb643e8f 799 int rawToReport(float v)
mjr 5:a70c0bce770d 800 {
mjr 6:cc35eb643e8f 801 // scale to the joystick report range and round to integer
mjr 6:cc35eb643e8f 802 int i = int(round(v*JOYMAX));
mjr 5:a70c0bce770d 803
mjr 6:cc35eb643e8f 804 // if it's near the center, scale it roughly as 20*(i/20)^2,
mjr 6:cc35eb643e8f 805 // to suppress noise near the rest position
mjr 6:cc35eb643e8f 806 static const int filter[] = {
mjr 6:cc35eb643e8f 807 -18, -16, -14, -13, -11, -10, -8, -7, -6, -5, -4, -3, -2, -2, -1, -1, 0, 0, 0, 0,
mjr 6:cc35eb643e8f 808 0,
mjr 6:cc35eb643e8f 809 0, 0, 0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 18
mjr 6:cc35eb643e8f 810 };
mjr 6:cc35eb643e8f 811 return (i > 20 || i < -20 ? i : filter[i+20]);
mjr 5:a70c0bce770d 812 }
mjr 5:a70c0bce770d 813
mjr 3:3514575d4f86 814 // interrupt handler
mjr 3:3514575d4f86 815 void isr()
mjr 3:3514575d4f86 816 {
mjr 3:3514575d4f86 817 // Read the axes. Note that we have to read all three axes
mjr 3:3514575d4f86 818 // (even though we only really use x and y) in order to clear
mjr 3:3514575d4f86 819 // the "data ready" status bit in the accelerometer. The
mjr 3:3514575d4f86 820 // interrupt only occurs when the "ready" bit transitions from
mjr 3:3514575d4f86 821 // off to on, so we have to make sure it's off.
mjr 5:a70c0bce770d 822 float x, y, z;
mjr 5:a70c0bce770d 823 mma_.getAccXYZ(x, y, z);
mjr 3:3514575d4f86 824
mjr 3:3514575d4f86 825 // calculate the time since the last interrupt
mjr 3:3514575d4f86 826 float dt = tInt_.read_us()/1.0e6;
mjr 3:3514575d4f86 827 tInt_.reset();
mjr 6:cc35eb643e8f 828
mjr 6:cc35eb643e8f 829 // integrate the time slice from the previous reading to this reading
mjr 6:cc35eb643e8f 830 vx_ += (x + ax_ - 2*cx_)*dt/2;
mjr 6:cc35eb643e8f 831 vy_ += (y + ay_ - 2*cy_)*dt/2;
mjr 3:3514575d4f86 832
mjr 6:cc35eb643e8f 833 // store the updates
mjr 6:cc35eb643e8f 834 ax_ = x;
mjr 6:cc35eb643e8f 835 ay_ = y;
mjr 6:cc35eb643e8f 836 az_ = z;
mjr 3:3514575d4f86 837 }
mjr 3:3514575d4f86 838
mjr 3:3514575d4f86 839 // underlying accelerometer object
mjr 3:3514575d4f86 840 MMA8451Q mma_;
mjr 3:3514575d4f86 841
mjr 5:a70c0bce770d 842 // last raw acceleration readings
mjr 6:cc35eb643e8f 843 float ax_, ay_, az_;
mjr 5:a70c0bce770d 844
mjr 6:cc35eb643e8f 845 // integrated velocity reading since last get()
mjr 6:cc35eb643e8f 846 float vx_, vy_;
mjr 6:cc35eb643e8f 847
mjr 3:3514575d4f86 848 // timer for measuring time between get() samples
mjr 3:3514575d4f86 849 Timer tGet_;
mjr 3:3514575d4f86 850
mjr 3:3514575d4f86 851 // timer for measuring time between interrupts
mjr 3:3514575d4f86 852 Timer tInt_;
mjr 5:a70c0bce770d 853
mjr 6:cc35eb643e8f 854 // Calibration reference point for accelerometer. This is the
mjr 6:cc35eb643e8f 855 // average reading on the accelerometer when in the neutral position
mjr 6:cc35eb643e8f 856 // at rest.
mjr 6:cc35eb643e8f 857 float cx_, cy_;
mjr 5:a70c0bce770d 858
mjr 5:a70c0bce770d 859 // timer for atuo-centering
mjr 5:a70c0bce770d 860 Timer tCenter_;
mjr 6:cc35eb643e8f 861
mjr 6:cc35eb643e8f 862 // Auto-centering history. This is a separate history list that
mjr 6:cc35eb643e8f 863 // records results spaced out sparesely over time, so that we can
mjr 6:cc35eb643e8f 864 // watch for long-lasting periods of rest. When we observe nearly
mjr 6:cc35eb643e8f 865 // no motion for an extended period (on the order of 5 seconds), we
mjr 6:cc35eb643e8f 866 // take this to mean that the cabinet is at rest in its neutral
mjr 6:cc35eb643e8f 867 // position, so we take this as the calibration zero point for the
mjr 6:cc35eb643e8f 868 // accelerometer. We update this history continuously, which allows
mjr 6:cc35eb643e8f 869 // us to continuously re-calibrate the accelerometer. This ensures
mjr 6:cc35eb643e8f 870 // that we'll automatically adjust to any actual changes in the
mjr 6:cc35eb643e8f 871 // cabinet's orientation (e.g., if it gets moved slightly by an
mjr 6:cc35eb643e8f 872 // especially strong nudge) as well as any systematic drift in the
mjr 6:cc35eb643e8f 873 // accelerometer measurement bias (e.g., from temperature changes).
mjr 5:a70c0bce770d 874 int iAccPrv_, nAccPrv_;
mjr 5:a70c0bce770d 875 static const int maxAccPrv = 5;
mjr 6:cc35eb643e8f 876 AccHist accPrv_[maxAccPrv];
mjr 6:cc35eb643e8f 877
mjr 5:a70c0bce770d 878 // interurupt pin name
mjr 5:a70c0bce770d 879 PinName irqPin_;
mjr 5:a70c0bce770d 880
mjr 5:a70c0bce770d 881 // interrupt router
mjr 5:a70c0bce770d 882 InterruptIn intIn_;
mjr 3:3514575d4f86 883 };
mjr 3:3514575d4f86 884
mjr 5:a70c0bce770d 885
mjr 5:a70c0bce770d 886 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 887 //
mjr 14:df700b22ca08 888 // Clear the I2C bus for the MMA8451Q. This seems necessary some of the time
mjr 5:a70c0bce770d 889 // for reasons that aren't clear to me. Doing a hard power cycle has the same
mjr 5:a70c0bce770d 890 // effect, but when we do a soft reset, the hardware sometimes seems to leave
mjr 5:a70c0bce770d 891 // the MMA's SDA line stuck low. Forcing a series of 9 clock pulses through
mjr 14:df700b22ca08 892 // the SCL line is supposed to clear this condition. I'm not convinced this
mjr 14:df700b22ca08 893 // actually works with the way this component is wired on the KL25Z, but it
mjr 14:df700b22ca08 894 // seems harmless, so we'll do it on reset in case it does some good. What
mjr 14:df700b22ca08 895 // we really seem to need is a way to power cycle the MMA8451Q if it ever
mjr 14:df700b22ca08 896 // gets stuck, but this is simply not possible in software on the KL25Z.
mjr 14:df700b22ca08 897 //
mjr 14:df700b22ca08 898 // If the accelerometer does get stuck, and a software reboot doesn't reset
mjr 14:df700b22ca08 899 // it, the only workaround is to manually power cycle the whole KL25Z by
mjr 14:df700b22ca08 900 // unplugging both of its USB connections.
mjr 5:a70c0bce770d 901 //
mjr 5:a70c0bce770d 902 void clear_i2c()
mjr 5:a70c0bce770d 903 {
mjr 5:a70c0bce770d 904 // assume a general-purpose output pin to the I2C clock
mjr 5:a70c0bce770d 905 DigitalOut scl(MMA8451_SCL_PIN);
mjr 5:a70c0bce770d 906 DigitalIn sda(MMA8451_SDA_PIN);
mjr 5:a70c0bce770d 907
mjr 5:a70c0bce770d 908 // clock the SCL 9 times
mjr 5:a70c0bce770d 909 for (int i = 0 ; i < 9 ; ++i)
mjr 5:a70c0bce770d 910 {
mjr 5:a70c0bce770d 911 scl = 1;
mjr 5:a70c0bce770d 912 wait_us(20);
mjr 5:a70c0bce770d 913 scl = 0;
mjr 5:a70c0bce770d 914 wait_us(20);
mjr 5:a70c0bce770d 915 }
mjr 5:a70c0bce770d 916 }
mjr 14:df700b22ca08 917
mjr 14:df700b22ca08 918 // ---------------------------------------------------------------------------
mjr 14:df700b22ca08 919 //
mjr 14:df700b22ca08 920 // CCD read interval callback. When reading the CCD, we'll call this
mjr 14:df700b22ca08 921 // several times over the course of the read loop to refresh the button
mjr 14:df700b22ca08 922 // states. This allows us to debounce the buttons while the long CCD
mjr 14:df700b22ca08 923 // read cycle is taking place, so that we can reliably report button
mjr 14:df700b22ca08 924 // states after each CCD read cycle. (The read cycle takes about 30ms,
mjr 14:df700b22ca08 925 // which should be enough time to reliably debounce the buttons.)
mjr 14:df700b22ca08 926 //
mjr 14:df700b22ca08 927 void ccdReadCB(void *)
mjr 14:df700b22ca08 928 {
mjr 14:df700b22ca08 929 // read the keyboard
mjr 14:df700b22ca08 930 readButtonsDebounced();
mjr 14:df700b22ca08 931 }
mjr 5:a70c0bce770d 932
mjr 5:a70c0bce770d 933 // ---------------------------------------------------------------------------
mjr 5:a70c0bce770d 934 //
mjr 17:ab3cec0c8bf4 935 // Include the appropriate plunger sensor definition. This will define a
mjr 17:ab3cec0c8bf4 936 // class called PlungerSensor, with a standard interface that we use in
mjr 17:ab3cec0c8bf4 937 // the main loop below. This is *kind of* like a virtual class interface,
mjr 17:ab3cec0c8bf4 938 // but it actually defines the methods statically, which is a little more
mjr 17:ab3cec0c8bf4 939 // efficient at run-time. There's no need for a true virtual interface
mjr 17:ab3cec0c8bf4 940 // because we don't need to be able to change sensor types on the fly.
mjr 17:ab3cec0c8bf4 941 //
mjr 17:ab3cec0c8bf4 942
mjr 17:ab3cec0c8bf4 943 #ifdef ENABLE_CCD_SENSOR
mjr 17:ab3cec0c8bf4 944 #include "ccdSensor.h"
mjr 17:ab3cec0c8bf4 945 #elif ENABLE_POT_SENSOR
mjr 17:ab3cec0c8bf4 946 #include "potSensor.h"
mjr 17:ab3cec0c8bf4 947 #else
mjr 17:ab3cec0c8bf4 948 #include "nullSensor.h"
mjr 17:ab3cec0c8bf4 949 #endif
mjr 17:ab3cec0c8bf4 950
mjr 17:ab3cec0c8bf4 951
mjr 17:ab3cec0c8bf4 952 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 953 //
mjr 17:ab3cec0c8bf4 954 // Non-volatile memory (NVM)
mjr 17:ab3cec0c8bf4 955 //
mjr 17:ab3cec0c8bf4 956
mjr 17:ab3cec0c8bf4 957 // Structure defining our NVM storage layout. We store a small
mjr 17:ab3cec0c8bf4 958 // amount of persistent data in flash memory to retain calibration
mjr 17:ab3cec0c8bf4 959 // data when powered off.
mjr 17:ab3cec0c8bf4 960 struct NVM
mjr 17:ab3cec0c8bf4 961 {
mjr 17:ab3cec0c8bf4 962 // checksum - we use this to determine if the flash record
mjr 17:ab3cec0c8bf4 963 // has been properly initialized
mjr 17:ab3cec0c8bf4 964 uint32_t checksum;
mjr 17:ab3cec0c8bf4 965
mjr 17:ab3cec0c8bf4 966 // signature value
mjr 17:ab3cec0c8bf4 967 static const uint32_t SIGNATURE = 0x4D4A522A;
mjr 17:ab3cec0c8bf4 968 static const uint16_t VERSION = 0x0003;
mjr 17:ab3cec0c8bf4 969
mjr 17:ab3cec0c8bf4 970 // Is the data structure valid? We test the signature and
mjr 17:ab3cec0c8bf4 971 // checksum to determine if we've been properly stored.
mjr 17:ab3cec0c8bf4 972 int valid() const
mjr 17:ab3cec0c8bf4 973 {
mjr 17:ab3cec0c8bf4 974 return (d.sig == SIGNATURE
mjr 17:ab3cec0c8bf4 975 && d.vsn == VERSION
mjr 17:ab3cec0c8bf4 976 && d.sz == sizeof(NVM)
mjr 17:ab3cec0c8bf4 977 && checksum == CRC32(&d, sizeof(d)));
mjr 17:ab3cec0c8bf4 978 }
mjr 17:ab3cec0c8bf4 979
mjr 17:ab3cec0c8bf4 980 // save to non-volatile memory
mjr 17:ab3cec0c8bf4 981 void save(FreescaleIAP &iap, int addr)
mjr 17:ab3cec0c8bf4 982 {
mjr 17:ab3cec0c8bf4 983 // update the checksum and structure size
mjr 17:ab3cec0c8bf4 984 checksum = CRC32(&d, sizeof(d));
mjr 17:ab3cec0c8bf4 985 d.sz = sizeof(NVM);
mjr 17:ab3cec0c8bf4 986
mjr 17:ab3cec0c8bf4 987 // erase the sector
mjr 17:ab3cec0c8bf4 988 iap.erase_sector(addr);
mjr 17:ab3cec0c8bf4 989
mjr 17:ab3cec0c8bf4 990 // save the data
mjr 17:ab3cec0c8bf4 991 iap.program_flash(addr, this, sizeof(*this));
mjr 17:ab3cec0c8bf4 992 }
mjr 17:ab3cec0c8bf4 993
mjr 17:ab3cec0c8bf4 994 // reset calibration data for calibration mode
mjr 17:ab3cec0c8bf4 995 void resetPlunger()
mjr 17:ab3cec0c8bf4 996 {
mjr 17:ab3cec0c8bf4 997 // set extremes for the calibration data
mjr 17:ab3cec0c8bf4 998 d.plungerMax = 0;
mjr 17:ab3cec0c8bf4 999 d.plungerZero = npix;
mjr 17:ab3cec0c8bf4 1000 d.plungerMin = npix;
mjr 17:ab3cec0c8bf4 1001 }
mjr 17:ab3cec0c8bf4 1002
mjr 17:ab3cec0c8bf4 1003 // stored data (excluding the checksum)
mjr 17:ab3cec0c8bf4 1004 struct
mjr 17:ab3cec0c8bf4 1005 {
mjr 17:ab3cec0c8bf4 1006 // Signature, structure version, and structure size - further verification
mjr 17:ab3cec0c8bf4 1007 // that we have valid initialized data. The size is a simple proxy for a
mjr 17:ab3cec0c8bf4 1008 // structure version, as the most common type of change to the structure as
mjr 17:ab3cec0c8bf4 1009 // the software evolves will be the addition of new elements. We also
mjr 17:ab3cec0c8bf4 1010 // provide an explicit version number that we can update manually if we
mjr 17:ab3cec0c8bf4 1011 // make any changes that don't affect the structure size but would affect
mjr 17:ab3cec0c8bf4 1012 // compatibility with a saved record (e.g., swapping two existing elements).
mjr 17:ab3cec0c8bf4 1013 uint32_t sig;
mjr 17:ab3cec0c8bf4 1014 uint16_t vsn;
mjr 17:ab3cec0c8bf4 1015 int sz;
mjr 17:ab3cec0c8bf4 1016
mjr 17:ab3cec0c8bf4 1017 // has the plunger been manually calibrated?
mjr 17:ab3cec0c8bf4 1018 int plungerCal;
mjr 17:ab3cec0c8bf4 1019
mjr 17:ab3cec0c8bf4 1020 // Plunger calibration min, zero, and max. The zero point is the
mjr 17:ab3cec0c8bf4 1021 // rest position (aka park position), where it's in equilibrium between
mjr 17:ab3cec0c8bf4 1022 // the main spring and the barrel spring. It can travel a small distance
mjr 17:ab3cec0c8bf4 1023 // forward of the rest position, because the barrel spring can be
mjr 17:ab3cec0c8bf4 1024 // compressed by the user pushing on the plunger or by the momentum
mjr 17:ab3cec0c8bf4 1025 // of a release motion. The minimum is the maximum forward point where
mjr 17:ab3cec0c8bf4 1026 // the barrel spring can't be compressed any further.
mjr 17:ab3cec0c8bf4 1027 int plungerMin;
mjr 17:ab3cec0c8bf4 1028 int plungerZero;
mjr 17:ab3cec0c8bf4 1029 int plungerMax;
mjr 17:ab3cec0c8bf4 1030
mjr 17:ab3cec0c8bf4 1031 // is the plunger sensor enabled?
mjr 17:ab3cec0c8bf4 1032 int plungerEnabled;
mjr 17:ab3cec0c8bf4 1033
mjr 17:ab3cec0c8bf4 1034 // LedWiz unit number
mjr 17:ab3cec0c8bf4 1035 uint8_t ledWizUnitNo;
mjr 17:ab3cec0c8bf4 1036 } d;
mjr 17:ab3cec0c8bf4 1037 };
mjr 17:ab3cec0c8bf4 1038
mjr 17:ab3cec0c8bf4 1039
mjr 17:ab3cec0c8bf4 1040 // ---------------------------------------------------------------------------
mjr 17:ab3cec0c8bf4 1041 //
mjr 5:a70c0bce770d 1042 // Main program loop. This is invoked on startup and runs forever. Our
mjr 5:a70c0bce770d 1043 // main work is to read our devices (the accelerometer and the CCD), process
mjr 5:a70c0bce770d 1044 // the readings into nudge and plunger position data, and send the results
mjr 5:a70c0bce770d 1045 // to the host computer via the USB joystick interface. We also monitor
mjr 5:a70c0bce770d 1046 // the USB connection for incoming LedWiz commands and process those into
mjr 5:a70c0bce770d 1047 // port outputs.
mjr 5:a70c0bce770d 1048 //
mjr 0:5acbbe3f4cf4 1049 int main(void)
mjr 0:5acbbe3f4cf4 1050 {
mjr 1:d913e0afb2ac 1051 // turn off our on-board indicator LED
mjr 4:02c7cd7b2183 1052 ledR = 1;
mjr 4:02c7cd7b2183 1053 ledG = 1;
mjr 4:02c7cd7b2183 1054 ledB = 1;
mjr 1:d913e0afb2ac 1055
mjr 6:cc35eb643e8f 1056 // initialize the LedWiz ports
mjr 6:cc35eb643e8f 1057 initLwOut();
mjr 6:cc35eb643e8f 1058
mjr 11:bd9da7088e6e 1059 // initialize the button input ports
mjr 11:bd9da7088e6e 1060 initButtons();
mjr 11:bd9da7088e6e 1061
mjr 6:cc35eb643e8f 1062 // we don't need a reset yet
mjr 6:cc35eb643e8f 1063 bool needReset = false;
mjr 6:cc35eb643e8f 1064
mjr 5:a70c0bce770d 1065 // clear the I2C bus for the accelerometer
mjr 5:a70c0bce770d 1066 clear_i2c();
mjr 5:a70c0bce770d 1067
mjr 2:c174f9ee414a 1068 // set up a flash memory controller
mjr 2:c174f9ee414a 1069 FreescaleIAP iap;
mjr 2:c174f9ee414a 1070
mjr 2:c174f9ee414a 1071 // use the last sector of flash for our non-volatile memory structure
mjr 2:c174f9ee414a 1072 int flash_addr = (iap.flash_size() - SECTOR_SIZE);
mjr 2:c174f9ee414a 1073 NVM *flash = (NVM *)flash_addr;
mjr 2:c174f9ee414a 1074 NVM cfg;
mjr 2:c174f9ee414a 1075
mjr 2:c174f9ee414a 1076 // check for valid flash
mjr 6:cc35eb643e8f 1077 bool flash_valid = flash->valid();
mjr 2:c174f9ee414a 1078
mjr 2:c174f9ee414a 1079 // if the flash is valid, load it; otherwise initialize to defaults
mjr 2:c174f9ee414a 1080 if (flash_valid) {
mjr 2:c174f9ee414a 1081 memcpy(&cfg, flash, sizeof(cfg));
mjr 6:cc35eb643e8f 1082 printf("Flash restored: plunger cal=%d, min=%d, zero=%d, max=%d\r\n",
mjr 6:cc35eb643e8f 1083 cfg.d.plungerCal, cfg.d.plungerMin, cfg.d.plungerZero, cfg.d.plungerMax);
mjr 2:c174f9ee414a 1084 }
mjr 2:c174f9ee414a 1085 else {
mjr 2:c174f9ee414a 1086 printf("Factory reset\r\n");
mjr 2:c174f9ee414a 1087 cfg.d.sig = cfg.SIGNATURE;
mjr 2:c174f9ee414a 1088 cfg.d.vsn = cfg.VERSION;
mjr 6:cc35eb643e8f 1089 cfg.d.plungerCal = 0;
mjr 17:ab3cec0c8bf4 1090 cfg.d.plungerMin = 0; // assume we can go all the way forward...
mjr 17:ab3cec0c8bf4 1091 cfg.d.plungerMax = npix; // ...and all the way back
mjr 17:ab3cec0c8bf4 1092 cfg.d.plungerZero = npix/6; // the rest position is usually around 1/2" back
mjr 6:cc35eb643e8f 1093 cfg.d.ledWizUnitNo = DEFAULT_LEDWIZ_UNIT_NUMBER;
mjr 17:ab3cec0c8bf4 1094 cfg.d.plungerEnabled = true;
mjr 2:c174f9ee414a 1095 }
mjr 1:d913e0afb2ac 1096
mjr 6:cc35eb643e8f 1097 // Create the joystick USB client. Note that we use the LedWiz unit
mjr 6:cc35eb643e8f 1098 // number from the saved configuration.
mjr 6:cc35eb643e8f 1099 MyUSBJoystick js(
mjr 6:cc35eb643e8f 1100 USB_VENDOR_ID,
mjr 6:cc35eb643e8f 1101 USB_PRODUCT_ID | cfg.d.ledWizUnitNo,
mjr 6:cc35eb643e8f 1102 USB_VERSION_NO);
mjr 17:ab3cec0c8bf4 1103
mjr 17:ab3cec0c8bf4 1104 // last report timer - we use this to throttle reports, since VP
mjr 17:ab3cec0c8bf4 1105 // doesn't want to hear from us more than about every 10ms
mjr 17:ab3cec0c8bf4 1106 Timer reportTimer;
mjr 17:ab3cec0c8bf4 1107 reportTimer.start();
mjr 17:ab3cec0c8bf4 1108
mjr 17:ab3cec0c8bf4 1109 // initialize the calibration buttons, if present
mjr 17:ab3cec0c8bf4 1110 DigitalIn *calBtn = (CAL_BUTTON_PIN == NC ? 0 : new DigitalIn(CAL_BUTTON_PIN));
mjr 17:ab3cec0c8bf4 1111 DigitalOut *calBtnLed = (CAL_BUTTON_LED == NC ? 0 : new DigitalOut(CAL_BUTTON_LED));
mjr 6:cc35eb643e8f 1112
mjr 1:d913e0afb2ac 1113 // plunger calibration button debounce timer
mjr 1:d913e0afb2ac 1114 Timer calBtnTimer;
mjr 1:d913e0afb2ac 1115 calBtnTimer.start();
mjr 1:d913e0afb2ac 1116 int calBtnLit = false;
mjr 1:d913e0afb2ac 1117
mjr 1:d913e0afb2ac 1118 // Calibration button state:
mjr 1:d913e0afb2ac 1119 // 0 = not pushed
mjr 1:d913e0afb2ac 1120 // 1 = pushed, not yet debounced
mjr 1:d913e0afb2ac 1121 // 2 = pushed, debounced, waiting for hold time
mjr 1:d913e0afb2ac 1122 // 3 = pushed, hold time completed - in calibration mode
mjr 1:d913e0afb2ac 1123 int calBtnState = 0;
mjr 1:d913e0afb2ac 1124
mjr 1:d913e0afb2ac 1125 // set up a timer for our heartbeat indicator
mjr 1:d913e0afb2ac 1126 Timer hbTimer;
mjr 1:d913e0afb2ac 1127 hbTimer.start();
mjr 1:d913e0afb2ac 1128 int hb = 0;
mjr 5:a70c0bce770d 1129 uint16_t hbcnt = 0;
mjr 1:d913e0afb2ac 1130
mjr 1:d913e0afb2ac 1131 // set a timer for accelerometer auto-centering
mjr 1:d913e0afb2ac 1132 Timer acTimer;
mjr 1:d913e0afb2ac 1133 acTimer.start();
mjr 1:d913e0afb2ac 1134
mjr 0:5acbbe3f4cf4 1135 // create the accelerometer object
mjr 5:a70c0bce770d 1136 Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN);
mjr 0:5acbbe3f4cf4 1137
mjr 17:ab3cec0c8bf4 1138 // last accelerometer report, in joystick units (we report the nudge
mjr 17:ab3cec0c8bf4 1139 // acceleration via the joystick x & y axes, per the VP convention)
mjr 17:ab3cec0c8bf4 1140 int x = 0, y = 0;
mjr 17:ab3cec0c8bf4 1141
mjr 17:ab3cec0c8bf4 1142 // create our plunger sensor object
mjr 17:ab3cec0c8bf4 1143 PlungerSensor plungerSensor;
mjr 17:ab3cec0c8bf4 1144
mjr 17:ab3cec0c8bf4 1145 // last plunger report position, in 'npix' normalized pixel units
mjr 17:ab3cec0c8bf4 1146 int pos = 0;
mjr 17:ab3cec0c8bf4 1147
mjr 17:ab3cec0c8bf4 1148 // last plunger report, in joystick units (we report the plunger as the
mjr 17:ab3cec0c8bf4 1149 // "z" axis of the joystick, per the VP convention)
mjr 17:ab3cec0c8bf4 1150 int z = 0;
mjr 17:ab3cec0c8bf4 1151
mjr 17:ab3cec0c8bf4 1152 // most recent prior plunger readings, for tracking release events(z0 is
mjr 17:ab3cec0c8bf4 1153 // reading just before the last one we reported, z1 is the one before that,
mjr 17:ab3cec0c8bf4 1154 // z2 the next before that)
mjr 17:ab3cec0c8bf4 1155 int z0 = 0, z1 = 0, z2 = 0;
mjr 17:ab3cec0c8bf4 1156
mjr 17:ab3cec0c8bf4 1157 // Simulated "bounce" position when firing. We model the bounce off of
mjr 17:ab3cec0c8bf4 1158 // the barrel spring when the plunger is released as proportional to the
mjr 17:ab3cec0c8bf4 1159 // distance it was retracted just before being released.
mjr 17:ab3cec0c8bf4 1160 int zBounce = 0;
mjr 2:c174f9ee414a 1161
mjr 17:ab3cec0c8bf4 1162 // Simulated Launch Ball button state. If a "ZB Launch Ball" port is
mjr 17:ab3cec0c8bf4 1163 // defined for our LedWiz port mapping, any time that port is turned ON,
mjr 17:ab3cec0c8bf4 1164 // we'll simulate pushing the Launch Ball button if the player pulls
mjr 17:ab3cec0c8bf4 1165 // back and releases the plunger, or simply pushes on the plunger from
mjr 17:ab3cec0c8bf4 1166 // the rest position. This allows the plunger to be used in lieu of a
mjr 17:ab3cec0c8bf4 1167 // physical Launch Ball button for tables that don't have plungers.
mjr 17:ab3cec0c8bf4 1168 //
mjr 17:ab3cec0c8bf4 1169 // States:
mjr 17:ab3cec0c8bf4 1170 // 0 = default
mjr 17:ab3cec0c8bf4 1171 // 1 = cocked (plunger has been pulled back about 1" from state 0)
mjr 17:ab3cec0c8bf4 1172 // 2 = uncocked (plunger is pulled back less than 1" from state 1)
mjr 17:ab3cec0c8bf4 1173 // 3 = launching (plunger has been released from state 1 or 2, or
mjr 17:ab3cec0c8bf4 1174 // pushed forward about 1/4" from state 0)
mjr 17:ab3cec0c8bf4 1175 // 4 = launching, plunger is no longer pushed forward
mjr 17:ab3cec0c8bf4 1176 int lbState = 0;
mjr 6:cc35eb643e8f 1177
mjr 17:ab3cec0c8bf4 1178 // Time since last lbState transition. Some of the states are time-
mjr 17:ab3cec0c8bf4 1179 // sensitive. In the "uncocked" state, we'll return to state 0 if
mjr 17:ab3cec0c8bf4 1180 // we remain in this state for more than a few milliseconds, since
mjr 17:ab3cec0c8bf4 1181 // it indicates that the plunger is being slowly returned to rest
mjr 17:ab3cec0c8bf4 1182 // rather than released. In the "launching" state, we need to release
mjr 17:ab3cec0c8bf4 1183 // the Launch Ball button after a moment, and we need to wait for
mjr 17:ab3cec0c8bf4 1184 // the plunger to come to rest before returning to state 0.
mjr 17:ab3cec0c8bf4 1185 Timer lbTimer;
mjr 17:ab3cec0c8bf4 1186 lbTimer.start();
mjr 17:ab3cec0c8bf4 1187
mjr 17:ab3cec0c8bf4 1188 // Simulated button states. This is a vector of button states
mjr 17:ab3cec0c8bf4 1189 // for the simulated buttons. We combine this with the physical
mjr 17:ab3cec0c8bf4 1190 // button states on each USB joystick report, so we will report
mjr 17:ab3cec0c8bf4 1191 // a button as pressed if either the physical button is being pressed
mjr 17:ab3cec0c8bf4 1192 // or we're simulating a press on the button. This is used for the
mjr 17:ab3cec0c8bf4 1193 // simulated Launch Ball button.
mjr 17:ab3cec0c8bf4 1194 uint32_t simButtons = 0;
mjr 6:cc35eb643e8f 1195
mjr 6:cc35eb643e8f 1196 // Firing in progress: we set this when we detect the start of rapid
mjr 6:cc35eb643e8f 1197 // plunger movement from a retracted position towards the rest position.
mjr 17:ab3cec0c8bf4 1198 //
mjr 17:ab3cec0c8bf4 1199 // When we detect a firing event, we send VP a series of synthetic
mjr 17:ab3cec0c8bf4 1200 // reports simulating the idealized plunger motion. The actual physical
mjr 17:ab3cec0c8bf4 1201 // motion is much too fast to report to VP; in the time between two USB
mjr 17:ab3cec0c8bf4 1202 // reports, the plunger can shoot all the way forward, rebound off of
mjr 17:ab3cec0c8bf4 1203 // the barrel spring, bounce back part way, and bounce forward again,
mjr 17:ab3cec0c8bf4 1204 // or even do all of this more than once. This means that sampling the
mjr 17:ab3cec0c8bf4 1205 // physical motion at the USB report rate would create a misleading
mjr 17:ab3cec0c8bf4 1206 // picture of the plunger motion, since our samples would catch the
mjr 17:ab3cec0c8bf4 1207 // plunger at random points in this oscillating motion. From the
mjr 17:ab3cec0c8bf4 1208 // user's perspective, the physical action that occurred is simply that
mjr 17:ab3cec0c8bf4 1209 // the plunger was released from a particular distance, so it's this
mjr 17:ab3cec0c8bf4 1210 // high-level event that we want to convey to VP. To do this, we
mjr 17:ab3cec0c8bf4 1211 // synthesize a series of reports to convey an idealized version of
mjr 17:ab3cec0c8bf4 1212 // the release motion that's perfectly synchronized to the VP reports.
mjr 17:ab3cec0c8bf4 1213 // Essentially we pretend that our USB position samples are exactly
mjr 17:ab3cec0c8bf4 1214 // aligned in time with (1) the point of retraction just before the
mjr 17:ab3cec0c8bf4 1215 // user released the plunger, (2) the point of maximum forward motion
mjr 17:ab3cec0c8bf4 1216 // just after the user released the plunger (the point of maximum
mjr 17:ab3cec0c8bf4 1217 // compression as the plunger bounces off of the barrel spring), and
mjr 17:ab3cec0c8bf4 1218 // (3) the plunger coming to rest at the park position. This series
mjr 17:ab3cec0c8bf4 1219 // of reports is synthetic in the sense that it's not what we actually
mjr 17:ab3cec0c8bf4 1220 // see on the CCD at the times of these reports - the true plunger
mjr 17:ab3cec0c8bf4 1221 // position is oscillating at high speed during this period. But at
mjr 17:ab3cec0c8bf4 1222 // the same time it conveys a more faithful picture of the true physical
mjr 17:ab3cec0c8bf4 1223 // motion to VP, and allows VP to reproduce the true physical motion
mjr 17:ab3cec0c8bf4 1224 // more faithfully in its simulation model, by correcting for the
mjr 17:ab3cec0c8bf4 1225 // relatively low sampling rate in the communication path between the
mjr 17:ab3cec0c8bf4 1226 // real plunger and VP's model plunger.
mjr 17:ab3cec0c8bf4 1227 //
mjr 17:ab3cec0c8bf4 1228 // If 'firing' is non-zero, it's the index of our current report in
mjr 17:ab3cec0c8bf4 1229 // the synthetic firing report series.
mjr 9:fd65b0a94720 1230 int firing = 0;
mjr 2:c174f9ee414a 1231
mjr 2:c174f9ee414a 1232 // start the first CCD integration cycle
mjr 17:ab3cec0c8bf4 1233 plungerSensor.init();
mjr 9:fd65b0a94720 1234
mjr 9:fd65b0a94720 1235 // Device status. We report this on each update so that the host config
mjr 9:fd65b0a94720 1236 // tool can detect our current settings. This is a bit mask consisting
mjr 9:fd65b0a94720 1237 // of these bits:
mjr 9:fd65b0a94720 1238 // 0x01 -> plunger sensor enabled
mjr 17:ab3cec0c8bf4 1239 uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00);
mjr 10:976666ffa4ef 1240
mjr 10:976666ffa4ef 1241 // flag: send a pixel dump after the next read
mjr 10:976666ffa4ef 1242 bool reportPix = false;
mjr 1:d913e0afb2ac 1243
mjr 1:d913e0afb2ac 1244 // we're all set up - now just loop, processing sensor reports and
mjr 1:d913e0afb2ac 1245 // host requests
mjr 0:5acbbe3f4cf4 1246 for (;;)
mjr 0:5acbbe3f4cf4 1247 {
mjr 0:5acbbe3f4cf4 1248 // Look for an incoming report. Continue processing input as
mjr 0:5acbbe3f4cf4 1249 // long as there's anything pending - this ensures that we
mjr 0:5acbbe3f4cf4 1250 // handle input in as timely a fashion as possible by deferring
mjr 0:5acbbe3f4cf4 1251 // output tasks as long as there's input to process.
mjr 0:5acbbe3f4cf4 1252 HID_REPORT report;
mjr 6:cc35eb643e8f 1253 while (js.readNB(&report))
mjr 0:5acbbe3f4cf4 1254 {
mjr 6:cc35eb643e8f 1255 // all Led-Wiz reports are 8 bytes exactly
mjr 6:cc35eb643e8f 1256 if (report.length == 8)
mjr 1:d913e0afb2ac 1257 {
mjr 6:cc35eb643e8f 1258 uint8_t *data = report.data;
mjr 6:cc35eb643e8f 1259 if (data[0] == 64)
mjr 0:5acbbe3f4cf4 1260 {
mjr 6:cc35eb643e8f 1261 // LWZ-SBA - first four bytes are bit-packed on/off flags
mjr 6:cc35eb643e8f 1262 // for the outputs; 5th byte is the pulse speed (0-7)
mjr 6:cc35eb643e8f 1263 //printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n",
mjr 6:cc35eb643e8f 1264 // data[1], data[2], data[3], data[4], data[5]);
mjr 0:5acbbe3f4cf4 1265
mjr 6:cc35eb643e8f 1266 // update all on/off states
mjr 6:cc35eb643e8f 1267 for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1)
mjr 6:cc35eb643e8f 1268 {
mjr 6:cc35eb643e8f 1269 if (bit == 0x100) {
mjr 6:cc35eb643e8f 1270 bit = 1;
mjr 6:cc35eb643e8f 1271 ++ri;
mjr 6:cc35eb643e8f 1272 }
mjr 6:cc35eb643e8f 1273 wizOn[i] = ((data[ri] & bit) != 0);
mjr 6:cc35eb643e8f 1274 }
mjr 6:cc35eb643e8f 1275
mjr 6:cc35eb643e8f 1276 // update the physical outputs
mjr 1:d913e0afb2ac 1277 updateWizOuts();
mjr 6:cc35eb643e8f 1278
mjr 6:cc35eb643e8f 1279 // reset the PBA counter
mjr 6:cc35eb643e8f 1280 pbaIdx = 0;
mjr 6:cc35eb643e8f 1281 }
mjr 6:cc35eb643e8f 1282 else if (data[0] == 65)
mjr 6:cc35eb643e8f 1283 {
mjr 6:cc35eb643e8f 1284 // Private control message. This isn't an LedWiz message - it's
mjr 6:cc35eb643e8f 1285 // an extension for this device. 65 is an invalid PBA setting,
mjr 6:cc35eb643e8f 1286 // and isn't used for any other LedWiz message, so we appropriate
mjr 6:cc35eb643e8f 1287 // it for our own private use. The first byte specifies the
mjr 6:cc35eb643e8f 1288 // message type.
mjr 6:cc35eb643e8f 1289 if (data[1] == 1)
mjr 6:cc35eb643e8f 1290 {
mjr 9:fd65b0a94720 1291 // 1 = Set Configuration:
mjr 6:cc35eb643e8f 1292 // data[2] = LedWiz unit number (0x00 to 0x0f)
mjr 6:cc35eb643e8f 1293 // data[3] = feature enable bit mask:
mjr 6:cc35eb643e8f 1294 // 0x01 = enable CCD
mjr 6:cc35eb643e8f 1295
mjr 6:cc35eb643e8f 1296 // we'll need a reset if the LedWiz unit number is changing
mjr 6:cc35eb643e8f 1297 uint8_t newUnitNo = data[2] & 0x0f;
mjr 6:cc35eb643e8f 1298 needReset |= (newUnitNo != cfg.d.ledWizUnitNo);
mjr 6:cc35eb643e8f 1299
mjr 6:cc35eb643e8f 1300 // set the configuration parameters from the message
mjr 6:cc35eb643e8f 1301 cfg.d.ledWizUnitNo = newUnitNo;
mjr 17:ab3cec0c8bf4 1302 cfg.d.plungerEnabled = data[3] & 0x01;
mjr 6:cc35eb643e8f 1303
mjr 9:fd65b0a94720 1304 // update the status flags
mjr 9:fd65b0a94720 1305 statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01);
mjr 9:fd65b0a94720 1306
mjr 9:fd65b0a94720 1307 // if the ccd is no longer enabled, use 0 for z reports
mjr 17:ab3cec0c8bf4 1308 if (!cfg.d.plungerEnabled)
mjr 9:fd65b0a94720 1309 z = 0;
mjr 9:fd65b0a94720 1310
mjr 6:cc35eb643e8f 1311 // save the configuration
mjr 6:cc35eb643e8f 1312 cfg.save(iap, flash_addr);
mjr 6:cc35eb643e8f 1313 }
mjr 9:fd65b0a94720 1314 else if (data[1] == 2)
mjr 9:fd65b0a94720 1315 {
mjr 9:fd65b0a94720 1316 // 2 = Calibrate plunger
mjr 9:fd65b0a94720 1317 // (No parameters)
mjr 9:fd65b0a94720 1318
mjr 9:fd65b0a94720 1319 // enter calibration mode
mjr 9:fd65b0a94720 1320 calBtnState = 3;
mjr 9:fd65b0a94720 1321 calBtnTimer.reset();
mjr 9:fd65b0a94720 1322 cfg.resetPlunger();
mjr 9:fd65b0a94720 1323 }
mjr 10:976666ffa4ef 1324 else if (data[1] == 3)
mjr 10:976666ffa4ef 1325 {
mjr 10:976666ffa4ef 1326 // 3 = pixel dump
mjr 10:976666ffa4ef 1327 // (No parameters)
mjr 10:976666ffa4ef 1328 reportPix = true;
mjr 10:976666ffa4ef 1329
mjr 10:976666ffa4ef 1330 // show purple until we finish sending the report
mjr 10:976666ffa4ef 1331 ledR = 0;
mjr 10:976666ffa4ef 1332 ledB = 0;
mjr 10:976666ffa4ef 1333 ledG = 1;
mjr 10:976666ffa4ef 1334 }
mjr 6:cc35eb643e8f 1335 }
mjr 6:cc35eb643e8f 1336 else
mjr 6:cc35eb643e8f 1337 {
mjr 6:cc35eb643e8f 1338 // LWZ-PBA - full state dump; each byte is one output
mjr 6:cc35eb643e8f 1339 // in the current bank. pbaIdx keeps track of the bank;
mjr 6:cc35eb643e8f 1340 // this is incremented implicitly by each PBA message.
mjr 6:cc35eb643e8f 1341 //printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n",
mjr 6:cc35eb643e8f 1342 // pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
mjr 6:cc35eb643e8f 1343
mjr 6:cc35eb643e8f 1344 // update all output profile settings
mjr 6:cc35eb643e8f 1345 for (int i = 0 ; i < 8 ; ++i)
mjr 6:cc35eb643e8f 1346 wizVal[pbaIdx + i] = data[i];
mjr 6:cc35eb643e8f 1347
mjr 6:cc35eb643e8f 1348 // update the physical LED state if this is the last bank
mjr 6:cc35eb643e8f 1349 if (pbaIdx == 24)
mjr 13:72dda449c3c0 1350 {
mjr 6:cc35eb643e8f 1351 updateWizOuts();
mjr 13:72dda449c3c0 1352 pbaIdx = 0;
mjr 13:72dda449c3c0 1353 }
mjr 13:72dda449c3c0 1354 else
mjr 13:72dda449c3c0 1355 pbaIdx += 8;
mjr 6:cc35eb643e8f 1356 }
mjr 0:5acbbe3f4cf4 1357 }
mjr 0:5acbbe3f4cf4 1358 }
mjr 1:d913e0afb2ac 1359
mjr 1:d913e0afb2ac 1360 // check for plunger calibration
mjr 17:ab3cec0c8bf4 1361 if (calBtn != 0 && !calBtn->read())
mjr 0:5acbbe3f4cf4 1362 {
mjr 1:d913e0afb2ac 1363 // check the state
mjr 1:d913e0afb2ac 1364 switch (calBtnState)
mjr 0:5acbbe3f4cf4 1365 {
mjr 1:d913e0afb2ac 1366 case 0:
mjr 1:d913e0afb2ac 1367 // button not yet pushed - start debouncing
mjr 1:d913e0afb2ac 1368 calBtnTimer.reset();
mjr 1:d913e0afb2ac 1369 calBtnState = 1;
mjr 1:d913e0afb2ac 1370 break;
mjr 1:d913e0afb2ac 1371
mjr 1:d913e0afb2ac 1372 case 1:
mjr 1:d913e0afb2ac 1373 // pushed, not yet debounced - if the debounce time has
mjr 1:d913e0afb2ac 1374 // passed, start the hold period
mjr 9:fd65b0a94720 1375 if (calBtnTimer.read_ms() > 50)
mjr 1:d913e0afb2ac 1376 calBtnState = 2;
mjr 1:d913e0afb2ac 1377 break;
mjr 1:d913e0afb2ac 1378
mjr 1:d913e0afb2ac 1379 case 2:
mjr 1:d913e0afb2ac 1380 // in the hold period - if the button has been held down
mjr 1:d913e0afb2ac 1381 // for the entire hold period, move to calibration mode
mjr 9:fd65b0a94720 1382 if (calBtnTimer.read_ms() > 2050)
mjr 1:d913e0afb2ac 1383 {
mjr 1:d913e0afb2ac 1384 // enter calibration mode
mjr 1:d913e0afb2ac 1385 calBtnState = 3;
mjr 9:fd65b0a94720 1386 calBtnTimer.reset();
mjr 9:fd65b0a94720 1387 cfg.resetPlunger();
mjr 1:d913e0afb2ac 1388 }
mjr 1:d913e0afb2ac 1389 break;
mjr 2:c174f9ee414a 1390
mjr 2:c174f9ee414a 1391 case 3:
mjr 9:fd65b0a94720 1392 // Already in calibration mode - pushing the button here
mjr 9:fd65b0a94720 1393 // doesn't change the current state, but we won't leave this
mjr 9:fd65b0a94720 1394 // state as long as it's held down. So nothing changes here.
mjr 2:c174f9ee414a 1395 break;
mjr 0:5acbbe3f4cf4 1396 }
mjr 0:5acbbe3f4cf4 1397 }
mjr 1:d913e0afb2ac 1398 else
mjr 1:d913e0afb2ac 1399 {
mjr 2:c174f9ee414a 1400 // Button released. If we're in calibration mode, and
mjr 2:c174f9ee414a 1401 // the calibration time has elapsed, end the calibration
mjr 2:c174f9ee414a 1402 // and save the results to flash.
mjr 2:c174f9ee414a 1403 //
mjr 2:c174f9ee414a 1404 // Otherwise, return to the base state without saving anything.
mjr 2:c174f9ee414a 1405 // If the button is released before we make it to calibration
mjr 2:c174f9ee414a 1406 // mode, it simply cancels the attempt.
mjr 9:fd65b0a94720 1407 if (calBtnState == 3 && calBtnTimer.read_ms() > 15000)
mjr 2:c174f9ee414a 1408 {
mjr 2:c174f9ee414a 1409 // exit calibration mode
mjr 1:d913e0afb2ac 1410 calBtnState = 0;
mjr 2:c174f9ee414a 1411
mjr 6:cc35eb643e8f 1412 // save the updated configuration
mjr 6:cc35eb643e8f 1413 cfg.d.plungerCal = 1;
mjr 6:cc35eb643e8f 1414 cfg.save(iap, flash_addr);
mjr 2:c174f9ee414a 1415
mjr 2:c174f9ee414a 1416 // the flash state is now valid
mjr 2:c174f9ee414a 1417 flash_valid = true;
mjr 2:c174f9ee414a 1418 }
mjr 2:c174f9ee414a 1419 else if (calBtnState != 3)
mjr 2:c174f9ee414a 1420 {
mjr 2:c174f9ee414a 1421 // didn't make it to calibration mode - cancel the operation
mjr 1:d913e0afb2ac 1422 calBtnState = 0;
mjr 2:c174f9ee414a 1423 }
mjr 1:d913e0afb2ac 1424 }
mjr 1:d913e0afb2ac 1425
mjr 1:d913e0afb2ac 1426 // light/flash the calibration button light, if applicable
mjr 1:d913e0afb2ac 1427 int newCalBtnLit = calBtnLit;
mjr 1:d913e0afb2ac 1428 switch (calBtnState)
mjr 0:5acbbe3f4cf4 1429 {
mjr 1:d913e0afb2ac 1430 case 2:
mjr 1:d913e0afb2ac 1431 // in the hold period - flash the light
mjr 9:fd65b0a94720 1432 newCalBtnLit = ((calBtnTimer.read_ms()/250) & 1);
mjr 1:d913e0afb2ac 1433 break;
mjr 1:d913e0afb2ac 1434
mjr 1:d913e0afb2ac 1435 case 3:
mjr 1:d913e0afb2ac 1436 // calibration mode - show steady on
mjr 1:d913e0afb2ac 1437 newCalBtnLit = true;
mjr 1:d913e0afb2ac 1438 break;
mjr 1:d913e0afb2ac 1439
mjr 1:d913e0afb2ac 1440 default:
mjr 1:d913e0afb2ac 1441 // not calibrating/holding - show steady off
mjr 1:d913e0afb2ac 1442 newCalBtnLit = false;
mjr 1:d913e0afb2ac 1443 break;
mjr 1:d913e0afb2ac 1444 }
mjr 3:3514575d4f86 1445
mjr 3:3514575d4f86 1446 // light or flash the external calibration button LED, and
mjr 3:3514575d4f86 1447 // do the same with the on-board blue LED
mjr 1:d913e0afb2ac 1448 if (calBtnLit != newCalBtnLit)
mjr 1:d913e0afb2ac 1449 {
mjr 1:d913e0afb2ac 1450 calBtnLit = newCalBtnLit;
mjr 2:c174f9ee414a 1451 if (calBtnLit) {
mjr 17:ab3cec0c8bf4 1452 if (calBtnLed != 0)
mjr 17:ab3cec0c8bf4 1453 calBtnLed->write(1);
mjr 4:02c7cd7b2183 1454 ledR = 1;
mjr 4:02c7cd7b2183 1455 ledG = 1;
mjr 9:fd65b0a94720 1456 ledB = 0;
mjr 2:c174f9ee414a 1457 }
mjr 2:c174f9ee414a 1458 else {
mjr 17:ab3cec0c8bf4 1459 if (calBtnLed != 0)
mjr 17:ab3cec0c8bf4 1460 calBtnLed->write(0);
mjr 4:02c7cd7b2183 1461 ledR = 1;
mjr 4:02c7cd7b2183 1462 ledG = 1;
mjr 9:fd65b0a94720 1463 ledB = 1;
mjr 2:c174f9ee414a 1464 }
mjr 1:d913e0afb2ac 1465 }
mjr 1:d913e0afb2ac 1466
mjr 17:ab3cec0c8bf4 1467 // If the plunger is enabled, and we're not already in a firing event,
mjr 17:ab3cec0c8bf4 1468 // and the last plunger reading had the plunger pulled back at least
mjr 17:ab3cec0c8bf4 1469 // a bit, watch for plunger release events until it's time for our next
mjr 17:ab3cec0c8bf4 1470 // USB report.
mjr 17:ab3cec0c8bf4 1471 if (!firing && cfg.d.plungerEnabled && z >= JOYMAX/6)
mjr 17:ab3cec0c8bf4 1472 {
mjr 17:ab3cec0c8bf4 1473 // monitor the plunger until it's time for our next report
mjr 17:ab3cec0c8bf4 1474 while (reportTimer.read_ms() < 15)
mjr 17:ab3cec0c8bf4 1475 {
mjr 17:ab3cec0c8bf4 1476 // do a fast low-res scan; if it's at or past the zero point,
mjr 17:ab3cec0c8bf4 1477 // start a firing event
mjr 17:ab3cec0c8bf4 1478 if (plungerSensor.lowResScan() <= cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1479 firing = 1;
mjr 17:ab3cec0c8bf4 1480 }
mjr 17:ab3cec0c8bf4 1481 }
mjr 17:ab3cec0c8bf4 1482
mjr 6:cc35eb643e8f 1483 // read the plunger sensor, if it's enabled
mjr 17:ab3cec0c8bf4 1484 if (cfg.d.plungerEnabled)
mjr 6:cc35eb643e8f 1485 {
mjr 6:cc35eb643e8f 1486 // start with the previous reading, in case we don't have a
mjr 6:cc35eb643e8f 1487 // clear result on this frame
mjr 6:cc35eb643e8f 1488 int znew = z;
mjr 17:ab3cec0c8bf4 1489 if (plungerSensor.highResScan(pos))
mjr 6:cc35eb643e8f 1490 {
mjr 17:ab3cec0c8bf4 1491 // We got a new reading. If we're in calibration mode, use it
mjr 17:ab3cec0c8bf4 1492 // to figure the new calibration, otherwise adjust the new reading
mjr 17:ab3cec0c8bf4 1493 // for the established calibration.
mjr 17:ab3cec0c8bf4 1494 if (calBtnState == 3)
mjr 6:cc35eb643e8f 1495 {
mjr 17:ab3cec0c8bf4 1496 // Calibration mode. If this reading is outside of the current
mjr 17:ab3cec0c8bf4 1497 // calibration bounds, expand the bounds.
mjr 17:ab3cec0c8bf4 1498 if (pos < cfg.d.plungerMin)
mjr 17:ab3cec0c8bf4 1499 cfg.d.plungerMin = pos;
mjr 17:ab3cec0c8bf4 1500 if (pos < cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1501 cfg.d.plungerZero = pos;
mjr 17:ab3cec0c8bf4 1502 if (pos > cfg.d.plungerMax)
mjr 17:ab3cec0c8bf4 1503 cfg.d.plungerMax = pos;
mjr 6:cc35eb643e8f 1504
mjr 17:ab3cec0c8bf4 1505 // normalize to the full physical range while calibrating
mjr 17:ab3cec0c8bf4 1506 znew = int(round(float(pos)/npix * JOYMAX));
mjr 17:ab3cec0c8bf4 1507 }
mjr 17:ab3cec0c8bf4 1508 else
mjr 17:ab3cec0c8bf4 1509 {
mjr 17:ab3cec0c8bf4 1510 // Not in calibration mode, so normalize the new reading to the
mjr 17:ab3cec0c8bf4 1511 // established calibration range.
mjr 17:ab3cec0c8bf4 1512 //
mjr 17:ab3cec0c8bf4 1513 // Note that negative values are allowed. Zero represents the
mjr 17:ab3cec0c8bf4 1514 // "park" position, where the plunger sits when at rest. A mechanical
mjr 17:ab3cec0c8bf4 1515 // plunger has a smmall amount of travel in the "push" direction,
mjr 17:ab3cec0c8bf4 1516 // since the barrel spring can be compressed slightly. Negative
mjr 17:ab3cec0c8bf4 1517 // values represent travel in the push direction.
mjr 17:ab3cec0c8bf4 1518 if (pos > cfg.d.plungerMax)
mjr 17:ab3cec0c8bf4 1519 pos = cfg.d.plungerMax;
mjr 17:ab3cec0c8bf4 1520 znew = int(round(float(pos - cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1521 / (cfg.d.plungerMax - cfg.d.plungerZero + 1) * JOYMAX));
mjr 6:cc35eb643e8f 1522 }
mjr 6:cc35eb643e8f 1523 }
mjr 7:100a25f8bf56 1524
mjr 17:ab3cec0c8bf4 1525 // If we're not already in a firing event, check to see if the
mjr 17:ab3cec0c8bf4 1526 // new position is forward of the last report. If it is, a firing
mjr 17:ab3cec0c8bf4 1527 // event might have started during the high-res scan. This might
mjr 17:ab3cec0c8bf4 1528 // seem unlikely given that the scan only takes about 5ms, but that
mjr 17:ab3cec0c8bf4 1529 // 5ms represents about 25-30% of our total time between reports,
mjr 17:ab3cec0c8bf4 1530 // there's about a 1 in 4 chance that a release starts during a
mjr 17:ab3cec0c8bf4 1531 // scan.
mjr 17:ab3cec0c8bf4 1532 if (!firing && z0 > 0 && znew < z0)
mjr 17:ab3cec0c8bf4 1533 {
mjr 17:ab3cec0c8bf4 1534 // The plunger has moved forward since the previous report.
mjr 17:ab3cec0c8bf4 1535 // Watch it for a few more ms to see if we can get a stable
mjr 17:ab3cec0c8bf4 1536 // new position.
mjr 17:ab3cec0c8bf4 1537 int pos1 = plungerSensor.lowResScan();
mjr 17:ab3cec0c8bf4 1538 Timer tw;
mjr 17:ab3cec0c8bf4 1539 tw.start();
mjr 17:ab3cec0c8bf4 1540 while (tw.read_ms() < 6)
mjr 17:ab3cec0c8bf4 1541 {
mjr 17:ab3cec0c8bf4 1542 // if we've crossed the rest position, it's a firing event
mjr 17:ab3cec0c8bf4 1543 if (pos1 < cfg.d.plungerZero)
mjr 17:ab3cec0c8bf4 1544 {
mjr 17:ab3cec0c8bf4 1545 firing = 1;
mjr 17:ab3cec0c8bf4 1546 break;
mjr 17:ab3cec0c8bf4 1547 }
mjr 17:ab3cec0c8bf4 1548
mjr 17:ab3cec0c8bf4 1549 // read the new position
mjr 17:ab3cec0c8bf4 1550 int pos2 = plungerSensor.lowResScan();
mjr 17:ab3cec0c8bf4 1551
mjr 17:ab3cec0c8bf4 1552 // if it's stable, stop looping
mjr 17:ab3cec0c8bf4 1553 if (abs(pos2 - pos1) < int(npix/(3.2*8)))
mjr 17:ab3cec0c8bf4 1554 break;
mjr 17:ab3cec0c8bf4 1555
mjr 17:ab3cec0c8bf4 1556 // the new reading is now the prior reading
mjr 17:ab3cec0c8bf4 1557 pos1 = pos2;
mjr 17:ab3cec0c8bf4 1558 }
mjr 17:ab3cec0c8bf4 1559 }
mjr 17:ab3cec0c8bf4 1560
mjr 17:ab3cec0c8bf4 1561 // Check for a simulated Launch Ball button press, if enabled
mjr 17:ab3cec0c8bf4 1562 if (ZBLaunchBallPort != 0 && wizOn[ZBLaunchBallPort-1])
mjr 17:ab3cec0c8bf4 1563 {
mjr 17:ab3cec0c8bf4 1564 int newState = lbState;
mjr 17:ab3cec0c8bf4 1565 switch (lbState)
mjr 17:ab3cec0c8bf4 1566 {
mjr 17:ab3cec0c8bf4 1567 case 0:
mjr 17:ab3cec0c8bf4 1568 // Base state. If the plunger is pulled back by an inch
mjr 17:ab3cec0c8bf4 1569 // or more, go to "cocked" state. If the plunger is pushed
mjr 17:ab3cec0c8bf4 1570 // forward by 1/4" or more, go to "launch" state.
mjr 17:ab3cec0c8bf4 1571 if (znew >= JOYMAX/3)
mjr 17:ab3cec0c8bf4 1572 newState = 1;
mjr 17:ab3cec0c8bf4 1573 else if (znew < -JOYMAX/12)
mjr 17:ab3cec0c8bf4 1574 newState = 3;
mjr 17:ab3cec0c8bf4 1575 break;
mjr 17:ab3cec0c8bf4 1576
mjr 17:ab3cec0c8bf4 1577 case 1:
mjr 17:ab3cec0c8bf4 1578 // Cocked state. If a firing event is now in progress,
mjr 17:ab3cec0c8bf4 1579 // go to "launch" state. Otherwise, if the plunger is less
mjr 17:ab3cec0c8bf4 1580 // than 1" retracted, go to "uncocked" state - the player
mjr 17:ab3cec0c8bf4 1581 // might be slowly returning the plunger to rest so as not
mjr 17:ab3cec0c8bf4 1582 // to trigger a launch.
mjr 17:ab3cec0c8bf4 1583 if (firing || znew <= 0)
mjr 17:ab3cec0c8bf4 1584 newState = 3;
mjr 17:ab3cec0c8bf4 1585 else if (znew < JOYMAX/3)
mjr 17:ab3cec0c8bf4 1586 newState = 2;
mjr 17:ab3cec0c8bf4 1587 break;
mjr 17:ab3cec0c8bf4 1588
mjr 17:ab3cec0c8bf4 1589 case 2:
mjr 17:ab3cec0c8bf4 1590 // Uncocked state. If the plunger is more than an inch
mjr 17:ab3cec0c8bf4 1591 // retracted, return to cocked state. If we've been in
mjr 17:ab3cec0c8bf4 1592 // the uncocked state for more than half a second, return
mjr 17:ab3cec0c8bf4 1593 // to the base state.
mjr 17:ab3cec0c8bf4 1594 if (znew >= JOYMAX/3)
mjr 17:ab3cec0c8bf4 1595 newState = 1;
mjr 17:ab3cec0c8bf4 1596 else if (lbTimer.read_ms() > 500)
mjr 17:ab3cec0c8bf4 1597 newState = 0;
mjr 17:ab3cec0c8bf4 1598 break;
mjr 17:ab3cec0c8bf4 1599
mjr 17:ab3cec0c8bf4 1600 case 3:
mjr 17:ab3cec0c8bf4 1601 // Launch state. If the plunger is no longer pushed
mjr 17:ab3cec0c8bf4 1602 // forward, switch to launch rest state.
mjr 17:ab3cec0c8bf4 1603 if (znew > -JOYMAX/24)
mjr 17:ab3cec0c8bf4 1604 newState = 4;
mjr 17:ab3cec0c8bf4 1605 break;
mjr 17:ab3cec0c8bf4 1606
mjr 17:ab3cec0c8bf4 1607 case 4:
mjr 17:ab3cec0c8bf4 1608 // Launch rest state. If the plunger is pushed forward
mjr 17:ab3cec0c8bf4 1609 // again, switch back to launch state. If not, and we've
mjr 17:ab3cec0c8bf4 1610 // been in this state for at least 200ms, return to the
mjr 17:ab3cec0c8bf4 1611 // default state.
mjr 17:ab3cec0c8bf4 1612 if (znew < -JOYMAX/12)
mjr 17:ab3cec0c8bf4 1613 newState = 3;
mjr 17:ab3cec0c8bf4 1614 else if (lbTimer.read_ms() > 200)
mjr 17:ab3cec0c8bf4 1615 newState = 0;
mjr 17:ab3cec0c8bf4 1616 break;
mjr 17:ab3cec0c8bf4 1617 }
mjr 17:ab3cec0c8bf4 1618
mjr 17:ab3cec0c8bf4 1619 // change states if desired
mjr 17:ab3cec0c8bf4 1620 if (newState != lbState)
mjr 17:ab3cec0c8bf4 1621 {
mjr 17:ab3cec0c8bf4 1622 // if we're entering Launch state, press the Launch Ball button
mjr 17:ab3cec0c8bf4 1623 if (newState == 3 && lbState != 4)
mjr 17:ab3cec0c8bf4 1624 simButtons |= (1 << (LaunchBallButton - 1));
mjr 17:ab3cec0c8bf4 1625
mjr 17:ab3cec0c8bf4 1626 // if we're switching to state 0, release the button
mjr 17:ab3cec0c8bf4 1627 if (newState == 0)
mjr 17:ab3cec0c8bf4 1628 simButtons &= ~(1 << (LaunchBallButton - 1));
mjr 17:ab3cec0c8bf4 1629
mjr 17:ab3cec0c8bf4 1630 // switch to the new state
mjr 17:ab3cec0c8bf4 1631 lbState = newState;
mjr 17:ab3cec0c8bf4 1632
mjr 17:ab3cec0c8bf4 1633 // start timing in the new state
mjr 17:ab3cec0c8bf4 1634 lbTimer.reset();
mjr 17:ab3cec0c8bf4 1635 }
mjr 17:ab3cec0c8bf4 1636 }
mjr 17:ab3cec0c8bf4 1637
mjr 17:ab3cec0c8bf4 1638 // If a firing event is in progress, generate synthetic reports to
mjr 17:ab3cec0c8bf4 1639 // describe an idealized version of the plunger motion to VP rather
mjr 17:ab3cec0c8bf4 1640 // than reporting the actual physical plunger position.
mjr 6:cc35eb643e8f 1641 //
mjr 17:ab3cec0c8bf4 1642 // We use the synthetic reports during a release event because the
mjr 17:ab3cec0c8bf4 1643 // physical plunger motion when released is too fast for VP to track.
mjr 17:ab3cec0c8bf4 1644 // VP only syncs its internal physics model with the outside world
mjr 17:ab3cec0c8bf4 1645 // about every 10ms. In that amount of time, the plunger moves
mjr 17:ab3cec0c8bf4 1646 // fast enough when released that it can shoot all the way forward,
mjr 17:ab3cec0c8bf4 1647 // bounce off of the barrel spring, and rebound part of the way
mjr 17:ab3cec0c8bf4 1648 // back. The result is the classic analog-to-digital problem of
mjr 17:ab3cec0c8bf4 1649 // sample aliasing. If we happen to time our sample during the
mjr 17:ab3cec0c8bf4 1650 // release motion so that we catch the plunger at the peak of a
mjr 17:ab3cec0c8bf4 1651 // bounce, the digital signal incorrectly looks like the plunger
mjr 17:ab3cec0c8bf4 1652 // is moving slowly forward - VP thinks we went from fully
mjr 17:ab3cec0c8bf4 1653 // retracted to half retracted in the sample interval, whereas
mjr 17:ab3cec0c8bf4 1654 // we actually traveled all the way forward and half way back,
mjr 17:ab3cec0c8bf4 1655 // so the speed VP infers is about 1/3 of the actual speed.
mjr 9:fd65b0a94720 1656 //
mjr 17:ab3cec0c8bf4 1657 // To correct this, we take advantage of our ability to sample
mjr 17:ab3cec0c8bf4 1658 // the CCD image several times in the course of a VP report. If
mjr 17:ab3cec0c8bf4 1659 // we catch the plunger near the origin after we've seen it
mjr 17:ab3cec0c8bf4 1660 // retracted, we go into Release Event mode. During this mode,
mjr 17:ab3cec0c8bf4 1661 // we stop reporting the true physical plunger position, and
mjr 17:ab3cec0c8bf4 1662 // instead report an idealized pattern: we report the plunger
mjr 17:ab3cec0c8bf4 1663 // immediately shooting forward to a position in front of the
mjr 17:ab3cec0c8bf4 1664 // park position that's in proportion to how far back the plunger
mjr 17:ab3cec0c8bf4 1665 // was just before the release, and we then report it stationary
mjr 17:ab3cec0c8bf4 1666 // at the park position. We continue to report the stationary
mjr 17:ab3cec0c8bf4 1667 // park position until the actual physical plunger motion has
mjr 17:ab3cec0c8bf4 1668 // stabilized on a new position. We then exit Release Event
mjr 17:ab3cec0c8bf4 1669 // mode and return to reporting the true physical position.
mjr 17:ab3cec0c8bf4 1670 if (firing)
mjr 6:cc35eb643e8f 1671 {
mjr 17:ab3cec0c8bf4 1672 // Firing in progress. Keep reporting the park position
mjr 17:ab3cec0c8bf4 1673 // until the physical plunger position comes to rest.
mjr 17:ab3cec0c8bf4 1674 const int restTol = JOYMAX/24;
mjr 17:ab3cec0c8bf4 1675 if (firing == 1)
mjr 6:cc35eb643e8f 1676 {
mjr 17:ab3cec0c8bf4 1677 // For the first couple of frames, show the plunger shooting
mjr 17:ab3cec0c8bf4 1678 // forward past the zero point, to simulate the momentum carrying
mjr 17:ab3cec0c8bf4 1679 // it forward to bounce off of the barrel spring. Show the
mjr 17:ab3cec0c8bf4 1680 // bounce as proportional to the distance it was retracted
mjr 17:ab3cec0c8bf4 1681 // in the prior report.
mjr 17:ab3cec0c8bf4 1682 z = zBounce = -z0/6;
mjr 17:ab3cec0c8bf4 1683 ++firing;
mjr 6:cc35eb643e8f 1684 }
mjr 17:ab3cec0c8bf4 1685 else if (firing == 2)
mjr 9:fd65b0a94720 1686 {
mjr 17:ab3cec0c8bf4 1687 // second frame - keep the bounce a little longer
mjr 17:ab3cec0c8bf4 1688 z = zBounce;
mjr 17:ab3cec0c8bf4 1689 ++firing;
mjr 17:ab3cec0c8bf4 1690 }
mjr 17:ab3cec0c8bf4 1691 else if (firing > 4
mjr 17:ab3cec0c8bf4 1692 && abs(znew - z0) < restTol
mjr 17:ab3cec0c8bf4 1693 && abs(znew - z1) < restTol
mjr 17:ab3cec0c8bf4 1694 && abs(znew - z2) < restTol)
mjr 17:ab3cec0c8bf4 1695 {
mjr 17:ab3cec0c8bf4 1696 // The physical plunger has come to rest. Exit firing
mjr 17:ab3cec0c8bf4 1697 // mode and resume reporting the actual position.
mjr 17:ab3cec0c8bf4 1698 firing = false;
mjr 17:ab3cec0c8bf4 1699 z = znew;
mjr 9:fd65b0a94720 1700 }
mjr 9:fd65b0a94720 1701 else
mjr 9:fd65b0a94720 1702 {
mjr 17:ab3cec0c8bf4 1703 // until the physical plunger comes to rest, simply
mjr 17:ab3cec0c8bf4 1704 // report the park position
mjr 9:fd65b0a94720 1705 z = 0;
mjr 17:ab3cec0c8bf4 1706 ++firing;
mjr 9:fd65b0a94720 1707 }
mjr 6:cc35eb643e8f 1708 }
mjr 6:cc35eb643e8f 1709 else
mjr 6:cc35eb643e8f 1710 {
mjr 17:ab3cec0c8bf4 1711 // not in firing mode - report the true physical position
mjr 17:ab3cec0c8bf4 1712 z = znew;
mjr 6:cc35eb643e8f 1713 }
mjr 17:ab3cec0c8bf4 1714
mjr 17:ab3cec0c8bf4 1715 // shift the new reading into the recent history buffer
mjr 6:cc35eb643e8f 1716 z2 = z1;
mjr 6:cc35eb643e8f 1717 z1 = z0;
mjr 6:cc35eb643e8f 1718 z0 = znew;
mjr 2:c174f9ee414a 1719 }
mjr 6:cc35eb643e8f 1720
mjr 11:bd9da7088e6e 1721 // update the buttons
mjr 11:bd9da7088e6e 1722 uint32_t buttons = readButtonsDebounced();
mjr 17:ab3cec0c8bf4 1723
mjr 17:ab3cec0c8bf4 1724 // If it's been long enough since our last USB status report,
mjr 17:ab3cec0c8bf4 1725 // send the new report. We throttle the report rate because
mjr 17:ab3cec0c8bf4 1726 // it can overwhelm the PC side if we report too frequently.
mjr 17:ab3cec0c8bf4 1727 // VP only wants to sync with the real world in 10ms intervals,
mjr 17:ab3cec0c8bf4 1728 // so reporting more frequently only creates i/o overhead
mjr 17:ab3cec0c8bf4 1729 // without doing anything to improve the simulation.
mjr 17:ab3cec0c8bf4 1730 if (reportTimer.read_ms() > 15)
mjr 17:ab3cec0c8bf4 1731 {
mjr 17:ab3cec0c8bf4 1732 // read the accelerometer
mjr 17:ab3cec0c8bf4 1733 int xa, ya;
mjr 17:ab3cec0c8bf4 1734 accel.get(xa, ya);
mjr 17:ab3cec0c8bf4 1735
mjr 17:ab3cec0c8bf4 1736 // confine the results to our joystick axis range
mjr 17:ab3cec0c8bf4 1737 if (xa < -JOYMAX) xa = -JOYMAX;
mjr 17:ab3cec0c8bf4 1738 if (xa > JOYMAX) xa = JOYMAX;
mjr 17:ab3cec0c8bf4 1739 if (ya < -JOYMAX) ya = -JOYMAX;
mjr 17:ab3cec0c8bf4 1740 if (ya > JOYMAX) ya = JOYMAX;
mjr 17:ab3cec0c8bf4 1741
mjr 17:ab3cec0c8bf4 1742 // store the updated accelerometer coordinates
mjr 17:ab3cec0c8bf4 1743 x = xa;
mjr 17:ab3cec0c8bf4 1744 y = ya;
mjr 17:ab3cec0c8bf4 1745
mjr 17:ab3cec0c8bf4 1746 // Send the status report. Note that the nominal x and y axes
mjr 17:ab3cec0c8bf4 1747 // are reversed - this makes it more intuitive to set up in VP.
mjr 17:ab3cec0c8bf4 1748 // If we mount the Freesale card flat on the floor of the cabinet
mjr 17:ab3cec0c8bf4 1749 // with the USB connectors facing the front of the cabinet, this
mjr 17:ab3cec0c8bf4 1750 // arrangement of our nominal axes aligns with VP's standard
mjr 17:ab3cec0c8bf4 1751 // setting, so that we can configure VP with X Axis = X on the
mjr 17:ab3cec0c8bf4 1752 // joystick and Y Axis = Y on the joystick.
mjr 17:ab3cec0c8bf4 1753 js.update(y, x, z, buttons | simButtons, statusFlags);
mjr 17:ab3cec0c8bf4 1754
mjr 17:ab3cec0c8bf4 1755 // we've just started a new report interval, so reset the timer
mjr 17:ab3cec0c8bf4 1756 reportTimer.reset();
mjr 17:ab3cec0c8bf4 1757 }
mjr 1:d913e0afb2ac 1758
mjr 10:976666ffa4ef 1759 // If we're in pixel dump mode, report all pixel exposure values
mjr 10:976666ffa4ef 1760 if (reportPix)
mjr 10:976666ffa4ef 1761 {
mjr 17:ab3cec0c8bf4 1762 // send the report
mjr 17:ab3cec0c8bf4 1763 plungerSensor.sendExposureReport(js);
mjr 17:ab3cec0c8bf4 1764
mjr 10:976666ffa4ef 1765 // we have satisfied this request
mjr 10:976666ffa4ef 1766 reportPix = false;
mjr 10:976666ffa4ef 1767 }
mjr 10:976666ffa4ef 1768
mjr 6:cc35eb643e8f 1769 #ifdef DEBUG_PRINTF
mjr 6:cc35eb643e8f 1770 if (x != 0 || y != 0)
mjr 6:cc35eb643e8f 1771 printf("%d,%d\r\n", x, y);
mjr 6:cc35eb643e8f 1772 #endif
mjr 6:cc35eb643e8f 1773
mjr 6:cc35eb643e8f 1774 // provide a visual status indication on the on-board LED
mjr 5:a70c0bce770d 1775 if (calBtnState < 2 && hbTimer.read_ms() > 1000)
mjr 1:d913e0afb2ac 1776 {
mjr 5:a70c0bce770d 1777 if (js.isSuspended() || !js.isConnected())
mjr 2:c174f9ee414a 1778 {
mjr 5:a70c0bce770d 1779 // suspended - turn off the LED
mjr 4:02c7cd7b2183 1780 ledR = 1;
mjr 4:02c7cd7b2183 1781 ledG = 1;
mjr 4:02c7cd7b2183 1782 ledB = 1;
mjr 5:a70c0bce770d 1783
mjr 5:a70c0bce770d 1784 // show a status flash every so often
mjr 5:a70c0bce770d 1785 if (hbcnt % 3 == 0)
mjr 5:a70c0bce770d 1786 {
mjr 6:cc35eb643e8f 1787 // disconnected = red/red flash; suspended = red
mjr 5:a70c0bce770d 1788 for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n)
mjr 5:a70c0bce770d 1789 {
mjr 5:a70c0bce770d 1790 ledR = 0;
mjr 5:a70c0bce770d 1791 wait(0.05);
mjr 5:a70c0bce770d 1792 ledR = 1;
mjr 5:a70c0bce770d 1793 wait(0.25);
mjr 5:a70c0bce770d 1794 }
mjr 5:a70c0bce770d 1795 }
mjr 2:c174f9ee414a 1796 }
mjr 6:cc35eb643e8f 1797 else if (needReset)
mjr 2:c174f9ee414a 1798 {
mjr 6:cc35eb643e8f 1799 // connected, need to reset due to changes in config parameters -
mjr 6:cc35eb643e8f 1800 // flash red/green
mjr 6:cc35eb643e8f 1801 hb = !hb;
mjr 6:cc35eb643e8f 1802 ledR = (hb ? 0 : 1);
mjr 6:cc35eb643e8f 1803 ledG = (hb ? 1 : 0);
mjr 6:cc35eb643e8f 1804 ledB = 0;
mjr 6:cc35eb643e8f 1805 }
mjr 17:ab3cec0c8bf4 1806 else if (cfg.d.plungerEnabled && !cfg.d.plungerCal)
mjr 6:cc35eb643e8f 1807 {
mjr 6:cc35eb643e8f 1808 // connected, plunger calibration needed - flash yellow/green
mjr 6:cc35eb643e8f 1809 hb = !hb;
mjr 6:cc35eb643e8f 1810 ledR = (hb ? 0 : 1);
mjr 6:cc35eb643e8f 1811 ledG = 0;
mjr 6:cc35eb643e8f 1812 ledB = 1;
mjr 6:cc35eb643e8f 1813 }
mjr 6:cc35eb643e8f 1814 else
mjr 6:cc35eb643e8f 1815 {
mjr 6:cc35eb643e8f 1816 // connected - flash blue/green
mjr 2:c174f9ee414a 1817 hb = !hb;
mjr 4:02c7cd7b2183 1818 ledR = 1;
mjr 4:02c7cd7b2183 1819 ledG = (hb ? 0 : 1);
mjr 4:02c7cd7b2183 1820 ledB = (hb ? 1 : 0);
mjr 2:c174f9ee414a 1821 }
mjr 1:d913e0afb2ac 1822
mjr 1:d913e0afb2ac 1823 // reset the heartbeat timer
mjr 1:d913e0afb2ac 1824 hbTimer.reset();
mjr 5:a70c0bce770d 1825 ++hbcnt;
mjr 1:d913e0afb2ac 1826 }
mjr 1:d913e0afb2ac 1827 }
mjr 0:5acbbe3f4cf4 1828 }