diff --git a/.github/workflows/gulp-deploy.yml b/.github/workflows/gulp-deploy.yml new file mode 100644 index 0000000..413c77e --- /dev/null +++ b/.github/workflows/gulp-deploy.yml @@ -0,0 +1,74 @@ +name: Build and Deploy to GitHub Pages + +on: + push: + branches: + - "**" + workflow_dispatch: + inputs: + deploy: + description: "Deploy to GitHub Pages?" + required: true + default: "true" + type: choice + options: + - "true" + - "false" + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build:prod + + - name: Setup Pages + uses: actions/configure-pages@v4 + if: inputs.deploy == 'true' + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + if: inputs.deploy == 'true' + with: + path: "./dist" + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + if: inputs.deploy == 'true' + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5eaae7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +.tmp/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env \ No newline at end of file diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..f2bdc9e --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,13 @@ +# Credits + +## Developers +- the_al +- Mathias Malmqvist + +## Special Thanks +- **Board model detection**: [Battle Beaver Customs](https://battlebeavercustoms.com/) +- **Color detection**: romek77 from Poland + +--- + +Thank you to everyone who contributed to this project, whether through code, testing, feedback, or translations. Your support makes this project possible! diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..f741e01 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,136 @@ +# Development Guide + +## Quick Start + +```bash +# Install dependencies +npm install + +# Start development with auto-reload +npm run dev:full +``` + +Open `https://localhost:8443` in your browser (accept the SSL certificate warning). + +## Available Scripts + +| Script | Description | +| --------------------- | --------------------------------------------------------- | +| `npm run build` | Build for development (with source maps) | +| `npm run build:prod` | Build for production (minified, optimized) | +| `npm run clean` | Clean the dist directory | +| `npm run serve:https` | Serve built app over HTTPS (required for WebHID) | +| `npm run serve` | Serve built app over HTTP (WebHID won't work) | +| `npm run start` | Build and serve over HTTPS | +| `npm run dev:full` | **Recommended**: Build, watch, and serve with auto-reload | +| `npm run watch` | Watch files and rebuild on changes | + +## Development Workflow + +### For Active Development + +```bash +npm run dev:full +``` + +This starts the complete development environment: + +- Builds the application +- Watches for file changes +- Serves over HTTPS at `https://localhost:8443` +- Automatically rebuilds when you save files + +### For Testing Built Version + +```bash +npm run start +``` + +This builds once and serves the result. + +## Important Notes + +### HTTPS Requirement + +The WebHID API requires HTTPS. The development server uses self-signed certificates located at: + +- `server.crt` - SSL certificate +- `server.key` - SSL private key + +### Browser Compatibility + +- **Chrome/Edge**: Full WebHID support ✅ +- **Firefox**: No WebHID support ❌ +- **Safari**: No WebHID support ❌ + +### SSL Certificate Warning + +When first accessing `https://localhost:8443`, your browser will show a security warning because we're using a self-signed certificate. This is normal for development - click "Advanced" and "Proceed to localhost" to continue. + +## File Structure + +``` +├── js/ # Source JavaScript files +│ ├── core.js # Main application entry point +│ ├── controllers/ # Controller-specific classes +│ └── modals/ # Modal dialog handlers +├── css/ # Source CSS files +├── templates/ # HTML template files +├── lang/ # Translation JSON files +├── assets/ # SVG assets +├── dist/ # Built application (auto-generated) +└── dev-server.js # Custom development server +``` + +## Build Process + +The build system uses Gulp with the following steps: + +1. **JavaScript**: Bundled with Rollup, supports ES modules +2. **CSS**: Concatenated and optionally minified +3. **HTML**: Processed and optionally minified +4. **Assets**: Copied to dist, SVGs can be inlined in production +5. **Languages**: JSON files copied and optionally minified + +### Development vs Production + +| Feature | Development | Production | +| -------------- | ----------- | ---------- | +| Source maps | ✅ | ❌ | +| Minification | ❌ | ✅ | +| Asset inlining | ❌ | ✅ | +| File hashing | ❌ | ✅ | + +## Troubleshooting + +### Port Already in Use + +If port 8443 is busy: + +```bash +PORT=8444 npm run serve:https +``` + +### Build Errors + +Clean and rebuild: + +```bash +npm run clean +npm run build +``` + +### SSL Certificate Issues + +The certificates are pre-generated. If you need new ones: + +```bash +openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj "/CN=localhost" +``` + +### WebHID Not Working + +1. Make sure you're using HTTPS (`npm run serve:https`) +2. Use Chrome or Edge browser +3. Accept the SSL certificate warning +4. Check browser console for errors diff --git a/README.md b/README.md index 9603137..99f3bd3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,93 @@ -# dualshock-tools.github.io +# DualShock Calibration GUI -The code behind the DualShock Calibration GUI +A web-based calibration tool for PlayStation DualShock 4, DualSense, and DualSense Edge controllers using the WebHID API. + +## Features + +- Controller connection via WebHID API +- Stick calibration and range calibration +- Input testing and visualization +- Battery status display +- Multi-language support (20+ languages) +- Progressive Web App capabilities + +## Development + +### Prerequisites + +- Node.js (v16 or higher) +- npm or yarn +- Modern browser with WebHID support (Chrome/Edge) + +### Getting Started + +1. **Install dependencies:** + + ```bash + npm install + ``` + +2. **Build the application:** + + ```bash + npm run build + ``` + +3. **Start the development server:** + + ```bash + npm run start + ``` + + The app will be available at `https://localhost:8443` + +### Development Scripts + +- `npm run build` - Build the application for development +- `npm run build:prod` - Build the application for production +- `npm run clean` - Clean the dist directory +- `npm run serve:https` - Serve the built app over HTTPS (required for WebHID) +- `npm run serve` - Serve the built app over HTTP (WebHID won't work) +- `npm run start` - Build and serve the app +- `npm run dev:full` - Build, watch for changes, and serve with auto-reload +- `npm run watch` - Watch for file changes and rebuild + +### Development Workflow + +For active development with auto-rebuild: + +```bash +npm run dev:full +``` + +This will: + +1. Build the application +2. Start watching for file changes +3. Serve the app over HTTPS at `https://localhost:8443` +4. Automatically rebuild when files change + +### Important Notes + +- **HTTPS Required**: The WebHID API requires HTTPS. The development server uses self-signed certificates. +- **Browser Security**: You may need to accept the self-signed certificate warning in your browser. +- **Controller Support**: Only works in browsers with WebHID support (Chrome, Edge, Opera). + +### Project Structure + +- `js/` - Source JavaScript files +- `css/` - Source CSS files +- `templates/` - HTML template files +- `lang/` - Translation files +- `assets/` - SVG assets +- `dist/` - Built application (generated) + +### Build System + +The project uses Gulp for building: + +- JavaScript bundling with Rollup +- CSS concatenation and minification +- HTML processing and minification +- Asset optimization +- Development vs production builds diff --git a/donate.png b/assets/donate.png similarity index 100% rename from donate.png rename to assets/donate.png diff --git a/assets/dualsense-controller.svg b/assets/dualsense-controller.svg new file mode 100644 index 0000000..6a2a82c --- /dev/null +++ b/assets/dualsense-controller.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 % + 0 % + + diff --git a/assets/dualshock-controller.svg b/assets/dualshock-controller.svg new file mode 100644 index 0000000..8a08822 --- /dev/null +++ b/assets/dualshock-controller.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 % + 0 % + + diff --git a/assets/icons.svg b/assets/icons.svg new file mode 100644 index 0000000..7c45cfe --- /dev/null +++ b/assets/icons.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core.js b/core.js deleted file mode 100644 index 45c9804..0000000 --- a/core.js +++ /dev/null @@ -1,2177 +0,0 @@ -var device = null; -var devname = ""; -var mode = 0; - -// bitmask: 1: clone, 2: update ds5 firmware, 4: battery low, 8: ds-edge not supported -var disable_btn = 0; -var last_disable_btn = 0; - -// 1 if there is any change that can be stored permanently -var has_changes_to_write = 0; - -var lang_orig_text = {}; -var lang_cur = {}; -var lang_disabled = true; -var lang_cur_direction = "ltr"; -var gj = 0; -var gu = 0; - -// DS5 finetuning -var finetune_original_data = [] -var last_written_finetune_data = [] -var finetune_visible = false -var on_finetune_updating = false - - -// Alphabetical order -var available_langs = { - "ar_ar": { "name": "العربية", "file": "ar_ar.json", "direction": "rtl"}, - "bg_bg": { "name": "Български", "file": "bg_bg.json", "direction": "ltr"}, - "cz_cz": { "name": "Čeština", "file": "cz_cz.json", "direction": "ltr"}, - "de_de": { "name": "Deutsch", "file": "de_de.json", "direction": "ltr"}, - "es_es": { "name": "Español", "file": "es_es.json", "direction": "ltr"}, - "fr_fr": { "name": "Français", "file": "fr_fr.json", "direction": "ltr"}, - "hu_hu": { "name": "Magyar", "file": "hu_hu.json", "direction": "ltr"}, - "it_it": { "name": "Italiano", "file": "it_it.json", "direction": "ltr"}, - "jp_jp": { "name": "日本語", "file": "jp_jp.json", "direction": "ltr"}, - "ko_kr": { "name": "한국어", "file": "ko_kr.json", "direction": "ltr"}, - "nl_nl": { "name": "Nederlands", "file": "nl_nl.json", "direction": "ltr"}, - "pl_pl": { "name": "Polski", "file": "pl_pl.json", "direction": "ltr"}, - "pt_br": { "name": "Português do Brasil", "file": "pt_br.json", "direction": "ltr"}, - "pt_pt": { "name": "Português", "file": "pt_pt.json", "direction": "ltr"}, - "rs_rs": { "name": "Srpski", "file": "rs_rs.json", "direction": "ltr"}, - "ru_ru": { "name": "Русский", "file": "ru_ru.json", "direction": "ltr"}, - "tr_tr": { "name": "Türkçe", "file": "tr_tr.json", "direction": "ltr"}, - "ua_ua": { "name": "Українська", "file": "ua_ua.json", "direction": "ltr"}, - "zh_cn": { "name": "中文", "file": "zh_cn.json", "direction": "ltr"}, - "zh_tw": { "name": "中文(繁)", "file": "zh_tw.json", "direction": "ltr"} -}; - -function buf2hex(buffer) { - return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')) .join(''); -} - -function dec2hex(i) { - return (i+0x10000).toString(16).substr(-4).toUpperCase(); -} - -function dec2hex32(i) { - return (i+0x100000000).toString(16).substr(-8).toUpperCase(); -} - -function dec2hex8(i) { - return (i+0x100).toString(16).substr(-2).toUpperCase(); -} - -function ds5_hw_to_bm(hw_ver) { - a = (hw_ver >> 8) & 0xff; - if(a == 0x03) { - return "BDM-010"; - } else if(a == 0x04) { - return "BDM-020"; - } else if(a == 0x05) { - return "BDM-030"; - } else if(a == 0x06) { - return "BDM-040"; - } else if(a == 0x07 || a == 0x08) { - return "BDM-050"; - } else { - return l("Unknown"); - } -} - -function ds4_hw_to_bm(hw_ver) { - a = hw_ver >> 8; - if(a == 0x31) { - return "JDM-001"; - } else if(a == 0x43) { - return "JDM-011"; - } else if(a == 0x54) { - return "JDM-030"; - } else if(a >= 0x64 && a <= 0x74) { - return "JDM-040"; - } else if((a > 0x80 && a < 0x84) || a == 0x93) { - return "JDM-020"; - } else if(a == 0xa4 || a == 0x90) { - return "JDM-050"; - } else if(a == 0xb0) { - return "JDM-055 (Scuf?)"; - } else if(a == 0xb4) { - return "JDM-055"; - } else { - if(is_rare(hw_ver)) - return "WOW!"; - return l("Unknown"); - } -} - -function is_rare(hw_ver) { - a = hw_ver >> 8; - b = a >> 4; - return ((b == 7 && a > 0x74) || (b == 9 && a != 0x93 && a != 0x90) || a == 0xa0); -} - -async function ds4_info() { - try { - var ooc = l("unknown"); - var is_clone = false; - - const view = lf("ds4_info", await device.receiveFeatureReport(0xa3)); - - var cmd = view.getUint8(0, true); - - if(cmd != 0xa3 || view.buffer.byteLength < 49) { - if(view.buffer.byteLength != 49) { - ooc = l("clone"); - is_clone = true; - } - } - - var k1 = new TextDecoder().decode(view.buffer.slice(1, 0x10)); - var k2 = new TextDecoder().decode(view.buffer.slice(0x10, 0x20)); - k1=k1.replace(/\0/g, ''); - k2=k2.replace(/\0/g, ''); - - var hw_ver_major= view.getUint16(0x21, true) - var hw_ver_minor= view.getUint16(0x23, true) - var sw_ver_major= view.getUint32(0x25, true) - var sw_ver_minor= view.getUint16(0x25+4, true) - try { - if(!is_clone) { - const view = await device.receiveFeatureReport(0x81); - ooc = l("original"); - } - } catch(e) { - la("clone"); - is_clone = true; - ooc = "" + l("clone") + ""; - disable_btn |= 1; - } - - clear_info(); - append_info(l("Build Date"), k1 + " " + k2); - append_info(l("HW Version"), "" + dec2hex(hw_ver_major) + ":" + dec2hex(hw_ver_minor)); - append_info(l("SW Version"), dec2hex32(sw_ver_major) + ":" + dec2hex(sw_ver_minor)); - append_info(l("Device Type"), ooc); - if(!is_clone) { - b_info = ' ' + - ''; - append_info(l("Board Model"), ds4_hw_to_bm(hw_ver_minor) + b_info); - - // All ok, safe to lock NVS, query it and get BD Addr - nvstatus = await ds4_nvstatus(); - - if(nvstatus == 0) - await ds4_nvlock(); - bd_addr = await ds4_getbdaddr(); - append_info(l("Bluetooth Address"), bd_addr); - - if(is_rare(hw_ver_minor)) { - show_popup("Wow, this is a rare/weird controller! Please write me an email at ds4@the.al or contact me on Discord (the_al)"); - } - } - } catch(e) { - ooc = "" + l("clone") + ""; - disable_btn |= 1; - } - return true; -} - -async function ds4_flash() { - la("ds4_flash"); - try { - await ds4_nvunlock(); - await ds4_nvlock(); - - show_popup(l("Changes saved successfully")); - - } catch(error) { - show_popup(l("Error while saving changes:") + " " + str(error)); - } -} - -async function ds5_flash() { - la("ds5_flash"); - try { - await ds5_nvunlock(); - await ds5_nvlock(); - - show_popup(l("Changes saved successfully")); - } catch(error) { - show_popup(l("Error while saving changes: ") + toString(error)); - } -} - -async function ds5_edge_flash() { - la("ds5_edge_flash"); - try { - await ds5_edge_flash_modules(); - await ds5_reset(); - show_popup("" + l("Changes saved successfully") + ".

" + l("If the calibration is not stored permanently, please double-check the wirings of the hardware mod.") + "
" + l("Please disconnect and reconnect the controller."), true); - await disconnect(); - } catch(error) { - show_popup(l("Error while saving changes: ") + toString(error)); - } -} - -async function ds4_reset() { - la("ds4_reset"); - try { - await device.sendFeatureReport(0xa0, alloc_req(0xa0, [4,1,0])) - } catch(error) { - } -} - -async function ds5_reset() { - la("ds5_reset"); - try { - await device.sendFeatureReport(0x80, alloc_req(0x80, [1,1])) - } catch(error) { - } -} - -async function ds4_calibrate_range_begin() { - la("ds4_calibrate_range_begin"); - var err = l("Range calibration failed: "); - try { - // Begin - await device.sendFeatureReport(0x90, alloc_req(0x90, [1,1,2])) - - // Assert - data = await device.receiveFeatureReport(0x91) - data2 = await device.receiveFeatureReport(0x92) - d1 = data.getUint32(0, false); - d2 = data2.getUint32(0, false); - if(d1 != 0x91010201 || d2 != 0x920102ff) { - la("ds4_calibrate_range_begin_failed", {"d1": d1, "d2": d2}); - close_calibrate_window(); - return show_popup(err + l("Error 1")); - } - } catch(e) { - la("ds4_calibrate_range_begin_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(err + e); - } -} - -async function ds4_calibrate_range_end() { - la("ds4_calibrate_range_end"); - var err = l("Range calibration failed: "); - try { - // Write - await device.sendFeatureReport(0x90, alloc_req(0x90, [2,1,2])) - - data = await device.receiveFeatureReport(0x91) - data2 = await device.receiveFeatureReport(0x92) - d1 = data.getUint32(0, false); - d2 = data2.getUint32(0, false); - if(d1 != 0x91010202 || d2 != 0x92010201) { - la("ds4_calibrate_range_end_failed", {"d1": d1, "d2": d2}); - close_calibrate_window(); - return show_popup(err + l("Error 3")); - } - - update_nvs_changes_status(1); - close_calibrate_window(); - show_popup(l("Range calibration completed")); - } catch(e) { - la("ds4_calibrate_range_end_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(err + e); - } -} - -async function ds4_calibrate_sticks_begin() { - la("ds4_calibrate_sticks_begin"); - var err = l("Stick calibration failed: "); - try { - // Begin - await device.sendFeatureReport(0x90, alloc_req(0x90, [1,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x91); - data2 = await device.receiveFeatureReport(0x92); - d1 = data.getUint32(0, false); - d2 = data2.getUint32(0, false); - if(d1 != 0x91010101 || d2 != 0x920101ff) { - la("ds4_calibrate_sticks_begin_failed", {"d1": d1, "d2": d2}); - show_popup(err + l("Error 1")); - return false; - } - - return true; - } catch(e) { - la("ds4_calibrate_sticks_begin_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - show_popup(err + e); - return false; - } -} - -async function ds4_calibrate_sticks_sample() { - la("ds4_calibrate_sticks_sample"); - var err = l("Stick calibration failed: "); - try { - // Sample - await device.sendFeatureReport(0x90, alloc_req(0x90, [3,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x91); - data2 = await device.receiveFeatureReport(0x92); - if(data.getUint32(0, false) != 0x91010101 || data2.getUint32(0, false) != 0x920101ff) { - close_calibrate_window(); - d1 = dec2hex32(data.getUint32(0, false)); - d2 = dec2hex32(data2.getUint32(0, false)); - la("ds4_calibrate_sticks_sample_failed", {"d1": d1, "d2": d2}); - show_popup(err + l("Error 2") + " (" + d1 + ", " + d2 + " at i=" + i + ")"); - return false; - } - return true; - } catch(e) { - await new Promise(r => setTimeout(r, 500)); - show_popup(err + e); - return false; - } -} - -async function ds4_calibrate_sticks_end() { - la("ds4_calibrate_sticks_end"); - var err = l("Stick calibration failed: "); - try { - // Write - await device.sendFeatureReport(0x90, alloc_req(0x90, [2,1,1])) - if(data.getUint32(0, false) != 0x91010101 || data2.getUint32(0, false) != 0x920101FF) { - d1 = dec2hex32(data.getUint32(0, false)); - d2 = dec2hex32(data2.getUint32(0, false)); - la("ds4_calibrate_sticks_end_failed", {"d1": d1, "d2": d2}); - show_popup(err + l("Error 3") + " (" + d1 + ", " + d2 + " at i=" + i + ")"); - return false; - } - - update_nvs_changes_status(1); - return true; - } catch(e) { - la("ds4_calibrate_sticks_end_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - show_popup(err + e); - return false; - } -} - -async function ds4_calibrate_sticks() { - la("ds4_calibrate_sticks"); - var err = l("Stick calibration failed: "); - try { - set_progress(0); - - // Begin - await device.sendFeatureReport(0x90, alloc_req(0x90, [1,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x91); - data2 = await device.receiveFeatureReport(0x92); - d1 = data.getUint32(0, false); - d2 = data2.getUint32(0, false); - if(d1 != 0x91010101 || d2 != 0x920101ff) { - la("ds4_calibrate_sticks_failed", {"s": 1, "d1": d1, "d2": d2}); - close_calibrate_window(); - return show_popup(err + l("Error 1")); - } - - set_progress(10); - await new Promise(r => setTimeout(r, 100)); - - for(var i=0;i<3;i++) { - // Sample - await device.sendFeatureReport(0x90, alloc_req(0x90, [3,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x91); - data2 = await device.receiveFeatureReport(0x92); - if(data.getUint32(0, false) != 0x91010101 || data2.getUint32(0, false) != 0x920101ff) { - d1 = dec2hex32(data.getUint32(0, false)); - d2 = dec2hex32(data2.getUint32(0, false)); - la("ds4_calibrate_sticks_failed", {"s": 2, "i": i, "d1": d1, "d2": d2}); - close_calibrate_window(); - return show_popup(err + l("Error 2") + " (" + d1 + ", " + d2 + " at i=" + i + ")"); - } - - await new Promise(r => setTimeout(r, 500)); - set_progress(20 + i * 30); - } - - // Write - await device.sendFeatureReport(0x90, alloc_req(0x90, [2,1,1])) - if(data.getUint32(0, false) != 0x91010101 || data2.getUint32(0, false) != 0x920101FF) { - d1 = dec2hex32(data.getUint32(0, false)); - d2 = dec2hex32(data2.getUint32(0, false)); - la("ds4_calibrate_sticks_failed", {"s": 3, "d1": d1, "d2": d2}); - close_calibrate_window(); - return show_popup(err + l("Error 3") + " (" + d1 + ", " + d2 + " at i=" + i + ")"); - } - - set_progress(100); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window() - show_popup(l("Stick calibration completed")); - } catch(e) { - la("ds4_calibrate_sticks_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(err + e); - } -} - -async function ds4_nvstatus() { - try { - await device.sendFeatureReport(0x08, alloc_req(0x08, [0xff,0, 12])) - data = lf("ds4_nvstatus", await device.receiveFeatureReport(0x11)) - // 1: temporary, 0: permanent - ret = data.getUint8(1, false); - if(ret == 1) { - $("#d-nvstatus").html("" + l("locked") + ""); - return 1; - } else if(ret == 0) { - $("#d-nvstatus").html("" + l("unlocked") + ""); - return 0; - } else { - $("#d-nvstatus").html("unk " + ret + ""); - if(ret == 0 || ret == 1) - return 2; - return ret; - } - return ret; - } catch(e) { - $("#d-nvstatus").html("" + l("error") + ""); - return 2; // error - } -} - -async function ds5_nvstatus() { - try { - await device.sendFeatureReport(0x80, alloc_req(0x80, [3,3])) - data = lf("ds5_nvstatus", await device.receiveFeatureReport(0x81)) - ret = data.getUint32(1, false); - if(ret == 0x15010100) { - return 4; - } - if(ret == 0x03030201) { - $("#d-nvstatus").html("" + l("locked") + ""); - return 1; // temporary - } else if(ret == 0x03030200) { - $("#d-nvstatus").html("" + l("unlocked") + ""); - return 0; // permanent - } else { - $("#d-nvstatus").html("unk " + dec2hex32(ret) + ""); - if(ret == 0 || ret == 1) - return 2; - return ret; // unknown - } - } catch(e) { - $("#d-nvstatus").html("" + l("error") + ""); - return 2; // error - } -} - -async function ds4_getbdaddr() { - try { - data = lf("ds4_getbdaddr", await device.receiveFeatureReport(0x12)); - out = "" - for(i=0;i<6;i++) { - if(i >= 1) out += ":"; - out += dec2hex8(data.getUint8(6-i, false)); - } - return out; - } catch(e) { - return "error"; - } -} - -async function ds5_getbdaddr() { - try { - await device.sendFeatureReport(0x80, alloc_req(0x80, [9,2])); - data = lf("ds5_getbdaddr", await device.receiveFeatureReport(0x81)); - out = "" - for(i=0;i<6;i++) { - if(i >= 1) out += ":"; - out += dec2hex8(data.getUint8(4 + 5 - i, false)); - } - return out; - } catch(e) { - return "error"; - } -} - -async function ds4_nvlock() { - la("ds4_nvlock"); - await device.sendFeatureReport(0xa0, alloc_req(0xa0, [10,1,0])) -} - -async function ds4_nvunlock() { - la("ds4_nvunlock"); - await device.sendFeatureReport(0xa0, alloc_req(0xa0, [10,2,0x3e,0x71,0x7f,0x89])) -} - -async function ds5_system_info(base, num, length, decode = true) { - await device.sendFeatureReport(128, alloc_req(128, [base,num])) - var pcba_id = lf("ds5_pcba_id", await device.receiveFeatureReport(129)); - if(pcba_id.getUint8(1) != base || pcba_id.getUint8(2) != num || pcba_id.getUint8(3) != 2) { - return l("error"); - } else { - if(decode) - return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length)); - else - return buf2hex(pcba_id.buffer.slice(4, 4+length)); - } - return l("Unknown"); -} - -async function ds5_info() { - try { - const view = lf("ds5_info", await device.receiveFeatureReport(0x20)); - - var cmd = view.getUint8(0, true); - if(cmd != 0x20 || view.buffer.byteLength != 64) - return false; - - var build_date = new TextDecoder().decode(view.buffer.slice(1, 1+11)); - var build_time = new TextDecoder().decode(view.buffer.slice(12, 20)); - - var fwtype = view.getUint16(20, true); - var swseries = view.getUint16(22, true); - var hwinfo = view.getUint32(24, true); - var fwversion = view.getUint32(28, true); - - var deviceinfo = new TextDecoder().decode(view.buffer.slice(32, 32+12)); - var updversion = view.getUint16(44, true); - var unk = view.getUint8(46, true); - - var fwversion1 = view.getUint32(48, true); - var fwversion2 = view.getUint32(52, true); - var fwversion3 = view.getUint32(56, true); - - clear_info(); - - b_info = ' ' + - ''; - - append_info(l("Serial Number"), await ds5_system_info(1, 19, 17), "hw"); - append_info_extra(l("MCU Unique ID"), await ds5_system_info(1, 9, 9, false), "hw"); - append_info_extra(l("PCBA ID"), await ds5_system_info(1, 17, 14), "hw"); - append_info_extra(l("Battery Barcode"), await ds5_system_info(1, 24, 23), "hw"); - append_info_extra(l("VCM Left Barcode"), await ds5_system_info(1, 26, 16), "hw"); - append_info_extra(l("VCM Right Barcode"), await ds5_system_info(1, 28, 16), "hw"); - append_info(l("Board Model"), ds5_hw_to_bm(hwinfo) + b_info, "hw"); - - append_info(l("FW Build Date"), build_date + " " + build_time, "fw"); - append_info_extra(l("FW Type"), "0x" + dec2hex(fwtype), "fw"); - append_info_extra(l("FW Series"), "0x" + dec2hex(swseries), "fw"); - append_info_extra(l("HW Model"), "0x" + dec2hex32(hwinfo), "hw"); - append_info(l("FW Version"), "0x" + dec2hex32(fwversion), "fw"); - append_info(l("FW Update"), "0x" + dec2hex(updversion), "fw"); - append_info_extra(l("FW Update Info"), "0x" + dec2hex8(unk), "fw"); - append_info_extra(l("SBL FW Version"), "0x" + dec2hex32(fwversion1), "fw"); - append_info_extra(l("Venom FW Version"), "0x" + dec2hex32(fwversion2), "fw"); - append_info_extra(l("Spider FW Version"), "0x" + dec2hex32(fwversion3), "fw"); - - append_info_extra(l("Touchpad ID"), await ds5_system_info(5, 2, 8, false), "hw"); - append_info_extra(l("Touchpad FW Version"), await ds5_system_info(5, 4, 8, false), "fw"); - - old_controller = build_date.search(/ 2020| 2021/); - if(old_controller != -1) { - la("ds5_info_error", {"r": "old"}) - disable_btn |= 2; - return true; - } - - nvstatus = await ds5_nvstatus(); - if(nvstatus == 0) - await ds5_nvlock(); - bd_addr = await ds5_getbdaddr(); - append_info(l("Bluetooth Address"), bd_addr, "hw"); - } catch(e) { - la("ds5_info_error", {"r": e}) - show_popup(l("Cannot read controller information")); - return false; - } - return true; -} - -async function ds5_calibrate_sticks_begin() { - la("ds5_calibrate_sticks_begin"); - var err = l("Range calibration failed: "); - try { - // Begin - await device.sendFeatureReport(0x82, alloc_req(0x82, [1,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010101) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_begin_failed", {"d1": d1}); - show_popup(err + l("Error 1") + " (" + d1 + ")."); - return false; - } - return true; - } catch(e) { - la("ds5_calibrate_sticks_begin_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - show_popup(err + e); - return false; - } -} - -async function ds5_calibrate_sticks_sample() { - la("ds5_calibrate_sticks_sample"); - var err = l("Stick calibration failed: "); - try { - // Sample - await device.sendFeatureReport(0x82, alloc_req(0x82, [3,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010101) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_sample_failed", {"d1": d1}); - show_popup(err + l("Error 2") + " (" + d1 + ")."); - return false; - } - return true; - } catch(e) { - la("ds5_calibrate_sticks_sample_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - show_popup(err + e); - return false; - } -} - -async function ds5_calibrate_sticks_end() { - la("ds5_calibrate_sticks_end"); - var err = l("Stick calibration failed: "); - try { - // Write - await device.sendFeatureReport(0x82, alloc_req(0x82, [2,1,1])) - - data = await device.receiveFeatureReport(0x83) - - if(mode == 2) { - if(data.getUint32(0, false) != 0x83010102) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 3") + " (" + d1 + ")."); - } - } else if(mode == 3) { - if(data.getUint32(0, false) != 0x83010101) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 4") + " (" + d1 + ")."); - } - - await device.sendFeatureReport(0x82, alloc_req(0x82, [2,1,1])) - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010103 && data.getUint32(0, false) != 0x83010312) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 5") + " (" + d1 + ")."); - } - } - - update_nvs_changes_status(1); - return true; - } catch(e) { - la("ds5_calibrate_sticks_end_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - show_popup(err + e); - return false; - } -} - -async function ds5_calibrate_sticks() { - la("ds5_fast_calibrate_sticks"); - var err = l("Stick calibration failed: "); - try { - set_progress(0); - - // Begin - await device.sendFeatureReport(0x82, alloc_req(0x82, [1,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010101) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 1, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 1") + " (" + d1 + ")."); - } - - set_progress(10); - - await new Promise(r => setTimeout(r, 100)); - - for(var i=0;i<3;i++) { - // Sample - await device.sendFeatureReport(0x82, alloc_req(0x82, [3,1,1])) - - // Assert - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010101) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 2, "i": i, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 2") + " (" + d1 + ")."); - } - - await new Promise(r => setTimeout(r, 500)); - set_progress(20 + i * 20); - } - - await new Promise(r => setTimeout(r, 200)); - set_progress(80); - - // Write - await device.sendFeatureReport(0x82, alloc_req(0x82, [2,1,1])) - - data = await device.receiveFeatureReport(0x83) - - if(mode == 2) { - if(data.getUint32(0, false) != 0x83010102) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 3") + " (" + d1 + ")."); - } - } else if(mode == 3) { - if(data.getUint32(0, false) != 0x83010101) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 4") + " (" + d1 + ")."); - } - - await device.sendFeatureReport(0x82, alloc_req(0x82, [2,1,1])) - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010103) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_sticks_failed", {"s": 3, "d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 5") + " (" + d1 + ")."); - } - - } - - set_progress(100); - update_nvs_changes_status(1); - - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window() - - show_popup(l("Stick calibration completed")); - } catch(e) { - la("ds5_calibrate_sticks_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(err + e); - } -} - -async function ds5_calibrate_range_begin() { - la("ds5_calibrate_range_begin"); - var err = l("Range calibration failed: "); - try { - // Begin - await device.sendFeatureReport(0x82, alloc_req(0x82, [1,1,2])) - - // Assert - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010201) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_range_begin_failed", {"d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 1") + " (" + d1 + ")."); - } - } catch(e) { - la("ds5_calibrate_range_begin_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(err + e); - } -} - -async function ds5_calibrate_range_end() { - la("ds5_calibrate_range_end"); - var err = l("Range calibration failed: "); - try { - // Write - await device.sendFeatureReport(0x82, alloc_req(0x82, [2,1,2])) - - // Assert - data = await device.receiveFeatureReport(0x83) - - if(mode == 2) { - if(data.getUint32(0, false) != 0x83010202) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_range_end_failed", {"d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 3") + " (" + d1 + ")."); - } - } else { - if(data.getUint32(0, false) != 0x83010201) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_range_end_failed", {"d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 4") + " (" + d1 + ")."); - } - - await device.sendFeatureReport(0x82, alloc_req(0x82, [2,1,2])) - data = await device.receiveFeatureReport(0x83) - if(data.getUint32(0, false) != 0x83010203) { - d1 = dec2hex32(data.getUint32(0, false)); - la("ds5_calibrate_range_end_failed", {"d1": d1}); - close_calibrate_window(); - return show_popup(err + l("Error 5") + " (" + d1 + ")."); - } - } - - update_nvs_changes_status(1); - close_calibrate_window(); - show_popup(l("Range calibration completed")); - } catch(e) { - la("ds5_calibrate_range_end_failed", {"r": e}); - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(err + e); - } -} - -async function ds5_nvlock() { - la("ds5_nvlock"); - try { - await device.sendFeatureReport(0x80, alloc_req(0x80, [3,1])) - data = await device.receiveFeatureReport(0x81) - } catch(e) { - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(l("NVS Lock failed: ") + e); - } -} - -async function ds5_edge_flash_modules() { - la("ds5_edge_flash_modules"); - var modal = null; - try { - modal = new bootstrap.Modal(document.getElementById('edgeProgressModal'), {}) - modal.show(); - - if(device != null) { - await device.sendFeatureReport(0x80, alloc_req(0x80, [21, 6, 0, 11])) - await new Promise(r => setTimeout(r, 200)); - await device.sendFeatureReport(0x80, alloc_req(0x80, [21, 5, 0])) - await new Promise(r => setTimeout(r, 200)); - - await device.sendFeatureReport(0x80, alloc_req(0x80, [21, 6, 1, 11])) - await new Promise(r => setTimeout(r, 200)); - await device.sendFeatureReport(0x80, alloc_req(0x80, [21, 5, 1])) - await new Promise(r => setTimeout(r, 200)); - } - await new Promise(r => setTimeout(r, 500)); - if(device != null) { - data = await device.receiveFeatureReport(0x81) - } - await new Promise(r => setTimeout(r, 1000)); - modal.hide(); - await ds5_reset(); - } catch(e) { - modal.hide(); - - await new Promise(r => setTimeout(r, 500)); - return show_popup(l("NVS Unlock failed: ") + e); - } -} - -async function ds5_nvunlock() { - la("ds5_nvunlock"); - try { - await device.sendFeatureReport(0x80, alloc_req(0x80, [3,2, 101, 50, 64, 12])) - data = await device.receiveFeatureReport(0x81) - } catch(e) { - await new Promise(r => setTimeout(r, 500)); - close_calibrate_window(); - return show_popup(l("NVS Unlock failed: ") + e); - } -} - -async function disconnect() { - la("disconnect"); - if(device == null) - return; - gj = 0; - update_nvs_changes_status(0); - mode = 0; - device.close(); - device = null; - disable_btn = 0; - reset_circularity(); - $("#offlinebar").show(); - $("#onlinebar").hide(); - $("#mainmenu").hide(); - $("#d-nvstatus").text = l("Unknown"); - $("#d-bdaddr").text = l("Unknown"); - close_calibrate_window(); -} - -function handleDisconnectedDevice(e) { - la("disconnected"); - console.log("Disconnected: " + e.device.productName) - disconnect(); -} - -function createCookie(name, value, days) { - var expires; - - if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = "; expires=" + date.toGMTString(); - } else { - expires = ""; - } - document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/"; -} - -function readCookie(name) { - var nameEQ = encodeURIComponent(name) + "="; - var ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) === ' ') - c = c.substring(1, c.length); - if (c.indexOf(nameEQ) === 0) - return decodeURIComponent(c.substring(nameEQ.length, c.length)); - } - return null; -} - -function eraseCookie(name) { - createCookie(name, "", -1); -} - -function welcome_modal() { - var already_accepted = readCookie("welcome_accepted"); - if(already_accepted == "1") - return; - - curModal = new bootstrap.Modal(document.getElementById('welcomeModal'), {}) - curModal.show(); -} - -function welcome_accepted() { - la("welcome_accepted"); - createCookie("welcome_accepted", "1"); - $("#welcomeModal").modal("hide"); -} - -function gboot() { - gu = crypto.randomUUID(); - $("#infoshowall").hide(); - window.addEventListener('DOMContentLoaded', function() { - lang_init(); - welcome_modal(); - $("#checkCircularity").on('change', on_circ_check_change); - on_circ_check_change(); - }); - - if (!("hid" in navigator)) { - $("#offlinebar").hide(); - $("#onlinebar").hide(); - $("#missinghid").show(); - return; - } - - $("#offlinebar").show(); - navigator.hid.addEventListener("disconnect", handleDisconnectedDevice); -} - -function alloc_req(id, data=[]) { - len = data.length; - try { - fr = device.collections[0].featureReports; - fr.forEach((e) => { if(e.reportId == id) { len = e.items[0].reportCount; }}); - } catch(e) { - console.log(e); - } - out = new Uint8Array(len); - for(i=0;i> 8) - } - await device.sendFeatureReport(0x80, alloc_req(0x80, pkg)) -} - -function refresh_finetune() { - if (!finetune_visible) - return; - if (on_finetune_updating) - return; - - on_finetune_updating = true - setTimeout(ds5_finetune_update_all, 10); -} - -function ds5_finetune_update_all() { - ds5_finetune_update("finetuneStickCanvasL", last_lx, last_ly) - ds5_finetune_update("finetuneStickCanvasR", last_rx, last_ry) -} - -function ds5_finetune_update(name, plx, ply) { - on_finetune_updating = false - var c = document.getElementById(name); - var ctx = c.getContext("2d"); - var sz = 60; - var hb = 20 + sz; - var yb = 15 + sz; - var w = c.width; - ctx.clearRect(0, 0, c.width, c.height); - ctx.lineWidth = 1; - ctx.fillStyle = '#ffffff'; - ctx.strokeStyle = '#000000'; - - // Left circle - ctx.beginPath(); - ctx.arc(hb, yb, sz, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - - ctx.strokeStyle = '#aaaaaa'; - ctx.beginPath(); - ctx.moveTo(hb-sz, yb); - ctx.lineTo(hb+sz, yb); - ctx.closePath(); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(hb, yb-sz); - ctx.lineTo(hb, yb+sz); - ctx.closePath(); - ctx.stroke(); - - ctx.fillStyle = '#000000'; - ctx.strokeStyle = '#000000'; - ctx.beginPath(); - ctx.arc(hb+plx*sz,yb+ply*sz,4, 0, 2*Math.PI); - ctx.fill(); - - ctx.beginPath(); - ctx.moveTo(hb, yb); - ctx.lineTo(hb+plx*sz, yb+ply*sz); - ctx.stroke(); - - $("#"+ name + "x-lbl").text(float_to_str(plx)); - $("#"+ name + "y-lbl").text(float_to_str(ply)); -} - -function finetune_close() { - $("#finetuneModal").modal("hide"); - finetune_visible = false - - finetune_original_data = [] -} - -function finetune_save() { - finetune_close(); - - // Unlock button - update_nvs_changes_status(1); -} - -async function finetune_cancel() { - if(finetune_original_data.length == 12) - await write_finetune_data(finetune_original_data) - - finetune_close(); -} - -var last_lx = 0, last_ly = 0, last_rx = 0, last_ry = 0; -var ll_updated = false; - -var ll_data=new Array(48); -var rr_data=new Array(48); -var enable_circ_test = false; - -function reset_circularity() { - for(i=0;i 0.2) { - lcounter += 1; - ofl += Math.pow(ll_data[i] - 1, 2); - } - for (i=0;i 0.2) { - rcounter += 1; - ofr += Math.pow(rr_data[i] - 1, 2); - } - } - if(lcounter > 0) - ofl = Math.sqrt(ofl / lcounter) * 100; - if(rcounter > 0) - ofr = Math.sqrt(ofr / rcounter) * 100; - - el = ofl.toFixed(2) + "%"; - er = ofr.toFixed(2) + "%"; - $("#el-lbl").text(el); - $("#er-lbl").text(er); - } -} - -function circ_checked() { return $("#checkCircularity").is(':checked') } - -function on_circ_check_change() { - enable_circ_test = circ_checked(); - for(i=0;i= -0.004) return "+0.00"; - return (f<0?"":"+") + f.toFixed(2); -} - -var on_delay = false; - -function timeout_ok() { - on_delay = false; - if(ll_updated) - refresh_stick_pos(); -} - -function refresh_sticks() { - if(on_delay) - return; - - refresh_stick_pos(); - on_delay = true; - setTimeout(timeout_ok, 20); -} - -var last_bat_txt = ""; -var last_bat_disable = null; - -function bat_percent_to_text(bat_charge, is_charging, is_error) { - var icon_txt = ""; - - if(bat_charge < 20) { - icon_txt = 'fa-battery-empty'; - } else if(bat_charge < 40) { - icon_txt = 'fa-battery-quarter'; - } else if(bat_charge < 60) { - icon_txt = 'fa-battery-half'; - } else if(bat_charge < 80) { - icon_txt = 'fa-battery-three-quarters'; - } else { - icon_txt = 'fa-battery-full'; - } - - var icon_full = ''; - var bolt_txt = ''; - if(is_charging) - bolt_txt = ''; - bat_txt = bat_charge + "%" + ' ' + bolt_txt + ' ' + icon_full; - - if(is_error) { - bat_txt = '' + l("error") + ''; - } - return bat_txt; -} - -function update_nvs_changes_status(new_value) { - if (new_value == has_changes_to_write) - return; - - if (new_value == 1) { - $("#savechanges").prop("disabled", false); - $("#savechanges").addClass("btn-success").removeClass("btn-outline-secondary"); - } else { - $("#savechanges").prop("disabled", true); - $("#savechanges").removeClass("btn-success").addClass("btn-outline-secondary"); - } - - has_changes_to_write = new_value; -} - -function update_battery_status(bat_capacity, cable_connected, is_charging, is_error) { - var bat_txt = bat_percent_to_text(bat_capacity, is_charging); - var can_use_tool = (bat_capacity >= 30 && cable_connected && !is_error); - - if(bat_txt != last_bat_txt) { - $("#d-bat").html(bat_txt); - last_bat_txt = bat_txt; - } -} - -function process_ds4_input(data) { - var lx = data.data.getUint8(0); - var ly = data.data.getUint8(1); - var rx = data.data.getUint8(2); - var ry = data.data.getUint8(3); - - var new_lx = Math.round((lx - 127.5) / 128 * 100) / 100; - var new_ly = Math.round((ly - 127.5) / 128 * 100) / 100; - var new_rx = Math.round((rx - 127.5) / 128 * 100) / 100; - var new_ry = Math.round((ry - 127.5) / 128 * 100) / 100; - - if(last_lx != new_lx || last_ly != new_ly || last_rx != new_rx || last_ry != new_ry) { - last_lx = new_lx; - last_ly = new_ly; - last_rx = new_rx; - last_ry = new_ry; - ll_updated = true; - refresh_sticks(); - } - - // Read battery - var bat = data.data.getUint8(29); - var bat_data = bat & 0x0f; - var bat_status = (bat >> 4) & 1; - - var bat_capacity = 0; - var cable_connected = false; - var is_charging = false; - var is_error = false; - - if(bat_status == 1) { - cable_connected = true; - if(bat_data < 10) { - bat_capacity = Math.min(bat_data * 10 + 5, 100); - is_charging = true; - } else if(bat_data == 10) { - bat_capacity = 100; - is_charging = true; - } else if(bat_data == 11) { - bat_capacity = 100; - // charged - } else { - // error - bat_capacity = 0; - is_error = true; - } - } else { - cable_connected = false; - if(bat_data < 10) { - bat_capacity = bat_data * 10 + 5; - } else { - bat_capacity = 100; - } - } - - update_battery_status(bat_capacity, cable_connected, is_charging, is_error); -} - -function process_ds_input(data) { - var lx = data.data.getUint8(0); - var ly = data.data.getUint8(1); - var rx = data.data.getUint8(2); - var ry = data.data.getUint8(3); - - var new_lx = Math.round((lx - 127.5) / 128 * 100) / 100; - var new_ly = Math.round((ly - 127.5) / 128 * 100) / 100; - var new_rx = Math.round((rx - 127.5) / 128 * 100) / 100; - var new_ry = Math.round((ry - 127.5) / 128 * 100) / 100; - - if(last_lx != new_lx || last_ly != new_ly || last_rx != new_rx || last_ry != new_ry) { - last_lx = new_lx; - last_ly = new_ly; - last_rx = new_rx; - last_ry = new_ry; - ll_updated = true; - refresh_sticks(); - refresh_finetune(); - } - - var bat = data.data.getUint8(52); - var bat_charge = bat & 0x0f; - var bat_status = bat >> 4; - - var bat_capacity = 0; - var cable_connected = false; - var is_charging = false; - var is_error = false; - - if(bat_status == 0) { - bat_capacity = Math.min(bat_charge * 10 + 5, 100); - } else if(bat_status == 1) { - bat_capacity = Math.max(bat_charge * 10 + 5, 100); - is_charging = true; - cable_connected = true; - } else if(bat_status == 2) { - bat_capacity = 100; - cable_connected = true; - } else { - is_error = true; - } - - update_battery_status(bat_capacity, cable_connected, is_charging, is_error); -} - -async function continue_connection(report) { - try { - device.oninputreport = null; - var reportLen = report.data.byteLength; - - var connected = false; - - // Detect if the controller is connected via USB - if(reportLen != 63) { - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - disconnect(); - show_popup(l("Please connect the device using a USB cable.")) - return; - } - - if(device.productId == 0x05c4) { - $("#infoshowall").hide() - $("#ds5finetune").hide() - if(await ds4_info()) { - connected = true; - mode = 1; - devname = l("Sony DualShock 4 V1"); - device.oninputreport = process_ds4_input; - } - } else if(device.productId == 0x09cc) { - $("#infoshowall").hide() - $("#ds5finetune").hide() - if(await ds4_info()) { - connected = true; - mode = 1; - devname = l("Sony DualShock 4 V2"); - device.oninputreport = process_ds4_input; - } - } else if(device.productId == 0x0ce6) { - $("#infoshowall").show() - $("#ds5finetune").show() - if(await ds5_info()) { - connected = true; - mode = 2; - devname = l("Sony DualSense"); - device.oninputreport = process_ds_input; - } - } else if(device.productId == 0x0df2) { - $("#infoshowall").hide() - $("#ds5finetune").hide() - if(await ds5_info()) { - connected = true; - mode = 3; - devname = l("Sony DualSense Edge"); - } - - n = await ds5_nvstatus(); - if(n == 4) { - // dualsense edge with pending reboot - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - disconnect(); - show_popup(l("A reboot is needed to continue using this DualSense Edge. Please disconnect and reconnect your controller.")); - return; - } - - device.oninputreport = process_ds_input; - } else { - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - show_popup(l("Connected invalid device: ") + dec2hex(device.vendorId) + ":" + dec2hex(device.productId)) - disconnect(); - return; - } - - if(connected) { - $("#devname").text(devname + " (" + dec2hex(device.vendorId) + ":" + dec2hex(device.productId) + ")"); - $("#offlinebar").hide(); - $("#onlinebar").show(); - $("#mainmenu").show(); - $("#resetBtn").show(); - $("#d-nvstatus").text = l("Unknown"); - $("#d-bdaddr").text = l("Unknown"); - } else { - show_popup(l("Connected invalid device: ") + l("Error 1")); - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - disconnect(); - return; - } - - if(mode == 3) - show_edge_modal(); - - if(disable_btn != 0) - update_disable_btn(); - - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - } catch(error) { - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - show_popup(l("Error: ") + error); - return; - } -} - -function update_disable_btn() { - if(disable_btn == last_disable_btn) - return; - - if(disable_btn == 0) { - $(".ds-btn").prop("disabled", false); - last_disable_btn = 0; - return; - } - - $(".ds-btn").prop("disabled", true); - - // show only one popup - if(disable_btn & 1 && !(last_disable_btn & 1)) { - show_popup(l("The device appears to be a DS4 clone. All functionalities are disabled.")); - } else if(disable_btn & 2 && !(last_disable_btn & 2)) { - show_popup(l("This DualSense controller has outdated firmware.") + "
" + l("Please update the firmware and try again."), true); - } else if(disable_btn & 4 && !(last_disable_btn & 4)) { - show_popup(l("Please charge controller battery over 30% to use this tool.")); - } - last_disable_btn = disable_btn; -} - -async function connect() { - gj = crypto.randomUUID(); - - // This trigger default disable - has_changes_to_write = -1; - update_nvs_changes_status(0); - - reset_circularity(); - la("begin"); - last_bat_txt = ""; - try { - $("#btnconnect").prop("disabled", true); - $("#connectspinner").show(); - await new Promise(r => setTimeout(r, 100)); - - let ds4v1 = { vendorId: 0x054c, productId: 0x05c4 }; - let ds4v2 = { vendorId: 0x054c, productId: 0x09cc }; - let ds5 = { vendorId: 0x054c, productId: 0x0ce6 }; - let ds5edge = { vendorId: 0x054c, productId: 0x0df2 }; - let requestParams = { filters: [ds4v1,ds4v2,ds5,ds5edge] }; - - var devices = await navigator.hid.getDevices(); - if (devices.length == 0) { - devices = await navigator.hid.requestDevice(requestParams); - } - - if (devices.length == 0) { - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - return; - } - - if (devices.length > 1) { - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - show_popup(l("Please connect only one controller at time.")); - return; - } - - await devices[0].open(); - - device = devices[0] - la("connect", {"p": device.productId, "v": device.vendorId}); - - device.oninputreport = continue_connection - - } catch(error) { - $("#btnconnect").prop("disabled", false); - $("#connectspinner").hide(); - show_popup(l("Error: ") + error); - return; - } -} - -var curModal = null - -async function multi_flash() { - if(mode == 1) - ds4_flash(); - else if(mode == 2) - ds5_flash(); - else if(mode == 3) - ds5_edge_flash(); - update_nvs_changes_status(0); -} - -async function multi_reset() { - if(mode == 1) - ds4_reset(); - else - ds5_reset(); -} - -async function multi_nvstatus() { - if(mode == 1) - ds4_nvstatus(); - else - ds5_nvstatus(); -} - -async function multi_nvsunlock() { - if(mode == 1) { - await ds4_nvunlock(); - await ds4_nvstatus(); - } else { - await ds5_nvunlock(); - await ds5_nvstatus(); - } -} - -async function multi_nvslock() { - if(mode == 1) { - await ds4_nvlock(); - await ds4_nvstatus(); - } else { - await ds5_nvlock(); - await ds5_nvstatus(); - } -} - -async function multi_calib_sticks_begin() { - if(mode == 1) - return ds4_calibrate_sticks_begin(); - else - return ds5_calibrate_sticks_begin(); -} - -async function multi_calib_sticks_end() { - if(mode == 1) - await ds4_calibrate_sticks_end(); - else - await ds5_calibrate_sticks_end(); - on_circ_check_change(); -} - -async function multi_calib_sticks_sample() { - if(mode == 1) - return ds4_calibrate_sticks_sample(); - else - return ds5_calibrate_sticks_sample(); -} - -async function multi_calibrate_range() { - if(mode == 0) - return; - - set_progress(0); - curModal = new bootstrap.Modal(document.getElementById('rangeModal'), {}) - curModal.show(); - - await new Promise(r => setTimeout(r, 1000)); - - if(mode == 1) - ds4_calibrate_range_begin(); - else - ds5_calibrate_range_begin(); -} - -async function multi_calibrate_range_on_close() { - if(mode == 1) - await ds4_calibrate_range_end(); - else - await ds5_calibrate_range_end(); - on_circ_check_change(); -} - - -async function multi_calibrate_sticks() { - if(mode == 0) - return; - - set_progress(0); - curModal = new bootstrap.Modal(document.getElementById('calibrateModal'), {}) - curModal.show(); - - await new Promise(r => setTimeout(r, 1000)); - - if(mode == 1) - ds4_calibrate_sticks(); - else - ds5_calibrate_sticks(); -} - -function close_calibrate_window() { - if (curModal != null) { - curModal.hide(); - curModal = null; - } - - $("#calibCenterModal").modal("hide"); - cur_calib = 0; - return; -} - -function set_progress(i) { - $(".progress-bar").css('width', '' + i + '%') -} - -function clear_info() { - $("#fwinfo").html(""); - $("#fwinfoextra-hw").html(""); - $("#fwinfoextra-fw").html(""); -} - -function append_info_extra(key, value, cat) { - // TODO escape html - var s = '
' + key + '
' + value + '
'; - $("#fwinfoextra-" + cat).html($("#fwinfoextra-" + cat).html() + s); -} - - -function append_info(key, value, cat) { - // TODO escape html - var s = '
' + key + '
' + value + '
'; - $("#fwinfo").html($("#fwinfo").html() + s); - append_info_extra(key, value, cat); -} - -function show_popup(text, is_html = false) { - if(is_html) { - $("#popupBody").html(text); - } else { - $("#popupBody").text(text); - } - new bootstrap.Modal(document.getElementById('popupModal'), {}).show() -} - -function show_faq_modal() { - la("faq_modal"); - new bootstrap.Modal(document.getElementById('faqModal'), {}).show() -} - -function show_donate_modal() { - la("donate_modal"); - new bootstrap.Modal(document.getElementById('donateModal'), {}).show() -} - -function show_edge_modal() { - la("edge_modal"); - new bootstrap.Modal(document.getElementById('edgeModal'), {}).show() -} - -function show_info_modal() { - la("info_modal"); - new bootstrap.Modal(document.getElementById('infoModal'), {}).show() -} - -function discord_popup() { - la("discord_popup"); - show_popup(l("My handle on discord is: the_al")); -} - -function board_model_info() { - la("bm_info"); - l1 = l("This feature is experimental."); - l2 = l("Please let me know if the board model of your controller is not detected correctly."); - l3 = l("Board model detection thanks to") + ' Battle Beaver Customs.'; - show_popup(l3 + "

" + l1 + " " + l2, true); -} - -function close_new_calib() { - $("#calibCenterModal").modal("hide"); - cur_calib = 0; -} - -async function calib_step(i) { - la("calib_step", {"i": i}) - if(i < 1 || i > 7) return; - - var ret = true; - if(i >= 2 && i <= 6) { - $("#btnSpinner").show(); - $("#calibNext").prop("disabled", true); - } - - if(i == 2) { - $("#calibNextText").text(l("Initializing...")); - await new Promise(r => setTimeout(r, 100)); - ret = await multi_calib_sticks_begin(); - } else if(i == 6) { - $("#calibNextText").text(l("Sampling...")); - await new Promise(r => setTimeout(r, 100)); - ret = await multi_calib_sticks_sample(); - await new Promise(r => setTimeout(r, 100)); - $("#calibNextText").text(l("Storing calibration...")); - await new Promise(r => setTimeout(r, 100)); - ret = await multi_calib_sticks_end(); - } else if(i > 2 && i < 6){ - $("#calibNextText").text(l("Sampling...")); - await new Promise(r => setTimeout(r, 100)); - ret = await multi_calib_sticks_sample(); - } - if(i >= 2 && i <= 6) { - await new Promise(r => setTimeout(r, 200)); - $("#calibNext").prop("disabled", false); - $("#btnSpinner").hide(); - } - - if(ret == false) { - close_new_calib(); - return; - } - - for(j=1;j<7;j++) { - $("#list-" + j).hide(); - $("#list-" + j + "-calib").removeClass("active"); - } - - $("#list-" + i).show(); - $("#list-" + i + "-calib").addClass("active"); - - if(i == 1) { - $("#calibTitle").text(l("Stick center calibration")); - $("#calibNextText").text(l("Start")); - } - else if(i == 6) { - $("#calibTitle").text(l("Stick center calibration")); - $("#calibNextText").text(l("Done")); - } - else { - $("#calibTitle").html(l("Calibration in progress")); - $("#calibNextText").text(l("Continue")); - } - if(i == 1 || i == 6) - $("#calibCross").show(); - else - $("#calibCross").hide(); - -} - -var cur_calib = 0; -async function calib_open() { - la("calib_open"); - cur_calib = 0; - await calib_next(); - new bootstrap.Modal(document.getElementById('calibCenterModal'), {}).show() -} - -async function calib_next() { - la("calib_next"); - if(cur_calib == 6) { - close_new_calib() - return; - } - if(cur_calib < 6) { - cur_calib += 1; - await calib_step(cur_calib); - } -} - -function la(k,v={}) { - $.ajax({type: 'POST', url:"https://the.al/ds4_a/l", - data: JSON.stringify( {"u": gu, "j": gj, "k": k, "v": v}), - contentType: "application/json", dataType: 'json'}); -} - -function lf(k, f) { la(k, buf2hex(f.buffer)); return f; } - -function lang_init() { - var id_iter = 0; - var items = document.getElementsByClassName('ds-i18n'); - for(i=0; iEnglish'; - for(i=0;i' + name + ''; - } - olangs += '
  • '; - olangs += '
  • Missing your language?
  • '; - $("#availLangs").html(olangs); - -} - -function lang_set(l, skip_modal=false) { - la("lang_set", {"l": l}) - if(l == "en_us") { - lang_reset_page(); - } else { - var file = available_langs[l]["file"]; - var direction = available_langs[l]["direction"]; - lang_translate(file, l, direction); - } - - createCookie("force_lang", l); - if(!skip_modal) { - createCookie("welcome_accepted", "0"); - welcome_modal(); - } -} - -function lang_reset_page() { - lang_set_direction("ltr", "en_us"); - var items = document.getElementsByClassName('ds-i18n'); - for(i=0; i 0) { - lang_disabled = false; - } - - var items = document.getElementsByClassName('ds-i18n'); - for(i=0; i 0) { - $(item).html(tnew[0]); - } else { - console.log("Cannot find mapping for " + old); - $(item).html(old); - } - } - var old_title = lang_orig_text[".title"]; - document.title = lang_cur[old_title]; - if(lang_cur[".authorMsg"] !== undefined) { - $("#authorMsg").html(lang_cur[".authorMsg"]); - } - $("#curLang").html(available_langs[target_lang]["name"]); - }); - -} diff --git a/css/finetune.css b/css/finetune.css new file mode 100644 index 0000000..73f5f8a --- /dev/null +++ b/css/finetune.css @@ -0,0 +1,150 @@ +/* Styles for fine-tuning interface */ + +/* Styling for coordinate labels - base state to prevent layout shift */ +#finetuneStickCanvasLx-lbl, +#finetuneStickCanvasLy-lbl, +#finetuneStickCanvasRx-lbl, +#finetuneStickCanvasRy-lbl { + padding: 2px 4px !important; + border-radius: 3px !important; + background-color: transparent !important; +} + +/* Styling for finetune input boxes - base state to prevent layout shift */ +input[id^="finetune"] { + border: 1px solid transparent !important; + width: 90px !important; + min-width: 90px !important; + color: #969696 !important; +} + +/* Styling for highlighted coordinate labels */ +#finetuneStickCanvasLx-lbl.text-primary, +#finetuneStickCanvasLy-lbl.text-primary, +#finetuneStickCanvasRx-lbl.text-primary, +#finetuneStickCanvasRy-lbl.text-primary { + color: #0d6efd !important; + background-color: rgba(13, 110, 253, 0.1) !important; +} + +/* CSS Grid layout for finetune inputs around canvas */ +.finetune-grid { + display: grid; + grid-template-columns: 1fr auto 1fr; + grid-template-rows: auto 1fr auto; + grid-template-areas: + ". top ." + "left center right" + ". bottom ."; + justify-items: center; + align-items: center; + width: 100%; + margin: 0 auto; + max-width: fit-content; +} + +.finetune-top { + grid-area: top; +} + +.finetune-left { + grid-area: left; +} + +.finetune-center { + grid-area: center; +} + +.finetune-right { + grid-area: right; +} + +.finetune-bottom { + grid-area: bottom; +} + +/* Finetune mode visibility controls */ +.finetune-center-mode { + display: block; +} + +.finetune-circularity-mode { + display: none; +} + +/* When circularity mode is active */ +#finetuneModal.circularity-mode .finetune-center-mode { + display: none; +} + +#finetuneModal.circularity-mode .finetune-circularity-mode { + display: block; +} + +/* Hide raw numbers mode - hide input boxes when checkbox is unchecked */ +#finetuneModal.hide-raw-numbers .finetune-top, +#finetuneModal.hide-raw-numbers .finetune-left, +#finetuneModal.hide-raw-numbers .finetune-right, +#finetuneModal.hide-raw-numbers .finetune-bottom { + display: none; +} + +/* Hide spacers when raw numbers are hidden */ +#finetuneModal.hide-raw-numbers .spacer.hide-raw-numbers { + display: none; +} + +/* Adjust grid layout when raw numbers are hidden - center the canvas */ +#finetuneModal.hide-raw-numbers .finetune-grid { + grid-template-columns: 1fr; + grid-template-rows: 1fr; + grid-template-areas: "center"; +} + +/* when element with id finetuneModal has class hide-raw-numbers, hide all elements with id finetuneStickCanvasL and finetuneStickCanvasR */ +#finetuneModal.hide-raw-numbers #finetuneStickCanvasL, +#finetuneModal.hide-raw-numbers #finetuneStickCanvasR { + display: none; +} +#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasL, +#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasR { + display: block; +} +#finetuneModal.hide-raw-numbers #finetuneStickCanvasL_large, +#finetuneModal.hide-raw-numbers #finetuneStickCanvasR_large +{ + display: block; +} +#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasL_large, +#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasR_large { + display: none; +} + +/* Error slack button toggle states */ +.left-stick-values, +.right-stick-values { + display: block; +} + +.left-stick-slider, +.right-stick-slider { + display: none; +} + +/* When left stick is in slider mode (only in circularity mode) */ +#finetuneModal.circularity-mode #left-stick-card.show-slider .left-stick-values { + display: none !important; +} + +#finetuneModal.circularity-mode #left-stick-card.show-slider .left-stick-slider.finetune-circularity-mode { + display: block !important; +} + +/* When right stick is in slider mode (only in circularity mode) */ +#finetuneModal.circularity-mode #right-stick-card.show-slider .right-stick-values { + display: none !important; +} + +#finetuneModal.circularity-mode #right-stick-card.show-slider .right-stick-slider.finetune-circularity-mode { + display: block !important; +} \ No newline at end of file diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..bbcc3e2 --- /dev/null +++ b/css/main.css @@ -0,0 +1,130 @@ +/* Main styles for DualShock Calibration GUI */ + +/* Add padding to body to prevent content from being hidden behind fixed footer */ +body { + padding-bottom: 80px; +} + +dl.row dt { + font-weight: normal; +} +dl.row dd { + font-family: monospace; +} + +#left-stick-card, +#right-stick-card { + cursor: pointer; + outline: none !important; +} + +.stick-card-active { + border: 1px solid #0d6efd !important; + box-shadow: 0 0 10px rgba(13, 110, 253, 0.3) !important; +} + +.stick-card-active .card-header { + background-color: #0d6efd !important; + color: white !important; +} + +/* Quick Test Icon Animations */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); } + 20%, 40%, 60%, 80% { transform: translateX(2px); } +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.8; } +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } + 40% { transform: translateY(-3px); } + 60% { transform: translateY(-2px); } +} + +@keyframes glow { + 0%, 100% { text-shadow: 0 0 5px rgba(13, 110, 253, 0.5); } + 50% { text-shadow: 0 0 15px rgba(13, 110, 253, 0.8), 0 0 25px rgba(13, 110, 253, 0.6); } +} + +/* Animation classes for different test types - only animate when accordion is expanded */ +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-usb { + animation: pulse 1s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-buttons { + animation: bounce 0.6s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-haptic { + animation: shake 0.5s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-adaptive { + animation: pulse 1s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-speaker { + animation: bounce 0.6s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-microphone { + animation: glow 1.5s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-lights { + animation: glow 1.2s ease-in-out infinite !important; +} + +.accordion-item:has(.accordion-collapse.show) i.fas.test-icon-headphone { + animation: pulse 1s ease-in-out infinite !important; +} + +/* Quick Test accordion button height reduction */ +#quickTestAccordion .accordion-button { + padding: 0.5rem 1rem; +} + +/* Quick Test accordion body tint */ +#quickTestAccordion .accordion-collapse .accordion-body { + background-color: rgba(13, 125, 253, 0.05); +} + +/* Skip button hover behavior */ +.skip-btn { + opacity: 0; + transition: opacity 0.2s ease-in-out; +} + +.accordion-header:hover .skip-btn { + opacity: 1; +} + +/* Blinking animation for range calibration alert */ +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.3; } +} + +.blink-text { + animation: blink 1s infinite; +} + +/* Pulsing animation for text */ +@keyframes pulse-text { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.pulsing-text { + animation: pulse-text 0.75s ease-in-out infinite; +} + +/* Set text color to red for internationalized elements */ +/* .ds-i18n { + color: red; +} */ diff --git a/dev-server.js b/dev-server.js new file mode 100644 index 0000000..8665830 --- /dev/null +++ b/dev-server.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +import https from 'https'; +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Configuration +const config = { + port: process.env.PORT || 8443, + httpPort: process.env.HTTP_PORT || 8080, + host: process.env.HOST || 'localhost', + distDir: path.join(__dirname, 'dist'), + certFile: path.join(__dirname, 'server.crt'), + keyFile: path.join(__dirname, 'server.key'), + useHttps: process.env.HTTPS === 'true' +}; + +// MIME types +const mimeTypes = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.webmanifest': 'application/manifest+json' +}; + +function getMimeType(filePath) { + const ext = path.extname(filePath).toLowerCase(); + return mimeTypes[ext] || 'application/octet-stream'; +} + +function requestHandler(req, res) { + // Parse URL and remove query parameters + let urlPath = new URL(req.url, `http://${req.headers.host}`).pathname; + + // Default to index.html for root requests + if (urlPath === '/') { + urlPath = '/index.html'; + } + + const filePath = path.join(config.distDir, urlPath); + const mimeType = getMimeType(filePath); + + // Security check - ensure file is within dist directory + if (!filePath.startsWith(config.distDir)) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Forbidden'); + return; + } + + // Set CORS headers for development + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Disable caching for development + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + + // Handle OPTIONS requests + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + fs.readFile(filePath, (err, data) => { + if (err) { + if (err.code === 'ENOENT') { + // Try to serve index.html for SPA routing + const indexPath = path.join(config.distDir, 'index.html'); + fs.readFile(indexPath, (indexErr, indexData) => { + if (indexErr) { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } else { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(indexData); + } + }); + } else { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } + } else { + res.writeHead(200, { 'Content-Type': mimeType }); + res.end(data); + } + }); +} + +function startServer() { + // Check if dist directory exists + if (!fs.existsSync(config.distDir)) { + console.error(`❌ Dist directory not found: ${config.distDir}`); + console.log('💡 Run "npm run build" first to build the application'); + process.exit(1); + } + + if (config.useHttps) { + // Check if SSL certificates exist + if (!fs.existsSync(config.certFile) || !fs.existsSync(config.keyFile)) { + console.error('❌ SSL certificates not found'); + console.log('💡 SSL certificates are required for WebHID API'); + console.log(' Make sure server.crt and server.key exist in the project root'); + process.exit(1); + } + + // Read SSL certificates + const options = { + key: fs.readFileSync(config.keyFile), + cert: fs.readFileSync(config.certFile) + }; + + // Create HTTPS server + const server = https.createServer(options, requestHandler); + + server.listen(config.port, config.host, () => { + console.log('🚀 Development server started!'); + console.log(`📱 App running at: https://${config.host}:${config.port}`); + console.log('🔒 HTTPS enabled (required for WebHID API)'); + console.log('💡 Press Ctrl+C to stop the server'); + console.log(''); + console.log('📝 Note: You may need to accept the self-signed certificate in your browser'); + }); + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`❌ Port ${config.port} is already in use`); + console.log('💡 Try using a different port: PORT=8444 npm run serve:https'); + } else { + console.error('❌ Server error:', err.message); + } + process.exit(1); + }); + } else { + // Create HTTP server (for testing only - WebHID won't work) + const server = http.createServer(requestHandler); + + server.listen(config.httpPort, config.host, () => { + console.log('🚀 Development server started!'); + console.log(`📱 App running at: http://${config.host}:${config.httpPort}`); + console.log('⚠️ HTTP mode - WebHID API will only work on localhost'); + console.log('💡 Use "npm run serve:https" to enable WebHID support to other clients on the local network'); + console.log('💡 Press Ctrl+C to stop the server'); + }); + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`❌ Port ${config.httpPort} is already in use`); + console.log('💡 Try using a different port: HTTP_PORT=8081 npm run serve'); + } else { + console.error('❌ Server error:', err.message); + } + process.exit(1); + }); + } +} + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\n👋 Shutting down development server...'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\n👋 Shutting down development server...'); + process.exit(0); +}); + +// Start the server +startServer(); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..8e80c6f --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,318 @@ +import gulp from 'gulp'; +import cleanCSS from 'gulp-clean-css'; +import htmlmin from 'gulp-htmlmin'; +import concat from 'gulp-concat'; +import sourcemaps from 'gulp-sourcemaps'; +import gulpif from 'gulp-if'; +import rename from 'gulp-rename'; +import { deleteAsync } from 'del'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import replace from 'gulp-replace'; +import jsonMinify from 'gulp-json-minify'; +import crypto from 'crypto'; +import { rollup } from 'rollup'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import rollupTerser from '@rollup/plugin-terser'; +import fs from 'fs/promises'; +import path from 'path'; +import { glob } from 'glob'; + +// Get command line arguments +const argv = yargs(hideBin(process.argv)).argv; +const isProduction = argv.production || process.env.NODE_ENV === 'production'; + +// Paths configuration +const paths = { + src: { + js: { + entry: 'js/core.js', + all: 'js/**/*.js' + }, + css: ['css/main.css', 'css/finetune.css'], + html: { + main: 'index.html', + templates: 'templates/**/*.html' + }, + lang: 'lang/**/*.json', + assets: [ + 'favicon.ico', + 'favicon.svg', + 'favicon-16x16.png', + 'favicon-32x32.png', + 'favicon-96x96.png', + 'apple-touch-icon.png', + 'web-app-manifest-192x192.png', + 'web-app-manifest-512x512.png', + 'site.webmanifest', + 'assets/donate.png', + 'googlec4c2e36a49e62fa3.html', + 'fa.min.css' + ], + svg: 'assets/**/*.svg' + }, + dist: 'dist', + temp: '.tmp' +}; + +// Clean task +function clean() { + return deleteAsync([paths.dist, paths.temp]); +} + +// JavaScript bundling with Rollup +async function scripts() { + const inputOptions = { + input: paths.src.js.entry, + plugins: [ + nodeResolve(), + ...(isProduction ? [ + rollupTerser({ + compress: { + drop_console: false, + drop_debugger: true, + pure_funcs: ['console.debug', 'console.trace'], + passes: 2, + unsafe: true, + unsafe_comps: true, + unsafe_math: true, + unsafe_proto: true + }, + mangle: { + reserved: [ + 'gboot', 'connect', 'disconnect', 'disconnectSync', + 'show_faq_modal', 'calibrate_stick_centers', 'calibrate_range', + 'ds5_finetune', 'auto_calibrate_stick_centers', 'flash_all_changes', + 'reboot_controller', 'welcome_accepted', 'show_info_tab', + 'nvslock', 'nvsunlock', 'refresh_nvstatus', 'show_edge_modal', + 'show_donate_modal' + ], + properties: { + regex: /^_/ + } + }, + format: { + comments: false + } + }) + ] : []) + ] + }; + + let filename = 'app.js'; + + if (isProduction) { + const bundle = await rollup(inputOptions); + const { output } = await bundle.generate({ format: 'es' }); + const code = output[0].code; + const hash = crypto.createHash('md5').update(code).digest('hex').substring(0, 8); + filename = `app-${hash}.js`; + + await fs.writeFile(path.join(paths.dist, filename), code); + await bundle.close(); + } else { + const outputOptions = { + file: path.join(paths.dist, filename), + format: 'es', + sourcemap: true + }; + + const bundle = await rollup(inputOptions); + await bundle.write(outputOptions); + await bundle.close(); + } + + // Store the filename for HTML processing + global.jsFilename = filename; + return Promise.resolve(); +} + +// CSS processing +function styles() { + let stream = gulp.src(paths.src.css) + .pipe(gulpif(!isProduction, sourcemaps.init())) + .pipe(concat('app.css')); + + if (isProduction) { + stream = stream.pipe(cleanCSS({ + level: 2 + })); + + // Add hash to filename in production + stream = stream.pipe(rename(function(path) { + const hash = crypto.createHash('md5').update(Date.now().toString()).digest('hex').substring(0, 8); + path.basename = `app-${hash}`; + global.cssFilename = `${path.basename}.css`; + })); + } else { + stream = stream.pipe(sourcemaps.write('.')); + global.cssFilename = 'app.css'; + } + + return stream.pipe(gulp.dest(paths.dist)); +} + +// Bundle templates and SVG assets into HTML for production +async function bundleAssets() { + if (!isProduction) { + return Promise.resolve(); + } + + try { + // Read all template files + const templateFiles = await glob('templates/**/*.html'); + const templates = {}; + + for (const templateFile of templateFiles) { + const content = await fs.readFile(templateFile, 'utf8'); + const templateName = path.basename(templateFile, '.html'); + templates[templateName] = content; + } + + // Read SVG assets + const svgFiles = await glob('assets/**/*.svg'); + const svgAssets = {}; + + for (const svgFile of svgFiles) { + const content = await fs.readFile(svgFile, 'utf8'); + const assetName = path.relative('assets', svgFile); + svgAssets[assetName] = content; + } + + // Create the bundled assets object + const bundledAssets = { + templates, + svg: svgAssets + }; + + // Store for use in HTML processing + global.bundledAssets = bundledAssets; + return Promise.resolve(); + } catch (error) { + console.error('Error bundling assets:', error); + throw error; + } +} + +// HTML processing +async function html() { + const jsFile = global.jsFilename || 'app.js'; + const cssFile = global.cssFilename || 'app.css'; + + let htmlContent = await fs.readFile(paths.src.html.main, 'utf8'); + + // Replace script and CSS references + htmlContent = htmlContent.replace('', ``); + htmlContent = htmlContent.replace('', ''); + htmlContent = htmlContent.replace('', ``); + + // In production, inject bundled assets + if (isProduction && global.bundledAssets) { + const bundledAssetsScript = ` + `; + + // Insert the bundled assets script before the main app script + htmlContent = htmlContent.replace( + ``, + `${bundledAssetsScript}\n` + ); + } + + if (isProduction) { + // Use htmlmin to minify the content + const htmlMinify = (await import('html-minifier-terser')).minify; + htmlContent = await htmlMinify(htmlContent, { + caseSensitive: false, + collapseBooleanAttributes: true, + collapseInlineTagWhitespace: false, + collapseWhitespace: true, + conservativeCollapse: false, + decodeEntities: true, + html5: true, + includeAutoGeneratedTags: true, + keepClosingSlash: false, + minifyCSS: true, + minifyJS: true, + minifyURLs: false, + preserveLineBreaks: false, + preventAttributesEscaping: false, + processConditionalComments: false, + removeAttributeQuotes: true, + removeComments: true, + removeEmptyAttributes: true, + removeEmptyElements: false, + removeOptionalTags: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + removeTagWhitespace: false, + sortAttributes: true, + sortClassName: true, + trimCustomFragments: true, + useShortDoctype: true + }); + } + + // Write the processed HTML file + await fs.writeFile(path.join(paths.dist, 'index.html'), htmlContent); + return Promise.resolve(); +} + +// Template processing (only for development builds) +function templates() { + if (isProduction) { + // In production, templates are bundled into the HTML file + return Promise.resolve(); + } + + return gulp.src(paths.src.html.templates) + .pipe(gulp.dest(`${paths.dist}/templates`)); +} + +// Language files processing +function languages() { + return gulp.src(paths.src.lang) + .pipe(gulpif(isProduction, jsonMinify())) + .pipe(gulp.dest(`${paths.dist}/lang`)); +} + +// Copy assets (SVGs are bundled in production) +function assets() { + if (isProduction) { + // In production, SVGs are bundled into the HTML file, so only copy other assets + return gulp.src(paths.src.assets, { base: '.', encoding: false }) + .pipe(gulp.dest(paths.dist)); + } + + return gulp.src([...paths.src.assets, paths.src.svg], { base: '.', encoding: false }) + .pipe(gulp.dest(paths.dist)); +} + +// Watch task +function watch() { + gulp.watch(paths.src.js.all, scripts); + gulp.watch(paths.src.css, styles); + gulp.watch([paths.src.html.main, paths.src.html.templates], gulp.series(html, templates)); + gulp.watch(paths.src.lang, languages); + gulp.watch([...paths.src.assets, paths.src.svg], assets); +} + +// Development task +function dev() { + console.log('🚀 Development mode - watching files for changes...'); + return watch(); +} + +// Build task +const build = gulp.series( + clean, + gulp.parallel(scripts, styles), + bundleAssets, + gulp.parallel(html, templates, languages, assets) +); + +// Export tasks +export { clean, scripts, styles, bundleAssets, html, templates, languages, assets, watch, dev, build }; +export default build; \ No newline at end of file diff --git a/index.html b/index.html index 8d7e970..10b9799 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ DualShock Calibration GUI @@ -15,7 +15,7 @@ integrity="sha384-NvKbDTEnL+A8F/AA5Tc5kmMLSJHUO868P+lDtTpJIeQdGYaUIuLr4lVGOEA1OcMy" crossorigin="anonymous"> - @@ -26,7 +26,7 @@ crossorigin="anonymous"> - + @@ -38,802 +38,378 @@ - + + + - - - - - - - + - - - - - - - - - - - - - - - - - - - + +
    - -
    - - - - - - - -