Hard-won VB.NET and ShiftBrite/ShiftBar code

The following code sends the minimum amount of data to the Maestro, which uses
three ‘servo’ channels to control three ‘output’ channels such that a string of
ShiftBrites and/or ShiftBars change according to the light data you send. Most
of this code is cut-and-paste ready if you follow the simple setup steps.

'SETUP FOR A MINI-MAESTRO (NOT A MICRO-MAESTRO-- AT LEAST NOT WITHOUT ADAPTATION).
Channel 0 as Output. Call it DATA.
Channel 1 as Output. Call it CLOCK.
Channel 2 as Output. Call it LATCH.
Channel 3 as Servo. Call it BLUE. Not really a servo; it is color component data.
Channel 4 as Servo. Call it RED. Not really a servo; it is color component data.
Channel 5 as Servo. Call it GREEN. Not really a servo; it is color component data.

‘Cut and paste this script into the Maestro Script tab, then click
’“Apply Settings”. Similar to the example provided by Pololu. Thanks, folks!

sub SendCLOCK			# The control computer and send_bit
				# use this for all data shifts.
         0 0 8000 0 servo servo 	# Toggle CLOCK (channel 0).  Channel
				# 0 to 8000 then to 0. Right->left. 
Return

sub SendLATCH			# The control computer calls this
				# one time after all data shifts.						0 1 8000 1 servo servo 	# Toggle LATCH (channel 1). Target=1;
				# Value=8000. Target=1. Value=0.
quit

Sub SendFront			# Send two bits of front matter
       0 send_bit			# Set DATA (channel 2) to < 6000 (clear)
       0 send_bit			# "Address" clear for color.  DATA=Clear.
quit

sub SetBlue 			# Color component in 10 bits 
	3 get_position
	SendTenBits		# Send ten bits of BLUE
quit					

sub SetRed 			# Color component in 10 bits 
	4 get_position
	SendTenBits		# Send ten bits of BLUE
quit					

sub SetGreen			# Color component in 10 bits 
	5 get_position
	SendTenBits		# Send ten bits of BLUE
quit					

sub SendTenBits
				# User computer put 3 10-bit color component
				# values into 3 'servo' channels.
				# One is currently top of stack.  Send 10 bits.
        512			             # 512 = ten bit counter/mask. Ten right-shifts.
        begin			# Start a loop
      	dup			# Duplicate the top value in the stack.  First
				# 1000000000 then 100000000 then 10000000, etc.
      	while 		             # If the new top value = 0, jump to loop end.
      	over			# Copy the 'position' value to top of stack.
				# Not really servo position but color component.
	over 			# Copy the counter/mask to top pf stack.
				# That is the bitmask (512, 256, 128, etc.)
	bitwise_and 	             # Boolean AND the top two values in the stack.
				# Now the top value in the stack is 0 or is
				# 512, 256, 128, etc.  0 or !0.
	send_bit 		             # Send 0 for a result of 0 or >= 6000 for !0.
             1 shift_right	             # Right-shift countermask (argument=1) bit.
				# Mask/2 and counter less 1.  Repeat the loop
	repeat		             # for ten bits until counter=0, then leave loop.
      	drop			# Drop the top stack value, the original 512.
  	drop			# Drop the top stack value, the color.
return

sub send_bit 			# sends a single bit 
	if 8000 else 0 endif 	# If the value in the stack is non-zero,
				# then put a value >= 6000 in the stack
				# else put < 6000 in the stack (clear).
             2 servo 			# Target=DATA (channel 2).  Value = 
				# clear (<6000) or set (>5999). 
	SendClock		# Toggle CLOCK bit (channel 0).
Return

#Ensure the control mode didn't get messed up by noise
sub ControlMode			# The control computer might call this
				# once for each light and then
				# call SendLATCH before it sends
				# Color data.
      0 send_bit			# This bit does not matter 
      1 send_bit 			# the "address" bit - 1 means control mode
      0 send_bit 			# ATB
      0 send_bit 			# ATB 
      0 send_bit 			# N/A 
      1 send_bit 		             # Dot Correction 2 
      1 send_bit 			# Dot Correction 2 
      0 send_bit 			# Dot Correction 2 
      0 send_bit 			# Dot Correction 2 
      1 send_bit 			# Dot Correction 2 
      0 send_bit 			# Dot Correction 2 
      0 send_bit 			# Dot Correction 2 
      0 send_bit 			# N/A 
      0 send_bit 			# N/A 
      0 send_bit 			# N/A 
      1 send_bit 			# Dot Correction 1 
      1 send_bit 			# Dot Correction 1 
      1 send_bit 			# Dot Correction 1 
      1 send_bit 			# Dot Correction 1 
      0 send_bit 			# Dot Correction 1 
      0 send_bit 			# Dot Correction 1 
      0 send_bit 			# Dot Correction 1 
     0 send_bit			# N/A
      0 send_bit 			# Clock Mode
      0 send_bit 			# Clock Mode
      1 send_bit 			# Dot Correction 0 
      1 send_bit 			# Dot Correction 0 
      0 send_bit 			# Dot Correction 0 
      0 send_bit 			# Dot Correction 0 
      1 send_bit 			# Dot Correction 0 
      0 send_bit 			# Dot Correction 0 
      0 send_bit 			# Dot Correction 0 
quit

Note: We don’t call the last subroutine above in this example, but it may be useful. See the MaceTech
documentation for ShiftBrites for an exceedingly brief explanation of Control Mode, which you may have
to mess with if your ShiftBrites or ShiftBars get serial noise that undesirably blank out the lights.

'SETUP FOR THIS VB.NET EXAMPLE.
'========================================
'The important code is enclosed like this
'========================================
'The rest of the code is provided because it is a) useful as-is in your project; or b) handy for explaining
'the context of the code you may pull out of this example for your project.

'Add a public array named paryMaestroData() as Byte
'Add a public 2-dimension array (making the assumption that you are changing light patterns over
'time) named paryData() as Integer.  Populate it with light data so that each light (first dimension) at each 
'timeslice (second dimension) uses this 10-bits-per-color-component format when converted to binary:
'   1111111111 1111111111 1111111111
'   ---Blue--- ----Red--- ---Green--

'Add a public integer named pintDataTimeslice (this example assumes you'll want to change lights over time)
'Add a public string named pstrCOMCommandPort
'Add a public string named pstrCOMTTLPort
'Add a public boolean named pbooUseHardware

'To your Windows Form:
'Add a COM Port named SerialPort1
'Add a groupbox named grpCOM.  In it:
'	Add a combobox named cboCOMPort
'	Add a combobox named cboTTLPort
'	Add a button named btnCOMOkay	
'Add a NumericUpDown control named nudLightCount.  Minimum = 0; Maximum = lights in your string.
 
'Add this event code as a handy way to select the USB ports
    Private Sub btnCOMOkay_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCOMOkay.Click
        Dim L, I, U As Integer
        'Test the caption
        If btnCOMOkay.Text = "ON" Then
            'Change the text to identify the COM comboboxes
            grpCOM.Text = "COM (USB)  Master Port.    Slave Port.    Pick the ports (see Start|Control Panel|Hardware and Sound|Devices and Printers[Device Manager]|Ports(COM & LPT) or equivalent), then 'Pick.'"
            'Change this button's caption
            btnCOMOkay.Text = "Pick"
            'Enable and show these controls
            cboCOMPort.Enabled = True
            cboTTLPort.Enabled = True
            cboCOMPort.Visible = True
            cboTTLPort.Visible = True
            'Create the COM port list
            COMList()
        ElseIf btnCOMOkay.Text = "Pick" Then
            'Disable these controls
            cboCOMPort.Enabled = False
            cboTTLPort.Enabled = False
            'Disable and hide this button
            btnCOMOkay.Enabled = False
            btnCOMOkay.Visible = False
            'Ensure both indices have been set and both or neither is '[None]'
            If ((cboCOMPort.SelectedIndex < 1) Or (cboTTLPort.SelectedIndex < 1)) Then
                'The user is not using hardware
                pbooNoHardware = True
                'No changes allowed
                grpCOM.Text = "COM (USB)  Master Port.    Slave Port.    Restart to select COM ports.  There is no COM Port output now."
                'Show the user has selected no COM ports
                cboCOMPort.Text = "[None]"
                cboTTLPort.Text = "[None]"
                pstrCOMCommandPort = "[None]"
                pstrCOMTTLPort = "[None]"
                'Show certain controls
                ControlHide(&H50005)     '000 0000 0101 0000 0000 0000 0101
            Else
                'Remember the COM port names
                pstrCOMCommandPort = cboCOMPort.Items(cboCOMPort.SelectedIndex)
                pstrCOMTTLPort = cboTTLPort.Items(cboTTLPort.SelectedIndex)
                'Show this textbox
                txtCOMOut.Enabled = True
                txtCOMOut.Visible = True
                'Get the width of the COM textbox less a little bit for new entries to prepend the text shown therein
                pintCOMOutWidth = ((txtCOMOut.Width / pcintCharWidth8pnt) - 10)
                'No changes allowed
                grpCOM.Text = "COM (USB)  Master Port.    Slave Port.    Restart to reselect COM ports.  Here is the most recent output to the Maestro:"
                'Standard baud rates:  9600, 14400, 19200, 28800, 33600, 56000
                'Set up the public COMMPort
                comUSB = My.Computer.Ports.OpenSerialPort(pstrCOMCommandPort, 56000, Ports.Parity.None, 8, Ports.StopBits.One)
                comUSB.DiscardInBuffer()
                comUSB.DiscardNull = False
                comUSB.Handshake = Ports.Handshake.None
                comUSB.WriteTimeout = -1
                'There may have been power-up serial errors on the Maestro.  Clear those errors, and meanwhile, initialize the Maestro.
                I = -1
                For L = 0 To (pintPuppetCount - 1)
                    'Get the ID
                    U = paryPuppetNumberAssociatedControllerID(L)
                    'Compare to the previous ID
                    If U <> I Then
                        'Clear errors and initialize the Maestro
                        MaestroClearError(U)
                        'Send the Maestro the Go Home command
                        MaestroGoHome(U)
                    End If
                    'Avoid the initial value after the first time
                    I = U
                Next
            End If
        End If
    End Sub

'Add some functions and subroutines
'=============================================================
    Function COMIn() As Byte
        Dim bytIn As Byte = 255
        If comUSB.BytesToRead > 0 Then bytIn = comUSB.ReadByte
        COMIn = bytIn
    End Function
'=============================================================

    Sub COMList()
        'No COM port
        cboCOMPort.Items.Add("[None]")
        cboTTLPort.Items.Add("[None]")
        'Show all available COM ports
        For Each sp As String In My.Computer.Ports.SerialPortNames
            cboCOMPort.Items.Add(sp)
            cboTTLPort.Items.Add(sp)
        Next
    End Sub

'=============================================================
    Sub COMOut(ByVal strCOMPort As String, ByVal bytOut() As Byte, ByVal intCount As Integer, ByVal booSubroutine As Boolean)
        Dim L, Cntr As Integer
        Dim bytIn As Byte
        Dim strOut As String
        Dim strTemp As String = ""
        Dim strCOMIn As String = ""

        Try
            'Send a byte array to the selected serial port
            comUSB.Write(bytOut, 0, intCount)
            'Show the bytes that may change
            For L = 1 To UBound(bytOut)
                strTemp += Format(bytOut(L))
                If L < UBound(bytOut) Then strTemp += ","
            Next
            'Collect the string of most recent data
            strOut = txtCOMOut.Text & "|" & strTemp
            'Cull the length
            If strOut.Length > 1000 Then strOut = Microsoft.VisualBasic.Right(strOut, 100)
            'Show the data that went out and any that came in
            txtCOMOut.Text = strOut
            'Move the caret to the end
            txtCOMOut.SelectionStart = txtCOMOut.Text.Length
            'Scroll to the end
            txtCOMOut.ScrollToCaret()
            'Test the subroutine flag
            If booSubroutine = True Then
                'Clear the buffer from last time
                comUSB.DiscardInBuffer()
                'Make a query and look for a response
                Cntr = 0
                bytIn = 255
                Do Until bytIn = 1
                    'Send the 'Get Script Status' command array prepared at startup
                    comUSB.Write(parySubroutine, 0, 3)
                    'Bump the counter
                    Cntr += 1
                    'Test the counter
                    If Cntr > 100 Then Exit Do
                    'Test the response
                    bytIn = COMIn()
                Loop
            End If
        Catch E As Exception
            ' Let the user know what went wrong.
            Console.WriteLine("Communications cannot be established via the USB port:")
            Console.WriteLine(E.Message)
            txtCOMOut.Text = E.Message & " " & Microsoft.VisualBasic.Left(txtCOMOut.Text, txtCOMOut.Width / pcintCharWidth8pnt)
        End Try
          End Sub
'===============================================================

'The routines that work together and with the COM port to shift light data values out to one or more ShiftBrites or ShiftBars
'===============================================================
    Sub LightShift()
        Dim L, Pntr, intData, intColor As Integer
        Dim B, bytLightCount As Byte

        'The Pololu ShiftBrite or ShiftBar (with Satellite01 LED module) are used for lighting.  Many lights can be used.  Each 
        'has a shift register.  When there is any change, we send out enough data to set all existing lights at 32 bits each.  
	  'This example assumes < 256 lights in your string.

        'Are we using hardware?
        If pbooNoHardware = True Then Exit Sub

        'How many lights are there?
        bytLightCount = CByte(nudLightCount.Value)
        'Constrain
        If bytLightCount = 0 Then Exit Sub

        'Set Multiple Targets (Mini Maestro 12, 18, and 24 only) in Pololu protocol: 
        '0xAA, device number, 0x1F, number of targets, first channel number, first target low bits, first target high bits, 
        'second target low bits, second target high bits…
        '"This command simultaneously sets the targets for a contiguous block of channels. The first byte specifies how many 
        'channels are in the contiguous block; this is the number of target values you will need to send. The second byte 
        'specifies the lowest channel number in the block. The subsequent bytes contain the target values for each of the 
        'channels, in order by channel number, in the same format as the Set Target command above. For example, to set 
        'channel 3 to 0 (off) and channel 4 to 6000 (neutral), you would send the following bytes:
        '0xAA, 0x00, 0x1F, 0x02, 0x03, 0x00, 0x00, 0x70, 0x2E"
        'One multiple targets command for each light
        For B = 0 To (bytLightCount - 1)
            'Reset the pointer
            Pntr = 0
            'Prepare an array for one light to receive one multiple targets command
            ReDim paryMaestroData(10)   'For each light: AA, device, 0x1F, starting target, target count, low, high, low, high, low, high.
            '&HAA
            paryMaestroData(Pntr) = &HAA : Pntr += 1
            'Device
            paryMaestroData(Pntr) = 0 : Pntr += 1
            'Multiple Targets Command
            paryMaestroData(Pntr) = &H1F : Pntr += 1
            'Number of targets
            paryMaestroData(Pntr) = 3 : Pntr += 1
            'First target number
            paryMaestroData(Pntr) = 3 : Pntr += 1
            'Get the light data.  We don't care if it is used or not because we still have to write the data to the COLOR channel.  
            intData = paryData(B, pintDataTimeslice)
            'Three channels per light
            For I = 3 To 5
                'Which color
                Select Case I
                    Case 3  'Blue
                        intColor = (((intData And &H3FF00000) >> 20) * 4)  'Data is 10-bit.  Multiply by 4. Servo 1/4 usec divides by 4.
                    Case 4  'Red
                        intColor = (((intData And &HFFC00) >> 10) * 4)     'Data is 10-bit.  Multiply by 4. Servo 1/4 usec divides by 4.
                    Case 5  'Green
                        intColor = ((intData And &H3FF) * 4)               'Data is 10-bit.  Multiply by 4. Servo 1/4 usec divides by 4.
                End Select
                'Low data
                paryMaestroData(Pntr) = (intColor And &H7F) : Pntr += 1
                'High data
                paryMaestroData(Pntr) = ((intColor And &H3F80) >> 7) : Pntr += 1
            Next
            'Talk to the COM port.  Three 10-bit settings are in three separate 'servo' channels.  No bits shift yet.  Not a subroutine.
            COMOut(pstrCOMCommandPort, paryMaestroData, paryMaestroData.Length, False)
            'Shift 32 bits in four separate COM port calls
            For I = 2 To 5
                'Reset the pointer
                Pntr = 0
                'Prepare an array to call one scripted subroutine
                ReDim paryMaestroData(3)   'For each light: AA, device, 0x27, subroutine number.
                'Scripted subroutine number is I
                Pntr = MaestroBuildArray(Pntr, 0, &H27, -1, I)
                'Talk to the COM port.  Two or ten bits shift into or further into ShiftBrite or ShiftBar registers
                COMOut(pstrCOMCommandPort, paryMaestroData, paryMaestroData.Length, True)
            Next
        Next
        'Reset the pointer
        Pntr = 0
        'Prepare the array for the LATCH script subroutine command, which sets the ShiftBrite(s)/ShiftBar(s).
        ReDim paryMaestroData(3)     'One time AA, device, 0x27, pcintShiftSubLatch.
        'Make COM start the script that sets LATCH after 32 bits for each light are sent.  Subroutine pcintShiftSubLatch (last argument) sets LATCH.
        Pntr = MaestroBuildArray(Pntr, 0, &H27, -1, pcintShiftSubLatch)      'Channel -1 means no channel is used
        'Talk to the COM port.  All the foregoing shifted data will now light all your lights at once according to the data you sent each light.
        COMOut(pstrCOMCommandPort, paryMaestroData, paryMaestroData.Length, True)
           End Sub

    Function MaestroBuildArray(ByVal Pntr As Integer, ByVal bytDevice As Byte, ByVal bytCommand As Byte, ByVal intChannel As Integer, ByVal intData As Integer) As Integer
        '&HAA
        paryMaestroData(Pntr) = &HAA : Pntr += 1
        'Device
        paryMaestroData(Pntr) = bytDevice : Pntr += 1
        'Command
        paryMaestroData(Pntr) = bytCommand : Pntr += 1
        'Channel?  intChannel can be negative, meaning no channel is used.
        If intChannel > -1 Then
            'Channel
            paryMaestroData(Pntr) = CByte(intChannel) : Pntr += 1
        End If
        'Data
        paryMaestroData(Pntr) = (intData And &H7F) : Pntr += 1
        'If command is 4, send to Target, then data is in two bytes.
        If (bytCommand = &H4) Then
            paryMaestroData(Pntr) = ((intData And &H3F80) >> 7)
            Pntr += 1
        End If
        'Return Pntr
        MaestroBuildArray = Pntr
    End Function
'=============================================================

'Upon exit
    Private Sub frmMain_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
        comUSB.Close()
        comUSB.Dispose()
    End Sub

There would appear to be innumerable ways to make the above code more efficient, especially in Sub LightShift(). Most of them don’t work! 'The Maestro somehow loses information. Say you called one script subroutine that calls SendTenBits three times for Blue, Red and Green. 'Much better, right? It doesn’t work. The rule seems to be, One Maestro command for one serial transmission, and one script subroutine 'at a time. Otherwise it all just doesn’t work as expected. So I hope this helps save you a lot of work! I sure do love the Maestro!

If you send two “Restart Script at Subroutine” serial commands without any delay, then the first command will cause your subroutine to start running, but the second command will be received and restart the scripting engine before your subroutine actually finishes running. So that wouldn’t work. You could use the “Get Script Status” command to wait until the first subroutine has finished (because a QUIT command was executed) or you could just add a sufficient delay in your program.

I’m not sure what you mean by “one Meastro command for one serial transmission”. You should definitely be able to send multiple commands in one transmission, and as long as one of them doesn’t undo the action of another, they should all work. For example a Set Target command for Channel 0 and a Set Target command for Channel 1 should work together. If you’d like to explore this further, tell me what commands you were sending together that didn’t work and how they didn’t work.

I’m glad that you got everything working and love the Maestro, and thanks for sharing your code!

–David

Thanks, David, I certainly would like to explore solutions. VB.NET accepts a byte array for COM Port output. One cannot interact with the Maestro-- testing for completed scripted subroutines, for example-- if that data array contains more than one “execute subroutine” command because the data is in the queue and gone. The operating system pumps it out and the Maestro takes it in as fast as they both can, and upon receipt of a 0x27 command the Maestro executes subroutines without regard to other subroutines already running. In the first place, I would have thought the Maestro processor could process a pretty sizeable script before the serial input could receive four more bytes, even at a high baud rate. There is no hint about how fast subroutines run in the documentation (what are the MIPS?), but apparently they’re slow because subroutines are stepping on previous subroutines. One has to time serial transmissions instead, breaking them up into little pieces, as I said: One serial transmission of four bytes (&HAA, Device, &H27, subroutine number) for every subroutine that has to be executed. Very inefficient on the computer end, because it has to watch for completed subroutines by polling the COM Port input! .NET has given up the excellent OnComm interrupt event of VB6, sad to say. When the computer software floods the queue with subroutine status queries and by polling finally sees the 1 come back, meaning the subroutine is complete, then it can send another small byte array of data. When one is churning out a lot of ShiftBrite/ShiftBar light settings disguised as position data, then calling a subroutine to process them, and even worse, trying to synchronize lights with servos-- well, one can’t in anything like a timely way. What would be better would be an “Execute Subroutine With Handshaking” or something like that. If the appropriate command comes in, the Maestro should stop the input stream, finish the subroutine, then start the input stream and take in more data. Or maybe there should be a way to access any available Maestro memory and send a command to buffer the whole remaining input in Maestro’s memory, say, and execute it in order while waiting for subroutines which are responding to settings to complete. We can’t just queue the subroutines, because they often need ‘position’ data or other foregoing data to work with, which changes on the fly.

Meanwhile, the code I provided is the best I can come up with. If you can improve it, I would be thrilled! Thanks!

Loren

Hello, LorenLogic. Keep in mind that you’re not sending these commands on a real serial port, you’re sending them over USB. Chances are that when you send two “Restart Script at Subroutine” commands together in the same array, they will be sent to the Maestro in the same USB packet, so from the Maestro processor’s perspective the two commands arrived at the same time. Even if you were sending your commands with real serial, you might still be disappointed in the speed of the commands. The Maestro’s PIC runs at 12 MHz, and it has lots of responsibilities other than running your script.

We don’t have documentation about MIPs for Maestro scripts because it depends on the intensity of everything else that the Maestro is doing. If you’re generating pulses for 24 servos, performing acceleration calculations for 24 servos, sending a constant stream of data in both directions on the TTL port, sending a constant stream of commands on command port, and reading data from the Maestro using the native USB interface, then your script will execute slower than normal. Anyway, it wouldn’t be MIPs: it would be more like KIPs, or a singular KIP :smiley: .

Your idea of a virtual COM port command the blocks that flow of data while for a while is interesting, and totally feasible on a USB virtual COM port, so we’ll consider it for future products.

–David

Thank you, David. I program PICs myself for a living, and I’ve always marveled at how much the Maestro is getting done handling PWM for up to 24 servos AND communicating AND running scripts AND doing housekeeping. To learn it is doing all that at 12MHz really astonishes me! Well done, you guys! I do love the product. Thanks for considering my ‘wait awhile to finish scripts’ idea. If and when you come out with that, I’ll be here to buy it! Meanwhile, I see that I’ve got 8K of scripting memory to work with. I have a few tricks in mind to try to get my ShiftBrite/ShiftBar light show up and running more or less at the same time as I’m controlling servos by using faux ‘position’ parameters as variables. It will still all happen fast enough to please the eye. Appreciate your help!

Loren