Lua for Profilab

"Lua ist die segensreichste Erfindung seit Einführung gemischter Jungs- und Mädchenschulen."
Ursprung bekannt

ProfiLua - a full, real-time Lua extension for ProfiLab.

What's new?

  • (04/2014) ProfiLua now comes with full support for serial interfaces.
    Use all your RS232, Bluetooth or other, virtual COM ports from within ProfiLua!

  • (12/2012) As of now, sources are also available via Github:

  • (04/2011) Now with MIDI support
    Let your MIDI audio equipment (mixer consoles, potentiometer racks, keyboards, etc...) control ProfiLua.

If you never heard about Lua, you really missed something...
At a glance, Lua looks like just another scripting language. At a glance!

Lua offers an incredible amount of features, it would be useless to even try to list them all.
Instead, here are a few examples of ProfiLua's implementation in Profilab and what it can do.

If you ever wrote a Profilab DLL or thought about doing so, you already might have run across these functions:

  uchar NumInputs();
  uchar NumOutputs();

  uchar CNumInputsEx(double *PUser);
  uchar CNumOutputsEx(double *PUser);

  void GetInputName(uchar Channel, uchar *Name);
  void GetOutputName(uchar Channel, uchar *Name);

  void CCalculate(double *PInput, double *POutput, double *PUser);
  void CCalculateEx(double *PInput, double *POutput, double *PUser, uchar **PStrings);

  void CSimStart(double *PInput, double *POutput, double *PUser);
  void CSimStop(double *PInput, double *POutput, double *PUser);

  void CConfigure(double *PUser);                                   

Inside Profilab, an element consists of a set of inputs (left) and outputs (right). While Profilab is running, the "CCalculate/Ex()" function determines the behaviour of the output channels. The rest of the functions specify the amount of in- or outputs, their names, initial values, etc...
As an example, consider an element that takes two input arguments (or channels) "Num1", "Num2" and calculates:

  • Num1 + Num2, sum
  • Num1 - Num2, difference
  • Num1 * Num2, product

The most simple C-implementation of the element above might look like this (disregarding the "CONSOLE" and "ERROR" in-/outputs):

  void NumInputs()
    return 2;

  void NumOutputs()
    return 3;

  void GetInputName(uchar Channel, uchar *Name)
      case 0: strcpy((uchar *)Name, "Num1"); break;
      case 1: strcpy((uchar *)Name, "Num2"); break;

  void GetOutputName(uchar Channel, uchar *Name)
      case 0: strcpy((uchar *)Name, "Add"); break;
      case 1: strcpy((uchar *)Name, "Sub"); break;
      case 2: strcpy((uchar *)Name, "Mul"); break;

  void CCalculate(double *PInput, double *POutput, double *PUser)
    POutput[0] = PInput[0] + PInput[1];  // add
    POutput[1] = PInput[0] - PInput[1];  // subtract
    POutput[2] = PInput[0] * PInput[1];  // multiply
The above code needs to be compiled (preferably with the great Dev-C++ (did I hear ad? ;-)) to a DLL, which can then be loaded by Profilab. If any changes are required, the code needs to be recompiled and copied over to your Profilab project directory (which requires that you terminate Profilab before the DLL can be replaced) until it can be used.

The same C-code above, written in ProfiLua:

  gNumInputs  = 2
  gNumOutputs = 3

  InputNames = {"Num1","Num2"}
  OutputNames = {"Add","Sub","Mul"}

  function gInputName(channel)
    return InputNames[channel]

  function gOutputName(channel)
    return OutputNames[channel]

  function gCalculate(In)
    return { In[1]+In[2], In[1]-In[2], In[1]*In[2] }


This code does not need to be compiled, it is just a plain text file inside your Profilab project directory. On start of the project, this text file is automatically compiled to Lua byte code and executed in the Lua "VM" (or byte code interpreter, if you prefer that).
If any changes are required, just do it. You can even edit the file while Profilab is running, but the code won't get recognized until you stopped and restarted the project.

Any change of the number of in- or outputs, as well as their names, require the DLL to be re-imported! (Double click the DLL block inside Profilab and select "IMPORT"). Any other code changes are transparent.

Although it is not necessary to know any of the Lua internals, at least regarding programming Lua, the Implementation of Lua 5.0 is a must read for people that like to know how it works (and don't have the time to browse the source code).

A good and first start in Lua programming is Roberto Ierusalimschys freely available book "Programming in Lua" and the v5.1 Reference Manual:

Programming in Lua
Lua 5.1 Reference Manual.

In case you need a good editor (highly recommended!), try Notepad++.

Simple Example

If you never programmed Lua before, you may wish to download this Windows executable. After starting it, the Lua interpreter welcomes you with:

  Lua 5.1.4  Copyright (C) 1994-2008, PUC-Rio
The interpreter is extremely useful for learning or testing new programs. Although ProfiLua includes a debug-console, it requires that a file can be loaded, which implies that its syntax is free of errors. Otherwise, nothing will be loaded and an error pin at the ProfiLua-block will get set...

*** Errors can crash Profilab! *** Test your code! *** Save project before execution! ***

Note: ProfiLua contains some extensions (e.g. a binary number representation), global variables and functions that are not available in "Standard" Lua. Scripts containing these, can not be run in the standard Lua interpreter.

The simple example on the right can be downloaded here.
The ProfiLua DLL block has five inputs:

  • Num1
  • Num2
  • $Str1
  • $Str2
and six outputs:
  • Add
  • Sub
  • Mul
  • Dummy
  • $StrOut

The first input "CONSOLE" and output "ERROR" are automatically added to every ProfiLua DLL-block. A high level at the "CONSOLE" pin (> 2.5V) will activate a debug window. It may be used for debugging purpose (printing out values, halting Profilab by pressing PAUSE-key, ...) and input action. All three STDIO pipes (stdin, -out and -err) are redirected to that console.
The "ERROR" output pin is set to high (5.0V) if anything went wrong.

By default, in- or outputs are numerical (double precision). If the first digit in the name is a dollar sign "$", this in- or output becomes a "string" pin.

If you start this Profilab project, it does nothing else but:

  • Add = Num1 + Num2
  • Sub = Num1 - Num2
  • Mul = Num1 * Num2
  • $StrOut = $Str1 + "<-MID->" + $Str2
  • printing some text on the debug console

Number of In- and Outputs

Let us take a look at the Lua code, step by step:

  gNumInputs  = 4
  gNumOutputs = 5
These two global variables determine the number of user in- and outputs. "User", because "CONSOLE" and "ERROR" pins will automatically be added by ProfiLua.
"gNumInputs" and "gNumOutputs" need to be global, not hidden in a function. They will be read out by ProfiLua.

Names of In- and Outputs

  InputNames  = {"Num1","Num2","$Str1","$Str2"}
  OutputNames = {"Add", "Sub", "Mul",  "Dummy", "$StrOut"}
These two Lua tables (a Lua table is comparable to an C-array [but can do a lot more]) define the names of the in- and outputs. The number of arguments in "InputNames" and "OutputNames" must match the value specified in "gNumInputs" and "gNumOutputs".
For string in- and output, the name of the variable needs to be preceded by an "$" (dollar sign).

NOTE #1: Two string in- and outputs must not be in the same row:

        -|Num1      OA|-
        -|$Str1     OB|-   <- OK
        -|$Str2    $OC|-   <- ERROR, two strings in a row
        -|Num2     $OD|-   <- OK
"Row" refers to the consecutively numbered order of in- or outputs:

      1 -|$Str        |
      2 -|Num1        |
      3 -|Num2    $Out|- 1 <- ERROR, two strings in row 1
      4 -|Num3      O2|- 2 <- OK
      5 -|Num4        |
Sometimes, this restriction may require "dummy" pins, or at least a clever pin layout:

        -|$In1   ODum1|-
        -|$In2   ODum2|-
        -|$In3   ODum3|-
        -|IDum1  $out1|-
        -|IDum2  $out2|-
        -|IDum3  $out3|-
Remember this! If you have two string pins in one row, the output pin will automatically be set to the value of the input pin. No matter how hard you code ;-)

Note #2: Due to the function arguments available inside Profilab, strings including "0s" (zeros) can not be passed over to ProfiLua! The string will end at the first "0". The other way, passing strings (including zeros) over from ProfiLua to Profilab, is possible (will be explained somewhere below...).

Numerical pins have no restrictions, except you can have a maximum of 100 in- and 100 output-pins.

  function gInputName(channel)
    return InputNames[channel]

  function gOutputName(channel)
    return OutputNames[channel]
Usually, these two functions do not need to be changed, but their presence is mandatory. These functions are called by ProfiLua to determine the names of the in- and outputs. These are equivalent to the Profilab-DLL functions "GetInputName()" and "GetOutputName()".

For every existing user input pin (excluding "CONSOLE" and "ERROR"), this function is called with the number of the pin, starting at "1" (ONE! Not zero!). The function then needs to return a string, representing the name of this pin (printable ASCII characters only).

Note: All indexed Lua tables inside ProfiLua begin with index "1", not "0".

LuaNote: Tables are associative arrays, which can be indexed by any value (except nil)). Example:

  > a={}
  > a[0]=321
  > =a
  > a['666.666']=123
  > =a['666.666']
Impressive, isn't it ? ;-)
Note: "=" at the start of a line in the interpreter is a short form of print(...). "=a" means "print(a)".

Example indexing for "InputNames" table:

  > InputNames  = {"Num1","Num2","$Str1","$Str2"}
  > =InputNames[1]
  > =InputNames[4]
  > =InputNames[20]


  function gCalculate(In)
    print("DLL#"..tostring(gPUser[101]), In[1], In[2], In[3], In[4])
    return {In[1]+In[2], In[1]-In[2], In[1]*In[2], 666, In[3].."<-MID->"..In[4]}
While a Profilab project is running, it continually calls the function "gCalculate()", with *all* input arguments in one table, numbers and strings mixed. After calculation, or whatever else, the function must return a table "{...}". The number of arguments must match the "gNumOutputs" variable.

Note: In the example above, one argument of "print()" references a "gPUser[]" table. This corresponds to the C-DLL array "PUser[0..100]", but has a "+1" index "gPUser[1..101]". The last index [101] contains the number of the DLL. Consult your Profilab manual for more information (help, search "DLL").

For experts: ProfiLua does not provide a call to "CConfigure()" (yet).

If you halt this function (e.g. by pressing the PAUSE-key in the debug console window), or create an endless loop, Profilab will halt too until you return from "gCalculate()".

Each iteration of Profilab requires the function "gCalculate()" to return its new values immediately.


Supplemental Functions and Variables

Lua Functions needed by ProfiLua

In addition to the above mentioned functions:

  function gInputName(Channel)
  function gOutputName(Channel)
  function gCalculate(In)
a first initialization might be done with:

  function gSimStart(In)
    return {0, 0, 0, 666, "DUMMY"}
The function "gSimStart()" is called once and directly after a Profilab project was started. It allows you to initialize the outputs or other, ProfiLua internal variables.
Note: The input table "In" does ONLY contain numerical arguments! All string contents are replaced by a "0" (zero, a number).

The number of arguments in the return table needs to match the number of outputs specified by "gNumOutputs", but strings can not be initialized. You may return any string, including zero length or a number. The arguments at a string index will (and can not) not be evaluated by ProfiLua.

Even if you don't need an initialization, the function "gSimStart()" has to be present AND needs to return the proper amount of arguments!.

On termination of the Profilab project, the function "gSimStop()" is called:

  function gSimStop(In)
If you need to do any cleanup, e.g. close an open file and write pending data, this is your last chance to do so.
Note: This function does *NOT* have any return arguments (they would go nowhere).

Lua Functions provided by ProfiLua

Inside the ProfiLua environment, all ProfiLua functions are available through the libraries:    -- general Lua functions    -- library for bitwise operations   -- library for MIDI equipment control -- library for serial interfaces
where "xyz" names the functions, which should be called.
Module "prolua"
Available functions in "prolua" (in addition to the complete Lua system):

  nothing, right now...
Module "probit"
Available bit manipulating functions in "probit":

  y = probit.tohex(x [,n])     -- returns hexadecimal   STRING with length of <n> digits (default: 2)
  y = probit.tobin(x [,n])     -- returns binary number STRING with length of <n> digits (default: 8)
  y = probit.bnot(x)           -- returns the bitwise NOT of <x> (beware of 2's complements ;-)
  y = probit.bor(x1 [,x2...])  -- returns bitwise OR  of all given arguments
  y = [,x2...]) -- returns bitwise AND of all given arguments
  y = probit.bxor(x1 [,x2...]) -- returns bitwise XOR of all given arguments
  y = probit.lshift(x, n)      -- returns <x> shifted left  by <n> digits
  y = probit.rshift(x, n)      -- returns <x> shifted right by <n> digits
  y = probit.arshift(x, n)     -- returns <x> arithmetically shifted right by <n> digits
  y = probit.rol(x, n)         -- returns <x> rotated left  [0..31] by <n> digits
  y = probit.ror(x, n)         -- returns <x> rotated right [0..31] by <n> digits
  y = probit.bswap(x)          -- returns <x> with bytes swapped (0x12345678 -> 0x78563412)
ProfiLua "probit" contains code from "BitOp", by Michael Pall.
Module "promidi"
Available functions for the MIDI implementation "promidi" in ProfiLua.

  n  = CountDevices(dir)   -- returns number of attached devices; <dir>=MIDIIN or <dir>=MIDIOUT
  r  = OpenMidiOut(num)    -- opens  MIDIOUT device number <num>; 0 = OK; <0 = ERROR
  r  = CloseMidiOut(num)   -- closes MIDIOUT device number <num>; 0 = OK; <0 = ERROR
  r  = OpenMidiIn(num)     -- opens  MIDIIN  device number <num>; 0 = OK; <0 = ERROR
  r  = CloseMidiIn(num)    -- closes MIDIIN  device number <num>; 0 = OK; <0 = ERROR
  "" = GetMidiOutName(num) -- returns device name string of device <num>, or nil if an error occured
  "" = GetMidiInName(num)  -- returns device name string of device <num>, or nil if an error occured
  r  = SendMessage(num,    -- sends out a MIDI message: <num> = number of device
               channel,         <channel> = MIDI channel 1..16
                   key,         <key>     = key to send
                   vel,         <vel>     = velocity
                   MSG)         <MSG>     = one of NOTEON, NOTEOFF or CTRL
                              returns 0 if OK, <0 if an error occured
  {} = GetMessage(num)     -- returns a table with a MIDI message:
                              <num> = number of device
                              returns table {event, channel, data1, data2} or nil, if an error occured:
                              <event>   = one of NOTEON, NOTEOFF, CTRL, PITCH
                              <channel> = MIDI channel
                              <data1>   = MIDI data1
                              <data2>   = MIDI data2
For "promidi" defined variables, see "GLOBAL Lua Variables" below.
For some MIDI examples, scroll down even more...
Module "proserial"
Available functions in "proserial":

  h = Mount()                -- creates a serial object; 0 = ERROR; >0 = handle of the object
  r = UnMount(handle)        -- delets the serial object "handle"
  r = CheckPort(handle,port) -- checks if COM port number "port" is available (on "handle")
  r = Open(handle, port)     -- opens COM port number "port" on handle "handle"
  r = Close(handle)          -- closes the opened COM port on handle "handle"
  r = Send(handle, data)     -- sends a number, a string or a table over the port reference by handle "handle"
  n = BufferCount(handle)    -- counts the number of bytes in the receive buffer of "handle"
  r = Config(handle,         -- configures the COM port referenced by "handle":
             baud,           -- baud rate (300..3000000 bits/s)
             bits,           -- number of bits (5, 6, 7, 8)
             parity,         -- parity (0, 1, 2 -> NONE, ODD, EVEN)
             stop)           -- number of stop bits (1, 2)
  n = ReadByte(handle)       -- read a byte from the reiceive buffer ( <0 = EMPTY; >=0 = byte)
  r = BufferFlush(handle)    -- empty the receive buffer

Internally, ProfiLua also supports sending or receiving DLE sequenced packets, but this is not yet completely built in and will be released with a later version.

Lua Variables needed by ProfiLua

These variables need to globally available (not hidden in a function) in your ProfiLua script:

  gNumInputs    -- number of inputs
  gNumOutputs   -- number of outputs

GLOBAL Lua Variables provided by ProfiLua

Inside ProfiLua, the following global variables exist (notice that this might not be complete due to newer versions):

  PROFILUAVERSION   -- returns a string "vHHH.LLL", e.g.: "v0.9b"
  gPUser[1..101]    -- contains PUser[0..100] C-array from Profilab DLL
  gHIGH   = 5.0     -- logic high state
  gLOW    = 0       -- logic low state
  gISHIGH = 2.5     -- logic high threshold (HIGH >= gISHIGH)

  MIDIIN  = 0       -- MIDIIN  (used for function calls)
  MIDIOUT = 1       -- MIDIOUT (used for function calls)
  NOTEON  = 0x90    -- MIDI NOTEON event
  NOTEOFF = 0x80    -- MIDI NOTEOFF event
  CTRL    = 0xB0    -- MIDI CONTROL CHANGE event
  PITCH   = 0xE0    -- MIDI PITCH BEND event

  PARITY_NONE = 0   -- for parity settings in proserial.Config()
  PARITY_ODD  = 1   -- for parity settings in proserial.Config()
  PARITY_EVEN = 2   -- for parity settings in proserial.Config()
PROFILUAVERSION returns a string containing the ProfiLua version.

"gPUser[]" corresponds to the C-array "PUser[]". On exit, any values stored in this table are written to the Profilab project and will be available the next time you start it.
"gPUser[101]" contains the number of the Profilab-DLL, this instance of ProfiLua was invoked with.

Note: Global variables do NOT need the "" syntax.

Enhancements to the Lua Language

Bit Operations

see above ^^^

Binary Numbers

Aside from the standard Lua number representation, which allows decimal (base of 10) and hexadecimal (base of 16; "0x") notation, ProfiLua supports binary numbers (base of 2, "0b").
The preceding digits "0b" or "0B" indicate a binary notation:

  0b1111     -- represents 15 (decimal)
  0b11111110 -- represents 254 (decimal)
Note #1:
The length of a number in a binary number format can not exceed 32 bits. Any larger value will be truncated ( = & 0xffff).

Note #2:
A two's complement does not exist for binary number format:

  0b11111111111111111111111111111111 -- 32 times a '1': 4294967295 dec, and not -1 dec.

Note #3:
A negative value entered (e.g.: "-0b0111") IS a negative value (-7dec, in this case).

MIDI Interface

MIDI for Profilab?
What an idiotic idea! And btw., Profilab already has MIDI blocks built in!

That's true. But did you ever try to filter out a range of CONTROL CHANGE or NOTEON/OFF messages, applying some logic to the values received while three or more MIDI devices are sending messages?
Although this is possible in Profilab, it requires tons of blocks and things are slowing down...

Well, this is where ProfiLua's MIDI implementation comes in...

Obviously, controlling Profilab from a MIDI master keyboard would not make much sense (although it is possible). ProfiLua's MIDI implementation clearly targets DAW-Controllers.

These devices contain many potentiometers, sliders, buttons and more...
And if both hands are already in use, what about a foot controller? ;-)

But there's even more:
All professional audio effect racks, filter banks or matrix signal switchers have a MIDI interface. ProfiLua's MIDI implementation can simplify automated audio measurements a lot (At last, that's what I think ;)

Although this example was done with an Akai MPK49 keyboard (Akai names it a "Performance Controller") and the resolution is not that brilliant, you'll get the clue:

Of course, the same can be done with Profilab's built in MIDI blocks. In case you just need a handful of NOTE or CONTROL CHANGE events, the ready to use blocks are probably a better choice, but ProfiLua's MIDI implementation is much more flexible if things get too complicated for "blocks".

A sample project and source code is available (somewhere) below...

Serial Interfaces

The support for serial ports in Profilab is quite limited.
This is especially true for the blocks:
  • send byte (Byte senden)
  • receive byte (Byte empfangen)
  • all other :-D

Did you ever try to assemble a sequence of bytes on the fly, send them to a device and decode a varying answer? Especially when the clocking mechanism prohibits a fluent operation?

Even if you managed to "block-code" your special implementation, didn't Profilab require dozens and more blocks just for a literally easy operation?
And even the smallest changes to that "code" usually result in a maximally invasive operation.

Profilab can send fixed numbers or strings and handle fixed answers, but as soon as it gets more complicated, you're lost completely...
Until now ;-)

ProfiLua's serial module can candle all of this.

It supports an unlimited amount of serial ports, knows all baud rates (110..3000000 bits/s), is easy to use and can handle the most complicated stuff in the background (*1*).

You'll probably surprised, seeing 3..5 lines of Lua code (assuming you know how to write them, though) replacing 20..30 blocks...


Note to brain:
Provide more examples, here 8)

(*1*) Only one thing is missing yet: hardware handshake

Step by Step Examples

The following examples are not particularly useful or may not even require a DLL, but they can provide a slightly deeper insight into ProfiLua and its handling inside Profilab.

For simplicity, none of the projects contain "metatables", "coroutines", "anonymous functions", ... or other fancy Lua stuff.

I don't want to scare off potential Lua beginners ;-)

Simple Trigger Block with Hysteresis


A trigger that sets an output pin to high, after the upper threshold is exceeded. But only, if the signal previously undershot the low threshold. The pin gets set to low if the signal undershoots the lower level again.
Both levels should be configurable via two input pins.

The image on the right shows the basic test environment without any DLL. Here follows a minimal step by step instruction on how to implement ProfiLua in this project.

  • copy the "ProfiLua_Vxy.dll" into your project directory
  • inside Profilab, insert a DLL-IMPORT block and remember its number (e.g.: "DLL1")
  • open a real text editor

Now, on to the Lua script.
The file name is always "n.lua", where "n" is the number of the DLL you just inserted:
  • DLL1 -> 1.lua
  • DLL2 -> 2.lua
  • ...

The new block requires three inputs (I) and one output (O):

  • I - signal in
  • I - trigger low
  • I - trigger high...

  • O - trigger output

Add these lines to your new file:

  gNumInputs  = 3
  gNumOutputs = 1

  InputNames  = {"SigIn","TrigH","TrigL"}
  OutputNames = {"TrigO"}

  function gInputName(channel)
    return InputNames[channel]

  function gOutputName(channel)
    return OutputNames[channel]

  function gSimStart(In)
    return {0}

  function gSimStop(In)

  function gCalculate(In)
    return {0}
  • double click the DLL block in Profilab
  • select "IMPORT DLL", choose "ProfiLua_Vxy.dll"
After doing so, the DLL block looks like this ------>

Although there is nothing useful in the "gCalculate()" function, except a table that returns {0}, the DLL can be wired and the project is ready for action.
If you start it, the trigger output will always be low ("0").

Try connecting the "CONSOLE" input to high (or fixed 5V value) and add a "print()" instruction to the function "gCalculate()", right before the line containing "return {0}".

You can even do this while Profilab is running:

  function gCalculate(In)
    return {0}

Restarting the Profilab project (STOP - RUN) immediately makes the changes visible. Now, the debug console contains the values of all three input channels.

For a nicer print, you may wish to try:

  function gCalculate(In)
    print(string.format("S:%3.1f; H:%2d; L:%2d",In[1],In[2],In[3]))
    return {0}

Or with "io.write()", instead of print:

  function gCalculate(In)
    io.write(string.format("Sig: %3.1f;   TH: %2d;   TL: %2d\n",In[1], In[2], In[3]))
    return {0}
Back to the mission, our trigger pin:

  tState = 0
  pState = gLOW;

  function gCalculate(In)
    io.write(string.format("Sig: %3.1f;   TH: %2d;   TL: %2d\n",In[1], In[2], In[3]))

    if (In[1] > In[2]) and (tState == 0) then
      pState = gHIGH
      tState = 1
    elseif (In[1] < In[3]) and (tState == 1) then
      pState = gLOW
      tState = 0

    return {pState}
Because all local variables inside the "gCalculate()" function won't keep their content until the next invocation of the function, we need two "markers", the global vars "tState" (a trigger state) and "pState", a pin state.

If the trigger state is "0", we just need to wait until "In[1] > In[2]". This changes both, the output pin as well as the trigger state, to high.
The reverse operation, clearing the pin, will be executed if the input signal "In[1]" is lower than the lower trigger threshold "In[3]", while the trigger state is set to "1".

You can download example#2 here.

Simple Number to BIN or HEX conversion (string)

An easy example on how to convert a number to a hexadecimal or binary string, with only one line of ProfiLua code. At least, if we only count "gCalculate()" ;-)

Is there anything easier than this?

You can download example#3 here.

  gNumInputs  = 1
  gNumOutputs = 2

  InputNames  = {"Num"}
  OutputNames = {"$BIN","$HEX"}

  function gInputName(channel)
    return InputNames[channel]

  function gOutputName(channel)
    return OutputNames[channel]

  function gSimStart(In)
    return {"",""}

  function gSimStop(In)

  function gCalculate(In)
    return {"0b"..probit.tobin(In[1]),"0x"..probit.tohex(In[1])}

Probability Density Function (PDF)

Some math? Ok, here's a minimal ProfiLua PDF...

You can download example#4 here.

  gNumInputs  = 3
  gNumOutputs = 1

  InputNames  = {"x","sigma","my"}
  OutputNames = {"PDF"}

  function gInputName(channel)
    return InputNames[channel]

  function gOutputName(channel)
    return OutputNames[channel]

  function gSimStart(In)
    return {0}

  function gSimStop(In)

  function gauss(x,s,m)
    if s < 0.01 then
      return 0
    return (1/(s*math.sqrt(2.0*math.pi)))*math.exp(-0.5*math.pow((x-m)/s,2))

  function gCalculate(In)
    return {gauss(In[1],In[2],In[3])*100}

Including multiple ProfiLua DLLs

Read this carefully!

If you include a ProfiLua DLL only once, you only have to care about ProfiLab's DLL number. (Note: If you delete a DLL inside Profilab, all DLLs are renumbered! Keep this in mind.)

What about including multiple instances of "ProfiLua.dll"?

Due to the internal DLL handling inside Profilab and ProfiLua's internal implementation of Lua, you can NOT LOAD THE SAME DLL MULTIPLE TIMES, but there's a simple workaround available:

The distribution includes several DLLs:

Their code is absolutely identical, but the internal names of the DLLs are different ("ProfiLuaA", "..B", ...). This way, Profilab uses different memory chunks for each of the DLL, and they are completely isolated.

I do not recommend loading more than three instances of ProfiLua. Even if separating different functions might please your eyes, a single instance of ProfiLua can do all the work.

Nevertheless, if you are going to include more than one ProfiLua DLL, do it like this --------->

You can download example#5 here.

Handling Zeros in Strings

(aka.: Passing over Lua tables to Profilab strings)

At a glance, the example on the right might look complicated, but this is the only way of passing more than one value at once. Profilab only allows "numbers" or "strings".
Note: A C-string inside Profilab is of type "char[]" (or "*char", if you like pointers). Its values can not exceed 0-255!

A special problem arises if your Lua table (or string) contains zeros. These need some proper handling.

Inside ProfiLua, we just convert a "0" (zero) and a "16" (sixteen) to

  • 0x00 -> 0x10 0xFF
  • 0x10 -> 0x10 0x10
After passing the new, enhanced string to Profilab, we "only" need to inverse this, back to the original state.

You can download example#6 here.

Little Clock State Machine

Inside ProfiLua, you can NOT toggle a Profilab variable or pin, e.g. by doing:

  for i in 1,10 do
    pOut = 1
    pOut = 0
For each Profilab iteration, it is required that "gCalculate()" returns the current state of the pin. Because local variables are lost after "gCalculate()" quit, you'll need to save the current state, possibly by storing the value in a global variable.

Just a stupid example:

  pOut = gLOW  -- initialized as Profilab loads the ProfiLua script

  function gCalculate(IPins)
    -- ...
    -- calculate whatever you like
    -- ...

      if pOut == gLOW then
        pOut = gHIGH
        pOut = gLOW
    return {pOut}

You can download example#7 here.

Saving gPUser values on exit

As of version v0.9b, ProfiLua supports saving the gPUser table back to Profilab's PUser array.
All values stored in there, will be available the next time you start your project.

It does not matter, where or when those values change, e.g.:

  function gSimStop(In)
    gPUser[1] = In[3]
    return {0}
  function gSimStop(In)
    gPUser[2] = 666

You can download example#8 here.

Real World Example

Although you can not execute this, because it requires this piece of hardware, it might be worth to take a look at the ProfiLua script.

This functionality could not have been implemented with plain Profilab blocks...

You can download example#100 here.

MIDI Basics

To query the number of attached MIDI IN or OUT devices, just use:

  in  = promidi.CountDevices(MIDIIN)
  out = promidi.CountDevices(MIDIOUT)
All available MIDI IN or OUT devices are separately and consecutively numbered, starting from 0. If the return value of CountDevices() returns, e.g. "3" inputs, they will be available as device number "0", "1" and "2".

Note that all in- and outputs are stored in separate lists!
An input "1" might address a different device than output "1".

To query the name of a device, type:

  iname = promidi.GetMidiInName(number)
  oname = promidi.GetMidiOutName(number)
This will return a Lua string, containing the name of the device, e.g. "UltraLite mk3 Hybrid" or "Microsoft GS Wavetable SW Synth", or nil, if the device number is invalid.

The following code prints out a list of all available output devices to the debug console (if enabled):

  for i=0, promidi.CountDevices(MIDIOUT)-1 do
Note the "-1" in the for-loop!
In Lua, a for-loop like "for i = 0,0 do..." indeed runs one iteration!

To open or close a device, execute:

  retcode = promidi.OpenMidiIn(number)
  retcode = promidi.OpenMidiOut(number)

  retcode = promidi.CloseMidiIn(number)
  retcode = promidi.CloseMidiOut(number)
If <retcode> is "0", everything went right. In case it is <0, an error occurred.

To simplify opening a specific device, a name-lookup function might be helpful:

  function MidiFindFirstByName(dir, name)
    local i
    if dir == MIDIIN then
      for i=0, promidi.CountDevices(MIDIIN)-1 do
        if string.match(promidi.GetMidiInName(i),name) ~= nil then
          return i

    elseif dir == MIDIOUT then
      for i=0, promidi.CountDevices(MIDIOUT)-1 do
        if string.match(promidi.GetMidiOutName(i),name) ~= nil then  
          return i

    return -1
The above piece of code will return the corresponding device number of a device-name that contains the string given by <name>, or "-1" if there is no such device.
<dir> specifies if MIDI IN or OUT devices should be scanned.

It is indeed a good practice to open all devices inside gSimStart() and close them in gSimStop() respectively:


  devIN  = -1
  devOUT = -1
  -- gSimStart
  function gSimStart(In)

    if promidi.OpenMidiIn(3) == 0 then
      devIN = 3

    if promidi.OpenMidiOut(2) == 0 then
      devOUT = 2

    return {}

  -- gSimStop
  function gSimStop(In)
    if devIN >= 0 then

    if devOUT >= 0  then

And just in case you are curious, it indeed is possible to open more than one in- or output:

  devIN1  = -1
  devIN2  = -1
  devIN3  = -1
  -- gSimStart
  function gSimStart(In)

    if promidi.OpenMidiIn(0) == 0 then devIN1 = 0 end
    if promidi.OpenMidiIn(1) == 0 then devIN2 = 1 end
    if promidi.OpenMidiIn(2) == 0 then devIN3 = 2 end

    return {}

  -- gCalculate
  function gCalculate(In)
    local ev1,ev2,ev3
    ev1 = promidi.GetMessage(devIN1)
    ev2 = promidi.GetMessage(devIN2)
    ev3 = promidi.GetMessage(devIN3)


    return {}


After a device has been opened, the following functions exist for sending or receiving MIDI messages or events:

  -- sending messages to OUT devices:
  retcode = promidi.SendMessage(number, channel, key, velocity, EVENT)  

  -- reading messages from IN devices:
  message = promidi.GetMessage(number)
While SendMessage() returns the usual "0"-means-good and "<0"-is-bad code, GetMessage() returns either:
  • a Lua table with four entries {event, channel, data1, data2}
  • nil, if there's no event available (or other, unusual stuff happened...)
For an overview of MIDI messages, take a look here.

ProfiLua supports:

  • 0x90 NOTEON
  • 0x80 NOTEOFF
  • 0xB0 CONTROL CHANGE (can do almost everything)
  • 0XE0 PITCH BEND (only useful as input)
Without going too much in detail, here are some examples:

Play a note (middle C [60dec]) on device 1, MIDI channel 3 with a velocity of 100:

  promidi.SendMessage(1, 3, 60, 100, NOTEON)

Light up a green, a red and a yellow LED on the Novation Launchpad, assuming it is available as output device number 2 (Launchpad uses MIDI channel 1):

  promidi.SendMessage(2, 1, 0, 0b00111100 , NOTEON) -- green upper left  
  promidi.SendMessage(2, 1, 1, 0b00001111 , NOTEON) -- red
  promidi.SendMessage(2, 1, 2, 0b00111111 , NOTEON) -- yellow
At this point, you probably should start reading the Launchpad Programming Reference Guide.

Read Akai MPK49 (device number stored in devIN) events and determine "K1-3" potentiometer values:

  function gCalculate(In)
    local ev
    ev = promidi.GetMessage(devIN)
    if ev ~= nil then
      if ev[1] == CTRL and ev[2] == 1 then
        if ev[3] == 0x16 then print("K1:",ev[4]) end
        if ev[3] == 0x17 then print("K2:",ev[4]) end  
        if ev[3] == 0x18 then print("K3:",ev[4]) end
    return {}
Notice that GetMessage() returns all events of the addresses device, but you can easily filter them out like above...

The only drawback of MIDI equipment, at least if it's used as an input device is, that the incoming values only have a range of 0..127.

What is to do if you have unknown equipment or no manual?
These four lines of ProfiLua code prints all events from a device with number <devIN>:

  -- assuming:
  --   - MIDI device number stored in <devIN>
  --   - MIDI device already opened (preferably in gSimStart())    		
  --   - no outputs (gCalculate returns an empty table)

  function gCalculate(In)
    local ev

    ev = promidi.GetMessage(devIN)
    if ev ~= nil then
      print(ev[1], ev[2], ev[3], ev[4])
    return {}

If you need to do more complex analyses or probing of MIDI messages, there's no way past MIDI-OX ==>

MIDI: Akai Slider Example

This example does nothing else, but query the slider (F1-4) and potentiometer (K1-4) values of an Akai MPK49 keyboard. The code, reduced to the gCalculate() function, only requires a few lines:

  AkaiStates = {0,0,0,0,0,0,0,0}
  -- gCalculate
  function gCalculate(In)
    local ev
    ev = promidi.GetMessage(devIN)
    if ev ~= nil then
      if ev[1] == CTRL and ev[2] == 1 then
        if ev[3] == 0x16 then AkaiStates[1] = ev[4] end
        if ev[3] == 0x17 then AkaiStates[2] = ev[4] end
        if ev[3] == 0x18 then AkaiStates[3] = ev[4] end
        if ev[3] == 0x19 then AkaiStates[4] = ev[4] end
        if ev[3] == 0x0c then AkaiStates[5] = ev[4] end
        if ev[3] == 0x0d then AkaiStates[6] = ev[4] end
        if ev[3] == 0x0e then AkaiStates[7] = ev[4] end
        if ev[3] == 0x0f then AkaiStates[8] = ev[4] end

    -- Notice that "AkaiStates" already is a table, hence it  
    -- does not require {} around it...
    return AkaiStates

You can download example#90 here.

Formatted Output to File

Just a piece of simple code that creates a nice looking output in a file.

  754 b387 9735.0 0b00000000000000000101010110100101
  805 16a2 9735.0 0b00000000000000000101010110101011
  805 16a2 2824.7 0b00000000000000000101010110101011
   27 054a 6811.5 0b00000000000000000000111010111101
  499 b7a4 6811.5 0b00000000000000000000101110011010
  499 b7a4  971.6 0b00000000000000000000101110011010
  202 2e4d  971.6 0b00000000000000001010110001000010
  202 2e4d 7988.9 0b00000000000000001010110001000010
  353 c106 4902.3 0b00000000000000001110101110000101
  424 0a6e 4902.3 0b00000000000000001101110000101100  

The code in gCalculate():

  written = 0
  -- gCalculate
  function gCalculate(In)
    local i
    if fout ~= nil and In[5] >= gISHIGH then
      written = written + 1
    if fout == nil then
      return {gLOW, written}
      return {gHIGH, written}

You can download example#91 here.

Serial Basics

Some basics about the usage of the serial interface.

Creating a Handle

Before you can do anything else, you need to call

  portHandle = proserial.Mount()
to create a reference to the serial object.
A "handle" is nothing else but a number, which can be used to distinguish several serial interfaces.
ProfiLua supports an unlimited amount of interfaces.

The returned value "portHandle" is

   0, if an error occured
  >0, if the handle is valid

Every other function needs this "handle".

Checking Ports

To check if a port is availabe and free for use, call:

  isAvailable = proserial.CheckPort( handle, port )  
Here, "handle" is the serial reference value obtained by proserial.Mount() and port a positive number, representing the COM port (COM1: 1, COM5: 5, ...).

The return value "isAvailable" contains:

   0, if the port is NOT available
   1, if the port exists and is not used

Configuring Ports

By default, all serial interfaces' parameters are set to 19200 bits/s, "8N1".
Eight bits, no partity ans one stop bit.

If you need other baud rates, call

  ret = proserial.Config(handle, -- configures the COM port referenced by "handle"  
                           baud, -- baud rate (300..3000000 bits/s)
                           bits, -- number of bits (5, 6, 7, 8)
                         parity, -- parity (0, 1, 2 -> NONE, ODD, EVEN)
                           stop) -- number of stop bits (1, 2)

Instead of using the numbers 0, 1 or 2 to specify the parity, you may use one of these global variables instead:


An example for setting the port to 115200 bits/s, "8N1":

  ret = proserial.Config( handle, 115200, 8, PARITY_NONE, 1 )  

The return values of the Config() function are:

   0, if an error occurred
   1, if everything went just fine

Valid baud rates in bits/s are:

      110     4800     56000     500000    3000000 
      300     9600     57600     512000
      600    14400    115200     921600 
     1200    19200    128000    1000000 
     2400    38400    256000    2000000
Notice that not every device supports every bit rate.

Opening Ports

Open a port with

  ret = proserial.Open( handle, port )  

Where "port" is the number of the COM port you wish to use (COM1: 1, COM3: 3, ...).

The return values of the Open() function are:

   0, if an error occurred
   1, if the port is now open and ready to send or receive data  

Sending Data

To send data over the serial interface, use

  ret = proserial.Send( handle, data )  

The second argument "data" can be:

                        a number: data = 123
                        a string: data = "*burp*"
   a table of numbers or strings: data = { 123, "*b", "urp*", 23 }  

Table items are sent in order of appearance.
Tables inside the table are not allowed, they will not get sent.

Notice that the Send() function can handle zeros (0)!

The return values of the Open() function are:

   0, nothing was sent (for whatever reason)  
  >0, number of bytes sent

Receiving Data

ProfiLua always receives any incoming byte in the background and stores it in memory. There's no limit to the size of data that can be received, except the amount of RAM available on your system.
You don't need to care about anything. If the port is opened, all incoming data will get saved.

You can read a byte from this memory buffer with:

  data = proserial.ReadByte( handle )

The call of ReadByte() can return:

  <0    : the buffer is empty
  0..255: the numerical value of the byte received

If the return value exceeds 255, you're drunk and doing it wrong...

Checking the Buffer

If you want to wait until a specific number of bytes was received, you can check the amount of data in the buffer with

  bytesInBuffer = proserial.BufferCount( handle )

The return values of this function are:

   <0, an error occurred (port no opened, etc...)
  >=0, the amount of data bytes in the buffer

Deleting the Buffer

If ever need to delete the complete receive buffer, just call:

  ret = proserial.BufferFlush( handle )

The buffer is now completely emtpy.
This function always returns a one (1).

Closing the Port

To close the serial port, simply call:

  ret = proserial.Close( handle )

The return values of the Close() function:

   0, an error occurred
   1, the port is closed; sending or receiving data is impossible

Notice that the "handle" is still valid and can be used to open, configure, ... the port again.

Deleting the Handle

A call to this function will destroy the handle:

  ret = proserial.UnMount( handle )

The return values of UnMount() can be;

   0, if an error occurred
   1, if the handle was destroyed successfully

As of now, the "handle" is invalid and can not be used anymore.

Simple Serial Interface

An example using the new (04/2014) serial interface.

Although this does nothing else but send Lua numbers, strings or tables by the push of a button, the provided code might come in handy as a mini-reference on how to use the serial interface.

Set the COM port of your serial interface in the "1.lua" file around line ~15

and connect TX to RX to obtain a loop.

Make sure to watch the console window, because most of the stuff happens right there!

The code in gCalculate():

  -- gCalculate
  function gCalculate(In)
    -- on top for better visibility
    local randLetter
    local randString
    local randTable
    local ch

    -- send number button
    if CheckButtonPressed( In[1], 'butTrigNumber' ) == 1 then
      randLetter = 64 + math.random(26)
      if proserial.Send( portHandle, randLetter ) > 0 then
        print("SENT " .. randLetter .. " -> " .. string.char(randLetter) )
        print("ERROR sending " .. randLetter )

    -- send string button
    if CheckButtonPressed( In[2], 'butTrigString' ) == 1 then
      randString = '*burp*'
      if proserial.Send( portHandle, randString ) > 0 then
        print("SENT " .. randString )
        print("ERROR sending " .. randString )

    -- send table button
    if CheckButtonPressed( In[3], 'butTrigTable' ) == 1 then
      randTable = { 65, 66, 67, 'a', 'b', 'c', "*burp*" }
      if proserial.Send( portHandle, randTable ) > 0 then
        io.write("SENT ")
        local i,v
        for i,v in pairs( randTable ) do
          io.write( v )
        print("ERROR sending the table")

    -- read button
    if CheckButtonPressed( In[4], 'butTrigRead' ) == 1 then
      print("RECEIVED ")
      while 1 do
        ch = proserial.ReadByte( portHandle )
        if ch < 0 then
          print("  " .. ch .. "  " .. string.char(ch) )

    return { portMountedAndOpen, proserial.BufferCount( portHandle ), 0 }

You can download example#10 here.



Q: Why did you built this?
Short answer: Boredom?

In detail:

Initially, ProfiLua was built as an universal USB device interface, including a commercial USB driver (...). I stripped down the non-free parts of it and, well, here it is...

Some day, libusb-win32 might get built in, but not until a stable version is released.

Q: What are the advantages? Why not code in plain C/C++/DELPHI?
Short answer: Lua is much more flexible than all of the above languages combined ;)

In detail:

You only need to edit a simple text file. No compiler is required. Changes will immediately be available after simply stopping and restarting the Profilab project. You can even edit the text file while Profilab is running.

Lua has a lot of features, just to name a few:

  • coroutines (sort of multi-tasking)
  • proper tail calls (allow unlimited recursion)
  • up-values (lexically scoped)
  • anonymous functions
  • first-class values (everything can be an argument)
  • weak tables (references to other objects)
  • metatables and meta-methods (object orientated; operator overloading; ...)
  • garbage collector
  • ...
Q: Is Lua/ProfiLua fast?
Short answer: What is "fast"?

In detail:

Due to the fact that Lua is an interpreting language, it will slow things down. How slow? Form your own opinion ;)

Q: I wrote a Lua script and Profilab crashes!
Short answer: Right!

In detail:

Running ProfiLua requires an error-free script!
All errors within ProfiLua (e.g. all runtime errors, calling a function that does not exist, trying to add a number to a string, and, and, and...) will crash Profilab!
So will memory leaks (Lua stack over- or underflows), kernel panics, and, and, and...

Even if Lua contains a byte compiler, it is still an interpreting language and most programming errors will show up while it is running...

This might get improved in a future version...


Q: Is it stable?
Short answer: Yes!

In detail:

There's no way around improving your Lua skills...
Any or all of the errors that make Profilab crash, are caused by you (and my personal freedom of not-including any sanity checks (Lua stack, etc...) to the code ;-)


Project Files

Q: I loaded another ProfiLua project, residing on a different path, and nothing happened!?
Short answer: Right!

In detail:

Profilab remembers the last path, from where a DLL was loaded.
If you already opened a ProfiLua project, you need to restart Profilab. Otherwise the corresponding ProfiLua file "x.lua" can not be opened.

Q: Can I use ProfiLua for compiled projects?
Short answer: Yes.

In detail:

Remember to copy the ProfiLua files "x.lua" to the EXE directory.


Q: Does ProfiLua support "0s" (zeros) in strings?
Short answer: No.

In detail:

Due to Profilab's implementation of the "CCalculateEx()" function

  void CCalculateEx(double *PInput, double *POutput, double *PUser, uchar **PStrings);  
passing over "0" to ProfiLua is not possible (for the other direction, ProfiLua to Profilab, please see a workaround in example #6). All zeros operate as string delimiters ("end markers"), hence a string ends at the first "0" discovered.

A workaround would be using the string REPLACE block, prior of feeding the string to a ProfiLua DLL-block, with a replacement like:

  0x00 -> 0x10 + 0xff
  0x10 -> 0x10 + 0x10
Just like a common DLE escape-sequence string (DLE, STX, ETX...), but unfortunately, the REPLACE block only converts "to" a zero (0x10 -> 0x00), but not "from" (0x00 -> 0x10).


Q: Can I have multiple console windows? Probably by loading the DLL more than once?
Short answer: No.

In detail:

Even if you include the DLL more than once (*1*) only a single "CONSOLE" pin needs to be set, preferably at the first ProfiLua DLL block. All other instances may print to the console window too.

(*1*) READ NOTES BELOW! ("Multiple DLLs").

Q: Does the console interfere with Profilab?
Short answer: No.

In detail:

Printing (a lot of stuff) to the console might decrease the performance of Profilab, but only a little. Usually, "io.write()" is faster than a "print()".

You can pause Profilab/ProfiLua execution by pressing the "PAUSE" key (console must have focus) and resume by hitting "SPACE". Sometimes, this comes in handy...

Even if the console is disabled, any remaining "io.write()" or "print()" call will reduce execution speed. The console is only a debug feature!

Multiple DLLs

Q: Can I rename the DLL and include it multiple times?
Short answer: No.

In detail:

Multiple instances of ProfiLua, loaded in Profilab, require a unique internal "DLL name". Renaming the file is not enough.
The compiled binary distribution of ProfiLua comes with a set of different DLLs ("_A", "_B", ...) that can be used for this.

Usually, a single DLL is all you need. You can have as many in- or outputs as you like.

Q: Can the console READ from stdin?
Short answer: No.

In detail:

Yes, but reading from stdin, e.g. by calling "", blocks Profilab until a character was received. There's an experimental non-blocking function available inside ProfiLua which might be functional in a future version:

But this should not be used until officially released. It does not work yet.


  • Values saved to gPUser table will be available on next start of the project.
  • Fixed some severe Lua stack bugs in CSimStop(), affecting arguments of gSimStop().
Upgrade to latest version is strongly recommended!
gSimStop() did not contain a correct table argument.
  • Now supports MIDI devices.
As of now, Profilab can be controlled by MIDI devices like mixer consoles or even keyboards (though the latter does not make much sense...).
  • Now full support for serial interfaces ("COM-ports").
As of now, ProfiLua supports serial interfaces.
You'll never need to shift bits around anymore ;-)
  • serial interface: added 500000 and 512000 bit/s
By demand...


- ProfiLua (DLL)
- script template (LUA)
- some Profilab examples (PRJ)
- source code (TDM-GCC/Code::Blocks)

DOWNLOAD: V1.0a (04/2014)

Note #1: Examples may contain OLDER ProfiLua DLLs!
Note #2: Most of the examples are also available in the old distribution below!
DOWNLOAD: Example #1: simple demo
DOWNLOAD: Example #2: hysteresis trigger
DOWNLOAD: Example #3: number to BIN and HEX strings
DOWNLOAD: Example #4: Probability Density Function
DOWNLOAD: Example #5: including multiple DLL instances
DOWNLOAD: Example #6: Lua table with zeros to Profilab string
DOWNLOAD: Example #7: Clock state machine
DOWNLOAD: Example #8: Saving gPUser values (v0.9b+)

DOWNLOAD: Example #90: MIDI: Query MIDI sliders, MPK49 (v0.9c+)
DOWNLOAD: Example #91: Formatted output to file

DOWNLOAD: Example #10: Simple Serial Interface Example

DOWNLOAD: source code on Github always up to date

OLDER RELEASES (including examples):

DOWNLOAD: V0.9c (04/2011)

DOWNLOAD: Example #100: JTAG-Scanner firmware tester

- Windows EXE (X86), for testing or playing around

DOWNLOAD: Lua 5.1.4 binary; EXE for Windows X86
As usual, no docs, no warranty, no support.
If this deletes your harddisk, heats up your beer or goes out with your girlfriend:
Bad luck!
ASkr 09/2010 internal release; V0.7
ASkr 12/2010 initial public release; V0.9
ASkr 04/2011 new version 0.9c
ASkr 12/2012 copied source code to Github
ASkr 04/2014 released V1.0 with serial interface support; tried to improve this (...) page
ASkr 04/2014 released V1.0a by demand; two more baud rates (500, 512kbit/s) available