glob extensions | cpp preset

This commit is contained in:
lovebird 2025-06-04 00:13:27 +02:00
parent ddc8df1cc9
commit ff0220b468
39 changed files with 4095 additions and 560 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12
packages/kbot/dist-in/glob.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import { sync as exists } from '@polymech/fs/exists';
import { IKBotTask } from '@polymech/ai-tools';
export declare const default_filters: {
isFile: (src: string) => boolean;
exists: typeof exists;
size: (filePath: string) => boolean;
};
export declare const isWebUrl: (str: string) => boolean;
export declare const glob: (projectPath: string, include?: string[], exclude?: string[], options?: IKBotTask) => {
files: string[];
webUrls: Set<string>;
};

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,7 @@ import { IKBotTask } from '@polymech/ai-tools';
export declare const logger: Logger<ILogObj>;
export declare const getLogger: (options: IKBotTask) => Logger<ILogObj>;
export { run } from './commands/run.js';
export { complete_options, complete_messages, complete_params } from './commands/run.js';
export declare const module_root: () => string;
export declare const assistant_supported: Record<string, string>;
export * from './types.js';

View File

@ -40,6 +40,7 @@ export const getLogger = (options) => {
return _logger;
};
export { run } from './commands/run.js';
export { complete_options, complete_messages, complete_params } from './commands/run.js';
export const module_root = () => path.resolve(path.join(get_var(isWindows ? 'HOMEPATH' : 'HOME'), `.${MODULE_NAME}`));
export const assistant_supported = {
".c": "text/x-c",
@ -70,4 +71,4 @@ export * from './zod_schema.js';
export { E_OPENAI_MODEL } from './models/cache/openai-models.js';
export { E_OPENROUTER_MODEL } from './models/cache/openrouter-models.js';
export { E_OPENROUTER_MODEL_FREE } from './models/cache/openrouter-models-free.js';
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGNBQWMsQ0FBQTtBQUN2QyxPQUFPLElBQUksTUFBTSxXQUFXLENBQUE7QUFDNUIsT0FBTyxFQUFFLE1BQU0sRUFBVyxNQUFNLE9BQU8sQ0FBQTtBQUN2QyxNQUFNLFNBQVMsR0FBRyxRQUFRLEtBQUssT0FBTyxDQUFBO0FBRXRDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQTtBQUMzQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHNUMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUM1QyxNQUFNLENBQUMsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLFdBQVcsQ0FBK0IsQ0FBQTtBQUU3RSxJQUFJLE9BQU8sR0FBb0IsTUFBTSxDQUFBO0FBRXJDLE1BQU0sQ0FBQyxNQUFNLFNBQVMsR0FBRyxDQUFDLE9BQWtCLEVBQW1CLEVBQUU7SUFDL0QsSUFBSSxPQUFPLEVBQUUsQ0FBQztRQUNaLE9BQU8sT0FBTyxDQUFBO0lBQ2hCLENBQUM7SUFDRCxNQUFNLFdBQVcsR0FBMkI7UUFDMUMsT0FBTyxFQUFFLENBQUM7UUFDVixPQUFPLEVBQUUsQ0FBQztRQUNWLE9BQU8sRUFBRSxDQUFDO1FBQ1YsTUFBTSxFQUFFLENBQUM7UUFDVCxNQUFNLEVBQUUsQ0FBQztRQUNULE9BQU8sRUFBRSxDQUFDO1FBQ1YsT0FBTyxFQUFFLENBQUM7S0FDWCxDQUFBO0lBRUQsSUFBSSxRQUFRLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFBO0lBQ2xDLElBQUksT0FBTyxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ3JCLElBQUksT0FBTyxPQUFPLENBQUMsUUFBUSxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3pDLE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFrQixDQUFBO1lBQzNDLFFBQVEsR0FBRyxXQUFXLENBQUMsUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLElBQUksV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFBO1FBQ3ZFLENBQUM7YUFBTSxDQUFDO1lBQ04sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUE7UUFDN0IsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLEdBQUcsSUFBSSxNQUFNLENBQVU7UUFDNUIsSUFBSSxFQUFFLFdBQVc7UUFDakIsUUFBUTtRQUNSLDRCQUE0QixFQUFFLElBQUk7UUFDbEMsZUFBZSxFQUFFLEtBQUs7UUFDdEIsaUJBQWlCLEVBQUUsb0RBQW9EO0tBQ3hFLENBQUMsQ0FBQTtJQUNGLE9BQU8sT0FBTyxDQUFBO0FBQ2hCLENBQUMsQ0FBQTtBQUVELE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQTtBQUN2QyxNQUFNLENBQUMsTUFBTSxXQUFXLEdBQUcsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEVBQUUsSUFBSSxXQUFXLEVBQUUsQ0FBQyxDQUFDLENBQUE7QUFFckgsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQTJCO0lBQ3pELElBQUksRUFBRSxVQUFVO0lBQ2hCLE1BQU0sRUFBRSxZQUFZO0lBQ3BCLEtBQUssRUFBRSxlQUFlO0lBQ3RCLE1BQU0sRUFBRSxVQUFVO0lBQ2xCLE1BQU0sRUFBRSxvQkFBb0I7SUFDNUIsT0FBTyxFQUFFLHlFQUF5RTtJQUNsRixLQUFLLEVBQUUsZUFBZTtJQUN0QixPQUFPLEVBQUUsV0FBVztJQUNwQixPQUFPLEVBQUUsYUFBYTtJQUN0QixLQUFLLEVBQUUsaUJBQWlCO0lBQ3hCLE9BQU8sRUFBRSxrQkFBa0I7SUFDM0IsS0FBSyxFQUFFLGVBQWU7SUFDdEIsTUFBTSxFQUFFLGlCQUFpQjtJQUN6QixNQUFNLEVBQUUsWUFBWTtJQUNwQixPQUFPLEVBQUUsMkVBQTJFO0lBQ3BGLEtBQUssRUFBRSxlQUFlO0lBQ3RCLEtBQUssRUFBRSxhQUFhO0lBQ3BCLEtBQUssRUFBRSxrQkFBa0I7SUFDekIsTUFBTSxFQUFFLFlBQVk7SUFDcEIsS0FBSyxFQUFFLHdCQUF3QjtJQUMvQixNQUFNLEVBQUUsWUFBWTtDQUNyQixDQUFBO0FBQ0QsY0FBYyxZQUFZLENBQUE7QUFDMUIsY0FBYyxnQkFBZ0IsQ0FBQTtBQUM5QixjQUFjLGlCQUFpQixDQUFBO0FBRS9CLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQTtBQUNoRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQTtBQUN4RSxPQUFPLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSwwQ0FBMEMsQ0FBQSJ9
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGNBQWMsQ0FBQTtBQUN2QyxPQUFPLElBQUksTUFBTSxXQUFXLENBQUE7QUFDNUIsT0FBTyxFQUFFLE1BQU0sRUFBVyxNQUFNLE9BQU8sQ0FBQTtBQUN2QyxNQUFNLFNBQVMsR0FBRyxRQUFRLEtBQUssT0FBTyxDQUFBO0FBRXRDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQTtBQUMzQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0JBQWdCLENBQUE7QUFHNUMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUM1QyxNQUFNLENBQUMsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLFdBQVcsQ0FBK0IsQ0FBQTtBQUU3RSxJQUFJLE9BQU8sR0FBb0IsTUFBTSxDQUFBO0FBRXJDLE1BQU0sQ0FBQyxNQUFNLFNBQVMsR0FBRyxDQUFDLE9BQWtCLEVBQW1CLEVBQUU7SUFDL0QsSUFBSSxPQUFPLEVBQUUsQ0FBQztRQUNaLE9BQU8sT0FBTyxDQUFBO0lBQ2hCLENBQUM7SUFDRCxNQUFNLFdBQVcsR0FBMkI7UUFDMUMsT0FBTyxFQUFFLENBQUM7UUFDVixPQUFPLEVBQUUsQ0FBQztRQUNWLE9BQU8sRUFBRSxDQUFDO1FBQ1YsTUFBTSxFQUFFLENBQUM7UUFDVCxNQUFNLEVBQUUsQ0FBQztRQUNULE9BQU8sRUFBRSxDQUFDO1FBQ1YsT0FBTyxFQUFFLENBQUM7S0FDWCxDQUFBO0lBRUQsSUFBSSxRQUFRLEdBQUcsV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFBO0lBQ2xDLElBQUksT0FBTyxDQUFDLFFBQVEsRUFBRSxDQUFDO1FBQ3JCLElBQUksT0FBTyxPQUFPLENBQUMsUUFBUSxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3pDLE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFrQixDQUFBO1lBQzNDLFFBQVEsR0FBRyxXQUFXLENBQUMsUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLElBQUksV0FBVyxDQUFDLE1BQU0sQ0FBQyxDQUFBO1FBQ3ZFLENBQUM7YUFBTSxDQUFDO1lBQ04sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUE7UUFDN0IsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLEdBQUcsSUFBSSxNQUFNLENBQVU7UUFDNUIsSUFBSSxFQUFFLFdBQVc7UUFDakIsUUFBUTtRQUNSLDRCQUE0QixFQUFFLElBQUk7UUFDbEMsZUFBZSxFQUFFLEtBQUs7UUFDdEIsaUJBQWlCLEVBQUUsb0RBQW9EO0tBQ3hFLENBQUMsQ0FBQTtJQUNGLE9BQU8sT0FBTyxDQUFBO0FBQ2hCLENBQUMsQ0FBQTtBQUVELE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQTtBQUN2QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsZUFBZSxFQUFFLE1BQU0sbUJBQW1CLENBQUE7QUFDeEYsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFHLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxFQUFFLElBQUksV0FBVyxFQUFFLENBQUMsQ0FBQyxDQUFBO0FBRXJILE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUEyQjtJQUN6RCxJQUFJLEVBQUUsVUFBVTtJQUNoQixNQUFNLEVBQUUsWUFBWTtJQUNwQixLQUFLLEVBQUUsZUFBZTtJQUN0QixNQUFNLEVBQUUsVUFBVTtJQUNsQixNQUFNLEVBQUUsb0JBQW9CO0lBQzVCLE9BQU8sRUFBRSx5RUFBeUU7SUFDbEYsS0FBSyxFQUFFLGVBQWU7SUFDdEIsT0FBTyxFQUFFLFdBQVc7SUFDcEIsT0FBTyxFQUFFLGFBQWE7SUFDdEIsS0FBSyxFQUFFLGlCQUFpQjtJQUN4QixPQUFPLEVBQUUsa0JBQWtCO0lBQzNCLEtBQUssRUFBRSxlQUFlO0lBQ3RCLE1BQU0sRUFBRSxpQkFBaUI7SUFDekIsTUFBTSxFQUFFLFlBQVk7SUFDcEIsT0FBTyxFQUFFLDJFQUEyRTtJQUNwRixLQUFLLEVBQUUsZUFBZTtJQUN0QixLQUFLLEVBQUUsYUFBYTtJQUNwQixLQUFLLEVBQUUsa0JBQWtCO0lBQ3pCLE1BQU0sRUFBRSxZQUFZO0lBQ3BCLEtBQUssRUFBRSx3QkFBd0I7SUFDL0IsTUFBTSxFQUFFLFlBQVk7Q0FDckIsQ0FBQTtBQUNELGNBQWMsWUFBWSxDQUFBO0FBQzFCLGNBQWMsZ0JBQWdCLENBQUE7QUFDOUIsY0FBYyxpQkFBaUIsQ0FBQTtBQUUvQixPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0saUNBQWlDLENBQUE7QUFDaEUsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0scUNBQXFDLENBQUE7QUFDeEUsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sMENBQTBDLENBQUEifQ==

View File

@ -1,4 +1,3 @@
import { sync as exists } from '@polymech/fs/exists';
import { IHandlerResult } from './mime-handlers.js';
import { ChatCompletionContentPartImage } from 'openai/resources/index.mjs';
import { IKBotTask } from '@polymech/ai-tools';
@ -6,21 +5,8 @@ import { IKBotTask } from '@polymech/ai-tools';
* @todos
* - add support for vector stores : https://platform.openai.com/docs/assistants/tools/file-search?lang=node.js
*/
export declare const default_filters: {
isFile: (src: string) => boolean;
exists: typeof exists;
size: (filePath: string) => boolean;
};
export declare const isPathOutsideSafe: (pathA: string, pathB: string) => boolean;
export declare const base64: (filePath: string) => string | null;
export declare const images: (files: string[]) => ChatCompletionContentPartImage[];
/**
* Check if a string is a web URL
*/
export declare const isWebUrl: (str: string) => boolean;
export declare const glob: (projectPath: string, include?: string[], exclude?: string[]) => {
files: string[];
webUrls: Set<string>;
};
export declare function get(projectPath: string, include: string[], options: IKBotTask): Promise<Array<IHandlerResult>>;
export declare function vectorize(file: string, options: IKBotTask): Promise<string>;

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,3 @@
import { IKBotTask } from '@polymech/ai-tools';
export declare const generateSingleFileVariables: (filePath: string, projectPath: string) => Record<string, string>;
export declare const variables: (options: IKBotTask) => any;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,8 +23,8 @@ export interface IKBotOptions {
include?: string[] | undefined;
/** Comma separated glob patterns or paths, eg --exclude=src/*.tsx,src/*.ts --exclude=package.json */
exclude?: string[] | undefined;
/** Specify a glob extension behavior. e.g., "match-cpp" to automatically include corresponding .cpp files for .h files. */
globExtension?: string | undefined;
/** Specify a glob extension behavior. Available presets: match-cpp. Also accepts a custom glob pattern with variables like ${SRC_DIR}, ${SRC_NAME}, ${SRC_EXT}. E.g., "match-cpp" or "${SRC_DIR}/${SRC_NAME}*.cpp" */
globExtension?: (("match-cpp") | string) | undefined;
/** Explicit API key to use */
api_key?: string | undefined;
/** AI model to use for processing. Available models:

File diff suppressed because one or more lines are too long

View File

@ -53,6 +53,7 @@
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "2.1.9",
"eslint": "^8.57.1",
"rimraf": "6.0.1",
"ts-json-schema-generator": "^2.3.0",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
@ -6564,6 +6565,26 @@
"node": ">=0.10.0"
}
},
"node_modules/rimraf": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^11.0.0",
"package-json-from-dist": "^1.0.0"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",

View File

@ -27,6 +27,7 @@
"test:basic": "vitest run tests/unit/basic.test.ts",
"test:math": "vitest run tests/unit/math.test.ts",
"test:format": "vitest run tests/unit/format.test.ts",
"test:options-glob": "vitest run tests/unit/options-glob.test.ts",
"test:seo": "vitest run tests/unit/seo.test.ts",
"test:language": "vitest run tests/unit/language.test.ts",
"test:tools": "vitest run tests/unit/tools.test.ts",
@ -113,6 +114,7 @@
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "2.1.9",
"eslint": "^8.57.1",
"rimraf": "6.0.1",
"ts-json-schema-generator": "^2.3.0",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",

View File

@ -99,8 +99,18 @@
"description": "Comma separated glob patterns or paths, eg --exclude=src/*.tsx,src/*.ts --exclude=package.json"
},
"globExtension": {
"type": "string",
"description": "Specify a glob extension behavior. e.g., \"match-cpp\" to automatically include corresponding .cpp files for .h files."
"anyOf": [
{
"type": "string",
"enum": [
"match-cpp"
]
},
{
"type": "string"
}
],
"description": "Specify a glob extension behavior. Available presets: match-cpp. Also accepts a custom glob pattern with variables like ${SRC_DIR}, ${SRC_NAME}, ${SRC_EXT}. E.g., \"match-cpp\" or \"${SRC_DIR}/${SRC_NAME}*.cpp\""
},
"api_key": {
"type": "string",

View File

@ -71,7 +71,7 @@
"ui:title": "Exclude"
},
"globExtension": {
"ui:description": "Specify a glob extension behavior. e.g., \"match-cpp\" to automatically include corresponding .cpp files for .h files.",
"ui:description": "Specify a glob extension behavior. Available presets: match-cpp. Also accepts a custom glob pattern with variables like ${SRC_DIR}, ${SRC_NAME}, ${SRC_EXT}. E.g., \"match-cpp\" or \"${SRC_DIR}/${SRC_NAME}*.cpp\"",
"ui:title": "Globextension"
},
"api_key": {

View File

@ -7,8 +7,11 @@ import { IKBotTask } from '@polymech/ai-tools'
import { logger } from '../index.js'
import { onCompletion } from './run-completion.js'
import { glob } from '../source.js'
import { glob } from '../glob.js'
import { prompt } from '../prompt.js'
import { AssistantStream } from "openai/lib/AssistantStream.mjs"
import { OptionsSchema } from "../zod_schema.js"
import { cwd as processCwd } from 'node:process'
export const supported: Record<string, string> = {
".c": "text/x-c",

View File

@ -1,4 +1,7 @@
import * as path from 'node:path'
import { lookup } from 'mime-types'
import { cwd as processCwd } from 'node:process'
import { hasMagic } from 'glob'
import { sync as dir } from '@polymech/fs/dir'
import { sync as exists } from '@polymech/fs/exists'
@ -13,7 +16,8 @@ import { IKBotTask } from '@polymech/ai-tools'
import { Logger, ILogObj } from 'tslog'
import { createClient } from '../client.js'
import { OptionsSchema } from '../zod_schema.js'
import { get, isWebUrl } from '../source.js'
import { get } from '../source.js'
import { isWebUrl } from '../glob.js'
import { flatten } from '../utils/array.js'
import { collector } from '../collector.js'
import { load as loadProfile } from '../profile.js'
@ -28,8 +32,7 @@ import { runAssistant } from './run-assistant.js'
import { all } from '../models/index.js'
import { getLogger } from '../index.js'
import { lookup } from 'mime-types'
import { cwd as processCwd } from 'node:process'
export const default_sort = (files: string[]): string[] => {
const getSortableParts = (filename: string) => {

152
packages/kbot/src/glob.ts Normal file
View File

@ -0,0 +1,152 @@
import * as path from 'node:path'
import * as fs from 'node:fs'
import { sync as read } from '@polymech/fs/read'
import { sync as dir } from '@polymech/fs/dir'
import { createItem as toNode } from '@polymech/fs/inspect'
import { sync as exists } from '@polymech/fs/exists'
import { isFile, forward_slash } from '@polymech/commons'
import { logger } from './index.js'
import { lookup } from 'mime-types'
import { globSync } from 'glob'
import { EXCLUDE_GLOB, MAX_FILE_SIZE } from './constants.js'
import { defaultMimeRegistry, IHandlerResult } from './mime-handlers.js'
import { ChatCompletionContentPartImage } from 'openai/resources/index.mjs'
import { IKBotTask, ICollector } from '@polymech/ai-tools'
import { supported } from './commands/run-assistant.js'
import { handleWebUrl } from './http.js'
import { pathInfoEx } from '@polymech/commons'
import { DEFAULT_ROOTS, DEFAULT_VARS } from '@polymech/commons'
import { variables, generateSingleFileVariables } from './variables.js'
import { E_GlobExtensionType } from './zod_schema.js'
export const default_filters = {
isFile,
exists,
size: (filePath: string) => toNode(filePath).size < MAX_FILE_SIZE,
};
const isPathInside = (childPath: string, parentPath: string): boolean => {
const relation = path.relative(parentPath, childPath);
return Boolean(
relation &&
!relation.startsWith('..') &&
!relation.startsWith('..' + path.sep)
);
};
export const isWebUrl = (str: string): boolean => {
return /^https?:\/\//.test(str);
}
const globExtensionPresets: Map<E_GlobExtensionType, string> = new Map([
['match-cpp', '${SRC_DIR}/${SRC_NAME}*.cpp']
]);
const resolveAndGlobExtensionPattern = (
initialFilePath: string,
patternString: string,
projectPath: string
): string[] => {
const fileVars = generateSingleFileVariables(initialFilePath, projectPath);
let substitutedPattern = patternString;
for (const key in fileVars) {
const placeholder = new RegExp(`\\\${\\s*${key}\\s*}`, 'g');
substitutedPattern = substitutedPattern.replace(placeholder, fileVars[key]);
}
try {
const foundFiles = globSync(substitutedPattern, {
cwd: projectPath,
absolute: false,
nodir: true
});
return foundFiles.map(f => path.resolve(projectPath, f));
} catch (e) {
return [];
}
};
export const glob = (
projectPath: string,
include: string[] = [],
exclude: string[] = [],
options?: IKBotTask
): { files: string[], webUrls: Set<string> } => {
if (!exists(projectPath)) {
dir(projectPath)
return { files: [], webUrls: new Set<string>() }
}
const filters = new Set<string>()
const absolutePathsFromInclude = new Set<string>()
const webUrls = new Set<string>()
const ignorePatterns = new Set<string>(EXCLUDE_GLOB)
include.forEach(pattern => {
if (isWebUrl(pattern)) {
webUrls.add(pattern)
return
}
if (path.isAbsolute(pattern)) {
absolutePathsFromInclude.add(path.resolve(pattern));
} else {
filters.add(pattern)
}
})
exclude.forEach(pattern => {
if (isWebUrl(pattern)) {
return;
}
ignorePatterns.add(pattern);
});
const initialRelativeGlobResults = globSync([...filters], {
cwd: projectPath,
absolute: false,
ignore: [...ignorePatterns],
nodir: true
})
const initialAbsoluteFiles = new Set<string>([
...initialRelativeGlobResults.map(file => path.resolve(projectPath, file)),
...Array.from(absolutePathsFromInclude)
]);
const allFilesToConsider = new Set<string>(initialAbsoluteFiles);
if (options && typeof options.globExtension === 'string' && options.globExtension.trim() !== '') {
let actualPatternToUse = options.globExtension;
if (globExtensionPresets.has(options.globExtension as E_GlobExtensionType)) {
actualPatternToUse = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
}
for (const initialFile of [...initialAbsoluteFiles]) {
const additionalFiles = resolveAndGlobExtensionPattern(initialFile, actualPatternToUse, projectPath);
additionalFiles.forEach(f => allFilesToConsider.add(f));
}
}
const finalFiles = Array.from(allFilesToConsider).filter(absoluteFilePath => {
if (!Object.keys(default_filters).every((key) => default_filters[key](absoluteFilePath))) {
return false;
}
const relativeFilePath = path.relative(projectPath, absoluteFilePath);
const checkResult = globSync([forward_slash(relativeFilePath)], {
cwd: projectPath,
ignore: [...ignorePatterns],
nodir: true,
absolute: false
});
if (checkResult.length === 0) {
return false;
}
return true;
});
return { files: finalFiles.map(f => forward_slash(f)), webUrls }
}

View File

@ -46,6 +46,7 @@ export const getLogger = (options: IKBotTask): Logger<ILogObj> => {
}
export { run } from './commands/run.js'
export { complete_options, complete_messages, complete_params } from './commands/run.js'
export const module_root = () => path.resolve(path.join(get_var(isWindows ? 'HOMEPATH' : 'HOME'), `.${MODULE_NAME}`))
export const assistant_supported: Record<string, string> = {

View File

@ -1,34 +1,38 @@
import * as path from 'node:path'
import * as fs from 'node:fs'
import { sync as read } from '@polymech/fs/read'
import { sync as dir } from '@polymech/fs/dir'
// import { sync as dir } from '@polymech/fs/dir' // Moved to glob.ts if only used there
import { createItem as toNode } from '@polymech/fs/inspect'
import { sync as exists } from '@polymech/fs/exists'
import { isFile, forward_slash } from '@polymech/commons'
// import { createItem as toNode } from '@polymech/fs/inspect' // Moved to glob.ts
import { sync as exists } from '@polymech/fs/exists' // Still needed for vectorize
import { isFile, forward_slash } from '@polymech/commons' // isFile potentially still needed for vectorize
import { logger } from './index.js'
import { lookup } from 'mime-types'
import { globSync } from 'glob'
import { EXCLUDE_GLOB, MAX_FILE_SIZE } from './constants.js'
// import { globSync } from 'glob' // Moved to glob.ts
// import { EXCLUDE_GLOB, MAX_FILE_SIZE } from './constants.js' // Moved to glob.ts
import { defaultMimeRegistry, IHandlerResult } from './mime-handlers.js'
import { ChatCompletionContentPartImage } from 'openai/resources/index.mjs'
import { IKBotTask, ICollector } from '@polymech/ai-tools'
import { supported } from './commands/run-assistant.js'
import { handleWebUrl } from './http.js'
import { glob } from './glob.js' // Import glob from glob.ts
/**
* @todos
* - add support for vector stores : https://platform.openai.com/docs/assistants/tools/file-search?lang=node.js
*/
export const default_filters = {
isFile,
exists,
size: (filePath: string) => toNode(filePath).size < MAX_FILE_SIZE,
};
// default_filters moved to glob.ts
// isPathInside moved to glob.ts
// isWebUrl moved to glob.ts (or handled by handleWebUrl directly)
const isPathInside = (childPath: string, parentPath: string): boolean => {
const relation = path.relative(parentPath, childPath);
export const isPathOutsideSafe = (pathA: string, pathB: string): boolean => {
const realA = fs.realpathSync(pathA);
const realB = fs.realpathSync(pathB);
// Assuming isPathInside was a local helper, if it's broadly used, it should be in commons or imported
// For now, this might break if isPathInside is not accessible.
// Let's assume for now it was only for the old glob. If not, this needs to be addressed.
const relation = path.relative(realB, realA); // Corrected order for typical usage
return Boolean(
relation &&
!relation.startsWith('..') &&
@ -36,12 +40,6 @@ const isPathInside = (childPath: string, parentPath: string): boolean => {
);
};
export const isPathOutsideSafe = (pathA: string, pathB: string): boolean => {
const realA = fs.realpathSync(pathA);
const realB = fs.realpathSync(pathB);
return !isPathInside(realA, realB);
};
export const base64 = (filePath: string): string | null => {
try {
const fileBuffer = fs.readFileSync(filePath);
@ -64,100 +62,16 @@ export const images = (files: string[]): ChatCompletionContentPartImage[] => {
}))
}
/**
* Check if a string is a web URL
*/
export const isWebUrl = (str: string): boolean => {
return /^https?:\/\//.test(str);
}
export const glob = (
projectPath: string,
include: string[] = [],
exclude: string[] = []
): { files: string[], webUrls: Set<string> } => {
if (!exists(projectPath)) {
dir(projectPath)
return { files: [], webUrls: new Set<string>() }
}
const filters = new Set<string>()
const absolutePaths = new Set<string>()
const webUrls = new Set<string>()
const ignorePatterns = new Set<string>(EXCLUDE_GLOB)
include.forEach(pattern => {
// Check if the pattern is a web URL
if (isWebUrl(pattern)) {
webUrls.add(pattern)
return
}
if (path.isAbsolute(pattern)) {
if (isPathInside(pattern, projectPath)) {
filters.add(pattern)
} else {
absolutePaths.add(pattern)
}
} else {
filters.add(pattern)
}
})
// Process exclude patterns
exclude.forEach(pattern => {
if (isWebUrl(pattern)) {
// Web URLs are typically not "excluded" in a file glob sense,
// but if there's a use case, it needs clarification.
// For now, we'll assume web URLs in exclude are ignored for globbing.
return;
}
// Add all exclude patterns (absolute or relative) to ignorePatterns
// globSync handles absolute paths correctly in its `ignore` option when `cwd` is set.
ignorePatterns.add(pattern);
});
const globFiles = globSync([...filters], {
cwd: projectPath,
absolute: false,
ignore: [...ignorePatterns]
})
const allFiles = Array.from(new Set([
...globFiles.map(file => path.join(projectPath, file)),
...Array.from(absolutePaths)
]))
let files = allFiles.filter((f) =>
Object.keys(default_filters).every((key) => default_filters[key](f))
)
return { files, webUrls }
}
// glob function definition removed from here
export async function get(
projectPath: string,
include: string[] = [],
options: IKBotTask
): Promise<Array<IHandlerResult>> {
const { files: initialFiles, webUrls } = glob(projectPath, include, options.exclude)
const { files, webUrls } = glob(projectPath, include, options.exclude, options)
const filesToProcess = new Set<string>(initialFiles.map(f => forward_slash(f)));
// Only add corresponding .cpp if options.globExtension is 'match-cpp'
if (options.globExtension === 'match-cpp') {
for (const initialFile of initialFiles) {
const itemPathInfo = path.parse(initialFile);
if (itemPathInfo.ext === '.h') {
const cppFilePath = path.join(itemPathInfo.dir, `${itemPathInfo.name}.cpp`);
if (exists(cppFilePath) && isFile(cppFilePath)) {
filesToProcess.add(forward_slash(cppFilePath));
}
}
}
}
// Process file contents from the final list of files
const fileResults = Array.from(filesToProcess).map((fullPath) => {
const fileResults = files.map((fullPath) => {
try {
const relativePath = forward_slash(path.relative(projectPath, fullPath))
if (isFile(fullPath) && exists(fullPath)) {
@ -175,21 +89,21 @@ export async function get(
}
})
// Process web URLs
// Reinstantiate web URL processing
const webUrlPromises = Array.from(webUrls).map(async (url: string) => {
try {
return await handleWebUrl(url)
return await handleWebUrl(url);
} catch (error) {
logger.error(`Error processing web URL ${url}:`, error)
return null
logger.error(`Error processing web URL ${url}:`, error);
return null;
}
})
});
const webResults = await Promise.all(webUrlPromises)
const webResults = await Promise.all(webUrlPromises);
// Combine and filter results
const results = [...fileResults, ...webResults].filter((r) => r !== null)
return results
const results = [...fileResults, ...webResults].filter((r) => r !== null);
return results;
}
export async function vectorize(file: string, options: IKBotTask): Promise<string> {

View File

@ -3,6 +3,47 @@ import { pathInfoEx } from '@polymech/commons'
import { DEFAULT_ROOTS, DEFAULT_VARS } from '@polymech/commons'
import { IKBotTask } from '@polymech/ai-tools'
export const generateSingleFileVariables = (filePath: string, projectPath: string): Record<string, string> => {
const fileSpecificVariables: Record<string, string> = {};
const srcParts = path.parse(filePath);
fileSpecificVariables.SRC_NAME = srcParts.name;
fileSpecificVariables.SRC_DIR = srcParts.dir;
fileSpecificVariables.SRC_EXT = srcParts.ext.startsWith('.') ? srcParts.ext.substring(1) : srcParts.ext; // Remove leading dot from ext
fileSpecificVariables.SRC_BASENAME = srcParts.base;
fileSpecificVariables.SRC_PATH = filePath;
// Calculate SRC_REL relative to projectPath for the file's directory
if (projectPath && path.isAbsolute(srcParts.dir) && path.isAbsolute(projectPath)) {
fileSpecificVariables.SRC_REL = path.relative(projectPath, srcParts.dir);
} else {
// If paths are not suitable for relative calculation (e.g., one is not absolute)
// or projectPath is not provided, SRC_REL might be less meaningful or empty.
fileSpecificVariables.SRC_REL = '';
}
const addIndexedParts = (baseKey: string, fullName: string, delimiter: string) => {
const parts = fullName.split(delimiter);
if (parts.length > 1) {
parts.forEach((part, i) => {
fileSpecificVariables[`${baseKey}${i}`] = part;
});
}
};
addIndexedParts('SRC_NAME-', srcParts.name, '-');
addIndexedParts('SRC_NAME.', srcParts.name, '.');
addIndexedParts('SRC_NAME_', srcParts.name, '_');
// Convert all keys to uppercase
const uppercasedVariables: Record<string, string> = {};
for (const key in fileSpecificVariables) {
uppercasedVariables[key.toUpperCase()] = fileSpecificVariables[key];
}
return uppercasedVariables;
};
export const variables = (options: IKBotTask) => {
const { model, router,baseURL } = options
let ret = {

View File

@ -46,9 +46,9 @@ export type E_AppendModeType = z.infer<typeof E_AppendMode>
export const E_WrapMode = z.enum(['meta', 'none'])
export type E_WrapModeType = z.infer<typeof E_WrapMode>
// Define the new enum for glob extensions
export const E_GlobExtension = z.enum(['match-cpp']);
export type E_GlobExtensionType = z.infer<typeof E_GlobExtension>;
// Define the new enum for glob extensions (presets)
export const E_GlobExtension = z.enum(['match-cpp']) // Add more presets here later if needed
export type E_GlobExtensionType = z.infer<typeof E_GlobExtension>
export type OptionsSchemaMeta = Record<string, unknown>
@ -141,9 +141,13 @@ export const OptionsSchema = (opts?: any): any => {
)
.add(
'globExtension',
E_GlobExtension
z.union([E_GlobExtension, z.string()]) // Allow preset enum or custom string
.optional()
.describe(`Specify a glob extension behavior. Available: ${E_GlobExtension.options.join(', ')}. e.g., "match-cpp" to automatically include corresponding .cpp files for .h files.`)
.describe(
'Specify a glob extension behavior. Available presets: ' + E_GlobExtension.options.join(', ') +
'. Also accepts a custom glob pattern with variables like ${SRC_DIR}, ${SRC_NAME}, ${SRC_EXT}. ' +
'E.g., \"match-cpp\" or \"${SRC_DIR}/${SRC_NAME}*.cpp\"'
)
)
.add(
'api_key',

View File

@ -23,8 +23,8 @@ export interface IKBotOptions {
include?: string[] | undefined;
/** Comma separated glob patterns or paths, eg --exclude=src/*.tsx,src/*.ts --exclude=package.json */
exclude?: string[] | undefined;
/** Specify a glob extension behavior. e.g., "match-cpp" to automatically include corresponding .cpp files for .h files. */
globExtension?: string | undefined;
/** Specify a glob extension behavior. Available presets: match-cpp. Also accepts a custom glob pattern with variables like ${SRC_DIR}, ${SRC_NAME}, ${SRC_EXT}. E.g., "match-cpp" or "${SRC_DIR}/${SRC_NAME}*.cpp" */
globExtension?: (("match-cpp") | string) | undefined;
/** Explicit API key to use */
api_key?: string | undefined;
/** AI model to use for processing. Available models:

View File

@ -0,0 +1,116 @@
#include "PHApp.h"
#ifdef ENABLE_MODBUS_TCP
#include <modbus/ModbusTCP.h> // For ModbusManager class
#include <ArduinoLog.h> // For logging
#include <enums.h> // For error codes like E_INVALID_PARAMETER
#include <ModbusServerTCPasync.h> // Include for ModbusServerTCPasync
#include <enums.h>
#include <modbus/ModbusTypes.h>
#include "config-modbus.h" // Include centralized addresses (defines MODBUS_PORT)
extern ModbusServerTCPasync mb;
void PHApp::mb_tcp_register(ModbusTCP *manager) const
{
if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS))
return;
ModbusBlockView *blocksView = mb_tcp_blocks();
Component *thiz = const_cast<PHApp *>(this);
for (int i = 0; i < blocksView->count; ++i)
{
MB_Registers info = blocksView->data[i];
info.componentId = this->id;
manager->registerModbus(thiz, info);
}
}
ModbusBlockView *PHApp::mb_tcp_blocks() const
{
static const MB_Registers kBlocks[] = {
{MB_ADDR_SYSTEM_ERROR, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast<ushort>(this->id), 0, "PHApp: System Error","PHApp"},
{MB_ADDR_APP_STATE, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast<ushort>(this->id), 0, "PHApp: App State","PHApp"},
{MB_ADDR_SUB_STATE_0, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast<ushort>(this->id), 0, "PHApp: Sub State 0","PHApp"},
{MB_ADDR_SUB_STATE_1, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, static_cast<ushort>(this->id), 0, "PHApp: Sub State 1","PHApp"},
{MB_ADDR_RESET_CONTROLLER, 1, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast<ushort>(this->id), 0, "PHApp: Reset Controller","PHApp"},
{MB_ADDR_ECHO_TEST, 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, static_cast<ushort>(this->id), 0, "PHApp: Echo Test","PHApp"},
};
static ModbusBlockView blockView = {kBlocks, int(sizeof(kBlocks) / sizeof(kBlocks[0]))};
return &blockView;
}
/**
* @brief Handles Modbus read requests for PHApp's specific registers.
*/
short PHApp::mb_tcp_read(short address)
{
switch (address)
{
case MB_ADDR_SYSTEM_ERROR:
return (short)getLastError();
case MB_ADDR_APP_STATE:
return (short)_state;
case MB_ADDR_ECHO_TEST:
return (short)88;
default:
return 0;
}
}
short PHApp::mb_tcp_write(MB_Registers *reg, short networkValue)
{
return mb_tcp_write(reg->startAddress, networkValue);
}
/**
* @brief Handles Modbus write requests for PHApp's specific registers.
*/
short PHApp::mb_tcp_write(short address, short value)
{
switch (address)
{
case MB_ADDR_RESET_CONTROLLER:
reset(0, 0);
return E_OK;
default:
return E_INVALID_PARAMETER;
}
}
short PHApp::loopModbus()
{
return E_OK;
}
short PHApp::getConnectedClients() const
{
if (!modbusManager) return 0;
return modbusManager->getConnectedClients();
}
short PHApp::setupModbus()
{
Log.infoln("Setting up Modbus TCP...");
modbusManager = new ModbusTCP(this, &mb);
if (!modbusManager)
{
Log.fatalln("Failed to create ModbusTCP!");
return E_INVALID_PARAMETER;
}
components.push_back(modbusManager); // Add manager to component list for setup/loop calls
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
// --- Register Components with Modbus Manager ---
Log.infoln("PHApp::setupModbus - Registering components with ModbusTCP...");
mb_tcp_register(modbusManager);
Log.infoln("--- End Modbus Mappings DUMP --- ");
if (modbusManager->enabled())
{
mb.start(MODBUS_PORT, 10, 2000);
}
else
{
Log.warningln("PHApp::setupModbus - ModbusTCP not available or disabled. Skipping Modbus server start.");
}
return E_OK;
}
#endif // ENABLE_MODBUS_TCP

View File

@ -0,0 +1,401 @@
#include "PHApp.h"
#include "config.h"
#include <ArduinoLog.h>
#include <enums.h>
#include <profiles/TemperatureProfile.h>
#ifdef ENABLE_PROFILE_TEMPERATURE
#include <LittleFS.h>
#include <ArduinoJson.h>
#endif
short PHApp::load(short val0, short val1)
{
Log.infoln(F("PHApp::load() - Loading application data..."));
Log.infoln(F("PHApp::load() - Attempting to load temperature profiles..."));
if (!LittleFS.begin(true))
{ // Ensure LittleFS is mounted (true formats if necessary)
Log.errorln(F("PHApp::load() - Failed to mount LittleFS. Cannot load profiles."));
return E_INVALID_PARAMETER; // Use invalid parameter as fallback
}
const char *filename = "/profiles/defaults.json"; // Path in LittleFS
File file = LittleFS.open(filename, "r");
if (!file)
{
Log.errorln(F("PHApp::load() - Failed to open profile file: %s"), filename);
LittleFS.end(); // Close LittleFS
return E_NOT_FOUND; // Use standard not found
}
// Increased size slightly for safety, adjust if needed
// DynamicJsonDocument doc(JSON_ARRAY_SIZE(PROFILE_TEMPERATURE_COUNT) + PROFILE_TEMPERATURE_COUNT * JSON_OBJECT_SIZE(5 + MAX_TEMP_CONTROL_POINTS));
// Replace DynamicJsonDocument with JsonDocument, letting it handle allocation.
JsonDocument doc;
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, file);
file.close(); // Close the file ASAP
LittleFS.end(); // Close LittleFS
if (error)
{
Log.errorln(F("PHApp::load() - Failed to parse profile JSON: %s"), error.c_str());
return E_INVALID_PARAMETER; // Use invalid parameter
}
// Check if the root is a JSON array
if (!doc.is<JsonArray>())
{
Log.errorln(F("PHApp::load() - Profile JSON root is not an array."));
return E_INVALID_PARAMETER; // Use invalid parameter
}
JsonArray profilesArray = doc.as<JsonArray>();
Log.infoln(F("PHApp::load() - Found %d profiles in JSON file."), profilesArray.size());
uint8_t profileIndex = 0;
for (JsonObject profileJson : profilesArray)
{
if (profileIndex >= PROFILE_TEMPERATURE_COUNT)
{
Log.warningln(F("PHApp::load() - Too many profiles in JSON (%d), only loading the first %d."), profilesArray.size(), PROFILE_TEMPERATURE_COUNT);
break;
}
if (!tempProfiles[profileIndex])
{
Log.errorln(F("PHApp::load() - TemperatureProfile slot %d is not initialized. Skipping JSON profile."), profileIndex);
// Don't increment profileIndex here, try to load next JSON into same slot if possible?
// Or increment profileIndex to align JSON index with slot index? Let's align.
profileIndex++;
continue;
}
// Assuming TemperatureProfile (or its base PlotBase) has a public method
// like loadFromJson that takes the JsonObject and calls the protected virtual load.
// We also assume it returns bool or short (E_OK for success).
Log.infoln(F("PHApp::load() - Loading JSON data into TemperatureProfile slot %d..."), profileIndex);
// Now call the protected load() directly, as PHApp is a friend
if (tempProfiles[profileIndex]->load(profileJson))
{ // returns bool
const char *name = profileJson["name"] | "Unnamed"; // Get name for logging
Log.infoln(F("PHApp::load() - Successfully loaded profile '%s' into slot %d."), name, profileIndex);
}
else
{
Log.errorln(F("PHApp::load() - Failed to load profile data into slot %d."), profileIndex);
// Decide if we should return an error or just continue loading others
// return E_INVALID_PARAMETER; // Option: Stop loading on first failure
}
Log.infoln(F("PHApp::load() - Loaded %d profiles from JSON into %d available slots and %d target registers."), profileIndex, PROFILE_TEMPERATURE_COUNT, tempProfiles[profileIndex]->getTargetRegisters().size());
profileIndex++; // Move to the next TemperatureProfile slot
}
// Handle case where JSON has fewer profiles than allocated slots
if (profileIndex < profilesArray.size())
{
Log.warningln(F("PHApp::load() - Processed %d JSON profiles but only %d slots were available/initialized."), profilesArray.size(), profileIndex);
}
else if (profileIndex < PROFILE_TEMPERATURE_COUNT)
{
Log.infoln(F("PHApp::load() - Loaded %d profiles from JSON into %d available slots."), profileIndex, PROFILE_TEMPERATURE_COUNT);
}
// --- Load Signal Plot Profiles ---
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
Log.infoln(F("PHApp::load() - Attempting to load signal plot profiles..."));
if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted, or re-mount if it was closed
Log.errorln(F("PHApp::load() - Failed to mount LittleFS for signal plots. Cannot load signal profiles."));
// Decide on return strategy: return E_INVALID_PARAMETER or continue?
// For now, let's log and continue, as temp profiles might have loaded.
} else {
const char *signalPlotFilename = "/profiles/signal_plots.json";
File signalPlotFile = LittleFS.open(signalPlotFilename, "r");
if (!signalPlotFile) {
Log.errorln(F("PHApp::load() - Failed to open signal plot profile file: %s. This might be normal if it doesn't exist yet."), signalPlotFilename);
} else {
JsonDocument signalPlotDoc; // Use a new document for signal plots
DeserializationError spError = deserializeJson(signalPlotDoc, signalPlotFile);
signalPlotFile.close();
if (spError) {
Log.errorln(F("PHApp::load() - Failed to parse signal plot JSON: %s"), spError.c_str());
} else if (!signalPlotDoc.is<JsonArray>()) {
Log.errorln(F("PHApp::load() - Signal plot JSON root is not an array."));
} else {
JsonArray spArray = signalPlotDoc.as<JsonArray>();
Log.infoln(F("PHApp::load() - Found %d signal plot profiles in JSON file."), spArray.size());
uint8_t spIndex = 0;
for (JsonObject spJson : spArray) {
if (spIndex >= PROFILE_SIGNAL_PLOT_COUNT) {
Log.warningln(F("PHApp::load() - Too many signal plot profiles in JSON (%d), only loading the first %d."), spArray.size(), PROFILE_SIGNAL_PLOT_COUNT);
break;
}
if (signalPlots[spIndex]) {
Log.infoln(F("PHApp::load() - Loading JSON data into SignalPlot slot %d..."), spIndex);
if (signalPlots[spIndex]->load(spJson)) {
const char *spName = spJson["name"] | "Unnamed Signal Plot";
Log.infoln(F("PHApp::load() - Successfully loaded signal plot profile '%s' into slot %d."), spName, spIndex);
} else {
Log.errorln(F("PHApp::load() - Failed to load signal plot profile data into slot %d."), spIndex);
}
} else {
Log.errorln(F("PHApp::load() - SignalPlot slot %d is not initialized. Skipping JSON profile."), spIndex);
}
spIndex++;
}
Log.infoln(F("PHApp::load() - Loaded %d signal plot profiles from JSON into %d available slots."), spIndex, PROFILE_SIGNAL_PLOT_COUNT);
}
}
LittleFS.end(); // Close LittleFS after signal plots are done
}
#endif // ENABLE_PROFILE_SIGNAL_PLOT
return E_OK;
}
#ifdef ENABLE_PROFILE_TEMPERATURE
/**
* @brief Handles GET requests to /api/v1/profiles
* Returns a list of available temperature profile slots.
*/
void PHApp::getProfilesHandler(AsyncWebServerRequest *request)
{
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
JsonArray profilesArray = doc["profiles"].to<JsonArray>();
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
TemperatureProfile *profile = this->tempProfiles[i];
if (profile)
{
Log.verboseln(" Processing Profile Slot %d: %s", i, profile->name.c_str());
JsonObject profileObj = profilesArray.add<JsonObject>();
profileObj["slot"] = i;
profileObj["duration"] = profile->getDuration();
profileObj["status"] = (int)profile->getCurrentStatus();
profileObj["currentTemp"] = profile->getTemperature(profile->getElapsedMs());
profileObj["name"] = profile->name;
profileObj["max"] = profile->max;
profileObj["enabled"] = profile->enabled();
profileObj["elapsed"] = profile->getElapsedMs();
profileObj["remaining"] = profile->getRemainingTime();
profileObj["signalPlot"] = profile->getSignalPlotSlotId();
JsonArray pointsArray = profileObj["controlPoints"].to<JsonArray>();
const TempControlPoint *points = profile->getTempControlPoints();
uint8_t numPoints = profile->getNumTempControlPoints();
for (uint8_t j = 0; j < numPoints; ++j)
{
JsonObject pointObj = pointsArray.add<JsonObject>();
pointObj["x"] = points[j].x;
pointObj["y"] = points[j].y;
}
JsonArray targetRegistersArray = profileObj["targetRegisters"].to<JsonArray>();
const std::vector<uint16_t> &targets = profile->getTargetRegisters();
for (uint16_t targetReg : targets)
{
targetRegistersArray.add(targetReg);
}
}
else
{
Log.warningln(" Profile slot %d is null", i);
}
}
serializeJson(doc, *response);
request->send(response);
}
/**
* @brief Handles POST requests to /api/v1/profiles/{slot}
* Updates the specified temperature profile using the provided JSON data.
*
* @param request The incoming web request.
* @param json The parsed JSON body from the request.
* @param slot The profile slot number extracted from the URL.
*/
void PHApp::setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot)
{
if (slot < 0 || slot >= PROFILE_TEMPERATURE_COUNT)
{
Log.warningln("REST: setProfileHandler - Invalid slot number %d provided.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number\"}");
return;
}
// Check if the profile object exists for this slot
TemperatureProfile *targetProfile = this->tempProfiles[slot];
if (!targetProfile)
{
Log.warningln("REST: setProfileHandler - No profile found for slot %d.", slot);
request->send(404, "application/json", "{\"success\":false,\"error\":\"Profile slot not found or not initialized\"}");
return;
}
// Check if the JSON is an object
if (!json.is<JsonObject>())
{
Log.warningln("REST: setProfileHandler - Invalid JSON payload (not an object) for slot %d.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: must be an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
// Attempt to load the configuration into the profile object
bool success = targetProfile->load(jsonObj);
if (success)
{
Log.infoln("REST: Profile slot %d updated successfully.", slot);
// Attempt to save all profiles back to JSON
if (saveProfilesToJson()) {
Log.infoln("REST: All profiles saved to JSON successfully after update.");
request->send(200, "application/json", "{\"success\":true, \"message\":\"Profile updated and saved.\"}");
} else {
Log.errorln("REST: Profile slot %d updated, but failed to save all profiles to JSON.", slot);
request->send(500, "application/json", "{\"success\":true, \"message\":\"Profile updated but failed to save configuration.\"}"); // Send 200 as profile was updated, but indicate save error
}
}
else
{
Log.errorln("REST: Failed to update profile slot %d from JSON.", slot);
// Provide a more specific error if `load` can indicate the reason
request->send(400, "application/json", "{\"success\":false,\"error\":\"Failed to load profile data. Check format and values.\"}");
}
}
bool PHApp::saveProfilesToJson() {
Log.infoln(F("PHApp::saveProfilesToJson() - Saving all temperature profiles to JSON..."));
if (!LittleFS.begin(true)) {
Log.errorln(F("PHApp::saveProfilesToJson() - Failed to mount LittleFS. Cannot save profiles."));
return false;
}
const char *filename = "/profiles/defaults.json"; // Path in LittleFS
JsonDocument doc; // Use a single JsonDocument for the array
JsonArray profilesArray = doc.to<JsonArray>();
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) {
TemperatureProfile *profile = this->tempProfiles[i];
if (profile) {
JsonObject profileJson = profilesArray.add<JsonObject>();
profileJson["type"] = "temperature"; // Assuming this is fixed for now
profileJson["slot"] = i;
profileJson["name"] = profile->name;
profileJson["duration"] = profile->getDuration(); // Duration in ms
profileJson["max"] = profile->max;
profileJson["signalPlot"] = profile->getSignalPlotSlotId();
// controlPoints
JsonArray pointsArray = profileJson.createNestedArray("controlPoints");
const TempControlPoint *points = profile->getTempControlPoints();
uint8_t numPoints = profile->getNumTempControlPoints();
for (uint8_t j = 0; j < numPoints; ++j) {
JsonObject pointObj = pointsArray.add<JsonObject>();
pointObj["x"] = points[j].x;
pointObj["y"] = points[j].y;
}
// targetRegisters
JsonArray targetRegistersArray = profileJson.createNestedArray("targetRegisters");
const std::vector<uint16_t> &targets = profile->getTargetRegisters();
for (uint16_t targetReg : targets) {
targetRegistersArray.add(targetReg);
}
} else {
Log.warningln(F("PHApp::saveProfilesToJson() - Profile slot %d is null, skipping."), i);
}
}
File file = LittleFS.open(filename, "w"); // Open for writing, creates if not exists, truncates if exists
if (!file) {
Log.errorln(F("PHApp::saveProfilesToJson() - Failed to open profile file for writing: %s"), filename);
LittleFS.end();
return false;
}
size_t bytesWritten = serializeJson(doc, file);
file.close();
LittleFS.end();
if (bytesWritten > 0) {
Log.infoln(F("PHApp::saveProfilesToJson() - Successfully wrote %d bytes to %s"), bytesWritten, filename);
return true;
} else {
Log.errorln(F("PHApp::saveProfilesToJson() - Failed to serialize JSON or write to file: %s"), filename);
return false;
}
}
#endif // ENABLE_PROFILE_TEMPERATURE
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
bool PHApp::saveSignalPlotsToJson() {
Log.infoln(F("PHApp::saveSignalPlotsToJson() - Saving all SignalPlot profiles to JSON..."));
if (!LittleFS.begin(true)) {
Log.errorln(F("PHApp::saveSignalPlotsToJson() - Failed to mount LittleFS. Cannot save profiles."));
return false;
}
const char *filename = "/profiles/signal_plots.json"; // Corrected filename
JsonDocument doc;
JsonArray profilesArray = doc.to<JsonArray>();
for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; ++i) {
SignalPlot *profile = this->signalPlots[i];
if (profile) {
JsonObject profileJson = profilesArray.add<JsonObject>();
profileJson["type"] = "signal";
profileJson["slot"] = i;
profileJson["name"] = profile->name;
profileJson["duration"] = profile->getDuration();
JsonArray pointsArray = profileJson.createNestedArray("controlPoints");
const S_SignalControlPoint *points = profile->getControlPoints();
uint8_t numPoints = profile->getNumControlPoints();
for (uint8_t j = 0; j < numPoints; ++j) {
JsonObject pointObj = pointsArray.add<JsonObject>();
pointObj["id"] = points[j].id;
pointObj["time"] = points[j].time;
pointObj["name"] = points[j].name;
pointObj["description"] = points[j].description;
pointObj["state"] = (int16_t)points[j].state;
pointObj["type"] = (int16_t)points[j].type;
pointObj["arg_0"] = points[j].arg_0;
pointObj["arg_1"] = points[j].arg_1;
pointObj["arg_2"] = points[j].arg_2;
}
} else {
Log.warningln(F("PHApp::saveSignalPlotsToJson() - SignalPlot slot %d is null, skipping."), i);
}
}
File file = LittleFS.open(filename, "w");
if (!file) {
Log.errorln(F("PHApp::saveSignalPlotsToJson() - Failed to open profile file for writing: %s"), filename);
LittleFS.end();
return false;
}
size_t bytesWritten = serializeJson(doc, file);
file.close();
LittleFS.end();
if (bytesWritten > 0) {
Log.infoln(F("PHApp::saveSignalPlotsToJson() - Successfully wrote %d bytes to %s"), bytesWritten, filename);
return true;
} else {
Log.errorln(F("PHApp::saveSignalPlotsToJson() - Failed to serialize JSON or write to file: %s"), filename);
return false;
}
}
#endif // ENABLE_PROFILE_SIGNAL_PLOT

View File

@ -0,0 +1,824 @@
#include <Arduino.h>
#include <macros.h>
#include <Component.h>
#include <enums.h>
#include <Logger.h>
#include "./PHApp.h"
#include "./config.h"
#include "./config_adv.h"
#include "./config-modbus.h"
#include "./features.h"
#include <components/OmronE5Types.h>
#include <components/OmronE5.h>
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
#include <profiles/SignalPlot.h>
#endif
#ifdef ENABLE_MODBUS_TCP
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
#endif
#define MB_R_APP_STATE_REG 9
#define MB_R_SYSTEM_CMD_PRINT_RESET 1
#define MB_R_SYSTEM_CMD_PRINT_REGS 2
#define MB_R_SYSTEM_CMD_PRINT_MEMORY 5
#define MB_R_SYSTEM_CMD_PRINT_VFD 6
#ifdef ENABLE_PROFILER
uint32_t PHApp::initialFreeHeap = 0;
uint64_t PHApp::initialCpuTicks = 0;
#endif
#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_LEVEL_NONE
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Network Servers
//
#if defined(ENABLE_WEBSERVER)
WiFiServer server(80);
#endif
#ifdef ENABLE_MODBUS_TCP
ModbusServerTCPasync mb;
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Omron E5
//
#undef ENABLE_TRUTH_COLLECTOR
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// Factory : Instances
//
#define ADD_RELAY(relayNum, relayPin, relayKey, relayAddr) \
relay_##relayNum = new Relay(this, relayPin, relayKey, relayAddr); \
components.push_back(relay_##relayNum);
#define ADD_POT(potNum, potPin, potKey, potAddr) \
pot_##potNum = new POT(this, potPin, potKey, potAddr); \
components.push_back(pot_##potNum);
#define ADD_POS3ANALOG(posNum, switchPin1, switchPin2, switchKey, switchAddr) \
pos3Analog_##posNum = new Pos3Analog(this, switchPin1, switchPin2, switchKey, switchAddr); \
components.push_back(pos3Analog_##posNum);
#ifdef ENABLE_PID
#define ADD_PID(pidNum, nameStr, doPin, csPin, clkPin, outPin, key) \
pidController_##pidNum = new PIDController(key, nameStr, doPin, csPin, clkPin, outPin); \
components.push_back(pidController_##pidNum);
#endif
void PHApp::printRegisters()
{
Log.verboseln(F("--- Entering PHApp::printRegisters ---"));
#if ENABLED(HAS_MODBUS_REGISTER_DESCRIPTIONS)
Log.setShowLevel(false);
Serial.print("| Name | ID | Address | RW | Function Code | Number Addresses |Register Description| \n");
Serial.print("|------|----------|----|----|----|----|-------|\n");
short size = components.size();
Log.verboseln(F("PHApp::printRegisters - Processing %d components..."), size);
for (int i = 0; i < size; i++)
{
Component *component = components[i];
if (!component)
{
Log.errorln(F("PHApp::printRegisters - Found NULL component at index %d"), i);
continue;
}
Log.verboseln(F("PHApp::printRegisters - Component %d: ID=%d, Name=%s"), i, component->id, component->name.c_str());
// if (!(component->nFlags & 1 << OBJECT_NET_CAPS::E_NCAPS_MODBUS)) // <-- Modbus flag check might be different now
// {
// continue;
// }
// Log.verbose("| %s | %d | %d | %s | %d | %d | %s |\n", // <-- Calls to removed ModbusGateway methods
// component->name.c_str(),
// component->id,
// component->getAddress(),
// component->getRegisterMode(),
// component->getFunctionCode(),
// component->getNumberAddresses(),
// component->getRegisterDescription().c_str());
Log.verbose("| %s | %d | - | - | - | - | - |\n", // <-- Simplified output
component->name.c_str(),
component->id);
}
Log.setShowLevel(true);
#endif
Log.verboseln(F("--- Exiting PHApp::printRegisters ---"));
}
short PHApp::reset(short val0, short val1)
{
_state = APP_STATE::RESET;
_error = E_OK;
#if defined(ESP32) || defined(ESP8266) // Use ESP.restart() for ESP32 and ESP8266
ESP.restart();
#else
return E_NOT_IMPLEMENTED;
#endif
return E_OK;
}
short PHApp::list(short val0, short val1)
{
uchar s = components.size();
for (uchar i = 0; i < s; i++)
{
Component *component = components[i];
if (component)
{
Log.verboseln("PHApp::list - %d | %s (ID: %d)", i, component->name.c_str(), component->id);
}
else
{
Log.warningln("PHApp::list - NULL component at index %d", i);
}
}
return E_OK;
}
short PHApp::setup()
{
Log.verbose("--------------------PHApp::setup() Begin.-------------------");
_state = APP_STATE::RESET;
_error = E_OK;
#ifdef ENABLE_PROFILER
if (initialFreeHeap == 0 && initialCpuTicks == 0)
{
initialFreeHeap = ESP.getFreeHeap();
initialCpuTicks = esp_cpu_get_ccount();
}
#endif
#ifndef DISABLE_SERIAL_LOGGING
// Serial Setup
Serial.begin(SERIAL_BAUD_RATE);
while (!Serial && !Serial.available())
{
}
// Log Setup
Log.begin(LOG_LEVEL, &Serial);
Log.setShowLevel(true);
#else
// Log Setup (without Serial)
Log.begin(LOG_LEVEL_WARNING, nullptr); // Or LOG_LEVEL_NONE, or some other target if available
Log.setShowLevel(false);
#endif
// Components
bridge = new Bridge(this);
#ifndef DISABLE_SERIAL_LOGGING
com_serial = new SerialMessage(Serial, bridge);
components.push_back(com_serial);
#endif
components.push_back(bridge);
// Network
short networkSetupResult = setupNetwork();
if (networkSetupResult != E_OK)
{
Log.errorln("Network setup failed with error code: %d", networkSetupResult);
}
// Components
#ifdef ENABLE_RELAYS
#ifdef AUX_RELAY_0
ADD_RELAY(0, AUX_RELAY_0, COMPONENT_KEY_RELAY_0, MB_ADDR_AUX_5);
#endif
#ifdef AUX_RELAY_1
ADD_RELAY(1, AUX_RELAY_1, COMPONENT_KEY_RELAY_1, MB_ADDR_AUX_6);
#endif
#endif
#ifdef MB_ANALOG_0
ADD_POT(0, MB_ANALOG_0, COMPONENT_KEY_ANALOG_0, MB_ADDR_AUX_2);
#endif
#ifdef MB_ANALOG_1
ADD_POT(1, MB_ANALOG_1, COMPONENT_KEY_ANALOG_1, MB_ADDR_AUX_3);
#endif
#if (defined(AUX_ANALOG_3POS_SWITCH_0) && (defined(AUX_ANALOG_3POS_SWITCH_1)))
ADD_POS3ANALOG(0, AUX_ANALOG_3POS_SWITCH_0, AUX_ANALOG_3POS_SWITCH_1, COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_0, MB_ADDR_AUX_3);
#endif
#if (defined(MB_ANALOG_3POS_SWITCH_2) && (defined(MB_ANALOG_3POS_SWITCH_3)))
// ADD_POS3ANALOG(1, MB_ANALOG_3POS_SWITCH_2, MB_ANALOG_3POS_SWITCH_3, COMPONENT_KEY_MB_ANALOG_3POS_SWITCH_1, MB_R_SWITCH_1); // <-- Temporarily disable
#endif
#ifdef MB_GPIO_MB_MAP_7
// --- Define configuration for the MB_GPIO group ---
std::vector<GPIO_PinConfig> gpioConfigs;
gpioConfigs.reserve(2); // Reserve space for 2 elements
gpioConfigs.push_back(
GPIO_PinConfig(
E_GPIO_7, // pinNumber: The physical pin to manage
E_GPIO_TYPE_ANALOG_INPUT, // pinType: Treat as analog input
300, // startAddress: Modbus register address
E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register
MB_ACCESS_READ_ONLY, // access: Allow Modbus read only
1000, // opIntervalMs: Update interval in milliseconds
"GPIO_6", // name: Custom name for this pin
"GPIO_Group" // group: Group name for this pin
));
gpioConfigs.push_back(
GPIO_PinConfig(
E_GPIO_15, // pinNumber: The physical pin to manage
E_GPIO_TYPE_ANALOG_INPUT, // pinType: Treat as analog input
301, // startAddress: Modbus register address
E_FN_CODE::FN_READ_HOLD_REGISTER, // type: Map to a Holding Register
MB_ACCESS_READ_ONLY, // access: Allow Modbus read only
1000, // opIntervalMs: Update interval in milliseconds
"GPIO_15", // name: Custom name for this pin
"GPIO_Group" // group: Group name for this pin
));
const short gpioGroupId = COMPONENT_KEY_GPIO_MAP; // Using defined key
gpio_0 = new MB_GPIO(this, gpioGroupId, gpioConfigs);
components.push_back(gpio_0);
#endif
#ifdef PIN_ANALOG_LEVEL_SWITCH_0
analogLevelSwitch_0 = new AnalogLevelSwitch(
this, // owner
PIN_ANALOG_LEVEL_SWITCH_0, // analogPin
ALS_0_NUM_LEVELS, // numLevels
ALS_0_ADC_STEP, // levelStep
ALS_0_ADC_OFFSET, // adcValueOffset
ID_ANALOG_LEVEL_SWITCH_0, // id
ALS_0_MB_ADDR // modbusAddress
);
if (analogLevelSwitch_0)
{
components.push_back(analogLevelSwitch_0);
Log.infoln(F("AnalogLevelSwitch_0 initialized. Pin:%d, ID:%d, Levels:%d, Step:%d, Offset:%d, Smooth:%d(Fixed), Debounce:%d(Fixed), MB:%d"),
PIN_ANALOG_LEVEL_SWITCH_0, ID_ANALOG_LEVEL_SWITCH_0, ALS_0_NUM_LEVELS,
ALS_0_ADC_STEP, ALS_0_ADC_OFFSET,
ALS_SMOOTHING_SIZE,
ALS_DEBOUNCE_COUNT,
ALS_0_MB_ADDR);
}
else
{
Log.errorln(F("AnalogLevelSwitch_0 initialization failed."));
}
#endif
#ifdef ENABLE_STATUS
statusLight_0 = new StatusLight(this,
STATUS_WARNING_PIN,
COMPONENT_KEY_FEEDBACK_0,
MB_MONITORING_STATUS_FEEDBACK_0); // Keep original address for now, add to config-modbus.h later if needed
components.push_back(statusLight_0);
statusLight_1 = new StatusLight(this,
STATUS_ERROR_PIN,
COMPONENT_KEY_FEEDBACK_1,
MB_MONITORING_STATUS_FEEDBACK_1); // Keep original address for now
components.push_back(statusLight_1);
#else
statusLight_0 = NULL;
statusLight_1 = NULL;
#endif
Log.infoln("PHApp::setup - Base App::setup() called.");
#ifdef PIN_LED_FEEDBACK_0
ledFeedback_0 = new LEDFeedback(
this, // owner
PIN_LED_FEEDBACK_0, // pin
LED_PIXEL_COUNT_0, // pixelCount
ID_LED_FEEDBACK_0, // id
LED_FEEDBACK_0_MB_ADDR // modbusAddress
);
if (ledFeedback_0)
{
components.push_back(ledFeedback_0);
Log.infoln(F("LEDFeedback_0 initialized. Pin:%d, Count:%d, ID:%d, MB:%d"),
PIN_LED_FEEDBACK_0, LED_PIXEL_COUNT_0,
ID_LED_FEEDBACK_0, LED_FEEDBACK_0_MB_ADDR);
}
else
{
Log.errorln(F("LEDFeedback_0 initialization failed."));
}
#endif
#ifdef ENABLE_JOYSTICK
joystick_0 = new Joystick(
this, // owner
PIN_JOYSTICK_UP, // UP pin
PIN_JOYSTICK_DOWN, // DOWN pin
PIN_JOYSTICK_LEFT, // LEFT pin
PIN_JOYSTICK_RIGHT, // RIGHT pin
MB_ADDR_AUX_7 // modbusAddress
);
if (joystick_0)
{
components.push_back(joystick_0);
}
else
{
Log.errorln(F("Joystick_0 initialization failed."));
}
#endif
// Motors
#ifdef ENABLE_SAKO_VFD
vfd_0 = new SAKO_VFD(MB_SAKO_VFD_SLAVE_ID, MB_SAKO_VFD_READ_INTERVAL);
components.push_back(vfd_0);
#endif
// Temperature
#ifdef ENABLE_PROFILE_TEMPERATURE
for (ushort i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
// Assign unique ID: COMPONENT_KEY_PROFILE_START (910) + slot index
ushort profileComponentId = COMPONENT_KEY_PROFILE_START + i;
tempProfiles[i] = new TemperatureProfile(this, i, profileComponentId);
components.push_back(tempProfiles[i]);
Log.infoln("PHApp::setup - Initialized TemperatureProfile Slot %d with Component ID %d", i, profileComponentId);
}
#endif
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; i++) {
ushort profileComponentId = COMPONENT_KEY_SIGNAL_PLOT_START + i;
signalPlots[i] = new SignalPlot(this, i, profileComponentId);
components.push_back(signalPlots[i]);
Log.infoln("PHApp::setup - Initialized SignalPlot Slot %d with Component ID %d", i, profileComponentId);
}
#endif
#ifdef ENABLE_PID
const int8_t PID2_THERMO_DO = 19; // Example MISO pin
const int8_t PID2_THERMO_CS = 5; // Example Chip Select pin
const int8_t PID2_THERMO_CLK = 18; // Example SCK pin
const int8_t PID2_OUTPUT_PIN = 23; // Example PWM/Output pin
ADD_PID(0, "PID Temp Controller 2", PID2_THERMO_DO, PID2_THERMO_CS, PID2_THERMO_CLK, PID2_OUTPUT_PIN, COMPONENT_KEY_PID_2);
#endif
// RS485
#ifdef ENABLE_RS485
rs485 = new RS485(this);
components.push_back(rs485);
#endif
#ifdef ENABLE_AMPERAGE_BUDGET_MANAGER
pidManagerAmperage = new AmperageBudgetManager(this);
components.push_back(pidManagerAmperage);
#endif
// Systems : Extruder
#ifdef ENABLE_EXTRUDER
extruder_0 = new Extruder(this, vfd_0, nullptr, nullptr, nullptr);
components.push_back(extruder_0);
#endif
#ifdef ENABLE_PLUNGER
plunger_0 = new Plunger(this, vfd_0, joystick_0, pot_0, pot_1);
components.push_back(plunger_0);
#endif
// Application stuff
registerComponents(bridge);
serial_register(bridge);
App::setup();
onRun();
return E_OK;
}
short PHApp::onRun()
{
App::onRun();
#ifdef ENABLE_MODBUS_TCP
for (Component *comp : components)
{
if (comp && comp->hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS))
{
comp->mb_tcp_register(modbusManager);
}
}
#endif
#ifdef ENABLE_PROFILE_TEMPERATURE
load(0, 0);
#endif
#ifdef ENABLE_RELAYS
#ifdef AUX_RELAY_0
relay_0->setValue(1);
#endif
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
//
// Post initialization
//
#ifdef ENABLE_WEBSERVER
registerRoutes(webServer);
#endif
#if ENABLED(ENABLE_AMPERAGE_BUDGET_MANAGER, ENABLE_OMRON_E5)
RTU_Base *const *devices = rs485->deviceManager.getDevices();
int numDevicesInManager = rs485->deviceManager.getMaxDevices();
for (int i = 0; i < numDevicesInManager; ++i)
{
if (devices[i] != nullptr)
{
Component *comp = devices[i];
if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID)
{
continue;
}
if (!pidManagerAmperage->addManagedDevice(static_cast<OmronE5 *>(comp)))
{
Log.errorln("Failed to add OmronE5 device to AmperageBudgetManager");
}
}
}
#endif
#if ENABLED(ENABLE_TEMPERATURE_PROFILES, ENABLE_OMRON_E5, ENABLE_MODBUS_TCP)
tempProfiles[0]->disable();
if (tempProfiles[0] && rs485)
{
RTU_Base *const *devices = rs485->deviceManager.getDevices();
int numDevicesInManager = rs485->deviceManager.getMaxDevices();
int targetRegisterIndex = 0; // Dedicated index for _targetRegisters
for (int i = 0; i < numDevicesInManager; i++)
{
Component *comp = devices[i];
if (comp == nullptr || comp->type != COMPONENT_TYPE::COMPONENT_TYPE_PID)
{
continue;
}
// Ensure we don't write out of bounds for _targetRegisters
/*
if (targetRegisterIndex < TEMP_PROFILE_MAX_TARGET_REGS)
{
uint16_t spCmdAddr = comp->mb_tcp_base_address() + static_cast<uint16_t>(E_OmronTcpOffset::CMD_SP);
tempProfiles[0]->setTargetRegister(targetRegisterIndex, spCmdAddr);
targetRegisterIndex++;
}
else
{
Log.warningln("Max target registers (%d) reached. Cannot set for Omron device (Slave ID: %d)", TEMP_PROFILE_MAX_TARGET_REGS, comp->slaveId);
}
*/
}
}
#endif
return E_OK;
}
short PHApp::serial_register(Bridge *bridge)
{
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("list"), (ComponentFnPtr)&PHApp::list);
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("reset"), (ComponentFnPtr)&PHApp::reset);
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("printRegisters"), (ComponentFnPtr)&PHApp::printRegisters);
bridge->registerMemberFunction(COMPONENT_KEY_BASE::COMPONENT_KEY_APP, this, C_STR("load"), (ComponentFnPtr)&PHApp::load);
return E_OK;
}
short PHApp::onWarning(short code)
{
return E_OK;
}
short PHApp::onError(short id, short code)
{
if (code == getLastError())
{
return code;
}
Log.error(F("* App:onError - component=%d code=%d" CR), id, code);
setLastError(code);
#ifdef ENABLE_STATUS
if (statusLight_0)
{
switch (id)
{
case COMPONENT_KEY_PLUNGER:
{
switch (code)
{
#if defined(ENABLE_PLUNGER)
case (short)PlungerState::IDLE:
{
statusLight_1->set(0, 0);
break;
}
case (short)PlungerState::JAMMED:
{
statusLight_1->set(1, 1);
break;
}
default:
{
statusLight_1->set(1, 0);
break;
}
}
#endif
}
}
}
#endif
return code;
}
short PHApp::onStop(short val)
{
return E_OK;
}
short PHApp::clearError()
{
setLastError(E_OK);
return E_OK;
}
short PHApp::loop()
{
_loop_start_time_us = micros();
App::loop();
#ifdef ENABLE_WEBSERVER
loopWeb();
#endif
#ifdef ENABLE_MODBUS_TCP
loopModbus();
#endif
_loop_duration_us = micros() - _loop_start_time_us;
return E_OK;
}
short PHApp::loopWeb()
{
#if defined(ENABLE_WIFI) && defined(ENABLE_WEBSERVER)
if (webServer != nullptr)
{
webServer->loop();
}
#endif
return E_OK;
}
short PHApp::getAppState(short val)
{
return _state;
}
PHApp::PHApp() : App()
{
name = "PHApp";
webServer = nullptr;
pidController_0 = nullptr;
bridge = nullptr;
com_serial = nullptr;
pot_0 = nullptr;
pot_1 = nullptr;
pot_2 = nullptr;
statusLight_0 = nullptr;
statusLight_1 = nullptr;
relay_0 = nullptr;
relay_1 = nullptr;
relay_2 = nullptr;
relay_3 = nullptr;
relay_4 = nullptr;
relay_5 = nullptr;
relay_6 = nullptr;
relay_7 = nullptr;
pos3Analog_0 = nullptr;
pos3Analog_1 = nullptr;
modbusManager = nullptr;
logPrinter = nullptr;
rs485 = nullptr;
joystick_0 = nullptr;
#ifdef ENABLE_PROFILE_TEMPERATURE
// Initialize the array elements to nullptr
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
tempProfiles[i] = nullptr;
}
#endif
// WiFi settings are now initialized by WiFiNetworkSettings constructor
}
void PHApp::cleanupComponents()
{
Log.infoln("PHApp::cleanupComponents - Cleaning up %d components...", components.size());
for (Component *comp : components)
{
delete comp;
}
components.clear(); // Clear the vector AFTER deleting objects
// Nullify pointers that were manually managed or outside the vector
bridge = nullptr;
com_serial = nullptr;
pot_0 = nullptr;
pot_1 = nullptr;
pot_2 = nullptr;
statusLight_0 = nullptr;
statusLight_1 = nullptr;
relay_0 = nullptr;
relay_1 = nullptr;
relay_2 = nullptr;
relay_3 = nullptr;
relay_4 = nullptr;
relay_5 = nullptr;
relay_6 = nullptr;
relay_7 = nullptr;
pos3Analog_0 = nullptr;
pos3Analog_1 = nullptr;
pidController_0 = nullptr;
modbusManager = nullptr;
joystick_0 = nullptr;
#ifdef ENABLE_PROFILE_TEMPERATURE
// Clean up temperature profiles (they were also added to components vector, so already deleted there)
// Ensure the pointers in the array are nulled
for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i)
{
tempProfiles[i] = nullptr;
}
#endif
#ifdef ENABLE_WEBSERVER
if (webServer)
{
delete webServer;
webServer = nullptr;
}
#endif
#ifdef ENABLE_RS485
rs485 = nullptr; // RS485 interface was in the vector, already deleted
#endif
Log.infoln("PHApp::cleanupComponents - Cleanup complete.");
}
PHApp::~PHApp()
{
cleanupComponents();
}
short PHApp::setAppState(short newState)
{
if (_state != newState)
{
_state = (APP_STATE)newState;
}
return E_OK;
}
#ifdef ENABLE_PID
short PHApp::getPid2Register(short offset, short unused)
{
if (!pidController_0)
{
Log.errorln("Serial Command Error: PID Controller 2 not initialized.");
return E_INVALID_PARAMETER; // Use defined error code
}
if (offset < 0 || offset >= PID_2_REGISTER_COUNT)
{
Log.errorln("Serial Command Error: Invalid PID2 offset %d.", offset);
return E_INVALID_PARAMETER;
}
short address = MB_HREG_PID_2_BASE_ADDRESS + offset;
short value = pidController_0->mb_tcp_read(address);
Log.noticeln("PID2 Register Offset %d (Addr %d) Value: %d", offset, address, value);
// Optionally send value back over serial if needed by the protocol
return E_OK;
}
short PHApp::setPid2Register(short offset, short value)
{
if (!pidController_0)
{
Log.errorln("Serial Command Error: PID Controller 2 not initialized.");
return E_INVALID_PARAMETER; // Use defined error code
}
if (offset < 0 || offset >= PID_2_REGISTER_COUNT)
{
Log.errorln("Serial Command Error: Invalid PID2 offset %d.", offset);
return E_INVALID_PARAMETER;
}
short address = MB_HREG_PID_2_BASE_ADDRESS + offset;
short result = pidController_0->mb_tcp_write(address, value);
if (result == E_OK)
{
Log.noticeln("PID2 Register Offset %d (Addr %d) set to: %d", offset, address, value);
}
else
{
Log.errorln("PID2 Register Offset %d (Addr %d) failed to set to %d. Error: %d", offset, address, value, result);
}
return result;
}
#endif
short PHApp::onMessage(int id, E_CALLS verb, E_MessageFlags flags, void *user, Component *src)
{
#if ENABLED(ENABLE_RS485, ENABLE_WEBSERVER, ENABLE_WEBSOCKET)
if (verb == E_CALLS::EC_USER && user != nullptr && webServer != nullptr)
{
return webServer->onMessage(id, E_CALLS::EC_USER, E_MessageFlags::E_MF_NONE, user, this);
}
#endif
return App::onMessage(id, verb, flags, user, src);
}
/**
* @brief Retrieves a component by its ID.
*
* @param id The ID of the component to retrieve.
* @return A pointer to the component with the specified ID, or nullptr if not found.
* @note Top-Level PHApp cant be part of components vector, so we need to handle it separately.
*/
Component *PHApp::byId(ushort id)
{
Component *comp = App::byId(id);
if (comp)
{
return comp;
}
else if (id == COMPONENT_KEY_APP)
{
return this;
}
return nullptr;
}
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
void PHApp::startSignalPlot(short slotId)
{
if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)
{
Log.infoln("PHApp: Starting SignalPlot in slot %d (triggered by TemperatureProfile).", slotId);
signalPlots[slotId]->start();
}
else
{
Log.warningln("PHApp: Could not start SignalPlot. Invalid slotId %d or plot not initialized.", slotId);
}
}
void PHApp::stopSignalPlot(short slotId)
{
if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)
{
Log.infoln("PHApp: Stopping SignalPlot in slot %d (triggered by TemperatureProfile).", slotId);
signalPlots[slotId]->stop();
}
else
{
Log.warningln("PHApp: Could not stop SignalPlot. Invalid slotId %d or plot not initialized.", slotId);
}
}
void PHApp::enableSignalPlot(short slotId, bool enable)
{
if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)
{
if (enable)
{
Log.infoln("PHApp: Enabling SignalPlot in slot %d (triggered by TemperatureProfile).", slotId);
signalPlots[slotId]->enable();
}
else
{
Log.infoln("PHApp: Disabling SignalPlot in slot %d (triggered by TemperatureProfile).", slotId);
signalPlots[slotId]->disable();
}
}
else
{
Log.warningln("PHApp: Could not enable/disable SignalPlot. Invalid slotId %d or plot not initialized.", slotId);
}
}
void PHApp::pauseSignalPlot(short slotId)
{
if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)
{
Log.infoln("PHApp: Pausing SignalPlot in slot %d (triggered by TemperatureProfile).", slotId);
signalPlots[slotId]->pause();
}
else
{
Log.warningln("PHApp: Could not pause SignalPlot. Invalid slotId %d or plot not initialized.", slotId);
}
}
void PHApp::resumeSignalPlot(short slotId)
{
if (slotId >= 0 && slotId < PROFILE_SIGNAL_PLOT_COUNT && signalPlots[slotId] != nullptr)
{
Log.infoln("PHApp: Resuming SignalPlot in slot %d (triggered by TemperatureProfile).", slotId);
signalPlots[slotId]->resume();
}
else
{
Log.warningln("PHApp: Could not resume SignalPlot. Invalid slotId %d or plot not initialized.", slotId);
}
}
#endif // ENABLE_PROFILE_SIGNAL_PLOT

View File

@ -0,0 +1,279 @@
#ifndef PHAPP_H
#define PHAPP_H
#include "config.h"
#include "config-modbus.h"
#include "features.h"
#include <enums.h>
#include <vector>
#include <xmath.h>
#include <macros.h>
#include <App.h>
#include <Component.h>
#include <Bridge.h>
#include <SerialMessage.h>
#include <ArduinoLog.h>
#include <Logger.h>
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <xstatistics.h>
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
#include <profiles/SignalPlot.h>
#include <profiles/WiFiNetworkSettings.h>
#include <components/OmronE5.h>
class POT;
class Relay;
class RS485;
class Pos3Analog;
class StatusLight;
class RESTServer;
class PIDController;
class TemperatureProfile;
class SignalPlot;
class SAKO_VFD;
class MB_GPIO;
class AnalogLevelSwitch;
class LEDFeedback;
class Extruder;
class Plunger;
class Joystick;
class PHApp;
class AmperageBudgetManager;
class AsyncWebServerRequest;
class PHApp : public App
{
public:
//////////////////////////////////////////////////////////////
// Enums
//////////////////////////////////////////////////////////////
enum CONTROLLER_STATE
{
E_CS_OK = 0,
E_CS_ERROR = 10
};
enum APP_STATE
{
RESET = 0,
EXTRUDING = 1,
STANDBY = 2,
ERROR = 5,
PID_TIMEOUT = 11,
FEED_TIMEOUT = 12,
CONTROL_PANEL_INVALID = 13,
PID_ERROR = 20,
FEED_ERROR = 40,
};
//////////////////////////////////////////////////////////////
// Constructor / Destructor
//////////////////////////////////////////////////////////////
PHApp();
~PHApp() override;
//////////////////////////////////////////////////////////////
// Core Application Logic
//////////////////////////////////////////////////////////////
virtual short setup();
virtual short onRun();
short loop() override;
short load(short val0 = 0, short val1 = 0);
virtual short serial_register(Bridge *bridge);
virtual Component *byId(ushort id);
// App States & Error Handling
short _state;
short _cstate;
short _error;
short setAppState(short newState);
short getAppState(short val);
short getLastError() { return _error; }
short setLastError(short val = 0) { _error = val; return _error; }
short onError(short id, short code);
short clearError();
short reset(short arg1, short arg2); // Related to resetting state?
//////////////////////////////////////////////////////////////
// Components
//////////////////////////////////////////////////////////////
SerialMessage *com_serial;
POT *pot_0;
POT *pot_1;
POT *pot_2;
StatusLight *statusLight_0;
StatusLight *statusLight_1;
Relay *relay_0;
Relay *relay_1;
Relay *relay_2;
Relay *relay_3;
Relay *relay_4;
Relay *relay_5;
Relay *relay_6;
Relay *relay_7;
Pos3Analog *pos3Analog_0;
Pos3Analog *pos3Analog_1;
PIDController *pidController_0;
SAKO_VFD *vfd_0;
Extruder *extruder_0;
Plunger *plunger_0;
MB_GPIO *gpio_0;
AnalogLevelSwitch *analogLevelSwitch_0;
LEDFeedback *ledFeedback_0;
Joystick *joystick_0;
AmperageBudgetManager *pidManagerAmperage;
// Component Callbacks/Control
short onStop(short code = 0);
short onWarning(short code);
//////////////////////////////////////////////////////////////
// Logging
//////////////////////////////////////////////////////////////
std::vector<String> logBuffer;
size_t currentLogIndex = 0;
CircularLogPrinter *logPrinter = nullptr;
std::vector<String> getLogSnapshot()
{
std::vector<String> snapshot;
snapshot.reserve(logBuffer.size());
if (logBuffer.size() < LOG_BUFFER_LINES)
{
for (size_t i = 0; i < logBuffer.size(); ++i)
{
snapshot.push_back(logBuffer[i]);
}
}
else
{
// Buffer is full and circular
size_t startIndex = (currentLogIndex + 1) % LOG_BUFFER_LINES; // <-- Note: LOG_BUFFER_LINES is now defined in Logger.h
for (size_t i = 0; i < LOG_BUFFER_LINES; ++i)
{
snapshot.push_back(logBuffer[(startIndex + i) % LOG_BUFFER_LINES]);
}
}
return snapshot;
}
//////////////////////////////////////////////////////////////
// Network Management
//////////////////////////////////////////////////////////////
short setupNetwork();
short loadNetworkSettings();
short saveNetworkSettings(JsonObject& doc);
WiFiNetworkSettings wifiSettings;
//////////////////////////////////////////////////////////////
// Modbus TCP
//////////////////////////////////////////////////////////////
ModbusTCP *modbusManager;
short loopModbus();
#ifdef ENABLE_MODBUS_TCP
short setupModbus();
short mb_tcp_write(short address, short value) override;
short mb_tcp_write(MB_Registers *reg, short networkValue) override;
short mb_tcp_read(short address) override;
void mb_tcp_register(ModbusTCP *manager) const override;
ModbusBlockView *mb_tcp_blocks() const override;
int client_count;
int client_max;
int client_total;
millis_t client_track_ts;
short updateClientCount(short val0, short val1);
short resetClientStats(short val0, short val1);
short getClientStats(short val0, short val1);
// Modbus PID Specific (Conditional)
short getConnectedClients() const; // Returns number of currently connected Modbus TCP clients
#ifdef ENABLE_PID
short getPid2Register(short offset, short unused);
short setPid2Register(short offset, short value);
#endif // ENABLE_PID
#endif // ENABLE_MODBUS_TCP
RESTServer *webServer;
short loopWeb();
#ifdef ENABLE_RS485
friend class RS485Devices;
#endif // ENABLE_RS485
//////////////////////////////////////////////////////////////
// Component Overrides / Message Handling
/////////////////////////////////////////////////////////////
/**
* @brief Handles incoming messages, including RTU updates via void*.
*/
short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override;
//////////////////////////////////////////////////////////////
// Debugging & Utility Methods
//////////////////////////////////////////////////////////////
void printRegisters();
short list(short val0, short val1);
short print(short arg1, short arg2);
//////////////////////////////////////////////////////////////
// Profiling & Feature Specific (Conditional)
//////////////////////////////////////////////////////////////
#ifdef ENABLE_PROFILER
static uint32_t initialFreeHeap;
static uint64_t initialCpuTicks;
#endif // ENABLE_PROFILER
#ifdef ENABLE_PROFILE_TEMPERATURE
TemperatureProfile* tempProfiles[PROFILE_TEMPERATURE_COUNT]; // Array to hold multiple temperature profiles
void getProfilesHandler(AsyncWebServerRequest *request);
void setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot); // Adjusted for body handling
bool saveProfilesToJson();
#endif // ENABLE_PROCESS_PROFILE
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
SignalPlot* signalPlots[PROFILE_SIGNAL_PLOT_COUNT]; // Array to hold multiple signal plot profiles
void getSignalPlotsHandler(AsyncWebServerRequest *request);
void setSignalPlotsHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot);
bool saveSignalPlotsToJson();
// Methods to control SignalPlot from TemperatureProfile
void startSignalPlot(short slotId);
void stopSignalPlot(short slotId);
void enableSignalPlot(short slotId, bool enable);
void pauseSignalPlot(short slotId);
void resumeSignalPlot(short slotId);
#endif // ENABLE_PROFILE_SIGNAL_PLOT
//////////////////////////////////////////////////////////////
// Web Server
//////////////////////////////////////////////////////////////
/**
* @brief Register routes with the RESTServer. This will be called upon built-in RESTServer initialization.
*
* @param server The RESTServer instance to register routes with.
* @return short The result of the operation.
*/
short registerRoutes(RESTServer *instance);
// Network settings handlers
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
void handleGetNetworkSettings(AsyncWebServerRequest *request);
void handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json);
#endif
void getSystemLogsHandler(AsyncWebServerRequest *request);
void getBridgeMethodsHandler(AsyncWebServerRequest *request);
private:
//////////////////////////////////////////////////////////////
// Private Methods
//////////////////////////////////////////////////////////////
void handleSerialCommand(const String &command); // Moved here as it's private impl detail
void cleanupComponents(); // Moved here as it's private impl detail
};
#endif

View File

@ -0,0 +1,272 @@
#include <Arduino.h>
#include <macros.h>
#include <Component.h>
#include <enums.h>
#include "Logger.h"
#include "./PHApp.h"
#include <ESPmDNS.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "./config.h"
#include "./config_adv.h"
#include "./config-modbus.h"
#include "./features.h"
#ifdef ENABLE_PROCESS_PROFILE
#include "profiles/PlotBase.h"
#include "profiles/SignalPlot.h"
#include "profiles/TemperatureProfile.h"
#endif
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
short PHApp::loadNetworkSettings() {
Log.infoln(F("PHApp::loadNetworkSettings() - Attempting to load network configuration from LittleFS..."));
if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted (true formats if necessary)
Log.errorln(F("PHApp::loadNetworkSettings() - Failed to mount LittleFS. Cannot load network configuration."));
wifiSettings.print(); // Print defaults before returning
return E_FATAL; // Use E_FATAL for critical FS failure
}
File configFile = LittleFS.open(NETWORK_CONFIG_FILENAME, "r");
if (!configFile) {
Log.warningln(F("PHApp::loadNetworkSettings() - Failed to open network config file: %s. Using default settings."), NETWORK_CONFIG_FILENAME);
LittleFS.end(); // Close LittleFS
wifiSettings.print(); // Print defaults before returning
return E_NOT_FOUND; // Indicates file wasn't found, defaults will be used.
}
Log.infoln(F("PHApp::loadNetworkSettings() - Opened network config file: %s"), NETWORK_CONFIG_FILENAME);
JsonDocument doc; // Using JsonDocument for automatic memory management
DeserializationError error = deserializeJson(doc, configFile);
configFile.close(); // Close the file as soon as possible
if (error) {
Log.errorln(F("PHApp::loadNetworkSettings() - Failed to parse network config JSON: %s. Using default settings."), error.c_str());
LittleFS.end(); // Close LittleFS
wifiSettings.print(); // Print defaults before returning
return E_INVALID_PARAMETER; // Indicates a parsing error, defaults will be used.
}
JsonObject root = doc.as<JsonObject>();
if (root.isNull()) {
Log.errorln(F("PHApp::loadNetworkSettings() - Network config JSON root is not an object. Using default settings."));
LittleFS.end();
wifiSettings.print(); // Print defaults before returning
return E_INVALID_PARAMETER;
}
Log.infoln(F("PHApp::loadNetworkSettings() - Successfully parsed network config file. Applying settings..."));
short loadResult = wifiSettings.loadSettings(root); // Call the existing method in WiFiNetworkSettings
LittleFS.end(); // Ensure LittleFS is closed after operations
if (loadResult == E_OK) {
Log.infoln(F("PHApp::loadNetworkSettings() - Network settings loaded successfully from %s."), NETWORK_CONFIG_FILENAME);
} else {
Log.warningln(F("PHApp::loadNetworkSettings() - Issues applying parsed network settings. Some defaults may still be in use."));
}
wifiSettings.print(); // Print settings after attempting to load them
return loadResult;
}
short PHApp::saveNetworkSettings(JsonObject& doc) {
Log.infoln(F("PHApp::saveNetworkSettings() - Attempting to save network configuration to LittleFS..."));
if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted
Log.errorln(F("PHApp::saveNetworkSettings() - Failed to mount LittleFS. Cannot save network configuration."));
return E_FATAL; // Or a more specific LittleFS error
}
File configFile = LittleFS.open(NETWORK_CONFIG_FILENAME, "w"); // Open for writing, creates if not exists, truncates if exists
if (!configFile) {
Log.errorln(F("PHApp::saveNetworkSettings() - Failed to open network config file '%s' for writing."), NETWORK_CONFIG_FILENAME);
LittleFS.end(); // Close LittleFS
return E_FATAL; // Replaced E_FS_ERROR with E_FATAL
}
Log.infoln(F("PHApp::saveNetworkSettings() - Opened/created network config file: %s for writing."), NETWORK_CONFIG_FILENAME);
size_t bytesWritten = serializeJson(doc, configFile);
configFile.close(); // Close the file as soon as possible
if (bytesWritten > 0) {
Log.infoln(F("PHApp::saveNetworkSettings() - Successfully wrote %d bytes to %s."), bytesWritten, NETWORK_CONFIG_FILENAME);
} else {
Log.errorln(F("PHApp::saveNetworkSettings() - Failed to serialize JSON to file or wrote 0 bytes to %s."), NETWORK_CONFIG_FILENAME);
LittleFS.end(); // Close LittleFS
// Attempt to remove the (potentially empty or corrupted) file if serialization failed.
if (LittleFS.exists(NETWORK_CONFIG_FILENAME)) {
LittleFS.remove(NETWORK_CONFIG_FILENAME);
}
return E_INVALID_PARAMETER; // Or a more specific serialization error
}
LittleFS.end(); // Ensure LittleFS is closed after operations
Log.infoln(F("PHApp::saveNetworkSettings() - Network settings saved successfully to %s."), NETWORK_CONFIG_FILENAME);
// Optionally, after saving, you might want to immediately reload and apply these settings:
// loadNetworkSettings();
// Or, signal that a restart is needed for settings to take full effect if they are only read at boot.
return E_OK;
}
short PHApp::setupNetwork()
{
loadNetworkSettings(); // Load settings from LittleFS first
bool sta_connected = false;
bool ap_started = false;
#if defined(ENABLE_AP_STA)
WiFi.mode(WIFI_AP_STA);
Log.infoln("Setting up AP_STA with SSID: %s", wifiSettings.ap_ssid.c_str());
if (!WiFi.softAPConfig(wifiSettings.ap_config_ip, wifiSettings.ap_config_gateway, wifiSettings.ap_config_subnet))
{
Log.errorln("AP Failed to configure");
}
else
{
if (!WiFi.softAP(wifiSettings.ap_ssid.c_str(), wifiSettings.ap_password.c_str()))
{
Log.errorln("AP Failed to start");
}
else
{
Log.infoln("AP IP address: %s", WiFi.softAPIP().toString().c_str());
ap_started = true;
}
}
// Configure Station (STA) part
Log.infoln("Configuring STA for AP_STA mode...");
if (!WiFi.config(wifiSettings.sta_local_IP, wifiSettings.sta_gateway, wifiSettings.sta_subnet, wifiSettings.sta_primary_dns, wifiSettings.sta_secondary_dns))
{
Log.errorln("STA (for AP_STA) Failed to configure");
}
WiFi.begin(wifiSettings.sta_ssid.c_str(), wifiSettings.sta_password.c_str());
Log.infoln("Attempting to connect to STA WiFi: %s", wifiSettings.sta_ssid.c_str());
int connect_timeout_ms = 30000;
unsigned long start_time = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start_time < connect_timeout_ms))
{
delay(100);
}
if (WiFi.status() == WL_CONNECTED)
{
Log.infoln("STA IP address (AP_STA mode): %s", WiFi.localIP().toString().c_str());
sta_connected = true;
}
else
{
Log.warningln("STA (for AP_STA) connection failed or timed out. AP is still active.");
}
#elif defined(ENABLE_WIFI) // STA mode only
Log.infoln("Configuring WiFi in STA mode...");
if (!WiFi.config(wifiSettings.sta_local_IP, wifiSettings.sta_gateway, wifiSettings.sta_subnet, wifiSettings.sta_primary_dns, wifiSettings.sta_secondary_dns))
{
Log.errorln("STA Failed to configure");
}
WiFi.begin(wifiSettings.sta_ssid.c_str(), wifiSettings.sta_password.c_str());
int connect_timeout_ms = 30000;
unsigned long start_time = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start_time < connect_timeout_ms))
{
delay(100);
}
if (WiFi.status() == WL_CONNECTED)
{
Log.infoln("IP address: %s", WiFi.localIP().toString().c_str());
sta_connected = true;
}
else
{
Log.errorln("WiFi connection timed out!");
// return E_WIFI_CONNECTION_FAILED; // Keep network setup going if AP might work or for mDNS on AP
}
#endif
// Initialize mDNS
// It should be started if either STA is connected or AP is successfully started.
if (sta_connected || ap_started) {
const char* mdns_hostname = "polymech-cassandra";
if (MDNS.begin(mdns_hostname)) {
Log.infoln("mDNS responder started. Hostname: %s", mdns_hostname);
MDNS.addService("http", "tcp", 80);
Log.infoln("mDNS service _http._tcp.local on port 80 advertised.");
Log.infoln("Access the web server at: http://%s.local", mdns_hostname);
} else {
Log.errorln("Error starting mDNS responder!");
}
} else {
Log.warningln("Neither STA connected nor AP started. mDNS will not be initialized.");
}
#ifdef ENABLE_MODBUS_TCP
setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
setupModbus();
#else
modbusManager = nullptr;
#endif
#if defined(ENABLE_WEBSERVER) && defined(ENABLE_MODBUS_TCP)
if (modbusManager) // Check Modbus dependency first
{
IPAddress webserverIP = IPAddress(0,0,0,0);
bool canStartWebServer = false;
#if defined(ENABLE_AP_STA)
webserverIP = WiFi.softAPIP(); // IP of the AP interface
if (webserverIP && webserverIP != IPAddress(0,0,0,0)) {
Log.infoln("AP_STA mode: Web server will use AP IP: %s", webserverIP.toString().c_str());
canStartWebServer = true;
} else {
Log.errorln("AP_STA mode: Soft AP IP is invalid or not yet available. Cannot determine IP for web server on AP.");
}
// Log STA IP for informational purposes if connected
if (WiFi.status() == WL_CONNECTED) {
Log.infoln("AP_STA mode: STA interface is also connected with IP: %s", WiFi.localIP().toString().c_str());
Log.infoln(" External clients (on STA network) might try http://%s", WiFi.localIP().toString().c_str());
}
#elif defined(ENABLE_WIFI) // STA mode only
if (WiFi.status() == WL_CONNECTED) {
webserverIP = WiFi.localIP();
Log.infoln("STA mode: Web server will use STA IP: %s", webserverIP.toString().c_str());
canStartWebServer = true;
} else {
Log.errorln("STA mode: WiFi not connected. Cannot start web server.");
}
#else
// This case should not be hit if ENABLE_WEBSERVER implies one of the WiFi modes for IP-based server.
Log.warningln("WebServer enabled, but no WiFi mode (AP_STA or STA) is configured to provide an IP address.");
#endif
if (canStartWebServer) {
webServer = new RESTServer(webserverIP, 80, modbusManager, this);
components.push_back(webServer);
Log.infoln("RESTServer initialized.");
Log.infoln("Clients connected to the ESP32 (e.g., via AP) should try accessing the server at: http://%s", webserverIP.toString().c_str());
} else {
Log.errorln("Cannot initialize RESTServer: No suitable IP address available from current WiFi configuration.");
webServer = nullptr;
}
}
else
{
Log.errorln("Cannot initialize RESTServer: ModbusTCP is null! Ensure Modbus is setup first.");
webServer = nullptr;
return E_DEPENDENCY_NOT_MET;
}
#elif defined(ENABLE_WEBSERVER) && !defined(ENABLE_MODBUS_TCP)
Log.warningln("WebServer enabled but Modbus TCP is not. RESTServer initialization might be incomplete.");
webServer = nullptr; // Keep it null if it relies on ModbusTCP
#endif
return E_OK;
}

View File

@ -0,0 +1,5 @@
#include "PHApp.h"
#include "config.h"
#include <ArduinoLog.h>
#include <ArduinoJson.h>

View File

@ -0,0 +1,463 @@
#include "PHApp.h"
#include <components/RestServer.h>
#include <ESPAsyncWebServer.h>
short PHApp::registerRoutes(RESTServer *instance)
{
#ifdef ENABLE_PLUNGER
instance->server.on("/api/v1/plunger/settings", HTTP_GET, [instance](AsyncWebServerRequest *request)
{
Component* comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);
if (!comp) {
request->send(404, "application/json", "{\"success\":false,\"error\":\"Plunger component not found\"}");
return;
}
Plunger* plunger = static_cast<Plunger*>(comp);
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
plunger->getSettingsJson(doc);
serializeJson(doc, *response);
request->send(response); });
AsyncCallbackJsonWebHandler *setPlungerSettingsHandler = new AsyncCallbackJsonWebHandler("/api/v1/plunger/settings",
[instance](AsyncWebServerRequest *request, JsonVariant &json)
{
Component *comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);
if (!comp)
{
request->send(404, "application/json", "{\"success\":false,\"error\":\"Plunger component not found\"}");
return;
}
Plunger *plunger = static_cast<Plunger *>(comp);
if (!json.is<JsonObject>())
{
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: Expected an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
if (plunger->updateSettingsFromJson(jsonObj))
{
request->send(200, "application/json", "{\"success\":true,\"message\":\"Plunger settings updated and saved.\"}");
}
else
{
request->send(500, "application/json", "{\"success\":false,\"error\":\"Failed to update or save Plunger settings.\"}");
}
});
setPlungerSettingsHandler->setMethod(HTTP_POST);
instance->server.addHandler(setPlungerSettingsHandler);
instance->server.on("/api/v1/plunger/settings/load-defaults", HTTP_POST, [instance](AsyncWebServerRequest *request)
{
Component* comp = instance->owner->byId(COMPONENT_KEY_PLUNGER);
if (!comp) {
request->send(404, "application/json", "{\"success\":false,\"error\":\"Plunger component not found\"}");
return;
}
Plunger* plunger = static_cast<Plunger*>(comp);
if (plunger->loadDefaultSettings()) {
request->send(200, "application/json", "{\"success\":true,\"message\":\"Plunger default settings loaded and applied to operational settings.\"}");
} else {
request->send(500, "application/json", "{\"success\":false,\"error\":\"Failed to load default settings or save them to operational path.\"}");
} });
#endif
#ifdef ENABLE_PROFILE_TEMPERATURE
// --- Temperature Profile Routes ---
instance->server.on("/api/v1/profiles", HTTP_GET, [this](AsyncWebServerRequest *request)
{ this->getProfilesHandler(request); });
// Handler for POST /api/v1/profiles
// The slot is now taken from the JSON payload.
AsyncCallbackJsonWebHandler* postProfileHandler = new AsyncCallbackJsonWebHandler("/api/v1/profiles",
[this](AsyncWebServerRequest *request, JsonVariant &json) {
if (!json.is<JsonObject>() || !json["slot"].is<int>()) {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid payload: Must be JSON object containing an integer 'slot' field.\"}");
return;
}
int slot = json["slot"].as<int>();
// Basic validation for slot number (e.g., non-negative)
// You might want to add an upper bound check against PROFILE_TEMPERATURE_COUNT if it's accessible here
// or rely on setProfilesHandler to do more thorough validation.
if (slot < 0) {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number in payload.\"}");
return;
}
// Call the actual handler, passing the parsed JSON and extracted slot number
this->setProfilesHandler(request, json, slot);
});
postProfileHandler->setMethod(HTTP_POST); // Ensure it's set for POST
instance->server.addHandler(postProfileHandler);
#endif
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
// --- Signal Plot Profile Routes ---
instance->server.on("/api/v1/signalplots", HTTP_GET, [this](AsyncWebServerRequest *request)
{ this->getSignalPlotsHandler(request); });
AsyncCallbackJsonWebHandler* postSignalPlotHandler = new AsyncCallbackJsonWebHandler("/api/v1/signalplots",
[this](AsyncWebServerRequest *request, JsonVariant &json) {
if (!json.is<JsonObject>() || !json["slot"].is<int>()) {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid payload: Must be JSON object containing an integer 'slot' field.\"}");
return;
}
int slot = json["slot"].as<int>();
if (slot < 0) {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number in payload.\"}");
return;
}
this->setSignalPlotsHandler(request, json, slot);
});
postSignalPlotHandler->setMethod(HTTP_POST);
instance->server.addHandler(postSignalPlotHandler);
#endif
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
instance->server.on("/api/network/settings", HTTP_GET, std::bind(&PHApp::handleGetNetworkSettings, this, std::placeholders::_1));
AsyncCallbackJsonWebHandler *setNetworkSettingsHandler = new AsyncCallbackJsonWebHandler("/api/network/settings",
std::bind(&PHApp::handleSetNetworkSettings, this, std::placeholders::_1, std::placeholders::_2));
setNetworkSettingsHandler->setMethod(HTTP_POST);
instance->server.addHandler(setNetworkSettingsHandler);
#endif
instance->server.on("/api/v1/system/logs", HTTP_GET, [this](AsyncWebServerRequest *request)
{ this->getSystemLogsHandler(request); });
instance->server.on("/api/v1/methods", HTTP_GET, [this](AsyncWebServerRequest *request)
{ this->getBridgeMethodsHandler(request); });
AsyncCallbackJsonWebHandler* postMethodHandler = new AsyncCallbackJsonWebHandler("/api/v1/methods",
[this](AsyncWebServerRequest *request, JsonVariant &json) {
if (!json.is<JsonObject>() || !json["command"].is<String>()) {
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid payload: Must be JSON object containing a 'command' string field.\"}");
return;
}
String cmdStr = json["command"].as<String>();
Log.infoln("REST: Received method call command: %s", cmdStr.c_str());
Bridge* bridge = static_cast<Bridge*>(byId(COMPONENT_KEY_MB_BRIDGE));
if (!bridge) {
Log.errorln("REST: Bridge component not found!");
request->send(500, "application/json", "{\"success\":false,\"error\":\"Bridge component not found\"}");
return;
}
CommandMessage msg;
if (!msg.parse(cmdStr)) {
Log.errorln("REST: Failed to parse command string: %s", cmdStr.c_str());
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid command string format\"}");
return;
}
short result = bridge->onMessage(msg.id, msg.verb, msg.flags, msg.payload, this);
if (result == E_OK) {
request->send(200, "application/json", "{\"success\":true,\"message\":\"Method executed successfully\"}");
} else {
Log.errorln("REST: Method execution failed with error %d", result);
request->send(500, "application/json", "{\"success\":false,\"error\":\"Method execution failed\"}");
}
});
postMethodHandler->setMethod(HTTP_POST);
instance->server.addHandler(postMethodHandler);
return E_OK;
}
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
void PHApp::handleGetNetworkSettings(AsyncWebServerRequest *request)
{
JsonDocument doc = wifiSettings.toJSON();
String responseStr;
serializeJson(doc, responseStr);
request->send(200, "application/json", responseStr);
}
void PHApp::handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json)
{
if (!json.is<JsonObject>())
{
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: Expected an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
// Attempt to save the settings
short saveResult = saveNetworkSettings(jsonObj);
if (saveResult != E_OK)
{
Log.errorln("REST: Failed to save network settings, error: %d", saveResult);
request->send(500, "application/json", "{\"success\":false,\"error\":\"Failed to save network settings to persistent storage.\"}");
return;
}
// Attempt to load and apply the new settings immediately
short loadResult = loadNetworkSettings();
if (loadResult != E_OK && loadResult != E_NOT_FOUND)
{ // E_NOT_FOUND is ok if we just saved it, means it was applied from the save buffer
Log.warningln("REST: Issue loading network settings after save, error: %d. Settings might not be immediately active.", loadResult);
// Decide if this is a critical failure for the response
}
request->send(200, "application/json", "{\"success\":true,\"message\":\"Network settings saved. Device will attempt to apply them. A restart might be required for all changes to take effect.\"}");
}
void PHApp::getSystemLogsHandler(AsyncWebServerRequest *request)
{
String levelStr = "verbose"; // Default to verbose
if (request->hasParam("level"))
{
levelStr = request->getParam("level")->value();
}
// Map string log levels to their integer values
int requestedLevel = LOG_LEVEL_VERBOSE; // Default to verbose
if (levelStr == "none")
requestedLevel = LOG_LEVEL_SILENT;
else if (levelStr == "error")
requestedLevel = LOG_LEVEL_ERROR;
else if (levelStr == "warning")
requestedLevel = LOG_LEVEL_WARNING;
else if (levelStr == "notice")
requestedLevel = LOG_LEVEL_NOTICE;
else if (levelStr == "trace")
requestedLevel = LOG_LEVEL_TRACE;
else if (levelStr == "verbose")
requestedLevel = LOG_LEVEL_VERBOSE;
else
{
request->send(400, "application/json", "{\"error\":\"Invalid log level\"}");
return;
}
String response;
// Get logs using existing logBuffer implementation in PHApp
std::vector<String> logSnapshot = getLogSnapshot();
// Begin JSON array response
response = "[";
bool first = true;
// Function to escape special characters in JSON
auto escapeJSON = [](const String &str) -> String
{
String result;
for (size_t i = 0; i < str.length(); i++)
{
char c = str.charAt(i);
switch (c)
{
case '"':
result += "\\\"";
break;
case '\\':
result += "\\\\";
break;
case '\b':
result += "\\b";
break;
case '\f':
result += "\\f";
break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
if (c < ' ')
{
char hex[7];
snprintf(hex, sizeof(hex), "\\u%04x", c);
result += hex;
}
else
{
result += c;
}
}
}
return result;
};
// Function to determine log level from a log line
auto getLogLevel = [](const String &line) -> int
{
if (line.startsWith("E:"))
return LOG_LEVEL_ERROR;
if (line.startsWith("W:"))
return LOG_LEVEL_WARNING;
if (line.startsWith("N:"))
return LOG_LEVEL_NOTICE;
if (line.startsWith("T:"))
return LOG_LEVEL_TRACE;
if (line.startsWith("V:"))
return LOG_LEVEL_VERBOSE;
if (line.startsWith("I:"))
return LOG_LEVEL_INFO;
return LOG_LEVEL_VERBOSE; // Default to verbose if no prefix found
};
// Add each log entry to the response if it meets the requested level
for (const auto &logLine : logSnapshot)
{
int lineLevel = getLogLevel(logLine);
if (lineLevel <= requestedLevel)
{
if (!first)
response += ",";
response += "\"" + escapeJSON(logLine) + "\"";
first = false;
}
}
response += "]";
request->send(200, "application/json", response);
}
#endif
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
/**
* @brief Handles GET requests to /api/v1/signalplots
* Returns a list of available signal plot profiles.
*/
void PHApp::getSignalPlotsHandler(AsyncWebServerRequest *request)
{
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
JsonArray profilesArray = doc["signalplots"].to<JsonArray>();
for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; ++i)
{
SignalPlot *profile = this->signalPlots[i];
if (profile)
{
Log.verboseln(" Processing SignalPlot Slot %d: %s", i, profile->name.c_str());
JsonObject profileObj = profilesArray.add<JsonObject>();
profileObj["slot"] = i;
profileObj["name"] = profile->name;
profileObj["duration"] = profile->getDuration();
profileObj["status"] = (int)profile->getCurrentStatus();
profileObj["enabled"] = profile->enabled();
profileObj["elapsed"] = profile->getElapsedMs();
profileObj["remaining"] = profile->getRemainingTime();
JsonArray pointsArray = profileObj["controlPoints"].to<JsonArray>();
const S_SignalControlPoint *points = profile->getControlPoints();
uint8_t numPoints = profile->getNumControlPoints();
for (uint8_t j = 0; j < numPoints; ++j)
{
const S_SignalControlPoint& cp = points[j];
JsonObject pointObj = pointsArray.add<JsonObject>();
pointObj["id"] = cp.id;
pointObj["time"] = cp.time;
pointObj["name"] = cp.name;
pointObj["description"] = cp.description;
pointObj["state"] = (int16_t)cp.state;
pointObj["type"] = (int16_t)cp.type;
pointObj["arg_0"] = cp.arg_0;
pointObj["arg_1"] = cp.arg_1;
pointObj["arg_2"] = cp.arg_2;
}
}
else
{
Log.warningln(" SignalPlot slot %d is null", i);
}
}
serializeJson(doc, *response);
request->send(response);
}
/**
* @brief Handles POST requests to /api/v1/signalplots (slot from payload)
* Updates the specified signal plot profile using the provided JSON data.
*
* @param request The incoming web request.
* @param json The parsed JSON body from the request.
* @param slot The profile slot number extracted from the payload.
*/
void PHApp::setSignalPlotsHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot)
{
if (slot < 0 || slot >= PROFILE_SIGNAL_PLOT_COUNT)
{
Log.warningln("REST: setSignalPlotsHandler - Invalid slot number %d provided.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number\"}");
return;
}
SignalPlot *targetProfile = this->signalPlots[slot];
if (!targetProfile)
{
Log.warningln("REST: setSignalPlotsHandler - No profile found for slot %d.", slot);
request->send(404, "application/json", "{\"success\":false,\"error\":\"Profile slot not found or not initialized\"}");
return;
}
if (!json.is<JsonObject>())
{
Log.warningln("REST: setSignalPlotsHandler - Invalid JSON payload (not an object) for slot %d.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: must be an object.\"}");
return;
}
JsonObject jsonObj = json.as<JsonObject>();
bool success = targetProfile->load(jsonObj);
if (success)
{
Log.infoln("REST: SignalPlot slot %d updated successfully.", slot);
if (saveSignalPlotsToJson()) {
Log.infoln("REST: All SignalPlot profiles saved to JSON successfully after update.");
request->send(200, "application/json", "{\"success\":true, \"message\":\"SignalPlot profile updated and saved.\"}");
} else {
Log.errorln("REST: SignalPlot slot %d updated, but failed to save all profiles to JSON.", slot);
request->send(500, "application/json", "{\"success\":true, \"message\":\"SignalPlot profile updated but failed to save configuration.\"}");
}
}
else
{
Log.errorln("REST: Failed to update SignalPlot slot %d from JSON.", slot);
request->send(400, "application/json", "{\"success\":false,\"error\":\"Failed to load SignalPlot profile data. Check format and values.\"}");
}
}
#endif // ENABLE_PROFILE_SIGNAL_PLOT
void PHApp::getBridgeMethodsHandler(AsyncWebServerRequest *request)
{
AsyncResponseStream *response = request->beginResponseStream("application/json");
JsonDocument doc;
JsonArray methodsArray = doc.to<JsonArray>();
Bridge *bridge = static_cast<Bridge *>(byId(COMPONENT_KEY_MB_BRIDGE));
if (!bridge)
{
Log.errorln(F("REST: Bridge component not found!"));
request->send(500, "application/json", "{\"success\":false,\"error\":\"Bridge component not found\"}");
return;
}
const Vector<SComponentInfo *> &componentList = bridge->getComponentList();
for (size_t i = 0; i < componentList.size(); ++i)
{
SComponentInfo *compInfo = componentList.at(i);
if (compInfo && compInfo->instance)
{
Component *component = static_cast<Component *>(compInfo->instance);
JsonObject methodObj = methodsArray.add<JsonObject>();
methodObj["id"] = compInfo->key;
methodObj["component"] = component->name;
methodObj["method"] = compInfo->methodName;
}
}
serializeJson(doc, *response);
request->send(response);
}

View File

@ -31,17 +31,14 @@ export const getCodingModels = (): string[] => {
export const getFileModels = (): string[] => {
return [
E_OPENROUTER_MODEL.MODEL_OPENAI_GPT_4O_MINI,
E_OPENROUTER_MODEL.MODEL_NVIDIA_LLAMA_3_3_NEMOTRON_SUPER_49B_V1_FREE,
E_OPENROUTER_MODEL.MODEL_GOOGLE_GEMINI_2_0_FLASH_EXP_FREE
]
}
export const getLanguageModels = (): string[] => {
return [
E_OPENROUTER_MODEL.MODEL_ANTHROPIC_CLAUDE_3_5_SONNET,
E_OPENROUTER_MODEL.MODEL_QWEN_QWQ_32B,
E_OPENROUTER_MODEL.MODEL_OPENAI_GPT_4O_MINI,
E_OPENROUTER_MODEL.MODEL_OPENAI_GPT_3_5_TURBO
E_OPENROUTER_MODEL.MODEL_ANTHROPIC_CLAUDE_3_7_SONNET,
E_OPENROUTER_MODEL.MODEL_OPENAI_GPT_4O_MINI
]
}

View File

@ -0,0 +1,87 @@
import * as path from 'node:path'
// import * as fs from 'node:fs/promises' // No longer needed for this test
import { describe, it, expect } from 'vitest' // Removed afterAll, beforeAll
import { E_Mode, run, IKBotTask, complete_options, complete_messages, complete_params } from '../../src/index'
// import { LOGGING_DIRECTORY } from '../../src/constants.js' // No longer needed for this test
// import { sync as rimrafSync } from 'rimraf' // No longer needed for this test
describe('globExtension with complete_params output', () => {
const testDataBaseDir = path.resolve(__dirname, '../test-data');
const testDataRoot = path.resolve(testDataBaseDir, 'glob');
// const defaultLogsDir = path.resolve(LOGGING_DIRECTORY); // No longer needed
// const paramsJsonPath = path.resolve(defaultLogsDir, 'params.json'); // No longer needed
// beforeAll/afterAll for log cleanup removed
const expectedFileNames = [
'PHApp.h',
'PHApp.cpp',
'PHApp-Modbus.cpp',
'PHApp-Profiles.cpp',
'PHAppNetwork.cpp',
'PHAppSettings.cpp',
'PHAppWeb.cpp'
];
const expectedAbsoluteFilePaths = expectedFileNames.map(f => path.normalize(path.resolve(testDataRoot, f)));
const mockLogger = {
debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {},
} as any;
it('should correctly include .h and related .cpp files using the "match-cpp" preset', async () => {
const initialOpts: IKBotTask = {
path: testDataRoot,
include: ['*.h'],
globExtension: 'match-cpp',
mode: E_Mode.COMPLETION,
prompt: 'test-prompt-preset-match-cpp',
logger: mockLogger,
};
// 1. Complete Options
const completedOptions = await complete_options(initialOpts);
delete initialOpts.client;
expect(completedOptions).not.toBeNull();
if (!completedOptions) return; // Guard for type safety
// 2. Complete Messages (this is where glob and globExtension are applied)
const { messages: gatheredMessages, files: gatheredFiles } = await complete_messages(initialOpts, completedOptions);
expect(gatheredMessages).toBeInstanceOf(Array);
expect(gatheredMessages.length).toBeGreaterThan(0); // Prefs, Prompt + Files
// 3. Complete Params
const finalParams = await complete_params(completedOptions, gatheredMessages);
expect(finalParams.messages).toBeInstanceOf(Array);
// 4. Assert on finalParams.messages
const collectedPathsFromFinalParams: string[] = finalParams.messages
.filter((msg: any) => msg.role === 'user' && (typeof msg.content === 'string' || msg.path))
.map((msg: any) => {
const contentStr = typeof msg.content === 'string' ? msg.content : '';
const metaPathMatch = contentStr.match(/^File: (.*)$/m);
if (metaPathMatch && metaPathMatch[1]) {
const metaRelPath = metaPathMatch[1];
return path.normalize(path.resolve(testDataRoot, metaRelPath));
}
if (msg.path && typeof msg.path === 'string') {
// Assuming msg.path from complete_messages is relative to initialOpts.path (testDataRoot)
return path.normalize(path.resolve(testDataRoot, msg.path));
}
return null;
})
.filter((p: string | null) => p !== null) as string[];
const collectedPathsSet = new Set(collectedPathsFromFinalParams);
// console.log("Expected paths:", expectedAbsoluteFilePaths);
// console.log("Collected paths from finalParams.messages:", collectedPathsFromFinalParams);
expectedAbsoluteFilePaths.forEach(expectedFile => {
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be in finalParams.messages.`).toBe(true);
});
expect(collectedPathsSet.size, "Number of unique collected files in finalParams.messages should match expected").toBe(expectedAbsoluteFilePaths.length);
}, 10000);
});
// To run this test, you would typically use your npm script, e.g., `npm run vi-test` or `npx vitest`

View File

@ -19198,6 +19198,531 @@
"duration": 5277,
"reason": "Expected [], but got it seems there is an issue with accessing the files in the specified directory. please ensure that the directory path is correct and try again. if there's anything else you'd like to check or update, let me know.",
"category": "tools"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:28:32.187Z",
"passed": true,
"duration": 1631,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:28:32.880Z",
"passed": true,
"duration": 678,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:28:33.428Z",
"passed": true,
"duration": 534,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:28:33.992Z",
"passed": true,
"duration": 551,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:28:34.567Z",
"passed": true,
"duration": 561,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:28:35.213Z",
"passed": true,
"duration": 633,
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "unknown",
"router": "openrouter",
"timestamp": "2025-06-03T21:28:35.226Z",
"passed": false,
"duration": 0,
"error": {
"message": "__vite_ssr_import_11__.isWebUrl is not a function",
"code": "UNKNOWN",
"type": "TypeError",
"details": {
"stack": "TypeError: __vite_ssr_import_11__.isWebUrl is not a function\n at C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:87\n at Array.filter (<anonymous>)\n at Module.run (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:44)\n at Module.runTest (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\commons.ts:191:7)\n at __vite_ssr_import_0__.it.each.timeout (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\basic.test.ts:57:26)\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:633:57\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:146:14\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:533:11\n at runWithTimeout (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:39:7)\n at runTest (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:1056:17)",
"message": "__vite_ssr_import_11__.isWebUrl is not a function"
}
},
"reason": "__vite_ssr_import_11__.isWebUrl is not a function",
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "unknown",
"router": "openrouter",
"timestamp": "2025-06-03T21:28:35.242Z",
"passed": false,
"duration": 0,
"error": {
"message": "__vite_ssr_import_11__.isWebUrl is not a function",
"code": "UNKNOWN",
"type": "TypeError",
"details": {
"stack": "TypeError: __vite_ssr_import_11__.isWebUrl is not a function\n at C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:87\n at Array.filter (<anonymous>)\n at Module.run (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:44)\n at Module.runTest (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\commons.ts:191:7)\n at __vite_ssr_import_0__.it.each.timeout (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\basic.test.ts:57:26)\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:633:57\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:146:14\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:533:11\n at runWithTimeout (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:39:7)\n at runTest (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:1056:17)",
"message": "__vite_ssr_import_11__.isWebUrl is not a function"
}
},
"reason": "__vite_ssr_import_11__.isWebUrl is not a function",
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:27.198Z",
"passed": true,
"duration": 2867,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:28.171Z",
"passed": true,
"duration": 958,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:28.724Z",
"passed": true,
"duration": 539,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:29.457Z",
"passed": true,
"duration": 719,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:30.238Z",
"passed": true,
"duration": 768,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:30.779Z",
"passed": true,
"duration": 528,
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:31.490Z",
"passed": false,
"duration": 699,
"reason": "Model returned empty response",
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [
"Yes"
],
"expected": "yes",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:36.073Z",
"passed": true,
"duration": 4567,
"category": "basic"
},
{
"test": "file-inclusion",
"prompt": "What is the name of the algorithm implemented in these files? Return only the name.",
"result": [
"bubble sort"
],
"expected": "bubble sort",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:59.976Z",
"passed": true,
"duration": 868,
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the name of the algorithm implemented in these files? Return only the name.",
"result": [
"Bubble Sort"
],
"expected": "bubble sort",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:01.654Z",
"passed": true,
"duration": 1663,
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the name of the algorithm implemented in these files? Return only the name.",
"result": [
"Bubble Sort\n"
],
"expected": "bubble sort",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:04.140Z",
"passed": true,
"duration": 2473,
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "List all algorithms implemented in these files, as JSON array.",
"result": [
"[\"factorial\", \"bubbleSort\"]"
],
"expected": "[\"bubble sort\",\"factorial\"]",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:31:04.923Z",
"passed": false,
"duration": 770,
"reason": "Expected [\"bubble sort\",\"factorial\"], but got [\"factorial\", \"bubblesort\"]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "List all algorithms implemented in these files, as JSON array.",
"result": [
"[\n {\n \"Algorithm\": \"Factorial Calculation\",\n \"Type\": \"Mathematical Recursive Function\",\n \"Filename/Code Snippet\": \"factorial.js\" (inferred, as no filename was provided)\n },\n {\n \"Algorithm\": \"Bubble Sort\",\n \"Type\": \"Sorting Algorithm (Comparative, Adaptive, Stable)\",\n \"Filename/Code Snippet\": \"bubbleSort.js\" (inferred, as no filename was provided)\n }\n]"
],
"expected": "[\"bubble sort\",\"factorial\"]",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:11.688Z",
"passed": false,
"duration": 6752,
"reason": "Expected [\"bubble sort\",\"factorial\"], but got [\n {\n \"algorithm\": \"factorial calculation\",\n \"type\": \"mathematical recursive function\",\n \"filename/code snippet\": \"factorial.js\" (inferred, as no filename was provided)\n },\n {\n \"algorithm\": \"bubble sort\",\n \"type\": \"sorting algorithm (comparative, adaptive, stable)\",\n \"filename/code snippet\": \"bubblesort.js\" (inferred, as no filename was provided)\n }\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "List all algorithms implemented in these files, as JSON array.",
"result": [
"[\n \"factorial\",\n \"bubbleSort\"\n]"
],
"expected": "[\"bubble sort\",\"factorial\"]",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:14.676Z",
"passed": false,
"duration": 2974,
"reason": "Expected [\"bubble sort\",\"factorial\"], but got [\n \"factorial\",\n \"bubblesort\"\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the title of the product in data.json? Return only the title.",
"result": [
"Injection Barrel"
],
"expected": "Injection Barrel",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:31:15.351Z",
"passed": false,
"duration": 661,
"reason": "Expected Injection Barrel, but got injection barrel",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the title of the product in data.json? Return only the title.",
"result": [
"Injection Barrel"
],
"expected": "Injection Barrel",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:18.353Z",
"passed": false,
"duration": 2989,
"reason": "Expected Injection Barrel, but got injection barrel",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the title of the product in data.json? Return only the title.",
"result": [
"Injection Barrel\n"
],
"expected": "Injection Barrel",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:19.922Z",
"passed": false,
"duration": 1554,
"reason": "Expected Injection Barrel, but got injection barrel",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What animals are shown in these images? Return as JSON array.",
"result": [
"[\n \"cat\",\n \"fox\"\n]"
],
"expected": "[\"cat\",\"fox\"]",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:31:22.978Z",
"passed": false,
"duration": 3043,
"reason": "Expected [\"cat\",\"fox\"], but got [\n \"cat\",\n \"fox\"\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What animals are shown in these images? Return as JSON array.",
"result": [],
"expected": "[\"cat\",\"fox\"]",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:34.164Z",
"passed": false,
"duration": 11172,
"reason": "Model returned empty response",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What animals are shown in these images? Return as JSON array.",
"result": [
"[\n \"cat\",\n \"fox\"\n]"
],
"expected": "[\"cat\",\"fox\"]",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:37.393Z",
"passed": false,
"duration": 3214,
"reason": "Expected [\"cat\",\"fox\"], but got [\n \"cat\",\n \"fox\"\n]",
"category": "files"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:32:56.625Z",
"passed": true,
"duration": 783,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:32:57.299Z",
"passed": true,
"duration": 657,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:32:57.878Z",
"passed": true,
"duration": 566,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:32:58.561Z",
"passed": true,
"duration": 670,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:33:00.962Z",
"passed": true,
"duration": 2385,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:33:01.584Z",
"passed": true,
"duration": 609,
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:33:01.887Z",
"passed": false,
"duration": 290,
"reason": "Model returned empty response",
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [
"yes"
],
"expected": "yes",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:33:09.183Z",
"passed": true,
"duration": 7277,
"category": "basic"
}
],
"highscores": [
@ -19286,8 +19811,8 @@
},
{
"model": "openai/gpt-4o-mini",
"duration": 741,
"duration_secs": 0.741
"duration": 657,
"duration_secs": 0.657
}
]
},
@ -19300,9 +19825,9 @@
"duration_secs": 0.406
},
{
"model": "openai/gpt-4o-mini",
"duration": 666,
"duration_secs": 0.666
"model": "openai/gpt-3.5-turbo",
"duration": 566,
"duration_secs": 0.566
}
]
},
@ -19316,8 +19841,8 @@
},
{
"model": "openai/gpt-4o-mini",
"duration": 905,
"duration_secs": 0.905
"duration": 609,
"duration_secs": 0.609
}
]
},
@ -19450,9 +19975,9 @@
"duration_secs": 0.794
},
{
"model": "google/gemini-2.0-flash-exp:free",
"duration": 2016,
"duration_secs": 2.016
"model": "openrouter/quasar-alpha",
"duration": 2346,
"duration_secs": 2.346
}
]
},
@ -19614,15 +20139,15 @@
{
"test": "web_content",
"rankings": [
{
"model": "unknown",
"duration": 0,
"duration_secs": 0
},
{
"model": "deepseek/deepseek-r1-distill-qwen-14b:free",
"duration": 261,
"duration_secs": 0.261
},
{
"model": "anthropic/claude-3.5-sonnet",
"duration": 3226,
"duration_secs": 3.226
}
]
},
@ -19672,5 +20197,5 @@
]
}
],
"lastUpdated": "2025-04-18T07:47:33.710Z"
"lastUpdated": "2025-06-03T21:33:09.190Z"
}

View File

@ -3071,6 +3071,356 @@
"passed": true,
"duration": 733,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:28:32.187Z",
"passed": true,
"duration": 1631,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:28:32.880Z",
"passed": true,
"duration": 678,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:28:33.428Z",
"passed": true,
"duration": 534,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:28:33.992Z",
"passed": true,
"duration": 551,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:28:34.567Z",
"passed": true,
"duration": 561,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:28:35.213Z",
"passed": true,
"duration": 633,
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "unknown",
"router": "openrouter",
"timestamp": "2025-06-03T21:28:35.226Z",
"passed": false,
"duration": 0,
"error": {
"message": "__vite_ssr_import_11__.isWebUrl is not a function",
"code": "UNKNOWN",
"type": "TypeError",
"details": {
"stack": "TypeError: __vite_ssr_import_11__.isWebUrl is not a function\n at C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:87\n at Array.filter (<anonymous>)\n at Module.run (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:44)\n at Module.runTest (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\commons.ts:191:7)\n at __vite_ssr_import_0__.it.each.timeout (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\basic.test.ts:57:26)\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:633:57\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:146:14\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:533:11\n at runWithTimeout (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:39:7)\n at runTest (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:1056:17)",
"message": "__vite_ssr_import_11__.isWebUrl is not a function"
}
},
"reason": "__vite_ssr_import_11__.isWebUrl is not a function",
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "unknown",
"router": "openrouter",
"timestamp": "2025-06-03T21:28:35.242Z",
"passed": false,
"duration": 0,
"error": {
"message": "__vite_ssr_import_11__.isWebUrl is not a function",
"code": "UNKNOWN",
"type": "TypeError",
"details": {
"stack": "TypeError: __vite_ssr_import_11__.isWebUrl is not a function\n at C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:87\n at Array.filter (<anonymous>)\n at Module.run (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\src\\commands\\run.ts:323:44)\n at Module.runTest (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\commons.ts:191:7)\n at __vite_ssr_import_0__.it.each.timeout (C:\\Users\\zx\\Desktop\\polymech\\polymech-mono\\packages\\kbot\\tests\\unit\\basic.test.ts:57:26)\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:633:57\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:146:14\n at file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:533:11\n at runWithTimeout (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:39:7)\n at runTest (file:///C:/Users/zx/Desktop/polymech/polymech-mono/packages/kbot/node_modules/@vitest/runner/dist/index.js:1056:17)",
"message": "__vite_ssr_import_11__.isWebUrl is not a function"
}
},
"reason": "__vite_ssr_import_11__.isWebUrl is not a function",
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:27.198Z",
"passed": true,
"duration": 2867,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:28.171Z",
"passed": true,
"duration": 958,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:28.724Z",
"passed": true,
"duration": 539,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:29.457Z",
"passed": true,
"duration": 719,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:30.238Z",
"passed": true,
"duration": 768,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:30.779Z",
"passed": true,
"duration": 528,
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:30:31.490Z",
"passed": false,
"duration": 699,
"reason": "Model returned empty response",
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [
"Yes"
],
"expected": "yes",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:36.073Z",
"passed": true,
"duration": 4567,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:32:56.625Z",
"passed": true,
"duration": 783,
"category": "basic"
},
{
"test": "addition",
"prompt": "add 5 and 3. Return only the number, no explanation.",
"result": [
"8"
],
"expected": "8",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:32:57.299Z",
"passed": true,
"duration": 657,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:32:57.878Z",
"passed": true,
"duration": 566,
"category": "basic"
},
{
"test": "multiplication",
"prompt": "multiply 8 and 3. Return only the number, no explanation.",
"result": [
"24"
],
"expected": "24",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:32:58.561Z",
"passed": true,
"duration": 670,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:33:00.962Z",
"passed": true,
"duration": 2385,
"category": "basic"
},
{
"test": "division",
"prompt": "divide 15 by 3. Return only the number, no explanation.",
"result": [
"5"
],
"expected": "5",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:33:01.584Z",
"passed": true,
"duration": 609,
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [],
"expected": "yes",
"model": "openai/gpt-3.5-turbo",
"router": "openai/gpt-3.5-turbo",
"timestamp": "2025-06-03T21:33:01.887Z",
"passed": false,
"duration": 290,
"reason": "Model returned empty response",
"category": "basic"
},
{
"test": "web_content",
"prompt": "Check if the content contains a section about Human prehistory. Reply with \"yes\" if it does, \"no\" if it does not.",
"result": [
"yes"
],
"expected": "yes",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:33:09.183Z",
"passed": true,
"duration": 7277,
"category": "basic"
}
],
"highscores": [
@ -3084,8 +3434,8 @@
},
{
"model": "openai/gpt-4o-mini",
"duration": 741,
"duration_secs": 0.741
"duration": 657,
"duration_secs": 0.657
}
]
},
@ -3098,9 +3448,9 @@
"duration_secs": 0.406
},
{
"model": "openai/gpt-4o-mini",
"duration": 666,
"duration_secs": 0.666
"model": "openai/gpt-3.5-turbo",
"duration": 566,
"duration_secs": 0.566
}
]
},
@ -3114,26 +3464,26 @@
},
{
"model": "openai/gpt-4o-mini",
"duration": 905,
"duration_secs": 0.905
"duration": 609,
"duration_secs": 0.609
}
]
},
{
"test": "web_content",
"rankings": [
{
"model": "unknown",
"duration": 0,
"duration_secs": 0
},
{
"model": "deepseek/deepseek-r1-distill-qwen-14b:free",
"duration": 261,
"duration_secs": 0.261
},
{
"model": "anthropic/claude-3.5-sonnet",
"duration": 3226,
"duration_secs": 3.226
}
]
}
],
"lastUpdated": "2025-04-18T07:47:08.344Z"
"lastUpdated": "2025-06-03T21:33:09.184Z"
}

View File

@ -6,14 +6,14 @@
| Test | Model | Duration (ms) | Duration (s) |
|------|-------|--------------|--------------|
| addition | openai/gpt-4o-mini | 1162 | 1.16 |
| addition | openai/gpt-3.5-turbo | 2646 | 2.65 |
| multiplication | openai/gpt-4o-mini | 666 | 0.67 |
| multiplication | openai/gpt-3.5-turbo | 958 | 0.96 |
| division | openai/gpt-4o-mini | 905 | 0.91 |
| division | openai/gpt-3.5-turbo | 1096 | 1.10 |
| web_content | openai/gpt-3.5-turbo | 3306 | 3.31 |
| web_content | openai/gpt-4o-mini | 7600 | 7.60 |
| addition | openai/gpt-4o-mini | 657 | 0.66 |
| addition | openai/gpt-3.5-turbo | 783 | 0.78 |
| multiplication | openai/gpt-3.5-turbo | 566 | 0.57 |
| multiplication | openai/gpt-4o-mini | 670 | 0.67 |
| division | openai/gpt-4o-mini | 609 | 0.61 |
| division | openai/gpt-3.5-turbo | 2385 | 2.38 |
| web_content | openai/gpt-3.5-turbo | 290 | 0.29 |
| web_content | openai/gpt-4o-mini | 7277 | 7.28 |
## Summary
@ -21,7 +21,7 @@
- Passed: 7
- Failed: 1
- Success Rate: 87.50%
- Average Duration: 2292ms (2.29s)
- Average Duration: 1655ms (1.65s)
## Failed Tests
@ -30,9 +30,9 @@
- Prompt: `Check if the content contains a section about Human prehistory. Reply with "yes" if it does, "no" if it does not.`
- Expected: `yes`
- Actual: ``
- Duration: 3306ms (3.31s)
- Duration: 290ms (0.29s)
- Reason: Model returned empty response
- Timestamp: 4/18/2025, 8:48:00 AM
- Timestamp: 6/3/2025, 11:33:01 PM
## Passed Tests
@ -41,54 +41,54 @@
- Prompt: `add 5 and 3. Return only the number, no explanation.`
- Expected: `8`
- Actual: `8`
- Duration: 2646ms (2.65s)
- Timestamp: 4/18/2025, 8:47:52 AM
- Duration: 783ms (0.78s)
- Timestamp: 6/3/2025, 11:32:56 PM
### addition - openai/gpt-4o-mini
- Prompt: `add 5 and 3. Return only the number, no explanation.`
- Expected: `8`
- Actual: `8`
- Duration: 1162ms (1.16s)
- Timestamp: 4/18/2025, 8:47:53 AM
- Duration: 657ms (0.66s)
- Timestamp: 6/3/2025, 11:32:57 PM
### multiplication - openai/gpt-3.5-turbo
- Prompt: `multiply 8 and 3. Return only the number, no explanation.`
- Expected: `24`
- Actual: `24`
- Duration: 958ms (0.96s)
- Timestamp: 4/18/2025, 8:47:54 AM
- Duration: 566ms (0.57s)
- Timestamp: 6/3/2025, 11:32:57 PM
### multiplication - openai/gpt-4o-mini
- Prompt: `multiply 8 and 3. Return only the number, no explanation.`
- Expected: `24`
- Actual: `24`
- Duration: 666ms (0.67s)
- Timestamp: 4/18/2025, 8:47:55 AM
- Duration: 670ms (0.67s)
- Timestamp: 6/3/2025, 11:32:58 PM
### division - openai/gpt-3.5-turbo
- Prompt: `divide 15 by 3. Return only the number, no explanation.`
- Expected: `5`
- Actual: `5`
- Duration: 1096ms (1.10s)
- Timestamp: 4/18/2025, 8:47:56 AM
- Duration: 2385ms (2.38s)
- Timestamp: 6/3/2025, 11:33:00 PM
### division - openai/gpt-4o-mini
- Prompt: `divide 15 by 3. Return only the number, no explanation.`
- Expected: `5`
- Actual: `5`
- Duration: 905ms (0.91s)
- Timestamp: 4/18/2025, 8:47:57 AM
- Duration: 609ms (0.61s)
- Timestamp: 6/3/2025, 11:33:01 PM
### web_content - openai/gpt-4o-mini
- Prompt: `Check if the content contains a section about Human prehistory. Reply with "yes" if it does, "no" if it does not.`
- Expected: `yes`
- Actual: `yes`
- Duration: 7600ms (7.60s)
- Timestamp: 4/18/2025, 8:48:08 AM
- Duration: 7277ms (7.28s)
- Timestamp: 6/3/2025, 11:33:09 PM

View File

@ -2725,6 +2725,181 @@
"duration": 2016,
"reason": "Expected [\"cat\",\"fox\"], but got [\n \"cat\",\n \"fox\"\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the name of the algorithm implemented in these files? Return only the name.",
"result": [
"bubble sort"
],
"expected": "bubble sort",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:30:59.976Z",
"passed": true,
"duration": 868,
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the name of the algorithm implemented in these files? Return only the name.",
"result": [
"Bubble Sort"
],
"expected": "bubble sort",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:01.654Z",
"passed": true,
"duration": 1663,
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the name of the algorithm implemented in these files? Return only the name.",
"result": [
"Bubble Sort\n"
],
"expected": "bubble sort",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:04.140Z",
"passed": true,
"duration": 2473,
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "List all algorithms implemented in these files, as JSON array.",
"result": [
"[\"factorial\", \"bubbleSort\"]"
],
"expected": "[\"bubble sort\",\"factorial\"]",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:31:04.923Z",
"passed": false,
"duration": 770,
"reason": "Expected [\"bubble sort\",\"factorial\"], but got [\"factorial\", \"bubblesort\"]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "List all algorithms implemented in these files, as JSON array.",
"result": [
"[\n {\n \"Algorithm\": \"Factorial Calculation\",\n \"Type\": \"Mathematical Recursive Function\",\n \"Filename/Code Snippet\": \"factorial.js\" (inferred, as no filename was provided)\n },\n {\n \"Algorithm\": \"Bubble Sort\",\n \"Type\": \"Sorting Algorithm (Comparative, Adaptive, Stable)\",\n \"Filename/Code Snippet\": \"bubbleSort.js\" (inferred, as no filename was provided)\n }\n]"
],
"expected": "[\"bubble sort\",\"factorial\"]",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:11.688Z",
"passed": false,
"duration": 6752,
"reason": "Expected [\"bubble sort\",\"factorial\"], but got [\n {\n \"algorithm\": \"factorial calculation\",\n \"type\": \"mathematical recursive function\",\n \"filename/code snippet\": \"factorial.js\" (inferred, as no filename was provided)\n },\n {\n \"algorithm\": \"bubble sort\",\n \"type\": \"sorting algorithm (comparative, adaptive, stable)\",\n \"filename/code snippet\": \"bubblesort.js\" (inferred, as no filename was provided)\n }\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "List all algorithms implemented in these files, as JSON array.",
"result": [
"[\n \"factorial\",\n \"bubbleSort\"\n]"
],
"expected": "[\"bubble sort\",\"factorial\"]",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:14.676Z",
"passed": false,
"duration": 2974,
"reason": "Expected [\"bubble sort\",\"factorial\"], but got [\n \"factorial\",\n \"bubblesort\"\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the title of the product in data.json? Return only the title.",
"result": [
"Injection Barrel"
],
"expected": "Injection Barrel",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:31:15.351Z",
"passed": false,
"duration": 661,
"reason": "Expected Injection Barrel, but got injection barrel",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the title of the product in data.json? Return only the title.",
"result": [
"Injection Barrel"
],
"expected": "Injection Barrel",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:18.353Z",
"passed": false,
"duration": 2989,
"reason": "Expected Injection Barrel, but got injection barrel",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What is the title of the product in data.json? Return only the title.",
"result": [
"Injection Barrel\n"
],
"expected": "Injection Barrel",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:19.922Z",
"passed": false,
"duration": 1554,
"reason": "Expected Injection Barrel, but got injection barrel",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What animals are shown in these images? Return as JSON array.",
"result": [
"[\n \"cat\",\n \"fox\"\n]"
],
"expected": "[\"cat\",\"fox\"]",
"model": "openai/gpt-4o-mini",
"router": "openai/gpt-4o-mini",
"timestamp": "2025-06-03T21:31:22.978Z",
"passed": false,
"duration": 3043,
"reason": "Expected [\"cat\",\"fox\"], but got [\n \"cat\",\n \"fox\"\n]",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What animals are shown in these images? Return as JSON array.",
"result": [],
"expected": "[\"cat\",\"fox\"]",
"model": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"router": "nvidia/llama-3.3-nemotron-super-49b-v1:free",
"timestamp": "2025-06-03T21:31:34.164Z",
"passed": false,
"duration": 11172,
"reason": "Model returned empty response",
"category": "files"
},
{
"test": "file-inclusion",
"prompt": "What animals are shown in these images? Return as JSON array.",
"result": [
"[\n \"cat\",\n \"fox\"\n]"
],
"expected": "[\"cat\",\"fox\"]",
"model": "google/gemini-2.0-flash-exp:free",
"router": "google/gemini-2.0-flash-exp:free",
"timestamp": "2025-06-03T21:31:37.393Z",
"passed": false,
"duration": 3214,
"reason": "Expected [\"cat\",\"fox\"], but got [\n \"cat\",\n \"fox\"\n]",
"category": "files"
}
],
"highscores": [
@ -2737,12 +2912,12 @@
"duration_secs": 0.794
},
{
"model": "google/gemini-2.0-flash-exp:free",
"duration": 2016,
"duration_secs": 2.016
"model": "openrouter/quasar-alpha",
"duration": 2346,
"duration_secs": 2.346
}
]
}
],
"lastUpdated": "2025-04-18T06:49:35.725Z"
"lastUpdated": "2025-06-03T21:31:37.395Z"
}

View File

@ -6,9 +6,9 @@
| Test | Model | Duration (ms) | Duration (s) |
|------|-------|--------------|--------------|
| file-inclusion | google/gemini-2.0-flash-exp:free | 2016 | 2.02 |
| file-inclusion | openai/gpt-4o-mini | 2392 | 2.39 |
| file-inclusion | nvidia/llama-3.3-nemotron-super-49b-v1:free | 15252 | 15.25 |
| file-inclusion | openai/gpt-4o-mini | 3043 | 3.04 |
| file-inclusion | google/gemini-2.0-flash-exp:free | 3214 | 3.21 |
| file-inclusion | nvidia/llama-3.3-nemotron-super-49b-v1:free | 11172 | 11.17 |
## Summary
@ -16,7 +16,7 @@
- Passed: 3
- Failed: 9
- Success Rate: 25.00%
- Average Duration: 3058ms (3.06s)
- Average Duration: 3178ms (3.18s)
## Failed Tests
@ -24,57 +24,25 @@
- Prompt: `What animals are shown in these images? Return as JSON array.`
- Expected: `["cat","fox"]`
- Actual: `["wildcat", "fox"]`
- Duration: 2392ms (2.39s)
- Reason: Expected ["cat","fox"], but got ["wildcat", "fox"]
- Timestamp: 4/18/2025, 8:49:18 AM
- Actual: `[
"cat",
"fox"
]`
- Duration: 3043ms (3.04s)
- Reason: Expected ["cat","fox"], but got [
"cat",
"fox"
]
- Timestamp: 6/3/2025, 11:31:22 PM
### file-inclusion - nvidia/llama-3.3-nemotron-super-49b-v1:free
- Prompt: `What animals are shown in these images? Return as JSON array.`
- Expected: `["cat","fox"]`
- Actual: `[
{
"Image Number": 1,
"Description": {
"Appearance": "Large, with a long neck and spotted body",
"Actions/Posture": "Eating leaves from a tall tree",
"Environment": "Savannah",
"Guess": "Giraffe"
}
},
{
"Image Number": 2,
"Description": {
"Appearance": "Small, furry, with big round eyes",
"Actions/Posture": "Sitting on a couch",
"Environment": "Domestic",
"Guess": "Cat or Dog, unsure"
}
}
]`
- Duration: 15252ms (15.25s)
- Reason: Expected ["cat","fox"], but got [
{
"image number": 1,
"description": {
"appearance": "large, with a long neck and spotted body",
"actions/posture": "eating leaves from a tall tree",
"environment": "savannah",
"guess": "giraffe"
}
},
{
"image number": 2,
"description": {
"appearance": "small, furry, with big round eyes",
"actions/posture": "sitting on a couch",
"environment": "domestic",
"guess": "cat or dog, unsure"
}
}
]
- Timestamp: 4/18/2025, 8:49:33 AM
- Actual: ``
- Duration: 11172ms (11.17s)
- Reason: Model returned empty response
- Timestamp: 6/3/2025, 11:31:34 PM
### file-inclusion - google/gemini-2.0-flash-exp:free
@ -84,12 +52,12 @@
"cat",
"fox"
]`
- Duration: 2016ms (2.02s)
- Duration: 3214ms (3.21s)
- Reason: Expected ["cat","fox"], but got [
"cat",
"fox"
]
- Timestamp: 4/18/2025, 8:49:35 AM
- Timestamp: 6/3/2025, 11:31:37 PM
## Passed Tests