Vuzix
Technical exploration of the Z100 AR glasses.
Initially, I'm working around Android and implementation in .NET (Blazor) as this is my usual code stack with the purpose to build towards Android.If there is any need or demand for iOS and windows integrations - I can explore those at a later stage as well.
Introduction
After playing with the demo, sending some text, trying to modify the images, you might start to get inspired to create your own displayed content.And realize, you don't know where to start as the image you try to send to the glasses isn't displayed. Or are fumbling with "embedded resources" and don't know where the images end up and how to send them to the glasses.Do not despair.In short, the initial difficulties I have encountered (and solved) are:
The resources have to be a particular size, else they will not be displayed
Using the Android bitmap generator does some scaling and it's not very clear how to get fine control on the output size, this
route will burn a lot of time and energy to try to "find the sweet spot"
If you're comfortable with all the above and go your own route this is the base take-away:
=> The glasses seem to be work best at 490x490 pixels. They handle 640x480 but also 480x640. So scaling is done - it's easier to not rely on Android bitmap scaler or the glasses
How to get started
The easiest way to get started has a few easy steps if you are familiar with Android Studio or building Android applications.
If you haven't gone through it before, it might not always be so clear. So this might help you to get through it in a couple of steps and help you get started.
Install the Vuzix SDK
Install the Vuzix Connect application
Pair the Z100 glasses
Setup your development environment
Install Android Studio
Probably you'll need to put your Android phone into Developer mode
And install the USB driver
Download the sample code
Start the sample code with Android Studio
.NET Integration
Examples to get started fairly quickly.I'm trying to be as complete possible - so someone who comes without experience, will not get lost.
.NET integration: Vuzix on MAUI
What we'll try to achieve
We'll build an integration in MAUI that will hide all the complexity doing interfacing work on Android - and perhaps in a later stage on IOS as well.
Microsoft MAUI is a way to build code in C# / .NET and run it on several devices. And builds upon Microsoft Blazor - the coding experience is the same. The resulting application is either a web application or service vs a Mobile application. This means, you can build "a desktop app that runs on Android or IPhone IOS" without any change in the code. This works with running HTML code with asyncronous calls to an interpreter that runs on either system. All this complexity is handled for you - so either you code in your usual .NET environment or write some JavaScript that will "drive the device in the background". Lets get started!
Set up the project
Let's assume you've installed Visual Studio and checked the "mobile development" option (or have added this featureset
Create a new MAUI Blazor Hybrid project
Click through the options and you'll see the folder structure. The folder "Platforms" contains code specific for the target system, in this guide, we'll only focus on Android for simplicity.
So lets right-click the project, to edit the project file
Remove the targets we don't use right now
We also need to set the minimum version to 30 for Android
Add the Nuget package
It is possible to create this project yourself - but I have taken the liberty to make a nuget package for it . (the generator requires some manual editting that is cumbersome, so a package will be easier)
Open nuget package manager
Find the nuget package and add it to the project
Application first run
If everything is setup properly, you should be able to select your target to deploy (or emulate) the application on. If you've installed the USB drivers with Android studio , you should be able to put your Android phone in Debug-mode and run the application on your physical real phone.
You should now see your emulator running with your MAUI application that is set up to work with Vuzix Z100 !
Continue to the next page, to start contacting your glasses.
.NET MAUI Integration: First contact
Introduction
On this page, it's assumed you have a MAUI project with the nuget package ready to go.
In the end, you will be able to make a simple HTML website that receives text and it will be shown in the Z100 AR glasses. If you don't want to spend time for going step by step, here are the source files:
Excercise1.zip ExerciseFinal.zip
Background
Don't be overwhelmed: you don't need to study this, but it will help you to conceptualize where which code will be run. And what you can access or in what context you are operating.
MAUI is a front that runs in a HTML engine with JQuery and Bootstrap which make it easy to make interactive web interfaces. It then communicates to the actual back-end of the application via asynchronous calls - via their SignalR engine. This helps to write "classic server code" but having the fluid easy GUI that web applications have. In the back on the "server code" - there is a translation layer towards the target host technology. In this example, this will be Android.
We just have to somehow translate a mouse click, to go to the server code. And from the server code, towards the Android device.
For this reason, we're using the CommunityToolkit.Mvvm.Messaging package, which allows to "listen" and "trigger" events that can be picked up via event handlers. We do not have to concern who triggered it, or where it was triggered. Via the magic of dependency injection, this is all handled for us.
Lets get to it!
Excercise1.zip
1. First, lets get the CommunityToolkit.Mvvm.Messaging package into the solution.
2. Now we can write a simple code in the Home.razor page to take text.
This example connects the value of the variable @ message into the textbox. When the textbox is clicked, the function SendMessage() is triggered. Because of the nature of Blazor - the page is refreshed. At the refresh, there is now a value. And your value is displayed.
@page "/"
@code{
public string MyMessage;
void SendMessage()
{
// Send the message to ANdroid ???
}
}
Hello, world!
@if (MyMessage != null && MyMessage.Length>0 )
{
You wrote: @MyMessage
}
Run your emulator to try it out and see everything is working okay:
If you'd like you can set a breakpoint to inspect your variable as well, by clicking the left bar to set a red dot. And running your example. While hovering over your variable.
3. And now the real work (in 5 minutes) - Set up the GUI to send the message. In this step we will send messages from the GUI towards Android. Know, that we can do the reverse, and send from Android towards the GUI if desired. But in our case, Android will forward the message to our glasses!
Add using directives, so the page knows where to find the nuget classes
@page "/"
@using CommunityToolkit.Mvvm.Messaging;
@using VuzixSDK.Class
@using VuzixSDK.Enum
And add the following 2 functions to your code.
These functions will use the Mvvm.Messaging WeakReferenceMessanger and send a class of UltraLiteMessage in one function, based on the string. And an UltraLiteError in the second function. So we can display errors to the user.
protected async static void UltraLiteMessage(String Message)
{
WeakReferenceMessenger.Default.Send(new UltraLiteMessage()
{
Data = Message
});
}
protected async static void UltraLiteError(Exception Excpetion)
{
WeakReferenceMessenger.Default.Send(new UltraLiteError()
{
Source = "Counter",
Exception = Excpetion
});
}
And update your SendMessage function to trigger the UltraLiteMessage function.
void SendMessage()
{
// Send the message to ANdroid ???
UltraLiteMessage(MyMessage);
}
Your full home.razor page will look as such:
@page "/"
@using CommunityToolkit.Mvvm.Messaging;
@using VuzixSDK.Class
@using VuzixSDK.Enum
@code{
public string MyMessage;
void SendMessage()
{
// Send the message to ANdroid ???
UltraLiteMessage(MyMessage);
}
protected async static void UltraLiteMessage(String Message)
{
WeakReferenceMessenger.Default.Send(new UltraLiteMessage()
{
Data = Message
});
}
protected async static void UltraLiteError(Exception Excpetion)
{
WeakReferenceMessenger.Default.Send(new UltraLiteError()
{
Source = "Counter",
Exception = Excpetion
});
}
}
Hello, world!
@if (MyMessage != null && MyMessage.Length>0 )
{
You wrote: @MyMessage
}
That's it for your personal code.
What is left, is to receive your text and show it on the glasses.
4. Receive the text in Android
Locate the file MainActivity.cs and add the using statement towards the Mvvm.Messaging library.
And create the following functions:
OnCreate is an entrypoint in Android and will be run at the start of the application.
We then register a listener for the messages that the front end is sending to us: UltraLiteError and UltraLiteMessage by the WeakReferenceMessenger declaration.
showMessage will show a basic Android notification on the screen, it's always convenient to show errors if something goes wrong
In essense; when the application is created, when a message with object of class UltraLiteError is received, it will end up in the processUltraLiteError function. When the application receives a message with object of class UltraLiteMessage , it will end up in the processUltraLiteMessage function.
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
try
{
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteError(e); });
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteMessage(e.Data); });
}
catch (System.Exception ex)
{
showMessage(ex.Message);
}
}
public void showMessage(string message)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var toast = Toast.MakeText(this, message, ToastLength.Short);
toast.Show();
});
}
protected void processUltraLiteError(UltraLiteError error)
{
}
protected void processUltraLiteMessage(String message)
{
}
At this point you can run or compile the code, but lets now connect the glasses.
Integrating the VUZIX SDK
ExerciseFinal.zip
First add using statements for the Vuzix SDK
using VuzixSDK.Class;
using Com.Vuzix.Ultralite;
Then, declare a variable _sdk, which will hold a reference to the Vuzix SDK
IUltraliteSDK _sdk;
In the OnCreate function, asssign the variable
_sdk = Com.Vuzix.Ultralite.IUltraliteSDK.Get(this);
And update our functions, to display the text on the glasses:
protected void processUltraLiteError(UltraLiteError error)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
string _title = $"[Error]{(error.Source != null ? " " + error.Source : "")}";
string _error = (error.Exception != null ? $"Exception : {error.Exception.Message}" : "Error occured");
_sdk.SendNotification(_title, _error);
}
}
}
protected void processUltraLiteMessage(String message)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
_sdk.SetLayout(Layout.Canvas, 0, true);
int textId = _sdk.Canvas.CreateText(message, TextAlignment.Auto, UltraliteColor.White, Anchor.TopCenter, 0, 0, 640, -1, TextWrapMode.Wrap, true);
if (textId == -1)
{
showMessage("Text failed");
}
_sdk.Canvas.Commit();
}
}
}
Your full MainActivity.cs will look like this:
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Widget;
using CommunityToolkit.Mvvm.Messaging;
using VuzixSDK.Class;
using Com.Vuzix.Ultralite;
using Layout = Com.Vuzix.Ultralite.Layout;
using TextAlignment = Com.Vuzix.Ultralite.TextAlignment;
namespace MauiApp1
{
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
IUltraliteSDK _sdk;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
try
{
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteError(e); });
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteMessage(e.Data); });
_sdk = Com.Vuzix.Ultralite.IUltraliteSDK.Get(this);
}
catch (System.Exception ex)
{
showMessage(ex.Message);
}
}
public void showMessage(string message)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var toast = Toast.MakeText(this, message, ToastLength.Short);
toast.Show();
});
}
protected void processUltraLiteError(UltraLiteError error)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
string _title = $"[Error]{(error.Source != null ? " " + error.Source : "")}";
string _error = (error.Exception != null ? $"Exception : {error.Exception.Message}" : "Error occured");
_sdk.SendNotification(_title, _error);
}
}
}
protected void processUltraLiteMessage(String message)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
_sdk.SetLayout(Layout.Canvas, 0, true);
int textId = _sdk.Canvas.CreateText(message, TextAlignment.Auto, UltraliteColor.White, Anchor.TopCenter, 0, 0, 640, -1, TextWrapMode.Wrap, true);
if (textId == -1)
{
showMessage("Text failed");
}
_sdk.Canvas.Commit();
}
}
}
}
}
When you run the code on your Android phone, which has the Vuzix Connect app installed, the text from the input field will appear on the glasses when you click the button.
In the next chapter , we will update this code to send images from the GUI into the glasses.
.NET MAUI Integration: Images & Image Size exploration
Introduction
At the end of this page, you will generate an image and display it in the glasses.
We continue from the previous page. With this resulting project: ExerciseFinal.zip
And will end up with this project: ExerciseSendImages.zip - which will allow to specify the dimensions and generate an image with text that shows the dimensions. This image is then sent to the Z100 AR glasses and displayed.
The SDK allows us to send a "bitmap" and it will be displayed. However, I haven't gotten such results with it as I wanted: at first I was a bit lost including and referencing files and resources in Android, from the MAUI application. After I got that figured out, the images were either not displaying, with a strange aspect ratio either or image was scaled down and I couldn't really figure out if it was .NET or Android or the SDK that is doing the scaling. After reading up on "pixel density" and other complicated tutorials I have gone the more simple route.
To find the way to find the "most ideal image size" I generated the bitmap myself. I believe this guide is a good enough base to allow you to draw anything on the glasses and gain full control to its potential. So, let's get to it!
Approach
We need to send a bitmap - which is an image - in binary form to the Android application. But we do not want to "depend on any framework" to do the scaling.
As we are in the GUI working with HTML5 - we have access to a canvas-element . Which is basically, a sort of image that can be built via JavaScript in the browser.
This way, we can easily try some things out in a local text-editor and browser to see if it is generated well. Before we get into a heavier compilation and deployment - which loses a lot of time (and motivation). I will first step you through the preparation of the code, to be able to render a byte[] containing a bitmap. And then, we'll write a piece of code to generate an image and send it to the glasses.
Project adjustement: boiler plate
We first have to add some Android code, to listen to the new UltraLiteOperationRequest event, convert the byte to an image format the glasses can work with. And then send it to the glasses.
We start again in MainActivity.cs
1. Add 3 functions to translate a List and a byte[] to LVGImage which Vuzix works with
private static Bitmap loadBitmap(byte[] bitmapbytes)
{
BitmapFactory.Options options = new BitmapFactory.Options();
// https://proandroiddev.com/image-decoding-bitmaps-android-c039790ee07e
options.InSampleSize = 2;
options.InPreferredConfig = Bitmap.Config.Argb8888;
Bitmap bmp = BitmapFactory.DecodeByteArray(bitmapbytes, 0, bitmapbytes.Length, options);
return bmp;// resize(bmp, 640, 480);
}
private static LVGLImage[] loadLVGLImage(List images)
{
List _images = new List();
foreach(var image in images)
{
_images.Add(LVGLImage.FromBitmap(loadBitmap(image), LVGLImage.CfIndexed1Bit));
}
return _images.ToArray();
}
private static LVGLImage loadLVGLImage(byte[] image)
{
//ColorObject[] _colors = { LVGLImage.IColorMapper.White, LVGLImage.IColorMapper.Mid };
//LVGLImage _img = new LVGLImage(LVGLImage.CfIndexed1Bit, 480, 640, _colors, image);
LVGLImage _img2 = LVGLImage.FromBitmap(loadBitmap(image), LVGLImage.CfIndexed1Bit);
return _img2;
}
2. Add the function processUltraLiteOperation that will process the incoming message
protected void processUltraLiteOperation(UltraLiteOperationRequest Request)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
_sdk.SetLayout(Layout.Canvas, 0, true);
if (Request.Operation == eUltraLiteOperation.ShowImage && Request.ImageBitMap != null)
{
LVGLImage image = loadLVGLImage(Request.ImageBitMap);
int _imageID = _sdk.Canvas.CreateImage(image, Anchor.Center);
if (_imageID == -1)
{
showMessage("Image failed");
}
_sdk.Canvas.Commit();
}
}
}
else
{
showMessage("SDK is not connected");
}
}
3. Add, in the OnCreate function, the event listener for the message
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
try
{
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteError(e); });
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteMessage(e.Data); });
_sdk = Com.Vuzix.Ultralite.IUltraliteSDK.Get(this);
}
catch (System.Exception ex)
{
showMessage(ex.Message);
}
}
Your full Mainactivity.cs should look like this now:
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Widget;
using CommunityToolkit.Mvvm.Messaging;
using VuzixSDK.Class;
using Com.Vuzix.Ultralite;
using Layout = Com.Vuzix.Ultralite.Layout;
using TextAlignment = Com.Vuzix.Ultralite.TextAlignment;
using VuzixSDK.Enum;
using Android.Graphics;
namespace MauiApp1
{
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
IUltraliteSDK _sdk;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
try
{
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteError(e); });
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteMessage(e.Data); });
WeakReferenceMessenger.Default.Register(this, (sender, e) => { processUltraLiteOperation(e); });
_sdk = Com.Vuzix.Ultralite.IUltraliteSDK.Get(this);
}
catch (System.Exception ex)
{
showMessage(ex.Message);
}
}
public void showMessage(string message)
{
MainThread.BeginInvokeOnMainThread(() =>
{
var toast = Toast.MakeText(this, message, ToastLength.Short);
toast.Show();
});
}
protected void processUltraLiteError(UltraLiteError error)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
string _title = $"[Error]{(error.Source != null ? " " + error.Source : "")}";
string _error = (error.Exception != null ? $"Exception : {error.Exception.Message}" : "Error occured");
_sdk.SendNotification(_title, _error);
}
}
}
protected void processUltraLiteOperation(UltraLiteOperationRequest Request)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
_sdk.SetLayout(Layout.Canvas, 0, true);
if (Request.Operation == eUltraLiteOperation.ShowImage && Request.ImageBitMap != null)
{
LVGLImage image = loadLVGLImage(Request.ImageBitMap);
int _imageID = _sdk.Canvas.CreateImage(image, Anchor.Center);
if (_imageID == -1)
{
showMessage("Image failed");
}
_sdk.Canvas.Commit();
}
}
}
else
{
showMessage("SDK is not connected");
}
}
private static Bitmap loadBitmap(byte[] bitmapbytes)
{
BitmapFactory.Options options = new BitmapFactory.Options();
// https://proandroiddev.com/image-decoding-bitmaps-android-c039790ee07e
options.InSampleSize = 2;
options.InPreferredConfig = Bitmap.Config.Argb8888;
Bitmap bmp = BitmapFactory.DecodeByteArray(bitmapbytes, 0, bitmapbytes.Length, options);
return bmp;// resize(bmp, 640, 480);
}
private static LVGLImage[] loadLVGLImage(List images)
{
List _images = new List();
foreach (var image in images)
{
_images.Add(LVGLImage.FromBitmap(loadBitmap(image), LVGLImage.CfIndexed1Bit));
}
return _images.ToArray();
}
private static LVGLImage loadLVGLImage(byte[] image)
{
//ColorObject[] _colors = { LVGLImage.IColorMapper.White, LVGLImage.IColorMapper.Mid };
//LVGLImage _img = new LVGLImage(LVGLImage.CfIndexed1Bit, 480, 640, _colors, image);
LVGLImage _img2 = LVGLImage.FromBitmap(loadBitmap(image), LVGLImage.CfIndexed1Bit);
return _img2;
}
protected void processUltraLiteMessage(String message)
{
if (_sdk.IsConnected)
{
if (!_sdk.IsControlledByMe)
{
_sdk.RequestControl();
}
if (_sdk.IsControlledByMe)
{
_sdk.SetLayout(Layout.Canvas, 0, true);
int textId = _sdk.Canvas.CreateText(message, TextAlignment.Auto, UltraliteColor.White, Anchor.TopCenter, 0, 0, 640, -1, TextWrapMode.Wrap, true);
if (textId == -1)
{
showMessage("Text failed");
}
_sdk.Canvas.Commit();
}
}
}
}
}
The MAUI code to send the image
We'll go back to the Home.razor page and connect the GUI with the Android code
1. Add the function to send the UltraLiteOperationRequest containing a byte[] to the Android back end
protected async static void UltraLiteOperation(eUltraLiteOperation operation, byte[] BitMap = null)
{
WeakReferenceMessenger.Default.Send(new UltraLiteOperationRequest()
{
Operation = operation,
ImageBitMap = BitMap
});
}
Add the folllowing function in a code block in the Home.razor page, this will expose the function to the script that we will write to trigger
public static void SendGeneratedBMP(string response)
{
try
{
UltraLiteOperation(eUltraLiteOperation.ShowImage, Convert.FromBase64String(response));
}
catch (System.Exception ex)
{
UltraLiteError(ex);
}
}
You are all set for the backend! But, what will you send over the line ?
CANVAS to BITMAP
Let's take a step back and rephrase what we want to achieve: "I want to define a size for an image. And then send it to the Z100"
1. Write the Javascript code to handle the GUI
This is where it gets confusing for some: we're now in the context of the browser or the GUI - which can't just access the "machine code". We need to make use of the DotNet functions to pass data from the front to the back. While the drawing and clicking is happening on the front end.
If it sounds confusing, just take the code an play with it.
Open the index.html to add some javascript - this isn't the most "clean and proper way", but this will get it working for you while you write the gold standard code:
The first will get the element you specified and resize it to your specified width and height.
The other, will use standard JavaScript functions to parse the displayed graphic into a Base64 string (which is a text representation of a byte) - and will send it to the backend by this DotNet.invokeMethodAsync (you might need to replace the projectname MauiApp1 with your own for it to work)