Soup-to-Nuts: Phonebook Module (Part 1)

I thought it would be a good idea to document from start to finish (soup to nuts) the process of building modules in SIMPL#. I hope to do a few of these as I build out some helpful modules.

Code is available at https://github.com/kielthecoder/SoupToNutsModules.

Scope of Work

What we’re trying to build in this post is a simple Phonebook module. It should be able to:

  • Store and retrieve contact names and phone numbers
  • Display one page at a time
  • Allow navigating by page
  • Track the current page and total number of pages
  • Add new entries
  • Delete old entries

That’s enough to get started, we could always add more features later in a follow-up post. We’ll develop a module that can be used in our SIMPL Windows programs.

Create a New Project

Since I’m using a CP3 to test with, I need to create my project in Visual Studio 2008. Make sure you create a new Crestron SIMPL# Library. I’ve named mine SoupToNuts, but feel free to call it whatever you like:

Your new project exists with a default Class1 created. We’re going to build a Phonebook namespace for this module, so add a new Folder to this project:

Right-click on your project in the Solution Explorer window.

Name the new folder Phonebook, then right-click on it and select Add > Class… to create a new PhonebookEntry class. Your Solution Explorer window should now look like this:

PhonebookEntry

We’ll start by defining what each phonebook entry should look like. To keep things simple, type this into PhonebookEntry.cs:

using System;

namespace SoupToNuts.Phonebook
{
    public class PhonebookEntry
    {
        public string Name { get; set; }
        public string Number { get; set; }
    }
}

Right now, each PhonebookEntry can store a Name and Number (both as a string). And that’s it! Build this to make sure there aren’t any errors and we can move onto organizing our entries.

Phonebook

Create another class named Phonebook in the Phonebook folder. Your Solution Explorer should look like:

Open Phonebook.cs and type in the following:

using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;

namespace SoupToNuts.Phonebook
{
    public class Phonebook
    {
        private List<PhonebookEntry> _entries;
        private string _filename;

        public Phonebook()
        {
            _entries = new List<PhonebookEntry>();
        }

        public void Initialize(string filename)
        {
            _filename = filename;
            
            _entries.Clear();

            using (var stream = File.OpenText(_filename))
            {
                while (!stream.EndOfStream)
                {
                    var text = stream.ReadLine();

                    if (text.IndexOf('|') > 0)
                    {
                        var fields = text.Split('|');
                        _entries.Add(new PhonebookEntry {
                            Name = fields[0],
                            Number = fields[1]
                        });
                    }
                }
            }
        }
    }
}

At a quick glance, our Phonebook class creates a new list of PhonebookEntrys. But why do we have a separate Initialize method? This is a limitation we have to deal with in SIMPL+: we can only call a default constructor without any parameters. So we move our initialization code to a separate method and call that from function Main in SIMPL+.

Build the solution and you should end up with a SoupToNuts.clz file in your output folder. To get there, right-click on your project in the Solution Explorer and select Open Folder in Windows Explorer:

Navigate into the bin/Debug subfolder and you’ll find SoupToNutz.clz. Any time we rebuild our library, you’ll need to make sure to copy this file into your other project directories. Don’t worry, I’ll remind you.

Phonebook Wrapper

Back in our root solution folder, create a new subfolder named SIMPL Modules:

Inside this new folder, copy the SoupToNuts.clz library and create a new SIMPL+ module named Phonebook Wrapper v1.0.usp:

// --- Compiler Directives ---

// #CATEGORY "" 
// #DIGITAL_EXPAND 
// #ANALOG_SERIAL_EXPAND 

#DEFAULT_VOLATILE
#ENABLE_STACK_CHECKING
#ENABLE_TRACE

// #DEFINE_CONSTANT

#USER_SIMPLSHARP_LIBRARY "SoupToNuts"

// --- Main ---

Function Main()
{
	WaitForInitializationComplete();
}

Save and Compile this module. Note that you can only target 3- or 4-series processors because we’re using a SIMPL# library. You shouldn’t get any errors. Once linking has finished, right-click on the #USER_SIMPLESHARP_LIBRARY “SoupToNuts” line and select Open API for SoupToNuts

This will generate a description of what we can access in our library from SIMPL+. This can be handy if you get compiler errors later because SIMPL+ is pretty limited in how it talks to SIMPL# classes. Here’s the SoupToNuts.api file as it is now (I’ve highlighted the lines we care about):

namespace SoupToNuts.Phonebook;
{
     class Phonebook 
    {
        // class delegates

        // class events

        // class functions
        FUNCTION Initialize ( STRING filename );
        SIGNED_LONG_INTEGER_FUNCTION GetHashCode ();
        STRING_FUNCTION ToString ();

        // class variables
        INTEGER __class_id__;

        // class properties
    };

     class PhonebookEntry 
    {
        // class delegates

        // class events

        // class functions
        SIGNED_LONG_INTEGER_FUNCTION GetHashCode ();
        STRING_FUNCTION ToString ();

        // class variables
        INTEGER __class_id__;

        // class properties
        STRING Name[];
        STRING Number[];
    };
}

namespace SoupToNuts;
{
     class Class1 
    {
        // class delegates

        // class events

        // class functions
        SIGNED_LONG_INTEGER_FUNCTION GetHashCode ();
        STRING_FUNCTION ToString ();

        // class variables
        INTEGER __class_id__;

        // class properties
    };
}

You’ll see the Phonebook, PhonebookEntry, and Class1 classes are all available for us to use in our SIMPL+ module. Let’s create a new Phonebook. Add this code to Phonebook Wrapper v1.0.usp:

#USER_SIMPLSHARP_LIBRARY "SoupToNuts"

// --- Module Parameters ---

STRING_PARAMETER File_Name[200];

// --- Global Variables ---

Phonebook Contacts;

// --- Main ---

Then update function Main with this:

// --- Main ---

Function Main()
{
	WaitForInitializationComplete();
	Contacts.Initialize(File_Name);
}

Here you can see why Initialize had to be split away from the constructor in our library. SIMPL+ doesn’t allow us to call a different constructor when creating our Phonebook object named Contacts. We pass the File_Name string parameter into our call to Initialize.

We can’t run standalone SIMPL+ modules, they need to be added to a SIMPL Windows program.

Phonebook Demo

Similar to how we created a new subfolder for our modules to live, lets create a subfolder for programs named Demo Programs:

Copy the Phonebook Wrapper files (*.ush and *.usp) and SoupToNuts.clz into this new subfolder. Then create a new SIMPL Windows program named Phonebook Demo v1.0.smw. You can use whichever control processor you like, and we’re going to add our SIMPL+ module to it. As you can see, there’s not much to this program:

Hit Save and Compile. Load this program to your control processor and then SSH into it (using Toolbox or your SSH client of choice, like PuTTY). Type errlog to see how our program is doing:

Error: splusmanagerapp.exe [App 1] # 2020-10-29 16:15:36  # Module S-1 : Phonebook_Wrapper_v1_0 at line 28: Unhandled Exception: Crestron.SimplSharp.CrestronIO.FileNotFoundException: Could not find file '\USER\Contacts.txt'.
 at Crestron.SimplSharp.CrestronIO.FileStream..ctor(String path, FileMode mode, FileAcc 16. Error: splusmanagerapp.exe [App 1] # 2020-10-29 16:15:36  # ess access)
 at Crestron.SimplSharp.CrestronIO.StreamReader..ctor(String path)
 at Crestron.SimplSharp.CrestronIO.File.OpenText(String path)
 at SoupToNuts.Phonebook.Phonebook.Initialize(String filename)
 at UserModule_PHONEBOOK_WRAPPER_V1_0.U 17. Error: splusmanagerapp.exe [App 1] # 2020-10-29 16:15:36  # serModuleClass_PHONEBOOK_WRAPPER_V1_0.FunctionMain(Object obj)
 at Amib.Threading.Internal.WorkItem.n()
 at Amib.Threading.Internal.WorkItem.Execute()
 at Amib.Threading.SmartThreadPool.w(WorkItem A_0)
 at Amib.Threading.SmartThreadPool.i( 18. Error: splusmanagerapp.exe [App 1] # 2020-10-29 16:15:36  # ) 

Oh no, we broke it! But at least we can see our library being called. CrestronIO.File.OpenText threw an exception because the \USER\Contacts.txt file doesn’t exist on the processor! We should clean up our library to make sure errors like this don’t cause everything to grind to a halt.

Back to Visual Studio

If anything, I hope this post shows you how tedious it can be to find and fix bugs in a SIMPL Windows program. We need to go back to Visual Studio and clean up our Phonebook class to handle errors more gracefully. In Phonebook.cs add a try catch block around the offending code:

public void Initialize(string filename)
{
    _filename = filename;
            
    _entries.Clear();

    try
    {
        using (var stream = File.OpenText(_filename))
        {
            while (!stream.EndOfStream)
            {
                var text = stream.ReadLine();

                if (text.IndexOf('|') > 0)
                {
                    var fields = text.Split('|');
                    _entries.Add(new PhonebookEntry {
                        Name = fields[0],
                        Number = fields[1]
                    });
                }
            }
        }
    }
    catch (Exception e)
    {
        ErrorLog.Error("Exception in Phonebook.Initialize: {0}",
            e.Message);
    }
}

We’ll still dump something into the error log but at least the program will keep running. We should also have a way to signal to SIMPL+ that we successfully initialized. Lets add a new delegate to our class:

public class Phonebook
{
    public delegate void StatusFeedback (ushort success);

    private List<PhonebookEntry> _entries;
    private string _filename;

    public StatusFeedback OnInitialize { get; set; }

This will give our SIMPL+ module a way to add a callback if Initialization succeeds or fails. We just need to check if it’s defined before we call it within Initialize:

while (!stream.EndOfStream)
{
    var text = stream.ReadLine();

    if (text.IndexOf('|') > 0)
    {
        var fields = text.Split('|');
        _entries.Add(new PhonebookEntry {
            Name = fields[0],
            Number = fields[1]
        });
    }
}

if (OnInitialize != null) OnInitialize(1);

And for when it fails:

catch (Exception e)
{
    ErrorLog.Error("Exception in Phonebook.Initialize: {0}",
        e.Message);

    if (OnInitialize != null) OnInitialize(0);
}

You might be wondering why we don’t signal success or failure with true and false. It’s another limitation of SIMPL+: there is no BOOLEAN data type we can use in SIMPL+, but we can use INTEGER. If our delegate signature had a bool parameter, it wouldn’t show up in SIMPL+. Build your solution and copy the library file back into your SIMPL Modules folder.

We could just recompile our Phonebook Wrapper module, but lets hook into the OnInitialize delegate while we’re in here. Create a new Initialized_Fb output to signal success to our SIMPL Windows program:

#USER_SIMPLSHARP_LIBRARY "SoupToNuts"

// --- Outputs ---

DIGITAL_OUTPUT Initialized_Fb;

// --- Module Parameters ---

And define a callback function to pass the status to our output signal:

// --- Global Variables ---

Phonebook Contacts;

// --- Callbacks ---

CALLBACK FUNCTION Contacts_OnInitialize (INTEGER success)
{
	Initialized_Fb = success;
}

// --- Main ---

CALLBACK is a keyword that is used to signal this is a function that will be tied to a delegate. And in Main, use RegisterDelegate to bind our callback function:

Function Main()
{
	RegisterDelegate(Contacts, OnInitialize, Contacts_OnInitialize);
	WaitForInitializationComplete();
	Contacts.Initialize(File_Name);
}

Now when our library signals success, we can pass that into SIMPL Windows. Build this module and copy it into our Demo Programs folder.

OK now build your program and load it. If we check errlog again:

Error: splusmanagerapp.exe [App 1] # 2020-10-29 17:10:27  # Exception in Phonebook.Initialize: Could not find file '\USER\Contacts.txt'. 

At least we see our exception message now rather than a stack trace! Upload a blank Contacts.txt file to the \USER folder on your processor for now and restart the program. If you check in SIMPL Debugger, you should see:

Phew! Take a quick break before continuing on.

What We Learned

In this first part, we laid the groundwork for our Phonebook class:

  • Created a new SIMPL# Library
  • Observed the limitations of SIMPL+, such as default constructors and allowed data types
  • Created a SIMPL+ wrapper around our SIMPL# Library
  • Handled exceptions in our SIMPL# code and passed status back up to our SIMPL Windows program

In the next installment, we’ll add functionality to our class in one shot so we can cut down on how much copy-n-paste we’re doing between folders, I promise.

2 thoughts on “Soup-to-Nuts: Phonebook Module (Part 1)

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 )

Facebook photo

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

Connecting to %s