Skip to content

Quick Start

A step-by-step guide to building a .NET equipment application with Port.


1. Converting Documents to Pages

1.1 What is a Page?

A Page is the fundamental data definition unit in the Port system. A single .page file is a collection of related Entries. Once pushed to the Port server, those entries are created in the in-memory data store.

Page = a list of Entries belonging to one functional unit (e.g. IO, Sensor, Motor)

Pages can be generated automatically from external documents (.docx, .xlsx, .csv), or defined directly in C# using the [Page] attribute.


1.2 Page Structure and Role

.page File Syntax

[EntryKey]  [DataType]  [pkg:PackageName]  [property:{...}]
Field Required Description
EntryKey Unique identifier for the entry
DataType Data type (f8, Enum.OnOff, char, etc.)
pkg: optional Package to bind to
property: optional Hardware mapping info (JSON)

Example (io.page):

Bulb1OnOff    Enum.OnOff  property:{"IO.No":"D0.01","Model":"IODevice"}
Bulb2OnOff    Enum.OnOff  property:{"IO.No":"D0.02","Model":"IODevice"}
Bulb1Temp     f8          property:{"IO.No":"A0.01","Model":"IODevice"}
Bulb2Temp     f8          property:{"IO.No":"A0.02","Model":"IODevice"}

Entry Structure

An Entry is the smallest data unit in the Port system. Each entry has:

  • Key — fully-qualified identifier (category.entryName, e.g. Bulb1.OnOff)
  • DataType — type of the value (numeric, enum, char, etc.)
  • Value — current value held in memory (real-time state)
  • Property — supplementary config such as hardware address or unit (JSON)

SECS-compatible data types:

Type Description
f4 / f8 Floating point (4 / 8 bytes)
i1 ~ i8 Signed integer
u1 ~ u8 Unsigned integer
char ASCII string
Enum.XXX Enumeration referencing a pre-defined enum
bool Boolean

Local Entry Definitions (category-scoped)

Logical setpoints with no corresponding hardware document entry (e.g. a target temperature) are defined in a separate .page file inside the category folder:

bulb1/.page:

TargetTemp  f8

1.3 Converting a Document to a Page

The framework translates specification tables into structured data models.

Source Specification

Assume a table exists in C:\Users\admin\Documents\IO.docx:

IO.No Description Model
D0.01 Bulb1.OnOff IODevice
D0.02 Bulb2.OnOff IODevice
A0.01 Bulb1.Temp IODevice
A0.02 Bulb2.Temp IODevice

Figure 1: Sample Specification Table

Define a Document Model

Map the table columns to a C# class using attributes.

public class IOModel
{
    [ColumnHeader("IO.No"), EntryProperty]
    public string IONo { get; set; } = null!;

    [ColumnHeader("Description"), EntryKey]
    public string Description { get; set; } = null!;

    [ColumnHeader("Model"), EntryProperty]
    public string Model { get; set; } = null!;
}
Attribute Role
[ColumnHeader] Maps the property to an Excel/Word column by name
[EntryKey] Designates this column as the entry name
[EntryProperty] Includes this column in the property:{...} JSON

Generated .cs File

Port.Document<T> writes a constants class alongside the .page file.

// Auto-generated by Port. Do not edit manually.
namespace sample
{
    public static class Io
    {
        public const string Bulb1OnOff = "Bulb1OnOff";
        public const string Bulb2OnOff = "Bulb2OnOff";
        public const string Bulb1Temp  = "Bulb1Temp";
        public const string Bulb2Temp  = "Bulb2Temp";
    }
}

Figure 2: Sample .cs File


1.4 Inline Entry Definitions with [Page]

For entries that don't come from an external document (e.g. EFEM I/O signals), define them directly in a C# class decorated with [Page].

Auto-generated class (from Port.Pull)

Port.Pull writes a partial class file (e.g. entry.cs) containing every pulled entry as a const string. This file is regenerated on every pull — do not edit it manually.

entry.cs also includes a Defined class that contains all enum definitions stored in the database, generated automatically alongside the entry constants.

// Auto-generated by Port.Pull — do not edit manually.
namespace Portdic
{
    public partial class EFEM
    {
        public const string LP1_Main_Air_i = "EFEM.LP1_Main_Air_i";
        public const string LP2_Cont_o     = "EFEM.LP2_Cont_o";
        // ... all pulled entries ...
    }

    public partial class Defined
    {
        public enum OffOn : int
        {
            Off = 0,
            On  = 1,
        }

        public enum UnkOffOn : int
        {
            Unknown = 0,
            Off     = 1,
            On      = 2,
        }
        // ... all enums from the database ...
    }
}

User-defined extension (CustomEFEM)

Add entries and enum types not yet in the pulled class by creating a separate [Page]-decorated partial class.

Rules: - The field value is the enum key — EnumName on [PageEntry] must match it exactly. - [PageEnum] fields are pushed before [PageEntry] fields so enum references always resolve. - Enum definitions land in app/.enum so port pull keeps them in the right file.

using Portdic;
using Portdic.SECS;

namespace sample.Controller
{
    [Page("EFEM")]
    public partial class CustomEFEM
    {
        // ── Enum declarations ─────────────────────────────────────────
        [PageEnum("Unknown", "Off", "On")]
        public const string UnkOffOn = "UnkOffOn";

        [PageEnum("Unknown", "TurnOff", "TrunOn")]
        public const string UnkTurnOffOn = "UnkTurnOffOn";

        // ── Entry declarations ────────────────────────────────────────
        [PageEntry(PortDataType.Char)]
        public const string LP1_Cont1_o = "EFEM.LP1_Cont1_o";

        [PageEntry(PortDataType.Enum, EnumName = UnkTurnOffOn)]
        public const string LP1_OffOn_o = "EFEM.LP1_OffOn_o";

        // ── Package & Property binding ────────────────────────────────
        // Package: "Name.PropertyName" → pushed as pkg:Name.PropertyName
        // Property: raw JSON string   → pushed as property:{...}
        [PageEntry(PortDataType.Enum, EnumName = "OffOn",
            Package  = "Bulb1.OffOn",
            Property = "{\"MIN\":0,\"MAX\":1}")]
        public const string LP1_BulbOnOff_o = "EFEM.LP1_BulbOnOff_o";
    }
}

Push the class instance before starting the port server:

Port.Push("sample", new CustomEFEM());

1.5 How to Push and Pull Pages

Push — Register Entries with the Server

Three overloads are available depending on the source of the entry data:

Overload Use case
Push(reponame, obj) Push from a [Page]-decorated class instance
Push(reponame, page) Push from a Page returned by Document<T>.NewPage()
Push(repo) Push an entire directory via the port REST API (RepositoryInfo)
// From a [Page]-decorated class (enums + entries)
Port.Push("sample", new CustomEFEM());

// From a document-derived Page (entries only)
Port.Push("sample", ioDoc.NewPage("Device"));

Pull — Reconstruct Files from the DB

Port.Pull calls port pull {reponame} via the CLI and writes reconstructed .page, .enum, and related files into a port/ subfolder inside the specified root directory.

// Syntax
Port.Pull(string reponame, string root);

// Example: writes files to D:\sample\Repo\pull\port\
Port.Pull("sample", @"D:\sample\Repo\pull\");

port.exe is resolved from the parent of %PortPath% first; if not found, it falls back to the system PATH.

Full Initialization Pattern (#if DEBUG)

The recommended pattern in a #if DEBUG block synchronizes the DB with your latest class definitions before loading the repository at runtime:

#if DEBUG
// 1. Ensure project root exists
Port.Repository.New(@"D:\sample\Repo\pull\", "sample");

// 2. Convert external document to entries
var ioDoc = Port.Document<IOModel>(@"C:\Users\admin\Documents\IO.docx");
ioDoc.Where(v => v.Key.Contains("OnOff")).ToList()
     .ForEach(v => v.DataType = "Enum.OnOff");
ioDoc.Where(v => v.Key.Contains("Temp")).ToList()
     .ForEach(v => v.DataType = "f8");

if (ioDoc.Count > 0)
{
    ioDoc.New(@"C:\Users\admin\Documents\sample\.page\io.page");
    ioDoc.New(@"C:\Users\admin\Documents\sample\.net\io.cs");
}

// 3. Push inline-defined entries (enums + EFEM signals)
Port.Push("sample", new CustomEFEM());

// 4. Push document-derived entries
Port.Push("sample", ioDoc.NewPage("Device"));

// 5. Reconstruct .page/.enum files from DB
Port.Pull("sample", @"D:\sample\Repo\pull\");
#endif

2. Mapping Pages to Models

2.1 What is a Model?

A Model is the data-binding layer that connects Port Entries to C# properties.

  • Declare the class with the [Model] attribute.
  • Each property is linked to a specific Entry via [ModelBinding].
  • Controllers and Flows read and write Entry values through the Model.

2.2 Relationship Between Model and Page

.page file (Entry definitions)
    ↓  Push
Port in-memory DB (Entry values)
    ↕  ModelBinding
Model (C# property ↔ Entry mapping)
Controller / Flow (business logic)

The Page is the Single Source of Truth; the Model is a type-safe view of that data in code.


2.3 How to Map a Model to a Page

Use the [ModelBinding(instanceKey, entryKey)] attribute.

  • First argument — controller instance key (e.g. "Bulb1", "LP1")
  • Second argument — Entry constant from the auto-generated .cs file
[Model]
public class BulbModel
{
    // Both "Bulb1" and "Bulb2" instances share the same property structure
    [ModelBinding("Bulb1", Io.Bulb1OnOff)]
    [ModelBinding("Bulb2", Io.Bulb2OnOff)]
    public Entry OnOff { get; set; }

    [ModelBinding("Bulb1", Io.Bulb1Temp)]
    [ModelBinding("Bulb2", Io.Bulb2Temp)]
    public Entry Temp { get; set; }

    [ModelBinding("Bulb1", Io.Bulb1TargetTemp)]
    [ModelBinding("Bulb2", Io.Bulb2TargetTemp)]
    public Entry TargetTemp { get; set; }
}

[ModelBinding] attributes can freely mix entries from the auto-generated class and a user-defined [Page] class:

[Model]
public class LoadportModel
{
    // Entry added via CustomEFEM
    [ModelBinding("LP1", CustomEFEM.LP1_Cont1_o)]
    // Entry from the auto-generated EFEM class
    [ModelBinding("LP2", EFEM.LP2_Cont_o)]
    public Entry LP_Cont_o { get; set; }

    [ModelBinding("LP1", CustomEFEM.LP1_OffOn_o)]
    public Entry LP_OffOn_o { get; set; }
}

3. Using Models in Controllers and Flows

3.1 What is a Controller?

A Controller is a logic container that holds one or more Flows.

  • Declare it with the [Controller] attribute.
  • Register it with Port.Add<TController, TModel>(instanceKey).
  • The same Controller can be reused across multiple instances (e.g. LP1, LP2).
Port.Add<BulbController, BulbModel>("Bulb1");
Port.Add<BulbController, BulbModel>("Bulb2");
Port.Run();

3.2 What is a Flow?

A Flow is a sequential workflow defined inside a Controller.

  • Declare an inner class with [Flow("FlowName")].
  • Define each step as a method decorated with [FlowStep(order)].
  • Trigger execution externally via Port.Set("Bulb1", FlowAction.Executing).

3.3 Handling a Model Inside a Flow

Receive the Model directly as a method parameter to access Entry values:

[Controller]
public class BulbController
{
    [Flow("BulbOn")]
    public class BulbOn
    {
        [FlowHandler]
        public IFlowHandler handler { get; set; } = null!;

        [FlowStep(0)] // Validation Step
        public void CheckInitialState(BulbModel model)
        {
            if (model.Temp.Value <= 100)
                handler?.Next();
        }

        [FlowStep(1)] // Action Step
        public void TurnOn(BulbModel model)
        {
            model.OnOff.Set("On");
            handler?.Next();
        }

        [FlowStep(2)] // Monitoring Step
        public void MonitorTemperature(BulbModel model)
        {
            if (model.Temp.Value >= model.TargetTemp.Value)
            {
                model.OnOff.Set("Off");
                handler?.Next(); // Marks Flow as Completed
            }
        }
    }
}

Start / cancel a Flow:

Port.Set("Bulb1", FlowAction.Executing);   // Start BulbOn Flow
Port.Set("Bulb1", FlowAction.Canceled);    // Cancel

3.4 Application Entry Point (Port.App<T>)

Port.App<T>() is the mandatory first call when using the [Port] attribute-based initialization style. Decorate your application class with [Port] to declare the repository name and pull path, then call Port.App<T>() before any other Port API.

[Port("sample")]
public class SampleApp { }

// In startup (e.g. constructor or Program.cs) — must be called first
Port.App<SampleApp>();

Port.App<T>() reads the [Port] attribute on T, creates the repository via Port.Repository.New(pullPath, reponame), and starts the PortDic instance. All subsequent Port.Push, Port.Pull, and Port.Repository.Load calls depend on this initialization being completed.

Full Startup Example

[Port("sample")]
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        try
        {
            Port.App<MainWindow>();

#if DEBUG
            var ioDoc = Port.Document<IOModel>(@"C:\Users\admin\Documents\IO.docx");

            ioDoc.Where(v => v.Key.Contains("OnOff")).ToList()
                 .ForEach(v => v.DataType = "Enum.OnOff");
            ioDoc.Where(v => v.Key.Contains("Temp")).ToList()
                 .ForEach(v => v.DataType = "f8");

            if (ioDoc.Count > 0)
            {
                ioDoc.New(@"C:\Users\admin\Documents\sample\.page\io.page");
                ioDoc.New(@"C:\Users\admin\Documents\sample\.net\io.cs");
            }

            Port.Push("sample", new CustomEFEM());
            Port.Push("sample", ioDoc.NewPage("Device"));
            Port.Pull("sample", @"D:\PORT\SampleArduinoLib\sample\Repo\pull\");
#endif

            Port.Add<LoadportController, LoadportModel>("LP1");
            Port.Add<LoadportController, LoadportModel>("LP2");

            Port.OnReady += Port_OnReady;
            Port.Run();
        }
        catch (Exception ex)
        {
            MessageBox.Show(
                $"{ex.Message}\n\nInner: {ex.InnerException?.Message}\n\nStack: {ex.StackTrace}");
        }
    }
}

4. Handlers

4.1 Handler Types

Interface Purpose
IFlowHandler Basic Flow progression control (Next())
IFlowWithModelHandler<T> Flow event subscriptions that carry the Model
ISchedulerHandler Transfer-completion scheduling

4.2 Handler Roles and Usage

IFlowHandler — Basic Progression Control

[FlowHandler]
public IFlowHandler handler { get; set; } = null!;

handler.Next();   // Advance to the next FlowStep
handler.Done();   // Synchronously force the Flow to Idle

IFlowWithModelHandler\<T> — Event-Driven Model Access

IFlowCACD<T> is the recommended interface for equipment transfer flows. Declare the handler as IFlowWithModelHandler<T> to gain access to flow lifecycle events that carry the model. Subscribe in [Preset] — subscriptions survive across all executions of the same flow key.

[Controller]
internal class WTRController
{
    [Flow("Pick")]
    public class Pick : IFlowCACD<WTRCommModel>
    {
        [Handler]
        public IFlowWithModelHandler<WTRCommModel> handler { set; get; } = null!;

        [Handler]
        public ISchedulerHandler scheduler { set; get; } = null!;

        // Preset() runs once at registration time.
        // Event subscriptions made here are preserved for every subsequent execution.
        [Preset]
        public void Preset()
        {
            handler.SetLogger(@"D:\log");
            handler.OnFlowFinished += (s, e) =>
                scheduler.TransferCompleted(e.Model.SelectedArm);
        }

        public void CheckStatus(WTRCommModel m) { Task.Delay(300).Wait(); handler.Next(); }
        public void Action(WTRCommModel m)      { handler.Next(); }
        public void CheckAction(WTRCommModel m) { handler.Next(); }
        public void Done(WTRCommModel m)        { handler.Done(); }
    }
}

handler.Next() vs handler.Done()

Method Behaviour
handler.Next() Advances to the next step via the normal state machine
handler.Done() Synchronously forces the flow to Idle before returning, then fires OnFlowFinished

Use handler.Done() in the last step when a sibling flow must start inside the OnFlowFinished callback. Using handler.Next() there risks an AlreadyExecutingFlowException because the flow has not yet transitioned to Idle when the callback fires.


4.3 Handler Property Table

IFlowCACD\<T> — Standard 4-Step Flow

The recommended interface for equipment transfer flows. Step order is fixed:

Step Method Description
0 CheckStatus Verify preconditions before acting
1 Action Execute the physical operation
2 CheckAction Confirm the operation completed correctly
3 Done Finalize — call handler.Done() to close the flow

IFlowWithModelHandler\<T> Lifecycle Events

Event When Fired Args
OnFlowFinished Flow reached the Done step and completed FlowFinishedWithModelArgs<T> — Model, timing, step records
OnFlowOccured A step transition occurred PortFlowOccuredWithModelArgs<T> — Model, step status
OnFlowIssue Flow stopped due to an alarm PortFlowIssueWithModelArgs<T> — Model, alarm code

All event args expose a Model property that is the singleton model instance bound to the flow key, so reading e.Model.Target.Value always reflects the current shared-memory value.


5. Calling Packages

5.1 What is a Package?

A Package is a reusable device-driver module that bridges a physical device (Bulb, Heater, IO card, etc.) with Port Entries.

  • Link a package in a .page file using the pkg:PackageName.PropertyName syntax.
  • Declare the C# class with the [Package] attribute.
  • When an Entry value changes, the matching property setter on the package is called automatically.

5.2 How to Call Package APIs

Linking a Package in a .page File

Bulb1OnOff  Enum.OnOff  pkg:Bulb.OffOn  property:{"IO.No":"D0.01"}
Bulb1Temp   f8          pkg:Bulb.Temp   property:{"IO.No":"A0.01"}

Linking a Package from a [Page] Class

Use the Package parameter of the [PageEntry] attribute:

[Page("EFEM")]
public partial class CustomEFEM
{
    // Package: "Name.PropertyName" → pushed as pkg:Name.PropertyName
    // Property: raw JSON string   → pushed as property:{...}
    [PageEntry(PortDataType.Enum, EnumName = "OffOn",
        Package  = "Bulb1.OffOn",
        Property = "{\"MIN\":0,\"MAX\":1}")]
    public const string LP1_BulbOnOff_o = "EFEM.LP1_BulbOnOff_o";
}

Implementing a Package Class

[Package]
public class Bulb
{
    [Logger]
    public ILogger Logger { get; set; }

    [Property]
    public IProperty Property { get; set; }

    [Valid("Device not connected")]
    public bool Valid() => serialPort.IsOpen;

    // Setter is called automatically when the Entry value changes
    [API(EntryDataType.Enum)]
    public string OffOn
    {
        set { Logger.Write($"[INFO] Bulb OffOn → {value}"); }
        get => _offOn;
    }

    private string _offOn = "Off";
    private SerialPort serialPort = new SerialPort();
}

Push the [Page] class before Port.Run():

Port.Push("sample", new CustomEFEM());

6. Port.Set

Port.Set writes a value to the specified Entry in the in-memory DB immediately. If a Package is bound to that Entry, its setter is invoked, triggering the physical operation.

Basic Usage

// Write an Entry value
Port.Set("Bulb1.OnOff", "On");
Port.Set("Bulb1.TargetTemp", "85.0");

// Trigger / cancel a Flow
Port.Set("Bulb1", FlowAction.Executing);
Port.Set("Bulb1", FlowAction.Canceled);

Set via Model Property

// Inside a Flow — set directly through the model parameter
model.OnOff.Set("On");
model.TargetTemp.Set("80.0");

Set via IFlowModel (@ Binding)

[FlowModel]
public IFlowModel model { get; set; }

model.Set("@OnOff", "On");   // @ prefix references a ModelBinding key

7. Port.Get

Port.Get reads the current value of the specified Entry from the in-memory DB. It always returns the latest value and supports high-performance in-memory access.

Basic Usage

// Read an Entry value
string onOff = Port.Get("Bulb1.OnOff");   // → "On" or "Off"

if (Port.Get("Bulb1.OnOff") == "On")
{
    Port.Set("Bulb1.OnOff", "Off");
}

Get via Model Property

// Access the current value through the Entry.Value property
double temp   = (double)model.Temp.Value;
string status = (string)model.OnOff.Value;

Get via IFlowModel (@ Binding)

var value = model.Get("@Temp");   // Reads the Entry bound to the ModelBinding key

8. Controlling Set/Get with Rule Scripts

A Rule Script defines write guards and periodic automation using .rule files stored in the app/ directory (e.g. app/.rule). Two rule types are available: set and get.


8.1 set Rule — Write Guard

A set rule is evaluated synchronously whenever Port.Set is called on the matching key. It acts as a gate: if the action condition is not satisfied, the write is blocked and an error is returned.

Syntax

set("write condition", "allow condition")
Argument Role
First — write condition Specifies which write attempt triggers this rule.
Bare key (Bulb1.OnOff) matches any write;
key+op+value (Bulb1.OnOff == Off) matches only that value.
Second — allow condition Boolean expression evaluated against the current memory state.
If true → write is allowed. If false → write is blocked.

Example

// Allow turning Bulb1 off only when temperature is safe (>= 80)
set("Bulb1.OnOff == Off", "Bulb1.Temp >= 80")

// Block any write to Bulb2.OnOff while Bulb1 is still On
set("Bulb2.OnOff", "Bulb1.OnOff == Off")

set rules evaluate the allow condition against the state before the write. A write is blocked if any matching rule's allow condition is false.


8.2 get Rule — Periodic Automation

A get rule runs in the background every second. When the condition becomes true, the listed assignments execute once (one-shot). They will not re-trigger until the condition first goes false and then becomes true again.

Syntax

get("condition", "key1=value1; key2=value2; ...")
Argument Role
First — condition Boolean expression evaluated against current memory state
Second — assignments Semicolon-separated key=value pairs to write when condition is true

Example

// When Bulb1 temperature reaches 80 or above, turn it off automatically
get("Bulb1.Temp >= 80", "Bulb1.OnOff=Off")

// When both temperatures are in range, reset both bulbs
get("(Bulb1.Temp >= 0) && (Bulb2.Temp >= 0)", "Bulb1.OnOff=Off; Bulb2.OnOff=Off")

The condition must transition false → true to re-execute. Once fired, assignments will not repeat until the condition resets.


8.3 Operators

Comparison operators (both rule types):

Operator Description
== Equal (string or numeric)
> Greater than
< Less than
>= Greater than or equal
<= Less than or equal

Logical operators (combine conditions with parentheses):

Operator Description
&& Logical AND
\|\| Logical OR

Always wrap each sub-condition in parentheses when combining:

(Bulb1.Temp >= 80) && (Bulb2.Temp >= 80)
(Bulb1.OnOff == Off) || (Bulb2.OnOff == Off)

8.4 Complete .rule File Example

// ── SET rules (write guards) ──────────────────────────────────────────
// Block turning Bulb1 off unless temperature has reached the target
set("Bulb1.OnOff == Off", "Bulb1.Temp >= 80")

// Block turning Bulb2 off unless both bulbs are warm enough
set("Bulb2.OnOff == Off", "(Bulb1.Temp >= 80) && (Bulb2.Temp >= 80)")

// ── GET rules (periodic automation) ──────────────────────────────────
// Auto-turn off when temperature exceeds limit
get("Bulb1.Temp >= 100", "Bulb1.OnOff=Off")

// Reset both bulbs once temperatures are both safe
get("(Bulb1.Temp >= 0) && (Bulb2.Temp >= 0)", "Bulb1.OnOff=Off; Bulb2.OnOff=Off")

8.5 Condition Monitoring Inside a Flow

For logic that depends on flow state, express conditions directly inside Flow steps instead of a rule file:

[FlowStep(2)]
public void MonitorTemperature(BulbModel model)
{
    if (model.Temp.Value >= model.TargetTemp.Value)
    {
        model.OnOff.Set("Off");
        handler?.Next();
    }
}

8.6 Data Flow

Port.Set("Bulb1.OnOff", "Off") called
set rule evaluated (write guard)
    ├─ allow condition true  → write proceeds → Package setter called → Hardware responds
    └─ allow condition false → write BLOCKED, error returned

Every 1 second (background)
get rule condition evaluated
    ├─ condition true (first time) → assignments executed → Port.Set called internally
    └─ condition false or already fired → no action

Rule Scripts operate independently of Flows. Conditions can be modified at any time by editing the .rule file — no code recompilation required.