The P3 SDK shows how to activate a coil in the Drivers section of P3_SDK_V0.8/P3SampleApp/Documentation/html/_physical_features.html
The coil is an instance of the Driver class documented in P3_SDK_V0.8/P3SampleApp/Documentation/html/class_multimorphic_1_1_net_proc_machine_1_1_machine_1_1_driver.html
The Driver class can also control flashers, motors, shakers and similar devices.
We will be exploring the Driver methods in more detail.
Disclaimer: this information was tested with a P-ROC before I received my P3 machine.
Multimorphic suggests an architecture where the module driver exposes a high-level API to control the mechs. The application never calls the drivers directly, instead it sends events to the module driver to control the mechs.
Take a look at the module driver documentation to learn which events are available. For example, Heist has events to control the crane. Take advantage of these events to leverage all the logic implemented in the module driver.
So why bother learning how to control drivers directly? You might have to call the drivers directly for flashers. Maybe you need a work-around for a gap of functionality in the module driver (ball search maybe). You might have to write a module driver one day. Finally, it’s always good to know how it works under the hood.
The list of coils consists of the coils in the base machine plus the coils in the installed module. The list of coils in the base machine is built-in to the SDK. The list of coils in the module comes from the Coils section of the module definition file. In the simulator, that file is here ~/.multimorphic/P3/ModuleDrivers/<moduleName>/<moduleVersion>/<moduleName>.json
The P3Controller instance stores the list of coils in an AttrCollection named Coils. Within a mode, you can access a specific coil with p3.Coils["coilName"]
You can activate a coil forever with the Enable method.
public void Enable()
For example: p3.Coils["coilName"].Enable()
This is common to do for a flipper hold coil, but this can be a bad idea in other contexts. The application might be careful to deactivate the coil, but if something goes wrong, the coil could overheat. Whenever possible, it is better to enforce a time limit in the activation itself or use pulse width modulation to lower the power as described later.
You can deactivate a coil with the Disable method.
public void Disable()
For example: p3.Coils["coilName"].Disable()
It is perfectly fine to disable a coil that is already disabled.
You can activate a coil with the Pulse method.
public void Pulse()
For example: p3.Coils["coilName"].Pulse()
This will activate the coil for the default pulse time in milliseconds. The default pulse time is accessible with the property p3.Coils["coilName"].DefaultPulseTime. The property data type is int but the value is constrained to a byte (0 to 255) in the set accessor.
The default pulse time is initialized from the PulseTime field in the module definition file. For flippers and bumpers, the default pulse time comes from a GameAttribute (i.e. a settings in the service menu). The settings name is declared in the module definition file:
"Flippers": [
{
"Name": "flipperUpR",
"SettingPrefix": "UpperRightFlipper",
"SwitchName": "buttonRight2",
"SecondarySwitchName": "buttonLeft2",
"AlternateSwitchName": "buttonRight0",
"MainCoilName": "auxRight0",
"HoldCoilName": "auxRight1"
}
],
"Bumpers": [
{
"Name": "popBumper",
"SwitchName": "popBumper",
"CoilName": "popBumper",
"SettingName": "popBumperStrength",
"SwitchPolarity": true,
"AllowShotgunning": true
}
],
"Coils": [
{
"Name": "dropTargetReset",
"Number": "A6-B0-1",
"PulseTime": "30",
"Label": "Drop Target Reset"
},
A module driver can choose to initialize the default pulse time from a settings for other coils as well. This is module dependent.
I simplified a little because the settings is often a strength value and not a pulse time. The value maps to a table of patter patterns sorted in increasing power. This is one of the advantages you get by letting the module driver pulse the coil for you. More on patter below.
You can activate a coil for a specific time by calling the Pulse method with an argument.
public void Pulse(int milliseconds)
For example: p3.Coils["coilName"].Pulse(30)
When milliseconds < 0, this activates the coil for the default pulse time.
When milliseconds == 0, this activates the coil forever. Be careful with that.
When milliseconds <= 255, this activates the coil for that many milliseconds.
When milliseconds > 255, this throws ArgumentOutOfRangeException.
The Pulse() method with no arguments is equivalent to Pulse(-1)
The Enable() method is equivalent to Pulse(0)
You can activate a coil after a time delay with the FuturePulse method.
public void FuturePulse(int milliseconds, UInt16 futureTime)
For example: p3.Coils["coilName"].FuturePulse(30, 500)
The milliseconds argument behaves like in the Pulse method.
The C header in libpinproc describes the futureTime argument as: “The value at which hardware timestamp the pulse should occur. Currently only the low 10-bits are used.” The value is taken modulo 1024 so the effective range is from 0 to 1023. The hardware timestamp appears to wrap around after approximately 1 second, so each unit of futureTime is close to 1 millisecond.
The coil is activated when the hardware timestamp is equal to futureTime. Said another way, the coil is activated at a time relative to a one second internal clock plus an offset. This could be very soon from now, or up to 1 second for any value of futureTime.
When the coil is already activated, this method does not appear to deactivate the coil while waiting for the futureTime to match.
See the Schedule() method for an easier API to use.
Activating a coil for a long time heats the coil, making the coil weaker and possibly leads to permanent damage. A better approach is to activate and deactivate the coil repeatedly in quick succession such that the coil is energized only a fraction of the time. This technique is called Pulse Width Modulation or PWM.
The application can try to do this by enabling and disabling the coil explicitly but with the USB latency, the time resolution is coarse and not uniform. A better way is to let the P3-ROC take care of PWM at 1 millisecond resolution without network traffic.
You can activate a coil with PWM with the Patter method.
public void Patter(ushort on_time, ushort off_time, ushort orig_on_time)
For example: p3.Coils["coilName"].Patter(2, 8, 12)
The values of on_time, off_time and orig_on_time must be between 0 and 255 inclusive, otherwise this throws ArgumentOutOfRangeException.
If orig_on_time is 0, this will repeatedly enable the coil for on_time milliseconds and disable it for off_time milliseconds. For Patter(a,b,0), you get a,b,a,b,…
If orig_on_time is greater than 0, this will activate the coil for orig_on_time milliseconds, then it will repeatedly disable the coil for off_time milliseconds and enable it for on_time milliseconds. For Patter(a,b,c) you get c,b,a,b,a,… The documentation suggests c,a,b,a,b… but that’s not what happens.
The orig_on_time is convenient for single-winded flipper coils where the full power is needed to flip but only minimal PWM power is needed to hold the flipper up. Similarly, a shaker might need full power to start the motor spinning and then only minimal PWM power to keep spinning.
The coil continues to patter until it is explicitly disabled.
You can activate a coil with PWM for a specific runtime with the PulsedPatter method.
public void PulsedPatter(ushort on_time, ushort off_time, ushort run_time)
For example: p3.Coils["coilName"].PulsedPatter(2, 8, 128)
The values of on_time, off_time must be between 0 and 127 inclusive, otherwise this throws ArgumentOutOfRangeException. The 127 maximum is less than the 255 maximum in the Patter method.
The value of run_time must be between 0 and 255 inclusive, otherwise this throws ArgumentOutOfRangeException.
This will repeatedly activate the coil for on_time milliseconds and deactivate the coil for off_time milliseconds. This ends after run_time milliseconds.
See the Schedule() method for the possibility to patter a coil for longer albeit with a coarser time resolution.
You can activate a coil according to a one second schedule with the Schedule method.
public void Schedule(uint schedule, int cycle_seconds)
public void Schedule(uint schedule, int cycle_seconds, bool now)
For example: p3.Coils["coilName"].Schedule(0x11F, 1, true)
The call Schedule(millis, secs) is equivalent to Schedule(millis, secs, true).
The schedule is a 32-bit mask. Each bit represents a 1/32 second timeslot with the least-significant bit coming first and the most-significant bit coming last. The coil is activated for the timeslot if the corresponding bit is 1, otherwise the coil is deactivated for that timeslot.
This can achieve a pulse longer than 255 milliseconds which is the maximum with Pulse(int).
If now is true, the schedule starts immediately. If now is false, the coil is deactivated until an internal one second boundary is reached, and then the schedule starts. Given multiple coils scheduled with now=false, they will all synchronize on the same boundary, though the synchronization might take up to a full second to get going.
The cycle_seconds argument is the integer number of seconds the schedule will run. Since the schedule lasts one second, this is how many times the schedule will run. The schedule runs forever if cycle_seconds is 0. To achieve schedules shorter than 1 second, set the most-significant bits to 0 and set cycle_seconds to 1.
To activate a coil for 128 milliseconds, then wait 64 milliseconds and activate for another 64 milliseconds, use the schedule 0b11001111. Binary literals are not available in the C# version used by the P3 SDK, so use hex instead. If the schedule does not repeat, you get: Schedule(0xCF, 1, true).
To achieve a pulse which is a multiple number of seconds use Schedule(0xffffffff, secs, true).
FuturePulse(milliseconds, futureTime) can be emulated with Schedule(schedule, 1, false) at a resolution of 1/32 of a second. To compute the schedule, divide milliseconds by 32, this is how many bits to set in the schedule. For example, if milliseconds is 130, we need to set 4 bits, which gives 0xf.
The Driver class exists in the netproc DLL. This DLL is a .NET wrapper over the C++ library libpinproc.
You can take a look at the libpinproc source code if you are interested in the low-level details.
The libpinproc header file contains some interesting documentation for the Driver functions, which I tried to summarize above. This file also defines the structure for the Driver state. This structure explains many of the range limits in the Driver methods.
typedef struct PRDriverState {
uint16_t driverNum;
uint8_t outputDriveTime;
bool_t polarity;
bool_t state;
bool_t waitForFirstTimeSlot;
uint32_t timeslots;
uint8_t patterOnTime;
uint8_t patterOffTime;
bool_t patterEnable;
bool_t futureEnable;
} PRDriverState;
pypinproc is a Python wrapper over libpinproc. It can be considered an ancestor cousin of netproc.
pypinproc is not directly relevant to P3 programming. I’m mentioning pypinproc here because its documentation also contains interesting information, which I tried to summarize above.
If you know pyprocgame or SkeletonGame, the Driver class in gameitems.py is the equivalent of the Driver class in netproc. Here are some differences between the two classes: