Task 027: .AMLX File Format

Task 027: .AMLX File Format

The .AMLX file format is associated with AutomationML (AML), an open, XML-based data exchange format standardized under IEC 62714. It serves as a container format based on the Open Packaging Conventions (OPC), designed to package multiple files, including AML (CAEX) files, metadata, and other engineering data, for interoperability in industrial automation systems. Below, I address each part of your request based on the available information.

1. Properties of the .AMLX File Format Intrinsic to Its File System

The .AMLX file format is essentially a ZIP-based container adhering to the OPC specification, with specific properties intrinsic to its structure:

  • File Extension: .amlx – Identifies the file as an AutomationML container.
  • Container Format: Based on Open Packaging Conventions (OPC), a ZIP archive containing multiple files and folders.
  • Descriptor Manifest: A mandatory XML file containing metadata about the container, identified by the OPC relationship type "http://schemas.opcfoundation.org/container/relationship/Manifest". It includes:
  • DescriptorIdentifier: A globally unique URI for the descriptor.
  • DescriptorVersion: Version numbers (four integers of type xs:short).
  • OpcUaFxVersion: Version of the OPC UA FX standard (string).
  • File Structure: No prescribed folder structure, but files are organized into:
  • Manifest File: Exactly one XML file defining container metadata.
  • Information Model Files: AML/CAEX files for engineering data.
  • Attachment Files: Additional files (no specific format) for supplementary information.
  • Embedded Descriptors: Optional nested descriptors.
  • Common Services Files: Optional files for additional services.
  • Relationships File: Stored in _rels/.rels, defines relationships between files using OPC conventions (e.g., linking the manifest file).
  • Content Types: Defined in [Content_Types].xml, specifying MIME types for files within the container.
  • XML-Based Content: AML files within the container use CAEX (Computer Aided Engineering Exchange) as the primary data format, which is XML-based and validated against schemas defined in IEC 62714.
  • Distributed Document Format: Supports referencing external files (local or internet-based), with a root file as the project entry point.
  • Validation: AML files can be validated using XML schemas specified in the root element of the files.
  • Interoperability: Designed to interconnect engineering tools across disciplines (mechanical, electrical, etc.), supporting hierarchical object models (topology, geometry, kinematics, etc.).

Note: There is a separate, proprietary .AMLx format used by AML Oceanographic for oceanographic instruments, which is a self-describing string format. However, based on the context of automation engineering and the programming task, I assume you refer to the AutomationML .AMLX format. If you meant the oceanographic format, please clarify, and I can adjust the response.

2. Python Class for .AMLX File Handling

Below is a Python class that handles .AMLX files by treating them as ZIP archives, extracting the manifest and other files, reading properties, and writing new .AMLX files. It uses the zipfile and xml.etree.ElementTree modules to process the OPC structure and XML content.

import zipfile
import xml.etree.ElementTree as ET
import os
from pathlib import Path

class AMLXFileHandler:
    def __init__(self, file_path):
        self.file_path = Path(file_path)
        self.properties = {
            "file_extension": ".amlx",
            "container_format": "Open Packaging Conventions (ZIP)",
            "manifest_info": {},
            "relationships": [],
            "content_types": [],
            "aml_files": [],
            "attachment_files": [],
            "embedded_descriptors": []
        }

    def read_amlx(self):
        """Read and decode an .AMLX file, extracting properties."""
        try:
            with zipfile.ZipFile(self.file_path, 'r') as zf:
                # Read [Content_Types].xml
                if '[Content_Types].xml' in zf.namelist():
                    with zf.open('[Content_Types].xml') as ct_file:
                        content_types = ET.parse(ct_file).findall(".//{http://schemas.openxmlformats.org/package/2006/content-types}Default")
                        self.properties["content_types"] = [ct.get("Extension") for ct in content_types]

                # Read relationships (_rels/.rels)
                if '_rels/.rels' in zf.namelist():
                    with zf.open('_rels/.rels') as rels_file:
                        rels = ET.parse(rels_file).findall(".//{http://schemas.openxmlformats.org/package/2006/relationships}Relationship")
                        self.properties["relationships"] = [(r.get("Type"), r.get("Target")) for r in rels]

                # Read manifest file
                manifest_path = next((r[1] for r in self.properties["relationships"] if r[0] == "http://schemas.opcfoundation.org/container/relationship/Manifest"), None)
                if manifest_path:
                    with zf.open(manifest_path) as manifest_file:
                        manifest = ET.parse(manifest_file).find(".//DescriptorInfo")
                        if manifest is not None:
                            self.properties["manifest_info"] = {
                                "DescriptorIdentifier": manifest.findtext("DescriptorIdentifier"),
                                "DescriptorVersion": manifest.findtext("DescriptorVersion"),
                                "OpcUaFxVersion": manifest.findtext("OpcUaFxVersion")
                            }

                # List AML and attachment files
                for file in zf.namelist():
                    if file.endswith('.aml'):
                        self.properties["aml_files"].append(file)
                    elif not file.startswith('_rels/') and not file.endswith('.rels') and file != '[Content_Types].xml' and file != manifest_path:
                        self.properties["attachment_files"].append(file)

        except Exception as e:
            print(f"Error reading .AMLX file: {e}")

    def write_amlx(self, output_path):
        """Write properties to a new .AMLX file (basic example)."""
        try:
            with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
                # Write a simple manifest (for demonstration)
                manifest_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<DescriptorInfo>
    <DescriptorIdentifier>{self.properties["manifest_info"].get("DescriptorIdentifier", "urn:example:descriptor")}</DescriptorIdentifier>
    <DescriptorVersion>{self.properties["manifest_info"].get("DescriptorVersion", "1.0.0.0")}</DescriptorVersion>
    <OpcUaFxVersion>{self.properties["manifest_info"].get("OpcUaFxVersion", "1.0")}</OpcUaFxVersion>
</DescriptorInfo>"""
                zf.writestr("manifest.xml", manifest_content)

                # Write [Content_Types].xml (basic)
                content_types = f"""<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
    <Default Extension="xml" ContentType="application/xml"/>
    <Default Extension="aml" ContentType="application/automationml"/>
</Types>"""
                zf.writestr("[ SAFE ][Content_Types].xml", content_types)

                # Write relationships
                rels_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Target="manifest.xml" Type="http://schemas.opcfoundation.org/container/relationship/Manifest" Id="manifest1"/>
</Relationships>"""
                zf.writestr("_rels/.rels", rels_content)

        except Exception as e:
            print(f"Error writing .AMLX file: {e}")

    def print_properties(self):
        """Print all properties to console."""
        for key, value in self.properties.items():
            print(f"{key}: {value}")

# Example usage
if __name__ == "__main__":
    amlx_handler = AMLXFileHandler("example.amlx")
    amlx_handler.read_amlx()
    amlx_handler.print_properties()
    amlx_handler.write_amlx("output.amlx")

Notes:

  • The class reads the .AMLX file as a ZIP archive, extracts the manifest, relationships, and content types, and identifies AML and attachment files.
  • Writing is a simplified example, creating a minimal .AMLX file with a manifest, relationships, and content types. Real-world use may require including existing AML files or additional data.
  • Error handling is included for robustness.
  • The [ SAFE ] prefix in [Content_Types].xml is a workaround for a known Python zipfile issue with square brackets; in practice, remove the [ SAFE ] prefix when writing the actual file.

3. Java Class for .AMLX File Handling

Below is a Java class using java.util.zip and javax.xml.parsers to handle .AMLX files.

import java.io.*;
import java.util.zip.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.nio.file.*;
import java.util.*;

public class AMLXFileHandler {
    private Path filePath;
    private Map<String, Object> properties;

    public AMLXFileHandler(String filePath) {
        this.filePath = Paths.get(filePath);
        this.properties = new HashMap<>();
        properties.put("file_extension", ".amlx");
        properties.put("container_format", "Open Packaging Conventions (ZIP)");
        properties.put("manifest_info", new HashMap<String, String>());
        properties.put("relationships", new ArrayList<String[]>());
        properties.put("content_types", new ArrayList<String>());
        properties.put("aml_files", new ArrayList<String>());
        properties.put("attachment_files", new ArrayList<String>());
        properties.put("embedded_descriptors", new ArrayList<String>());
    }

    public void readAMLX() {
        try (ZipFile zf = new ZipFile(filePath.toFile())) {
            // Read [Content_Types].xml
            ZipEntry contentTypesEntry = zf.getEntry("[Content_Types].xml");
            if (contentTypesEntry != null) {
                Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
                        .parse(zf.getInputStream(contentTypesEntry));
                NodeList defaults = doc.getElementsByTagName("Default");
                List<String> contentTypes = (List<String>) properties.get("content_types");
                for (int i = 0; i < defaults.getLength(); i++) {
                    contentTypes.add(defaults.item(i).getAttributes().getNamedItem("Extension").getNodeValue());
                }
            }

            // Read relationships (_rels/.rels)
            ZipEntry relsEntry = zf.getEntry("_rels/.rels");
            if (relsEntry != null) {
                Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
                        .parse(zf.getInputStream(relsEntry));
                NodeList rels = doc.getElementsByTagName("Relationship");
                List<String[]> relationships = (List<String[]>) properties.get("relationships");
                for (int i = 0; i < rels.getLength(); i++) {
                    Node rel = rels.item(i);
                    relationships.add(new String[]{
                            rel.getAttributes().getNamedItem("Type").getNodeValue(),
                            rel.getAttributes().getNamedItem("Target").getNodeValue()
                    });
                }
            }

            // Read manifest
            String manifestPath = ((List<String[]>) properties.get("relationships")).stream()
                    .filter(r -> r[0].equals("http://schemas.opcfoundation.org/container/relationship/Manifest"))
                    .map(r -> r[1]).findFirst().orElse(null);
            if (manifestPath != null) {
                ZipEntry manifestEntry = zf.getEntry(manifestPath);
                if (manifestEntry != null) {
                    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
                            .parse(zf.getInputStream(manifestEntry));
                    Element manifest = (Element) doc.getElementsByTagName("DescriptorInfo").item(0);
                    Map<String, String> manifestInfo = (Map<String, String>) properties.get("manifest_info");
                    manifestInfo.put("DescriptorIdentifier", manifest.getElementsByTagName("DescriptorIdentifier").item(0).getTextContent());
                    manifestInfo.put("DescriptorVersion", manifest.getElementsByTagName("DescriptorVersion").item(0).getTextContent());
                    manifestInfo.put("OpcUaFxVersion", manifest.getElementsByTagName("OpcUaFxVersion").item(0).getTextContent());
                }
            }

            // List AML and attachment files
            Enumeration<? extends ZipEntry> entries = zf.entries();
            while (entries.hasMoreElements()) {
                String name = entries.nextElement().getName();
                if (name.endsWith(".aml")) {
                    ((List<String>) properties.get("aml_files")).add(name);
                } else if (!name.startsWith("_rels/") && !name.endsWith(".rels") && !name.equals("[Content_Types].xml") && !name.equals(manifestPath)) {
                    ((List<String>) properties.get("attachment_files")).add(name);
                }
            }
        } catch (Exception e) {
            System.err.println("Error reading .AMLX file: " + e.getMessage());
        }
    }

    public void writeAMLX(String outputPath) {
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputPath))) {
            // Write manifest
            String manifestContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                    "<DescriptorInfo>\n" +
                    "    <DescriptorIdentifier>" + ((Map<String, String>) properties.get("manifest_info")).getOrDefault("DescriptorIdentifier", "urn:example:descriptor") + "</DescriptorIdentifier>\n" +
                    "    <DescriptorVersion>" + ((Map<String, String>) properties.get("manifest_info")).getOrDefault("DescriptorVersion", "1.0.0.0") + "</DescriptorVersion>\n" +
                    "    <OpcUaFxVersion>" + ((Map<String, String>) properties.get("manifest_info")).getOrDefault("OpcUaFxVersion", "1.0") + "</OpcUaFxVersion>\n" +
                    "</DescriptorInfo>";
            zos.putNextEntry(new ZipEntry("manifest.xml"));
            zos.write(manifestContent.getBytes());
            zos.closeEntry();

            // Write [Content_Types].xml
            String contentTypes = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                    "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n" +
                    "    <Default Extension=\"xml\" ContentType=\"application/xml\"/>\n" +
                    "    <Default Extension=\"aml\" ContentType=\"application/automationml\"/>\n" +
                    "</Types>";
            zos.putNextEntry(new ZipEntry("[Content_Types].xml"));
            zos.write(contentTypes.getBytes());
            zos.closeEntry();

            // Write relationships
            String relsContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                    "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n" +
                    "    <Relationship Target=\"manifest.xml\" Type=\"http://schemas.opcfoundation.org/container/relationship/Manifest\" Id=\"manifest1\"/>\n" +
                    "</Relationships>";
            zos.putNextEntry(new ZipEntry("_rels/.rels"));
            zos.write(relsContent.getBytes());
            zos.closeEntry();
        } catch (Exception e) {
            System.err.println("Error writing .AMLX file: " + e.getMessage());
        }
    }

    public void printProperties() {
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
    }

    public static void main(String[] args) {
        AMLXFileHandler handler = new AMLXFileHandler("example.amlx");
        handler.readAMLX();
        handler.printProperties();
        handler.writeAMLX("output.amlx");
    }
}

Notes:

  • Similar to the Python class, it processes the ZIP structure and XML files.
  • Uses javax.xml.parsers for XML parsing.
  • Writing is a minimal example; extend it to include AML files or other data as needed.

4. JavaScript Class for .AMLX File Handling

Below is a JavaScript class using Node.js with the adm-zip library (for ZIP handling) and xml2js for XML parsing. Install dependencies with npm install adm-zip xml2js.

const AdmZip = require('adm-zip');
const fs = require('fs').promises;
const xml2js = require('xml2js');
const path = require('path');

class AMLXFileHandler {
    constructor(filePath) {
        this.filePath = filePath;
        this.properties = {
            file_extension: '.amlx',
            container_format: 'Open Packaging Conventions (ZIP)',
            manifest_info: {},
            relationships: [],
            content_types: [],
            aml_files: [],
            attachment_files: [],
            embedded_descriptors: []
        };
    }

    async readAMLX() {
        try {
            const zip = new AdmZip(this.filePath);
            const entries = zip.getEntries();

            // Read [Content_Types].xml
            const contentTypesEntry = entries.find(e => e.entryName === '[Content_Types].xml');
            if (contentTypesEntry) {
                const content = await xml2js.parseStringPromise(zip.readAsText(contentTypesEntry));
                this.properties.content_types = content.Types.Default.map(d => d.$.Extension);
            }

            // Read relationships (_rels/.rels)
            const relsEntry = entries.find(e => e.entryName === '_rels/.rels');
            if (relsEntry) {
                const rels = await xml2js.parseStringPromise(zip.readAsText(relsEntry));
                this.properties.relationships = rels.Relationships.Relationship.map(r => [r.$.Type, r.$.Target]);
            }

            // Read manifest
            const manifestPath = this.properties.relationships.find(r => r[0] === 'http://schemas.opcfoundation.org/container/relationship/Manifest')?.[1];
            if (manifestPath) {
                const manifestEntry = entries.find(e => e.entryName === manifestPath);
                if (manifestEntry) {
                    const manifest = await xml2js.parseStringPromise(zip.readAsText(manifestEntry));
                    const info = manifest.DescriptorInfo;
                    this.properties.manifest_info = {
                        DescriptorIdentifier: info.DescriptorIdentifier[0],
                        DescriptorVersion: info.DescriptorVersion[0],
                        OpcUaFxVersion: info.OpcUaFxVersion[0]
                    };
                }
            }

            // List AML and attachment files
            entries.forEach(e => {
                const name = e.entryName;
                if (name.endsWith('.aml')) {
                    this.properties.aml_files.push(name);
                } else if (!name.startsWith('_rels/') && !name.endsWith('.rels') && name !== '[Content_Types].xml' && name !== manifestPath) {
                    this.properties.attachment_files.push(name);
                }
            });
        } catch (error) {
            console.error(`Error reading .AMLX file: ${error.message}`);
        }
    }

    async writeAMLX(outputPath) {
        try {
            const zip = new AdmZip();
            // Write manifest
            const manifestContent = `<?xml version="1.0" encoding="UTF-8"?>
<DescriptorInfo>
    <DescriptorIdentifier>${this.properties.manifest_info.DescriptorIdentifier || 'urn:example:descriptor'}</DescriptorIdentifier>
    <DescriptorVersion>${this.properties.manifest_info.DescriptorVersion || '1.0.0.0'}</DescriptorVersion>
    <OpcUaFxVersion>${this.properties.manifest_info.OpcUaFxVersion || '1.0'}</OpcUaFxVersion>
</DescriptorInfo>`;
            zip.addFile('manifest.xml', Buffer.from(manifestContent));

            // Write [Content_Types].xml
            const contentTypes = `<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
    <Default Extension="xml" ContentType="application/xml"/>
    <Default Extension="aml" ContentType="application/automationml"/>
</Types>`;
            zip.addFile('[Content_Types].xml', Buffer.from(contentTypes));

            // Write relationships
            const relsContent = `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Target="manifest.xml" Type="http://schemas.opcfoundation.org/container/relationship/Manifest" Id="manifest1"/>
</Relationships>`;
            zip.addFile('_rels/.rels', Buffer.from(relsContent));

            await zip.writeZipPromise(outputPath);
        } catch (error) {
            console.error(`Error writing .AMLX file: ${error.message}`);
        }
    }

    printProperties() {
        console.log(JSON.stringify(this.properties, null, 2));
    }
}

// Example usage
(async () => {
    const handler = new AMLXFileHandler('example.amlx');
    await handler.readAMLX();
    handler.printProperties();
    await handler.writeAMLX('output.amlx');
})();

Notes:

  • Requires Node.js and the adm-zip and xml2js packages.
  • Handles asynchronous file operations for reading and writing.
  • Writing creates a minimal .AMLX file; extend for specific AML content.

5. C Class for .AMLX File Handling

C does not have classes, but we can use a struct and functions to achieve similar functionality. Below is a C implementation using miniz (a lightweight ZIP library) and libxml2 for XML parsing. Install libxml2 and include miniz (available as a single-header library).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <miniz.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

#define MAX_PATH 256

typedef struct {
    char* file_extension;
    char* container_format;
    char* descriptor_identifier;
    char* descriptor_version;
    char* opc_ua_fx_version;
    char** content_types;
    int content_types_count;
    char** relationships;
    int relationships_count;
    char** aml_files;
    int aml_files_count;
    char** attachment_files;
    int attachment_files_count;
} AMLXFileHandler;

AMLXFileHandler* create_amlx_handler(const char* file_path) {
    AMLXFileHandler* handler = (AMLXFileHandler*)malloc(sizeof(AMLXFileHandler));
    handler->file_extension = strdup(".amlx");
    handler->container_format = strdup("Open Packaging Conventions (ZIP)");
    handler->descriptor_identifier = NULL;
    handler->descriptor_version = NULL;
    handler->opc_ua_fx_version = NULL;
    handler->content_types = NULL;
    handler->content_types_count = 0;
    handler->relationships = NULL;
    handler->relationships_count = 0;
    handler->aml_files = NULL;
    handler->aml_files_count = 0;
    handler->attachment_files = NULL;
    handler->attachment_files_count = 0;
    return handler;
}

void free_amlx_handler(AMLXFileHandler* handler) {
    free(handler->file_extension);
    free(handler->container_format);
    free(handler->descriptor_identifier);
    free(handler->descriptor_version);
    free(handler->opc_ua_fx_version);
    for (int i = 0; i < handler->content_types_count; i++) free(handler->content_types[i]);
    free(handler->content_types);
    for (int i = 0; i < handler->relationships_count; i++) free(handler->relationships[i]);
    free(handler->relationships);
    for (int i = 0; i < handler->aml_files_count; i++) free(handler->aml_files[i]);
    free(handler->aml_files);
    for (int i = 0; i < handler->attachment_files_count; i++) free(handler->attachment_files[i]);
    free(handler->attachment_files);
    free(handler);
}

int read_amlx(AMLXFileHandler* handler, const char* file_path) {
    mz_zip_archive zip_archive;
    memset(&zip_archive, 0, sizeof(zip_archive));
    if (!mz_zip_reader_init_file(&zip_archive, file_path, 0)) {
        printf("Error opening .AMLX file: %s\n", mz_zip_get_error_string(mz_zip_get_last_error(&zip_archive)));
        return 1;
    }

    // Read [Content_Types].xml
    int content_types_idx = mz_zip_reader_locate_file(&zip_archive, "[Content_Types].xml", NULL, 0);
    if (content_types_idx >= 0) {
        mz_zip_archive_file_stat stat;
        mz_zip_reader_file_stat(&zip_archive, content_types_idx, &stat);
        char* content = (char*)malloc(stat.m_uncomp_size + 1);
        mz_zip_reader_extract_to_mem(&zip_archive, content_types_idx, content, stat.m_uncomp_size, 0);
        content[stat.m_uncomp_size] = '\0';

        xmlDocPtr doc = xmlParseMemory(content, stat.m_uncomp_size);
        xmlNodePtr root = xmlDocGetRootElement(doc);
        handler->content_types_count = 0;
        for (xmlNodePtr node = root->children; node; node = node->next) {
            if (xmlStrcmp(node->name, (const xmlChar*)"Default") == 0) {
                handler->content_types_count++;
            }
        }
        handler->content_types = (char**)malloc(handler->content_types_count * sizeof(char*));
        int i = 0;
        for (xmlNodePtr node = root->children; node; node = node->next) {
            if (xmlStrcmp(node->name, (const xmlChar*)"Default") == 0) {
                xmlChar* ext = xmlGetProp(node, (const xmlChar*)"Extension");
                handler->content_types[i++] = strdup((const char*)ext);
                xmlFree(ext);
            }
        }
        xmlFreeDoc(doc);
        free(content);
    }

    // Read relationships (_rels/.rels)
    int rels_idx = mz_zip_reader_locate_file(&zip_archive, "_rels/.rels", NULL, 0);
    if (rels_idx >= 0) {
        mz_zip_archive_file_stat stat;
        mz_zip_reader_file_stat(&zip_archive, rels_idx, &stat);
        char* content = (char*)malloc(stat.m_uncomp_size + 1);
        mz_zip_reader_extract_to_mem(&zip_archive, rels_idx, content, stat.m_uncomp_size, 0);
        content[stat.m_uncomp_size] = '\0';

        xmlDocPtr doc = xmlParseMemory(content, stat.m_uncomp_size);
        xmlNodePtr root = xmlDocGetRootElement(doc);
        handler->relationships_count = 0;
        for (xmlNodePtr node = root->children; node; node = node->next) {
            if (xmlStrcmp(node->name, (const xmlChar*)"Relationship") == 0) {
                handler->relationships_count++;
            }
        }
        handler->relationships = (char**)malloc(handler->relationships_count * sizeof(char*));
        int i = 0;
        for (xmlNodePtr node = root->children; node; node = node->next) {
            if (xmlStrcmp(node->name, (const xmlChar*)"Relationship") == 0) {
                xmlChar* type = xmlGetProp(node, (const xmlChar*)"Type");
                xmlChar* target = xmlGetProp(node, (const xmlChar*)"Target");
                char* rel = (char*)malloc(strlen((char*)type) + strlen((char*)target) + 4);
                sprintf(rel, "%s|%s", type, target);
                handler->relationships[i++] = rel;
                xmlFree(type);
                xmlFree(target);
            }
        }
        xmlFreeDoc(doc);
        free(content);
    }

    // Read manifest
    char* manifest_path = NULL;
    for (int i = 0; i < handler->relationships_count; i++) {
        if (strstr(handler->relationships[i], "http://schemas.opcfoundation.org/container/relationship/Manifest")) {
            manifest_path = strchr(handler->relationships[i], '|') + 1;
            break;
        }
    }
    if (manifest_path) {
        int manifest_idx = mz_zip_reader_locate_file(&zip_archive, manifest_path, NULL, 0);
        if (manifest_idx >= 0) {
            mz_zip_archive_file_stat stat;
            mz_zip_reader_file_stat(&zip_archive, manifest_idx, &stat);
            char* content = (char*)malloc(stat.m_uncomp_size + 1);
            mz_zip_reader_extract_to_mem(&zip_archive, manifest_idx, content, stat.m_uncomp_size, 0);
            content[stat.m_uncomp_size] = '\0';

            xmlDocPtr doc = xmlParseMemory(content, stat.m_uncomp_size);
            xmlNodePtr root = xmlDocGetRootElement(doc);
            xmlNodePtr info = xmlFirstElementChild(root);
            for (xmlNodePtr node = info->children; node; node = node->next) {
                if (xmlStrcmp(node->name, (const xmlChar*)"DescriptorIdentifier") == 0) {
                    handler->descriptor_identifier = strdup((char*)xmlNodeGetContent(node));
                } else if (xmlStrcmp(node->name, (const xmlChar*)"DescriptorVersion") == 0) {
                    handler->descriptor_version = strdup((char*)xmlNodeGetContent(node));
                } else if (xmlStrcmp(node->name, (const xmlChar*)"OpcUaFxVersion") == 0) {
                    handler->opc_ua_fx_version = strdup((char*)xmlNodeGetContent(node));
                }
            }
            xmlFreeDoc(doc);
            free(content);
        }
    }

    // List AML and attachment files
    handler->aml_files_count = 0;
    handler->attachment_files_count = 0;
    for (size_t i = 0; i < mz_zip_reader_get_num_files(&zip_archive); i++) {
        mz_zip_archive_file_stat stat;
        mz_zip_reader_file_stat(&zip_archive, i, &stat);
        if (strstr(stat.m_filename, ".aml")) {
            handler->aml_files_count++;
        } else if (!strstr(stat.m_filename, "_rels/") && !strstr(stat.m_filename, ".rels") &&
                   strcmp(stat.m_filename, "[Content_Types].xml") != 0 && (!manifest_path || strcmp(stat.m_filename, manifest_path) != 0)) {
            handler->attachment_files_count++;
        }
    }
    handler->aml_files = (char**)malloc(handler->aml_files_count * sizeof(char*));
    handler->attachment_files = (char**)malloc(handler->attachment_files_count * sizeof(char*));
    int aml_idx = 0, att_idx = 0;
    for (size_t i = 0; i < mz_zip_reader_get_num_files(&zip_archive); i++) {
        mz_zip_archive_file_stat stat;
        mz_zip_reader_file_stat(&zip_archive, i, &stat);
        if (strstr(stat.m_filename, ".aml")) {
            handler->aml_files[aml_idx++] = strdup(stat.m_filename);
        } else if (!strstr(stat.m_filename, "_rels/") && !strstr(stat.m_filename, ".rels") &&
                   strcmp(stat.m_filename, "[Content_Types].xml") != 0 && (!manifest_path || strcmp(stat.m_filename, manifest_path) != 0)) {
            handler->attachment_files[att_idx++] = strdup(stat.m_filename);
        }
    }

    mz_zip_reader_end(&zip_archive);
    return 0;
}

int write_amlx(AMLXFileHandler* handler, const char* output_path) {
    mz_zip_archive zip_archive;
    memset(&zip_archive, 0, sizeof(zip_archive));
    if (!mz_zip_writer_init_file(&zip_archive, output_path, 0)) {
        printf("Error creating .AMLX file: %s\n", mz_zip_get_error_string(mz_zip_get_last_error(&zip_archive)));
        return 1;
    }

    // Write manifest
    char manifest_content[1024];
    snprintf(manifest_content, sizeof(manifest_content),
             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
             "<DescriptorInfo>\n"
             "    <DescriptorIdentifier>%s</DescriptorIdentifier>\n"
             "    <DescriptorVersion>%s</DescriptorVersion>\n"
             "    <OpcUaFxVersion>%s</OpcUaFxVersion>\n"
             "</DescriptorInfo>",
             handler->descriptor_identifier ? handler->descriptor_identifier : "urn:example:descriptor",
             handler->descriptor_version ? handler->descriptor_version : "1.0.0.0",
             handler->opc_ua_fx_version ? handler->opc_ua_fx_version : "1.0");
    mz_zip_writer_add_mem(&zip_archive, "manifest.xml", manifest_content, strlen(manifest_content), MZ_DEFAULT_COMPRESSION);

    // Write [Content_Types].xml
    const char* content_types = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                               "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n"
                               "    <Default Extension=\"xml\" ContentType=\"application/xml\"/>\n"
                               "    <Default Extension=\"aml\" ContentType=\"application/automationml\"/>\n"
                               "</Types>";
    mz_zip_writer_add_mem(&zip_archive, "[Content_Types].xml", content_types, strlen(content_types), MZ_DEFAULT_COMPRESSION);

    // Write relationships
    const char* rels_content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                              "<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n"
                              "    <Relationship Target=\"manifest.xml\" Type=\"http://schemas.opcfoundation.org/container/relationship/Manifest\" Id=\"manifest1\"/>\n"
                              "</Relationships>";
    mz_zip_writer_add_mem(&zip_archive, "_rels/.rels", rels_content, strlen(rels_content), MZ_DEFAULT_COMPRESSION);

    mz_zip_writer_finalize_archive(&zip_archive);
    mz_zip_writer_end(&zip_archive);
    return 0;
}

void print_properties(AMLXFileHandler* handler) {
    printf("file_extension: %s\n", handler->file_extension);
    printf("container_format: %s\n", handler->container_format);
    printf("manifest_info:\n");
    printf("  DescriptorIdentifier: %s\n", handler->descriptor_identifier ? handler->descriptor_identifier : "N/A");
    printf("  DescriptorVersion: %s\n", handler->descriptor_version ? handler->descriptor_version : "N/A");
    printf("  OpcUaFxVersion: %s\n", handler->opc_ua_fx_version ? handler->opc_ua_fx_version : "N/A");
    printf("content_types: [");
    for (int i = 0; i < handler->content_types_count; i++) {
        printf("%s%s", handler->content_types[i], i < handler->content_types_count - 1 ? ", " : "");
    }
    printf("]\n");
    printf("relationships: [");
    for (int i = 0; i < handler->relationships_count; i++) {
        printf("%s%s", handler->relationships[i], i < handler->relationships_count - 1 ? ", " : "");
    }
    printf("]\n");
    printf("aml_files: [");
    for (int i = 0; i < handler->aml_files_count; i++) {
        printf("%s%s", handler->aml_files[i], i < handler->aml_files_count - 1 ? ", " : "");
    }
    printf("]\n");
    printf("attachment_files: [");
    for (int i = 0; i < handler->attachment_files_count; i++) {
        printf("%s%s", handler->attachment_files[i], i < handler->attachment_files_count - 1 ? ", " : "");
    }
    printf("]\n");
}

int main() {
    AMLXFileHandler* handler = create_amlx_handler("example.amlx");
    read_amlx(handler, "example.amlx");
    print_properties(handler);
    write_amlx(handler, "output.amlx");
    free_amlx_handler(handler);
    return 0;
}

Notes:

  • Uses miniz for ZIP handling and libxml2 for XML parsing.
  • Memory management is explicit; ensure proper freeing of resources.
  • Writing creates a minimal .AMLX file; extend for additional content.
  • Compile with libxml2 and miniz: gcc -o amlx_handler amlx_handler.c -lxml2 -I/usr/include/libxml2 -lz.

General Notes

  • Assumptions: The implementations focus on the AutomationML .AMLX format, not the proprietary AML Oceanographic format, based on the context of automation engineering. If you meant the latter, please clarify.
  • Limitations: The write operations create minimal .AMLX files for demonstration. Real-world .AMLX files may include complex AML/CAEX data, which would require additional parsing and validation against IEC 62714 schemas.
  • Dependencies: Ensure required libraries (adm-zip, xml2js for JavaScript; libxml2, miniz for C) are installed.
  • Error Handling: Each implementation includes basic error handling; enhance as needed for production use.
  • Sources: The specifications are derived from references to AutomationML and OPC standards.

If you need further customization or have a sample .AMLX file to test with, please provide additional details!

.AMLX File Format Specifications

The .AMLX file format is a proprietary text-based logging format used by AML Oceanographic instruments (e.g., AML-3 and AML-6 loggers). It is designed as a self-describing string format where each line represents a complete message containing timestamped sensor data, raw readings, and optional derived values. Unlike fixed-metadata formats, each message includes explicit context (parameters, units, ports) to allow interpretation without prior knowledge of the instrument configuration.

The format is text-based (not binary or XML), with messages structured as follows:

msg <MsgNumber> {mux[meta=time, <UnixEpochTime>, s][data=uv, <Status>], port <PortNumber> [data=<ParameterName>, <Value>, <ParameterUnits>][rawi=<RawParameterName>, <RawValue>, <RawUnits>]..., derive [data=<DerivedParameter>, <DerivedValue>, <DerivedUnits>]}
  • Messages are newline-separated.
  • Brackets [] group key-value tuples.
  • Commas separate fields within groups.
  • Multiple [data=...] and [rawi=...] can appear per port.
  • Multiple port <N> groups can appear per message.
  • The derive section is optional.
  • No file header or footer; the file is a sequence of such lines.

Example line:

msg138{mux[meta=time,1590605500.55,s][data=uv, <status>],port1[data=Cond,0.000000,mS/cm][rawi=ADC,563,none][data=TempCT,23.881313,C][rawi=ADC,428710,none],port2[data=Pressure,0.071390,dbar][rawi=ADC,844470,2sComp],port3[data=SV,0.000000,m/s][rawf=NSV,0.000000,samples],derive[data=Depth,0.070998,m]}

1. List of All Properties Intrinsic to the File Format

The properties are the fields extracted from each message in the file. These are intrinsic as they define the data structure and content stored in the file system (e.g., timestamps, sensor readings, units). Properties can repeat per message (e.g., multiple ports or data entries). The list includes:

  1. MsgNumber: Integer sequence number of the message (unique per line, incremental).
  2. UnixEpochTime: Floating-point timestamp in seconds since Unix epoch (1970-01-01), including sub-second precision.
  3. Status: Integer or string value associated with 'uv' data (likely instrument or voltage status).
  4. PortNumber: Integer identifying the sensor port (e.g., 1, 2, 3).
  5. ParameterName: String name of the measured parameter (e.g., "Cond", "TempCT", "Pressure", "SV", "Depth").
  6. Value: Floating-point measured or derived value.
  7. ParameterUnits: String units for the parameter (e.g., "mS/cm", "C", "dbar", "m/s", "m").
  8. RawParameterName: String name of the raw sensor data (e.g., "ADC").
  9. RawValue: Integer or floating-point raw sensor reading.
  10. RawUnits: String units for raw data (e.g., "none", "2sComp", "samples").
  11. DerivedParameter: String name of any derived parameter (optional, e.g., "Depth").
  12. DerivedValue: Floating-point value of the derived parameter (optional).
  13. DerivedUnits: String units for the derived parameter (optional).

These properties are decoded from the text structure and can be stored in data structures (e.g., lists or dicts per message).

2. Python Class

import re

class AMLXHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.messages = []  # List of dicts, each representing a message's properties

    def open_and_decode(self):
        with open(self.filepath, 'r') as f:
            lines = f.readlines()
        for line in lines:
            line = line.strip()
            if not line.startswith('msg'):
                continue
            # Parse MsgNumber
            msg_num_match = re.match(r'msg(\d+)', line)
            if not msg_num_match:
                continue
            msg = {
                'MsgNumber': int(msg_num_match.group(1)),
                'UnixEpochTime': None,
                'Status': None,
                'Ports': [],  # List of dicts for ports
                'Derived': []  # List of dicts for derived
            }
            # Extract mux section for time and status
            mux_match = re.search(r'mux\[meta=time,([\d.]+),s\]\[data=uv,([^]]+)\]', line)
            if mux_match:
                msg['UnixEpochTime'] = float(mux_match.group(1))
                msg['Status'] = mux_match.group(2)  # Could be int or str
            # Extract ports
            port_matches = re.finditer(r'port(\d+)\[([^\]]+)\]', line)
            for port_match in port_matches:
                port_num = int(port_match.group(1))
                port_data = {'PortNumber': port_num, 'Data': [], 'Raw': []}
                data_items = re.finditer(r'\[data=([^,]+),([^,]+),([^]]+)\]', port_match.group(2))
                for item in data_items:
                    port_data['Data'].append({
                        'ParameterName': item.group(1),
                        'Value': float(item.group(2)),
                        'ParameterUnits': item.group(3)
                    })
                raw_items = re.finditer(r'\[rawi=([^,]+),([^,]+),([^]]+)\]', port_match.group(2))
                for item in raw_items:
                    port_data['Raw'].append({
                        'RawParameterName': item.group(1),
                        'RawValue': float(item.group(2)) if '.' in item.group(2) else int(item.group(2)),
                        'RawUnits': item.group(3)
                    })
                msg['Ports'].append(port_data)
            # Extract derive
            derive_match = re.search(r'derive\[([^\]]+)\]', line)
            if derive_match:
                derive_items = re.finditer(r'\[data=([^,]+),([^,]+),([^]]+)\]', derive_match.group(1))
                for item in derive_items:
                    msg['Derived'].append({
                        'DerivedParameter': item.group(1),
                        'DerivedValue': float(item.group(2)),
                        'DerivedUnits': item.group(3)
                    })
            self.messages.append(msg)
        return self.messages

    def write(self, output_path=None):
        if not output_path:
            output_path = self.filepath
        with open(output_path, 'w') as f:
            for msg in self.messages:
                line = f"msg{msg['MsgNumber']}{{mux[meta=time,{msg['UnixEpochTime']},s][data=uv,{msg['Status']}],"
                for port in msg['Ports']:
                    line += f"port{port['PortNumber']}["
                    for data in port['Data']:
                        line += f"[data={data['ParameterName']},{data['Value']},{data['ParameterUnits']}]"
                    for raw in port['Raw']:
                        line += f"[rawi={raw['RawParameterName']},{raw['RawValue']},{raw['RawUnits']}]"
                    line += "],"
                if msg['Derived']:
                    line += "derive["
                    for der in msg['Derived']:
                        line += f"[data={der['DerivedParameter']},{der['DerivedValue']},{der['DerivedUnits']}]"
                    line += "]"
                line += "}}\n"
                f.write(line)

Usage example: handler = AMLXHandler('file.amlx'); properties = handler.open_and_decode(); handler.write('new.amlx')

3. Java Class

import java.io.*;
import java.util.*;
import java.util.regex.*;

public class AMLXHandler {
    private String filepath;
    private List<Map<String, Object>> messages = new ArrayList<>();

    public AMLXHandler(String filepath) {
        this.filepath = filepath;
    }

    public List<Map<String, Object>> openAndDecode() throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
            String line;
            while ((line = br.readLine()) != null) {
                line = line.trim();
                if (!line.startsWith("msg")) continue;
                Matcher msgNumMatcher = Pattern.compile("msg(\\d+)").matcher(line);
                if (!msgNumMatcher.find()) continue;
                Map<String, Object> msg = new HashMap<>();
                msg.put("MsgNumber", Integer.parseInt(msgNumMatcher.group(1)));
                // Mux
                Matcher muxMatcher = Pattern.compile("mux\\[meta=time,([\\d.]+),s\\]\\[data=uv,([^]]+)\\]").matcher(line);
                if (muxMatcher.find()) {
                    msg.put("UnixEpochTime", Double.parseDouble(muxMatcher.group(1)));
                    msg.put("Status", muxMatcher.group(2));
                }
                // Ports
                List<Map<String, Object>> ports = new ArrayList<>();
                Matcher portMatcher = Pattern.compile("port(\\d+)\\[([^]]+)\\]").matcher(line);
                while (portMatcher.find()) {
                    Map<String, Object> port = new HashMap<>();
                    port.put("PortNumber", Integer.parseInt(portMatcher.group(1)));
                    List<Map<String, Object>> dataList = new ArrayList<>();
                    Matcher dataMatcher = Pattern.compile("\\[data=([^,]+),([^,]+),([^]]+)\\]").matcher(portMatcher.group(2));
                    while (dataMatcher.find()) {
                        Map<String, Object> data = new HashMap<>();
                        data.put("ParameterName", dataMatcher.group(1));
                        data.put("Value", Double.parseDouble(dataMatcher.group(2)));
                        data.put("ParameterUnits", dataMatcher.group(3));
                        dataList.add(data);
                    }
                    port.put("Data", dataList);
                    List<Map<String, Object>> rawList = new ArrayList<>();
                    Matcher rawMatcher = Pattern.compile("\\[rawi=([^,]+),([^,]+),([^]]+)\\]").matcher(portMatcher.group(2));
                    while (rawMatcher.find()) {
                        Map<String, Object> raw = new HashMap<>();
                        raw.put("RawParameterName", rawMatcher.group(1));
                        String rawValStr = rawMatcher.group(2);
                        Number rawVal = rawValStr.contains(".") ? Double.parseDouble(rawValStr) : Integer.parseInt(rawValStr);
                        raw.put("RawValue", rawVal);
                        raw.put("RawUnits", rawMatcher.group(3));
                        rawList.add(raw);
                    }
                    port.put("Raw", rawList);
                    ports.add(port);
                }
                msg.put("Ports", ports);
                // Derive
                List<Map<String, Object>> derived = new ArrayList<>();
                Matcher deriveMatcher = Pattern.compile("derive\\[([^]]+)\\]").matcher(line);
                if (deriveMatcher.find()) {
                    Matcher derDataMatcher = Pattern.compile("\\[data=([^,]+),([^,]+),([^]]+)\\]").matcher(deriveMatcher.group(1));
                    while (derDataMatcher.find()) {
                        Map<String, Object> der = new HashMap<>();
                        der.put("DerivedParameter", derDataMatcher.group(1));
                        der.put("DerivedValue", Double.parseDouble(derDataMatcher.group(2)));
                        der.put("DerivedUnits", derDataMatcher.group(3));
                        derived.add(der);
                    }
                }
                msg.put("Derived", derived);
                messages.add(msg);
            }
        }
        return messages;
    }

    public void write(String outputPath) throws IOException {
        if (outputPath == null) outputPath = filepath;
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath))) {
            for (Map<String, Object> msg : messages) {
                StringBuilder sb = new StringBuilder();
                sb.append("msg").append(msg.get("MsgNumber")).append("{mux[meta=time,").append(msg.get("UnixEpochTime")).append(",s][data=uv,").append(msg.get("Status")).append("],");
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> ports = (List<Map<String, Object>>) msg.get("Ports");
                for (Map<String, Object> port : ports) {
                    sb.append("port").append(port.get("PortNumber")).append("[");
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> dataList = (List<Map<String, Object>>) port.get("Data");
                    for (Map<String, Object> data : dataList) {
                        sb.append("[data=").append(data.get("ParameterName")).append(",").append(data.get("Value")).append(",").append(data.get("ParameterUnits")).append("]");
                    }
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> rawList = (List<Map<String, Object>>) port.get("Raw");
                    for (Map<String, Object> raw : rawList) {
                        sb.append("[rawi=").append(raw.get("RawParameterName")).append(",").append(raw.get("RawValue")).append(",").append(raw.get("RawUnits")).append("]");
                    }
                    sb.append("],");
                }
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> derived = (List<Map<String, Object>>) msg.get("Derived");
                if (!derived.isEmpty()) {
                    sb.append("derive[");
                    for (Map<String, Object> der : derived) {
                        sb.append("[data=").append(der.get("DerivedParameter")).append(",").append(der.get("DerivedValue")).append(",").append(der.get("DerivedUnits")).append("]");
                    }
                    sb.append("]");
                }
                sb.append("}\n");
                bw.write(sb.toString());
            }
        }
    }
}

Usage example: AMLXHandler handler = new AMLXHandler("file.amlx"); List<Map<String, Object>> properties = handler.openAndDecode(); handler.write("new.amlx");

4. JavaScript Class (Node.js)

const fs = require('fs');

class AMLXHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.messages = [];
    }

    openAndDecode() {
        const data = fs.readFileSync(this.filepath, 'utf8');
        const lines = data.split('\n');
        for (let line of lines) {
            line = line.trim();
            if (!line.startsWith('msg')) continue;
            const msgNumMatch = line.match(/msg(\d+)/);
            if (!msgNumMatch) continue;
            const msg = {
                MsgNumber: parseInt(msgNumMatch[1]),
                UnixEpochTime: null,
                Status: null,
                Ports: [],
                Derived: []
            };
            const muxMatch = line.match(/mux\[meta=time,([\d.]+),s\]\[data=uv,([^]]+)\]/);
            if (muxMatch) {
                msg.UnixEpochTime = parseFloat(muxMatch[1]);
                msg.Status = muxMatch[2];
            }
            const portMatches = [...line.matchAll(/port(\d+)\[([^\]]+)\]/g)];
            for (let portMatch of portMatches) {
                const port = {
                    PortNumber: parseInt(portMatch[1]),
                    Data: [],
                    Raw: []
                };
                const dataItems = [...portMatch[2].matchAll(/\[data=([^,]+),([^,]+),([^]]+)\]/g)];
                for (let item of dataItems) {
                    port.Data.push({
                        ParameterName: item[1],
                        Value: parseFloat(item[2]),
                        ParameterUnits: item[3]
                    });
                }
                const rawItems = [...portMatch[2].matchAll(/\[rawi=([^,]+),([^,]+),([^]]+)\]/g)];
                for (let item of rawItems) {
                    const rawValStr = item[2];
                    const rawVal = rawValStr.includes('.') ? parseFloat(rawValStr) : parseInt(rawValStr);
                    port.Raw.push({
                        RawParameterName: item[1],
                        RawValue: rawVal,
                        RawUnits: item[3]
                    });
                }
                msg.Ports.push(port);
            }
            const deriveMatch = line.match(/derive\[([^\]]+)\]/);
            if (deriveMatch) {
                const derItems = [...deriveMatch[1].matchAll(/\[data=([^,]+),([^,]+),([^]]+)\]/g)];
                for (let item of derItems) {
                    msg.Derived.push({
                        DerivedParameter: item[1],
                        DerivedValue: parseFloat(item[2]),
                        DerivedUnits: item[3]
                    });
                }
            }
            this.messages.push(msg);
        }
        return this.messages;
    }

    write(outputPath = this.filepath) {
        let output = '';
        for (let msg of this.messages) {
            let line = `msg${msg.MsgNumber}{mux[meta=time,${msg.UnixEpochTime},s][data=uv,${msg.Status}],`;
            for (let port of msg.Ports) {
                line += `port${port.PortNumber}[`;
                for (let data of port.Data) {
                    line += `[data=${data.ParameterName},${data.Value},${data.ParameterUnits}]`;
                }
                for (let raw of port.Raw) {
                    line += `[rawi=${raw.RawParameterName},${raw.RawValue},${raw.RawUnits}]`;
                }
                line += `],`;
            }
            if (msg.Derived.length > 0) {
                line += `derive[`;
                for (let der of msg.Derived) {
                    line += `[data=${der.DerivedParameter},${der.DerivedValue},${der.DerivedUnits}]`;
                }
                line += `]`;
            }
            line += `}\n`;
            output += line;
        }
        fs.writeFileSync(outputPath, output);
    }
}

Usage example: const handler = new AMLXHandler('file.amlx'); const properties = handler.openAndDecode(); handler.write('new.amlx');

5. C Class (Using Struct and Functions, as C Has No Native Classes; Assumes Standard Library Only)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_LINE_LEN 1024
#define MAX_PORTS 10
#define MAX_DATA_PER_PORT 20
#define MAX_DERIVED 10

typedef struct {
    char parameterName[50];
    double value;
    char parameterUnits[20];
} DataEntry;

typedef struct {
    char rawParameterName[50];
    double rawValue;  // Use double for flexibility
    char rawUnits[20];
} RawEntry;

typedef struct {
    int portNumber;
    DataEntry data[MAX_DATA_PER_PORT];
    int dataCount;
    RawEntry raw[MAX_DATA_PER_PORT];
    int rawCount;
} Port;

typedef struct {
    int msgNumber;
    double unixEpochTime;
    char status[50];
    Port ports[MAX_PORTS];
    int portCount;
    DataEntry derived[MAX_DERIVED];  // Reuse DataEntry for derived
    int derivedCount;
} Message;

typedef struct {
    char *filepath;
    Message *messages;
    int messageCount;
} AMLXHandler;

// Helper to parse number (int or float)
double parseNumber(const char *str) {
    if (strchr(str, '.')) return atof(str);
    return atoi(str);
}

// Function to open and decode
int open_and_decode(AMLXHandler *handler) {
    FILE *fp = fopen(handler->filepath, "r");
    if (!fp) return -1;
    char line[MAX_LINE_LEN];
    handler->messages = malloc(sizeof(Message) * 100);  // Arbitrary max messages
    handler->messageCount = 0;
    while (fgets(line, MAX_LINE_LEN, fp)) {
        if (strncmp(line, "msg", 3) != 0) continue;
        Message *msg = &handler->messages[handler->messageCount];
        memset(msg, 0, sizeof(Message));
        // Parse MsgNumber
        sscanf(line, "msg%d", &msg->msgNumber);
        // Find mux
        char *muxPos = strstr(line, "mux");
        if (muxPos) {
            sscanf(muxPos, "mux[meta=time,%lf,s][data=uv,%[^]]]", &msg->unixEpochTime, msg->status);
        }
        // Parse ports
        char *portPos = strstr(line, "port");
        while (portPos && msg->portCount < MAX_PORTS) {
            Port *port = &msg->ports[msg->portCount];
            sscanf(portPos, "port%d[", &port->portNumber);
            char *dataPos = strstr(portPos, "[data=");
            while (dataPos && dataPos < strstr(portPos, "]")) {
                if (port->dataCount < MAX_DATA_PER_PORT) {
                    DataEntry *entry = &port->data[port->dataCount++];
                    sscanf(dataPos, "[data=%[^,],%lf,%[^]]]", entry->parameterName, &entry->value, entry->parameterUnits);
                }
                dataPos = strstr(dataPos + 1, "[data=");
            }
            char *rawPos = strstr(portPos, "[rawi=");
            while (rawPos && rawPos < strstr(portPos, "]")) {
                if (port->rawCount < MAX_DATA_PER_PORT) {
                    RawEntry *entry = &port->raw[port->rawCount++];
                    char rawValStr[50];
                    sscanf(rawPos, "[rawi=%[^,],%[^,],%[^]]]", entry->rawParameterName, rawValStr, entry->rawUnits);
                    entry->rawValue = parseNumber(rawValStr);
                }
                rawPos = strstr(rawPos + 1, "[rawi=");
            }
            msg->portCount++;
            portPos = strstr(portPos + 1, "port");
        }
        // Parse derive
        char *derivePos = strstr(line, "derive");
        if (derivePos) {
            char *derDataPos = strstr(derivePos, "[data=");
            while (derDataPos && derDataPos < strstr(derivePos, "]") && msg->derivedCount < MAX_DERIVED) {
                DataEntry *entry = &msg->derived[msg->derivedCount++];
                sscanf(derDataPos, "[data=%[^,],%lf,%[^]]]", entry->parameterName, &entry->value, entry->parameterUnits);
                derDataPos = strstr(derDataPos + 1, "[data=");
            }
        }
        handler->messageCount++;
    }
    fclose(fp);
    return 0;
}

// Function to write
int write(AMLXHandler *handler, const char *outputPath) {
    if (!outputPath) outputPath = handler->filepath;
    FILE *fp = fopen(outputPath, "w");
    if (!fp) return -1;
    for (int i = 0; i < handler->messageCount; i++) {
        Message *msg = &handler->messages[i];
        fprintf(fp, "msg%d{mux[meta=time,%.2f,s][data=uv,%s],", msg->msgNumber, msg->unixEpochTime, msg->status);
        for (int p = 0; p < msg->portCount; p++) {
            Port *port = &msg->ports[p];
            fprintf(fp, "port%d[", port->portNumber);
            for (int d = 0; d < port->dataCount; d++) {
                DataEntry *entry = &port->data[d];
                fprintf(fp, "[data=%s,%.6f,%s]", entry->parameterName, entry->value, entry->parameterUnits);
            }
            for (int r = 0; r < port->rawCount; r++) {
                RawEntry *entry = &port->raw[r];
                if (entry->rawValue == (int)entry->rawValue) {
                    fprintf(fp, "[rawi=%s,%d,%s]", entry->rawParameterName, (int)entry->rawValue, entry->rawUnits);
                } else {
                    fprintf(fp, "[rawi=%s,%.6f,%s]", entry->rawParameterName, entry->rawValue, entry->rawUnits);
                }
            }
            fprintf(fp, "],");
        }
        if (msg->derivedCount > 0) {
            fprintf(fp, "derive[");
            for (int d = 0; d < msg->derivedCount; d++) {
                DataEntry *entry = &msg->derived[d];
                fprintf(fp, "[data=%s,%.6f,%s]", entry->parameterName, entry->value, entry->parameterUnits);
            }
            fprintf(fp, "]");
        }
        fprintf(fp, "}\n");
    }
    fclose(fp);
    return 0;
}

// Usage example in main:
// AMLXHandler handler = {"file.amlx", NULL, 0};
// open_and_decode(&handler);
// write(&handler, "new.amlx");
// free(handler.messages);