Write to the Log file from a Smart Office Script

Here is an unofficial technique to write to the Lawson Smart Office log file from a Personalized Script. Smart Office uses the Apache log4net library to write to the log file and we can similarly write to the log file from our scripts. Before using the techniques of this blog you must acknowledge my disclaimer.

The JScript.NET example comes from the log4net Source Distribution at:

log4net\examples\net\1.1\Tutorials\ConsoleApp\js\src\LoggingExample.js

First, we get a reference to the Smart Office logger via Mango.Core:

var log: log4net.ILog = Mango.Core.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

Then, we can write to the log file in any of the log levels:

log.Info("Hello World!");
log.Debug("This is a Debug");
log.Error("This is an Error");
log.Fatal("This is Fatal");
log.Warn("This is a Warning");

We should check if the logger is enabled for that level:

if (log.IsInfoEnabled) log.Info("Hello World!");
if (log.IsDebugEnabled) log.Debug("This is a Debug");
if (log.IsErrorEnabled) log.Error("This is an Error");
if (log.IsFatalEnabled) log.Fatal("This is Fatal");
if (log.IsWarnEnabled) log.Warn("This is a Warning");

The log file can be opened with Smart Office > Help > About Lawson Smart Office > View log file:

It opens in Notepad in Lawson Smart Office 9.1.2.x:

And it opens in the Log Viewer in the newer Lawson Smart Office 9.1.3.x:

For more information, the Apache log4net SDK Documentation details all the ILog Members.

To know the full path and file name of the log file, get this value:

Mango.Core.Storage.FileStorageMachineOnly.Current.FullFilePath("LawsonClient.log")

For example, in my computer the full path and file name to the Smart Office log file is:

C:\Users\12229\AppData\Local\Apps\2.0\Data\79O176HR.9E2\N43AOMBD.0HB\http..tion_201b89ff2ddb7d50_0009.0001_aca28e931f10a84f\Data\LawsonClient.log

Also, you can use a tail program such as Tail for Win32 to monitor the log file:

Here’s the complete source code of my sample:

import System;
import Mango;
import log4net;

package MForms.JScript {
    class MyLogTest {
        public function Init(element: Object, args: Object, controller: Object, debug: Object) {
            debug.WriteLine(Mango.Core.Storage.FileStorageMachineOnly.Current.FullFilePath("LawsonClient.log")); // filename + path
            var log: log4net.ILog = Mango.Core.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
            if (log.IsInfoEnabled) log.Info("Hello World!");
            if (log.IsDebugEnabled) log.Debug("This is a Debug");
            if (log.IsErrorEnabled) log.Error("This is an Error");
            if (log.IsFatalEnabled) log.Fatal("This is Fatal");
            if (log.IsWarnEnabled) log.Warn("This is a Warning");
        }
    }
}

Note: I have successfully tested this in Smart Office 9.1.2.x and 9.1.3.x.

That’s it!

Start a Mashup with input parameters

Here is the solution to start a Mashup in Lawson Smart Office with input parameters.

  1. Suppose we have a Mashup located at:
    C:\Test.xaml

  2. The URI to launch that Mashup in Smart Office would be:
    mashup:///?BaseUri=C:\&RelativeUri=Test.xaml

  3. To send a key:value pair as an input parameter to the Mashup we add the parameter DefaultValues to the URI with an arbitrary key and an arbitrary value separated by a colon. In this example I chose a key InputParameter with a value Hello World! and I URI-encoded them:
    DefaultValues=InputParameter:Hello+World!
  4. The resulting URI is:
    mashup:///?BaseUri=C:\&RelativeUri=Test.xaml&DefaultValues=InputParameter:Hello+World!
  5. In the Mashup we receive the value with the following Binding:
    {Binding Converter={StaticResource CurrentItemValue}, ConverterParameter=InputParameter, Mode=OneTime}
  6. We can show the value in a TextBox for example:
    <TextBox Text="{Binding Converter={StaticResource CurrentItemValue}, ConverterParameter=InputParameter, Mode=OneTime}" Grid.Column="1" />
  7. For that Binding to work we must add the converter Mango.UI.Services.Mashup.CurrentItemValue to our Grid.Resources:
    <Grid.Resources>
    	<mashup:CurrentItemValue x:Key="CurrentItemValue" />
    </Grid.Resources>
  8. Now let’s start the URI:
  9. Here’s the result, the input parameter is shown in the TextBox!
  10. We can also send multiple input parameters by separating the key:value pairs with semi-colons:
    InputParameter1:Hello+World!;InputParameter2:Yeehaa!;InputParameter3:ABC123
  11. The new URI is:
    mashup:///?BaseUri=C:\&RelativeUri=Test2.xaml&DefaultValues=InputParameter1:Hello+World!;InputParameter2:Yeehaa!;InputParameter3:ABC123
  12. The new result is:

Here is a screenshot of the final XAML in the Mashup Designer:

Here is the complete XAML source code:

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:Mango.UI.Controls;assembly=Mango.UI" xmlns:mashup="clr-namespace:Mango.UI.Services.Mashup;assembly=Mango.UI" xmlns:m3="clr-namespace:MForms.Mashup;assembly=MForms" Margin="10">

This solution was given by the following sample provided in the Mashup Designer:

That’s it!

UPDATE 2013-01-31:

Note 1: My example above showed the URI to start a Mashup from the C:\ drive and folder. But to start a Mashup that is deployed, the BaseUri must point to the *.mashup file, for example: mashup:///?BaseUri=MyMashup.mashup&RelativeUri=MyMashup.xaml&DefaultValues=key1:value1;key2=value2 . This will work with a Mashup deployed either privately (Mashup Designer > Deploy > Private), either locally (Smart Office > Show > My Local Applications). But for a Mashup deployed globally (LifeCycle Manager > Admin View > Install) you must prefix the BaseUri with the folder Mashups\, for example: mashup:///?BaseUri=Mashups\MyMashup.mashup&RelativeUri=MyMashup.xaml&DefaultValues=…

Note 2: We can create a Shortcut in Smart Office (for example, CRS610 > Tools > Personalize Shortcuts) and dynamically get values from the current panel by putting the field name in curly braces (angle brackets will work as well), for example: mashup:///?BaseUri=MyMashup.mashup&RelativeUri=MyMashup.xaml&DefaultValues=ItemNumber:{ITNO}

Lawson Enterprise Search query syntax

Lawson Enterprise Search (LES) is based on Apache Lucene and its syntax is based on Apache Lucene Query Parser Syntax which is interesting to read to learn about Fields, Term Modifiers (Wildcard Searches, Fuzzy Searches, Proximity Searches, Range Searches, Boosting a Term), Boolean Operators (AND, +, NOT, -), Grouping, Field Grouping, and Escaping Special Characters..

Warehouse 3D demo

I implemented a Warehouse 3D demo that demonstrates the integration capabilities of M3 with cool stuff from the software industry.

The Warehouse 3D demo displays racks and boxes with live data coming from Stock Location – MMS010, and Balance Identity – MMS060. The Location Aisle, Rack, and Level of MMS010 is written dynamically on each box. The Status Balance ID of MMS060 is rendered as the color of the box: 1=yellow, 2=green, 3=red, else brown. And the Item Number is generated dynamically as a real bar code that can be scanned on the front face of the box.

Here is a screenshot:

The demo uses the Google Earth plugin to render a 3D model that was modeled with Google SketchUp, Ruby scripts to geocode the boxes and identify the front faces of the boxes, PHP to make the 3D Collada model dynamic, SOAP-based Lawson Web Services that calls M3 API, and the PEAR and NuSOAP open source PHP libraries.

The result is useful for sales demos, and as a seed for customers interested in implementing such a solution.

Try for yourself

http://ibrix.info/warehouse3d/

You can try the demo for yourself with your own M3 environment. For that, you will need several things. You will need to install the Google Earth plugin in your browser. You will also need to deploy the Lawson Web Service for MMS060MI provided here; note that your LWS server must be in a DMZ so that the http://www.ibrix.info web server can make the SOAP call over HTTP. Also, you will need to follow the Settings wizard to setup your own M3 environment, user, password, CONO, WHLO, etc. The result is a long URL that is specific to your settings.

Constructing the 3D model

I built a 3D model with Google SketchUp.

Here is the video of the 3D model being built in Google SketchUp:

You can download my resulting SketchUp model here.

Identifying Aisles, Racks, Levels

Then, I set the Aisle, Rack, and Level of each box as in MMS010 using a custom Ruby script for Google SketchUp.

Here is a video that shows the script in action:

You can download this Ruby script here.

You can download the resulting SketchUp model here.

Identifying front faces

Then, I identified each front face of each box so as to dynamically overlay information, such as the Item Number, Item Name, etc. For that, I implemented another Ruby script.

Here is a video of that process:

You can also download this Ruby script here.

Exporting the Collada model

The original model is a SKP filetype, which is binary. I exported the model to a Collada DAE filetype, which is XML. The file is very big, 30.000 lines of XML.

The Collada file contains this:

  • Components (racks, boxes, walls, etc.)
  • Homogenous coordinates (X, Y, Z, H) relative to the model
  • Absolute coordinates (latitude, longitude)
  • Orientation (azimut, etc.)
  • Scale
  • Effects (surface, diffusion, textures, etc.)
  • Colors in RGBA

From the top of my head, the Collada hierarchy in XML is something like this:

Node Instance
	Node Definition
		Instance Geometry
			Instance Material
				Material
					Instance Effect
						Color
						Surface
							Image

Making the model dynamic

The goal is to set the color of each box dynamically, based on the Location of the box, and based on the Inventory Status in MMS060.

Unfortunately, Google Earth doesn’t have an API to change the color of a component dynamically. So, I decided to change the XML dynamically on the server. There are certainly better solutions but that’s the one I chose at the time. And I chose PHP because that’s what I had available on my server ibrix.info; otherwise any dynamic web language (ASP, JSP, etc.) would have been suitable.

In the XML, I found the mapping between the box (nodeDefinition) and its color (material). So, I changed the mapping from hard-coded to dynamic with a PHP function getColor() that determines the color based on the Location and based on the result of the web service call.

The color is determined by the Balance ID: 1=yellow, 2=green, 3=red, else brown. The Balance ID is stored in the SOAP Response of the web service.

Lawson Web Service

I created a SOAP-based Lawson Web Service for MMS060MI. I invoke the SOAP Web Service at the top of the PHP script, and store the Response in a global variable. To call SOAP Web Services, I use NuSOAP, an open source PHP library.

Generating front faces

I dynamically generate a texture for each each front face as a PNG image with the Item Number, Item Description, Quantity, and the bar code. I set the True Type Font, the size, the XY coordinates, and the background color.

Bar code

I generate an image of the bar code based on the Item Number using PEAR, an open source PHP library.

Settings wizard

I made a Settings wizard to assist the user in setting up a demo with their own M3 environment, user, password, CONO, WHLO, etc.

Applications

This Warehouse 3D demo illustrates possible applications such as:

  • Monitoring a warehouse
  • Locating a box for item picking
  • Implementing Augmented Reality to overlay relevant data on top of the boxes

Demo

Finally, I made a demo video using the back projection screen at the Lawson Schaumburg office, and using Johny Lee’s Low-Cost Multi-point Interactive Whiteboards Using the Wiimote and my home made IR pens to convert the back projection screen into a big touch screen. The 3D model in the demo has 10 Aisles, 6 Racks per Aisle (except the first aisle which only has 4 racks), and 4 Levels per Rack. That’s 224 boxes. There is also a floor plan that illustrates that structure.

Limitations

The main limitation of this demo is performance. When programming with Google Earth we do not have the capability of dynamically changing a 3D model. I would have liked to dynamically set the color of a box, and dynamically overlay text on the face of a box. Because that capability is lacking – there’s no such API in the Google Earth API – I chose to generate the XML of the 3D model dynamically on the server. As a result, the server has to send 30k lines of XML to the web browser over HTTP, it has to generate 224 PNG images and transfer them over the network, and the Google Earth plugin has to render it all. As a consequence, it takes between one and four minutes to fully download and render the demo. This design turns out to be inadequate for this type of application. Worse, it is not scalable nor improvable. I would have to re-think the design from scratch to get a more performant result.

Future Work

If I had to continue working on this project (which is not planned), I would implement the following:

  • Ideally, we would generate boxes, colors, and text dynamically on the client-side, with JavaScript and WebGL for example. Google Earth doesn’t support that, and generating the model on the server-side turns out to be a bad design. So we need a different technique.
  • Also, we could use a better 3D client, like O3D.
  • Also, we would need to implement easy keyboard navigation, like the First Person Camera demo, and like the Monster Milktruck demo.
  • Also, we would need to implement hit detection, so as to click on a box and display more M3 data in a pop-up for example. Google Earth supports even listeners but doesn’t yet support hit detection.
  • Finally, we would need to improve performance by an order of magnitude.

Thanks

Special thanks to Gunilla A for sponsoring this project and making it possible.

Resources

  • Download Ruby script to set the Aisle, Rack, and Level of each box as in MMS010
  • Download Ruby script to identify each front face of each box so as to dynamically overlay information
  • Download SketchUp model with floor plan, geo-location, racks, and walls
  • Download SketchUp model of boxes identified by Stock Location
  • Watch video of the 3D model being built in Google SketchUp
  • Watch video of the process of setting the Aisle, Rack, and Level of each box as in MMS010
  • Watch video of the process of identifying each front face of each box
  • Watch video of the demo on the large touch screen
  • Download Lawson Web Service for MMS060MI

Related articles

UPDATE

2012-09-27: I added the SketchUp models and Ruby scripts for download.

Detect entry mode in Smart Office script

Here is a solution for a Script in Lawson Smart Office to detect the entry mode of an M3 panel: 1-Create, 2-Change, 3-Copy, 4-Delete, or 5-Display.

Scenario

This solution is useful for a scenario like this. Suppose your script dynamically adds an input field to an M3 panel. The input field could be used for example to enter a discount amount which will re-calculate a net value in another field. You would like to either enable that input field so that it becomes editable for the user, either disable it so that it is read-only. That will match the behavior of the M3 panel, for better usability. For that, you will need to tell apart if the user entered the M3 panel in Change mode or in Display mode.

Detect the entry mode

Ideally, we would like to use an API in Smart Office like GetEntryMode() that would return the entry mode. But there is no such API.

Otherwise, we would like to simply read the Control’s properties such as IsReadOnly or IsEnabled. But somehow they incorrectly return false.

The workaround is to read the LSTOPT value that is sent by the M3 panel when it makes the HTTP Request to the M3 server. That value is sent from panels A and B. Note that the option Create returns -1 (negative) instead of 1 (positive).

function OnRequesting(sender: Object, e: CancelRequestEventArgs) {
    if (e.CommandType == 'LSTOPT') {
        e.CommandValue // the entry mode: -1, 2, 3, 4, or 5
    }
}

Set the value

Once we get the value from panels A or B, we have to store it somewhere because Smart Office will unload the script and our value off memory when it executes the said Option and the value will not be available anymore in subsequent panels. We can use the InstanceCache to store the value in the current Smart Office session. The value will then be available in panels E, F, G, etc. In the following example I called my key LSTOPT but you can choose another one:

InstanceCache.Add(controller, 'LSTOPT', e.CommandValue);

Get the value

Once we are in panels E, F, G, etc. we retrieve the value from the InstanceCache like this:

if (InstanceCache.ContainsKey(controller, 'LSTOPT')) {
    var LSTOPT = InstanceCache.Get(controller, 'LSTOPT'); // get the entry mode
    if (LSTOPT == -1) {
        // Create
    } else if (LSTOPT == 2) {
        // Change
    } else if (LSTOPT == 3) {
        // Copy
    } else if (LSTOPT == 4) {
        // Delete
    } else if (LSTOPT == 5) {
        // Display
    } else {
        // entry mode unknown
    }
} else {
     // entry mode not set
}

Installation

This script has a getter and a setter:

  • The setter must be placed in the OnRequesting event handler of panel A or B.
  • The getter must be attached in the panel (E, F, G, etc.) where you want to know the entry mode.

Tests

I tried this technique in the following conditions:

  • The technique works whether the user selects the option from the Options menu, or from double-clicking a record in panel B, or by selecting the option in the right-click context menu.
  • Also, it works whether the users enters the M3 program via panel A or via panel B.
  • Also, it works with multiple instances of the M3 program running at the same time.
  • Also, it works with different M3 programs using the same script.

Sample source code

Here is a sample source code for the setter:

import MForms;

package MForms.JScript {
	class EntryModeSetter {
		var controller;
		public function Init(element: Object, args: Object, controller: Object, debug: Object) {
			this.controller = controller;
			controller.add_Requesting(OnRequesting);
			controller.add_Requested(OnRequested);
		}
		function OnRequesting(sender: Object, e: CancelRequestEventArgs) {
			if (e.CommandType == 'LSTOPT') {
				InstanceCache.Add(controller, 'LSTOPT', e.CommandValue); // set the entry mode
			}
		}
		function OnRequested(sender: Object, e: RequestEventArgs) {
			sender.remove_Requesting(OnRequesting);
			sender.remove_Requested(OnRequested);
		}
	}
}

Here is a sample source code for the getter:

import MForms;

package MForms.JScript {
	class EntryModeGetter {
		public function Init(element: Object, args: Object, controller: Object, debug: Object) {
			if (InstanceCache.ContainsKey(controller, 'LSTOPT')) {
				var LSTOPT = InstanceCache.Get(controller, 'LSTOPT'); // get the entry mode
				if (LSTOPT == -1) {
					// Create
				} else if (LSTOPT == 2) {
					// Change
				} else if (LSTOPT == 3) {
					// Copy
				} else if (LSTOPT == 4) {
					// Delete
				} else if (LSTOPT == 5) {
					// Display
				} else {
					// entry mode unknown
				}
			} else {
				// entry mode not set
			}
		}
	}
}

That’s it!

M3 + Augmented Reality (idea)

Here is an idea that would be great to implement: M3 + Augmented Reality. I believe AR to be one of the next big revolutions in the software industry, and the technology is available today. We have mobile phones with cameras, GPS, compass, and millimetric indoor radio positioning, fast CPU for feature registration, localization and mapping, REST Web Services, etc. It went from being mostly reserved to research labs, to being the hype of emerging start ups. Get ready for the future 🙂

How to decrypt Smart Office’s encrypted traffic

In the PowerPoint document How to decrypt Smart Office’s encrypted traffic I describe how to intercept and decrypt the encrypted HTTPS traffic from Lawson Smart Office which sometimes cannot be captured with Fiddler, and which is unreadable in Wireshark. This technique is useful for troubleshooting Mashups, Smart Office, Personalized Scripts, IBrix, etc.

Does that demonstrate a security flaw in Smart Office? Not at all. Read the last chapter in the document.

Cash drawer integration with Smart Office Script

Here is a demo video of the integration I implemented for one of our customers between a cash drawer and Lawson Smart Office using a Script. (If you want to skip the talk, jump to time stamp 3:31 in the video to see the cash drawer open.)

The result is a single cash drawer controlled by multiple Points of Sales throughout the showroom. Each Point of Sales runs Smart Office. The solution requires authorization (who can access the cash drawer) and auditing (who opened the cash drawer when) for security purposes. For the user its just a click on a new button Open Register.

I implemented the solution as client/server: the clients are Personalized Scripts in JScript.NET for Smart Office, the server is a custom Java program connected to the cash drawer with JavaPOS and a Serial cable, a custom bridge between .NET and Java made with a tiny HTTP server, and Properties file.

Current date in Mashup

I found this post about getting the current date in XAML. I gave it a try in a Mashup.

Here’s the code to get the current date and time in various formats in a Mashup. You can complement this with the Date picker in Mashup.

<Gridxmlns:sys=”clr-namespace:System;assembly=mscorlib”>

<TextBlock Text="{Binding Source={x:Static sys:DateTime.Today}, StringFormat='Today is: {0:dddd, MMMM dd, yyyy, hh:mm:ss}'}" />
<TextBlock Text="{Binding Source={x:Static sys:DateTime.Today}, StringFormat='One of LSO formats: {0:MMddyy}'}" />
<TextBlock Text="{Binding Source={x:Static sys:DateTime.Today}, StringFormat='M3 API format: {0:yyyyddMM}'}" />
<TextBlock Text="{Binding Source={x:Static sys:DateTime.Today}, StringFormat='\{0:yyyy/MM/dd}'}" />
<TextBlock Text="{Binding Source={x:Static sys:DateTime.Today}, StringFormat='\{0:yyyyddMM}'}" />

And here is a screenshot of the result with the current date displayed in five different formats:

UPDATE 2012-03-26: To get the date in a TextBox add the binding property Mode=OneWay, for example:

<TextBox Text="{Binding Source={x:Static sys:DateTime.Today}, StringFormat='yyyy', Mode=OneWay}" />