comments 5

NetLinx: Modules

Using and writing modules has some tangible benefits:

  • They are reusable code that we can polish over time
  • They break up large programs into smaller, testable units
  • They abstract away details we don’t need to worry about

Grab the code for this post from the GitHub repo.

Other posts in this series:

Communicating into and out of a module in NetLinx is a little strange at first, so we’ll walk through it together. I was hoping to link to the NetLinx Programming Guide, but it didn’t include much about modules. There are some videos you can watch on Harman’s professional training portal where the material is covered in AMX Programmer (NetLinx) – Level 2.

To add a new module to our program, right-click Module in the Workspace explorer and select Add New Module File.

Answer No when asked if you would like to use a template.

Name the new module Room System and save it into the same location as your Main Program file.

When you complete the wizard, you’ll see a blank source code file with the module definition on the top line:

MODULE_NAME='Room System'

OK, we have a module! Go to the Build > Build Workspace menu and you’ll see the compiler output looks a little different now:

---------- Starting NetLinx Compile - Version[2.5.2.420] [07-16-2021 09:05:02] ----------  
C:\Jobs\repos\NetLinxTutorial\src\Room System.axs  
C:\Jobs\repos\NetLinxTutorial\src\Room System.axs - 0 error(s), 0 warning(s)  
Compiled Code takes 14284 bytes of memory -- Token and Variable Count is 784 (Maximum is 200000)  
   
Compressing Source Code Files...  
Created SRC file: C:\Jobs\repos\NetLinxTutorial\src\Room System.src  
NetLinx Compile Complete [07-16-2021 09:05:03]  
   
----------- Starting NetLinx Compile - Version[2.5.2.420] [07-16-2021 09:05:05] -------------  
C:\Jobs\repos\NetLinxTutorial\src\Main Program.axs  
C:\Jobs\repos\NetLinxTutorial\src\Main Program.axs - 0 error(s), 0 warning(s)  
Compiled Code takes 99370 bytes of memory -- Token and Variable Count is 3988 (Maximum is 200000)  
   
Compressing Source Code Files...  
Created SRC file: C:\Jobs\repos\NetLinxTutorial\src\Main Program.src  
NetLinx Compile Complete [07-16-2021 09:05:07]  
   
>>>>--- NetLinx Compiles:  2 Files  0 Total Error(s)  0 Total Warnings(s) ---<<<< 

Both programs (.axs) are compiled into separate token files (.tkn) and then get linked together. We’ll have to add a DEFINE_MODULE statement to the main program if we want the module to do anything. Be careful to add this section after DEFINE_VARIABLE because we may want to pass variables into our modules.

(***********************************************************)
(*               VARIABLE DEFINITIONS GO BELOW             *)
(***********************************************************)
DEFINE_VARIABLE

LONG lLoopTimes[] = { 500, 500, 500, 500, 500, 500, 500, 500 }

INTEGER btnSystemPower[] = { 1, 2 }
INTEGER btnSources[]     = { 1, 2, 3, 4, 5, 6, 7, 8 }

INTEGER btnAppleTV[] = {
	    PLAY,
 	    MENU_FUNC,
	    MENU_UP,
	    MENU_DN,
	    MENU_LT,
	    MENU_RT,
	    MENU_SELECT
    }

(***********************************************************)
(*                MODULE DEFINITIONS GO BELOW              *)
(***********************************************************)
DEFINE_MODULE 'Room System' mRoomSystem

(***********************************************************)
(*                 STARTUP CODE GOES BELOW                 *)
(***********************************************************)
DEFINE_START

Build Workspace again then transfer the new program to your controller. After it boots up, test the functionality again. Do you notice anything different? What is our module even doing?

How Modules Work

To peek under the hood a little bit, we’re going to add some print debugging to our program. First, in Main Program.axs add:

DEFINE_START

SEND_STRING dvCONSOLE, 'Main Program: Entered DEFINE_START'

TIMELINE_CREATE(TL_LOOP, lLoopTimes, LENGTH_ARRAY(lLoopTimes),
    TIMELINE_RELATIVE, TIMELINE_REPEAT);

SEND_STRING dvCONSOLE, 'Main Program: Leaving DEFINE_START'

Next, in RoomSystem.axs we’ll need to add a bit more. Remember, these programs are completely separate (for now) so they don’t have access to each other’s variables or includes. If we want something available in our module, we’ll need to define it there as well:

MODULE_NAME='Room System'

(***********************************************************)
(*          DEVICE NUMBER DEFINITIONS GO BELOW             *)
(***********************************************************)
DEFINE_DEVICE

dvCONSOLE = 0:1:0	// Master Controller

(***********************************************************)
(*                  INCLUDE FILES GO BELOW                 *)
(***********************************************************)

#INCLUDE 'SNAPI'

(***********************************************************)
(*                 STARTUP CODE GOES BELOW                 *)
(***********************************************************)
DEFINE_START

SEND_STRING dvCONSOLE, 'Room System: Entered DEFINE_START'

SEND_STRING dvCONSOLE, 'Room System: Leaving DEFINE_START'

Build Workspace again and send to the controller. The next part will require you to be persistant: while the controller is rebooting and restarting our program, we need to try and enable diagnostic messages. The easiest way I’ve found to do this is to have the Diagnostics tab open, right-click on it and select Enable Diagnostic Messages. If the controller is still booting, you’ll get an error message. Keep trying! Eventually it will connect and you can watch the startup messages fly by.

Keep hitting Retry until you connect successfully

In the Diagnostics window, you’ll eventually see something like:

Line     13 2021-07-16 (09:39:40)::  Main Program: Entered DEFINE_START
Line     14 2021-07-16 (09:39:40)::  Main Program: Leaving DEFINE_START
Line     15 2021-07-16 (09:39:40)::  Room System: Entered DEFINE_START
Line     16 2021-07-16 (09:39:40)::  Room System: Leaving DEFINE_START

Modules behave like little independent programs. First, our main program is allowed to run, then each module we’ve defined gets a chance to run as well.

Writing Our Own Modules

I’d like to develop a module that will manage the UI elements on our touchpanel for us. I don’t know if you’ve run into it yet, but you can get into a situation that looks like this pretty easily:

Oops!

If we think about what we need access to in our module, it’s only two devices:

  • vdvROOM, to keep track of the room state
  • dvTP1, to send commands to the touchpanel device

We can define arguments that get passed into our module from the main program. Add this to Room System.axs:

MODULE_NAME='Room System' (DEV vdvROOM, DEV dvTP)

vdvROOM and dvTP are just variables in our module. They don’t actually point to anything until we pass something in from the main program. Lets move some of the logic over from Main Program.axs into Room System.axs:

(***********************************************************)
(*                 STARTUP CODE GOES BELOW                 *)
(***********************************************************)
DEFINE_START

SEND_STRING dvCONSOLE, 'Room System: Entered DEFINE_START'

SEND_STRING dvCONSOLE, 'Room System: Leaving DEFINE_START'

(***********************************************************)
(*                  THE EVENTS GO BELOW                    *)
(***********************************************************)
DEFINE_EVENT

CHANNEL_EVENT[vdvROOM,POWER_FB]
{
    ON:
    {
	    SEND_COMMAND dvTP,'@PPX'
	    SEND_COMMAND dvTP,'@PPN-Source Selection'
    }
    OFF:
    {
	    SEND_COMMAND dvTP,'@PPX'
	    SEND_COMMAND dvTP,'@PPN-Start System'
    }
}

Notice how we have to change dvTP1 to dvTP in the module because that’s the variable name we’ve defined at the top. I’m also sending the @PPX command to the touchpanel to make sure all pop-ups are closed. This should keep everything in sync. We should also make sure that the touchpanel is in sync when it first comes online, so lets add a data event to our module as well:

(***********************************************************)
(*                  THE EVENTS GO BELOW                    *)
(***********************************************************)
DEFINE_EVENT

DATA_EVENT[dvTP]
{
    ONLINE:
    {
	    IF ([vdvROOM,POWER_FB])
 	    {
	        SEND_COMMAND dvTP,'@PPX'
	        SEND_COMMAND dvTP,'@PPN-Source Selection'
	    }
	    ELSE
	    {
	        SEND_COMMAND dvTP,'@PPX'
	        SEND_COMMAND dvTP,'@PPN-Start System'
	    }
    }
}

CHANNEL_EVENT[vdvROOM,POWER_FB]
{
    ON:
    {
	    SEND_COMMAND dvTP,'@PPX'
	    SEND_COMMAND dvTP,'@PPN-Source Selection'
    }
    OFF:
    {
	    SEND_COMMAND dvTP,'@PPX'
	    SEND_COMMAND dvTP,'@PPN-Start System'
    }
}

There’s a lot of repeated code here, so it might make sense to create a function instead. Since this code will be hidden behind a module, I’m not going to worry about it a whole lot for the purposes of this post. Also note that because we’re relying on SNAPI channels, the interface remains the same whether the code triggers within our module or not.

And finally, in Main Program.axs, we need to pass the correct devices into our module:

(***********************************************************)
(*                MODULE DEFINITIONS GO BELOW              *)
(***********************************************************)
DEFINE_MODULE 'Room System' mRoomSystem (vdvROOM,dvTP1)

(***********************************************************)
(*                 STARTUP CODE GOES BELOW                 *)
(***********************************************************)
DEFINE_START

Build Workspace then send the program to your controller. Play around with the UI and see if you can get it out of sync. If you open the transport controls pop up and restart the touchpanel, it should come back up on the source selection page but with the pop up closed. Also note that when the touchpanel comes online, it still double-beeps because the event handler in our main program is also getting called.

SNAPI SEND_COMMANDs

To explore module SEND_COMMANDs, we’re going to create one more module named Simple Switcher.axs. Here it is, in its entirety:

MODULE_NAME='Simple Switcher' (DEV vdv, DEV dv)

DEFINE_VARIABLE

INTEGER nOutputs[] = { 1 }

NON_VOLATILE INTEGER nSelectedInput[] = { 0 }

DEFINE_EVENT

DATA_EVENT[dv]
{
    ONLINE:
    {
	    SEND_COMMAND dv, 'SET BAUD 9600,N,8,1'
	    SEND_COMMAND dv, 'HSOFF'
    }
}

DATA_EVENT[vdv]
{
    ONLINE:
    {
	    LOCAL_VAR INTEGER i;
	
	    FOR (i = 1; i <= LENGTH_ARRAY(nSelectedInput); i++)
	    {
	        SEND_LEVEL vdv,i,nSelectedInput[i]
	    }
    }
    COMMAND:
    {
	    LOCAL_VAR INTEGER in;
	    LOCAL_VAR INTEGER out;
	
	    // CI<input>O<output>
	    // Switch input to output, All levels
	    IF (LEFT_STRING(DATA.TEXT, 2) == 'CI')
	    {
	        REMOVE_STRING(DATA.TEXT, 'I', 1)
	        in = ATOI(DATA.TEXT)
	        REMOVE_STRING(DATA.TEXT, 'O', 1)
	        out = ATOI(DATA.TEXT)
	    
	        nSelectedInput[out] = in
	        SEND_STRING dv, "ITOA(in),'*',ITOA(out),'!'"
	    }
    }
}

LEVEL_EVENT[vdv,nOutputs]
{
    SEND_COMMAND vdv,"'CI',ITOA(LEVEL.VALUE),'O',ITOA(LEVEL.INPUT.LEVEL)"
}

On line 1, we’re passing two arguments into our module. You’ll see this with a lot of modules: the first argument is a virtual device we control via SNAPI, the second is the physical device we’re controlling. In this case, we don’t have a real physical device, but you can watch the strings that get sent out the serial port in NetLinx Studio.

Lines 13-17 handle configuring the physical port to communicate with the device. Here, we make sure we’re set for 9600 baud, no parity, 8 data bits, and 1 stop bit. We also turn hardware handshaking off. These settings are determined by whichever device you’re physically communicating with (I’m just making them up for our example).

Lines 24-29 handle syncing our status when the virtual device comes online. We report which input is selected to which output using levels. Level 1 on our virtual device corresponds to output 1. So the value sent to level 1 is the currently selected input. While we loop through the nSelectedInput array, there’s only 1 element in this array, so we only update output 1 for now.

Lines 33-47 handle commands getting sent to our virtual device. This part is pure SNAPI. If you look up switcher control in the SNAPI help file, you’ll see the format to route an input to an output is CI<input>O<output>. This is the Autopatch command format (AMX acquired them a while ago). Once we process the command, we send the device control string to our physical device. You might notice this example looks a lot like Extron control strings.

Line 53 allows us to take a SEND_LEVEL event and turn it into a SEND_COMMAND. This might seem strange, but this pattern lets us keep the device-specific control in one place (the COMMAND event). Everything else can communicate with the virtual device exclusively. Our level event is also written to take advantage if we expand the number of outputs on our switcher at a later date. For now, everything routes to output 1.

Now we need to update Main Program to use this new module. Here’s Main Program in its entirety:

PROGRAM_NAME='Main Program'

(***********************************************************)
(*          DEVICE NUMBER DEFINITIONS GO BELOW             *)
(***********************************************************)
DEFINE_DEVICE

dvCONSOLE = 0:1:0	// Master Controller

dvCOM1 = 5001:1:0	// RS-232 port 1
dvCOM2 = 5001:2:0	// RS-232 port 2

dvIR1 = 5001:9:0	// IR port 1
dvIR2 = 5001:10:0	// IR port 2

dvRELAY = 5001:8:0	// Relays
dvIO    = 5001:17:0	// GPIO

dvTP1       = 10001:1:0	 // MXP-9000i
dvTP1_SWT   = 10001:2:0	 // Switcher controls
dvTP1_TPORT = 10001:3:0	 // Transport controls

vdvROOM = 33001:1:0
vdvSWT  = 33002:1:0

(***********************************************************)
(*               CONSTANT DEFINITIONS GO BELOW             *)
(***********************************************************)
DEFINE_CONSTANT

TL_LOOP = 1

(***********************************************************)
(*                  INCLUDE FILES GO BELOW                 *)
(***********************************************************)

#INCLUDE 'SNAPI'

(***********************************************************)
(*               VARIABLE DEFINITIONS GO BELOW             *)
(***********************************************************)
DEFINE_VARIABLE

LONG lLoopTimes[] = { 500, 500, 500, 500, 500, 500, 500, 500 }

INTEGER btnSystemPower[] = { 1, 2 }
INTEGER btnSources[]     = { 1, 2, 3, 4, 5, 6, 7, 8 }

INTEGER btnAppleTV[] = {
	    PLAY,
	    MENU_FUNC,
	    MENU_UP,
	    MENU_DN,
	    MENU_LT,
	    MENU_RT,
	    MENU_SELECT
    }

(***********************************************************)
(*                MODULE DEFINITIONS GO BELOW              *)
(***********************************************************)
DEFINE_MODULE 'Room System' mRoomSystem (vdvROOM,dvTP1)
DEFINE_MODULE 'Simple Switcher' mSwitcher (vdvSWT,dvCOM1)

(***********************************************************)
(*                 STARTUP CODE GOES BELOW                 *)
(***********************************************************)
DEFINE_START

SEND_STRING dvCONSOLE, 'Main Program: Entered DEFINE_START'

TIMELINE_CREATE(TL_LOOP, lLoopTimes, LENGTH_ARRAY(lLoopTimes),
    TIMELINE_RELATIVE, TIMELINE_REPEAT);

SEND_STRING dvCONSOLE, 'Main Program: Leaving DEFINE_START'

(***********************************************************)
(*                  THE EVENTS GO BELOW                    *)
(***********************************************************)
DEFINE_EVENT

DATA_EVENT[dvTP1]
{
    ONLINE:
    {
	    SEND_COMMAND dvTP1,'ADBEEP'
    }
}

BUTTON_EVENT[dvTP1,btnSystemPower]
{
    PUSH:
    {
	    TO[BUTTON.INPUT]
	
	    [vdvROOM,POWER_ON] = (GET_LAST(btnSystemPower) == 1)
    }
}

BUTTON_EVENT[dvTP1,3]	// Toggle AppleTV popup
{
    PUSH:
    {
	    TO[BUTTON.INPUT]
	
	    SEND_COMMAND dvTP1,'PPOG-AppleTV'
    }
}

BUTTON_EVENT[dvTP1_SWT,btnSources]
{
    PUSH:
    {
	    SEND_LEVEL vdvSWT,1,GET_LAST(btnSources)
    }
}

BUTTON_EVENT[dvTP1_TPORT,btnAppleTV]
{
    PUSH:
    {
	    TO[dvIR1,BUTTON.INPUT.CHANNEL]
    }
}

LEVEL_EVENT[vdvSWT,1]
{
    INTEGER i
    
    FOR (i = 1; i <= 8; i++)
    {
	    [dvTP1_SWT,i] = (i == LEVEL.VALUE)
    }
    
    IF (LEVEL.VALUE == 5)
    {
	    SEND_COMMAND dvTP1,'^SHO-3,1'
    }
    ELSE
    {
	    SEND_COMMAND dvTP1,'^SHO-3,0'
    }
}

CHANNEL_EVENT[vdvROOM,POWER_FB]
{
    OFF:
    {
	    SEND_LEVEL vdvSWT,1,0
    }
}

CHANNEL_EVENT[dvIO,1]	// Fire Alarm
{
    OFF:
    {
	    OFF[vdvROOM,POWER_ON]
    }
}

The Main Program has gotten ridiculously simple at this point. We’re basically just gluing modules to our interface using SNAPI channels. We even added more functionality by clearing our selected source at shutdown.

Next Time

The next post in this series will be the last. Hurray! We’ll put everything together and write a real-world program! Thanks for reading!

5 Comments

  1. Pingback: NetLinx: Getting Started | Kiel the Coder

  2. Pingback: NetLinx: Your First Program | Kiel the Coder

  3. Pingback: NetLinx: Testing | Kiel the Coder

  4. Pingback: NetLinx: SNAPI | Kiel the Coder

  5. Pingback: NetLinx: A Real Program – Kiel the Coder

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s