Lua scripts can be written / uploaded (see Remote Access for how to upload) to add new patterns to the ZC95. These scripts are able to switch each channel on/off, set the frequency and pulse width, and set the power level (scaled to what the front panel is set to - i.e. a Lua script can't set the output power to higher than set on the front panel). They can also receive notification of settings being changed via the menu, and inputs from the external trigger inputs along with the top left soft button being pressed. There is also limited support for audio - so far only reacting to volume, and no audio wave display when running over Wifi (and no audio support at all over serial).
There are a few example scripts in remote_access/lua, which demonstrate some of things that can be done from Lua. These scripts are:
-
fire.lua - If the soft button is pressed, all channel's are activated for as long as the button is held down. If any of the 4 external triggers (assuming stereo connectors are being used), the the corresponding channel (1-4) is activated for as long the trigger is active.
-
toggle.lua - switches between channel 1+2 & 3+4 at a speed that can be set via the menu. If the channels are switched on constant or just pulsed can also be set from the menu
-
waves.lua - a basic waves pattern. For each channel, this varies the frequency between 25Hz and 250Hz and the pulse width between 40us and 200us, with the time taken for each cycle being set per channel on the menu.
-
audio.lua - audio example assuming microphone input. Channels 1 & 2 are on constant at the default pulse width/frequency. Channels 3 & 4 react to volume changes
-
bluetooth_fire.lua - uses Left/Up/Down/Right buttons on a bluetooth remote to trigger chanel's 1 to 4 respectively for a configurable pulse duration
-
acc_test.lua - uses the accessory port to cycle between HIGH/LOW on the 3 output lines. See "AccIoWrite" section below for more details. Doesn't produce any estim output, so not really of any practical use, just a demo of how to use the accessory port.
The inbuilt Waves, Climb & Orgasm patterns are also written in Lua (contributed by someone who prefers to remain anonymous); the source for these can be found in source/zc95/LuaScripts/. Note that these are designed to closely resemble the patterns of the same name from another popular box, so might not be the best example to follow for new scripts.
It's probably easier to explain Lua as implemented in the ZC95 by going though one of the example scripts.
toggle.lua:
_delay_ms = 500
Config = {
name = "Toggle",
audio_processing_mode = "OFF",
menu_items = {
{
type = "MIN_MAX",
title = "Delay",
group = 0,
id = 1,
min = 100,
max = 2000,
increment_step = 100,
uom = "ms",
default = _delay_ms
},
{
type = "MULTI_CHOICE",
title = "Output",
group = 0,
id = 2,
choices = {
{choice_id = 1, description = "Pulse"},
{choice_id = 2, description = "Constant"}
}
}
}
}
_channel12_on = false
_wait_until_ms = 0
_pulse_mode = true
function MinMaxChange(menu_id, min_max_val)
if (menu_id == 1)
then
_delay_ms = min_max_val
end
end
function MultiChoiceChange(menu_id, choice_id)
if (menu_id == 2 and choice_id == 1)
then
_pulse_mode = true
end
if (menu_id == 2 and choice_id == 2)
then
_pulse_mode = false
end
end
function Loop(time_ms)
if (time_ms > _wait_until_ms)
then
ToggleChannel();
_wait_until_ms = time_ms + _delay_ms
end
end
function ToggleChannel()
if (_channel12_on == true)
then
zc.ChannelOff(1)
zc.ChannelOff(2)
if (_pulse_mode == true)
then
zc.ChannelPulseMs(3, 100)
zc.ChannelPulseMs(4, 100)
else
zc.ChannelOn(3)
zc.ChannelOn(4)
end
_channel12_on = false
else
if (_pulse_mode == true)
then
zc.ChannelPulseMs(1, 100)
zc.ChannelPulseMs(2, 100)
else
zc.ChannelOn(1)
zc.ChannelOn(2)
end
zc.ChannelOff(3)
zc.ChannelOff(4)
_channel12_on = true
end
end
The Config section is used to set the name and build the on screen menu:
Config = {
name = "Toggle",
audio_processing_mode = "OFF",
menu_items = {
{
type = "MIN_MAX",
title = "Delay",
id = 1,
group = 0,
min = 100,
max = 2000,
increment_step = 100,
uom = "ms",
default = _delay_ms
},
{
type = "MULTI_CHOICE",
title = "Output",
id = 2,
group = 0,
choices = {
{choice_id = 1, description = "Pulse"},
{choice_id = 2, description = "Constant"}
}
}
}
}
name = "Toggle" sets the name for the script - this is prefixed with U: then used on the patterns menu.
Optional fields:
group- group number; this only has any affect when ran remotely using the GUI, and allows related options to be grouped together, instead of appearing in one long list (useful for scripts with many options)audio_processing_mode- if the script can use audio, sets the mode. Currently eitherOFF(default) orAUDIO_INTENSITY. See audio section later.bluetooth_remote_passthrough- if present and set toTrue, keypresses from connected to bluetooth remotes are passed though to theBluetoothRemoteKeypress()function, instead of using the configured mappings from the config menu. SeeBluetoothRemoteKeypress()notes later for more detailsserial- see later "Serial I/O" section for options available in theserialblock. If supplied, allows access to the serial lines on the accessory port from the script.
A menu entry is displayed when the script is running for each item in menu_items; each must be given a unique id, numbered sequentially from 1.
There are four types supported:
MIN_MAX- shows a horizontal bar graph that can be changed between the set min/max using the adjust dial. The unit of measure (uom) text is displayed as suffix to the numeric value in the bar chartMULTI_CHOICE- used to show a menu option that allows for one of multiple settings to be picked. Each choice must have a unique id.AUDIO_VIEW_INTENSITY_STEREO- show a stereo waveform display; see audio section laterAUDIO_VIEW_INTENSITY_MONO- show a mono waveform display; see audio section later
function MinMaxChange(menu_id, min_max_val)
if (menu_id == 1)
then
_delay_ms = min_max_val
end
end
If present, the MinMaxChange(menu_id, min_max_val) function is called whenever a MIN_MAX menu type is changed, with the ID of the menu and the new value.
function MultiChoiceChange(menu_id, choice_id)
if (menu_id == 2 and choice_id == 1)
then
_pulse_mode = true
end
if (menu_id == 2 and choice_id == 2)
then
_pulse_mode = false
end
end
Similar to MinMaxChange but for MULTI_CHOICE menu types; if present, the MultiChoiceChange(menu_id, choice_id) function is called whenever a MULTI_CHOICE type menu entry is changed, with the menu ID and the choice ID now selected.
function Loop(time_ms)
if (time_ms > _wait_until_ms)
then
ToggleChannel();
_wait_until_ms = time_ms + _delay_ms
end
end
The Loop(time_ms) function is mandatory, and is called periodically for as long as the Lua script is running. The time_ms parameter is how long, in milliseconds, since the box was powered up.
function ToggleChannel()
if (_channel12_on == true)
then
zc.ChannelOff(1)
zc.ChannelOff(2)
if (_pulse_mode == true)
then
zc.ChannelPulseMs(3, 100)
zc.ChannelPulseMs(4, 100)
else
zc.ChannelOn(3)
zc.ChannelOn(4)
end
_channel12_on = false
else
if (_pulse_mode == true)
then
zc.ChannelPulseMs(1, 100)
zc.ChannelPulseMs(2, 100)
else
zc.ChannelOn(1)
zc.ChannelOn(2)
end
zc.ChannelOff(3)
zc.ChannelOff(4)
_channel12_on = true
end
end
The ToggleChannel() function (could have been named anything) is switching between the two pairs of channels. The relevant parts are the zc.* functions:
zc.ChannelPulseMs(channel, duration)- will pulse a channel on for the specified number of millisecondszc.ChannelOn(channel)- switches a channel on until switched offzc.ChannelOff(channel)- switches a channel off
See zc.* functions section below for more details along with all available zc.functions
Functions that can be called from Lua scripts to control the box. In addition to these, print("<whatever>") can be used from scripts (no zc. prefix); the output will appear in the serial output prefixed with '[LUA]', and in the debug window of the pattern_gui.py GUI if running remotely.
Params:
* channel - 1-4
* power - 0-1000
Sets the output power of channel from 0 to 1000. This output power is scaled based on what the front panel dial is set to for the channel. E.g if the front panel is set to 50% and a power level of 500 is set, the result will be a power level of 250 (25%).
If there is no Setup() function in the script, all channels will default to full power (i.e. only affected by front panel dials), so it often isn't necessary to use this. Most inbuilt patterns don't change the power level.
Params:
* channel number (1-4)
* frequency (1 - 300) Hz
Sets the output frequency of the specified channel. Currently defaults to 150Hz, but this default may move into the config menus at some point.
Params:
* channel number (1-4)
* positive pulse width (0-255) us
* negative pulse width (0-255) us
Sets the pulse width used for the channel, defaults to 150us, but this default may move into the config menus at some point. For symmetric pulses (as used by most/all inbuilt patterns), these two values should be the same.
Params:
* menu_id
* value
This can be used to allow a Lua script to change the setting of one of its menu options.
MenuId must match an id in the menu_items section of the Config block.
For MIN_MAX type menu entries, the value must be between the configured min and max for that menu entry.
For MULTI_CHOICE type menu entries, value must match one of the choice_ids.
Important: The change will result in the scripts MinMaxChange or MultiChoiceChange function being called asynchronously when the update takes effect. After calling SetMenuOption, it is possible (but not guaranteed) that the Loop function could be called many times before the menu is updated.
Params:
* channel number (1-4)
* duration (ms)
Switch the channel on for the specified number of milliseconds, using the previously set frequency, pulse width and power level (or the defaults, if not changed).
Params:
* channel number (1-4)
Switch on the specified channel, until ChannelOff is called, using the previously set frequency, pulse width and power level (or the defaults, if not changed).
Params:
* channel number (1-4)
Switch off the specified channel.
Params:
* enabled true/false
Enables triphase support - see Triphase section further down. Requires allow_triphase = true to be present in the config section, and allows the use of LinkChannels.
Params:
* lead channel 1-4
* linked channel 1-4, or 0 to unlink
* offset percentage (0-100)
EnableTriphase(true) must be called first.
Links linked channel to lead channel - causes a pulse on the linked channel to be generated to overlap with lead channel. An offset of 0% would generate the pulses simultaneously, where 100% would cause the pulses on the linked channel to be generated as the pulse on the lead finishes.
Once a channel is linked, the linked chanel should not be controlled directly other than changing its power setting - i.e. don't call ChannelOn, SetPulseWidth etc. for it - doing so will cause it to be unlinked.
See triphase section later for further details.
Params:
* Accessory I/O line number (1-3)
* State: true (high) or false (low)
Controls the 3 I/O lines on the accessory port - allows setting between high (3.3v) and low.
Note that the default state of these 3 lines from power on is HIGH, so bear that in mind when connecting anything. These lines can only source a few milliamps safely, so should only be used for signalling, i.e. it's probably best to connect a logic level MOSFET to switch anything more substantial than an LED.
Also worth noting that there is very limited protection on this port, something to be corrected in a possible future hardware revision, so be careful to avoid higher voltages. In particular, there is 12v on pin 7 (in hindsight a poor decision) - connecting this to pretty much any other pin would be bad.
Minimal example for using the accessory port to control 3 LEDs. Can be used with the "acc_test.lua" example LUA script to switch between each LED in turn.
Params:
* Accessory I/O line number (1-3)
Sets an I/O line on the accessory port to be an input, with a weak pull up. This is the default state of the lines from power on, and each is reset to be an input when a pattern is started; i.e. there should not normally be a need to call this method.
When the state of an I/O line set to input changes, the ExternalTrigger method in the Lua script (if present) is called - see description of ExternalTrigger for more details.
Params:
* String to write to serial on accessory port
See later "Serial I/O" section.
Params:
* Delay in milliseconds (0 - 10000)
Delay/sleep for the specified number of milliseconds. During the delay, other events (e.g. MinMaxChange, SoftButton) will still fire, so be sure the script is written to expect that.
These are functions that will be automatically called when applicable whilst the Lua script is running. With the exception of Loop(), all are optional.
Called when a MIN_MAX type pattern option is changed, and is called with the menu ID of the option, and the new value (which should lie between the min and max configured).
Called when a MULTI_CHOICE type pattern option is changed, and is called with the menu ID of the option, and the ID of the selected choice.
Called with pushed=True when the top left soft button is pressed, and then again when it is released with pushed=False. The soft button text is set by specifying soft_button = "<label>" in the Config = {} section. See fire.lua script for an example.
Called when an external trigger happens.
Socket: can be either "TRIGGER1", "TRIGGER2" or "ACCESSORY" for the Trigger1, Trigger2 or Accessory sockets respectively.
For the 3.5mm trigger sockets, part can be either "A" or "B". With a stereo 3.5mm TRS cable inserted, shorting Tip and Sleeve is part A (trigger LED lights up green). Shorting Tip and Ring is part B (trigger LED lights up red). When triggered, active will be True, when released it will be False.
This function will also be called for input on the accessory port. Shorting ACC_IO_1, 2 or 3 to ground will result in a part of A, B or C respectively:
| I/O line | Pin | Part |
|---|---|---|
| ACC_IO_1 | 9 | A |
| ACC_IO_2 | 4 | B |
| ACC_IO_3 | 8 | C |
| GND | 5 |
See bluetooth_fire.lua for an example of this.
If bluetooth_remote_passthrough = true is present in the config section, key presses from a connected bluetooth remote are passed through to this function, instead of using the mappings configured in the bluetooth menu. Key will be one of:
- "
KEY_BUTTON" - "
KEY_UP" - "
KEY_DOWN" - "
KEY_LEFT" - "
KEY_RIGHT" - "
KEY_SHUTTER" - "
KEY_UNKNOWN"
Note that unlike SoftButton and ExternalTrigger, there is only a single event, i.e. not pressed followed by released.
If bluetooth_remote_passthrough = false (or is absent), this function is never called, and key presses from a connected bluetooth remote are interpreted according to the configured mappings. E.g. if SHUTTER is mapped to "Top left soft", pressing the shutter button will cause SoftButton(true) immediately followed by SoftButton(false) to be called.
Allows the ZC95 to receive events from custom bluetooth devices. See bluetooth_hid.lua and the example BT project that can be paired with the ZC95 and use this functionality HidExample.
When paired to bluetooth HID device, this method will be called for each event received. If you value your sanity, I would suggest not attempting to write Lua scripts to support miscellaneous bluetooth devices unless you're particularly familiar with bt (I'm not) and exactly what the device in question is sending.
If serial enabled, is called whenever serial data is received. See "Serial I/O" section.
Receive audio data from aux socket.
See audio section later.
Called once when the pattern is started, before Loop(). It can be used to do any initial setup, including setting power level. If this function does not exist, the power level is defaulted to 1000, otherwise it is set to 0 and can be set to something more appropriate here.
Called periodically for as long as the pattern is running. time_ms is how long in milliseconds the box has been powered on, and since v1.7 is a floating point number with microsecond precision.
Loop() is called as often as possible by default, but how often will depend enormously on how much work is done in the Loop() function - expect an empty Loop() to be called around every 50us, and a complex loop() to be called closer to every 4000us (or more).
If a specific frequency is required (rather than just as often as possible), a loop_freq_hz = n option can be added to the Config = {} block, where n is between 1 and 400. Be aware that for particularly complex scripts, higher values are unlikely to work well - and note that this setting will only reduce how often Loop() is called when compared to the default.
Lua scripts can be made to react to audio intensity/volume changes when being ran locally, or over Wifi.
To use audio support:
- Configure the box for audio as per the usual audio operation notes, and confirm that the inbuilt
Audio intensitypattern is working as expected - Set
audio_processing_mode = "AUDIO_INTENSITY"in the scriptConfigsection - Optionally, add a
AUDIO_VIEW_INTENSITY_STEREOorAUDIO_VIEW_INTENSITY_MONOmenu item. Only useful if the script isn't being ran remotely - Implement
function AudioIntensityChange(left_chan, right_chan, virt_chan)in the script to receive audio data
With the microphone pre-amp enabled, the amplified signal will be present in the left_chan value. right_chan will have the same signal, but without the mic pre-amp so will be much weaker - likely unusablely so - therefore should be discarded. For microphone input, if a waveform display is desired a AUDIO_VIEW_INTENSITY_MONO menu item should be added.
With the microphone pre-amp disabled, the line level signal will be present in left_chan and right_chan for the left and right audio channels respectively.
If the signal is expected to be stereo, a stereo waveform display can be shown by adding a menu item with the type AUDIO_VIEW_INTENSITY_STEREO.
With audio_processing_mode = "AUDIO_INTENSITY" in the Config section of the script, it is expected that a AudioIntensityChange function will be present to receive audio volume/intensity changes.
The left_chan and right_chan parameters are for the left and right audio channels respectively. The virt_chan is an attempt a simulating a triphase effect on a separate logical channel, and probably isn't much use - I suggest ignoring it for now.
All values sent to these functions will be 0-255, so if used to modulate the output power of a channel, will need to be scaled to 0-1000.
The audio.lua script shows an example of audio support assuming microphone input. Channels 1 & 2 are on constant at the default pulse width/frequency, and channels 3 & 4 react to volume changes
The ZC95 now has limited triphase support from Lua.
As recap, "triphase" is where two channels are linked together with a common electrode. There is a good description of it on Joanne's Reviews. Due to the interactions between channels, it is more important to not make any connections above the waist with triphase enabled.
In normal operation, the ZC95 will never generate two pulses at same time - if the channels have been configured with frequencies that would cause two pulses to overlap, one is delayed. This is done as pulses overlapping in an uncontrolled/unintended manner can cause unexpected spikes if electrodes are in close proximately, and certainly if two have been joined together in a triphase configuration.
The triphase support is used to allow the ZC95 to intentionally generate pulses that overlap, but in a controlled manner with consistent results.
At present, it only allows one channel to be linked to another, where a pulse on the linked linked channel is generated with a set overlap with the lead channel (further options likely to follow in the future).
Before triphase can be used, the Config section must include allow_triphase = true - without this the triphase commands will be ignored. Setting this flag causes the pattern to be displayed prefixed with a (!) on the menu, as a warning that it uses triphase / disables channel isolation.
With that set, zc.EnableTriphase(true) can then be called - the best place is probably in the Setup function, but it can be anywhere. With triphase enabled, the ZC95 will no longer do anything to stop pulses being generated at the same time, regardless of where the pulses are coming from. So be careful - even unlinked channels will start generating pulses that can overlap if the frequencies set will cause them to clash. For the time being, it's probably best to stick to 2 channels when using triphase until it's expanded to be more configurable.
Finally, link two channel together using zc.LinkChannels, e.g. running zc.LinkChannels(1, 2, 0), will link channel 2 to channel 1 (lead channel), and cause a pulses to be generated on channel 2 at the same time as pulses on channel 1.
Changing the pulse width & frequency of channel 1 will affect channel 2. Once linked, only the power level of the linked channel should should be changed.
See trifade.lua for an example Lua script that enables triphase, then continually alters the offset between to the two channels at a speed set from the menu.
A graphic example of what's happening may help.
With channel 1 and 2 linked, using the default pulse width of 150, at three different offsets:
0% Offset - No offset; the pulses on the linked channels are generated at the same time (result of zc.LinkChannels(1, 2, 0))
50% Offset - the pulse on the linked channel starts half way through the pulse on the lead channel (result of zc.LinkChannels(1, 2, 50))
100% Offset - the pulse on the linked channel starts as the pulse on the lead channel finishes (result of zc.LinkChannels(1, 2, 100))
If enabled in the Config block, Lua scripts can interact with devices connected via serial on the Accessory port.
To enable, include a serial block in Config, e.g:
serial = {
enabled = true,
parity = "NONE",
stop_bits = 1,
baud = 9600,
line_mode = true
}
Fields:
-
enabled- enables serial access. Defaults to false. -
parity- valid options areODD,EVENorNONE(default) -
stop_bits- valid options are 1 or 2. Defaults to 1. -
baud- Only tested with 9600 and 115200, but all common rates between at least 150 and 115200 should work. Defaults to 115200. -
line_mode- If true, received data is buffered until CR is received, then passed as a full line to the scriptsSerialDatafunction. If false,SerialDatais called as data is received (can be 1 or a few characters at a time). When true, a CR is appended to any data sent usingzc.AccSerialWrite(). Defaults to true.Important: with
line_mode = true, any lines longer than 300 characters are discarded.
Where a field is omitted, the default is used.
When serial is enabled, it is expected that a 'SerialData` method will exist to receive serial data, e.g. to output received data:
function SerialData(data)
print("> " .. data)
end
To send serial data, use the zc.AccSerialWrite(<data>) method, e.g.: zc.AccSerialWrite("Hello world");.



