Monday, December 21, 2015

Spaceship Control Panel

My family went up to Washington State on vacation, and while there we went to a children's museum. They had a mock police car, fire truck, ambulance, and helicopter that kids could go in and play with the many controls. Some switches would turn lights on and off, others would play sounds. My son absolutely loved it. He needed one of his own. I made a big order off Adafruit the next day.

And what better thing to build than a spaceship control panel?

I want to create it such that it can be expanded in the future as he gets older. Then instead of just toggling lights and playing canned sounds, he would actually have to follow certain procedures to make things happen.

It's nowhere near this guy's creation:
https://www.raspberrypi.org/blog/mission-control-desk/
http://makezine.com/2014/06/26/making-fun-kids-room-spacecraft/
But it's sufficient for my son now, and it will get better. :-)

For this first iteration I won't get too complicated. I'll have a variety of inputs that control lights and sounds. I actually put a lot of time into which buttons and switches to have. The Apollo sim I linked to above it awesome, but I want my son to be able to replicate future space travel among the stars ("Foundation" style). So the controls I ultimately went with could be used for privateers smuggling cargo across the Confederation, battling Romulans near the neutral zone, or searching for the mythical planet Earth. At this point the buttons will repeat the same sounds every time you push them, but when my son gets older I can tie in a microcontroller to put more logic in the responses.



The Components

1 - 2'x2' 1/4" Birch Sheet (Lowes)
3 - Power On/Off Switch (Radio Shack)
2 - Red Toggle Button (Radio Shack)
2 - Protected Switch (Radio Shack)
9 - SPST 5VDC Reed Relay (Radio Shack)
6 - 16mm Illuminated Pushbutton - Red Momentary (Adafruit)
11 - Diffused White 10mm LED (Adafruit)
1 - LED Illuminated Pushbutton - 30mm Round (Adafruit)
1 - Audio FX Sound Board - WAV/OGG Trigger with 16MB Flash (Adafruit)
1 - USB Powered Speakers (Adafruit)
1 - 5V 2A (2000mA) switching power supply (Adafruit)
1 - Breadboard-friendly 2.1mm DC barrel jack (Adafruit)
1 - 5V 2A Switching Power Supply w/ USB-A Connector (Adafruit)
1 - Kindle Fire (Amazon)
1 - IKEA Side Table (IKEA)
Lots of 22 Gauge Solid Core Wire & Resistors

The Build


Measure and drill some holes... I also cut a large opening for the tablet screen...


Painted white with the controls mounted...


A spare IKEA side table was a quick way to give it a stable base.



Wiring it up...



(Yeah I know the wires are a mess. There is plenty of slack to get them tidied up, but the time to do so is limited.)



Final Product




I found a 3-D space program on the Kindle that is absolutely perfect for the control panel.

The primary and emergency light switches currently just toggle their white LEDs, but eventually I'll have them control 120V external lighting. (I've got some relays that can close a 120V A/C circuit with just 3V.)

The shield, cargo bay, and tractor beam switches toggle their corresponding white LEDs and close a relay for the Audio FX board to play an appropriate sound.

The Visual Scanner, Radar Scanner, Initiate Takeoff, and Initiate Landing buttons are all momentary buttons. While pressed, the white LED illuminates and the Audio FX board plays an appropriate sound. When released the LED extinguishes.

The weapon systems are probably the most complicated. The red momentary buttons are only illuminated (and functional) if the protected arming switches are raised. Pressing them causes some nice launch effects to be played.

The Initiate Warp Speed button is always illuminated, but it plays a nice sound effect when pressed.


There is always room for improvement, but for now my son is happy and occupied for a little longer each day! :-)

Thursday, November 12, 2015

Halloween 2015

There is decent progress on the servo controller, but this past month has been crazy busy. I've drawn up some designs in Blender that I need to 3-D print. The new parts include the upper frame of the controller and blocks the servos will manipulate.

In the mean time, another short term project popped up as a nice distraction. My son's first Halloween was coming up (technically second, but he was a bit young for the first), and he needed a costume. There's nothing better than a spaceship, so my wife and I built one...

The skeleton was built with PVC.


Some cardboard spray painted silver made up the skin.


My son is too young to hold a bag for candy, so my wife came up with the idea of a compartment under the hood of the spacecraft to be his candy bag. We cut a simple L shaped lever out of some plywood, drilled a pivot hole in the center of the lever, drilled a second hole for a string at the end of the lower leg, and mounted it underneath the hood.

The lever pivots freely about its center. We tied a string through the lower leg, brought it forward (wrapping it around the front frame), then funneled it back to the rear of the spacecraft. Now we can pull the string in the back and it forces the hood up!


No spacecraft is complete without lights, so we picked up 2 white LEDs for the front, a red LED for the left, and a green LED for the right. A little bit of soldering and the we were in business. (Not the best wiring job, but it only needed to last one night.)



Spacesuit is a requirement (he kept that helmet on just long enough to take this picture, then it was gone).


Ready to go Trick or Treating! (My wife made an outfit just for pushing around the spacecraft - she's the "flames" coming out the back.)


FYI - the name of the ship is "Far Star". Give yourself +10 geek points if you know the reference.

The bar has been set... what will we do next year?

Sunday, October 4, 2015

Servo Controller: Interface Control Document (ICD)

As I add new capabilities to the servo controller, I want to document the commands into a single Interface Control Document (ICD). I'll continue to edit this post as the project progresses to capture all the servo controller commands and responses.


Structures


' Must set Sequential with Pack = 1 so .NET creates a byte array
' with the Byte as 1 byte (and not as 2).
<StructLayout(LayoutKind.Sequential, Pack:=1)> _
Public Structure CommandSetServoValueStructure
    Public command As Byte
    Public servoNumber As UInt16
    Public newValue As UInt16

    Public Function getBytes() As Byte()
        Dim binaryBytes(5) As Byte
        Dim pointerCommand As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(Me))
        Marshal.StructureToPtr(Me, pointerCommand, False)
        Marshal.Copy(pointerCommand, binaryBytes, 0, Marshal.SizeOf(Me))
        Marshal.FreeHGlobal(pointerCommand)
        Return binaryBytes
    End Function
End Structure

struct CommandSetServoValueStructure
{
  byte     command;
  uint16_t servoNumber;
  uint16_t newValue;
  // Must denote structure as packed for variables to be
  // properly interpreted from a byte array.
} __attribute__((__packed__));


Sending Binary Commands (.NET)


Dim thisCommand As CommandSetServoValueStructure
' Fill in thisCommand as appropriate.
Dim binaryBytes() As Byte = thisCommand.getBytes
connectedSerialPort.Write(binaryBytes, 0, 5)

where 5 is the size of the structure


Binary Commands


To enter binary mode, send a value of 128 after the controller starts up.

Clear Command

Send an array of at least five 0s to force the microcontroller to fulfill any existing inputs and be ready for new input. Sending additional 0s will keep the controller in a cleared state.
Outgoing Data:
0
Response Data:
<none>

Set Servo Position in Percent

Call to set a servo's position in percent.
Outgoing Data:
CommandSetServoValueStructure
CommandSetServoValueStructure.command = 1
CommandSetServoValueStructure.servoNumber = <Index of Servo to Change>
CommandSetServoValueStructure.newValue = <0 to 100>
Response Data:
<none>

Set Servo Upper Limit in Pulses

Call to set a servo's upper limit in pulses. If sending too high then that can permanently damage the servo.
Outgoing Data:
CommandSetServoValueStructure
CommandSetServoValueStructure.command = 2
CommandSetServoValueStructure.servoNumber = <Index of Servo to Change>
CommandSetServoValueStructure.newValue = <0 to 4096>
Response Data:
<none>

Set Servo Lower Limit in Pulses

Call to set a servo's lower limit in pulses. If sending too low then that can permanently damage the servo.
Outgoing Data:
CommandSetServoValueStructure
CommandSetServoValueStructure.command = 3
CommandSetServoValueStructure.servoNumber = <Index of Servo to Change>
CommandSetServoValueStructure.newValue = <0 to 4096>
Response Data:
<none>


ASCII Commands


To enter ASCII mode, send a value of 10 (new line) after the controller starts up.

Set Servo

Select which servo to control.
Outgoing Data:
s=<Index of Servo to Change>
Response Data:
Debug Servo:<Index of Servo to Change>

Set Servo Position in Percent

Call to set a servo's position in percent. Must use "Set Servo" command first to specify which servo to control.
Outgoing Data:
p=<Percent Value to Set>
Response Data:
Set Percent:<Percent Value to Set>

Set Servo Position in Pulses

Call to set a servo's position in pulses. Must use "Set Servo" command first to specify which servo to control.
Outgoing Data:
v=<Pulse Value to Set>
Response Data:
Set pulse:<Pulse Value to Set>

Set Upper Limit in Pulses

Call to set a servo's upper limit in pulses. If sending too high then that can permanently damage the servo. Must use "Set Servo" command first to specify which servo to control.
Outgoing Data:
u=<Pulse Value to Set>
Response Data:
Set upper pulse:<Pulse Value to Set>

Set Lower Limit in Pulses

Call to set a servo's lower limit in pulses. If sending too low then that can permanently damage the servo. Must use "Set Servo" command first to specify which servo to control.
Outgoing Data:
l=<Pulse Value to Set>
Response Data:
Set lower pulse:<Pulse Value to Set>



Copyright (c) 2015 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Saturday, October 3, 2015

Servo Controller: Binary Serial Communication (Computer Side)

My previous post discussed the receiving end of the serial communications; now I'll cover the sending side.

My starting point was my Servo Control via .NET application described a couple posts ago. It already had the framework for sending and receiving over the serial port, so we just need to make the adjustments for binary.


First to change is the UI. The text input field and send button has been replaced by 3 buttons, 9 track bars, and 18 numeric up/down controls.

The buttons are named as follows:
btnAllDown, btnAllMiddle, btnAllUp

The track bars are named as follows (range 0 to 100):
trackBar11, trackBar12, trackBar13, trackBar21, trackBar22, trackBar23, trackBar31, trackBar32, trackBar33

The numeric up/downs are named as follows (range 0 to 4096):
nudServoLowerLimit11, nudServoLowerLimit12, nudServoLowerLimit13, nudServoLowerLimit21, nudServoLowerLimit22, nudServoLowerLimit23, nudServoLowerLimit31, nudServoLowerLimit32, nudServoLowerLimit33
nudServoUpperLimit11, nudServoUpperLimit12, nudServoUpperLimit13, nudServoUpperLimit21, nudServoUpperLimit22, nudServoUpperLimit23, nudServoUpperLimit31, nudServoUpperLimit32, nudServoUpperLimit33

Next I defined the structure that will be sent over the serial line. This structure must match the one on the receiving end. Since this structure will be reused for several commands, I have the first byte be the command and a built-in getBytes() function for convenience.

Warning! The Marshal commands in .NET will create a byte array for a given structure. However, by default the byte array created will reserve 2 bytes for any variables listed as a Byte (the structure below would be 6 bytes in length). For it to be compatible with the Arduino side of the communication, we must tag the structure "LayoutKind.Sequential, Pack :=1". Now a Byte variable will be treated as 1 byte and the structure returned will be 5 bytes in length.

    ' Must set Sequential with Pack = 1 so .NET creates a byte array
    ' with the Byte as 1 byte (and not as 2).
    <StructLayout(LayoutKind.Sequential, Pack:=1)> _
    Public Structure CommandSetServoValueStructure
        Public command As Byte
        Public servoNumber As UInt16
        Public newValue As UInt16

        Public Function getBytes() As Byte()
            Dim binaryBytes(5) As Byte
            Dim pointerCommand As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(Me))
            Marshal.StructureToPtr(Me, pointerCommand, False)
            Marshal.Copy(pointerCommand, binaryBytes, 0, Marshal.SizeOf(Me))
            Marshal.FreeHGlobal(pointerCommand)
            Return binaryBytes
        End Function
    End Structure

Each command will fill out the values of this structure. Sending it over the network is then a simple:

            Dim thisCommand As CommandSetServoValueStructure
            ' Fill in thisCommand as appropriate.
            Dim binaryBytes() As Byte = thisCommand.getBytes
            connectedSerialPort.Write(binaryBytes, 0, 5)

Write takes in the bytes to send, the starting index, and the number of bytes to send - which in our cause is the size of our structure.

Setting Servo Position



Command 1: Setting the Servo Position (in percent)


Each track bar sets the servo's position by percentage (from 0 to 100).

    Private Sub sendCommandSetServoPercentToSerial(ByVal servoNumber As Integer, ByVal value_percent As Integer)
        Try
            Dim commandSetServoPercent As CommandSetServoValueStructure
            commandSetServoPercent.command = 1
            commandSetServoPercent.servoNumber = servoNumber
            commandSetServoPercent.newValue = value_percent

            Dim binaryBytes() As Byte = commandSetServoPercent.getBytes
            connectedSerialPort.Write(binaryBytes, 0, 5)
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar11_ValueChanged(sender As Object, e As EventArgs) Handles trackBar11.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(0, trackBar11.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar21_ValueChanged(sender As Object, e As EventArgs) Handles trackBar21.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(1, trackBar21.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar31_ValueChanged(sender As Object, e As EventArgs) Handles trackBar31.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(2, trackBar31.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar12_ValueChanged(sender As Object, e As EventArgs) Handles trackBar12.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(3, trackBar12.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar22_ValueChanged(sender As Object, e As EventArgs) Handles trackBar22.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(4, trackBar22.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar32_ValueChanged(sender As Object, e As EventArgs) Handles trackBar32.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(5, trackBar32.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar13_ValueChanged(sender As Object, e As EventArgs) Handles trackBar13.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(6, trackBar13.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar23_ValueChanged(sender As Object, e As EventArgs) Handles trackBar23.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(7, trackBar23.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub trackBar33_ValueChanged(sender As Object, e As EventArgs) Handles trackBar33.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoPercentToSerial(8, trackBar33.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

Setting Servo Range



Command 2: Setting the Servo Upper Limit


The top numeric up/down sets the upper range for the servo.

    Private Sub sendCommandSetServoUpperLimitToSerial(ByVal servoNumber As Integer, ByVal newUpper_pulseLength As Integer)
        Try
            Dim commandSetServoUpperLimit_pulseLength As CommandSetServoValueStructure
            commandSetServoUpperLimit_pulseLength.command = 2
            commandSetServoUpperLimit_pulseLength.servoNumber = servoNumber
            commandSetServoUpperLimit_pulseLength.newValue = newUpper_pulseLength

            Dim binaryBytes() As Byte = commandSetServoUpperLimit_pulseLength.getBytes
            connectedSerialPort.Write(binaryBytes, 0, 5)
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit11_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit11.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(0, nudServoUpperLimit11.Value)
                sendCommandSetServoPercentToSerial(0, trackBar11.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit21_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit21.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(1, nudServoUpperLimit21.Value)
                sendCommandSetServoPercentToSerial(1, trackBar21.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit31_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit31.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(2, nudServoUpperLimit31.Value)
                sendCommandSetServoPercentToSerial(2, trackBar31.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit12_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit12.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(3, nudServoUpperLimit12.Value)
                sendCommandSetServoPercentToSerial(3, trackBar12.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit22_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit22.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(4, nudServoUpperLimit22.Value)
                sendCommandSetServoPercentToSerial(4, trackBar22.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit32_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit32.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(5, nudServoUpperLimit32.Value)
                sendCommandSetServoPercentToSerial(5, trackBar32.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit13_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit13.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(6, nudServoUpperLimit13.Value)
                sendCommandSetServoPercentToSerial(6, trackBar13.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit23_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit23.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(7, nudServoUpperLimit23.Value)
                sendCommandSetServoPercentToSerial(7, trackBar23.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoUpperLimit33_ValueChanged(sender As Object, e As EventArgs) Handles nudServoUpperLimit33.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoUpperLimitToSerial(8, nudServoUpperLimit33.Value)
                sendCommandSetServoPercentToSerial(8, trackBar33.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

Command 3: Setting the Servo Lower Limit


The bottom numeric up/down sets the lower range for the servo.

    Private Sub sendCommandSetServoLowerLimitToSerial(ByVal servoNumber As Integer, ByVal newLower_pulseLength As Integer)
        Try
            Dim commandSetServoLowerLimit_pulseLength As CommandSetServoValueStructure
            commandSetServoLowerLimit_pulseLength.command = 3
            commandSetServoLowerLimit_pulseLength.servoNumber = servoNumber
            commandSetServoLowerLimit_pulseLength.newValue = newLower_pulseLength

            Dim binaryBytes() As Byte = commandSetServoLowerLimit_pulseLength.getBytes
            connectedSerialPort.Write(binaryBytes, 0, 5)
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit11_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit11.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(0, nudServoLowerLimit11.Value)
                sendCommandSetServoPercentToSerial(0, trackBar11.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit21_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit21.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(1, nudServoLowerLimit21.Value)
                sendCommandSetServoPercentToSerial(1, trackBar21.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit31_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit31.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(2, nudServoLowerLimit31.Value)
                sendCommandSetServoPercentToSerial(2, trackBar31.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit12_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit12.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(3, nudServoLowerLimit12.Value)
                sendCommandSetServoPercentToSerial(3, trackBar12.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit22_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit22.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(4, nudServoLowerLimit22.Value)
                sendCommandSetServoPercentToSerial(4, trackBar22.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit32_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit32.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(5, nudServoLowerLimit32.Value)
                sendCommandSetServoPercentToSerial(5, trackBar32.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit13_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit13.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(6, nudServoLowerLimit13.Value)
                sendCommandSetServoPercentToSerial(6, trackBar13.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit23_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit23.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(7, nudServoLowerLimit23.Value)
                sendCommandSetServoPercentToSerial(7, trackBar23.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub nudServoLowerLimit33_ValueChanged(sender As Object, e As EventArgs) Handles nudServoLowerLimit33.ValueChanged
        Try
            If Not isPopulatingSettings Then
                sendCommandSetServoLowerLimitToSerial(8, nudServoLowerLimit33.Value)
                sendCommandSetServoPercentToSerial(8, trackBar33.Value)
            End If
        Catch ex As Exception
        End Try
    End Sub



Set All


The set all buttons are just shortcuts for changing every track bar to 0%, 50%, or 100%.


    Private Sub setAll(ByVal newValue_percent As Integer)
        Try
            trackBar11.Value = newValue_percent
            trackBar21.Value = newValue_percent
            trackBar31.Value = newValue_percent
            trackBar12.Value = newValue_percent
            trackBar22.Value = newValue_percent
            trackBar32.Value = newValue_percent
            trackBar13.Value = newValue_percent
            trackBar23.Value = newValue_percent
            trackBar33.Value = newValue_percent
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnAllDown_Click(sender As Object, e As EventArgs) Handles btnAllDown.Click
        Try
            setAll(0)
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnAllMiddle_Click(sender As Object, e As EventArgs) Handles btnAllMiddle.Click
        Try
            setAll(50)
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnAllUp_Click(sender As Object, e As EventArgs) Handles btnAllUp.Click
        Try
            setAll(100)
        Catch ex As Exception
        End Try
    End Sub

Additional Support Code


Imports System
Imports System.IO.Ports
Imports System.Runtime.InteropServices

Public Class FormMain

    Private _isInitializingForm As Boolean = True
    Private _isLoadingConfiguration As Boolean = False

    Public ReadOnly Property isPopulatingSettings As Boolean
        Get
            If _isInitializingForm Then
                Return True
            End If
            If _isLoadingConfiguration Then
                Return True
            End If
            Return False
        End Get
    End Property

    ''' <summary>
    ''' Definite a delegate to transfer incoming data from the background
    ''' (serial) thread to the foreground (UI) thread.
    ''' </summary>
    ''' <param name="incomingBuffer"></param>
    ''' <remarks></remarks>
    Private Delegate Sub ProcessIncomingTextDelegate(ByVal incomingBuffer As String)

    ''' <summary>
    ''' The serial port object that will communicate.
    ''' </summary>
    ''' <remarks></remarks>
    Private WithEvents connectedSerialPort As New SerialPort()

    Public Sub New()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        _isInitializingForm = False
    End Sub

    Private Sub loadConfigurationValues()
        Try
            _isLoadingConfiguration = True

            Try
                nudServoUpperLimit11.Value = 550
                nudServoUpperLimit12.Value = 550
                nudServoUpperLimit13.Value = 550
                nudServoUpperLimit21.Value = 550
                nudServoUpperLimit22.Value = 550
                nudServoUpperLimit23.Value = 550
                nudServoUpperLimit31.Value = 550
                nudServoUpperLimit32.Value = 550
                nudServoUpperLimit33.Value = 550

                nudServoLowerLimit11.Value = 200
                nudServoLowerLimit12.Value = 200
                nudServoLowerLimit13.Value = 200
                nudServoLowerLimit21.Value = 200
                nudServoLowerLimit22.Value = 200
                nudServoLowerLimit23.Value = 200
                nudServoLowerLimit31.Value = 200
                nudServoLowerLimit32.Value = 200
                nudServoLowerLimit33.Value = 200
            Catch ex As Exception
            End Try

            _isLoadingConfiguration = False
        Catch ex As Exception
        End Try
    End Sub

    Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Try
            loadConfigurationValues()

            refreshPortsList()

            If ddlCOMPort.SelectedIndex >= 0 Then
                ' Port is found, attempt to connect to it.
                connectSerialPort()
            Else
                ' Update which controls are enabled (so disconnect button is disabled at startup).
                updateConnectedEnableFields()
            End If
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnConnect_Click(sender As Object, e As EventArgs) Handles btnConnect.Click
        Try
            connectSerialPort()
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnDisconnect_Click(sender As Object, e As EventArgs) Handles btnDisconnect.Click
        Try
            disconnectSerialPort()
        Catch ex As Exception
        End Try
    End Sub

    Private Sub connectedSerialPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles connectedSerialPort.DataReceived
        Try
            ' Received data from the serial port, send it for processing.
            processIncomingData(connectedSerialPort.ReadExisting())
        Catch ex As Exception
        End Try
    End Sub

    Private Sub connectedSerialPort_ErrorReceived(sender As Object, e As SerialErrorReceivedEventArgs) Handles connectedSerialPort.ErrorReceived
        Try
        Catch ex As Exception
        End Try
    End Sub

    Private Sub btnRefreshPorts_Click(sender As Object, e As EventArgs) Handles btnRefreshPorts.Click
        Try
            refreshPortsList()
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Returns if the serial port is currently connected.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private ReadOnly Property isSerialPortConnected
        Get
            Return connectedSerialPort.IsOpen()
        End Get
    End Property

    ''' <summary>
    ''' Update whether or not each UI control should be enabled based on
    ''' the connection state.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub updateConnectedEnableFields()
        Try
            Dim isConnected As Boolean = isSerialPortConnected
            btnConnect.Enabled = Not isConnected
            btnDisconnect.Enabled = isConnected
            ddlCOMPort.Enabled = Not isConnected
            btnRefreshPorts.Enabled = Not isConnected
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Connect to the selected serial port.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub connectSerialPort()
        Try
            If ddlCOMPort.SelectedIndex >= 0 Then
                connectedSerialPort.PortName = ddlCOMPort.SelectedItem
                connectedSerialPort.BaudRate = 9600

                connectedSerialPort.WriteTimeout = -1
                connectedSerialPort.ReadTimeout = -1

                connectedSerialPort.ReadBufferSize = 4096
                connectedSerialPort.WriteBufferSize = 2048

                connectedSerialPort.Parity = IO.Ports.Parity.None
                connectedSerialPort.StopBits = IO.Ports.StopBits.One
                connectedSerialPort.DataBits = 8
                connectedSerialPort.Open()

                'Threading.Thread.Sleep(1000)

                ' Send Extended ASCII Code 128 for binary mode.
                Dim binaryBytes() As Byte = {128}
                connectedSerialPort.Write(binaryBytes, 0, 1)
            End If
            updateConnectedEnableFields()
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Disconnect the serial port.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub disconnectSerialPort()
        Try
            connectedSerialPort.Close()
            updateConnectedEnableFields()
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Detect available serial ports and update the COM port list.
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub refreshPortsList()
        Try
            ' Keep track of the selected port so we can reselect it after fresh.
            Dim currentlySelectedPort As String = ddlCOMPort.SelectedItem

            ' Get the available ports and update the dropdown list.
            Dim availablePorts As Array = IO.Ports.SerialPort.GetPortNames()
            ddlCOMPort.Items.Clear()
            ddlCOMPort.Items.AddRange(availablePorts)

            ' Reselect the previously selected port as a default.
            If currentlySelectedPort IsNot Nothing Then
                If ddlCOMPort.Items.Contains(currentlySelectedPort) Then
                    ddlCOMPort.SelectedItem = currentlySelectedPort
                End If
            End If

            If ddlCOMPort.SelectedIndex = -1 AndAlso ddlCOMPort.Items.Count > 0 Then
                ' No port selected but one is available, default to it.
                ddlCOMPort.SelectedIndex = 0
            End If
        Catch ex As Exception
        End Try
    End Sub

    ''' <summary>
    ''' Function to process incoming data from the serial port.
    ''' </summary>
    ''' <param name="incomingBuffer"></param>
    ''' <remarks></remarks>
    Private Sub processIncomingData(ByVal incomingBuffer As String)
        Try
            If txtReceive.InvokeRequired Then
                ' On background (serial port) thread. Invoke a delegate to call this
                ' function again on the foreground (UI) thread.
                Dim incomingTextDelegate As New ProcessIncomingTextDelegate(AddressOf processIncomingData)
                Me.Invoke(incomingTextDelegate, New Object() {incomingBuffer})
            Else
                ' On foreground (UI) thread. Process the message.
                txtReceive.Text += incomingBuffer
            End If
        Catch ex As Exception
        End Try
    End Sub

   ' ---------- CODE FROM ABOVE ----------

End Class

In Operation


At startup...

The Board Manager automatically connects to the first COM port it detects and sends the binary input command.


Setting some of the servo values...


And the servos in action...





Copyright (c) 2015 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Wednesday, September 23, 2015

Servo Controller: Binary Serial Communication (Arduino Side)

ASCII vs Binary


ASCII text communication is convenient for debugging, but it's not particular efficient. The microcontroller is having to take a string of characters, convert those strings into numbers, then process the instructions of those numbers.

For example, set servo 12 to a value of 50 percent:
s=12
p=50
Total 10 bytes as ASCII (including carriage returns).

Binary communication is much more efficient. A pointer can be used to interpret the array of bytes as a structure, so there is no CPU expensive conversions from strings to numbers. And where ASCII would take 5 bytes to represent 65535, binary would need just 2.

Let's define a structure for commands...

const int16_t commandLength_bytes = 5;

struct CommandSetServoValueStructure
{
  byte     command;
  uint16_t servoNumber;
  uint16_t newValue;
  // Must denote structure as packed for variables to be
  // properly interpreted from a byte array.
} __attribute__((__packed__));

We'll say command 1 is to set a percent value.
So now the byte array looks like:
[1][12][0][50][0]
Total 5 bytes as binary.

5 bytes instead of 10!

But let's make the example more interesting. We'll say command 2 is to set an upper pulse value. We want to control servo 10,000 to a pulse value of 1100.

That byte array looks like:
[2][16][39][76][4]
Total 5 bytes as binary.

As a string it would have been:
s=10000
u=1100
Total 15 bytes as ASCII (includes carriage returns).

5 bytes instead of 15!

Now imagine we were sending 10,000 servo commands, saving 10 bytes per command. 100 KB is a nice savings over a serial cable.

That saves on the amount of data we're sending over, but what about interpreting the data? String manipulation is a (relatively) slow process. In C, you can use a pointer to interpret that array of bytes as whatever structure you wish.

Say the incoming data is stored in an array...

byte *incomingCommand;

Use a pointer to interpret that array of bytes as our desired structure...

CommandSetServoValueStructure *commandSetServoPercent = (CommandSetServoValueStructure *)incomingCommand;

Then you can dereference the values from the structure to read them...

commandSetServoPercent->servoNumber
commandSetServoPercent->newValue

Reading Binary


In the examples above I listed out the byte array for values such as 12 - [12][0] and 10,000 - [16][39]. But why is it [12][0] and not [0][12] and where do you get 16 and 39 from 10,000?

Byte Order

With a variable consisting of more than 1 byte, there are two ways it can be ordered:
[Byte 1][Byte 2][Byte 3][Byte 4]
or
[Byte 4][Byte 3][Byte 2][Byte 1]

The order is known as Endianness. https://en.wikipedia.org/wiki/Endianness

The computer your code is running on could order variables either way. It doesn't matter which order is used, just so long as both ends of the communication use the same one. Swapping the order of the bytes is trivial, and there are plenty of examples around on how to do that. Since my .NET application and my Arduino use the same byte order, I didn't have to do any byte swapping.

Parsing a Number with >1 Bytes

Windows Calculator has a Programmer mode (under the View menu) that comes in very handy.

I've switched to Programmer mode, selected "Dec", and typed in 10000.

I then opened 2 more Calculators; entering the "more significant" byte into one and the "less significant" byte into the other. Now you can see where the 16 and 39 mentioned earlier come from!


Dual Input Modes


Rather than lose the ASCII debugging mode, I'm going to modify the code to support both kinds of input. When the microcontroller first starts, if the user presses the ENTER key (new line), then it goes into ASCII mode. If the program sends a character code of 128 (an extended code a user is unlikely to type), then it goes into binary mode.

Define an enumeration for the input modes and create a variable to keep track of what we're in...

enum inputModes
{
  InputModeUndefined = 0,
  InputModeASCII = 1,
  InputModeBinary = 2
};

int16_t inputMode = InputModeUndefined;

The main program loop of the Arduino continuously checks if data is available over the Serial line. If the input mode has never been defined, then it calls getSerialInputMode() to determine which input it is in. After the input mode has been determined, it will route all incoming Serial input to the appropriate parser - getSerialASCIICommand() or getSerialBinaryCommand().

void loop()
{
  // First input will be the mode to be in: binary or text
  // Once the flag is set we'll send the serial input to the
  // appropriate processor.
  if (Serial.available() > 0)
  {
    switch (inputMode)
    {
      case InputModeUndefined:
        getSerialInputMode();
        break;
      case InputModeASCII:
        getSerialASCIICommand();
        break;
      case InputModeBinary:
        getSerialBinaryCommand();
        break;
    }
  }
  return;
}

void getSerialInputMode()
{
  int16_t incomingByte = Serial.read();
  switch (incomingByte)
  {
    case 10: // New Line
      Serial.println("ASCII Input Mode");
      inputMode = InputModeASCII;
      break;
    case 128: // Extended ASCII Code
      Serial.println("Binary Input Mode");
      inputMode = InputModeBinary;
      break;
    default:
      Serial.println("Send 'New Line' for ASCII Input Mode.");
      break;
  }
  return;
}

The getSerialASCIICommand() function is basically how the commands were parsed before.

void getSerialASCIICommand()
{
  String serialInputString = Serial.readStringUntil('\n');

  // Echo back the command.
  Serial.println(serialInputString);
  
  if (serialInputString.startsWith("s=")) // Servo
  {
    String servoNumberString = serialInputString.substring(2);
    debugServoNumber = servoNumberString.toInt();
    Serial.print("Debug servo:");
    Serial.println(debugServoNumber);
  }
  else if (serialInputString.startsWith("p=")) // Percent Value
  {
    String valuePercentString = serialInputString.substring(2);
    float newValue_percent = valuePercentString.toFloat();
    servos[debugServoNumber].setToValue_percent(newValue_percent);
    Serial.print("Set percent:");
    Serial.println(newValue_percent);
  }
  else if (serialInputString.startsWith("v=")) // Pulse Value
  {
    String pulseValueString = serialInputString.substring(2);
    uint16_t newPulseValue = pulseValueString.toInt();
    servos[debugServoNumber].setToValue_pulseLength(newPulseValue);
    Serial.print("Set pulse:");
    Serial.println(newPulseValue);
  }
  else if (serialInputString.startsWith("u=")) // Upper Pulse Value
  {
    String upperPulseValueString = serialInputString.substring(2);
    uint16_t newUpperPulseValue = upperPulseValueString.toInt();
    servos[debugServoNumber].setUpperPulseLength(newUpperPulseValue);
    servos[debugServoNumber].setToMaximumValue();
    Serial.print("Set upper pulse:");
    Serial.println(newUpperPulseValue);
  }
  else if (serialInputString.startsWith("l=")) // Lower Pulse Value
  {
    String lowerPulseValueString = serialInputString.substring(2);
    uint16_t newLowerPulseValue = lowerPulseValueString.toInt();
    servos[debugServoNumber].setLowerPulseLength(newLowerPulseValue);
    servos[debugServoNumber].setToMinimumValue();
    Serial.print("Set lower pulse:");
    Serial.println(newLowerPulseValue);
  }
  return;
}

The getSerialBinaryCommand() is the new code. We'll define a byte array and a variable to keep track of where we are in that array.

byte incomingBytes[commandLength_bytes];
int16_t incomingBytePosition = 0;

Upon receiving a byte over the Serial line, it stores it in the byte array and increments the position. The very first byte of the array is the command. So once we get that first byte, we can determine if we've received enough bytes for our structure and then process as appropriate. For now all the commands use the same structure and thus are the same length. However, that will very likely change in the future with more advanced commands.

void getSerialBinaryCommand()
{
  incomingBytes[incomingBytePosition] = Serial.read();
  incomingBytePosition += 1;

  switch(incomingBytes[0])
  {
    case 0:
      // Reset Commands
      // If the controller gets in an unsynchronized state with the
      // serial commands, then it can send a series of all 0s to
      // reset to a new command. The number of 0s required would vary
      // by the size of the command structure the controller thinks
      // we're populating (and how far into that structure it thinks
      // we have populated). Once we get out of populating the structure,
      // the subsequent "0" bytes just continue to fall into this code
      // and are handled fine.

      // Reset incoming array position for next command.
      incomingBytePosition = 0;
      break;
    case 1:
      if (incomingBytePosition >= commandLength_bytes)
      {
        processCommandSetServoPercent(&incomingBytes[0]);
        // Reset incoming array position for next command.
        incomingBytePosition = 0;
      }
      break;
    case 2:
      if (incomingBytePosition >= commandLength_bytes)
      {
        processCommandSetServoUpperLimit(&incomingBytes[0]);
        // Reset incoming array position for next command.
        incomingBytePosition = 0;
      }
      break;
    case 3:
      if (incomingBytePosition >= commandLength_bytes)
      {
        processCommandSetServoLowerLimit(&incomingBytes[0]);
        // Reset incoming array position for next command.
        incomingBytePosition = 0;
      }
      break;
  }
  
  return;
}

Processing each command is very efficient: Interpret the incoming bytes as a structure, then read the values.

void processCommandSetServoPercent(byte *incomingCommand)
{
  CommandSetServoValueStructure *commandSetServoPercent = (CommandSetServoValueStructure *)incomingCommand;
  if (commandSetServoPercent->servoNumber >= 0 && 
      commandSetServoPercent->servoNumber < numberOfServos)
  {
    servos[commandSetServoPercent->servoNumber].setToValue_percent((float)commandSetServoPercent->newValue);
  }
  return;
}

void processCommandSetServoUpperLimit(byte *incomingCommand)
{
  CommandSetServoValueStructure *commandSetServoUpperLimit = (CommandSetServoValueStructure *)incomingCommand;
  if (commandSetServoUpperLimit->servoNumber >= 0 && 
      commandSetServoUpperLimit->servoNumber < numberOfServos)
  {
    servos[commandSetServoUpperLimit->servoNumber].setUpperPulseLength((float)commandSetServoUpperLimit->newValue);
  }
  return;
}

void processCommandSetServoLowerLimit(byte *incomingCommand)
{
  CommandSetServoValueStructure *commandSetServoLowerLimit = (CommandSetServoValueStructure *)incomingCommand;
  if (commandSetServoLowerLimit->servoNumber >= 0 && 
      commandSetServoLowerLimit->servoNumber < numberOfServos)
  {
    servos[commandSetServoLowerLimit->servoNumber].setLowerPulseLength((float)commandSetServoLowerLimit->newValue);
  }
  return;
}

Copyright (c) 2015 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Saturday, August 29, 2015

Servo Controller: Test Stand

I've done a lot of work on my servo controller project, but I don't have much to say. Luckily, I have progress pictures. :-)

I designed and 3-D printed a test stand for 9 servos.

All servos low...



All servos neutral...



All servos high...



Currently in the works is binary communication with the Arduino to efficiently give servo commands. Once that is complete, I'll begin thorough testing to decide which servo is best for my application.