Initial commit
This commit is contained in:
commit
852f748633
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/dist
|
||||
/src-capacitor
|
||||
/src-cordova
|
||||
/.quasar
|
||||
/node_modules
|
||||
.eslintrc.js
|
||||
/src-ssr
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
module.exports = {
|
||||
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||
// This option interrupts the configuration hierarchy at this file
|
||||
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||
root: true,
|
||||
|
||||
// https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
|
||||
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
|
||||
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
|
||||
parserOptions: {
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
|
||||
// Rules order is important, please avoid shuffling them
|
||||
extends: [
|
||||
// Base ESLint recommended rules
|
||||
// 'eslint:recommended',
|
||||
|
||||
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
|
||||
// ESLint typescript rules
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
|
||||
// Uncomment any of the lines below to choose desired strictness,
|
||||
// but leave only one uncommented!
|
||||
// See https://eslint.vuejs.org/rules/#available-rules
|
||||
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
||||
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
||||
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||
|
||||
// https://github.com/prettier/eslint-config-prettier#installation
|
||||
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||
'prettier',
|
||||
],
|
||||
|
||||
plugins: [
|
||||
// required to apply rules which need type information
|
||||
'@typescript-eslint',
|
||||
|
||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
|
||||
// required to lint *.vue files
|
||||
'vue',
|
||||
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
||||
// Prettier has not been included as plugin to avoid performance impact
|
||||
// add it as an extension for your IDE
|
||||
],
|
||||
|
||||
globals: {
|
||||
ga: 'readonly', // Google Analytics
|
||||
cordova: 'readonly',
|
||||
__statics: 'readonly',
|
||||
__QUASAR_SSR__: 'readonly',
|
||||
__QUASAR_SSR_SERVER__: 'readonly',
|
||||
__QUASAR_SSR_CLIENT__: 'readonly',
|
||||
__QUASAR_SSR_PWA__: 'readonly',
|
||||
process: 'readonly',
|
||||
Capacitor: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
|
||||
quotes: ['warn', 'single', { avoidEscape: true }],
|
||||
|
||||
// this rule, if on, would require explicit return type on the `render` function
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
|
||||
// in plain CommonJS modules, you can't use `import foo = require('foo')` to pass this rule, so it has to be disabled
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
|
||||
// The core 'no-unused-vars' rules (in the eslint:recommended ruleset)
|
||||
// does not work with type definitions
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
// allow debugger during development only
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.DS_Store
|
||||
.thumbs.db
|
||||
node_modules
|
||||
|
||||
# Quasar core related directories
|
||||
.quasar
|
||||
/dist
|
||||
|
||||
# Cordova related directories and files
|
||||
/src-cordova/node_modules
|
||||
/src-cordova/platforms
|
||||
/src-cordova/plugins
|
||||
/src-cordova/www
|
||||
|
||||
# Capacitor related directories and files
|
||||
/src-capacitor/www
|
||||
/src-capacitor/node_modules
|
||||
|
||||
# BEX related directories and files
|
||||
/src-bex/www
|
||||
/src-bex/js/core
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
||||
.nyc_output
|
||||
coverage/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "@quasar/quasar-app-extension-testing-e2e-cypress/nyc-config-preset"
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"semi": true
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar",
|
||||
"wayou.vscode-todo-highlight"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"octref.vetur",
|
||||
"hookyqr.beautify",
|
||||
"dbaeumer.jshint",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar"
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["cypress.json"],
|
||||
"url": "https://on.cypress.io/cypress.schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# The Biscuit Machine
|
||||
|
||||
This repository contains a Biscuit Machine implementation, as defined in the [enclosed PDF](public/The_biscuit_machine_task.pdf). The implementation is based on the [Quasar framework](https://quasar.dev), and thereby supports various deployment targets. A published live SSR version is available on [https://biscuit-machine.snikolov.me](http://biscuit-machine.snikolov.me).
|
||||
|
||||
Each component's logic is implemented as a composable function within `src/composables`. In particular, the main control system is in `src/composables/biscuitMachine.ts`.
|
||||
|
||||
Additionally, a visualization of the system has been provided, which allows users to interact with the machine and monitor its operation.
|
||||
Its implementation can be found in the `src/components` directory.
|
||||
|
||||
Unit tests are available in the `test/vitest/__tests__` directory and can be run with `yarn test:unit:ci`.
|
||||
|
||||
## Install the dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
# or
|
||||
npm install
|
||||
```
|
||||
|
||||
### Start the app in development mode (hot-code reloading, error reporting, etc.)
|
||||
|
||||
```bash
|
||||
quasar dev
|
||||
```
|
||||
|
||||
### Lint the files
|
||||
|
||||
```bash
|
||||
yarn lint
|
||||
# or
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Format the files
|
||||
|
||||
```bash
|
||||
yarn format
|
||||
# or
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Build the app for production
|
||||
|
||||
```bash
|
||||
quasar build
|
||||
```
|
||||
|
||||
### Customize the configuration
|
||||
|
||||
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= productName %></title>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="<%= productDescription %>" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="128x128"
|
||||
href="icons/favicon-128x128.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="96x96"
|
||||
href="icons/favicon-96x96.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="icons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="icons/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="icon" type="image/ico" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- quasar:entry-point -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "the-buscuit-machine",
|
||||
"version": "0.0.1",
|
||||
"description": "A biscuit machine implementation.",
|
||||
"productName": "The Buscuit Machine",
|
||||
"author": "Stanislav Nikolov <hello@snikolov.me>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.ts,.vue ./",
|
||||
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
|
||||
"test:unit:ui": "vitest --ui",
|
||||
"test:unit": "vitest",
|
||||
"test:unit:ci": "vitest run",
|
||||
"test:unit:coverage": "jest --coverage",
|
||||
"test:unit:watch": "jest --watch",
|
||||
"test:unit:watchAll": "jest --watchAll",
|
||||
"serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788",
|
||||
"concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"",
|
||||
"test:e2e": "cross-env NODE_ENV=test start-test \"quasar dev\" http-get://localhost:9000 \"cypress open\"",
|
||||
"test:e2e:ci": "cross-env NODE_ENV=test start-test \"quasar dev\" http-get://localhost:9000 \"cypress run\"",
|
||||
"test:component": "cross-env NODE_ENV=test cypress open-ct",
|
||||
"test:component:ci": "cross-env NODE_ENV=test cypress run-ct"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.0.0",
|
||||
"pinia": "^2.0.18",
|
||||
"quasar": "^2.6.0",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.0.0",
|
||||
"@quasar/quasar-app-extension-testing": "^2.0.4",
|
||||
"@quasar/quasar-app-extension-testing-unit-jest": "^3.0.0-alpha.10",
|
||||
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.1.2",
|
||||
"@types/node": "^12.20.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"@vitest/ui": "^0.15.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-jest": "^25.2.2",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"majestic": "^1.7.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.5.4",
|
||||
"vitest": "^0.15.0",
|
||||
"@vue/test-utils": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || ^16 || ^14.19",
|
||||
"npm": ">= 6.13.4",
|
||||
"yarn": ">= 1.21.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/* eslint-disable */
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// https://github.com/postcss/autoprefixer
|
||||
require('autoprefixer')({
|
||||
overrideBrowserslist: [
|
||||
'last 4 Chrome versions',
|
||||
'last 4 Firefox versions',
|
||||
'last 4 Edge versions',
|
||||
'last 4 Safari versions',
|
||||
'last 4 Android versions',
|
||||
'last 4 ChromeAndroid versions',
|
||||
'last 4 FirefoxAndroid versions',
|
||||
'last 4 iOS versions',
|
||||
],
|
||||
}),
|
||||
|
||||
// https://github.com/elchininet/postcss-rtlcss
|
||||
// If you want to support RTL css, then
|
||||
// 1. yarn/npm install postcss-rtlcss
|
||||
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||
// 3. uncomment the following line:
|
||||
// require('postcss-rtlcss')
|
||||
],
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 859 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,203 @@
|
|||
/* eslint-env node */
|
||||
|
||||
/*
|
||||
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
|
||||
* the ES6 features that are supported by your Node version. https://node.green/
|
||||
*/
|
||||
|
||||
// Configuration for your app
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||
|
||||
const { configure } = require('quasar/wrappers');
|
||||
|
||||
module.exports = configure(function (/* ctx */) {
|
||||
return {
|
||||
eslint: {
|
||||
// fix: true,
|
||||
// include = [],
|
||||
// exclude = [],
|
||||
// rawOptions = {},
|
||||
warnings: true,
|
||||
errors: true,
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
|
||||
// preFetch: true,
|
||||
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: [],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: ['app.scss'],
|
||||
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
// 'mdi-v5',
|
||||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'line-awesome',
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
|
||||
'roboto-font', // optional, you are not bound to it
|
||||
'material-icons', // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||
build: {
|
||||
target: {
|
||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||
node: 'node16',
|
||||
},
|
||||
|
||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||
// vueRouterBase,
|
||||
// vueDevtools,
|
||||
// vueOptionsAPI: false,
|
||||
|
||||
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||
|
||||
// publicPath: '/',
|
||||
// analyze: true,
|
||||
// env: {},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: false,
|
||||
// polyfillModulePreload: true,
|
||||
// distDir
|
||||
|
||||
// extendViteConf (viteConf) {},
|
||||
// viteVuePluginOptions: {},
|
||||
viteVuePluginOptions: { reactivityTransform: true },
|
||||
|
||||
// vitePlugins: [
|
||||
// [ 'package-name', { ..options.. } ]
|
||||
// ]
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
devServer: {
|
||||
// https: true
|
||||
open: true, // opens browser window automatically
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||
framework: {
|
||||
config: {},
|
||||
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
// lang: 'en-US', // Quasar language pack
|
||||
|
||||
// For special cases outside of where the auto-import strategy can have an impact
|
||||
// (like functional components as one of the examples),
|
||||
// you can manually specify Quasar components/directives to be available everywhere:
|
||||
//
|
||||
// components: [],
|
||||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [],
|
||||
},
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
// https://v2.quasar.dev/options/animations
|
||||
animations: [],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles
|
||||
// sourceFiles: {
|
||||
// rootComponent: 'src/App.vue',
|
||||
// router: 'src/router/index',
|
||||
// store: 'src/store/index',
|
||||
// registerServiceWorker: 'src-pwa/register-service-worker',
|
||||
// serviceWorker: 'src-pwa/custom-service-worker',
|
||||
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||
// electronMain: 'src-electron/electron-main',
|
||||
// electronPreload: 'src-electron/electron-preload'
|
||||
// },
|
||||
sourceFiles: {
|
||||
store: undefined,
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
||||
ssr: {
|
||||
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||
// will mess up SSR
|
||||
|
||||
// extendSSRWebserverConf (esbuildConf) {},
|
||||
// extendPackageJson (json) {},
|
||||
|
||||
pwa: false,
|
||||
|
||||
// manualStoreHydration: true,
|
||||
// manualPostHydrationTrigger: true,
|
||||
|
||||
prodPort: 3000, // The default port that the production server should use
|
||||
// (gets superseded if process.env.PORT is specified at runtime)
|
||||
|
||||
middlewares: [
|
||||
'render', // keep this as last one
|
||||
],
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||
pwa: {
|
||||
workboxMode: 'generateSW', // or 'injectManifest'
|
||||
injectPwaMetaTags: true,
|
||||
swFilename: 'sw.js',
|
||||
manifestFilename: 'manifest.json',
|
||||
useCredentialsForManifestTag: false,
|
||||
// extendGenerateSWOptions (cfg) {}
|
||||
// extendInjectManifestOptions (cfg) {},
|
||||
// extendManifestJson (json) {}
|
||||
// extendPWACustomSWConf (esbuildConf) {}
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
|
||||
cordova: {
|
||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
|
||||
capacitor: {
|
||||
hideSplashscreen: true,
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
||||
electron: {
|
||||
// extendElectronMainConf (esbuildConf)
|
||||
// extendElectronPreloadConf (esbuildConf)
|
||||
|
||||
inspectPort: 5858,
|
||||
|
||||
bundler: 'packager', // 'packager' or 'builder'
|
||||
|
||||
packager: {
|
||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||
// OS X / Mac App Store
|
||||
// appBundleId: '',
|
||||
// appCategoryType: '',
|
||||
// osxSign: '',
|
||||
// protocol: 'myapp://path',
|
||||
// Windows only
|
||||
// win32metadata: { ... }
|
||||
},
|
||||
|
||||
builder: {
|
||||
// https://www.electron.build/configuration/configuration
|
||||
|
||||
appId: 'the-buscuit-machine',
|
||||
},
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||
bex: {
|
||||
contentScripts: ['my-content-script'],
|
||||
|
||||
// extendBexScriptsConf (esbuildConf) {}
|
||||
// extendBexManifestJson (json) {}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"@quasar/testing-unit-vitest": {
|
||||
"options": [
|
||||
"scripts",
|
||||
"typescript",
|
||||
"ui"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"unit-jest": {
|
||||
"runnerCommand": "jest --ci"
|
||||
},
|
||||
"e2e-cypress": {
|
||||
"runnerCommand": "cross-env NODE_ENV=test start-test \"quasar dev\" http-get://localhost:9000 \"cypress run\""
|
||||
},
|
||||
"component-cypress": {
|
||||
"runnerCommand": "cross-env NODE_ENV=test cypress run-ct"
|
||||
},
|
||||
"unit-vitest": {
|
||||
"runnerCommand": "vitest run"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { RenderError } from '@quasar/app-vite';
|
||||
import { ssrMiddleware } from 'quasar/wrappers';
|
||||
|
||||
// This middleware should execute as last one
|
||||
// since it captures everything and tries to
|
||||
// render the page with Vue
|
||||
|
||||
export default ssrMiddleware(({ app, resolve, render, serve }) => {
|
||||
// we capture any other Express route and hand it
|
||||
// over to Vue and Vue Router to render our page
|
||||
app.get(resolve.urlPath('*'), (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
render(/* the ssrContext: */ { req, res })
|
||||
.then((html) => {
|
||||
// now let's send the rendered html to the client
|
||||
res.send(html);
|
||||
})
|
||||
.catch((err: RenderError) => {
|
||||
// oops, we had an error while rendering the page
|
||||
|
||||
// we were told to redirect to another URL
|
||||
if (err.url) {
|
||||
if (err.code) {
|
||||
res.redirect(err.code, err.url);
|
||||
} else {
|
||||
res.redirect(err.url);
|
||||
}
|
||||
} else if (err.code === 404) {
|
||||
// hmm, Vue Router could not find the requested route
|
||||
|
||||
// Should reach here only if no "catch-all" route
|
||||
// is defined in /src/routes
|
||||
res.status(404).send('404 | Page Not Found');
|
||||
} else if (process.env.DEV) {
|
||||
// well, we treat any other code as error;
|
||||
// if we're in dev mode, then we can use Quasar CLI
|
||||
// to display a nice error page that contains the stack
|
||||
// and other useful information
|
||||
|
||||
// serve.error is available on dev only
|
||||
serve.error({ err, req, res });
|
||||
} else {
|
||||
// we're in production, so we should have another method
|
||||
// to display something to the client when we encounter an error
|
||||
// (for security reasons, it's not ok to display the same wealth
|
||||
// of information as we do in development)
|
||||
|
||||
// Render Error Page on production or
|
||||
// create a route (/src/routes) for an error page and redirect to it
|
||||
res.status(500).send('500 | Internal Server Error');
|
||||
// console.error(err.stack)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* More info about this file:
|
||||
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
|
||||
*
|
||||
* Runs in Node context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Make sure to yarn add / npm install (in your project root)
|
||||
* anything you import here (except for express and compression).
|
||||
*/
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import {
|
||||
ssrClose,
|
||||
ssrCreate,
|
||||
ssrListen,
|
||||
ssrRenderPreloadTag,
|
||||
ssrServeStaticContent,
|
||||
} from 'quasar/wrappers';
|
||||
|
||||
/**
|
||||
* Create your webserver and return its instance.
|
||||
* If needed, prepare your webserver to receive
|
||||
* connect-like middlewares.
|
||||
*
|
||||
* Should NOT be async!
|
||||
*/
|
||||
export const create = ssrCreate((/* { ... } */) => {
|
||||
const app = express();
|
||||
|
||||
// attackers can use this header to detect apps running Express
|
||||
// and then launch specifically-targeted attacks
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// place here any middlewares that
|
||||
// absolutely need to run before anything else
|
||||
if (process.env.PROD) {
|
||||
app.use(compression());
|
||||
}
|
||||
|
||||
return app;
|
||||
});
|
||||
|
||||
/**
|
||||
* You need to make the server listen to the indicated port
|
||||
* and return the listening instance or whatever you need to
|
||||
* close the server with.
|
||||
*
|
||||
* The "listenResult" param for the "close()" definition below
|
||||
* is what you return here.
|
||||
*
|
||||
* For production, you can instead export your
|
||||
* handler for serverless use or whatever else fits your needs.
|
||||
*/
|
||||
export const listen = ssrListen(async ({ app, port, isReady }) => {
|
||||
await isReady();
|
||||
return app.listen(port, () => {
|
||||
if (process.env.PROD) {
|
||||
console.log('Server listening at port ' + port);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Should close the server and free up any resources.
|
||||
* Will be used on development only when the server needs
|
||||
* to be rebooted.
|
||||
*
|
||||
* Should you need the result of the "listen()" call above,
|
||||
* you can use the "listenResult" param.
|
||||
*
|
||||
* Can be async.
|
||||
*/
|
||||
export const close = ssrClose(({ listenResult }) => {
|
||||
return listenResult.close();
|
||||
});
|
||||
|
||||
const maxAge = process.env.DEV ? 0 : 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* Should return middleware that serves the indicated path
|
||||
* with static content.
|
||||
*/
|
||||
export const serveStaticContent = ssrServeStaticContent((path, opts) => {
|
||||
return express.static(path, {
|
||||
maxAge,
|
||||
...opts,
|
||||
});
|
||||
});
|
||||
|
||||
const jsRE = /\.js$/;
|
||||
const cssRE = /\.css$/;
|
||||
const woffRE = /\.woff$/;
|
||||
const woff2RE = /\.woff2$/;
|
||||
const gifRE = /\.gif$/;
|
||||
const jpgRE = /\.jpe?g$/;
|
||||
const pngRE = /\.png$/;
|
||||
|
||||
/**
|
||||
* Should return a String with HTML output
|
||||
* (if any) for preloading indicated file
|
||||
*/
|
||||
export const renderPreloadTag = ssrRenderPreloadTag((file) => {
|
||||
if (jsRE.test(file) === true) {
|
||||
return `<link rel="modulepreload" href="${file}" crossorigin>`;
|
||||
}
|
||||
|
||||
if (cssRE.test(file) === true) {
|
||||
return `<link rel="stylesheet" href="${file}">`;
|
||||
}
|
||||
|
||||
if (woffRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`;
|
||||
}
|
||||
|
||||
if (woff2RE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`;
|
||||
}
|
||||
|
||||
if (gifRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/gif">`;
|
||||
}
|
||||
|
||||
if (jpgRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/jpeg">`;
|
||||
}
|
||||
|
||||
if (pngRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/png">`;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import "quasar/dist/types/feature-flag";
|
||||
|
||||
declare module "quasar/dist/types/feature-flag" {
|
||||
interface QuasarFeatureFlags {
|
||||
ssr: true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
|
||||
<path
|
||||
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
|
||||
<path fill="#050A14"
|
||||
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
|
||||
<path fill="#050A14"
|
||||
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
|
||||
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div>
|
||||
<MainSwitch @position-changed="(p) => (mainSwitchPosition = p)" />
|
||||
|
||||
<BiscuitMachineVisualization :is-extruding="isExtruding" :is-stamping="isStamping" :is-heater-on-in="isHeaterOnIn"
|
||||
:target-temperature="targetTemperature" :current-temperature-in="currentTemperature" :motor-on="motorOn"
|
||||
:motor-pulse-out="motorPulseOut" :in-target-zone="inTargetZoneOut" :machine-state="machineState" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import MainSwitch, { BiscuitSwitchState } from './BiscuitSwitch.vue';
|
||||
import { useTimeSource } from 'src/composables/timeSource';
|
||||
import { useBiscuitOven } from 'src/composables/biscuitOven';
|
||||
import { useMotor } from 'src/composables/motor';
|
||||
import { useThermostat } from 'src/composables/thermostat';
|
||||
import { useBiscuitMachine } from 'src/composables/biscuitMachine';
|
||||
import { usePulseActivatedTool } from 'src/composables/pulseActivatedTool';
|
||||
import BiscuitMachineVisualization from './BiscuitMachineVisualization.vue';
|
||||
|
||||
// inputs
|
||||
const inTargetZoneIn = ref(false);
|
||||
const motorPulseIn = ref(false);
|
||||
const mainSwitchPosition = ref<BiscuitSwitchState>('off');
|
||||
|
||||
const {
|
||||
motorOn,
|
||||
extruderPulseOut,
|
||||
stamperPulseOut,
|
||||
targetTemperature,
|
||||
machineState,
|
||||
} = useBiscuitMachine(mainSwitchPosition, inTargetZoneIn, motorPulseIn);
|
||||
|
||||
// Time source
|
||||
const { tickIsOn } = useTimeSource(1000);
|
||||
// Motor
|
||||
const { pulse: motorPulseOut } = useMotor(tickIsOn, motorOn);
|
||||
// Oven
|
||||
const isHeaterOnIn = ref(false);
|
||||
const { currentTemperature } = useBiscuitOven(tickIsOn, isHeaterOnIn);
|
||||
// Heating Manager
|
||||
const { inTargetZoneOut, isHeaterOnOut } = useThermostat(
|
||||
targetTemperature,
|
||||
currentTemperature
|
||||
);
|
||||
// Extruder
|
||||
const { isActive: isExtruding } = usePulseActivatedTool(extruderPulseOut, 700);
|
||||
// Stamper
|
||||
const { isActive: isStamping } = usePulseActivatedTool(stamperPulseOut, 300);
|
||||
|
||||
// Connect signals that can't be straightforwardly connected due to circular dependencies
|
||||
watchEffect(() => {
|
||||
motorPulseIn.value = motorPulseOut.value;
|
||||
inTargetZoneIn.value = inTargetZoneOut.value;
|
||||
isHeaterOnIn.value = isHeaterOnOut.value;
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
<template>
|
||||
<h6>Visualization</h6>
|
||||
<div class="row flex-center q-pa-md items-stretch">
|
||||
<q-card class="col-5 col-sm-2 q-ma-md">
|
||||
<q-card-section>
|
||||
<q-item-label header>Machine State</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ props.machineState.toUpperCase() }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
|
||||
<q-card class="col-5 col-sm-2 q-ma-md">
|
||||
<q-card-section>
|
||||
<q-item-label header>Thermostat</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Target</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{
|
||||
targetTemperature ?? 'not set'
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Within Range</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{
|
||||
!targetTemperature ? 'N/A' : inTargetZone ? 'YES' : 'NO'
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
|
||||
<q-card class="col-5 col-sm-2 q-ma-md">
|
||||
<q-card-section>
|
||||
<q-item-label header>Motor</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Power</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{ motorOn ? 'ON' : 'OFF' }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Pulse Out</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{
|
||||
motorPulseOutUi ? 'YES' : ''
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
|
||||
<q-card class="col-5 col-sm-2 q-ma-md">
|
||||
<q-card-section>
|
||||
<q-item-label header>Biscuit Container</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Biscuits</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{ numReadyBiscuits }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-markup-table class="q-pa-md" separator="cell">
|
||||
<tbody>
|
||||
<tr>
|
||||
<template v-for="(_, i) in BELT_SIZE - 1" :key="i">
|
||||
<td v-if="i == EXTRUDER_POSITION">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-item-label header>Extruder</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>{{
|
||||
props.isExtruding ? 'Extruding...' : 'Idle'
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</td>
|
||||
<td v-else-if="i == STAMPER_POSITION">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-item-label header>Stamper</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>{{
|
||||
props.isStamping ? 'Stamping...' : 'Idle'
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</td>
|
||||
<td v-else-if="i == OVEN_POSITION" colspan="2">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-item-label header>Oven</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Heater</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{
|
||||
isHeaterOnIn ? 'ON' : 'OFF'
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Temperature</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-item-label caption>{{ currentTemperatureIn }}°C</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
</td>
|
||||
<td v-else></td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-for="(item, index) in items" :key="index">
|
||||
<span v-if="item == 'empty'"></span>
|
||||
<q-icon v-else-if="item == 'dough'" :name="matFilterDrama" size="xl" color="deep-orange-2" />
|
||||
<q-icon v-else-if="item == 'raw biscuit'" :name="outlinedCookie" size="xl" color="deep-orange-2" />
|
||||
<q-icon v-else-if="item == 'almost biscuit'" :name="outlinedCookie" size="xl" color="deep-orange-4" />
|
||||
<q-icon v-else-if="item == 'biscuit'" :name="outlinedCookie" size="xl" color="brown-10" />
|
||||
<span v-else>
|
||||
{{ item }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-for="(item, index) in conveyorItems" :key="index">
|
||||
<template v-for="i in 3" v-bind:key="i">
|
||||
<q-icon v-if="item == '>'" name="arrow_circle_right" size="lg" color="primary" />
|
||||
<q-icon v-else name="do_not_disturb_on" size="lg" color="primary" />
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { QMarkupTable } from 'quasar';
|
||||
import { matFilterDrama } from '@quasar/extras/material-icons';
|
||||
import { outlinedCookie } from '@quasar/extras/material-icons-outlined';
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
import { usePulseActivatedTool } from 'src/composables/pulseActivatedTool';
|
||||
import {
|
||||
BELT_SIZE,
|
||||
STAMPER_POSITION,
|
||||
EXTRUDER_POSITION,
|
||||
OVEN_POSITION,
|
||||
MachineState,
|
||||
} from 'src/composables/biscuitMachine';
|
||||
|
||||
const props = defineProps<{
|
||||
isExtruding: boolean;
|
||||
isStamping: boolean;
|
||||
isHeaterOnIn: boolean;
|
||||
motorOn: boolean;
|
||||
currentTemperatureIn: number;
|
||||
targetTemperature: number | 'none';
|
||||
motorPulseOut: boolean;
|
||||
inTargetZone: boolean;
|
||||
machineState: MachineState;
|
||||
}>();
|
||||
|
||||
type BeltItem = 'empty' | 'dough' | 'raw biscuit' | 'almost biscuit' | 'biscuit';
|
||||
|
||||
const numReadyBiscuits = ref(0);
|
||||
const items = ref(Array(BELT_SIZE).fill('empty') as BeltItem[]);
|
||||
const conveyorItems = ref(
|
||||
Array(BELT_SIZE)
|
||||
.fill('')
|
||||
.map((_, i) => (i % 2 == 0 ? '>' : '-'))
|
||||
);
|
||||
|
||||
const { isExtruding, isStamping, motorPulseOut } = toRefs(props);
|
||||
|
||||
const { isActive: motorPulseOutUi } = usePulseActivatedTool(motorPulseOut, 400)
|
||||
|
||||
// Move belt
|
||||
watch(motorPulseOut, (p) => {
|
||||
if (!p) return;
|
||||
|
||||
const lastConveyorItem = conveyorItems.value.pop() ?? '-';
|
||||
conveyorItems.value = [lastConveyorItem, ...conveyorItems.value];
|
||||
|
||||
const lastItem = items.value.pop();
|
||||
items.value = ['empty' as BeltItem].concat(items.value);
|
||||
|
||||
if (lastItem == 'biscuit') {
|
||||
numReadyBiscuits.value++;
|
||||
}
|
||||
});
|
||||
|
||||
// Extrude dough
|
||||
watch(isExtruding, (e) => {
|
||||
if (e) return;
|
||||
items.value[EXTRUDER_POSITION] = 'dough';
|
||||
});
|
||||
|
||||
// Stamp dough
|
||||
watch(isStamping, (s) => {
|
||||
if (s) return;
|
||||
if (items.value[STAMPER_POSITION] == 'dough') {
|
||||
items.value[STAMPER_POSITION] = 'raw biscuit';
|
||||
}
|
||||
});
|
||||
|
||||
// Bake cookies
|
||||
watch(motorPulseOut, (p) => {
|
||||
if (p) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (props.inTargetZone) {
|
||||
for (let pos of [OVEN_POSITION, OVEN_POSITION + 1]) {
|
||||
switch (items.value[pos]) {
|
||||
case 'raw biscuit':
|
||||
items.value[pos] = 'almost biscuit'
|
||||
break
|
||||
case 'almost biscuit':
|
||||
items.value[pos] = 'biscuit'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<div>
|
||||
<h6>Main Switch</h6>
|
||||
<q-btn-toggle v-model="state" toggle-color="primary" :options="options" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from 'vue';
|
||||
|
||||
export type BiscuitSwitchState = 'on' | 'off' | 'paused';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'positionChanged', value: BiscuitSwitchState): void;
|
||||
}>();
|
||||
|
||||
const options: { label: string; value: BiscuitSwitchState }[] = [
|
||||
{ label: 'Off', value: 'off' },
|
||||
{ label: 'Paused', value: 'paused' },
|
||||
{ label: 'On', value: 'on' },
|
||||
];
|
||||
|
||||
const state = ref<BiscuitSwitchState>('off');
|
||||
|
||||
watchEffect(() => emit('positionChanged', state.value));
|
||||
</script>
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { BiscuitSwitchState } from 'src/components/BiscuitSwitch.vue';
|
||||
import { nextTick, ref, Ref, watch, watchEffect } from 'vue';
|
||||
|
||||
export type MachineState = 'off' | 'heating' | 'on' | 'paused' | 'turning off';
|
||||
|
||||
export const OPTIMAL_COOKIES_TARGET_TEMPERATURE = 230;
|
||||
export const BELT_SIZE = 6;
|
||||
export const EXTRUDER_POSITION = 0;
|
||||
export const STAMPER_POSITION = 1;
|
||||
export const OVEN_POSITION = 3;
|
||||
|
||||
interface BiscuitMachineOutputs {
|
||||
/** A signal for controlling motor power */
|
||||
motorOn: Ref<boolean>;
|
||||
/** A signal for the desired target oven temperature in degrees Celsius, or 'none' if not set.*/
|
||||
targetTemperature: Ref<number | 'none'>;
|
||||
/** A pulse that controls the extruder operation */
|
||||
extruderPulseOut: Ref<boolean>;
|
||||
/** A pulse that controls the stamper operation */
|
||||
stamperPulseOut: Ref<boolean>;
|
||||
/** The current machines state */
|
||||
machineState: Ref<MachineState>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Biscuit Machine's core logical structure, connecting it to the I/O ports it needs.
|
||||
*
|
||||
* @param mainSwitchPosition A reference to the main switch position.
|
||||
* @param inTargetZoneIn A reference to a signal indicating if the oven target temperature has been reached.
|
||||
* @param motorPulseIn A reference to the motor pulse, generated by a running motor
|
||||
* @returns The output ports of the machine.
|
||||
*/
|
||||
export function useBiscuitMachine(
|
||||
mainSwitchPosition: Ref<BiscuitSwitchState>,
|
||||
inTargetZoneIn: Ref<boolean>,
|
||||
motorPulseIn: Ref<boolean>
|
||||
): BiscuitMachineOutputs {
|
||||
// External outputs
|
||||
const motorOn = ref(false);
|
||||
const targetTemperature = ref<number | 'none'>('none');
|
||||
const machineState = ref<MachineState>('off');
|
||||
const stamperPulseOut = ref(false);
|
||||
const extruderPulseOut = ref(false);
|
||||
|
||||
// Internal state
|
||||
const pulsesToShutdown = ref(0);
|
||||
const pulsesToStamperShutdown = ref(0);
|
||||
|
||||
// monitor the main switch position
|
||||
watchEffect(() => {
|
||||
switch (mainSwitchPosition.value) {
|
||||
case 'off':
|
||||
if (machineState.value != 'off') {
|
||||
machineState.value = 'turning off';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'on':
|
||||
machineState.value = inTargetZoneIn.value ? 'on' : 'heating';
|
||||
break;
|
||||
|
||||
case 'paused':
|
||||
machineState.value = 'paused';
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// monitor the heating process
|
||||
watchEffect(() => {
|
||||
if (inTargetZoneIn.value && machineState.value == 'heating') {
|
||||
machineState.value = 'on';
|
||||
}
|
||||
});
|
||||
|
||||
// transfer incoming motor pulses to the extruder & stamper
|
||||
watch(motorPulseIn, (newMotorPulse) => {
|
||||
if (!newMotorPulse) return;
|
||||
|
||||
switch (machineState.value) {
|
||||
case 'on':
|
||||
// while on, always transfer the incoming pulse to both appendages
|
||||
stamperPulseOut.value = true;
|
||||
extruderPulseOut.value = true;
|
||||
nextTick(() => {
|
||||
stamperPulseOut.value = false;
|
||||
extruderPulseOut.value = false;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'turning off':
|
||||
// during the turning off procedure, the extruder never runs
|
||||
// the stamper only runs long enough to stamp out the remaining dough
|
||||
if (pulsesToStamperShutdown.value > 0) {
|
||||
stamperPulseOut.value = true;
|
||||
nextTick(() => (stamperPulseOut.value = false));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// in any other case, ignore a spurious motor pulse
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// decrement internal shutdown counter on incoming motor pulses
|
||||
watch(
|
||||
motorPulseIn,
|
||||
(newMotorPulse) => {
|
||||
if (!newMotorPulse) return;
|
||||
|
||||
if (pulsesToStamperShutdown.value > 0) {
|
||||
pulsesToStamperShutdown.value--;
|
||||
}
|
||||
if (pulsesToShutdown.value > 0) {
|
||||
pulsesToShutdown.value--;
|
||||
}
|
||||
if (pulsesToShutdown.value == 0 && machineState.value == 'turning off') {
|
||||
machineState.value = 'off';
|
||||
}
|
||||
},
|
||||
{ flush: 'pre' }
|
||||
);
|
||||
|
||||
// React on changing machine state to configure the stable outputs
|
||||
watchEffect(() => {
|
||||
switch (machineState.value) {
|
||||
case 'off':
|
||||
motorOn.value = false;
|
||||
targetTemperature.value = 'none';
|
||||
break;
|
||||
case 'heating':
|
||||
motorOn.value = false;
|
||||
targetTemperature.value = OPTIMAL_COOKIES_TARGET_TEMPERATURE;
|
||||
break;
|
||||
case 'on':
|
||||
motorOn.value = true;
|
||||
targetTemperature.value = OPTIMAL_COOKIES_TARGET_TEMPERATURE;
|
||||
break;
|
||||
case 'paused':
|
||||
motorOn.value = false;
|
||||
targetTemperature.value = targetTemperature.value;
|
||||
break;
|
||||
case 'turning off':
|
||||
motorOn.value = true;
|
||||
targetTemperature.value = targetTemperature.value;
|
||||
|
||||
// turn off the stamper after it has stamped the last blob of dough
|
||||
pulsesToStamperShutdown.value = STAMPER_POSITION - EXTRUDER_POSITION;
|
||||
// turn off the machine after a full belt-worth of motor pulses
|
||||
pulsesToShutdown.value = BELT_SIZE;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// stable outputs
|
||||
motorOn,
|
||||
targetTemperature,
|
||||
// pulsing outputs
|
||||
stamperPulseOut,
|
||||
extruderPulseOut,
|
||||
// debugging outputs
|
||||
machineState,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { ref, Ref, watch } from 'vue';
|
||||
|
||||
export const ROOM_TEMPERATURE = 20;
|
||||
export const HEATER_PLUS_DEGREES_PER_TICK = 10;
|
||||
export const OVEN_MINUS_DEGREES_PER_TICK = 2;
|
||||
|
||||
export interface BiscuitOvenOutputs {
|
||||
/** The currently reached temperature, in degrees Celsius */
|
||||
currentTemperature: Ref<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A biscuit oven implementation.
|
||||
* @param tick The input tick source, used for calculating the current temperature.
|
||||
* @param isHeaterOn The input signal indicating if the heater should be on.
|
||||
* @returns The oven output signals.
|
||||
*/
|
||||
export function useBiscuitOven(
|
||||
tick: Ref<boolean>,
|
||||
isHeaterOn: Ref<boolean>
|
||||
): BiscuitOvenOutputs {
|
||||
const currentTemperature = ref(ROOM_TEMPERATURE);
|
||||
|
||||
watch(tick, (newTick) => {
|
||||
if (newTick) return;
|
||||
|
||||
if (isHeaterOn.value) {
|
||||
currentTemperature.value += HEATER_PLUS_DEGREES_PER_TICK;
|
||||
} else {
|
||||
currentTemperature.value -= OVEN_MINUS_DEGREES_PER_TICK;
|
||||
if (currentTemperature.value < ROOM_TEMPERATURE) {
|
||||
currentTemperature.value = ROOM_TEMPERATURE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { currentTemperature };
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { nextTick, ref, Ref, watch } from 'vue';
|
||||
|
||||
export const TICKS_PER_REVOLUTION = 3;
|
||||
|
||||
export interface MotorOutputs {
|
||||
/** An output pulse, indicating the completion of a full motor revolution. It stays on for a single VueJS tick. */
|
||||
pulse: Ref<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic motor implementation.
|
||||
* @param tick The input tick source, used for calculating number of revolutions.
|
||||
* @param isOn The input signal indicating if the motor power is on.
|
||||
* @returns The motor output signals.
|
||||
*/
|
||||
export function useMotor(tick: Ref<boolean>, isOn: Ref<boolean>): MotorOutputs {
|
||||
const totalTicks = ref(0);
|
||||
const pulse = ref(false);
|
||||
|
||||
watch(tick, (newTick) => {
|
||||
if (!newTick || !isOn.value) return;
|
||||
|
||||
totalTicks.value += 1;
|
||||
|
||||
if (totalTicks.value % TICKS_PER_REVOLUTION == 0) {
|
||||
pulse.value = true;
|
||||
nextTick(() => (pulse.value = false));
|
||||
}
|
||||
});
|
||||
|
||||
return { pulse };
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { ref, Ref, watch } from 'vue';
|
||||
|
||||
export interface PulseActivatedToolsOutputs {
|
||||
/** Indicates whether the tool is doing its work, as a result of an incoming pulse */
|
||||
isActive: Ref<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic implementation of a tool, whose work is triggered by an incoming pulse,
|
||||
* and it takes a fixed amount of time to complete it.
|
||||
*
|
||||
* @param pulse The incoming pulse, which kicks the tool into performing its activity.
|
||||
* @param workDuration The duration in milliseconds that the tool completes its assign work for.
|
||||
* @retursn The tool's activity status.
|
||||
*/
|
||||
export function usePulseActivatedTool(
|
||||
pulse: Ref<boolean>,
|
||||
workDuration: number
|
||||
): PulseActivatedToolsOutputs {
|
||||
const isActive = ref(false);
|
||||
|
||||
watch(pulse, (newPulse) => {
|
||||
if (!newPulse) return;
|
||||
isActive.value = true;
|
||||
setTimeout(() => (isActive.value = false), workDuration);
|
||||
});
|
||||
return { isActive };
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { ref, Ref, watchEffect } from 'vue';
|
||||
|
||||
export const TEMPERATURE_TOLERANCE = 10;
|
||||
|
||||
export interface ThermostatOutputs {
|
||||
/** An output signal indicating whether the current temperature is within
|
||||
* the desired tolerance level of the target. */
|
||||
inTargetZoneOut: Ref<boolean>;
|
||||
/** An output signal indicating whether the target device's heater needs to be on. */
|
||||
isHeaterOnOut: Ref<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simplistic thermostat implementation, which tries to keep the controlled
|
||||
* devices's temperature within a given range of a target set point.
|
||||
*
|
||||
* @param targetTemperature The given target temperature in degrees Celsius, or 'none' if not set.
|
||||
* @param currentTemperature The currently measured temperature in degrees Celsius.
|
||||
* @returns The thermostat outputs.
|
||||
*/
|
||||
export function useThermostat(
|
||||
targetTemperature: Ref<number | 'none'>,
|
||||
currentTemperature: Ref<number>
|
||||
): ThermostatOutputs {
|
||||
const inTargetZoneOut = ref(false);
|
||||
const isHeaterOnOut = ref(false);
|
||||
|
||||
watchEffect(() => {
|
||||
let heaterOn = false;
|
||||
if (targetTemperature.value != 'none') {
|
||||
const lowerBound = targetTemperature.value - TEMPERATURE_TOLERANCE;
|
||||
const upperBound = targetTemperature.value + TEMPERATURE_TOLERANCE;
|
||||
|
||||
if (isHeaterOnOut.value) {
|
||||
heaterOn = currentTemperature.value < upperBound;
|
||||
} else {
|
||||
heaterOn = currentTemperature.value <= lowerBound;
|
||||
}
|
||||
isHeaterOnOut.value = heaterOn;
|
||||
|
||||
inTargetZoneOut.value =
|
||||
Math.abs(targetTemperature.value - currentTemperature.value) <=
|
||||
TEMPERATURE_TOLERANCE;
|
||||
} else {
|
||||
isHeaterOnOut.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
return { inTargetZoneOut, isHeaterOnOut };
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Ref, ref } from 'vue';
|
||||
|
||||
export interface TimeSourceOutputs {
|
||||
/** A signal indicating the state of the time source. The signal keeps a
|
||||
* 50% duty cycle with the requested cycle duration. */
|
||||
tickIsOn: Ref<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic time source, providing a on/off pulse of a given duration.
|
||||
* @param tickDuration The desired duration of the pulse, in milliseconds.
|
||||
*/
|
||||
export function useTimeSource(tickDuration: number): TimeSourceOutputs {
|
||||
const tickIsOn = ref(false);
|
||||
|
||||
setInterval(() => {
|
||||
tickIsOn.value = true;
|
||||
setTimeout(() => (tickIsOn.value = false), tickDuration / 2);
|
||||
}, tickDuration);
|
||||
|
||||
return { tickIsOn };
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// app global css in SCSS form
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Quasar SCSS (& Sass) Variables
|
||||
// --------------------------------------------------
|
||||
// To customize the look and feel of this app, you can override
|
||||
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
||||
|
||||
// Check documentation for full list of Quasar variables
|
||||
|
||||
// Your own variables (that are declared here) and Quasar's own
|
||||
// ones will be available out of the box in your .vue/.scss/.sass files
|
||||
|
||||
// It's highly recommended to change the default colors
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary: #ffa60b;
|
||||
$secondary: #26a69a;
|
||||
$accent: #9c27b0;
|
||||
|
||||
$dark: #1d1d1d;
|
||||
$dark-page: #121212;
|
||||
|
||||
$positive: #21ba45;
|
||||
$negative: #c10015;
|
||||
$info: #31ccec;
|
||||
$warning: #f2c037;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable */
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: string;
|
||||
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
|
||||
VUE_ROUTER_BASE: string | undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title> The Biscuit Machine </q-toolbar-title>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div
|
||||
class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center"
|
||||
>
|
||||
<div>
|
||||
<div style="font-size: 30vh">404</div>
|
||||
|
||||
<div class="text-h2" style="opacity: 0.4">Oops. Nothing here...</div>
|
||||
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="blue"
|
||||
unelevated
|
||||
to="/"
|
||||
label="Go Home"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<BiscuitMachine />
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BiscuitMachine from 'src/components/BiscuitMachine.vue';
|
||||
</script>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// Forces TS to apply `@quasar/app-vite` augmentations of `quasar` package
|
||||
// Removing this would break `quasar/wrappers` imports as those typings are declared
|
||||
// into `@quasar/app-vite`
|
||||
// As a side effect, since `@quasar/app-vite` reference `quasar` to augment it,
|
||||
// this declaration also apply `quasar` own
|
||||
// augmentations (eg. adds `$q` into Vue component context)
|
||||
/// <reference types="@quasar/app-vite" />
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { route } from 'quasar/wrappers';
|
||||
import {
|
||||
createMemoryHistory,
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
import routes from './routes';
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Router instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Router instance.
|
||||
*/
|
||||
|
||||
export default route(function (/* { store, ssrContext } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: process.env.VUE_ROUTER_MODE === 'history'
|
||||
? createWebHistory
|
||||
: createWebHashHistory;
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
return Router;
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('layouts/MainLayout.vue'),
|
||||
children: [{ path: '', component: () => import('pages/IndexPage.vue') }],
|
||||
},
|
||||
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// Mocks all files ending in `.vue` showing them as plain Vue instances
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { BiscuitSwitchState } from 'src/components/BiscuitSwitch.vue';
|
||||
import {
|
||||
useBiscuitMachine,
|
||||
OPTIMAL_COOKIES_TARGET_TEMPERATURE,
|
||||
BELT_SIZE,
|
||||
STAMPER_POSITION,
|
||||
EXTRUDER_POSITION,
|
||||
} from 'src/composables/biscuitMachine';
|
||||
import { expect, test } from 'vitest';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { toggleTickAsync } from '../utils';
|
||||
|
||||
test('start in the expected state', async () => {
|
||||
const mainSwitchPosition = ref<BiscuitSwitchState>('off');
|
||||
const inTargetZoneIn = ref(false);
|
||||
const motorPulseIn = ref(false);
|
||||
|
||||
const {
|
||||
motorOn,
|
||||
stamperPulseOut,
|
||||
extruderPulseOut,
|
||||
targetTemperature,
|
||||
machineState,
|
||||
} = useBiscuitMachine(mainSwitchPosition, inTargetZoneIn, motorPulseIn);
|
||||
|
||||
expect(machineState.value).toBe('off');
|
||||
expect(stamperPulseOut.value).toBe(false);
|
||||
expect(extruderPulseOut.value).toBe(false);
|
||||
expect(targetTemperature.value).toBe('none');
|
||||
expect(motorOn.value).toBe(false);
|
||||
});
|
||||
|
||||
test('switches motor on only after target temperature reached', async () => {
|
||||
const mainSwitchPosition = ref<BiscuitSwitchState>('on');
|
||||
const inTargetZoneIn = ref(false);
|
||||
const motorPulseIn = ref(false);
|
||||
|
||||
const { motorOn, targetTemperature, machineState } = useBiscuitMachine(
|
||||
mainSwitchPosition,
|
||||
inTargetZoneIn,
|
||||
motorPulseIn
|
||||
);
|
||||
|
||||
expect(machineState.value).toBe('heating');
|
||||
expect(motorOn.value).toBe(false);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
|
||||
inTargetZoneIn.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(machineState.value).toBe('on');
|
||||
expect(motorOn.value).toBe(true);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
});
|
||||
|
||||
test('pausing the machine stops the motor & keeps the temperature', async () => {
|
||||
const mainSwitchPosition = ref<BiscuitSwitchState>('on');
|
||||
const inTargetZoneIn = ref(true);
|
||||
const motorPulseIn = ref(false);
|
||||
|
||||
const { motorOn, targetTemperature, machineState } = useBiscuitMachine(
|
||||
mainSwitchPosition,
|
||||
inTargetZoneIn,
|
||||
motorPulseIn
|
||||
);
|
||||
|
||||
expect(machineState.value).toBe('on');
|
||||
expect(motorOn.value).toBe(true);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
|
||||
mainSwitchPosition.value = 'paused';
|
||||
await nextTick();
|
||||
|
||||
expect(machineState.value).toBe('paused');
|
||||
expect(motorOn.value).toBe(false);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
});
|
||||
|
||||
test('turning off the machine waits for produce to get carted off', async () => {
|
||||
const mainSwitchPosition = ref<BiscuitSwitchState>('on');
|
||||
const inTargetZoneIn = ref(true);
|
||||
const motorPulseIn = ref(false);
|
||||
|
||||
const {
|
||||
motorOn,
|
||||
targetTemperature,
|
||||
machineState,
|
||||
extruderPulseOut,
|
||||
stamperPulseOut,
|
||||
} = useBiscuitMachine(mainSwitchPosition, inTargetZoneIn, motorPulseIn);
|
||||
|
||||
expect(machineState.value).toBe('on');
|
||||
expect(motorOn.value).toBe(true);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
|
||||
mainSwitchPosition.value = 'off';
|
||||
await nextTick();
|
||||
|
||||
expect(machineState.value).toBe('turning off');
|
||||
expect(motorOn.value).toBe(true);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
|
||||
for (let i = 0; i < BELT_SIZE - 1; i++) {
|
||||
motorPulseIn.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(machineState.value).toBe('turning off');
|
||||
expect(motorOn.value).toBe(true);
|
||||
expect(targetTemperature.value).toBe(OPTIMAL_COOKIES_TARGET_TEMPERATURE);
|
||||
expect(extruderPulseOut.value).toBe(false);
|
||||
expect(stamperPulseOut.value).toBe(
|
||||
i < STAMPER_POSITION - EXTRUDER_POSITION
|
||||
// i <= 3
|
||||
);
|
||||
|
||||
motorPulseIn.value = false;
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
await toggleTickAsync(motorPulseIn);
|
||||
expect(machineState.value).toBe('off');
|
||||
expect(motorOn.value).toBe(false);
|
||||
expect(targetTemperature.value).toBe('none');
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import {
|
||||
HEATER_PLUS_DEGREES_PER_TICK,
|
||||
ROOM_TEMPERATURE,
|
||||
useBiscuitOven,
|
||||
} from 'src/composables/biscuitOven';
|
||||
import { test, expect } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { toggleTickAsync } from '../utils';
|
||||
|
||||
test('starts off at room temperature', () => {
|
||||
const tick = ref(false);
|
||||
const isOn = ref(false);
|
||||
|
||||
const { currentTemperature } = useBiscuitOven(tick, isOn);
|
||||
expect(currentTemperature.value).toBe(ROOM_TEMPERATURE);
|
||||
});
|
||||
|
||||
test('does not heat up when off', async () => {
|
||||
const tick = ref(false);
|
||||
const isOn = ref(false);
|
||||
|
||||
const { currentTemperature } = useBiscuitOven(tick, isOn);
|
||||
|
||||
await toggleTickAsync(tick, 10);
|
||||
|
||||
expect(currentTemperature.value).toBe(ROOM_TEMPERATURE);
|
||||
});
|
||||
|
||||
test('heats up when on', async () => {
|
||||
const tick = ref(false);
|
||||
const isOn = ref(true);
|
||||
|
||||
const { currentTemperature } = useBiscuitOven(tick, isOn);
|
||||
|
||||
const heatingUpTime = 10;
|
||||
await toggleTickAsync(tick, heatingUpTime);
|
||||
|
||||
expect(currentTemperature.value).toBe(
|
||||
ROOM_TEMPERATURE + heatingUpTime * HEATER_PLUS_DEGREES_PER_TICK
|
||||
);
|
||||
});
|
||||
|
||||
test('cools down to room temperature when turned off', async () => {
|
||||
const tick = ref(false);
|
||||
const isOn = ref(true);
|
||||
|
||||
const { currentTemperature } = useBiscuitOven(tick, isOn);
|
||||
|
||||
const heatingUpTime = 10;
|
||||
await toggleTickAsync(tick, heatingUpTime);
|
||||
|
||||
expect(currentTemperature.value).toBe(
|
||||
ROOM_TEMPERATURE + heatingUpTime * HEATER_PLUS_DEGREES_PER_TICK
|
||||
);
|
||||
|
||||
isOn.value = false;
|
||||
|
||||
const cooldownTime = 1000;
|
||||
await toggleTickAsync(tick, cooldownTime);
|
||||
|
||||
expect(currentTemperature.value).toBe(ROOM_TEMPERATURE);
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { TICKS_PER_REVOLUTION, useMotor } from 'src/composables/motor';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { toggleTickAsync, usePulseCount } from '../utils';
|
||||
|
||||
describe('motor', () => {
|
||||
test('starts in the off state', () => {
|
||||
const isOn = ref(false);
|
||||
const tick = ref(false);
|
||||
|
||||
const { pulse } = useMotor(tick, isOn);
|
||||
const { pulseCount } = usePulseCount(pulse);
|
||||
|
||||
expect(pulseCount.value).toBe(0);
|
||||
});
|
||||
|
||||
test('does not pulse when off', async () => {
|
||||
const isOn = ref(false);
|
||||
const tick = ref(false);
|
||||
|
||||
const { pulse } = useMotor(tick, isOn);
|
||||
const { pulseCount } = usePulseCount(pulse);
|
||||
|
||||
for (let i = 0; i < 3 * TICKS_PER_REVOLUTION; i++) {
|
||||
await toggleTickAsync(tick);
|
||||
}
|
||||
expect(pulseCount.value).toBe(0);
|
||||
});
|
||||
|
||||
test('does pulse when on', async () => {
|
||||
const isOn = ref(true);
|
||||
const tick = ref(false);
|
||||
|
||||
const { pulse } = useMotor(tick, isOn);
|
||||
const { pulseCount } = usePulseCount(pulse);
|
||||
|
||||
for (let i = 0; i < 3 * TICKS_PER_REVOLUTION; i++) {
|
||||
await toggleTickAsync(tick);
|
||||
|
||||
const expectedValue = Math.floor((i + 1) / TICKS_PER_REVOLUTION);
|
||||
expect(pulseCount.value).toBe(expectedValue);
|
||||
}
|
||||
});
|
||||
|
||||
test('stops pulsing when turned off', async () => {
|
||||
const tick = ref(false);
|
||||
const isOn = ref(true);
|
||||
|
||||
const { pulse } = useMotor(tick, isOn);
|
||||
const { pulseCount } = usePulseCount(pulse);
|
||||
|
||||
for (let i = 0; i < 3 * TICKS_PER_REVOLUTION; i++) {
|
||||
await toggleTickAsync(tick);
|
||||
|
||||
const expectedValue = Math.floor((i + 1) / TICKS_PER_REVOLUTION);
|
||||
expect(pulseCount.value).toBe(expectedValue);
|
||||
}
|
||||
|
||||
const pulseValueAtStop = pulseCount.value;
|
||||
|
||||
isOn.value = false;
|
||||
await nextTick();
|
||||
|
||||
for (let i = 0; i < 3 * TICKS_PER_REVOLUTION; i++) {
|
||||
toggleTickAsync(tick);
|
||||
}
|
||||
|
||||
expect(pulseCount.value).toBe(pulseValueAtStop);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { usePulseActivatedTool } from 'src/composables/pulseActivatedTool';
|
||||
import { expect, test } from 'vitest';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
test('starts in the off state', () => {
|
||||
const pulse = ref(false);
|
||||
|
||||
const { isActive } = usePulseActivatedTool(pulse, 10);
|
||||
expect(isActive.value).toBe(false);
|
||||
});
|
||||
|
||||
test('is resoping to the input pulse', async () => {
|
||||
const pulse = ref(false);
|
||||
|
||||
const { isActive } = usePulseActivatedTool(pulse, 10);
|
||||
expect(isActive.value).toBe(false);
|
||||
|
||||
pulse.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(isActive.value).toBe(true);
|
||||
|
||||
pulse.value = false;
|
||||
await nextTick();
|
||||
|
||||
expect(isActive.value).toBe(true);
|
||||
|
||||
await await new Promise((p) => setTimeout(p, 15));
|
||||
expect(isActive.value).toBe(false);
|
||||
});
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { ROOM_TEMPERATURE, useBiscuitOven } from 'src/composables/biscuitOven';
|
||||
import {
|
||||
TEMPERATURE_TOLERANCE,
|
||||
useThermostat,
|
||||
} from 'src/composables/thermostat';
|
||||
import { expect, test } from 'vitest';
|
||||
import { nextTick, ref, watchEffect } from 'vue';
|
||||
import { toggleTickAsync } from '../utils';
|
||||
|
||||
test('starts in the off state', () => {
|
||||
const targetTemperature = ref<number | 'none'>('none');
|
||||
const currentTemperature = ref(ROOM_TEMPERATURE);
|
||||
|
||||
const { inTargetZoneOut, isHeaterOnOut } = useThermostat(
|
||||
targetTemperature,
|
||||
currentTemperature
|
||||
);
|
||||
|
||||
expect(inTargetZoneOut.value).toBe(false);
|
||||
expect(isHeaterOnOut.value).toBe(false);
|
||||
});
|
||||
|
||||
test('requests heats up when needed', async () => {
|
||||
const targetTemperature = ref(200);
|
||||
const currentTemperature = ref(ROOM_TEMPERATURE);
|
||||
|
||||
const { inTargetZoneOut, isHeaterOnOut } = useThermostat(
|
||||
targetTemperature,
|
||||
currentTemperature
|
||||
);
|
||||
|
||||
expect(inTargetZoneOut.value).toBe(false);
|
||||
expect(isHeaterOnOut.value).toBe(true);
|
||||
});
|
||||
|
||||
test('requests cooldown when needed', async () => {
|
||||
const targetTemperature = ref(200);
|
||||
const currentTemperature = ref(220);
|
||||
|
||||
const { inTargetZoneOut, isHeaterOnOut } = useThermostat(
|
||||
targetTemperature,
|
||||
currentTemperature
|
||||
);
|
||||
|
||||
expect(inTargetZoneOut.value).toBe(false);
|
||||
expect(isHeaterOnOut.value).toBe(false);
|
||||
});
|
||||
|
||||
test('target zone report is correct', async () => {
|
||||
const TARGET_TEMPERATURE = 200;
|
||||
|
||||
const targetTemperature = ref(TARGET_TEMPERATURE);
|
||||
const currentTemperature = ref(0);
|
||||
|
||||
const { inTargetZoneOut } = useThermostat(
|
||||
targetTemperature,
|
||||
currentTemperature
|
||||
);
|
||||
|
||||
currentTemperature.value = TARGET_TEMPERATURE + TEMPERATURE_TOLERANCE;
|
||||
await nextTick();
|
||||
expect(inTargetZoneOut.value).toBe(true);
|
||||
|
||||
currentTemperature.value = TARGET_TEMPERATURE - TEMPERATURE_TOLERANCE;
|
||||
await nextTick();
|
||||
expect(inTargetZoneOut.value).toBe(true);
|
||||
|
||||
currentTemperature.value = TARGET_TEMPERATURE - TEMPERATURE_TOLERANCE - 0.1;
|
||||
await nextTick();
|
||||
expect(inTargetZoneOut.value).toBe(false);
|
||||
|
||||
currentTemperature.value = TARGET_TEMPERATURE + TEMPERATURE_TOLERANCE + 0.1;
|
||||
await nextTick();
|
||||
expect(inTargetZoneOut.value).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps stable target temperature when connected to an oven', async () => {
|
||||
const TARGET_TEMPERATURE = 200;
|
||||
|
||||
const targetTemperature = ref(TARGET_TEMPERATURE);
|
||||
const isHeaterOnIn = ref(false);
|
||||
const tick = ref(false);
|
||||
|
||||
const { currentTemperature } = useBiscuitOven(tick, isHeaterOnIn);
|
||||
const { inTargetZoneOut, isHeaterOnOut } = useThermostat(
|
||||
targetTemperature,
|
||||
currentTemperature
|
||||
);
|
||||
|
||||
watchEffect(() => (isHeaterOnIn.value = isHeaterOnOut.value));
|
||||
|
||||
await toggleTickAsync(tick, 1000);
|
||||
|
||||
expect(inTargetZoneOut.value).toBe(true);
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
// This file will be run before each test file
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Ref, nextTick, ref, watch } from 'vue';
|
||||
|
||||
export async function toggleTickAsync(tick: Ref<boolean>, numTimes = 1) {
|
||||
for (let i = 0; i < numTimes; i++) {
|
||||
tick.value = true;
|
||||
await nextTick();
|
||||
tick.value = false;
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
export function usePulseCount(pulse: Ref<boolean>) {
|
||||
const pulseCount = ref(0);
|
||||
watch(pulse, (newPulse) => {
|
||||
if (newPulse) pulseCount.value++;
|
||||
});
|
||||
return { pulseCount };
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "@quasar/app-vite/tsconfig-preset",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
setupFiles: 'test/vitest/setup-file.ts',
|
||||
include: [
|
||||
// Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__'
|
||||
// Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'
|
||||
'src/**/*.vitest.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'test/vitest/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
quasar({
|
||||
sassVariables: 'src/quasar-variables.scss',
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
});
|
||||
Loading…
Reference in New Issue