Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
Dependencies: FastIO FastPWM SimpleDMA mbed
Fork of Pinscape_Controller by
main.cpp
- Committer:
- mjr
- Date:
- 2015-06-03
- Revision:
- 23:14f8c5004cd0
- Parent:
- 22:71422c359f2a
- Child:
- 25:e22b88bd783a
File content as of revision 23:14f8c5004cd0:
/* Copyright 2014 M J Roberts, MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//
// Pinscape Controller
//
// "Pinscape" is the name of my custom-built virtual pinball cabinet, so I call this
// software the Pinscape Controller. I wrote it to handle several tasks that I needed
// for my cabinet. It runs on a Freescale KL25Z microcontroller, which is a small and
// inexpensive device that attaches to the cabinet PC via a USB cable, and can attach
// via custom wiring to sensors, buttons, and other devices in the cabinet.
//
// I designed the software and hardware in this project especially for my own
// cabinet, but it uses standard interfaces in Windows and Visual Pinball, so it should
// work in any VP-based cabinet, as long as you're using the usual VP software suite.
// I've tried to document the hardware in enough detail for anyone else to duplicate
// the entire project, and the full software is open source.
//
// The Freescale board appears to the host PC as a standard USB joystick. This works
// with the built-in Windows joystick device drivers, so there's no need to install any
// new drivers or other software on the PC. Windows should recognize the Freescale
// as a joystick when you plug it into the USB port, and Windows shouldn't ask you to
// install any drivers. If you bring up the Windows control panel for USB Game
// Controllers, this device will appear as "Pinscape Controller". *Don't* do any
// calibration with the Windows control panel or third-part calibration tools. The
// software calibrates the accelerometer portion automatically, and has its own special
// calibration procedure for the plunger sensor, if you're using that (see below).
//
// This software provides a whole bunch of separate features. You can use any of these
// features individually or all together. If you're not using a particular feature, you
// can simply omit the extra wiring and/or hardware for that feature. You can use
// the nudging feature by itself without any extra hardware attached, since the
// accelerometer is built in to the KL25Z board.
//
// - Nudge sensing via the KL25Z's on-board accelerometer. Nudging the cabinet
// causes small accelerations that the accelerometer can detect; these are sent to
// Visual Pinball via the joystick interface so that VP can simulate the effect
// of the real physical nudges on its simulated ball. VP has native handling for
// this type of input, so all you have to do is set some preferences in VP to tell
// it that an accelerometer is attached.
//
// - Plunger position sensing via an attached TAOS TSL 1410R CCD linear array sensor.
// To use this feature, you need to buy the TAOS device (it's not built in to the
// KL25Z, obviously), wire it to the KL25Z (5 wire connections between the two
// devices are required), and mount the TAOS sensor in your cabinet so that it's
// positioned properly to capture images of the physical plunger shooter rod.
//
// The physical mounting and wiring details are desribed in the project
// documentation.
//
// If the CCD is attached, the software constantly captures images from the CCD
// and analyzes them to determine how far back the plunger is pulled. It reports
// this to Visual Pinball via the joystick interface. This allows VP to make the
// simulated on-screen plunger track the motion of the physical plunger in real
// time. As with the nudge data, VP has native handling for the plunger input,
// so you just need to set the VP preferences to tell it that an analog plunger
// device is attached. One caveat, though: although VP itself has built-in
// support for an analog plunger, not all existing tables take advantage of it.
// Many existing tables have their own custom plunger scripting that doesn't
// cooperate with the VP plunger input. All tables *can* be made to work with
// the plunger, and in most cases it only requires some simple script editing,
// but in some cases it requires some more extensive surgery.
//
// For best results, the plunger sensor should be calibrated. The calibration
// is stored in non-volatile memory on board the KL25Z, so it's only necessary
// to do the calibration once, when you first install everything. (You might
// also want to re-calibrate if you physically remove and reinstall the CCD
// sensor or the mechanical plunger, since their alignment shift change slightly
// when you put everything back together.) You can optionally install a
// dedicated momentary switch or pushbutton to activate the calibration mode;
// this is describe in the project documentation. If you don't want to bother
// with the extra button, you can also trigger calibration using the Windows
// setup software, which you can find on the Pinscape project page.
//
// The calibration procedure is described in the project documentation. Briefly,
// when you trigger calibration mode, the software will scan the CCD for about
// 15 seconds, during which you should simply pull the physical plunger back
// all the way, hold it for a moment, and then slowly return it to the rest
// position. (DON'T just release it from the retracted position, since that
// let it shoot forward too far. We want to measure the range from the park
// position to the fully retracted position only.)
//
// - Button input wiring. 24 of the KL25Z's GPIO ports are mapped as digital inputs
// for buttons and switches. The software reports these as joystick buttons when
// it sends reports to the PC. These can be used to wire physical pinball-style
// buttons in the cabinet (e.g., flipper buttons, the Start button) and miscellaneous
// switches (such as a tilt bob) to the PC. Visual Pinball can use joystick buttons
// for input - you just have to assign a VP function to each button using VP's
// keyboard options dialog. To wire a button physically, connect one terminal of
// the button switch to the KL25Z ground, and connect the other terminal to the
// the GPIO port you wish to assign to the button. See the buttonMap[] array
// below for the available GPIO ports and their assigned joystick button numbers.
// If you're not using a GPIO port, you can just leave it unconnected - the digital
// inputs have built-in pull-up resistors, so an unconnected port is the same as
// an open switch (an "off" state for the button).
//
// - LedWiz emulation. The KL25Z can appear to the PC as an LedWiz device, and will
// accept and process LedWiz commands from the host. The software can turn digital
// output ports on and off, and can set varying PWM intensitiy levels on a subset
// of ports. (The KL25Z can only provide 6 PWM ports. Intensity level settings on
// other ports is ignored, so non-PWM ports can only be used for simple on/off
// devices such as contactors and solenoids.) The KL25Z can only supply 4mA on its
// output ports, so external hardware is required to take advantage of the LedWiz
// emulation. Many different hardware designs are possible, but there's a simple
// reference design in the documentation that uses a Darlington array IC to
// increase the output from each port to 500mA (the same level as the LedWiz),
// plus an extended design that adds an optocoupler and MOSFET to provide very
// high power handling, up to about 45A or 150W, with voltages up to 100V.
// That will handle just about any DC device directly (wtihout relays or other
// amplifiers), and switches fast enough to support PWM devices.
//
// The device can report any desired LedWiz unit number to the host, which makes
// it possible to use the LedWiz emulation on a machine that also has one or more
// actual LedWiz devices intalled. The LedWiz design allows for up to 16 units
// to be installed in one machine - each one is invidually addressable by its
// distinct unit number.
//
// The LedWiz emulation features are of course optional. There's no need to
// build any of the external port hardware (or attach anything to the output
// ports at all) if the LedWiz features aren't needed. Most people won't have
// any use for the LedWiz features. I built them mostly as a learning exercise,
// but with a slight practical need for a handful of extra ports (I'm using the
// cutting-edge 10-contactor setup, so my real LedWiz is full!).
//
// The on-board LED on the KL25Z flashes to indicate the current device status:
//
// two short red flashes = the device is powered but hasn't successfully
// connected to the host via USB (either it's not physically connected
// to the USB port, or there was a problem with the software handshake
// with the USB device driver on the computer)
//
// short red flash = the host computer is in sleep/suspend mode
//
// long red/green = the LedWiz unti number has been changed, so a reset
// is needed. You can simply unplug the device and plug it back in,
// or presss and hold the reset button on the device for a few seconds.
//
// long yellow/green = everything's working, but the plunger hasn't
// been calibrated; follow the calibration procedure described above.
// This flash mode won't appear if the CCD has been disabled. Note
// that the device can't tell whether a CCD is physically attached;
// if you don't have a CCD attached, you can set the appropriate option
// in config.h or use the Windows config tool to disable the CCD
// software features.
//
// alternating blue/green = everything's working
//
// Software configuration: you can change option settings by sending special
// USB commands from the PC. I've provided a Windows program for this purpose;
// refer to the documentation for details. For reference, here's the format
// of the USB command for option changes:
//
// length of report = 8 bytes
// byte 0 = 65 (0x41)
// byte 1 = 1 (0x01)
// byte 2 = new LedWiz unit number, 0x01 to 0x0f
// byte 3 = feature enable bit mask:
// 0x01 = enable CCD (default = on)
//
// Plunger calibration mode: the host can activate plunger calibration mode
// by sending this packet. This has the same effect as pressing and holding
// the plunger calibration button for two seconds, to allow activating this
// mode without attaching a physical button.
//
// length = 8 bytes
// byte 0 = 65 (0x41)
// byte 1 = 2 (0x02)
//
// Exposure reports: the host can request a report of the full set of pixel
// values for the next frame by sending this special packet:
//
// length = 8 bytes
// byte 0 = 65 (0x41)
// byte 1 = 3 (0x03)
//
// We'll respond with a series of special reports giving the exposure status.
// Each report has the following structure:
//
// bytes 0:1 = 11-bit index, with high 5 bits set to 10000. For
// example, 0x04 0x80 indicates index 4. This is the
// starting pixel number in the report. The first report
// will be 0x00 0x80 to indicate pixel #0.
// bytes 2:3 = 16-bit unsigned int brightness level of pixel at index
// bytes 4:5 = brightness of pixel at index+1
// etc for the rest of the packet
//
// This still has the form of a joystick packet at the USB level, but
// can be differentiated by the host via the status bits. It would have
// been cleaner to use a different Report ID at the USB level, but this
// would have necessitated a different container structure in the report
// descriptor, which would have broken LedWiz compatibility. Given that
// constraint, we have to re-use the joystick report type, making for
// this somewhat kludgey approach.
#include "mbed.h"
#include "math.h"
#include "USBJoystick.h"
#include "MMA8451Q.h"
#include "tsl1410r.h"
#include "FreescaleIAP.h"
#include "crc32.h"
// our local configuration file
#define DECL_EXTERNS
#include "config.h"
// ---------------------------------------------------------------------------
// utilities
// number of elements in an array
#define countof(x) (sizeof(x)/sizeof((x)[0]))
// ---------------------------------------------------------------------------
// USB device vendor ID, product ID, and version.
//
// We use the vendor ID for the LedWiz, so that the PC-side software can
// identify us as capable of performing LedWiz commands. The LedWiz uses
// a product ID value from 0xF0 to 0xFF; the last four bits identify the
// unit number (e.g., product ID 0xF7 means unit #7). This allows multiple
// LedWiz units to be installed in a single PC; the software on the PC side
// uses the unit number to route commands to the devices attached to each
// unit. On the real LedWiz, the unit number must be set in the firmware
// at the factory; it's not configurable by the end user. Most LedWiz's
// ship with the unit number set to 0, but the vendor will set different
// unit numbers if requested at the time of purchase. So if you have a
// single LedWiz already installed in your cabinet, and you didn't ask for
// a non-default unit number, your existing LedWiz will be unit 0.
//
// Note that the USB_PRODUCT_ID value set here omits the unit number. We
// take the unit number from the saved configuration. We provide a
// configuration command that can be sent via the USB connection to change
// the unit number, so that users can select the unit number without having
// to install a different version of the software. We'll combine the base
// product ID here with the unit number to get the actual product ID that
// we send to the USB controller.
const uint16_t USB_VENDOR_ID = 0xFAFA;
const uint16_t USB_PRODUCT_ID = 0x00F0;
const uint16_t USB_VERSION_NO = 0x0006;
// Joystick axis report range - we report from -JOYMAX to +JOYMAX
#define JOYMAX 4096
// --------------------------------------------------------------------------
//
// Define a symbol to tell us whether any sort of plunger sensor code
// is enabled in this build. Note that this doesn't tell us that a
// plunger device is actually attached or *currently* enabled; it just
// tells us whether or not the code for plunger sensing is enabled in
// the software build. This lets us leave out some unnecessary code
// on installations where no physical plunger is attached.
//
const int PLUNGER_CODE_ENABLED =
#if defined(ENABLE_CCD_SENSOR) || defined(ENABLE_POT_SENSOR)
1;
#else
0;
#endif
// ---------------------------------------------------------------------------
//
// On-board RGB LED elements - we use these for diagnostic displays.
//
DigitalOut ledR(LED1), ledG(LED2), ledB(LED3);
// ---------------------------------------------------------------------------
//
// LedWiz emulation
//
static int pbaIdx = 0;
// LedWiz output pin interface. We create a cover class to virtualize
// digital vs PWM outputs and give them a common interface. The KL25Z
// unfortunately doesn't have enough hardware PWM channels to support
// PWM on all 32 LedWiz outputs, so we provide as many PWM channels as
// we can (10), and fill out the rest of the outputs with plain digital
// outs.
class LwOut
{
public:
virtual void set(float val) = 0;
};
class LwPwmOut: public LwOut
{
public:
LwPwmOut(PinName pin) : p(pin) { prv = -1; }
virtual void set(float val)
{
if (val != prv)
p.write(prv = val);
}
PwmOut p;
float prv;
};
class LwDigOut: public LwOut
{
public:
LwDigOut(PinName pin) : p(pin) { prv = -1; }
virtual void set(float val)
{
if (val != prv)
p.write((prv = val) == 0.0 ? 0 : 1);
}
DigitalOut p;
float prv;
};
class LwUnusedOut: public LwOut
{
public:
LwUnusedOut() { }
virtual void set(float val) { }
};
// output pin array
static LwOut *lwPin[32];
// initialize the output pin array
void initLwOut()
{
for (int i = 0 ; i < countof(lwPin) ; ++i)
{
PinName p = (i < countof(ledWizPortMap) ? ledWizPortMap[i].pin : NC);
if (p == NC)
lwPin[i] = new LwUnusedOut();
else if (ledWizPortMap[i].isPWM)
lwPin[i] = new LwPwmOut(p);
else
lwPin[i] = new LwDigOut(p);
}
}
// on/off state for each LedWiz output
static uint8_t wizOn[32];
// profile (brightness/blink) state for each LedWiz output
static uint8_t wizVal[32] = {
48, 48, 48, 48, 48, 48, 48, 48,
48, 48, 48, 48, 48, 48, 48, 48,
48, 48, 48, 48, 48, 48, 48, 48,
48, 48, 48, 48, 48, 48, 48, 48
};
static float wizState(int idx)
{
if (wizOn[idx])
{
// on - map profile brightness state to PWM level
uint8_t val = wizVal[idx];
if (val <= 48)
{
// PWM brightness/intensity level. Rescale from the LedWiz
// 0..48 integer range to our internal PwmOut 0..1 float range.
// Note that on the actual LedWiz, level 48 is actually about
// 98% on - contrary to the LedWiz documentation, level 49 is
// the true 100% level. (In the documentation, level 49 is
// simply not a valid setting.) Even so, we treat level 48 as
// 100% on to match the documentation. This won't be perfectly
// ocmpatible with the actual LedWiz, but it makes for such a
// small difference in brightness (if the output device is an
// LED, say) that no one should notice. It seems better to
// err in this direction, because while the difference in
// brightness when attached to an LED won't be noticeable, the
// difference in duty cycle when attached to something like a
// contactor *can* be noticeable - anything less than 100%
// can cause a contactor or relay to chatter. There's almost
// never a situation where you'd want values other than 0% and
// 100% for a contactor or relay, so treating level 48 as 100%
// makes us work properly with software that's expecting the
// documented LedWiz behavior and therefore uses level 48 to
// turn a contactor or relay fully on.
return val/48.0;
}
else if (val == 49)
{
// 49 is undefined in the LedWiz documentation, but actually
// means 100% on. The documentation says that levels 1-48 are
// the full PWM range, but empirically it appears that the real
// range implemented in the firmware is 1-49. Some software on
// the PC side (notably DOF) is aware of this and uses level 49
// to mean "100% on". To ensure compatibility with existing
// PC-side software, we need to recognize level 49.
return 1.0;
}
else if (val >= 129 && val <= 132)
{
// Values of 129-132 select different flashing modes. We don't
// support any of these. Instead, simply treat them as fully on.
// Note that DOF doesn't ever use modes 129-132, as it implements
// all flashing modes itself on the host side, so this limitation
// won't have any effect on DOF users. You can observe it using
// LedBlinky, though.
return 1.0;
}
else
{
// Other values are undefined in the LedWiz documentation. Hosts
// *should* never send undefined values, since whatever behavior an
// LedWiz unit exhibits in response is accidental and could change
// in a future version. We'll treat all undefined values as equivalent
// to 48 (fully on).
//
// NB: the 49 and 129-132 cases are broken out above for the sake
// of documentation. We end up using 1.0 as the return value for
// everything outside of the defined 0-48 range, so we could collapse
// this whole thing to a single 'else' branch, but I wanted to call
// out the specific reasons for handling the settings above as we do.
return 1.0;
}
}
else
{
// off - show at 0 intensity
return 0.0;
}
}
static void updateWizOuts()
{
for (int i = 0 ; i < 32 ; ++i)
lwPin[i]->set(wizState(i));
}
// ---------------------------------------------------------------------------
//
// Button input
//
// button input map array
DigitalIn *buttonDigIn[32];
// button state
struct ButtonState
{
// current on/off state
int pressed;
// Sticky time remaining for current state. When a
// state transition occurs, we set this to a debounce
// period. Future state transitions will be ignored
// until the debounce time elapses.
int t;
} buttonState[32];
// timer for button reports
static Timer buttonTimer;
// initialize the button inputs
void initButtons()
{
// create the digital inputs
for (int i = 0 ; i < countof(buttonDigIn) ; ++i)
{
if (i < countof(buttonMap) && buttonMap[i] != NC)
buttonDigIn[i] = new DigitalIn(buttonMap[i]);
else
buttonDigIn[i] = 0;
}
// start the button timer
buttonTimer.start();
}
// read the button input state
uint32_t readButtons()
{
// start with all buttons off
uint32_t buttons = 0;
// figure the time elapsed since the last scan
int dt = buttonTimer.read_ms();
// reset the timef for the next scan
buttonTimer.reset();
// scan the button list
uint32_t bit = 1;
DigitalIn **di = buttonDigIn;
ButtonState *bs = buttonState;
for (int i = 0 ; i < countof(buttonDigIn) ; ++i, ++di, ++bs, bit <<= 1)
{
// read this button
if (*di != 0)
{
// deduct the elapsed time since the last update
// from the button's remaining sticky time
bs->t -= dt;
if (bs->t < 0)
bs->t = 0;
// If the sticky time has elapsed, note the new physical
// state of the button. If we still have sticky time
// remaining, ignore the physical state; the last state
// change persists until the sticky time elapses so that
// we smooth out any "bounce" (electrical transients that
// occur when the switch contact is opened or closed).
if (bs->t == 0)
{
// get the new physical state
int pressed = !(*di)->read();
// update the button's logical state if this is a change
if (pressed != bs->pressed)
{
// store the new state
bs->pressed = pressed;
// start a new sticky period for debouncing this
// state change
bs->t = 25;
}
}
// if it's pressed, OR its bit into the state
if (bs->pressed)
buttons |= bit;
}
}
// return the new button list
return buttons;
}
// ---------------------------------------------------------------------------
//
// Customization joystick subbclass
//
class MyUSBJoystick: public USBJoystick
{
public:
MyUSBJoystick(uint16_t vendor_id, uint16_t product_id, uint16_t product_release)
: USBJoystick(vendor_id, product_id, product_release, true)
{
suspended_ = false;
}
// are we connected?
int isConnected() { return configured(); }
// Are we in suspend mode?
int isSuspended() const { return suspended_; }
protected:
virtual void suspendStateChanged(unsigned int suspended)
{ suspended_ = suspended; }
// are we suspended?
int suspended_;
};
// ---------------------------------------------------------------------------
//
// Some simple math service routines
//
inline float square(float x) { return x*x; }
inline float round(float x) { return x > 0 ? floor(x + 0.5) : ceil(x - 0.5); }
// ---------------------------------------------------------------------------
//
// Accelerometer (MMA8451Q)
//
// The MMA8451Q is the KL25Z's on-board 3-axis accelerometer.
//
// This is a custom wrapper for the library code to interface to the
// MMA8451Q. This class encapsulates an interrupt handler and
// automatic calibration.
//
// We install an interrupt handler on the accelerometer "data ready"
// interrupt to ensure that we fetch each sample immediately when it
// becomes available. The accelerometer data rate is fiarly high
// (800 Hz), so it's not practical to keep up with it by polling.
// Using an interrupt handler lets us respond quickly and read
// every sample.
//
// We automatically calibrate the accelerometer so that it's not
// necessary to get it exactly level when installing it, and so
// that it's also not necessary to calibrate it manually. There's
// lots of experience that tells us that manual calibration is a
// terrible solution, mostly because cabinets tend to shift slightly
// during use, requiring frequent recalibration. Instead, we
// calibrate automatically. We continuously monitor the acceleration
// data, watching for periods of constant (or nearly constant) values.
// Any time it appears that the machine has been at rest for a while
// (about 5 seconds), we'll average the readings during that rest
// period and use the result as the level rest position. This is
// is ongoing, so we'll quickly find the center point again if the
// machine is moved during play (by an especially aggressive bout
// of nudging, say).
//
// I2C address of the accelerometer (this is a constant of the KL25Z)
const int MMA8451_I2C_ADDRESS = (0x1d<<1);
// SCL and SDA pins for the accelerometer (constant for the KL25Z)
#define MMA8451_SCL_PIN PTE25
#define MMA8451_SDA_PIN PTE24
// Digital in pin to use for the accelerometer interrupt. For the KL25Z,
// this can be either PTA14 or PTA15, since those are the pins physically
// wired on this board to the MMA8451 interrupt controller.
#define MMA8451_INT_PIN PTA15
// accelerometer input history item, for gathering calibration data
struct AccHist
{
AccHist() { x = y = d = 0.0; xtot = ytot = 0.0; cnt = 0; }
void set(float x, float y, AccHist *prv)
{
// save the raw position
this->x = x;
this->y = y;
this->d = distance(prv);
}
// reading for this entry
float x, y;
// distance from previous entry
float d;
// total and count of samples averaged over this period
float xtot, ytot;
int cnt;
void clearAvg() { xtot = ytot = 0.0; cnt = 0; }
void addAvg(float x, float y) { xtot += x; ytot += y; ++cnt; }
float xAvg() const { return xtot/cnt; }
float yAvg() const { return ytot/cnt; }
float distance(AccHist *p)
{ return sqrt(square(p->x - x) + square(p->y - y)); }
};
// accelerometer wrapper class
class Accel
{
public:
Accel(PinName sda, PinName scl, int i2cAddr, PinName irqPin)
: mma_(sda, scl, i2cAddr), intIn_(irqPin)
{
// remember the interrupt pin assignment
irqPin_ = irqPin;
// reset and initialize
reset();
}
void reset()
{
// clear the center point
cx_ = cy_ = 0.0;
// start the calibration timer
tCenter_.start();
iAccPrv_ = nAccPrv_ = 0;
// reset and initialize the MMA8451Q
mma_.init();
// set the initial integrated velocity reading to zero
vx_ = vy_ = 0;
// set up our accelerometer interrupt handling
intIn_.rise(this, &Accel::isr);
mma_.setInterruptMode(irqPin_ == PTA14 ? 1 : 2);
// read the current registers to clear the data ready flag
mma_.getAccXYZ(ax_, ay_, az_);
// start our timers
tGet_.start();
tInt_.start();
}
void get(int &x, int &y)
{
// disable interrupts while manipulating the shared data
__disable_irq();
// read the shared data and store locally for calculations
float ax = ax_, ay = ay_;
float vx = vx_, vy = vy_;
// reset the velocity sum for the next run
vx_ = vy_ = 0;
// get the time since the last get() sample
float dt = tGet_.read_us()/1.0e6;
tGet_.reset();
// done manipulating the shared data
__enable_irq();
// adjust the readings for the integration time
vx /= dt;
vy /= dt;
// add this sample to the current calibration interval's running total
AccHist *p = accPrv_ + iAccPrv_;
p->addAvg(ax, ay);
// check for auto-centering every so often
if (tCenter_.read_ms() > 1000)
{
// add the latest raw sample to the history list
AccHist *prv = p;
iAccPrv_ = (iAccPrv_ + 1) % maxAccPrv;
p = accPrv_ + iAccPrv_;
p->set(ax, ay, prv);
// if we have a full complement, check for stability
if (nAccPrv_ >= maxAccPrv)
{
// check if we've been stable for all recent samples
static const float accTol = .01;
AccHist *p0 = accPrv_;
if (p0[0].d < accTol
&& p0[1].d < accTol
&& p0[2].d < accTol
&& p0[3].d < accTol
&& p0[4].d < accTol)
{
// Figure the new calibration point as the average of
// the samples over the rest period
cx_ = (p0[0].xAvg() + p0[1].xAvg() + p0[2].xAvg() + p0[3].xAvg() + p0[4].xAvg())/5.0;
cy_ = (p0[0].yAvg() + p0[1].yAvg() + p0[2].yAvg() + p0[3].yAvg() + p0[4].yAvg())/5.0;
}
}
else
{
// not enough samples yet; just up the count
++nAccPrv_;
}
// clear the new item's running totals
p->clearAvg();
// reset the timer
tCenter_.reset();
}
// report our integrated velocity reading in x,y
x = rawToReport(vx);
y = rawToReport(vy);
#ifdef DEBUG_PRINTF
if (x != 0 || y != 0)
printf("%f %f %d %d %f\r\n", vx, vy, x, y, dt);
#endif
}
private:
// adjust a raw acceleration figure to a usb report value
int rawToReport(float v)
{
// scale to the joystick report range and round to integer
int i = int(round(v*JOYMAX));
// if it's near the center, scale it roughly as 20*(i/20)^2,
// to suppress noise near the rest position
static const int filter[] = {
-18, -16, -14, -13, -11, -10, -8, -7, -6, -5, -4, -3, -2, -2, -1, -1, 0, 0, 0, 0,
0,
0, 0, 0, 0, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 18
};
return (i > 20 || i < -20 ? i : filter[i+20]);
}
// interrupt handler
void isr()
{
// Read the axes. Note that we have to read all three axes
// (even though we only really use x and y) in order to clear
// the "data ready" status bit in the accelerometer. The
// interrupt only occurs when the "ready" bit transitions from
// off to on, so we have to make sure it's off.
float x, y, z;
mma_.getAccXYZ(x, y, z);
// calculate the time since the last interrupt
float dt = tInt_.read_us()/1.0e6;
tInt_.reset();
// integrate the time slice from the previous reading to this reading
vx_ += (x + ax_ - 2*cx_)*dt/2;
vy_ += (y + ay_ - 2*cy_)*dt/2;
// store the updates
ax_ = x;
ay_ = y;
az_ = z;
}
// underlying accelerometer object
MMA8451Q mma_;
// last raw acceleration readings
float ax_, ay_, az_;
// integrated velocity reading since last get()
float vx_, vy_;
// timer for measuring time between get() samples
Timer tGet_;
// timer for measuring time between interrupts
Timer tInt_;
// Calibration reference point for accelerometer. This is the
// average reading on the accelerometer when in the neutral position
// at rest.
float cx_, cy_;
// timer for atuo-centering
Timer tCenter_;
// Auto-centering history. This is a separate history list that
// records results spaced out sparesely over time, so that we can
// watch for long-lasting periods of rest. When we observe nearly
// no motion for an extended period (on the order of 5 seconds), we
// take this to mean that the cabinet is at rest in its neutral
// position, so we take this as the calibration zero point for the
// accelerometer. We update this history continuously, which allows
// us to continuously re-calibrate the accelerometer. This ensures
// that we'll automatically adjust to any actual changes in the
// cabinet's orientation (e.g., if it gets moved slightly by an
// especially strong nudge) as well as any systematic drift in the
// accelerometer measurement bias (e.g., from temperature changes).
int iAccPrv_, nAccPrv_;
static const int maxAccPrv = 5;
AccHist accPrv_[maxAccPrv];
// interurupt pin name
PinName irqPin_;
// interrupt router
InterruptIn intIn_;
};
// ---------------------------------------------------------------------------
//
// Clear the I2C bus for the MMA8451Q. This seems necessary some of the time
// for reasons that aren't clear to me. Doing a hard power cycle has the same
// effect, but when we do a soft reset, the hardware sometimes seems to leave
// the MMA's SDA line stuck low. Forcing a series of 9 clock pulses through
// the SCL line is supposed to clear this condition. I'm not convinced this
// actually works with the way this component is wired on the KL25Z, but it
// seems harmless, so we'll do it on reset in case it does some good. What
// we really seem to need is a way to power cycle the MMA8451Q if it ever
// gets stuck, but this is simply not possible in software on the KL25Z.
//
// If the accelerometer does get stuck, and a software reboot doesn't reset
// it, the only workaround is to manually power cycle the whole KL25Z by
// unplugging both of its USB connections.
//
void clear_i2c()
{
// assume a general-purpose output pin to the I2C clock
DigitalOut scl(MMA8451_SCL_PIN);
DigitalIn sda(MMA8451_SDA_PIN);
// clock the SCL 9 times
for (int i = 0 ; i < 9 ; ++i)
{
scl = 1;
wait_us(20);
scl = 0;
wait_us(20);
}
}
// ---------------------------------------------------------------------------
//
// Include the appropriate plunger sensor definition. This will define a
// class called PlungerSensor, with a standard interface that we use in
// the main loop below. This is *kind of* like a virtual class interface,
// but it actually defines the methods statically, which is a little more
// efficient at run-time. There's no need for a true virtual interface
// because we don't need to be able to change sensor types on the fly.
//
#if defined(ENABLE_CCD_SENSOR)
#include "ccdSensor.h"
#elif defined(ENABLE_POT_SENSOR)
#include "potSensor.h"
#else
#include "nullSensor.h"
#endif
// ---------------------------------------------------------------------------
//
// Non-volatile memory (NVM)
//
// Structure defining our NVM storage layout. We store a small
// amount of persistent data in flash memory to retain calibration
// data when powered off.
struct NVM
{
// checksum - we use this to determine if the flash record
// has been properly initialized
uint32_t checksum;
// signature value
static const uint32_t SIGNATURE = 0x4D4A522A;
static const uint16_t VERSION = 0x0003;
// Is the data structure valid? We test the signature and
// checksum to determine if we've been properly stored.
int valid() const
{
return (d.sig == SIGNATURE
&& d.vsn == VERSION
&& d.sz == sizeof(NVM)
&& checksum == CRC32(&d, sizeof(d)));
}
// save to non-volatile memory
void save(FreescaleIAP &iap, int addr)
{
// update the checksum and structure size
checksum = CRC32(&d, sizeof(d));
d.sz = sizeof(NVM);
// erase the sector
iap.erase_sector(addr);
// save the data
iap.program_flash(addr, this, sizeof(*this));
}
// reset calibration data for calibration mode
void resetPlunger()
{
// set extremes for the calibration data
d.plungerMax = 0;
d.plungerZero = npix;
d.plungerMin = npix;
}
// stored data (excluding the checksum)
struct
{
// Signature, structure version, and structure size - further verification
// that we have valid initialized data. The size is a simple proxy for a
// structure version, as the most common type of change to the structure as
// the software evolves will be the addition of new elements. We also
// provide an explicit version number that we can update manually if we
// make any changes that don't affect the structure size but would affect
// compatibility with a saved record (e.g., swapping two existing elements).
uint32_t sig;
uint16_t vsn;
int sz;
// has the plunger been manually calibrated?
int plungerCal;
// Plunger calibration min, zero, and max. The zero point is the
// rest position (aka park position), where it's in equilibrium between
// the main spring and the barrel spring. It can travel a small distance
// forward of the rest position, because the barrel spring can be
// compressed by the user pushing on the plunger or by the momentum
// of a release motion. The minimum is the maximum forward point where
// the barrel spring can't be compressed any further.
int plungerMin;
int plungerZero;
int plungerMax;
// is the plunger sensor enabled?
int plungerEnabled;
// LedWiz unit number
uint8_t ledWizUnitNo;
} d;
};
// ---------------------------------------------------------------------------
//
// Main program loop. This is invoked on startup and runs forever. Our
// main work is to read our devices (the accelerometer and the CCD), process
// the readings into nudge and plunger position data, and send the results
// to the host computer via the USB joystick interface. We also monitor
// the USB connection for incoming LedWiz commands and process those into
// port outputs.
//
int main(void)
{
// turn off our on-board indicator LED
ledR = 1;
ledG = 1;
ledB = 1;
// initialize the LedWiz ports
initLwOut();
// initialize the button input ports
initButtons();
// we don't need a reset yet
bool needReset = false;
// clear the I2C bus for the accelerometer
clear_i2c();
// set up a flash memory controller
FreescaleIAP iap;
// use the last sector of flash for our non-volatile memory structure
int flash_addr = (iap.flash_size() - SECTOR_SIZE);
NVM *flash = (NVM *)flash_addr;
NVM cfg;
// check for valid flash
bool flash_valid = flash->valid();
// if the flash is valid, load it; otherwise initialize to defaults
if (flash_valid) {
memcpy(&cfg, flash, sizeof(cfg));
printf("Flash restored: plunger cal=%d, min=%d, zero=%d, max=%d\r\n",
cfg.d.plungerCal, cfg.d.plungerMin, cfg.d.plungerZero, cfg.d.plungerMax);
}
else {
printf("Factory reset\r\n");
cfg.d.sig = cfg.SIGNATURE;
cfg.d.vsn = cfg.VERSION;
cfg.d.plungerCal = 0;
cfg.d.plungerMin = 0; // assume we can go all the way forward...
cfg.d.plungerMax = npix; // ...and all the way back
cfg.d.plungerZero = npix/6; // the rest position is usually around 1/2" back
cfg.d.ledWizUnitNo = DEFAULT_LEDWIZ_UNIT_NUMBER - 1; // unit numbering starts from 0 internally
cfg.d.plungerEnabled = PLUNGER_CODE_ENABLED;
}
// Create the joystick USB client. Note that we use the LedWiz unit
// number from the saved configuration.
MyUSBJoystick js(
USB_VENDOR_ID,
USB_PRODUCT_ID | cfg.d.ledWizUnitNo,
USB_VERSION_NO);
// last report timer - we use this to throttle reports, since VP
// doesn't want to hear from us more than about every 10ms
Timer reportTimer;
reportTimer.start();
// initialize the calibration buttons, if present
DigitalIn *calBtn = (CAL_BUTTON_PIN == NC ? 0 : new DigitalIn(CAL_BUTTON_PIN));
DigitalOut *calBtnLed = (CAL_BUTTON_LED == NC ? 0 : new DigitalOut(CAL_BUTTON_LED));
// plunger calibration button debounce timer
Timer calBtnTimer;
calBtnTimer.start();
int calBtnLit = false;
// Calibration button state:
// 0 = not pushed
// 1 = pushed, not yet debounced
// 2 = pushed, debounced, waiting for hold time
// 3 = pushed, hold time completed - in calibration mode
int calBtnState = 0;
// set up a timer for our heartbeat indicator
Timer hbTimer;
hbTimer.start();
int hb = 0;
uint16_t hbcnt = 0;
// set a timer for accelerometer auto-centering
Timer acTimer;
acTimer.start();
// create the accelerometer object
Accel accel(MMA8451_SCL_PIN, MMA8451_SDA_PIN, MMA8451_I2C_ADDRESS, MMA8451_INT_PIN);
#ifdef ENABLE_JOYSTICK
// last accelerometer report, in joystick units (we report the nudge
// acceleration via the joystick x & y axes, per the VP convention)
int x = 0, y = 0;
// flag: send a pixel dump after the next read
bool reportPix = false;
#endif
// create our plunger sensor object
PlungerSensor plungerSensor;
// last plunger report position, in 'npix' normalized pixel units
int pos = 0;
// last plunger report, in joystick units (we report the plunger as the
// "z" axis of the joystick, per the VP convention)
int z = 0;
// most recent prior plunger readings, for tracking release events(z0 is
// reading just before the last one we reported, z1 is the one before that,
// z2 the next before that)
int z0 = 0, z1 = 0, z2 = 0;
// Simulated "bounce" position when firing. We model the bounce off of
// the barrel spring when the plunger is released as proportional to the
// distance it was retracted just before being released.
int zBounce = 0;
// Simulated Launch Ball button state. If a "ZB Launch Ball" port is
// defined for our LedWiz port mapping, any time that port is turned ON,
// we'll simulate pushing the Launch Ball button if the player pulls
// back and releases the plunger, or simply pushes on the plunger from
// the rest position. This allows the plunger to be used in lieu of a
// physical Launch Ball button for tables that don't have plungers.
//
// States:
// 0 = default
// 1 = cocked (plunger has been pulled back about 1" from state 0)
// 2 = uncocked (plunger is pulled back less than 1" from state 1)
// 3 = launching, plunger is forward beyond park position
// 4 = launching, plunger is behind park position
// 5 = pressed and holding (plunger has been pressed forward beyond
// the park position from state 0)
int lbState = 0;
// Time since last lbState transition. Some of the states are time-
// sensitive. In the "uncocked" state, we'll return to state 0 if
// we remain in this state for more than a few milliseconds, since
// it indicates that the plunger is being slowly returned to rest
// rather than released. In the "launching" state, we need to release
// the Launch Ball button after a moment, and we need to wait for
// the plunger to come to rest before returning to state 0.
Timer lbTimer;
lbTimer.start();
// Launch Ball simulated push timer. We start this when we simulate
// the button push, and turn off the simulated button when enough time
// has elapsed.
Timer lbBtnTimer;
// Simulated button states. This is a vector of button states
// for the simulated buttons. We combine this with the physical
// button states on each USB joystick report, so we will report
// a button as pressed if either the physical button is being pressed
// or we're simulating a press on the button. This is used for the
// simulated Launch Ball button.
uint32_t simButtons = 0;
// Firing in progress: we set this when we detect the start of rapid
// plunger movement from a retracted position towards the rest position.
//
// When we detect a firing event, we send VP a series of synthetic
// reports simulating the idealized plunger motion. The actual physical
// motion is much too fast to report to VP; in the time between two USB
// reports, the plunger can shoot all the way forward, rebound off of
// the barrel spring, bounce back part way, and bounce forward again,
// or even do all of this more than once. This means that sampling the
// physical motion at the USB report rate would create a misleading
// picture of the plunger motion, since our samples would catch the
// plunger at random points in this oscillating motion. From the
// user's perspective, the physical action that occurred is simply that
// the plunger was released from a particular distance, so it's this
// high-level event that we want to convey to VP. To do this, we
// synthesize a series of reports to convey an idealized version of
// the release motion that's perfectly synchronized to the VP reports.
// Essentially we pretend that our USB position samples are exactly
// aligned in time with (1) the point of retraction just before the
// user released the plunger, (2) the point of maximum forward motion
// just after the user released the plunger (the point of maximum
// compression as the plunger bounces off of the barrel spring), and
// (3) the plunger coming to rest at the park position. This series
// of reports is synthetic in the sense that it's not what we actually
// see on the CCD at the times of these reports - the true plunger
// position is oscillating at high speed during this period. But at
// the same time it conveys a more faithful picture of the true physical
// motion to VP, and allows VP to reproduce the true physical motion
// more faithfully in its simulation model, by correcting for the
// relatively low sampling rate in the communication path between the
// real plunger and VP's model plunger.
//
// If 'firing' is non-zero, it's the index of our current report in
// the synthetic firing report series.
int firing = 0;
// start the first CCD integration cycle
plungerSensor.init();
// Device status. We report this on each update so that the host config
// tool can detect our current settings. This is a bit mask consisting
// of these bits:
// 0x01 -> plunger sensor enabled
uint16_t statusFlags = (cfg.d.plungerEnabled ? 0x01 : 0x00);
// we're all set up - now just loop, processing sensor reports and
// host requests
for (;;)
{
// Look for an incoming report. Process a few input reports in
// a row, but stop after a few so that a barrage of inputs won't
// starve our output event processing. Also, pause briefly between
// reads; allowing reads to occur back-to-back seems to occasionally
// stall the USB pipeline (for reasons unknown; I'd fix the underlying
// problem if I knew what it was).
HID_REPORT report;
for (int rr = 0 ; rr < 4 && js.readNB(&report) ; ++rr, wait_ms(1))
{
// all Led-Wiz reports are 8 bytes exactly
if (report.length == 8)
{
uint8_t *data = report.data;
if (data[0] == 64)
{
// LWZ-SBA - first four bytes are bit-packed on/off flags
// for the outputs; 5th byte is the pulse speed (0-7)
//printf("LWZ-SBA %02x %02x %02x %02x ; %02x\r\n",
// data[1], data[2], data[3], data[4], data[5]);
// update all on/off states
for (int i = 0, bit = 1, ri = 1 ; i < 32 ; ++i, bit <<= 1)
{
if (bit == 0x100) {
bit = 1;
++ri;
}
wizOn[i] = ((data[ri] & bit) != 0);
}
// update the physical outputs
updateWizOuts();
// reset the PBA counter
pbaIdx = 0;
}
else if (data[0] == 65)
{
// Private control message. This isn't an LedWiz message - it's
// an extension for this device. 65 is an invalid PBA setting,
// and isn't used for any other LedWiz message, so we appropriate
// it for our own private use. The first byte specifies the
// message type.
if (data[1] == 1)
{
// 1 = Set Configuration:
// data[2] = LedWiz unit number (0x00 to 0x0f)
// data[3] = feature enable bit mask:
// 0x01 = enable plunger sensor
// we'll need a reset if the LedWiz unit number is changing
uint8_t newUnitNo = data[2] & 0x0f;
needReset |= (newUnitNo != cfg.d.ledWizUnitNo);
// set the configuration parameters from the message
cfg.d.ledWizUnitNo = newUnitNo;
cfg.d.plungerEnabled = data[3] & 0x01;
// update the status flags
statusFlags = (statusFlags & ~0x01) | (data[3] & 0x01);
// if the ccd is no longer enabled, use 0 for z reports
if (!cfg.d.plungerEnabled)
z = 0;
// save the configuration
cfg.save(iap, flash_addr);
}
#ifdef ENABLE_JOYSTICK
else if (data[1] == 2)
{
// 2 = Calibrate plunger
// (No parameters)
// enter calibration mode
calBtnState = 3;
calBtnTimer.reset();
cfg.resetPlunger();
}
else if (data[1] == 3)
{
// 3 = pixel dump
// (No parameters)
reportPix = true;
// show purple until we finish sending the report
ledR = 0;
ledB = 0;
ledG = 1;
}
#endif // ENABLE_JOYSTICK
}
else
{
// LWZ-PBA - full state dump; each byte is one output
// in the current bank. pbaIdx keeps track of the bank;
// this is incremented implicitly by each PBA message.
//printf("LWZ-PBA[%d] %02x %02x %02x %02x %02x %02x %02x %02x\r\n",
// pbaIdx, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
// update all output profile settings
for (int i = 0 ; i < 8 ; ++i)
wizVal[pbaIdx + i] = data[i];
// update the physical LED state if this is the last bank
if (pbaIdx == 24)
{
updateWizOuts();
pbaIdx = 0;
}
else
pbaIdx += 8;
}
}
}
// check for plunger calibration
if (calBtn != 0 && !calBtn->read())
{
// check the state
switch (calBtnState)
{
case 0:
// button not yet pushed - start debouncing
calBtnTimer.reset();
calBtnState = 1;
break;
case 1:
// pushed, not yet debounced - if the debounce time has
// passed, start the hold period
if (calBtnTimer.read_ms() > 50)
calBtnState = 2;
break;
case 2:
// in the hold period - if the button has been held down
// for the entire hold period, move to calibration mode
if (calBtnTimer.read_ms() > 2050)
{
// enter calibration mode
calBtnState = 3;
calBtnTimer.reset();
cfg.resetPlunger();
}
break;
case 3:
// Already in calibration mode - pushing the button here
// doesn't change the current state, but we won't leave this
// state as long as it's held down. So nothing changes here.
break;
}
}
else
{
// Button released. If we're in calibration mode, and
// the calibration time has elapsed, end the calibration
// and save the results to flash.
//
// Otherwise, return to the base state without saving anything.
// If the button is released before we make it to calibration
// mode, it simply cancels the attempt.
if (calBtnState == 3 && calBtnTimer.read_ms() > 15000)
{
// exit calibration mode
calBtnState = 0;
// save the updated configuration
cfg.d.plungerCal = 1;
cfg.save(iap, flash_addr);
// the flash state is now valid
flash_valid = true;
}
else if (calBtnState != 3)
{
// didn't make it to calibration mode - cancel the operation
calBtnState = 0;
}
}
// light/flash the calibration button light, if applicable
int newCalBtnLit = calBtnLit;
switch (calBtnState)
{
case 2:
// in the hold period - flash the light
newCalBtnLit = ((calBtnTimer.read_ms()/250) & 1);
break;
case 3:
// calibration mode - show steady on
newCalBtnLit = true;
break;
default:
// not calibrating/holding - show steady off
newCalBtnLit = false;
break;
}
// light or flash the external calibration button LED, and
// do the same with the on-board blue LED
if (calBtnLit != newCalBtnLit)
{
calBtnLit = newCalBtnLit;
if (calBtnLit) {
if (calBtnLed != 0)
calBtnLed->write(1);
ledR = 1;
ledG = 1;
ledB = 0;
}
else {
if (calBtnLed != 0)
calBtnLed->write(0);
ledR = 1;
ledG = 1;
ledB = 1;
}
}
// If the plunger is enabled, and we're not already in a firing event,
// and the last plunger reading had the plunger pulled back at least
// a bit, watch for plunger release events until it's time for our next
// USB report.
if (!firing && cfg.d.plungerEnabled && z >= JOYMAX/6)
{
// monitor the plunger until it's time for our next report
while (reportTimer.read_ms() < 15)
{
// do a fast low-res scan; if it's at or past the zero point,
// start a firing event
if (plungerSensor.lowResScan() <= cfg.d.plungerZero)
firing = 1;
}
}
// read the plunger sensor, if it's enabled
if (cfg.d.plungerEnabled)
{
// start with the previous reading, in case we don't have a
// clear result on this frame
int znew = z;
if (plungerSensor.highResScan(pos))
{
// We got a new reading. If we're in calibration mode, use it
// to figure the new calibration, otherwise adjust the new reading
// for the established calibration.
if (calBtnState == 3)
{
// Calibration mode. If this reading is outside of the current
// calibration bounds, expand the bounds.
if (pos < cfg.d.plungerMin)
cfg.d.plungerMin = pos;
if (pos < cfg.d.plungerZero)
cfg.d.plungerZero = pos;
if (pos > cfg.d.plungerMax)
cfg.d.plungerMax = pos;
// normalize to the full physical range while calibrating
znew = int(round(float(pos)/npix * JOYMAX));
}
else
{
// Not in calibration mode, so normalize the new reading to the
// established calibration range.
//
// Note that negative values are allowed. Zero represents the
// "park" position, where the plunger sits when at rest. A mechanical
// plunger has a small amount of travel in the "push" direction,
// since the barrel spring can be compressed slightly. Negative
// values represent travel in the push direction.
if (pos > cfg.d.plungerMax)
pos = cfg.d.plungerMax;
znew = int(round(float(pos - cfg.d.plungerZero)
/ (cfg.d.plungerMax - cfg.d.plungerZero + 1) * JOYMAX));
}
}
// If we're not already in a firing event, check to see if the
// new position is forward of the last report. If it is, a firing
// event might have started during the high-res scan. This might
// seem unlikely given that the scan only takes about 5ms, but that
// 5ms represents about 25-30% of our total time between reports,
// there's about a 1 in 4 chance that a release starts during a
// scan.
if (!firing && z0 > 0 && znew < z0)
{
// The plunger has moved forward since the previous report.
// Watch it for a few more ms to see if we can get a stable
// new position.
int pos0 = plungerSensor.lowResScan();
int pos1 = pos0;
Timer tw;
tw.start();
while (tw.read_ms() < 6)
{
// read the new position
int pos2 = plungerSensor.lowResScan();
// If it's stable over consecutive readings, stop looping.
// (Count it as stable if the position is within about 1/8".
// pos1 and pos2 are reported in pixels, so they range from
// 0 to npix. The overall travel of a standard plunger is
// about 3.2", so we have (npix/3.2) pixels per inch, hence
// 1/8" is (npix/3.2)*(1/8) pixels.)
if (abs(pos2 - pos1) < int(npix/(3.2*8)))
break;
// If we've crossed the rest position, and we've moved by
// a minimum distance from where we starting this loop, begin
// a firing event. (We require a minimum distance to prevent
// spurious firing from random analog noise in the readings
// when the plunger is actually just sitting still at the
// rest position. If it's at rest, it's normal to see small
// random fluctuations in the analog reading +/- 1% or so
// from the 0 point, especially with a sensor like a
// potentionemeter that reports the position as a single
// analog voltage.) Note that we compare the latest reading
// to the first reading of the loop - we don't require the
// threshold motion over consecutive readings, but any time
// over the stability wait loop.
if (pos1 < cfg.d.plungerZero
&& abs(pos2 - pos0) > int(npix/(3.2*8)))
{
firing = 1;
break;
}
// the new reading is now the prior reading
pos1 = pos2;
}
}
// Check for a simulated Launch Ball button press, if enabled
if (ZBLaunchBallPort != 0)
{
const int cockThreshold = JOYMAX/3;
const int pushThreshold = int(-JOYMAX/3 * LaunchBallPushDistance);
int newState = lbState;
switch (lbState)
{
case 0:
// Base state. If the plunger is pulled back by an inch
// or more, go to "cocked" state. If the plunger is pushed
// forward by 1/4" or more, go to "pressed" state.
if (znew >= cockThreshold)
newState = 1;
else if (znew <= pushThreshold)
newState = 5;
break;
case 1:
// Cocked state. If a firing event is now in progress,
// go to "launch" state. Otherwise, if the plunger is less
// than 1" retracted, go to "uncocked" state - the player
// might be slowly returning the plunger to rest so as not
// to trigger a launch.
if (firing || znew <= 0)
newState = 3;
else if (znew < cockThreshold)
newState = 2;
break;
case 2:
// Uncocked state. If the plunger is more than an inch
// retracted, return to cocked state. If we've been in
// the uncocked state for more than half a second, return
// to the base state. This allows the user to return the
// plunger to rest without triggering a launch, by moving
// it at manual speed to the rest position rather than
// releasing it.
if (znew >= cockThreshold)
newState = 1;
else if (lbTimer.read_ms() > 500)
newState = 0;
break;
case 3:
// Launch state. If the plunger is no longer pushed
// forward, switch to launch rest state.
if (znew >= 0)
newState = 4;
break;
case 4:
// Launch rest state. If the plunger is pushed forward
// again, switch back to launch state. If not, and we've
// been in this state for at least 200ms, return to the
// default state.
if (znew <= pushThreshold)
newState = 3;
else if (lbTimer.read_ms() > 200)
newState = 0;
break;
case 5:
// Press-and-Hold state. If the plunger is no longer pushed
// forward, AND it's been at least 50ms since we generated
// the simulated Launch Ball button press, return to the base
// state. The minimum time is to ensure that VP has a chance
// to see the button press and to avoid transient key bounce
// effects when the plunger position is right on the threshold.
if (znew > pushThreshold && lbTimer.read_ms() > 50)
newState = 0;
break;
}
// change states if desired
const uint32_t lbButtonBit = (1 << (LaunchBallButton - 1));
if (newState != lbState)
{
// If we're entering Launch state OR we're entering the
// Press-and-Hold state, AND the ZB Launch Ball LedWiz signal
// is turned on, simulate a Launch Ball button press.
if (((newState == 3 && lbState != 4) || newState == 5)
&& wizOn[ZBLaunchBallPort-1])
{
lbBtnTimer.reset();
lbBtnTimer.start();
simButtons |= lbButtonBit;
}
// if we're switching to state 0, release the button
if (newState == 0)
simButtons &= ~(1 << (LaunchBallButton - 1));
// switch to the new state
lbState = newState;
// start timing in the new state
lbTimer.reset();
}
// If the Launch Ball button press is in effect, but the
// ZB Launch Ball LedWiz signal is no longer turned on, turn
// off the button.
//
// If we're in one of the Launch states (state #3 or #4),
// and the button has been on for long enough, turn it off.
// The Launch mode is triggered by a pull-and-release gesture.
// From the user's perspective, this is just a single gesture
// that should trigger just one momentary press on the Launch
// Ball button. Physically, though, the plunger usually
// bounces back and forth for 500ms or so before coming to
// rest after this gesture. That's what the whole state
// #3-#4 business is all about - we stay in this pair of
// states until the plunger comes to rest. As long as we're
// in these states, we won't send duplicate button presses.
// But we also don't want the one button press to continue
// the whole time, so we'll time it out now.
//
// (This could be written as one big 'if' condition, but
// I'm breaking it out verbosely like this to make it easier
// for human readers such as myself to comprehend the logic.)
if ((simButtons & lbButtonBit) != 0)
{
int turnOff = false;
// turn it off if the ZB Launch Ball signal is off
if (!wizOn[ZBLaunchBallPort-1])
turnOff = true;
// also turn it off if we're in state 3 or 4 ("Launch"),
// and the button has been on long enough
if ((lbState == 3 || lbState == 4) && lbBtnTimer.read_ms() > 250)
turnOff = true;
// if we decided to turn off the button, do so
if (turnOff)
{
lbBtnTimer.stop();
simButtons &= ~lbButtonBit;
}
}
}
// If a firing event is in progress, generate synthetic reports to
// describe an idealized version of the plunger motion to VP rather
// than reporting the actual physical plunger position.
//
// We use the synthetic reports during a release event because the
// physical plunger motion when released is too fast for VP to track.
// VP only syncs its internal physics model with the outside world
// about every 10ms. In that amount of time, the plunger moves
// fast enough when released that it can shoot all the way forward,
// bounce off of the barrel spring, and rebound part of the way
// back. The result is the classic analog-to-digital problem of
// sample aliasing. If we happen to time our sample during the
// release motion so that we catch the plunger at the peak of a
// bounce, the digital signal incorrectly looks like the plunger
// is moving slowly forward - VP thinks we went from fully
// retracted to half retracted in the sample interval, whereas
// we actually traveled all the way forward and half way back,
// so the speed VP infers is about 1/3 of the actual speed.
//
// To correct this, we take advantage of our ability to sample
// the CCD image several times in the course of a VP report. If
// we catch the plunger near the origin after we've seen it
// retracted, we go into Release Event mode. During this mode,
// we stop reporting the true physical plunger position, and
// instead report an idealized pattern: we report the plunger
// immediately shooting forward to a position in front of the
// park position that's in proportion to how far back the plunger
// was just before the release, and we then report it stationary
// at the park position. We continue to report the stationary
// park position until the actual physical plunger motion has
// stabilized on a new position. We then exit Release Event
// mode and return to reporting the true physical position.
if (firing)
{
// Firing in progress. Keep reporting the park position
// until the physical plunger position comes to rest.
const int restTol = JOYMAX/24;
if (firing == 1)
{
// For the first couple of frames, show the plunger shooting
// forward past the zero point, to simulate the momentum carrying
// it forward to bounce off of the barrel spring. Show the
// bounce as proportional to the distance it was retracted
// in the prior report.
z = zBounce = -z0/6;
++firing;
}
else if (firing == 2)
{
// second frame - keep the bounce a little longer
z = zBounce;
++firing;
}
else if (firing > 4
&& abs(znew - z0) < restTol
&& abs(znew - z1) < restTol
&& abs(znew - z2) < restTol)
{
// The physical plunger has come to rest. Exit firing
// mode and resume reporting the actual position.
firing = false;
z = znew;
}
else
{
// until the physical plunger comes to rest, simply
// report the park position
z = 0;
++firing;
}
}
else
{
// not in firing mode - report the true physical position
z = znew;
}
// shift the new reading into the recent history buffer
z2 = z1;
z1 = z0;
z0 = znew;
}
// update the buttons
uint32_t buttons = readButtons();
#ifdef ENABLE_JOYSTICK
// If it's been long enough since our last USB status report,
// send the new report. We throttle the report rate because
// it can overwhelm the PC side if we report too frequently.
// VP only wants to sync with the real world in 10ms intervals,
// so reporting more frequently only creates i/o overhead
// without doing anything to improve the simulation.
if (reportTimer.read_ms() > 15)
{
// read the accelerometer
int xa, ya;
accel.get(xa, ya);
// confine the results to our joystick axis range
if (xa < -JOYMAX) xa = -JOYMAX;
if (xa > JOYMAX) xa = JOYMAX;
if (ya < -JOYMAX) ya = -JOYMAX;
if (ya > JOYMAX) ya = JOYMAX;
// store the updated accelerometer coordinates
x = xa;
y = ya;
// Report the current plunger position UNLESS the ZB Launch Ball
// signal is on, in which case just report a constant 0 value.
// ZB Launch Ball turns off the plunger position because it
// tells us that the table has a Launch Ball button instead of
// a traditional plunger.
int zrep = (ZBLaunchBallPort != 0 && wizOn[ZBLaunchBallPort-1] ? 0 : z);
// Send the status report. Note that the nominal x and y axes
// are reversed - this makes it more intuitive to set up in VP.
// If we mount the Freesale card flat on the floor of the cabinet
// with the USB connectors facing the front of the cabinet, this
// arrangement of our nominal axes aligns with VP's standard
// setting, so that we can configure VP with X Axis = X on the
// joystick and Y Axis = Y on the joystick.
js.update(y, x, zrep, buttons | simButtons, statusFlags);
// we've just started a new report interval, so reset the timer
reportTimer.reset();
}
// If we're in pixel dump mode, report all pixel exposure values
if (reportPix)
{
// send the report
plungerSensor.sendExposureReport(js);
// we have satisfied this request
reportPix = false;
}
#else // ENABLE_JOYSTICK
// We're a secondary controller, with no joystick reporting. Send
// a generic status report to the host periodically for the sake of
// the Windows config tool.
if (reportTimer.read_ms() > 200)
{
js.updateStatus(0);
}
#endif // ENABLE_JOYSTICK
#ifdef DEBUG_PRINTF
if (x != 0 || y != 0)
printf("%d,%d\r\n", x, y);
#endif
// provide a visual status indication on the on-board LED
if (calBtnState < 2 && hbTimer.read_ms() > 1000)
{
if (js.isSuspended() || !js.isConnected())
{
// suspended - turn off the LED
ledR = 1;
ledG = 1;
ledB = 1;
// show a status flash every so often
if (hbcnt % 3 == 0)
{
// disconnected = red/red flash; suspended = red
for (int n = js.isConnected() ? 1 : 2 ; n > 0 ; --n)
{
ledR = 0;
wait(0.05);
ledR = 1;
wait(0.25);
}
}
}
else if (needReset)
{
// connected, need to reset due to changes in config parameters -
// flash red/green
hb = !hb;
ledR = (hb ? 0 : 1);
ledG = (hb ? 1 : 0);
ledB = 0;
}
else if (cfg.d.plungerEnabled && !cfg.d.plungerCal)
{
// connected, plunger calibration needed - flash yellow/green
hb = !hb;
ledR = (hb ? 0 : 1);
ledG = 0;
ledB = 1;
}
else
{
// connected - flash blue/green
hb = !hb;
ledR = 1;
ledG = (hb ? 0 : 1);
ledB = (hb ? 1 : 0);
}
// reset the heartbeat timer
hbTimer.reset();
++hbcnt;
}
}
}
