Over the past few months, I’ve largely been blogging about how to use various cloud services. This is typically the stuff that you could be using in a commercial setting.
Programming is something that can be a hobby for some people, not just a career skill. For this article, I thought we should do something a little different and just play with code for fun. I’ve chosen to look at something that is IoT related and produces “physically visible” results.
We’re going to experiment with a small piece of kit called the Line-us which originated from a 2017 Kickstarter campaign.
Line-us is a device that holds a pen on the end of a small robotic arm and draws doodle-style sketches. The device has built in Wifi and internet-connectivity, making it easy to connect to.
What will we be doing?
For this project, we’re going to write a .NET Core Console App that converts an SVG vector image into G-Code.
G-Code is a simple instruction language typically used by industrial CNC machines.
We’ll transmit the G-Code instructions, using TCP networking, to the Line-us and sit back as a drawing appears on a piece of paper!
Prerequisites
A Line-us robot
There will be an assumption that you have familiarity working with:
This project uses .NET Core 2.x, so you’ll need to have installed the .NET Core 2.x SDK, as appropriate to your platform. .NET Core is cross-platform, which means this project will work on Windows, Mac and Linux.
You’ll need something to edit and build your project. This demo was primarily created using .NET Core 2.2 on a Windows 10 system using Visual Studio 2019 Community edition. There is no reason why you cannot use other platforms, code editors and CLI tools - my recommendation would be to use Visual Studio Code.
You’ll need to clone the sample code that accompanies this article from here : https://github.com/SiliconOrchid/DrawWithLineUs
The road from photo to robot-drawn sketch
The Line-us is not a printer. It is a line-drawing machine, more akin to a plotter. Everything will be monochromatic and based purely on strokes of a pen on a page.
Your source material will still most likely be a bitmapped image, but there are a number of transformations that this image will need to undergo, before it can be sketched on paper by the Line-us.
Let’s overview the steps we need to take:
First, an image needs to be processed into an approximation of a black-and-white line drawing.
You can use an online utility or a photo-editing program for this step.
A quick search for “online photo to line-art” will return a wide selection of free online conversion tools. I tried both RapidResizer Stencil Maker and Snaptouch Sketch with good results.
I also used Adobe Photoshop to process a photo. If using a photo-editor, I suggest:
- start by adjusting the image contrast heavily
- then apply an “edge-detection” filter.
Next, the processed bitmap needs to be converted into an SVG vector image.
I used the open-source utility Potrace. Be mindful that Potrace only accepts images in the .BMP file format, so you will need to export your modified artwork into this format.
- Using Potrace, I used the command
potrace.exe -s lovebird_processed.bmp
, where the-s
switch is used to declare that you want to output an .SVG file.
- Using Potrace, I used the command
Finally, the SVG needs to be converted into G-Code.
Our project will not create a solution that addresses the first two items in the above list - but we will write code to read an SVG file.The following image is a photo of the actual hardcopy that is produced by the Line-us!
The Line-us coordinate system
Line-us has a small robotic arm, which holds a pen at its end. It can draw on a sheet of paper which is clamped immediately adjacent to the body of the device.
The robotic arm can position the pen in 2-axes of movement (X and Y coordinates). The pen can also be raised or lowered (the Z coordinate) by way of a cam mechanism that moves the body of the unit up and down.
The arm only has a limited range of reach, meaning that although the device can draw in a sweeping arc around the body, the available “rectangular” area is limited to something about the size of a postcard.
The coordinate system originates at the fulcrum of the robotic arm and covers the following range of values :-
- X-axis: 0 to 2000 (with only 650 to 1775 in the main drawable area)
- Y-axis: -1600 to 1600 (with only -1000 to 1000 in the main drawable area)
- Z-axis 0 to 1000 (with 0 being pen-fully-down and 1000 being pen-fully-up
According to the documentation, each 100 units corresponds to 5mm of travel.
The following diagram has been duplicated from the Line-us documentation on GitHub and clearly illustrates the range of motion and the drawable area:-
What is G-Code?
G-Code is typically used to instruct CNC machines (computer-controlled lathes and cutters, etc).
G-Code started life in the 1950’s, became standardised in the 1970’s and has more recently evolved to support more complex logic, that is required by increasingly advanced industrial machines.
The Line-us however, is a relatively straightforward device and does not have a complex set of drawing features or commands.
The Line-us firmware only supports a small set of G-Code commands, which you can find here:
The “language” of the commands is straightforward - for example, if we wanted to reset the pen to a “home” position, we need to send the device nothing other than the command “G28”.
In this project, we’re going to use a subset of just two commands! These are:-
G00
- Rapid Repositioning - Moves the pen as quickly as possible to new coordinates. In our code, we use this to reposition the pen after a line has been drawn, ready to start drawing the next one. ExampleG00 X1000 Y1000 Z1000
G01
- Linear Interpolation - This is what we use to draw a line between two points. ExampleG00 X1200 Y1200 Z0
A limitation to be aware of, is that the Line-us provides no native support for drawing true curves (usually described as beziers). The Line-us can only draw straight lines, directly from one point to another.
In practice however, this doesn’t seem to pose a problem for our project, as many SVG source-images will be created from relatively small shapes, meaning that the lack of true curves isn’t really apparent. Also, remember that this is a fun toy for drawing “wonky” pictures - it’s not meant to be perfect.
Manually test Line-us
Before we get stuck into any coding, we should ensure that our Line-us is working correctly and that we can communicate with it from your computer.
We’ll check this by performing a manual test, which will verify that you have a working network connection and that our Line-us is responding to commands as expected.
We’re not going to cover the setup and configuration of the Line-us device itself in this article, as there is already ample support provided by the manufacturers documentation and a community forum. For this article, we will assume that we have already successfully added the device to our WiFi network.
We need to know what the local IP address of the Line-us is, so if you haven’t already, connect to your network router and make a note of the IP that has been assigned.
Introducing and setting-up Telnet
We’re going to use Telnet to communicate with the Line-us during our manual test.
Younger readers, in particular, may not be familiar with Telnet, so briefly, Telnet is one of the oldest protocols on the Internet. It is used to connect to a remote system, over TCP, and provides a bi-directional text-based interface. You tend not to find it used much these days, as it is insecure and has been replaced with SSH
To use Telnet, you will need a Telnet client installed on your system.
- Telnet for Windows users
A Telnet client is included with Windows, but nowadays is disabled by default. To use it, we’ll need to re-enable it.
- In the Windows Start search bar, type “turn win” and select the “Turn Windows Features On or Off” option.
- In the “Windows Features” window that opens, scroll down the list and enable the “Telnet Client” option.
- Windows will install the feature and Telnet will now be available on the command prompt.
- Telnet for Mac users
The Telnet Client has been removed from recent versions of MacOs.
- Follow the instructions in this guide to install Telnet on a Mac.
When I first encountered Telnet as a teenager, I would use it to connect to “MUDs” (multi-user dungeons) which were online multi-user text-based adventure games!
Connect to the Line-us
With the Telnet client installed, open a command prompt and enter the following command. You will need to substitute the local IP address in the example, with the one you identified from your own router:
Telnet 192.168.1.200 1337
The number 1337
is the port number that Line-us uses by default and must be included.
If the connection is successful, you will hear/see the robot twitch its arm and a message similar to the following will be returned to the console:
hello VERSION:"3.0.0 Feb 10 2019 10:25:48" NAME:line-us SERIAL:155xxxxx
At this point, we are successfully connected to the Line-us in a two-way conversation. The Line-us has accepted our connection, responded with a “hello” and is now awaiting for further instructions.
Start by sending some test commands. Simply enter the following, ensuring that you use capital letters were shown (and press enter):-
G28
- you should see the arm recenter to the home position. The pen will also be raised.- With the Line-us raised, this is a perfect opportunity to correctly set the pen height. The Line-us instructions recommend placing a UK £1 coin or two stacked US Dimes (which for the benefit of readers in other countries, is approx 3mm thick) underneath the tip of the pen and then tightening the clamp in this position.
G01 X1600 Y-800 Z1000
- this should move the arm noticeably.
If you can see an obvious result, then we’re all set to progress.
Quick start to using the code
Aside from a small configuration edit, you should be able to clone the project from GitHub, add the Line-us to your network and just run the code.
Before running the project, open the file ....\DrawWithLineUs\Config\ProgramConfig.cs
and update the configuration values.
You will need to change the path to a sample SVG image.
You will need to change the IP address, to whatever your router has assigned to your own Line-us.
If you want to actually send commands to the Line-us, you should set
Testmode
to befalse
. Alternatively, if you just want to check that the code works and emits results to the console window, leave it set totrue
.
Overview of the solution
- You should clone the solution from https://github.com/SiliconOrchid/DrawWithLineUs and refer to the code as we go along.
Our C# .NET Core solution is primarily a console app called DrawWithLineUs
.
The app can be broadly categorised into the following areas:
- Configuration/Enumerations -
static
classes that containconst
fields, for items such as the “drawable canvas area”. - Models - classes that are used to model things such as the list of coordinates.
Services - classes that contain the main program logic. Exploring this area further, there are four services that provide the following functionality:
- SVGService - has the job of extracting data from an SVG file and turning it into a list of coordinates.
- GeometryService - is concerned with activities such as determining the extremities of points within an image and calculating an appropriate scaling, so that our image can be resized into the drawable area.
- GCodeService - responsible for converting a list of C# types into appropriate G-Code strings.
- CommunicationService - this class is responsible for handling the network communication and transmitting bytes back and forth between your app and the Line-us.
Designed to use Dependency Injection
The application uses .NET Core’s built-in dependency injection.
Services are written so that they implement interfaces. Using the DI framework, the services can easily be added to a class via its constructor. If you are unfamiliar with this type of code, you can learn about Dependency Injection here.
- A majority of the code in
Program.cs
is related to the configuration of the DI system. - Most of the code that relates to the app logic, can be found in the file
ConsoleApplication.cs
.
Code written in this way, lends itself better to unit testing. The source code comes supplied with a small set of unit tests that can be found in ....\DrawWithLineUs\DrawWithLineUs.Console.Test\
Reading SVG data
Scalable Vector Graphics (SVG) are an open standard which was created by the W3C and dates back to 1999. Unlike other image formats which are binary files, an SVG is nothing more than a text file containing XML. This means they can be readily viewed and edited in any text editor and can easily be read and parsed using .NET libraries.
The SVG description language is extensive and beyond the scope of this article. However, for our purposes, we can focus on a relatively small subset.
There is a huge caveat, regarding the code in this solution, that should be made clear.
This article is intended to be an exploration of the Line-us, so we are not attempting to write a fully-featured SVG parser.
Therefore, we are assuming that we will only be using SVG images that have been created using Path elements. I have only tested using SVG files that are the output from a Potrace conversion (which seems to generate images using only Path elements).
This means that you cannot expect to get results from all SVG files. This is because an SVG file can be created using many different elements, including basic shapes such as Rectangles, Circles, Ellipses, etc.
However, there is absolutely no reason why this project couldn’t be forked and expanded upon to support SVG more fully! You could even submit a PR to my repo!
The following code snippet shows the first couple of lines of an example SVG file.
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="850.000000pt" height="650.000000pt" viewBox="0 0 850.000000 650.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,650.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M6627 5334 c-13 -13 -7 -23 23 -39 40 -21 50 -19 21 4 l-25 19 30 -5
c20 -4 26 -2 19 5 -13 14 -59 25 -68 16z"/>
<path d="M5360 5695 c0 -9 -4 -13 -10 -10 -5 3 -10 1 -10 -4 0 -6 6 -11 14
-11 17 0 29 27 16 35 -6 4 -10 -1 -10 -10z"/>
<path d="M5440 5665 c-6 -8 -10 -20 -7 -27 4 -8 8 -4 13 10 5 18 9 20 15 10 7
-10 9 -9 9 5 0 21 -13 22 -30 2z"/>
<path d="M6478 5673 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
<path d="M5571 5653 c0 -12 2 -13 6 -5 4 10 8 10 19 1 18 -15 18 -29 -1 -29
-23 0 -18 -16 10 -28 20 -9 23 -9 18 4 -4 10 0 12 11 8 9 -3 2 10 -16 30 -35
40 -48 45 -47 19z"/>
[… truncated for brevity …]
For our purposes, we can disregard entirely the first few lines and concentrate purely on the <path/>
nodes. Specifically, we are only interested in the d
(data) attribute, so our code will need to be able to parse and extract this information.
Understanding an SVG Path
Mozilla provides an excellent guide to reading SVG Paths
Next, let’s concentrate on the structure of the actual data in the <path/>
. On first inspection, it looks to be nothing more than a long list of numbers.
- These numbers are always found in pairs representing X and Y values.
If you look more closely, you may spot that these numbers are interspersed with the occasional letter. These letters mean:
- M - Start of the path
- l - Start a sequence of lines : Unless changed, all subsequent number-pairs will relate to lines.
- c - Start a sequence of curves : Unless changed, all subsequent number-pairs will relate to curves.
- z - End of a path
The first pair of numbers immediately following an “M” are the absolute X/Y starting coordinates of a path.
All subsequent number-pairs are the delta (the change) from the previous position and not an absolute coordinate. In order to derive absolute coordinates, we would need to track the change(s) all the way back to the starting pair.
Recalculating these delta points as absolute coordinates will be one of the requirements of our code.
Lines comprise of a single pair of numbers. The number-pair being inspected is the ending-point of the line. The starting-point will be the ending-point of the previous step.
Curves comprise of three pairs of numbers, which describe the shape of a bezier curve. The first two pairs relate to control-points that influence the shape of the curve, whilst the third pair is the ending-point.
In the image below, point A represents the starting point of the curve (the previous step), point B represents the first pair of digits, point C the second pair of digits and finally, point D represents the third and final pair (the end point).
Line-us cannot draw curves
Importantly, Line-us does not understand curves and only works with straight lines.
This means that when we encounter a path element that describes a curve, we simply have to ignore the first two pairs of values and simply extract the end-point. For our solution, a curve will effectively be the same thing as a straight-line.
This inevitably means that our image will not be the same as the source SVG and may appear jagged. However, in practice, this wasn’t really noticeable because most of the paths in the test images were so small that even straight-lines held up acceptably well to scrutiny.
Let’s look at the code!
Program entry point
As previously mentioned, the file Program.cs
mostly contains the setup for Dependency Injection, so instead you should look to the file ConsoleApplication.cs
for the entry point into the program.
Within this file, you will find a clearly named set of method calls. These address each of the four main steps that our program needs to perform:-
public void Run()
{
GetCoordinatesFromSVG(); // extracts a list of coordinates from the source file
ApplyGeometry(); // rescales the coordinates, to fit within the drawable area
GenerateGCode(); // produces a list of G-Code from the list of coordinates
Draw(); // Sends the G-Code to the Line-us
}
SvgService
The SvGService is probably the most complex part of the project, so don’t worry if you find it a bit confusing!
The code is located here: ....\DrawWithLineUs\DrawWithLineUs\Service\SvgService.cs
The SvgService is responsible for:
- Opening the SVG file specified in configuration and reading the XML content, by using
System.Xml.XmlReader
- Locating only the
path
XML nodes from within the document and returning a string - we ignore anything else in the XML. - For each path, tracking and extracting the appropriate coordinates that comprise the path.
Extracting XML data is a relatively straightforward problem to address, as the .NET framework has a library that does all of the heavy-lifting. The code looks like this:-
using (XmlReader reader = XmlReader.Create($"{PathToSourceSVG}", settings))
{
while (reader.Read())
{
switch (reader.Name.ToString())
{
case "path":
listPathNodes.Add(reader.GetAttribute("d"));
break;
}
}
}
Extracting the appropriate coordinates that comprise a path is not entirely straightforward, so let’s look at this code closely.
- Our program is initially presented with a long, single string that represents the “raw” data that has been extracted from the XML source.
- The first thing we need to do, is to chop this up into a List
of individual words, using the .NET String.Split() method.
As a reminder, in an earlier section, we looked at how each SVG Path is made up of lines and curves. We looked at how the data description uses the characters
l
andc
as delimiters. We learned that lines comprise of a single pair of numbers, whilst a curve comprises three pairs of numbers. Finally we learned that we can’t actually take advantage of curves, so we are only interested in extracting the third pair of digits.
The XML data is not usable to us in its original form. We need to model the data by using C# objects that store a list of paths, each of which contains a nested-list of absolute coordinates (not just the delta, as found in the XML). These coordinates will eventually be used to drive the Line-us robot arm.
- We use the model
...\DrawWithLineUs\Model\CoordinateStructure.cs
as a container for this information. Rather than store the X/Y coordinates into separateint
fields, we can use .NET’sSystem.Drawing.Point
struct, as it is intended for this kind of use.
public class CoordinateStructure
{
public List<Point> ListPoints { get; set; }
public List<PointF> ListRescaledPoints { get; set; } //using PointF for better precision, having been scaled
public CoordinateStructure()
{
ListPoints = new List<Point>();
ListRescaledPoints = new List<PointF>();
}
}
We then need to parse through the list of words. We use the term words here deliberately, because they are not necessarily just numbers. For example, a word may include a letter like this: l-25
, which conveys the double meaning that we are starting a new sequence of lines and that the first X value is “negative 25”.
Your first idea may be that, because numbers always appear as pairs of X/Y values, that we just need to iterate over the list of words, incrementing our index value of 2 on each pass of the loop. However, this doesn’t work, because curves require us to skip the index forward by 6.
The increasing complication is that a path can contain a mixture of both lines and curves (we’ll call this a “variant”), but it doesn’t tell us which variant we are looking at each time - only when a change happens. This means that we also need to track what the current variant is, as we iterate.
- We use the variable
currentSvgPathVariantEnum
to persist the variant that is currently in effect.- We have used the enumeration
SvgPathVariantEnum
to make it clearer, in the code, which type of variant we are working with.
- We have used the enumeration
- We use a
while
loop, which ultimately keeps iterating forward over the words, until we reach the end of the list. - Within the
while
loop, we use the methodGetCurrentPathVariant(...)
to test the current word to see if it contains the charactersi
orc
. If either of these are found, we update the variablecurrentSvgPathVariantEnum
. - Still within the
while
loop, we use the methodExtractCoordinate(...)
to extract the appropriate set of delta numbers.
- The index used to extract the number will vary, depending on whether we are looking at a line or a curve. We use a Regular Expression to strip out any letters. This code looks like the snippet just below:-
- In addition to extracting the delta numbers, we pass in the previous absolute coordinates, combine this with the new delta values, and ultimately return new absolute coordinates as a
Point
struct.
string coordX;
string coordY;
switch (currentSvgPathVariantEnum)
{
case SvgPathVariantEnum.Line:
coordX = Regex.Replace(listWords[i], "[^0-9.+-]", "");
coordY = Regex.Replace(listWords[i + 1], "[^0-9.+-]", "");
break;
case SvgPathVariantEnum.Curve:
coordX = Regex.Replace(listWords[i + 4], "[^0-9.+-]", "");
coordY = Regex.Replace(listWords[i + 5], "[^0-9.+-]", "");
break;
...
- Finally within the
while
loop, we use the methodIncrementCurrentPathIndex(...)
to determine the correct number by which we should increment the loop’s index (i.e. either 2 or 6).
The service ultimately returns a list of the container objects : List<CoordinateStructure>
.
The partial code listing of the main method looks like this (check the source on GitHub for the class in its entirety):
public List<CoordinateStructure> ExtractCoordinates(List<string> listPathNodes)
{
// an array of arrays (which contain point coordinates, which are constructed from the list of offsets in the SVG
List<CoordinateStructure> listCoordinateStructures = new List<CoordinateStructure>();
Console.WriteLine($"Parsing nodes...");
foreach (var pathNode in listPathNodes)
{
var coordinateStructure = new CoordinateStructure();
var listWords = pathNode.Split(" ");
// extract start coordinates
int coordStartX = int.Parse(Regex.Replace(listWords[0], "[^0-9.+-]", ""));
int coordStartY = int.Parse(Regex.Replace(listWords[1], "[^0-9.+-]", ""));
coordinateStructure.ListPoints.Add(new Point(coordStartX, coordStartY));
int i = 2; // don't start at very beginning, skip over the first pair of values as these are always the "starting position"
SvgPathVariantEnum currentSvgPathVariantEnum = SvgPathVariantEnum.Unset;
while (i < listWords.Length)
{
//check to see if the "variant" has changed (is this a line or curve, or are we carrying on from the last previously set variant?)
var newPathVariant = GetCurrentPathVariant(listWords, i);
if (newPathVariant != SvgPathVariantEnum.Unset)
{
currentSvgPathVariantEnum = newPathVariant;
}
// depending on the "variant" extract the appropriate coordinates and add this to "coordinateStructure.ListPoints" collection.
Point previousPoint = coordinateStructure.ListPoints[coordinateStructure.ListPoints.Count - 1];
Point? nextPoint = ExtractCoordinate(previousPoint, listWords, i, currentSvgPathVariantEnum);
if (nextPoint is null)
{
// skip over, as point was a "MoveTo" command.
}
else
{
coordinateStructure.ListPoints.Add((Point)nextPoint);
}
//depending on the current variant, increment the current index appropriately (either by 2 for a line, or 6 for a curve)
i = i + IncrementCurrentPathIndex(currentSvgPathVariantEnum);
}
listCoordinateStructures.Add(coordinateStructure);
}
return listCoordinateStructures;
}
GeometryService
The Line-us only accept coordinates within a certain range of values (refer to the diagram earlier in the article).
The SvgService however, may return coordinates with large values that exceed the bounds of the drawable area.
We need to be able to rescale the image so that everything fits correctly - this is where the GeometryService comes in.
This service is responsible for:
- Iterating through each and every single point returned from the SvgService and determining the coordinate extremities (in your mind, you can picture this as a rectangle which goes around the very edge of your image).
- We use the class
BoundingBox
to model this result. This class has methods to return the height and width of this box.
- We use the class
- We compare the width and height of this box against the height and width of the Line-us available drawing area. From this, we can determine an appropriate scaling ratio.
- We then reiterate through all points a second time, apply the scaling factor to resize/reposition them.
- The recalculated coordinates are stored back into the instance of the model
CoordinateStructure
, except that this time they are store intoListRescaledPoints
. - The fields inside
ListRescaledPoints
are of typePointF
(for a floating-point point, rather than integer point). This is so that we don’t accidentally lose precision, should we end up scaling a very differently sized image. We want to preserve the accuracy of the new points as much as possible, up until the point that we export GCode.
- The recalculated coordinates are stored back into the instance of the model
GCodeService
This service is responsible for:
- Iterating the list of rescaled coordinates (having just been rescaled by the GeometryService). The data is passed to this service via a populated instance of the
CoordinateStructure
class. - Producing a list of appropriate GCode commands that will be later sent to the device.
By the time we come to use the GCodeService, we’ve done all the hard work of preparing lists of data that represent multiple paths each containing multiple points.
All that is left for us to do, is pay attention to what the pen of the robot is doing, by lifting it off the page and moving it to the start of each new path.
- When we start drawing a new path, we reposition the pen to the new starting coordinates.
- Lower the pen ready to start drawing.
- Move it to each point in sequence.
- We then raise the pen at the end of the sequence, ready for the move to the start of the next path.
Because our rescaled coordinates have been stored as a PointF
(a floating-point precision point), we need to convert back to an int
when producing our GCode.
Tip: try and use enums or static classes to describe fixed values
As a developer, something you should avoid the anti-pattern of using magic numbers or hardcoding of strings that, although make sense at the time you write the code, are not apparent to other people (or indeed yourself in the future, when you forget the details!).
As such, you should aim to make the meaning of your code as clear as possible by using easily-understood words in code. You can usually use enumerations or static classes to help you here.
So in the case of our GCode, we could easily have written the snippet above so it looked like this…
listGCodes.Add($"G01 x{(int)step.X} y{(int)step.Y} z0\n");
… but how would someone who is new to the project, who is reading your code, know that “G01” means “Linear Interpolation” - or that the Z axis of zero means “pen down”. To get meaning, they would have to stop what they’re doing and cross-reference with the documentation.
Instead, we can improve the meaning in our code like this:-
public static class GCodes
{
public const string RapidReposition = "g00";
public const string LinearInterpolation = "g01";
}
public static class Pen
{
public const int PenUpIndex = 1000;
public const int PenDownIndex = 1;
}
With awareness of the above, we can instead write our code that produces GCode for the path like this:-
private static void SequencePath(List<string> listGCodes, CoordinateStructure sequence)
{
foreach (var step in sequence.ListRescaledPoints)
{
listGCodes.Add($"{GCodes.LinearInterpolation} x{(int)step.X} y{(int)step.Y} z{Pen.PenDownIndex}\n");
}
}
If you are not entirely familiar with some of the syntax in the example above, we are using C#6 String Interpolation to build our string (where previously, you may have used
String.Format()
to achieve the same outcome).
CommunicationService
This service is responsible for:
- Establishing a TCP connection between your computer and the Line-us, by using the .NET library
System.Net.Sockets
. - Receiving the initial “hello” back from the Line-us, signalling the device is ready to start a two-way communication.
- Converting GCode into a byte-array and sending the command.
The class in its entirety looks like this:-
public class CommunicationService : ICommunicationService
{
public void ConnectToLineUs(out TcpClient client, out NetworkStream stream, string lineusIP, int lineusport)
{
IPAddress lineusipaddress = System.Net.IPAddress.Parse(lineusIP);
client = new TcpClient();
client.Connect(new IPEndPoint(lineusipaddress, lineusport));
stream = client.GetStream();
SayHello(stream);
}
private void SayHello(NetworkStream stream)
{
Byte[] data = new Byte[256];
int bytes = stream.Read(data, 0, data.Length);
String responseData = System.Text.Encoding.ASCII.GetString(data, 0, bytes);
Console.WriteLine($"Received: {responseData}");
}
public void Transmit(NetworkStream stream, string instruction)
{
//Console.WriteLine($"Sent: {instruction}");
byte[] data = System.Text.Encoding.ASCII.GetBytes(instruction);
stream.Write(data, 0, data.Length);
data = new Byte[256];
int bytes = stream.Read(data, 0, data.Length);
string responseData = System.Text.Encoding.ASCII.GetString(data, 0, bytes);
Console.WriteLine($"Received: {responseData}");
}
}
Wrapping up
Line-us is a nice little machine, thoughtfully designed and easy to use. It sidesteps much of the complications that come with “rolling your own” hardware systems.
It’s very much something I could envisage being used as a way to get kids interested in programming and for me, mildly echoes the LOGO programming that I was shown in school, a generation ago.
I’m hoping this article can re-assert that programming can be just for fun and hopefully inspire folks to try something different.
If you’ve discovered this article through a search, coming as someone wanting to learn about Line-us, then hopefully this has been useful to you as an example of C# code.
Where to next?
Check out this things-to-do link by line-us.com for inspiration.
Here are some ideas for you to improve this project:
- Extend the SVG parser to support more shapes - not just paths.
- Extend the code to support multi-colour painting - check out the following image (from line-us press images):
Further reading
Line-us Documentation (including GCode Spec and drawable area diagram)
A useful guide to the syntax of a ‘Path’ element of an SVG file by Mozilla
DI in a console application by Larry Schoeneman
Disclosure
I always disclose my position, association or bias at the time of writing; No third party compensate me or otherwise endorse me for my promotion of their services.