diff --git a/packages/imagetools_3/astroViteConfigs.js b/packages/imagetools_3/astroViteConfigs.js
index 2f7b5cc..8b2d327 100644
--- a/packages/imagetools_3/astroViteConfigs.js
+++ b/packages/imagetools_3/astroViteConfigs.js
@@ -1,12 +1,12 @@
export default {
- "environment": "build",
+ "environment": "dev",
"isSsrBuild": false,
"projectBase": "",
"publicDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\public\\",
"rootDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\",
- "mode": "production",
- "outDir": "C:\\Users\\zx\\Desktop\\polymech\\site2\\dist\\",
- "assetsDir": "_astro",
+ "mode": "dev",
+ "outDir": "dist",
+ "assetsDir": "/_astro",
"sourcemap": false,
"assetFileNames": "/_astro/[name]@[width].[hash][extname]"
}
\ No newline at end of file
diff --git a/packages/polymech/src/app/config-loader.ts b/packages/polymech/src/app/config-loader.ts
index 60c74f4..e31e9ef 100644
--- a/packages/polymech/src/app/config-loader.ts
+++ b/packages/polymech/src/app/config-loader.ts
@@ -22,8 +22,7 @@ export function loadConfig(
}
const variables = {
- LANG: locale,
- ...process.env
+ LANG: locale
};
const substitutedContent = substitute(false, rawContent, variables);
diff --git a/packages/polymech/src/components/modbus/CoilsTable.astro b/packages/polymech/src/components/modbus/CoilsTable.astro
new file mode 100644
index 0000000..fe57101
--- /dev/null
+++ b/packages/polymech/src/components/modbus/CoilsTable.astro
@@ -0,0 +1,117 @@
+---
+import { getProcessedCoilsData } from '../../utils/modbus-data';
+import { getModbusFunctionName, getFunctionCategory } from '../../utils/modbusUtils';
+
+const groupedCoils = getProcessedCoilsData();
+const groupNames = Object.keys(groupedCoils).sort();
+---
+
+
+ {groupNames.map(groupName => (
+
+
{groupName}
+
+
+
+
+ | FC |
+ Address |
+ Name |
+ Component |
+ ID |
+
+
+
+ {groupedCoils[groupName].map(coil => {
+ return (
+
+ | 1/5 |
+ {coil.address} |
+ {coil.name} |
+ {coil.component} |
+ {coil.id} |
+
+ );
+ })}
+
+
+
+
+ ))}
+
+
+
diff --git a/packages/polymech/src/components/modbus/RegistersTable.astro b/packages/polymech/src/components/modbus/RegistersTable.astro
new file mode 100644
index 0000000..2df667d
--- /dev/null
+++ b/packages/polymech/src/components/modbus/RegistersTable.astro
@@ -0,0 +1,258 @@
+---
+import {
+ getProcessedRegistersData,
+ getRegisterDescription,
+} from "@polymech/astro-base/utils/modbus-data.js";
+
+import { parseRegisterName } from "@polymech/astro-base/utils/modbusUtils.js";
+
+const groupedRegisters = getProcessedRegistersData();
+const groupNames = Object.keys(groupedRegisters).sort();
+---
+
+
+ {
+ groupNames.map((groupName) => (
+
+
{groupName}
+
+
+
+
+ | FC |
+ Address |
+ Name |
+ Component |
+ Description |
+
+
+
+ {groupedRegisters[groupName].map((register) => {
+ const parsed = parseRegisterName(register.name);
+ const baseName = register.name.split("(")[0];
+ const description = getRegisterDescription(register.address);
+
+ return (
+
+ | {register.type} |
+ {register.address} |
+
+
+ {baseName}
+ {parsed && (
+
+ {parsed.enumValues.map(({ val, label }, index) => (
+
+ {val}
+ {label}
+
+ ))}
+
+ )}
+
+ |
+ {register.component} |
+ {description || ""} |
+
+ );
+ })}
+
+
+
+
+ ))
+ }
+
+
+
diff --git a/packages/polymech/src/components/polymech/conditional.astro b/packages/polymech/src/components/polymech/conditional.astro
new file mode 100644
index 0000000..eb2af1c
--- /dev/null
+++ b/packages/polymech/src/components/polymech/conditional.astro
@@ -0,0 +1,8 @@
+---
+import DefaultFallback from "@/components/Default.astro"
+const { expression = false, fallback = DefaultFallback } = Astro.props
+const currentLocal = Astro.currentLocale || "en"
+---
+
diff --git a/packages/polymech/src/components/polymech/image.astro b/packages/polymech/src/components/polymech/image.astro
new file mode 100644
index 0000000..1649622
--- /dev/null
+++ b/packages/polymech/src/components/polymech/image.astro
@@ -0,0 +1,22 @@
+---
+import { DEFAULT_IMAGE_URL } from '@/app/config.js'
+import { Picture } from "imagetools/components"
+import { image_url } from "@/base/media.js"
+
+interface SafeImageProps {
+ src: string
+ alt: string
+ fallback?: string
+ [propName: string]: any
+}
+
+const {
+ src,
+ alt,
+ fallback = DEFAULT_IMAGE_URL,
+ ...rest
+} = Astro.props as SafeImageProps
+
+const srcSafe = await image_url(src, fallback)
+---
+
diff --git a/packages/polymech/src/components/polymech/jsx.astro b/packages/polymech/src/components/polymech/jsx.astro
new file mode 100644
index 0000000..71a70b5
--- /dev/null
+++ b/packages/polymech/src/components/polymech/jsx.astro
@@ -0,0 +1,7 @@
+---
+import JsxParser from 'react-jsx-parser'
+const {...rest} = Astro.props
+---
+
diff --git a/packages/polymech/src/components/polymech/link.astro b/packages/polymech/src/components/polymech/link.astro
new file mode 100644
index 0000000..2517026
--- /dev/null
+++ b/packages/polymech/src/components/polymech/link.astro
@@ -0,0 +1,11 @@
+---
+const { href, className } = Astro.props;
+---
+
+
+
+
+
diff --git a/packages/polymech/src/components/polymech/readme.astro b/packages/polymech/src/components/polymech/readme.astro
new file mode 100644
index 0000000..1bab92b
--- /dev/null
+++ b/packages/polymech/src/components/polymech/readme.astro
@@ -0,0 +1,48 @@
+---
+import { createMarkdownComponent } from "@/base/index.js";
+import { translate } from "@/base/i18n.js";
+import { I18N_SOURCE_LANGUAGE, ASSET_URL } from "config/config.js";
+import { fromMarkdown } from "mdast-util-from-markdown";
+import { toMarkdown } from "mdast-util-to-markdown";
+import { visit } from "unist-util-visit";
+import { Root, Image } from "mdast";
+
+interface Props {
+ markdown: string;
+ className?: string;
+ baseImageUrl?: string;
+ translate?: boolean;
+ data?: any;
+}
+const {
+ markdown,
+ className = "",
+ baseImageUrl = "",
+ translate: _translate = false,
+ data = {},
+ ...rest
+} = Astro.props as Props;
+
+const processImageUrls = (content: string, data: Record) => {
+ const tree = fromMarkdown(content) as Root;
+ visit(tree, "image", (node: Image) => {
+ if (!node.url.startsWith("http") && !node.url.startsWith("/")) {
+ node.url = ASSET_URL(node.url, data);
+ }
+ });
+ const markup = toMarkdown(tree);
+ return markup;
+};
+
+const content = await translate(
+ processImageUrls(markdown, data),
+ I18N_SOURCE_LANGUAGE,
+ Astro.currentLocale,
+);
+
+const ReadmeContent = await createMarkdownComponent(content);
+---
+
+
+
+
diff --git a/packages/polymech/src/components/polymech/remote.astro b/packages/polymech/src/components/polymech/remote.astro
new file mode 100644
index 0000000..8c66509
--- /dev/null
+++ b/packages/polymech/src/components/polymech/remote.astro
@@ -0,0 +1,11 @@
+---
+// Example: Fetch Markdown from a remote API
+// and render it to HTML, at runtime.
+// Using "marked" (https://github.com/markedjs/marked)
+import { marked } from 'marked'
+const {...rest} = Astro.props
+const response = await fetch(rest.src || 'https://raw.githubusercontent.com/wiki/adam-p/markdown-here/Markdown-Cheatsheet.md')
+const markdown = await response.text();
+const content = marked.parse(markdown);
+---
+
\ No newline at end of file
diff --git a/packages/polymech/src/components/polymech/renderer.ts b/packages/polymech/src/components/polymech/renderer.ts
new file mode 100644
index 0000000..172ae99
--- /dev/null
+++ b/packages/polymech/src/components/polymech/renderer.ts
@@ -0,0 +1,53 @@
+import { unified } from "unified"
+import remarkParse from "remark-parse"
+import remarkGfm from "remark-gfm"
+import remarkRehype from "remark-rehype"
+import rehypeRaw from "rehype-raw"
+import emoji from "remark-emoji"
+import rehypeStringify from "rehype-stringify"
+import { createComponent } from "astro/runtime/server/astro-component.js"
+import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js"
+
+// Define the type for the component map
+type ComponentMap = Record) => unknown>;
+
+// Function to convert Markdown to HTML and replace elements with Astro components
+export async function markdown(input: string, componentsMap: ComponentMap = {}): Promise {
+ const slots: unknown[] = [];
+ let slotIndex = 0;
+
+ // Ensure input is treated as UTF-8
+ const markdownText = new TextDecoder("utf-8").decode(new TextEncoder().encode(input));
+
+ const processedHtml = await unified()
+ .use(remarkParse) // Parse Markdown to AST
+ .use(emoji, {
+ accessible: true, // Defaults to false
+ emoticon: true, // Defaults to false
+ })
+ .use(remarkGfm) // Enable tables, strikethrough, autolinks, etc.
+ .use(remarkRehype, { allowDangerousHtml: true }) // Convert Markdown to HTML AST
+ .use(rehypeRaw) // Process raw HTML inside Markdown
+ .use(() => (tree) => {
+ function transformNode(node: { type: string; tagName?: string; properties?: Record; value?: string; children?: any[] }) {
+ if (node.type === "element" && node.tagName && componentsMap[node.tagName]) {
+ const props = node.properties || {};
+
+ // Register the component as a slot instead of calling it directly
+ const slotName = `COMPONENT_SLOT_${slotIndex++}`;
+ slots[slotName] = { component: componentsMap[node.tagName], props };
+ node.type = "text";
+ node.value = ``; // Slot placeholder
+ }
+
+ if (node.children) {
+ node.children.forEach(transformNode);
+ }
+ }
+ transformNode(tree);
+ })
+ .use(rehypeStringify) // Convert AST back to HTML
+ .process(markdownText);
+
+ return createComponent(() => renderTemplate(unescapeHTML(processedHtml.toString()), slots));
+}
diff --git a/packages/polymech/src/components/polymech/resources.astro b/packages/polymech/src/components/polymech/resources.astro
new file mode 100644
index 0000000..b2b6344
--- /dev/null
+++ b/packages/polymech/src/components/polymech/resources.astro
@@ -0,0 +1,132 @@
+---
+import Link from "./link.astro";
+import { createMarkdownComponent } from "@/base/index.js";
+import Translation from "@polymech/astro-base/components/i18n.astro";
+
+const { frontmatter: data } = Astro.props;
+const LINK_CLASSES = "text-blue-400 hover:text-blue-800 hover:underline";
+
+const getHref = (key, data) => {
+ switch (key) {
+ case "cad":
+ return data.cad?.[0]?.[".html"];
+ default:
+ return data[key];
+ }
+};
+
+const checkCondition = (key, data) => {
+ switch (key) {
+ case "cad":
+ return !!data.Preview3d && !!data.cad?.[0]?.[".html"];
+ default:
+ return true;
+ }
+};
+
+const links = (data) =>
+ ["forum", "firmware", "download", "tests", "cad"].map((key) => {
+ return {
+ key,
+ label: key.charAt(0).toUpperCase() + key.slice(1),
+ getHref: () => getHref(key, data),
+ condition: () => checkCondition(key, data),
+ };
+ });
+
+const componentsLinks = (data) =>
+ data.components?.map((comp) => ({
+ key: comp.name,
+ label: comp.name,
+ getHref: () => comp.store,
+ condition: () => !!comp.store,
+ })) || [];
+
+const authorsLinks = (data) =>
+ data.authors?.map((author) => ({
+ key: author.name,
+ label: author.name,
+ getHref: () => author.url,
+ condition: () => !!author.url,
+ })) || [];
+
+const groups = {
+ Resources: links(data),
+ ...(data.components && { Components: componentsLinks(data) }),
+ ...(data.authors && { Authors: authorsLinks(data) }),
+};
+
+const filteredGroups: Record<
+ string,
+ {
+ key: string;
+ label: string;
+ getHref: () => string;
+ condition: () => boolean;
+ }[]
+> = Object.entries(groups).reduce((acc, [name, links]) => {
+ const validLinks = (links as any[]).filter(
+ (link) => link.getHref() && link.condition(),
+ );
+ if (validLinks.length > 0) {
+ acc[name] = validLinks;
+ }
+ return acc;
+}, {});
+
+const extraContent = [data.extra_resources, data.tests]
+ .filter((s) => s && s.length)
+ .map(createMarkdownComponent)
+
+const sharedContent = [data.shared_resources]
+ .filter((s) => s && s.length)
+ .map(createMarkdownComponent)
+---
+
+
+
+
+ {
+ Object.entries(filteredGroups).map(([name, links]) => (
+
+
+ {name}
+
+
+ {links.map((link) => (
+
+ {link.label}
+
+ ))}
+
+
+ ))
+ }
+
+ {
+ extraContent.length > 0 && (
+
+
+ Extras
+
+
+
+ )
+ }
+ {
+ sharedContent.length > 0 && (
+
+
+
+ )
+ }
+
+
diff --git a/packages/polymech/src/utils/markdown/rehype-custom-img.mjs b/packages/polymech/src/utils/markdown/rehype-custom-img.mjs
new file mode 100644
index 0000000..9dd07bb
--- /dev/null
+++ b/packages/polymech/src/utils/markdown/rehype-custom-img.mjs
@@ -0,0 +1,36 @@
+import { visit } from 'unist-util-visit';
+import path from 'path';
+
+export default function rehypeCustomImg() {
+ return (tree, file) => {
+ if(!file.path.endsWith('.mdx')) {
+ return;
+ }
+
+ const contentDir = path.join(process.cwd(), 'src', 'content');
+ const entryPath = path.relative(contentDir, file.history[0]).replace(/\\/g, '/');
+
+ // 1. Add the import statement for RelativeImage.
+ tree.children.unshift({
+ type: 'mdxjsEsm',
+ value: "import RelativeImage from '~/components/imagetools/RelativeImage.astro';"
+ });
+
+ // 2. Visit all JSX nodes and inject entryPath into
tags.
+ visit(tree, 'mdxJsxFlowElement', (node) => {
+ if (node.name === 'img') {
+ node.attributes.push({
+ type: 'mdxJsxAttribute',
+ name: 'entryPath',
+ value: entryPath
+ });
+ }
+ });
+
+ // 3. Export the components mapping.
+ tree.children.push({
+ type: 'mdxjsEsm',
+ value: 'export const components = { img: RelativeImage };'
+ });
+ };
+}
diff --git a/packages/polymech/src/utils/modbus-data.ts b/packages/polymech/src/utils/modbus-data.ts
new file mode 100644
index 0000000..7a75741
--- /dev/null
+++ b/packages/polymech/src/utils/modbus-data.ts
@@ -0,0 +1,128 @@
+import fs from 'fs';
+import path from 'path';
+
+export interface CoilData {
+ address: number;
+ value: number;
+ name: string;
+ component: string;
+ id: number;
+ group: string;
+}
+
+export interface RegisterData {
+ error: number;
+ address: number;
+ value: number;
+ name: string;
+ component: string;
+ id: number;
+ type: number;
+ slaveId: number;
+ flags: number;
+ group: string;
+}
+
+export interface GroupedData {
+ [groupName: string]: T[];
+}
+
+export interface RegDescription {
+ address: number;
+ description: string;
+}
+
+/**
+ * Read and parse coils.json file
+ */
+export function readCoilsData(): CoilData[] {
+ try {
+ const filePath = path.join(process.cwd(), 'src/content/resources/cassandra/coils.json');
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
+ return JSON.parse(fileContent) as CoilData[];
+ } catch (error) {
+ console.error('Error reading coils.json:', error);
+ return [];
+ }
+}
+
+/**
+ * Read and parse registers.json file
+ */
+export function readRegistersData(): RegisterData[] {
+ try {
+ const filePath = path.join(process.cwd(), 'src/content/resources/cassandra/registers.json');
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
+ return JSON.parse(fileContent) as RegisterData[];
+ } catch (error) {
+ console.error('Error reading registers.json:', error);
+ return [];
+ }
+}
+
+/**
+ * Group data by the 'group' field
+ */
+export function groupData(data: T[]): GroupedData {
+ return data.reduce((acc, item) => {
+ if (!acc[item.group]) {
+ acc[item.group] = [];
+ }
+ acc[item.group].push(item);
+ return acc;
+ }, {} as GroupedData);
+}
+
+/**
+ * Sort grouped data by address within each group
+ */
+export function sortGroupedDataByAddress(groupedData: GroupedData): GroupedData {
+ const sorted: GroupedData = {};
+
+ Object.keys(groupedData).forEach(group => {
+ sorted[group] = groupedData[group].sort((a, b) => a.address - b.address);
+ });
+
+ return sorted;
+}
+
+/**
+ * Get processed coils data grouped by component/group
+ */
+export function getProcessedCoilsData(): GroupedData {
+ const coilsData = readCoilsData();
+ const groupedData = groupData(coilsData);
+ return sortGroupedDataByAddress(groupedData);
+}
+
+/**
+ * Read and parse regs.json file for descriptions
+ */
+export function readRegDescriptions(): RegDescription[] {
+ try {
+ const filePath = path.join(process.cwd(), 'src/content/resources/cassandra/regs.json');
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
+ return JSON.parse(fileContent) as RegDescription[];
+ } catch (error) {
+ console.error('Error reading regs.json:', error);
+ return [];
+ }
+}
+
+/**
+ * Get processed registers data grouped by component/group
+ */
+export function getProcessedRegistersData(): GroupedData {
+ const registersData = readRegistersData();
+ const groupedData = groupData(registersData);
+ return sortGroupedDataByAddress(groupedData);
+}
+
+/**
+ * Get description for a specific register address
+ */
+export function getRegisterDescription(address: number): string | null {
+ const descriptions = readRegDescriptions();
+ const found = descriptions.find(desc => desc.address === address);
+ return found ? found.description : null;
+}
diff --git a/packages/polymech/src/utils/modbusUtils.ts b/packages/polymech/src/utils/modbusUtils.ts
new file mode 100644
index 0000000..0da19b5
--- /dev/null
+++ b/packages/polymech/src/utils/modbusUtils.ts
@@ -0,0 +1,128 @@
+export interface EnumValue {
+ val: number;
+ label: string;
+}
+
+export interface ParsedRegister {
+ enumValues: EnumValue[];
+ isFlags?: boolean;
+}
+
+/**
+ * Parse register name to extract enum values and flag information
+ * Examples:
+ * - "Status(0:IDLE, 1:INITIALIZING, 2:RUNNING, 3:PAUSED, 4:STOPPED, 5:FINISHED)"
+ * - "CFlags(1:MINLOAD,2:MAX_TIME,4:STALLED,8:BALANCE,16:LOADCELL,32:MULTI_TIMEOUT)"
+ * - "Mode(0:NONE,1:MANUAL,2:AUTO,3:MANUAL_MULTI,4:AUTO_MULTI,5:AUTO_MULTI_BALANCED,6:REMOTE)"
+ */
+export function parseRegisterName(name: string): ParsedRegister | null {
+ if (!name) return null;
+
+ // Look for pattern like "Name(0:VALUE, 1:VALUE2, ...)"
+ const enumMatch = name.match(/\(([^)]+)\)/);
+ if (!enumMatch) return null;
+
+ const enumString = enumMatch[1];
+ const enumValues: EnumValue[] = [];
+
+ // Split by comma and parse each enum value
+ const parts = enumString.split(',');
+
+ for (const part of parts) {
+ const trimmed = part.trim();
+ // Match pattern like "0:IDLE" or "1:INITIALIZING"
+ const valueMatch = trimmed.match(/^(\d+):(.+)$/);
+
+ if (valueMatch) {
+ const val = parseInt(valueMatch[1], 10);
+ const label = valueMatch[2].trim();
+
+ if (!isNaN(val)) {
+ enumValues.push({ val, label });
+ }
+ }
+ }
+
+ if (enumValues.length === 0) return null;
+
+ // Determine if this is flags based on the name or values
+ const isFlags = name.toLowerCase().includes('flags') ||
+ name.toLowerCase().includes('cflags') ||
+ // Check if values are powers of 2 (typical for flags)
+ enumValues.every(ev => ev.val > 0 && (ev.val & (ev.val - 1)) === 0);
+
+ return {
+ enumValues,
+ isFlags
+ };
+}
+
+/**
+ * Format enum values for display
+ */
+export function formatEnumValues(enumValues: EnumValue[]): string {
+ return enumValues.map(ev => `${ev.val}:${ev.label}`).join(', ');
+}
+
+/**
+ * Get the label for a specific enum value
+ */
+export function getEnumLabel(enumValues: EnumValue[], value: number): string | null {
+ const found = enumValues.find(ev => ev.val === value);
+ return found ? found.label : null;
+}
+
+/**
+ * Get active flag labels for a flag value
+ */
+export function getActiveFlagLabels(enumValues: EnumValue[], value: number): string[] {
+ return enumValues
+ .filter(ev => (value & ev.val) === ev.val && ev.val > 0)
+ .map(ev => ev.label);
+}
+
+/**
+ * Modbus function type mappings
+ */
+export const MODBUS_FUNCTION_TYPES = {
+ // Coil functions
+ 1: 'Read Coils',
+ 5: 'Write Single Coil',
+ 15: 'Write Multiple Coils',
+
+ // Discrete Input functions
+ 2: 'Read Discrete Inputs',
+
+ // Holding Register functions
+ 3: 'Read Holding Registers',
+ 6: 'Write Single Register',
+ 16: 'Write Multiple Registers',
+
+ // Input Register functions
+ 4: 'Read Input Registers'
+} as const;
+
+/**
+ * Get the Modbus function name from type number
+ */
+export function getModbusFunctionName(type: number): string {
+ return MODBUS_FUNCTION_TYPES[type as keyof typeof MODBUS_FUNCTION_TYPES] || `Function ${type}`;
+}
+
+/**
+ * Determine if a register/coil is writable based on its type
+ */
+export function isWritableFunction(type: number): boolean {
+ return [5, 6, 15, 16].includes(type);
+}
+
+/**
+ * Get function type category
+ */
+export function getFunctionCategory(type: number): 'coil' | 'discrete' | 'holding' | 'input' | 'unknown' {
+ if ([1, 5, 15].includes(type)) return 'coil';
+ if ([2].includes(type)) return 'discrete';
+ if ([3, 6, 16].includes(type)) return 'holding';
+ if ([4].includes(type)) return 'input';
+ return 'unknown';
+}