glob extensions | cpp preset
This commit is contained in:
parent
ddc8df1cc9
commit
ff0220b468
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
12
packages/kbot/dist-in/glob.d.ts
vendored
Normal 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>;
|
||||
};
|
||||
111
packages/kbot/dist-in/glob.js
Normal file
111
packages/kbot/dist-in/glob.js
Normal file
File diff suppressed because one or more lines are too long
1
packages/kbot/dist-in/index.d.ts
vendored
1
packages/kbot/dist-in/index.d.ts
vendored
@ -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';
|
||||
|
||||
@ -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==
|
||||
14
packages/kbot/dist-in/source.d.ts
vendored
14
packages/kbot/dist-in/source.d.ts
vendored
@ -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
1
packages/kbot/dist-in/variables.d.ts
vendored
1
packages/kbot/dist-in/variables.d.ts
vendored
@ -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
4
packages/kbot/dist-in/zod_types.d.ts
vendored
4
packages/kbot/dist-in/zod_types.d.ts
vendored
@ -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
21
packages/kbot/package-lock.json
generated
21
packages/kbot/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
152
packages/kbot/src/glob.ts
Normal 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 }
|
||||
}
|
||||
@ -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> = {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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:
|
||||
|
||||
116
packages/kbot/tests/test-data/glob/PHApp-Modbus.cpp
Normal file
116
packages/kbot/tests/test-data/glob/PHApp-Modbus.cpp
Normal 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
|
||||
401
packages/kbot/tests/test-data/glob/PHApp-Profiles.cpp
Normal file
401
packages/kbot/tests/test-data/glob/PHApp-Profiles.cpp
Normal 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
|
||||
824
packages/kbot/tests/test-data/glob/PHApp.cpp
Normal file
824
packages/kbot/tests/test-data/glob/PHApp.cpp
Normal 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
|
||||
279
packages/kbot/tests/test-data/glob/PHApp.h
Normal file
279
packages/kbot/tests/test-data/glob/PHApp.h
Normal 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
|
||||
272
packages/kbot/tests/test-data/glob/PHAppNetwork.cpp
Normal file
272
packages/kbot/tests/test-data/glob/PHAppNetwork.cpp
Normal 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;
|
||||
}
|
||||
5
packages/kbot/tests/test-data/glob/PHAppSettings.cpp
Normal file
5
packages/kbot/tests/test-data/glob/PHAppSettings.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include "PHApp.h"
|
||||
#include "config.h"
|
||||
#include <ArduinoLog.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
463
packages/kbot/tests/test-data/glob/PHAppWeb.cpp
Normal file
463
packages/kbot/tests/test-data/glob/PHAppWeb.cpp
Normal 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);
|
||||
}
|
||||
@ -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
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
87
packages/kbot/tests/unit/options-glob.test.ts
Normal file
87
packages/kbot/tests/unit/options-glob.test.ts
Normal 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`
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user