Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
355 changes: 355 additions & 0 deletions interface/harpdevice.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ parameter name="Namespace" type="string" #>
<#@ parameter name="MetadataPath" type="string" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ include file="Interface.tt" #><##>
<#@ output extension=".py" #>
<#
var deviceMetadata = TemplateHelper.ReadDeviceMetadata(MetadataPath);
var publicRegisters = deviceMetadata.Registers.Where(register => register.Value.Visibility == RegisterVisibility.Public).ToList();
var deviceRegisters = deviceMetadata.Registers;
var deviceName = deviceMetadata.Device;
var bitmaskTypes = new HashSet<string>(deviceMetadata.BitMasks.Keys);
var groupmaskTypes = new HashSet<string>(deviceMetadata.GroupMasks.Keys);
#>
from dataclasses import dataclass
from enum import IntEnum, IntFlag

from harp.protocol import MessageType, PayloadType
from harp.protocol.exceptions import HarpException, HarpReadException, HarpWriteException
from harp.protocol.messages import HarpMessage
from harp.serial import Device
<#
// Order is important for acronyms, so longer acronyms should be listed first
string[] AcronymExceptions = new[] { "DIO", "DI", "DO", "IR", "USB" };
// Regex for units: ms, hz, v (case-insensitive)
Regex UnitRegex = new Regex(@"^\d+(ms|hz|v)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

string ToSnakeCase(string name, bool allCaps = false)
{
if (string.IsNullOrEmpty(name)) return name;
var sb = new System.Text.StringBuilder();
int i = 0;
while (i < name.Length)
{
bool matched = false;
// Acronym exceptions
foreach (var acronym in AcronymExceptions)
{
if (i + acronym.Length <= name.Length &&
string.Compare(name, i, acronym, 0, acronym.Length, ignoreCase: false, culture: null) == 0)
{
if (i > 0 && sb[sb.Length - 1] != '_')
sb.Append('_');
sb.Append(acronym.ToLowerInvariant());
i += acronym.Length;
matched = true;
break;
}
}
if (matched) continue;

// Digits+unit pattern (e.g., 5V, 10300Hz, 1Ms)
var unitMatch = UnitRegex.Match(name.Substring(i));
if (unitMatch.Success)
{
int matchLen = unitMatch.Length;
int nextIdx = i + matchLen;
// Only treat as a unit if next char does not exist or is uppercase
if (nextIdx >= name.Length || char.IsUpper(name[nextIdx]))
{
if (i > 0 && sb[sb.Length - 1] != '_')
sb.Append('_');
sb.Append(name.Substring(i, matchLen).ToLowerInvariant());
i += matchLen;
// If next char is uppercase, treat as word boundary
if (nextIdx < name.Length && char.IsUpper(name[nextIdx]))
sb.Append('_');
continue;
}
}

// Digits: keep with previous word, and if next char is uppercase, treat as word boundary
if (char.IsDigit(name[i]))
{
sb.Append(name[i]);
int nextIdx = i + 1;
if (nextIdx < name.Length && char.IsUpper(name[nextIdx]))
sb.Append('_');
i++;
continue;
}

// Default PascalCase to snake_case
var c = name[i];
if (char.IsUpper(c) && i > 0 && sb[sb.Length - 1] != '_')
sb.Append('_');
sb.Append(char.ToLowerInvariant(c));
i++;
}
var result = sb.ToString();
return allCaps ? result.ToUpperInvariant() : result;
}

// Helper for type conversion
Func<string, string> pyType = t =>
{
switch (t)
{
case "byte":
case "sbyte":
case "ushort":
case "short":
case "uint":
case "int":
case "ulong":
case "long":
return "int";
case "float":
return "float";
case "ArraySegment<byte>":
case "byte[]":
return "bytes";
case "short[]":
case "ushort[]":
case "uint[]":
case "int[]":
case "long[]":
return "list[int]";
case "EnableFlag":
return "bool";
default:
return t; // fallback
}
};
#>
<# // Payload dataclasses
var payloadTypes = new HashSet<string>();
foreach (var registerMetadata in deviceRegisters)
{
var register = registerMetadata.Value;
if (register.PayloadSpec == null) continue;
var interfaceType = TemplateHelper.GetInterfaceType(registerMetadata.Key, register);
if (!payloadTypes.Add(interfaceType)) continue;
#>


@dataclass
class <#= interfaceType #>:
<#
foreach (var member in register.PayloadSpec)
{
var memberType = TemplateHelper.GetInterfaceType(member.Value, register.Type);
if (!string.IsNullOrEmpty(member.Value.Description))
{
#>
# <#= member.Value.Description #>
<# }
#>
<#= member.Key #>: <#= pyType(memberType) #>
<#
}
}
#>
<# // BitMask enums
foreach (var bitMask in deviceMetadata.BitMasks)
{
var mask = bitMask.Value;
#>


class <#= bitMask.Key #>(IntFlag):
"""
<#= mask.Description #>

Attributes
----------
<#
foreach (var bitField in mask.Bits)
{
var fieldInfo = bitField.Value;
#>
<#= ToSnakeCase(bitField.Key, allCaps:true) #> : int
<#= string.IsNullOrEmpty(fieldInfo.Description) ? "_No description currently available_" : fieldInfo.Description #>
<# } #>
"""

<#
// Add a NONE value if there are no bits set
var bitCount = mask.Bits.Count;
if (!mask.Bits.Values.Any(fieldInfo => fieldInfo.Value == 0))
{
#>
NONE = 0x0
<#
}
foreach (var bitField in mask.Bits)
{
var fieldInfo = bitField.Value;
#>
<#= ToSnakeCase(bitField.Key, allCaps:true) #> = 0x<#= fieldInfo.Value.ToString("X") #>
<#
}
}
#>
<#
// GroupMask enums
foreach (var groupMask in deviceMetadata.GroupMasks)
{
var mask = groupMask.Value;
#>


class <#= groupMask.Key #>(IntEnum):
"""
<#= mask.Description #>

Attributes
----------
<#
foreach (var member in mask.Values)
{
var memberInfo = member.Value;
#>
<#= ToSnakeCase(member.Key, allCaps:true) #> : int
<#= string.IsNullOrEmpty(memberInfo.Description) ? "_No description currently available_" : memberInfo.Description #>
<# } #>
"""

<#
foreach (var member in mask.Values)
{
var memberInfo = member.Value;
#>
<#= ToSnakeCase(member.Key, allCaps:true) #> = <#= memberInfo.Value #>
<#
}
}
#>


class <#= deviceName #>Registers(IntEnum):
"""Enum for all available registers in the <#= deviceName #> device.

Attributes
----------
<#
foreach (var registerMetadata in publicRegisters)
{
var regName = registerMetadata.Key;
var reg = registerMetadata.Value;
var interfaceType = TemplateHelper.GetInterfaceType(registerMetadata.Key, registerMetadata.Value);
#>
<#= ToSnakeCase(regName, allCaps:true) #> : int
<#= reg.Description ?? "Register address for the " + regName + " register." #>
<# } #>
"""

<#
foreach (var registerMetadata in publicRegisters)
{
var regName = registerMetadata.Key;
var reg = registerMetadata.Value;
var address = reg.Address;
#>
<#= ToSnakeCase(regName, allCaps:true) #> = <#= address #>
<#
}
#>


class <#= deviceName #>(Device):
"""
<#= deviceName #> class for controlling the device.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# connect and load already happened in the base class
# verify that WHO_AM_I matches the expected value
if self.WHO_AM_I != <#= deviceMetadata.WhoAmI #>:
self.disconnect()
raise HarpException(f"WHO_AM_I mismatch: expected {<#= deviceMetadata.WhoAmI #>}, got {self.WHO_AM_I}")

<# foreach (var registerMetadata in publicRegisters) {
var regName = registerMetadata.Key;
var reg = registerMetadata.Value;
var interfaceType = TemplateHelper.GetInterfaceType(registerMetadata.Key, registerMetadata.Value);
var hasPayloadSpec = reg.PayloadSpec != null;
var isBitmask = bitmaskTypes.Contains(interfaceType);
var isGroupmask = groupmaskTypes.Contains(interfaceType);
#>
def read_<#= ToSnakeCase(regName) #>(self) -> <#= pyType(interfaceType) #> | None:
"""
Reads the contents of the <#= regName #> register.

Returns
-------
<#= pyType(interfaceType) #> | None
Value read from the <#= regName #> register.
"""
address = <#= deviceName #>Registers.<#= ToSnakeCase(regName, allCaps:true) #>
reply = self.send(HarpMessage(MessageType.READ, PayloadType.<#= reg.Type.ToString().ToUpperInvariant() #>, address))
if reply is not None and reply.is_error:
raise HarpReadException("<#= deviceName #>Registers.<#= ToSnakeCase(regName, allCaps:true) #>", reply)

if reply is not None:
<# if (hasPayloadSpec)
{
#>
# Map payload (list/array) to dataclass fields by offset
payload = reply.payload
return <#= interfaceType #>(
<#
var memberCount = reg.PayloadSpec.Count;
var memberIndex = 0;
foreach (var member in reg.PayloadSpec) {
var offset = member.Value.Offset ?? memberIndex;
#>
<#= member.Key #>=payload[<#= offset #>]<#= ++memberIndex < memberCount ? "," : "" #>
<#
}
#>
)
<# }
else if (isBitmask || isGroupmask)
{
#>
return <#= interfaceType #>(reply.payload)
<# }
else
{
#>
# Directly return the payload as it is a primitive type
return reply.payload
<#
}
#>
return None

<# if ((reg.Access & RegisterAccess.Write) != 0) { #>
def write_<#= ToSnakeCase(regName) #>(self, value: <#= pyType(interfaceType) #>) -> HarpMessage | None:
"""
Writes a value to the <#= regName #> register.

Parameters
----------
value : <#= pyType(interfaceType) #>
Value to write to the <#= regName #> register.
"""
address = <#= deviceName #>Registers.<#= ToSnakeCase(regName, allCaps:true) #>
reply = self.send(HarpMessage(MessageType.WRITE, PayloadType.<#= reg.Type.ToString().ToUpperInvariant() #>, address, value))
if reply is not None and reply.is_error:
raise HarpWriteException("<#= deviceName #>Registers.<#= ToSnakeCase(regName, allCaps:true) #>", reply)

return reply

<# } #>
<# } #>
Loading