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¶
| 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:
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";
}
}
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:
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
.csfile
[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
.pagefile using thepkg:PackageName.PropertyNamesyntax. - 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():
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)¶
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¶
| 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")
setrules 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¶
| 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:
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
.rulefile — no code recompilation required.