Monday, February 29, 2016

Orbital Aero Model: Body Model & Orbital Aero Model Classes

Things have been mighty busy the past month - purchasing a house across the country, prepping our current home to sell, and everything else involved with moving. On the plus side the new property has a 24x36 shop that (after some renovations) will become an excellent project space. It doesn't have electricity yet, but I'm looking forward to building a solar panel array for power.

I have a goal of at least one post per month, but I haven't had much time to devote to my projects lately (not to mention most of my tools are packed in moving boxes). So for now I'm going to jump back to the Orbital Aero Model where the posts are way behind the actual coding. Watching SpaceX land rockets and reading The Martian are good motivators as well!

TypeDefinitions.h


Defining a variable as "unsigned long long" throughout the code would be pretty awful, so I keep around a bunch of type definitions in my projects so it's easier for me to track. Nothing too amazing here, but you would need these typedefs to compile my code!

#pragma once

typedef char int8;
typedef unsigned char uint8;
typedef short int16;
typedef unsigned short uint16;
typedef int int32;
typedef unsigned int uint32;
typedef unsigned long long uint64;
typedef float float32;
typedef double double64;

Body Model


I've already gone over the VectorModel which will aid in the math routines. Next up is the BodyModel which is the basis for each simulated entity.

BodyModel.h


#pragma once

#include "math.h"
#include "TypeDefinitions.h"
#include "VectorModel.h"

class BodyModel
{
public:
BodyModel(void);
~BodyModel(void);

uint64 index; // Unique identification of this body.
bool stationary; // Whether or not this body can move in space.
bool active; // Whether or not this body is to be included in calculations.


Vector forceDirect_newtons;
Vector forceIndirect_newtons;
Vector forceTotal_newtons();

Vector location_meters;
Vector velocity_metersPerSecond;
Vector acceleration_metersPerSecond2;

double massBase_kilograms;
double massTotal_kilograms();

// 3-D shape for collision detection
double radius_meters;

// 3-D Coefficient of Drag
double dragReferenceArea_meters2;
double dragCoefficient;

// Atmosphere
int atmosphereDensityCount;
double atmosphereDensityInterval_meters;
double atmosphereMaximumAltitudeMSL_meters();
double atmosphereDensityMSL_kilogramsPerMeter3[120];
double getAtmosphereDensityMSL_kilogramsPerMeter3(double altitudeMSL_meters);

private:
};

The index variable is the unique identification of the body; no two bodies will share the same index. When forces are calculated, they are stored in the 2 force vectors: forceDirect and forceIndirect. The location, velocity, and acceleration are referenced from an arbitrary static location in the world - not any particular body in space (such as the Earth or Sun). The other fields keep track of the physical properties of the body so the physics interactions between bodies can be calculated.

BodyModel.cpp


And the function calls for that class...

#include "BodyModel.h"

// Body Constructor BodyModel::BodyModel(void)
{
this->stationary = false;
this->active = false;

this->radius_meters = 0.0;

this->atmosphereDensityCount = 0;
this->atmosphereDensityInterval_meters = 0.0;
this->atmosphereDensityMSL_kilogramsPerMeter3[0] = 0.0;

this->location_meters = 0.0;
this->velocity_metersPerSecond = 0.0;
this->acceleration_metersPerSecond2 = 0.0;

this->massBase_kilograms = 0.0;

this->dragReferenceArea_meters2 = 0.0;
this->dragCoefficient = 0.0;

return;
}

// Body Destructor BodyModel::~BodyModel(void)
{
return;
}

// Return the total force acting on the body.
Vector BodyModel::forceTotal_newtons()
{
return (this->forceIndirect_newtons + this->forceDirect_newtons);
}

// Return the total mass of the body.
double BodyModel::massTotal_kilograms()
{
return massBase_kilograms; // + fuel/payload/etc masses
}

// Get the highest altitude with significant atmosphere for this body. double BodyModel::atmosphereMaximumAltitudeMSL_meters()
{
return (double)(atmosphereDensityCount - 1) * atmosphereDensityInterval_meters;
}

// Get the atmospheric density for an altitude relative to this body. double BodyModel::getAtmosphereDensityMSL_kilogramsPerMeter3(double altitudeMSL_meters)
{
if (atmosphereDensityInterval_meters > 0.0 && altitudeMSL_meters >= 0.0)
{
// Interpolate
int altitudeIndex = (int)floor(altitudeMSL_meters / atmosphereDensityInterval_meters);
if (altitudeIndex < atmosphereDensityCount)
{
return atmosphereDensityMSL_kilogramsPerMeter3[altitudeIndex];
}
}
return 0.0;
}

Orbital Aero Model


The OrbitalAeroModel manages the physics and kinematics of all the bodies; expect this class to be majorly overhauled as the software progresses.

Of note:

  • A single "OrbtialAeroModel" class is created that hosts the entire simulation.
  • Public functions from the class are available to create bodies, remove bodies, update properties of bodies, and propogate the simulation.
  • Private functions within the class handle the physics and kinematics of the simulation as well as entity record keeping.
  • Each body has a unique 64-bit unsigned integer for identification. 18,446,744,073,709,551,616 entities should be enough ;-)


OrbitalAeroModel.h


using std::map;

class OrbitalAeroModel
{
public:
OrbitalAeroModel(void);
~OrbitalAeroModel(void);

// Body Management
uint64 createBody();
bool removeBody(uint64 index);
BodyModel &OrbitalAeroModel::getBody(uint64 index);

// Primary Update for Simulation Processing
bool update(double timeStep_seconds);

private:
// Counter to ensure unique identifications for bodies.
uint64 indexCounter;

// Table to hold all the bodies.
map <uint64, BodyModel> tableBodies;

// Specific Physics Processing
bool processIndirectForces();
bool processDirectForces();
bool processKinematics(double timeStep_seconds);
bool processCollisions(double timeStep_seconds);

// Table Management
bool doesBodyExistInTable(uint64 index);
BodyModel &getBodyFromBodiesTable(uint64 index);
bool updateBodyInBodiesTable(uint64 index, BodyModel bodyObject);
bool removeBodyFromBodiesTable(uint64 index);
bool clearBodiesTable();
};

Before I dive into the processing of the bodies, let's briefly cover the management of them.

The header defines a tableBodies which holds all bodies in the simulation. The key of the table is the unique 64-bit unsigned integer of the body, and the value of the table is a BodyModel containing all the body's data.

Tables.cpp


5 private helper functions handle the table record keeping:

// Return whether or not a given body exists in the simulation.
bool OrbitalAeroModel::doesBodyExistInTable(uint64 index)
{
// Search through the table for a given index.
if (tableBodies.find(index) != tableBodies.end())
{
// Record found, return true.
return true;
}
else
{
// Record not found, return false.
return false;
}
}

// Get a reference to a body from its unique ID.
BodyModel &OrbitalAeroModel::getBodyFromBodiesTable(uint64 index)
{
// Define an iterator to store values from the record table.
map <uint64, BodyModel>::iterator iteratorBodyObject;

// Search through the table for a given index and store in the iterator.
iteratorBodyObject = tableBodies.find(index);

// Return value portion (the record) of the table iterator.
return iteratorBodyObject->second;
}

// Update a body into the simulation.
bool OrbitalAeroModel::updateBodyInBodiesTable(uint64 index, BodyModel bodyObject)
{
// Update the table with the given record.
tableBodies[index] = bodyObject;
return true;
}

// Remove a body from the simulation.
bool OrbitalAeroModel::removeBodyFromBodiesTable(uint64 index)
{
// Remove the given index from the table.
tableBodies.erase(index);

return true;
}

// Clear all bodies in the simulation.
bool OrbitalAeroModel::clearBodiesTable()
{
// Clear the table.
tableBodies.clear();

return true;
}

OrbitalAeroModel.cpp


3 public functions gives access to the bodies:

// Create a new body to act in the simulation.
uint64 OrbitalAeroModel::createBody()
{
BodyModel thisBody;
indexCounter ++;
thisBody.index = indexCounter;
thisBody.active = true;
updateBodyInBodiesTable(thisBody.index, thisBody);
return thisBody.index;
}

// Remove a body from the simulation.
bool OrbitalAeroModel::removeBody(uint64 index)
{
if (doesBodyExistInTable(index))
{
return removeBodyFromBodiesTable(index);
}
return false;
}

// Get a simulation body.
BodyModel &OrbitalAeroModel::getBody(uint64 index)
{
return getBodyFromBodiesTable(index);
}

A single update function is called to do all processing of bodies within the simulation. A time step is passed in so it knows how far to propagate the entities. The shorter the time step the closer the simulation will match reality, but it also means it's more computational intensive.

// Primary function to process the physics and kinematics all bodies.
bool OrbitalAeroModel::update(double timeStep_seconds)
{
// It's important all forces are calculated before all positions are updated before all collisions
// are checked, etc. That way the order in which bodies are looped through does not affect how they
// affect other bodies in space.

// First calculate indirect forces, such as those from gravity and atmospheric drag.
// Currently includes the thrust of engines, but that may be separated eventually.
processIndirectForces();

// Next add frictional forces.
// This must be done after gravity/atmospheric forces to know the normal force for friction.
processDirectForces();

// Can now translate and rotate each body.
processKinematics(timeStep_seconds);

// After moving the body, check for collisions and adjust positions/velocities as appropriate.
processCollisions(timeStep_seconds);

return true;
}


Each of those process functions perform the actual physics on the bodies. Rather than try to cover them all in this post, I'll devote individual posts to each one.

To cover everything there is on the record management side, I will show how each of those process functions loops through bodies:

bool OrbitalAeroModel::process######()
{
map <uint64, BodyModel>::iterator iteratorPrimaryBody = tableBodies.begin();
while (iteratorPrimaryBody != tableBodies.end())
{
BodyModel &primaryBody = iteratorPrimaryBody->second;
if (primaryBody.active)
{
map <uint64, BodyModel>::iterator iteratorSecondaryBody = iteratorPrimaryBody;
iteratorSecondaryBody++;
while (iteratorSecondaryBody != tableBodies.end())
{
BodyModel &secondaryBody = iteratorSecondaryBody->second;
if (secondaryBody.active && (&primaryBody != &secondaryBody))
{

// Do some math!

} // ((secondaryBody.active) && (primaryBody.index != secondaryBody.index))
iteratorSecondaryBody++;
} // (iteratorSecondaryBody != tableBodies.end())
} // (primaryBody.active)
iteratorPrimaryBody++;
} // (iteratorPrimaryBody != tableBodies.end())
return true;
}

Sunday, January 10, 2016

Dynamic Board: 3x3 Prototype Blocks

I love some of the new Virtual Reality / Augmented Reality devices coming out (especially CastAR). A couple limitations with the approaches I've seen though is you can't physically touch the environment (video only) and they all require some type of goggles to be worn.

I'm sure there are products in development for special haptic gloves you could wear to simulate touch sensations, but can I build something where the real physical touch is built in? I'd like for the 3-D shape to physically emerge out of the table. In fact there have been a couple movies recently of this in action. If I can find some pictures of those, I'll update this post later with them.

All of my work so far on the servo controller has built up to this goal. Basically I want to make a table top of blocks arranged in a grid pattern that can be individually extruded a controlled distance from the surface. Each block also needs to have its color/image changeable and have some type of touch input. A computer controls the distance of the extrusion, the color/image on the block, and what to do when touch input is detected. (For any patent lovers out there, please consider the ideas in this post public domain prior art.)

To really be useful, I think it needs to have a resolution of 8x8 (Checkers/Chessboard), but I'm starting with a 3x3 prototype - just enough to play Tic Tac Toe.

I'm testing out 3 different block shapes: no taper, low taper, and high taper. If I use an overhead projection to put an image onto the blocks, then the taper will allow me to project images on the sides. I like how the "no taper" will sit flush with the table when fully down though. A collapsible block would probably be best, but that complicates things more than I'd like.


You can see how the low / no taper blocks became unstable near the end of the print and came out rough. I'm going to clean those up later.

Each block has a mount at the bottom that I can tie the servo to.


I then printed the block mount that will sit on top of my servos mount.


That print alone took 15 hours; I was so glad it came out flawless on the first try!

The blocks within the block mount on top of the servo mount...


And a side shot to see the servos beneath...


This is just a progress report. I need to actually move the block mount higher so the servos arms can clear the mount and have plenty of throw. When complete, the servos should be able to go from fully extended to fully flush with the table top.

Not including failed prints, that was 68 hours of 3-D printing!

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.