Appendix 3. Plunger Sensor Technical Notes

Tracking a plunger for pinball simulation is a surprisingly difficult technical challenge. At first glance, it looks like a pretty easy problem with some simple solutions: it's just a linear position sensor, and there are some standard approaches for that. But there are two details to pinball simulation that make the problem harder: you need fine spatial resolution, and you need to be able to take readings quickly.

Spatial resolution and sampling speed

How well a plunger performs in a pinball simulator depends upon its spatial resolution and sampling speed. I consider a sensor suitable if it can determine the true plunger position to within 1/100", and produce at least 200 such position readings per second.
Spatial resolution refers to how precisely you can locate the current position of the physical plunger. The pinball simulator uses the position reading from the sensor to draw the on-screen plunger on the video display in real time, so that the player can see the on-screen plunger animated in sync with the physical plunger. For that animation to be realistic and smooth, the on-screen position has to track the physical position as finely as the video display can draw it. On an HD screen at playfield size, the spacing between pixels is about 1/50 of an inch, and on a 4K monitor it's about 1/100 of an inch. On a full-sized pin cab, the on-screen plunger should be roughly the same size as the real plunger, so if you want smooth, pixel-accurate animation, the sensor has to be able to pick up movement of the physical plunger on the same scale as the size of the on-screen pixels. That requires 1/100" precision from the sensor.
Spatial resolution also affects the quality of the physics simulation. All of the pinball simulators read the plunger position via the Windows USB joystick interface, which is capable of representing high precisions. Visual Pinball can handle 16-bit resolution, which translates to one part in 65,536, or about 50 microns (millionths of an inch) over the 3" travel of a physical plunger. That's a lot finer than the 1/100" resolution required for smooth animation, and much finer than any virtual pinball plunger sensors currently available (for Pinscape or any other system). The point is that there are practical benefits to working on better sensor designs than we have today, since VP's physics model can take advantage of a lot finer spatial resolution than today's sensors provide.
Sensor speed is also critically important. The plunger moves really fast when you pull it back and release it, so in order to track that motion in real-time, the sensor has to be able to take readings quickly. My measurements with the AEDR-8300 sensor indicate that the plunger in my test rig, which is a standard Williams/Bally plunger with a medium-tension spring, moves at a peak speed of about 4.5 meters per second. That means that the plunger covers its full 80mm travel range in about 25ms. To keep up with that fast motion, the sensor has to be able to take readings at least about every 5ms, or 200 times per second. Sensors with slower sampling rates than that will suffer from "aliasing", where they mistake retrograde motion for forward motion (or vice versa) when the plunger bounces back after hitting the barrel spring. When aliasing occurs, VP can't calculate the speed of the plunger correctly, which screws up the physics simulation. Aliasing can lead to wildly erroneous speed calculations. The Pinscape firmware has special handling to detect and correct for aliasing in slower sensors, but that necessarily loses information about the true instantaneous speed and position, so it's better if the sensor can produce readings frequently enough that the aliasing filter is never triggered.

The Pinscape plunger input processing algorithm

Pinscape does some artificial processing on the plunger signal it sends to the host, to compensate for the limitations of the USB gamepad interface that Visual Pinball and other simulators have traditionally used to receive the data.
In the original KL25Z version of Pinscape, the processing was a built-in part of the plunger system that was used unconditionally. In Pinscape Pico, you can configure the device to send the processed version of the data, which the Pinscape Pico JSON configuration refers to as "plunger.z", or the unprocessed position signal, referred to as "plunger.z0". The unprocessed "z0" version is just the real-time reading from the sensor, adjusted for the calibration ranges, while the processed "z" signal has additional time-based adjustments designed to compensate for the limitations of the USB interface.
The name "plunger.z" comes from the long-standing Visual Pinball convention of assigning the plunger input to the joystick Z axis. We refer to the unprocessed signal as "plunger.z0" to suggest that this is the original, raw version of the signal. (A zero suffix or subscript is commonly used in math and programming notation to refer to the initial value of a variable.)
In the Pico version of Pinscape, you can choose which plunger signal to send to the PC when you configure the gamepad input, by mapping the gamepad Z axis to either "plunger.z" or "plunger.z0". You can even have the Pico send both signals to the host, by mapping one gamepad axis to "plunger.z", and mapping a different gamepad axis to "plunger.z0". That way, you can choose which axis to use as the plunger input on a game-by-game basis, by using each game's joystick input setup dialogs to select which Pico gamepad axis to use for that game's plunger input. The reason you might wish to choose on a game-by-game basis is that the "plunger.z" processing might work better for some games than for others. The processing algorithm is very specifically designed around the way Visual Pinball's plunger input system works, and other games might have different physics processing models that result in less beneficial interactions with the Pinscape processing algorithm.
The "plunger.z" algorithm is designed to overcome (or at least mitigate) two major limitations in the way USB gamepads transmit data to the host. The first problem is that the USB gamepad sampling rate is relatively slow compared to the high-speed portion of a mechanical plunger's motion, so the host might miss some of the fast parts of the motion. The second problem, which is subtler but even more of a problem, is that the USB gamepad system doesn't give the host any information on the timing of the sensor readings being transmitted. This makes it impossible for the host to accurately calculate the speed of the plunger's motion, because to calculate speed, you need to know both the distance traveled, and the amount of time it took to travel that distance. Speed calculations can only be as accurate as the time measurements between position readings, and the USB gamepad system gives the host poor information on the time between readings.
The USB joystick system is specifically designed to allow a host program to sample the instantaneous state of a control. At first glance, this sounds wonderful for real-time signal processing: if you can know the state of a control at any moment in time, surely you can reconstruct an accurate position-vs-time curve for that signal. Not so. The problem is that there's a big, hidden assumption wrapped up in that phrase at any moment in time. USB unfortunately doesn't allow the host to sample the control at any moment in time; it only allows the host to sample the control at certain moments in the time. And what's more, USB doesn't allow the host to know what those certain points in time are! The host can only know the time that it asked USB for a sample; it can't know the point in time that the sample was actually taken. Under the covers, USB receives samples from the device at certain discrete points in time, and when the host asks for a sample, USB relays the latest one it received. The host can't determine when the sample were actually taken in real time, so it can't determine how much time elapsed between the samples. This makes it impossible to accurately reconstruct a position-vs-time curve, because we can only precisely know the position to plot on the curve for any given sample, but we can't know exactly where to put it on the timeline; we only that it occurred somewhere within a window that extends from the time we asked for the last sample to the time ask for the next sample.
This inherent imprecision in the USB gamepad sampling time base is a huge problem for plunger input. For slow-moving signals, where the signal doesn't change much on the time scale of the USB sampling interval, you can still reconstruct a fairly accurate picture of the position-vs-time curve, because the uncertainty about the exact time of each sample averages out over a long time scale consisting of many samples. But plungers move fast relative to the USB time scale. When pulled back and released, a plunger crosses its entire range of motion in about 30ms, which is time for 30 USB samples at the highest sampling rate available. The plunger accelerates through most of this range, so its position when its at or near its highest speed (as it reaches the end of its travel) only allows USB to capture one or two samples. The uncertainty in the elapsed time between successive signals translates to an uncertainty in speed (since speed is defined as change in position divided by elapsed time) that's around 100% of the calculated speed - in other words, we're reduced to practically guessing about the speed.
The Pinscape "plunger.z" algorithm works around these USB limitations by artificially slowing down the change in position readings sent to VP at key places in the plunger's motion, to increase the odds that VP will "see" the key position readings that define the overall plunger motion.
It's important to understand that Pinscape's "plunger.z" processing relies on how the simulator interprets the data, and in particular on how VP interprets the data. That's why the processing might not interact beneficially with every simulator: it's tuned especially for the way VP interprets the data. I expect that most other simulators use a similar model out of practical necessity, but VP is really the only open-source simulator, so it's the only one where we can know for sure what's going on inside its brain. VP doesn't just take the USB input and directly apply it to the simulated on-screen plunger motion, because the designers of VP were aware of the USB timing problem described above from the time they first added mechanical plunger input support. At the time they designed that, all of the plunger devices were closed-source commercial products, so VP's designers couldn't do anything to improve the data the devices were sending - they had to work with the crappy data they were getting from the devices, made crappier by these USB timing problems. The way VP dealt with it was to create a simple physics model of the plunger inside VP, and apply the incoming USB data to that model as though the USB data were pulling on the model with a spring. This helped smooth out the erratic USB timing by interposing a model physical system, with mass and momentum, that could only move at physically realistic speeds and accelerations. When USB data contradicted the limits of the physical model, the physical model would catch up as fast as realistically possible.
The "plunger.z" algorithm works with the host-side physics model to further improve the accuracy of the motion reconstruction. The key principle that the algorithm is based on is that the plunger always acts like a plunger: the motion during the "release" phase of a pull-and-release gesture always follows the predictable physics of a mass being driven forward by a compressed spring.
In addition, the algorithm takes advantage of something we pointed out earlier, that the "raw" USB samples can be used to accurately reconstruct the motion of an object that moves slowly relative to the time scale of the USB samples, but not for fast motion. Plunger motion is only "fast" in this sense during the "release" phase, where you pull back the plunger and let it go, allowing it to be driven forward by the spring.
So the first thing the algorithm does is to try to detect when these "release" motions occur. Most of the time, when a release isn't in progress, the algorithm just sits quietly in the background and watches, without modifying the raw data sent across USB. VP sees the original, unfiltered plunger motion during these times. The algorithm only starts modifying the data sent to the host when it detects that a release might be in progress. It does this by watching for a series of successive forward motions at increasing speed.
When the algorithm detects the start of a release motion - a series of several position readings moving in the forward direction at increasing speed - it enters the "release" phase of the algorithm. At the start of this phase, it goes back and finds the maximum retraction position recently recorded, which it assumes is the point where you let go of the plunger. The algorithm then artificially repeats position in the USB readings for several USB samples in a row.
The point of repeating the maximum retraction reading is to increase the odds that the host actually detects this reading, so that the host will use it as the start of its own "model" plunger motion. One of the unpredictable aspects of the USB system is that the simulator has no guarantee that it sees every USB sample that the device sends. As noted above, the USB gamepad interface on the host allows the host to read the instantaneous state of an input: so if the host doesn't happen to ask for an input at a given moment, it might never see that input. This doesn't reflect a performance problem in VP - it's not because VP "stuttered" or "lagged" or anything like that. Rather, it's because VP takes samples at specific intervals in time, and those intervals simply don't always line up with the real-time cycle of inputs sent by the device, and don't even line up with the cycle of inputs processed by the operating system's USB stack. When you have two or more independent timing cycles running on their own clocks, they will inevitably go in and out of phase with each other. At times when VP and the device are out of phase, VP will miss a reading or two, simply because it didn't happen to ask for one at the same time the device sent it.
In practice, this specific problem - that VP entirely misses some percentage of USB readings that the device sends - was a huge source of unpredictability in plunger tracking prior to the Pinscape "plunger.z" algorithm. VP's notion of where the release phase "started" was pretty much a random guess, based on the exact moment in time that it happened to sample the plunger when it was close to the start of the release motion. By "holding" the actual maximum retraction position in the USB readings for several consecutive readings, the "plunger.z" algorithm makes VP's reconstruction vastly more accurate and consistent by ensuring that VP sees the correct starting position, removing most of the variability that came from the unpredictable timing of its USB sampling.
After the "hold" period (repeating the maximum retraction position in the USB readings), the algorithm resumes sending real-time readings of the plunger's forward motion. This continues until the device detects (through its raw sensor input) that the plunger has crossed the resting point (the "zero" in the plunger calibration scale). At that point, the algorithm takes over again, and goes into a new "hold" pattern, this time at the "overshoot" position - an artificially inferred maximum forward travel position that an ideal plunger will eventually reach as it bangs into barrel spring that limits its forward travel. The algorithm calculates and then holds this artificial forward position for several consecutive USB readings, for exactly the same reason that it held the maximum retraction position at the start of the release travel: to make sure that the simulator sees that the plunger has reached its maximum forward extent, even if the simulator misses a one or more USB samples sent by the device due to accidents of the time alignment of its USB sampling cycle.
After holding the overshoot point, the algorithm enters its final step, which is to hold the rest position (the "zero" point in the plunger calibration) until the physical plunger comes to rest. This is important because it allows the simulated plunger on the VP side to come to rest without being disrupted by the highly random high-speed oscillations that the real plunger goes through after hitting the barrel spring. Prior to the "plunger.z" processing, those random oscillations caused another huge amount of erratic variability in VP's simulation, because the oscillations happen so fast that VP often misinterpreted them as sudden stops before hitting the barrel spring overshoot point. The oscillations are fast enough to cause a phenomenon known as "aliasing" in the context of digital signal processing, which happens when attempting to sample a signal with a characteristic frequency higher than 1/2 of the discrete sampling rate. VP has some code to attempt to suppress this problem, but it's essentially futile to try to suppress it after the fact; it is a fundamental principle of analog-to-digital converter design that you must suppress such high frequency signals in the analog domain, using a low-pass filter, before even taking the discrete-time samples. That's essentially what this phase of the algorithm is doing.
Once the physical plunger comes to rest (that is, when the motion drops below a certain distance change threshold for several successive sensor samples), the algorithm goes back to "background" mode, where it allows the raw data to pass through unmodified while waiting for the next release gesture.
All of the "hold" steps in the algorithm depend heavily on the assumption that the host-side simulator is using a physics model for its plunger based on the same principle as VP's plunger model. This approach works well with such a physics model because the model will naturally translate the artificial hold periods into smooth plunger motion. If the host were to just exactly replicate the input signal, without first applying it to a mass-and-momentum model, the signal would produce an unnaturally chunky effect. Even then, the effect would probably be more uncanny than obvious, since the artificial hold periods are very short in human terms, only a few USB samples in duration. So you probably wouldn't see the holds per se, except for the perfectly still stop at the zero point at the end of the release, which can go on for a couple of seconds while the real plunger comes to rest.

Plunger speed input and OpenPinDev

In 2024, I proposed some improvements to the VP input model for plunger and nudge data, including a way for the device to send the plunger's speed alongside its position in each reading. A device like Pinscape is capable of measuring the instantaneous speed of the plunger with considerable precision when using sensors that can be sampled at high speeds, and I felt that providing this information to the pinball simulator could improve the accuracy of the simulator's reconstruction of the plunger motion. VPX has since incorporated these new capabilities into its USB interfaces, and Pinscape Pico implements them on the device side.
Here's some technical background on the new features:
Improving the Virtual Pin Cab Input Model
In addition, I proposed a new HID interface, Open Pinball Device (also known as OpenPinDev), especially designed for virtual pinball That's now implemented in VPX and Pinscape Pico. The new interface doesn't change any of the technical details of plunger input, but it provides a more purpose-built way of providing the input to VP.
Open Pinball Device HID
When using a simulator like VPX that's capable of receiving plunger speed input, and a device like Pinscape Pico that's capable of providing it, you're probably better off enabling plunger speed input and sending the unprocessed "plunger.z0" data for the position input. Most of the problems that the Pinscape plunger algorithm addresses boil down to the host's inability to make accurate estimates of the plunger's speed from USB position readings alone. Since the device can calculate speeds with decent accuracy (owing to its direct access to the sensor and to a precise clock), it can send that speed data to the host, eliminating the need for the host to make wild guesses about the speed based on poor timing data. That eliminates the need for the "hold" periods, which are mostly there to give the host a better idea of the actual speed by increasing the effective time base to a point where it's long compared to the USB sampling cycle. The hold periods work pretty well for what they're intended to accomplish, which is to make the physics response in the game more consistent with the physical plunger motion. But they do add timing artifacts that are sometimes perceptible. So you might prefer the overall effect of using the raw position input in conjunction with speed input, which ideally will produce similarly consistent physics in the game without the timing artifacts of the processed position readings.

Notes on sensor classes

The following sections describe several of the sensor classes that I've experimented with over the years, trying to find the perfect way to provide plunger position input to a pinball simulator. Some of this is speculative, containing ideas for things to try, so it's probably mostly of interest to people designing their own virtual pinball controller software similar to Pinscape.

Potentiometer

This is one of the simplest ways to measure a linear position, and it actually compares pretty well to more complicated approaches. It's used in a lot of industrial applications where you need fast readings without extremely high resolution.
Physically, a potentiometer consists of a fine electrode that moves across a resistive conductor such as graphite - something that conducts electricity, but with relatively high resistance. Resistance in a material like graphite is a function of the distance between measuring points, so as you move the electrode across the graphite, the resistance between the electrode and any fixed point on the graphite varies with the distance between the two points. If you measure the resistance at point A as higher than the resistance at point B, you know that point A is further away from the fixed end than point B.
For our purposes, we use "linear taper" potentiometers, which are built so that the electrical resistance varies linearly with the electrode position. Voltage and resistance are also linearly related (through Ohm's Law), so if we supply a fixed voltage across the fixed ends of the pot, the voltage we read at the electrode will vary linearly with the resistance, which varies linearly with the position of the electrode. The KL25Z has an on-board analog-to-digital converter (ADC) that can sample an analog voltage level and report it to the software as a digital reading.
How accurate is a potentiometer? The one that we recommend in the parts list is readable to about 1% accuracy with the KL25Z ADC. That translates to about 3/100". So it's not quite as good as we need for perfectly smooth animation, and it means that the "jitter filter" in the Pinscape firmware is usually needed to get steady readings when the plunger is standing still.
This could be improved by using a different potentiometers with higher accuracy, but I haven't found anything available that looks like it would do better.
The potentiometer's spatial resolution might not be perfect, but its sampling speed is extremely fast. The potentiometer itself can be sampled in arbitrarily short intervals, as it provides an analog voltage level that adjusts to changes in position effectively instantaneously. Everything at the analog level moves at the speed of electric field propagation in the conductors, which is close to the speed of light. So the limiting factor isn't the analog signal, but rather the ADC sampling time. The ADC works by charging a small capacitor internally, and this takes a more macroscopic amount of time. The fastest possible ADC sampling time on the KL25Z is about 10µs, although there's a significant trade-off between speed and accuracy, so we use a slower mode that takes more like 30µs for each reading. That's still about 100 times faster than we need it to be (as discussed earlier, we only need readings about every 5ms), so we can take many readings over the sampling period and average them together, to further reduce noise and improve accuracy. Since the January 2020 version, the firmware takes readings continuously, and computes a rolling average each time a result needs to be sent to the PC. Each reading sent to the PC represents the average of 128 ADC samples. This smooths out the noise and makes for highly stable readings.

Quadrature sensors

Quadrature is an electrical engineering term describing two signals that are 90° out of phase with each other. This has numerous applications in signal processing, but the one we're concerned with here is its use for motion sensing. A quadrature motion encoder works by observing transitions between "on" and "off" zones marked along a "measuring stick", using two sensors that are offset slightly from one another. The measuring stick is more formally called a "scale".
The 90° phase difference in this case refers to the size of the offset between the two sensors: it's arranged so that it's half the size of the marked on/off zones on the scale. If you observe the on/off signal from each sensor as they move across the scale, you'll see the leading sensor make its on/off transitions 90° of phase ahead of the trailing sensor. Which sensor is the "leading" sensor and which is the "trailing" one depends on which direction they're moving across the scale, so you can tell which direction you're moving by observing the phase between the two signals and seeing which sensor is making its transitions first at each step. Or, equivalently, you can look to see if the signals are 90° or 270° out of phase with each other.
The scale can be implemented in many ways, but the common element is that it has to have a series of on/off zones of about equal size across the range of travel. These can be electrically conductive and non-conductive regions, for example, or magnetic north/south poles, or optical black/white or transparent/opaque bars. The specific quadrature sensors that we use in the Pinscape projects are optical sensors, so our scales use the transparent/opaque approach. In other words, our scale is simply a series of black bars printed on transparent film.
A quadrature sensor isn't actually a position sensor - it's a motion sensor. This type of sensor can only detect when it's moving from one bar to the next on the scale, and the direction of that motion (using the signal phase). If we combine these two capabilities - detecting a change from one bar to the next, and knowing which direction we went during that change - we can keep count of how many bars we are away from our starting point. So if we have a known starting point, and we know the length of each bar, we can infer our current position: our position at any given time is the starting position plus or minus the net distance we've moved since then.
For the plunger application in particular, we have an excellent "known starting point", namely the plunger rest position. It's usually safe to assume that the plunger starts at the rest position, so we take that to be the initial position at startup. All subsequent inferred positions are relative to this starting point.
There's an inherent drawback to using a quadrature sensor as a position sensor by way of the net travel distance, which is the possibility for missed transitions. If the sensor ever misses a transition, the count will no longer be in sync with reality, so the inferred position that we figure based on the count will differ from the actual position by the number of missed counts. Sensors in practice do tend to miss some fraction of transitions, so the error between inferred and actual position tends to increase over time. To mitigate this in our application, the Pinscape software can be set, optionally, to reset the inferred position to the starting (plunger rest) position after any extended period without any motion. Just as it's usually safe to assume that the initial position at system startup is the rest position, it's a good bet that a motionless plunger is in equilibrium at its rest position, since it's difficult to hold it perfectly still anywhere else for any length of time, and there's no reason to do so even if you could.
Interfacing a quadrature sensor to the KL25Z is straightforward. We treat the two sensor output channels as digital inputs (high/low signal levels, corresponding to the on/off zones in the scale), and set them up to trigger interrupts. The interrupt handler for each channel uses a lookup table to figure the direction increment represented by the current channel signal levels, adding or subtracting one from a running counter.
A fast-moving plunger can result in extremely rapid signal transitions on the channels - fast enough to overwhelm the KL25Z's interrupt response time. The quadrature sensor software interface in the Pinscape firmware uses custom hardware interrupt handlers to optimize the response time, and with that it requires 6.5µs to process each channel interrupt. When considering new quadrature sensors, we have to calculate the signal rate at peak plunger speeds (about 4.5 mm/ms) to make sure it doesn't exceed the 6.5µs response time, or equivalently, about 150kHz. In many cases, the quadrature sensor itself might be the limiting factor - most of the ones I've looked at quote peak signal rates in their data sheet far below 150kHz (it's usually closer to 20kHz to 30kHz).

AEDR-8300

The AEDR-8300 is a reflective optical sensor. It has an LED emitter that shines a narrow light beam towards the scale, and a pair of photoreceptors that capture the light reflected back from the scale. The photoreceptors use the usual quadrature arrangement where they're slightly offset from one another.
The AEDR-8300 product line has parts available with different bar spacings. The particular sensor we use has 75 line-per-inch bars, meaning that each black/white pair is 1/75" wide. The nature of quadrature sensing means that we can tell our position to within 1/4 of a line pair, so we effectively get 1/300" resolution.
At peak speeds, the 75 LPI spacing results in quadrature pulses from the sensor about every 19µs. That's safely above the 6.5µs interrupt response time I've measured on the KL25Z. Note that the finer-pitched sensors in the AEDR-8300 line would likely overwhelm the KL25Z, and probably wouldn't be able to keep up with the peak plunger speeds anyway, as these sensors have their own maximum signal rates that are below the KL25Z's limit. But that's okay, since the 75 LPI spacing gives us such high precision that we really have no need for a finer pitch.

Linear image sensors

The first Pinscape plunger sensor was based on a linear image sensor, the TSL1410R (and the similar TSL1412S). These sensors are no longer available. The TCD1103 works on a similar principle, but it requires a focusing lens, which makes it more complex to set up.
The principal of operation of these sensors is pretty simple. The sensor consisted of a single row of light sensor pixels about the same length as the plunger travel distance. We arranged the sensor so that the row of pixels ran along the axis of the plunger travel, and placed it directly adjacent to the plunger, with the pixels facing the plunger. We put a light source on the opposite side of the plunger, facing the pixels. This caused the plunger to cast a shadow on the pixel array. We'd then read the pixel array and find the location of the shadow, by looking for an edge between a light area and a dark area in the image. The edge tells us where the plunger is currently positioned.
In principle, a sensor of this type could achieve spatial resolution equal to its pixel size. In practice, though, the shadow cast by the plunger isn't perfectly sharp. Shadows always have a little fuzzy area when the light source isn't a perfect point source, because the light source is only partially blocked at the leading edge of the shadow. So we can't tell exactly where the plunger was; we have to guess based on the midpoint of the shadow. The best possible resolution in my setup is about 1/50". That's a bit below the ideal needed for pixel-level resolution on the TV display, so there's a little bit of jitter (by a pixel or two) as the guess about the shadow position would vary from frame to frame.
The TCD1103 can perform much closer to its theoretical pixel limit thanks to the requirement for focusing optics.
The Pinscape software uses a simple edge-detection algorithm with these sensors, where it looks for a region with a rapid transition from a run of bright pixels to a run of dark pixels. This algorithm inherently compensates to some extent for exposure levels, since it works based on the difference in brightness across pixels rather than looking for absolute brightness levels.

TSL1410R/TSL1412S

The TSL1410R happened to be perfectly designed for this application. Its pixel row was 3" long, with 1280 pixels across the file. That's 400 pixels per inch.
The TSL1412S was almost exactly the same, with the only difference being a slightly longer sensor window and a correspondingly larger number of pixels (1536). The pixel spacing was the same on both sensors, and they were identical in their electronic interface.
These sensors work by using photoreceptors to charge capacitors. The electronic interface lets the host microcontroller connect the individual integrating capacitors to an analog output port, one at a time through a shift register. The capacitor charge is an analog voltage level, so we use the KL25Z's ADC to sample each pixel's charge level and convert it to to a digital reading.
The KL25Z has the capability to control the ADC through its DMA (direct memory access) controller, which was key to making the image capture fast enough to work as a plunger sensor. The DMA runs concurrently with the CPU, so the firmware was able to start the DMA reading process and then return to other work while the pixels were clocked into memory by the DMA controller. This allowed a full reading to be taken in about 2.5ms, or 400 frames per second, which is fast enough to keep up with the peak plunger release speed.

TCD1103

This is a tiny CCD sensor made by Toshiba, with 1500 pixels arranged in a single linear file. It works using the same edge detection algorithm as the TSL1410R/TSL1412S, but its pixel window is only about 8mm long, so it requires a lens to focus a reduced image of the plunger on the sensor. If the lens is properly aimed and focused, this sensor can resolve the edge at the end of the plunger to a single pixel, so it achieves spatial resolution of nearly its full 1500 pixels across the 80mm travel range of the plunger, which translates to about 1/400" resolution.
The electronic interface to this sensor is similar to that of the TSL141x sensors. Like those sensors, the TCD1103 has an electronic shutter function that moves the pixel charges onto capacitors, which the microcontroller reads out by sampling an analog voltage output one pixel at a time. With DMA transfer, the KL25Z can transfer the pixel file in about 3ms.

Distance and proximity sensors

The original commercial plunger kits (e.g., the Nanotech Mot-ION controller, and the various generations of the VirtuaPin plunger kits) and all used IR-based proximity sensors. IR proximity sensors work by emitting an IR light signal and sensing the brightness of the reflection from a nearby object via a photoreceptor.
Technically, proximity sensors are only meant to detect proximity - a yes/no question, "is an object is within range of the sensor?" However, most of these sensors don't answer the yes/no question directly, but rather just give you their raw analog brightness reading, leaving it up to the application to interpret that by comparing it to a calibrated threshold. The commercial plunger kits don't just treat the brightness as a binary value above or below a threshold, however. They instead interpret it as a continuous quantity that correlates to the distance to the target (which, in this case, is the end of the plunger rod). At a basic physics level, the brightness of a light source (or of a reflection) varies inversely with the distance to the source, so by applying some math to the brightness reading, we can infer the distance.
Proximity sensors as a class aren't meant to be used this way, though. Their brightness sensors aren't designed to be fine measurement instruments, and attempting to interpret the brightness reading as a proxy for distance doesn't always yield very good results. The relationship between reflected brightness and distance is inherently non-linear at the physics level, even with an idealized theoretical model. It's worse in a practical setup, since the sensor is subjected to interference from stray light, reflections from other surfaces besides the target, variations due to ambient temperature, and electronic noise. The early commercial plungers, which were based on Sharp analog IR proximity sensors, could only achieve a spatial resolution about 1/2".
A newer IR proximity sensor chip, Vishay's VCNL4010, performs quite a lot better. This chip has its own on-board ADC for the brightness signal (the older Sharp chips produced an analog voltage that had to be sampled externally, on the microcontroller), which seems to make for much higher resolutions. This is reportedly the sensor used in the VirtuaPin v3 plunger kit, so I recently (May 2021) added support for it to the Pinscape firmware, to help out some VirtuaPin customers who had requested it because they switched to the Pinscape firmware on their controller boards. This sensor is actually pretty good: in my testing, it reliably resolved distances to less than 1mm over most of its range. (It gets coarser at the "far" end where the plunger is farthest away from the sensor; this region is the most challenging for the sensor because very little light is reflected back at that distance.) That's not as good as some of the other sensors (potentiometer, quadrature, linear imaging), but it's good enough to be usable. The tricky part of this sensor is that its response curve (the relationship between measured brightness and object distance) is a little bit erratic. But it's close enough to a power law relationship that the distance conversions come out looking plausibly linear, if you don't look too closely.
There's a related but different type of sensor that uses reflected IR light to actually measure distance - by design, not just incidentally. These are called "time-of-flight" sensors, because they measure distance by timing the round-trip time for light pulses to be returned from the target, instead of just measuring the brightness of the reflection. In principle, this should yield much more precise distance measurements, because the time measurement isn't affected by the reflectivity of the target, which is obviously a huge factor for the brightness sensors. In addition, the underlying physical quantity being measured - round-trip travel time of the IR photons - has an inherently linear relationship with the distance, so the conversion from measured quantity to distance is straightforward and doesn't lose precision or magnify uncertainties, the way that the inverse power-law relationship does for brightness-to-distance conversions.
The Pinscape software supports the current best-of-breed sensor in the time-of-flight class (as far as I've found), the VL6180X. Unfortunately, in my testing, this sensor is inferior to the VCNL4010, despite the more sophisticated technology. The VL6180X has a nominal resolution of 1mm (1/25"), which I'd consider just barely usable. Unfortunately, that's only the nominal resolution; the actual accuracy in my measurements is more like 5mm to 10mm. That's too coarse to be usable, in my view; it gives you some semblance of plunger operation, but isn't good enough for smooth animation, and definitely isn't good enough for skill shots. The sensor is also too slow; it takes it at least 14ms to produce a sample, in its fastest and least accurate averaging mode.
I'm planning to keep an eye out for new time-of-flight sensors coming onto the market, since this seems like a promising design approach that could eventually yield high-precision non-contact distance sensors. But right now there doesn't seem to be a production chip available that performs well enough for our purposes. By the same token, given that the VCNL4010 is already passably good, it would be worth monitoring developments in that product line; it wouldn't take a huge amount of improvement for that type of sensor to become a top choice.

Bar code absolution position sensing

This is an experimental approach that I've been looking at but haven't yet made work well enough. The idea is to do absolute position sensing using an optical Gray code and a suitable small image sensor. We create an optical scale with a Gray code printed on it with a unique code in each position across the 3" plunger travel range, then we use an optical sensor to reach the code at the current position. We translate the bar code by looking it up in a table to get the absolute position.
The principle here is simple, and a suitable sensor is available: TSL1401CL. As you might guess from the name, this sensor comes from the same series as the late, great TSL1410R that we mentioned earlier, but it has a much smaller window - only about 128 pixels over 1cm. That makes it unsuitable for the straightforward "shadow edge" method we used with the larger sensor. But 128 pixels is more than enough to read a bar code. With 128 pixels to work with, we could easily come up with a bar code that could represent perhaps 20 bits. That would be much more than we need; just 10 bits would be enough to encode 1000 unique positions along a scale. If we spread 1000 codes over 3", we'd be able to resolve about 300 positions per inch. That's at the high end of the resolution requirements we laid out earlier.
This all works nicely in principle, and the Pinscape firmware already has support for a form of this, since I've done some experiments with it. The challenge is making the optics work. I haven't been able to find a way to get a sharp enough image of the bar code to read it reliably at the required resolution. So the open area of research is how to arrange a lens or other optics to get a good enough image on the sensor.
If I can get this working, I think it would make a really great sensor option. It should have the excellent spatial resolution and sample-to-sample stability of the quadrature sensors, plus the absolute position sensing of all of the other types, which would be a powerful combination.

Rotary absolute position sensing

Absolute sensing like that described above is available in commercial position sensors that do rotary position sensing - that is, they measure the turn angle of a shaft, rather than a linear offset. There are some magnetic rotary absolute sensors available with very high angular resolutions.
The trick is to translate the linear motion of the plunger into a rotational angle.
One possibility is a lever between the plunger and rotating shaft. A lever connects to a shaft at one end and to the plunger at the other end; moving the plunger rotates the shaft through the lever. One complication is that the distance between the shaft and plunger would vary over the plunger's range of motion, so a sliding connector at one end would be required. The other complication is that the rotational angle wouldn't vary linearly with the plunger's position - it would be sinusoidal. The sine curve approaches closer to linearity the longer you make the lever, so one solution could be to make a very long lever. On the other hand, that would require a very high-res sensor, since you'd only be using a small fraction of its angular range. A better solution might be to use calibration in the software to figure out where you are on the sine curve, and translate mathematically from the sine curve to the linear position. That's easy in principle, but it seems challenging to make the mechanics of the calibration user-friendly.
Another possibility is to translate the linear motion to angular motion through a belt that wraps around a pair of wheels. The belt moves with the plunger, and as it moves, it turns the wheels. The angular sensor is connected to the axle of one of the wheels. This would yield a very straightforward linear relationship between position and angle, so it wouldn't have any of the calibration problems of the lever approach. This seems like a pretty straightforward mechanism, but I haven't tried to realize it physically yet. I think the main challenge would be ensuring a solid connection between the plunger, belt, and wheels, so that the plunger motion directly translates to wheel motion without any play or hysteresis. Any mechanical play in the system would manifest as loss of precision in the readings, so this would all have to be very solidly connected.
If the mechanics could be worked out, selecting a suitable absolute rotary encoder and connecting to the software should be relatively simple. There are a number of high-res magnetic encoders available with 12-bit or higher resolution, which means 4096 counts per turn. With the wheel arrangement describe above and 1" diameter wheels, the 3" of plunger motion would turn into almost one full turn, so we'd get perhaps 75% of the range of the sensor. That means we could achieve as many as 1000 counts on the sensor per inch, or 1/1000" linear resolution. That would be far higher than any of the other sensors. Interfacing one of these sensors to the software should be pretty easy, as the newer ones use modern microcontroller-friendly interfaces like SPI or I2C.