mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-04-25 15:49:01 +00:00
Compare commits
7 Commits
a16c79575f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
c7f46e8711
|
|||
|
40f8e3a430
|
|||
|
07491b9741
|
|||
|
04540a55ef
|
|||
|
8837c44300
|
|||
|
dee754c015
|
|||
|
5e4283a462
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||
"baseLocale": "en",
|
||||
"adapter": "svelte"
|
||||
}
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
|
||||
"baseLocale": "en",
|
||||
"adapter": "svelte"
|
||||
}
|
||||
@@ -107,6 +107,7 @@ const config = {
|
||||
"delete_sweep",
|
||||
"print",
|
||||
"restore_from_trash",
|
||||
"factory",
|
||||
"history",
|
||||
"history_toggle_off",
|
||||
"text_to_speech",
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"glob": "^11.0.3",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-css-order": "^2.1.2",
|
||||
|
||||
72
pnpm-lock.yaml
generated
72
pnpm-lock.yaml
generated
@@ -125,6 +125,9 @@ importers:
|
||||
jsdom:
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
npm-run-all:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
@@ -2562,7 +2565,7 @@ packages:
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
global-dirs@3.0.1:
|
||||
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
|
||||
@@ -2698,6 +2701,9 @@ packages:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immutable@5.1.1:
|
||||
resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==}
|
||||
|
||||
@@ -2936,6 +2942,9 @@ packages:
|
||||
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isarray@2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
|
||||
@@ -3016,6 +3025,9 @@ packages:
|
||||
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
keyv@5.5.5:
|
||||
resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==}
|
||||
|
||||
@@ -3044,6 +3056,9 @@ packages:
|
||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
@@ -3269,6 +3284,9 @@ packages:
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
@@ -3429,6 +3447,9 @@ packages:
|
||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
@@ -3464,6 +3485,9 @@ packages:
|
||||
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
readdirp@4.0.2:
|
||||
resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -3573,6 +3597,9 @@ packages:
|
||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
@@ -3631,6 +3658,9 @@ packages:
|
||||
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
shebang-command@1.2.0:
|
||||
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3789,6 +3819,9 @@ packages:
|
||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
stringify-object@3.3.0:
|
||||
resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -7280,6 +7313,8 @@ snapshots:
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immutable@5.1.1: {}
|
||||
|
||||
import-fresh@3.3.0:
|
||||
@@ -7510,6 +7545,8 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isarray@2.0.5: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
@@ -7596,6 +7633,13 @@ snapshots:
|
||||
json-schema: 0.4.0
|
||||
verror: 1.10.0
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
keyv@5.5.5:
|
||||
dependencies:
|
||||
'@keyv/serialize': 1.1.1
|
||||
@@ -7614,6 +7658,10 @@ snapshots:
|
||||
|
||||
leven@3.1.0: {}
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
listr2@3.14.0(enquirer@2.4.1):
|
||||
@@ -7823,6 +7871,8 @@ snapshots:
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
@@ -7948,6 +7998,8 @@ snapshots:
|
||||
|
||||
pretty-bytes@6.1.1: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
proxy-from-env@1.0.0: {}
|
||||
@@ -7981,6 +8033,16 @@ snapshots:
|
||||
normalize-package-data: 2.5.0
|
||||
path-type: 3.0.0
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.2
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdirp@4.0.2: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
@@ -8127,6 +8189,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
isarray: 2.0.5
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
@@ -8194,6 +8258,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
shebang-command@1.2.0:
|
||||
dependencies:
|
||||
shebang-regex: 1.0.0
|
||||
@@ -8410,6 +8476,10 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.0.0
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
stringify-object@3.3.0:
|
||||
dependencies:
|
||||
get-own-enumerable-property-symbols: 3.0.2
|
||||
|
||||
121
src/lib/assets/factory-flash.ps1
Normal file
121
src/lib/assets/factory-flash.ps1
Normal file
@@ -0,0 +1,121 @@
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
# Function to create a form for COM port selection
|
||||
function Show-ComPortSelectionForm {
|
||||
# Create the form
|
||||
$form = New-Object System.Windows.Forms.Form
|
||||
$form.Text = "Select a COM Port"
|
||||
$form.Size = New-Object System.Drawing.Size(400, 400) # Set the size of the form
|
||||
$form.StartPosition = "CenterScreen"
|
||||
|
||||
# Create a label to display a message
|
||||
$infoLabel = New-Object System.Windows.Forms.Label
|
||||
$infoLabel.Location = New-Object System.Drawing.Point(20, 20)
|
||||
$infoLabel.Size = New-Object System.Drawing.Size(340, 30)
|
||||
$infoLabel.Text = "Select a COM port."
|
||||
|
||||
# Create a ListBox to hold the COM port items
|
||||
$comPortListBox = New-Object System.Windows.Forms.ListBox
|
||||
$comPortListBox.Location = New-Object System.Drawing.Point(20, 60)
|
||||
$comPortListBox.Size = New-Object System.Drawing.Size(340, 200)
|
||||
|
||||
# Create a label for status output
|
||||
$statusLabel = New-Object System.Windows.Forms.Label
|
||||
$statusLabel.Location = New-Object System.Drawing.Point(20, 280)
|
||||
$statusLabel.Size = New-Object System.Drawing.Size(340, 60)
|
||||
$statusLabel.Text = "Status: Ready"
|
||||
$statusLabel.ForeColor = [System.Drawing.Color]::Black
|
||||
|
||||
# Function to populate COM ports
|
||||
function PopulateComPorts {
|
||||
# Clear existing items
|
||||
$comPortListBox.Items.Clear()
|
||||
|
||||
# Get all COM ports with "OK" status
|
||||
$comPorts = Get-PnpDevice | Where-Object { $_.Class -eq 'Ports' -and $_.Status -eq 'OK' }
|
||||
|
||||
# Add full friendly name to the ListBox
|
||||
foreach ($comPort in $comPorts) {
|
||||
$comPortListBox.Items.Add($comPort.FriendlyName)
|
||||
}
|
||||
}
|
||||
|
||||
# Populate COM ports initially
|
||||
PopulateComPorts
|
||||
|
||||
# Event handler for mouse click
|
||||
$comPortListBox.Add_MouseClick({
|
||||
ProcessSelection
|
||||
})
|
||||
|
||||
# Event handler for "Enter" key press
|
||||
$comPortListBox.Add_KeyDown({
|
||||
param($sender, $e)
|
||||
if ($e.KeyCode -eq 'Enter') {
|
||||
ProcessSelection
|
||||
}
|
||||
})
|
||||
|
||||
# Function to process selection
|
||||
function ProcessSelection {
|
||||
if ($comPortListBox.SelectedItem) {
|
||||
# Get the selected full friendly name
|
||||
$selectedFriendlyName = $comPortListBox.SelectedItem
|
||||
|
||||
# Extract the COMx part from the friendly name
|
||||
$selectedFriendlyName -match '(COM\d+)' | Out-Null
|
||||
$selectedComPort = $Matches[1]
|
||||
|
||||
if ($selectedComPort) {
|
||||
# Clear the status label
|
||||
$statusLabel.Text = "Status: Executing..."
|
||||
$statusLabel.ForeColor = [System.Drawing.Color]::Black
|
||||
|
||||
# Run the batch file with the extracted COM port (e.g., COM3)
|
||||
$process = Start-Process cmd.exe -ArgumentList "/c .\flash.bat $selectedComPort" -NoNewWindow -PassThru -Wait
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
if ($exitCode -eq 0) {
|
||||
# Successful execution
|
||||
$statusLabel.Text = "Status: Execution completed successfully."
|
||||
$statusLabel.ForeColor = [System.Drawing.Color]::Green
|
||||
} else {
|
||||
# Error occurred
|
||||
$statusLabel.Text = "Status: Error occurred during execution."
|
||||
$statusLabel.ForeColor = [System.Drawing.Color]::Red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create a timer for automatic updates
|
||||
$timer = New-Object System.Windows.Forms.Timer
|
||||
$timer.Interval = 1000 # Set interval to 1 second
|
||||
|
||||
# Timer Tick event to refresh the COM port items
|
||||
$timer.Add_Tick({
|
||||
PopulateComPorts # Refresh the COM port items
|
||||
})
|
||||
|
||||
# Start the timer when the form is shown
|
||||
$form.Add_Shown({
|
||||
$timer.Start()
|
||||
})
|
||||
|
||||
# Stop the timer when the form is closed
|
||||
$form.Add_FormClosing({
|
||||
$timer.Stop()
|
||||
})
|
||||
|
||||
# Add controls to the form
|
||||
$form.Controls.Add($infoLabel)
|
||||
$form.Controls.Add($comPortListBox) # Add the ListBox to the form
|
||||
$form.Controls.Add($statusLabel) # Add the status label
|
||||
|
||||
# Show the form
|
||||
$form.ShowDialog()
|
||||
}
|
||||
|
||||
# Show the COM port selection dialog
|
||||
Show-ComPortSelectionForm
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: T4G
|
||||
name: CCB
|
||||
col:
|
||||
- row:
|
||||
- switch: { e: 3, n: 5, w: 4, s: 6 }
|
||||
rotationAnchor: true
|
||||
- offset: [0.5, 0]
|
||||
row:
|
||||
- key: 2
|
||||
2
src/lib/assets/layouts/layout.d.ts
vendored
2
src/lib/assets/layouts/layout.d.ts
vendored
@@ -2,6 +2,8 @@ export interface CompiledLayout {
|
||||
name: string;
|
||||
size: [number, number];
|
||||
keys: CompiledLayoutKey[];
|
||||
fixedKeys: CompiledLayoutKey[];
|
||||
rotationAnchor?: [number, number];
|
||||
}
|
||||
|
||||
export interface CompiledLayoutKey {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { deviceLayout } from "$lib/serial/connection";
|
||||
import { deviceLayout, deviceMeta } from "$lib/serial/connection";
|
||||
import { dev } from "$app/environment";
|
||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
||||
import { get } from "svelte/store";
|
||||
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
||||
import { getContext, mount, unmount } from "svelte";
|
||||
import type { VisualLayoutConfig } from "./visual-layout.js";
|
||||
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
||||
import { changes, ChangeType, layout, settings } from "$lib/undo-redo";
|
||||
import { fly } from "svelte/transition";
|
||||
import { expoOut } from "svelte/easing";
|
||||
import { activeLayer, activeProfile } from "$lib/serial/connection";
|
||||
@@ -119,9 +119,13 @@
|
||||
}
|
||||
|
||||
function edit(index: number) {
|
||||
const keyInfo = layoutInfo.keys[index];
|
||||
const keyInfo =
|
||||
layoutInfo.keys.find(({ id }) => id === index) ??
|
||||
layoutInfo.fixedKeys.find(({ id }) => id === index);
|
||||
if (!keyInfo) return;
|
||||
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
||||
const clickedGroup = groupParent.querySelector(
|
||||
`g[data-id="${index}"]`,
|
||||
) as SVGGElement;
|
||||
const nextAction =
|
||||
get(layout)[get(activeProfile)]![get(activeLayer)]?.[keyInfo.id];
|
||||
const currentAction =
|
||||
@@ -189,30 +193,185 @@
|
||||
}
|
||||
|
||||
let focusKey: CompiledLayoutKey;
|
||||
let groupParent: SVGElement;
|
||||
let groupParent: SVGGElement;
|
||||
let rotationTarget: SVGCircleElement;
|
||||
let rotationSetting = $derived(
|
||||
$deviceMeta?.settings
|
||||
.find((it) => it.name === "misc")
|
||||
?.items.find((it) => it.name === "device rotation"),
|
||||
);
|
||||
let settingRotation = $derived(
|
||||
rotationSetting
|
||||
? ($settings[$activeProfile]?.[rotationSetting.id]?.value ?? 90)
|
||||
: 90,
|
||||
);
|
||||
|
||||
let flippedSetting = $derived(
|
||||
$deviceMeta?.settings
|
||||
.find((it) => it.name === "misc")
|
||||
?.items.find((it) => it.name === "device orientation"),
|
||||
);
|
||||
let flipped = $derived(
|
||||
flippedSetting
|
||||
? $settings[$activeProfile]?.[flippedSetting.id]?.value !== 0
|
||||
: false,
|
||||
);
|
||||
|
||||
let draggingRotation = $state(90);
|
||||
let isDragging = $state(false);
|
||||
let rotation = $derived(isDragging ? draggingRotation : settingRotation);
|
||||
let dragOffset = 0;
|
||||
|
||||
function calcDragOffset(event: MouseEvent) {
|
||||
const offset = rotationTarget.getBoundingClientRect();
|
||||
const cx = offset.x + offset.width / 2;
|
||||
const cy = offset.y + offset.height / 2;
|
||||
const a = Math.atan2(event.x - cx, event.y - cy) * (180 / Math.PI) + 90;
|
||||
return flipped ? (a + 180) % 360 : a;
|
||||
}
|
||||
|
||||
function toggleFlip() {
|
||||
changes.update((changes) => {
|
||||
changes.push([
|
||||
{
|
||||
type: ChangeType.Setting,
|
||||
id: flippedSetting!.id,
|
||||
setting: flipped ? 0 : 1,
|
||||
profile: get(activeProfile),
|
||||
},
|
||||
]);
|
||||
return changes;
|
||||
});
|
||||
}
|
||||
|
||||
function dragRotation(event: MouseEvent) {
|
||||
if (!isDragging) return;
|
||||
const value = Math.min(
|
||||
180,
|
||||
Math.max(0, Math.round(calcDragOffset(event) - dragOffset)),
|
||||
);
|
||||
if (draggingRotation !== value) {
|
||||
draggingRotation = value;
|
||||
}
|
||||
}
|
||||
|
||||
function dragEnable(event: MouseEvent) {
|
||||
dragOffset = calcDragOffset(event) - rotation;
|
||||
draggingRotation = rotation;
|
||||
isDragging = true;
|
||||
}
|
||||
function dragDisable() {
|
||||
isDragging = false;
|
||||
if (settingRotation !== draggingRotation) {
|
||||
changes.update((changes) => {
|
||||
changes.push([
|
||||
{
|
||||
type: ChangeType.Setting,
|
||||
id: rotationSetting!.id,
|
||||
setting: draggingRotation,
|
||||
profile: get(activeProfile),
|
||||
},
|
||||
]);
|
||||
return changes;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={navigate} />
|
||||
|
||||
<svg
|
||||
class="print"
|
||||
viewBox="0 0 {layoutInfo.size[0] * scale} {layoutInfo.size[1] * scale}"
|
||||
bind:this={groupParent}
|
||||
viewBox="0 {flipped ? margin * -3 : 0} {layoutInfo.size[0] *
|
||||
scale} {layoutInfo.size[1] * scale}"
|
||||
transition:fly={{ y: 48, easing: expoOut }}
|
||||
onmousemove={dragRotation}
|
||||
onmouseup={dragDisable}
|
||||
>
|
||||
{#each layoutInfo.keys as key, i}
|
||||
<KeyboardKey
|
||||
{i}
|
||||
{key}
|
||||
onfocusin={() => (focusKey = key)}
|
||||
onclick={() => edit(i)}
|
||||
onkeypress={({ key }) => {
|
||||
if (key === "Enter") {
|
||||
edit(i);
|
||||
}
|
||||
}}
|
||||
<g
|
||||
bind:this={groupParent}
|
||||
transform="rotate({flipped ? 180 : 0})"
|
||||
transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) *
|
||||
scale} {(layoutInfo.size[1] * scale) / 2}"
|
||||
>
|
||||
<g
|
||||
transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) *
|
||||
scale} {(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}"
|
||||
transform="rotate({-(rotation - 90)})"
|
||||
class="group"
|
||||
>
|
||||
{#each layoutInfo.keys as key}
|
||||
<KeyboardKey
|
||||
{key}
|
||||
{flipped}
|
||||
onfocusin={() => (focusKey = key)}
|
||||
onclick={() => edit(key.id)}
|
||||
onkeypress={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
edit(key.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#if rotationSetting}
|
||||
<rect
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
onmousedown={dragEnable}
|
||||
ondblclick={toggleFlip}
|
||||
class="handle"
|
||||
x={(layoutInfo.size[0] * scale) / 2 - (0.5 * scale) / 2}
|
||||
y={layoutInfo.size[1] * scale + margin - 0.05 * scale}
|
||||
width={0.5 * scale}
|
||||
height={0.05 * scale}
|
||||
ry={0.025 * scale}
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
stroke-width={strokeWidth}
|
||||
/>
|
||||
{#if isDragging}
|
||||
{@const x = (layoutInfo.size[0] * scale) / 2}
|
||||
{@const y = layoutInfo.size[1] * scale + margin * (flipped ? 2 : 3)}
|
||||
<text
|
||||
transition:fly={{ y: 2, easing: expoOut }}
|
||||
transform={flipped ? "rotate(180)" : undefined}
|
||||
transform-origin="{x} {y}"
|
||||
class="handle-label"
|
||||
text-anchor="middle"
|
||||
font-size={fontSize}
|
||||
fill="currentColor"
|
||||
{x}
|
||||
{y}>{rotation - 90}°</text
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</g>
|
||||
<circle
|
||||
bind:this={rotationTarget}
|
||||
cx={(layoutInfo.rotationAnchor?.[0] ?? 0) * scale}
|
||||
cy={(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}
|
||||
r="0"
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<g
|
||||
transform="rotate({flipped ? 180 : 0})"
|
||||
transform-origin="{(layoutInfo.rotationAnchor?.[0] ?? 0) *
|
||||
scale} {(layoutInfo.rotationAnchor?.[1] ?? 0) * scale}"
|
||||
>
|
||||
{#each layoutInfo.fixedKeys as key}
|
||||
<KeyboardKey
|
||||
{key}
|
||||
onfocusin={() => (focusKey = key)}
|
||||
onclick={() => edit(key.id)}
|
||||
onkeypress={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
edit(key.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -222,4 +381,27 @@
|
||||
max-height: calc(100% - 170px);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.handle {
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.1s;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.handle:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.handle:active {
|
||||
opacity: 1;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.handle:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.handle-label {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
const highlight = getContext<Writable<Set<number>> | undefined>("highlight");
|
||||
|
||||
let {
|
||||
i,
|
||||
key,
|
||||
flipped = false,
|
||||
onclick,
|
||||
onkeypress,
|
||||
onfocusin,
|
||||
}: {
|
||||
i: number;
|
||||
key: CompiledLayoutKey;
|
||||
flipped?: boolean;
|
||||
onclick: MouseEventHandler<SVGGElement>;
|
||||
onkeypress: KeyboardEventHandler<SVGGElement>;
|
||||
onfocusin: FocusEventHandler<SVGGElement>;
|
||||
@@ -44,7 +44,10 @@
|
||||
{onkeypress}
|
||||
{onfocusin}
|
||||
role="button"
|
||||
tabindex={i + 1}
|
||||
tabindex={key.id + 1}
|
||||
transform={flipped ? "rotate(180)" : undefined}
|
||||
transform-origin="center"
|
||||
data-id={key.id}
|
||||
>
|
||||
{#if key.shape === "square"}
|
||||
<rect
|
||||
@@ -122,6 +125,9 @@
|
||||
path,
|
||||
g {
|
||||
transform-box: fill-box;
|
||||
}
|
||||
|
||||
path {
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
import("$lib/assets/layouts/m4gr.layout.yml").then(
|
||||
(it) => it.default as CompiledLayout,
|
||||
),
|
||||
T4G: () =>
|
||||
import("$lib/assets/layouts/t4g.layout.yml").then(
|
||||
CCB: () =>
|
||||
import("$lib/assets/layouts/ccb.layout.yml").then(
|
||||
(it) => it.default as CompiledLayout,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import { CharaDevice, type SerialPortLike } from "$lib/serial/device";
|
||||
import type { Chord } from "$lib/serial/chord";
|
||||
import type { Writable } from "svelte/store";
|
||||
@@ -7,12 +7,28 @@ import { persistentWritable } from "$lib/storage";
|
||||
import { userPreferences } from "$lib/preferences";
|
||||
import { getMeta } from "$lib/meta/meta-storage";
|
||||
import type { VersionMeta } from "$lib/meta/types/meta";
|
||||
import { serial as serialPolyfill } from "web-serial-polyfill";
|
||||
import semverGte from "semver/functions/gte";
|
||||
|
||||
export const serialPort = writable<CharaDevice | undefined>();
|
||||
|
||||
navigator.serial?.addEventListener("disconnect", async (event) => {
|
||||
export const forceWebUSB = persistentWritable("force-webusb", false);
|
||||
|
||||
async function onSerialDisconnect() {
|
||||
serialPort.set(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
export const serialObject = derived<typeof forceWebUSB, Serial>(
|
||||
forceWebUSB,
|
||||
(forceWebUSB) =>
|
||||
forceWebUSB || !("serial" in navigator)
|
||||
? (serialPolyfill as any as Serial)
|
||||
: navigator.serial,
|
||||
);
|
||||
|
||||
if ("serial" in navigator) {
|
||||
navigator.serial.addEventListener("disconnect", onSerialDisconnect);
|
||||
}
|
||||
|
||||
export interface SerialLogEntry {
|
||||
type: "input" | "output" | "system";
|
||||
@@ -76,6 +92,20 @@ export async function initSerial(port: SerialPortLike, withSync: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForDevice(device: CharaDevice) {
|
||||
if (semverGte(device.version, "3.1.0")) {
|
||||
const startProgress = await device.getProgress();
|
||||
let total = startProgress;
|
||||
while (total < 1.0) {
|
||||
total = await device.getProgress();
|
||||
syncProgress.set({
|
||||
max: 1.0 - startProgress,
|
||||
current: total - startProgress,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
const device = get(serialPort);
|
||||
if (!device) return;
|
||||
@@ -85,6 +115,9 @@ export async function sync() {
|
||||
device.version.toString(),
|
||||
);
|
||||
deviceMeta.set(meta);
|
||||
|
||||
await waitForDevice(device);
|
||||
|
||||
const chordCount = await device.getChordCount();
|
||||
|
||||
const maxSettings = meta.settings
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
stringifyChordActions,
|
||||
stringifyPhrase,
|
||||
} from "$lib/serial/chord";
|
||||
import { browser } from "$app/environment";
|
||||
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||
import semverGte from "semver/functions/gte";
|
||||
|
||||
@@ -20,7 +19,7 @@ export const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||
["X", { usbProductId: 0x818b, usbVendorId: 0x303a }],
|
||||
["M4G S3 (pre-production)", { usbProductId: 0x1001, usbVendorId: 0x303a }],
|
||||
["M4G S3", { usbProductId: 0x829a, usbVendorId: 0x303a }],
|
||||
["T4G S2", { usbProductId: 0x82f2, usbVendorId: 0x303a }],
|
||||
["CCB S2", { usbProductId: 0x82f2, usbVendorId: 0x303a }],
|
||||
]);
|
||||
|
||||
const DEVICE_ALIASES = new Map<string, Set<string>>([
|
||||
@@ -31,7 +30,7 @@ const DEVICE_ALIASES = new Map<string, Set<string>>([
|
||||
["CCX", new Set(["X", "ccx"])],
|
||||
["M4G", new Set(["M4G S3", "m4g_s3", "M4G S3 (pre-production)"])],
|
||||
["M4G (right)", new Set(["M4GR S3", "m4gr_s3"])],
|
||||
["T4G", new Set(["T4G S2", "t4g_s2"])],
|
||||
["CCB", new Set(["T4G S2", "t4g_s2", "CCB S2", "ccb s2"])],
|
||||
]);
|
||||
|
||||
export function getName(alias: string): string {
|
||||
@@ -68,26 +67,12 @@ const KEY_COUNTS = {
|
||||
M4G: 90,
|
||||
M4GR: 90,
|
||||
T4G: 7,
|
||||
CCB: 7,
|
||||
ZERO: 256,
|
||||
} as const;
|
||||
|
||||
if (
|
||||
browser &&
|
||||
navigator.serial === undefined &&
|
||||
import.meta.env.TAURI_FAMILY !== undefined
|
||||
) {
|
||||
await import("./tauri-serial");
|
||||
}
|
||||
|
||||
if (browser && navigator.serial === undefined && navigator.usb !== undefined) {
|
||||
// @ts-expect-error polyfill
|
||||
navigator.serial = await import("web-serial-polyfill").then(
|
||||
({ serial }) => serial,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||
return navigator.serial.getPorts().then((ports) =>
|
||||
export async function getViablePorts(serial: Serial): Promise<SerialPort[]> {
|
||||
return serial.getPorts().then((ports) =>
|
||||
ports.filter((it) => {
|
||||
const { usbProductId, usbVendorId } = it.getInfo();
|
||||
for (const filter of PORT_FILTERS.values()) {
|
||||
@@ -109,8 +94,8 @@ type LengthArray<T, N extends number, R extends T[] = []> = number extends N
|
||||
? R
|
||||
: LengthArray<T, N, [T, ...R]>;
|
||||
|
||||
export async function canAutoConnect() {
|
||||
return getViablePorts().then((it) => it.length === 1);
|
||||
export async function canAutoConnect(serial: Serial) {
|
||||
return getViablePorts(serial).then((it) => it.length === 1);
|
||||
}
|
||||
|
||||
async function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
@@ -158,7 +143,7 @@ export class CharaDevice {
|
||||
|
||||
constructor(
|
||||
readonly port: SerialPortLike,
|
||||
public baudRate = 115200,
|
||||
public baudRate = 921600,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
@@ -405,6 +390,11 @@ export class CharaDevice {
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
async getProgress() {
|
||||
const [status] = await this.send(1, ["CML", "C5"]);
|
||||
return Number.parseFloat(status);
|
||||
}
|
||||
|
||||
async deleteChord(chord: Pick<Chord, "actions">) {
|
||||
const status = await this.send(1, [
|
||||
"CML",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
themeFromSourceColor,
|
||||
} from "@material/material-color-utilities";
|
||||
import { canAutoConnect, getViablePorts } from "$lib/serial/device";
|
||||
import { initSerial } from "$lib/serial/connection";
|
||||
import { initSerial, serialObject } from "$lib/serial/connection";
|
||||
import type { LayoutData } from "./$types";
|
||||
import { browser } from "$app/environment";
|
||||
import "tippy.js/animations/shift-away.css";
|
||||
@@ -74,8 +74,12 @@
|
||||
webManifestLink = await initPwa();
|
||||
}
|
||||
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||
const [port] = await getViablePorts();
|
||||
if (
|
||||
browser &&
|
||||
$userPreferences.autoConnect &&
|
||||
(await canAutoConnect($serialObject))
|
||||
) {
|
||||
const [port] = await getViablePorts($serialObject);
|
||||
await initSerial(port!, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<script lang="ts">
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import { preference, userPreferences } from "$lib/preferences";
|
||||
import { initSerial } from "$lib/serial/connection";
|
||||
import {
|
||||
forceWebUSB,
|
||||
initSerial,
|
||||
serialObject,
|
||||
} from "$lib/serial/connection";
|
||||
import {
|
||||
getPortName,
|
||||
PORT_FILTERS,
|
||||
type SerialPortLike,
|
||||
} from "$lib/serial/device";
|
||||
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||
import { onMount } from "svelte";
|
||||
import { persistentWritable } from "$lib/storage";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
let ports = $state<SerialPort[]>([]);
|
||||
let element: HTMLDivElement | undefined = $state();
|
||||
let supportsWebSerial = browser && "serial" in navigator;
|
||||
let supportsWebUSB = browser && "usb" in navigator;
|
||||
|
||||
onMount(() => {
|
||||
refreshPorts();
|
||||
$effect(() => {
|
||||
refreshPorts($serialObject);
|
||||
});
|
||||
|
||||
let hasDiscoveredAutoConnect = persistentWritable(
|
||||
@@ -29,8 +35,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshPorts() {
|
||||
ports = await navigator.serial.getPorts();
|
||||
async function refreshPorts(serial: Serial) {
|
||||
ports = await serial.getPorts();
|
||||
}
|
||||
|
||||
async function connect(port: SerialPortLike, withSync: boolean) {
|
||||
@@ -46,13 +52,13 @@
|
||||
element?.closest<HTMLElement>("[popover]")?.hidePopover();
|
||||
}
|
||||
|
||||
async function connectDevice(event: MouseEvent) {
|
||||
const port = await navigator.serial.requestPort({
|
||||
async function connectDevice(event: MouseEvent, serial: Serial) {
|
||||
const port = await serial.requestPort({
|
||||
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
|
||||
});
|
||||
if (!port) return;
|
||||
closePopover();
|
||||
refreshPorts();
|
||||
refreshPorts(serial);
|
||||
connect(port, true);
|
||||
}
|
||||
</script>
|
||||
@@ -60,55 +66,79 @@
|
||||
<div
|
||||
bind:this={element}
|
||||
class="device-list"
|
||||
onmouseenter={() => refreshPorts()}
|
||||
onmouseenter={() => refreshPorts($serialObject)}
|
||||
role="region"
|
||||
>
|
||||
{#if ports.length === 1}
|
||||
{#if supportsWebSerial || supportsWebUSB}
|
||||
<fieldset class:promote={!$hasDiscoveredAutoConnect}>
|
||||
<label
|
||||
><input type="checkbox" use:preference={"autoConnect"} />
|
||||
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
|
||||
</label>
|
||||
{#if ports.length === 1}
|
||||
<label
|
||||
><input type="checkbox" use:preference={"autoConnect"} />
|
||||
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
><input type="checkbox" use:preference={"backup"} />
|
||||
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
|
||||
<label
|
||||
><input type="checkbox" use:preference={"backup"} />
|
||||
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<label title="You can try this if you have trouble with the connection."
|
||||
><input
|
||||
type="checkbox"
|
||||
disabled={!supportsWebSerial}
|
||||
checked={!supportsWebSerial || $forceWebUSB}
|
||||
onchange={(event) => {
|
||||
$forceWebUSB = (event.target as HTMLInputElement).checked;
|
||||
}}
|
||||
/>
|
||||
<div class="title">WebUSB Fallback</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if ports.length !== 0}
|
||||
<h4>Recent Devices</h4>
|
||||
<div class="devices">
|
||||
<!--
|
||||
{#if ports.length !== 0}
|
||||
<h4>Recent Devices</h4>
|
||||
<div class="devices">
|
||||
<!--
|
||||
<div class="device">
|
||||
<button onclick={connectCC0}> CC0</button>
|
||||
</div>-->
|
||||
{#each ports as port}
|
||||
<div class="device">
|
||||
<button
|
||||
onclick={(event) => {
|
||||
connect(port, !event.shiftKey);
|
||||
}}
|
||||
>
|
||||
{getPortName(port)}</button
|
||||
>
|
||||
<button
|
||||
class="error"
|
||||
onclick={() => {
|
||||
port.forget();
|
||||
refreshPorts();
|
||||
}}><span class="icon">visibility_off</span> Hide</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{#each ports as port}
|
||||
<div class="device">
|
||||
<button
|
||||
onclick={(event) => {
|
||||
connect(port, !event.shiftKey);
|
||||
}}
|
||||
>
|
||||
{getPortName(port)}</button
|
||||
>
|
||||
<button
|
||||
class="error"
|
||||
onclick={() => {
|
||||
port.forget();
|
||||
refreshPorts($serialObject);
|
||||
}}><span class="icon">visibility_off</span> Hide</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pair">
|
||||
<button
|
||||
onclick={(event) => connectDevice(event, $serialObject)}
|
||||
class="primary"><span class="icon">add</span>Connect</button
|
||||
>
|
||||
<!--<a href="/ccos/zero_wasm/"><span class="icon">add</span>Virtual Device</a>-->
|
||||
</div>
|
||||
{#if !supportsWebSerial}
|
||||
<p>Browser with limited support detected.</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p><b>Your browser is missing support for critical features.</b></p>
|
||||
<p>
|
||||
Please use a Chromium-based browser such as Chrome, Edge or Chromium
|
||||
instead.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="pair">
|
||||
<button onclick={connectDevice} class="primary"
|
||||
><span class="icon">add</span>Connect</button
|
||||
>
|
||||
<!--<a href="/ccos/zero_wasm/"><span class="icon">add</span>Virtual Device</a>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { downloadBackup } from "$lib/backup/backup";
|
||||
import { initSerial, serialPort } from "$lib/serial/connection";
|
||||
import { initSerial, serialObject, serialPort } from "$lib/serial/connection";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { lt as semverLt } from "semver";
|
||||
import type { LoaderOptions, ESPLoader } from "esptool-js";
|
||||
import ProgressButton from "$lib/ProgressButton.svelte";
|
||||
import FactoryKit from "./FactoryKit.svelte";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -95,9 +96,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
async function connect(serial: Serial) {
|
||||
try {
|
||||
const port = await navigator.serial.requestPort();
|
||||
const port = await serial.requestPort();
|
||||
await initSerial(port!, true);
|
||||
step = 1;
|
||||
} catch (e) {
|
||||
@@ -138,9 +139,9 @@
|
||||
step = 4;
|
||||
}
|
||||
|
||||
async function espBootloader() {
|
||||
async function espBootloader(serial: Serial) {
|
||||
$serialPort?.forget();
|
||||
const port = await navigator.serial.requestPort();
|
||||
const port = await serial.requestPort();
|
||||
port.open({ baudRate: 1200 });
|
||||
}
|
||||
|
||||
@@ -172,8 +173,8 @@
|
||||
return espLoader;
|
||||
}
|
||||
|
||||
async function flashImages() {
|
||||
const port = await navigator.serial.requestPort();
|
||||
async function flashImages(serial: Serial) {
|
||||
const port = await serial.requestPort();
|
||||
try {
|
||||
const esptool = data.meta.update.esptool!;
|
||||
espLoader = await connectEsp(port);
|
||||
@@ -202,8 +203,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function eraseSPI() {
|
||||
const port = await navigator.serial.requestPort();
|
||||
async function eraseSPI(serial: Serial) {
|
||||
const port = await serial.requestPort();
|
||||
try {
|
||||
console.log(data.meta);
|
||||
const spiFlash = data.meta.spiFlash!;
|
||||
@@ -314,7 +315,7 @@
|
||||
<section>
|
||||
<ol>
|
||||
<li>
|
||||
<button class="inline-button" onclick={connect}
|
||||
<button class="inline-button" onclick={() => connect($serialObject)}
|
||||
><span class="icon">usb</span>Connect</button
|
||||
>
|
||||
your device
|
||||
@@ -354,6 +355,10 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if data.meta.update.esptool}
|
||||
<FactoryKit meta={data.meta as any} />
|
||||
{/if}
|
||||
|
||||
{#if false && data.meta.update.esptool}
|
||||
<section>
|
||||
<h3>Factory Flash (WIP)</h3>
|
||||
@@ -367,17 +372,17 @@
|
||||
</p>
|
||||
|
||||
<div class="esp-buttons">
|
||||
<button onclick={espBootloader}
|
||||
<button onclick={() => espBootloader($serialObject)}
|
||||
><span class="icon">memory</span>ESP Bootloader</button
|
||||
>
|
||||
<button onclick={flashImages}
|
||||
<button onclick={() => flashImages($serialObject)}
|
||||
><span class="icon">developer_board</span>Flash Images</button
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase
|
||||
All</label
|
||||
>
|
||||
<button onclick={eraseSPI}
|
||||
<button onclick={() => eraseSPI($serialObject)}
|
||||
><span class="icon">developer_board</span>Erase SPI Flash</button
|
||||
>
|
||||
</div>
|
||||
|
||||
64
src/routes/(app)/ccos/[device]/[version]/FactoryKit.svelte
Normal file
64
src/routes/(app)/ccos/[device]/[version]/FactoryKit.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import ProgressButton from "$lib/ProgressButton.svelte";
|
||||
import {
|
||||
assembleFactoryKit,
|
||||
type VersionMetaWithEsptool,
|
||||
} from "./factory-kit";
|
||||
|
||||
let { meta }: { meta: VersionMetaWithEsptool } = $props();
|
||||
|
||||
const osChoices = ["Windows", "Linux", "MacOS"] as const;
|
||||
let os: (typeof osChoices)[number] = $state(osChoices[0]);
|
||||
|
||||
let working = $state(false);
|
||||
let progress = $state(0);
|
||||
let error: string | undefined = $state(undefined);
|
||||
|
||||
async function download() {
|
||||
progress = 0;
|
||||
error = undefined;
|
||||
working = true;
|
||||
try {
|
||||
const result = await assembleFactoryKit(meta, os, (value) => {
|
||||
progress = value;
|
||||
});
|
||||
const url = URL.createObjectURL(result);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `factory-kit-${meta.device}-${meta.version}-${os}.zip`;
|
||||
a.click();
|
||||
} catch (err) {
|
||||
error = (err as Error).message;
|
||||
} finally {
|
||||
working = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h3><span class="icon">factory</span> Factory Kit</h3>
|
||||
<div class="os-selection">
|
||||
{#each osChoices as value}
|
||||
<label
|
||||
><input type="radio" name="os" {value} bind:group={os} />{value}</label
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
<ProgressButton onclick={download} {progress} {working} {error}
|
||||
><span class="icon">download</span> Download</ProgressButton
|
||||
>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
.os-selection {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
142
src/routes/(app)/ccos/[device]/[version]/factory-kit.ts
Normal file
142
src/routes/(app)/ccos/[device]/[version]/factory-kit.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { VersionMeta } from "$lib/meta/types/meta";
|
||||
|
||||
async function progressFetch(
|
||||
url: string,
|
||||
onProgress: (progress: number) => void,
|
||||
) {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open("GET", url, true);
|
||||
request.responseType = "arraybuffer";
|
||||
request.onprogress = (event) => {
|
||||
if (event.total > 0) {
|
||||
onProgress(event.loaded / event.total);
|
||||
}
|
||||
};
|
||||
const result = new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
request.onload = () => {
|
||||
onProgress(1);
|
||||
if (request.status >= 200 && request.status < 300) {
|
||||
resolve(request.response);
|
||||
} else {
|
||||
reject(new Error(`Failed to fetch ${url}: ${request.statusText}`));
|
||||
}
|
||||
};
|
||||
});
|
||||
request.send();
|
||||
return result;
|
||||
}
|
||||
export type VersionMetaWithEsptool = VersionMeta & {
|
||||
update: { esptool: Exclude<VersionMeta["update"]["esptool"], undefined> };
|
||||
};
|
||||
|
||||
export async function assembleFactoryKit(
|
||||
meta: VersionMetaWithEsptool,
|
||||
os: "Windows" | "Linux" | "MacOS",
|
||||
onProgress: (progress: number) => void,
|
||||
): Promise<Blob> {
|
||||
let otherProgress: number[] = new Array(
|
||||
Object.keys(meta.update.esptool.files).length,
|
||||
).fill(0);
|
||||
let esptoolProgress = 0;
|
||||
let compressProgress = 0;
|
||||
function reportProgress() {
|
||||
const total =
|
||||
0.1 * (otherProgress.reduce((a, b) => a + b) / otherProgress.length) +
|
||||
0.5 * esptoolProgress +
|
||||
0.4 * compressProgress;
|
||||
onProgress(total);
|
||||
}
|
||||
const esptool =
|
||||
os === "Windows"
|
||||
? progressFetch(`/esptool-v5.2.0-windows-amd64.zip`, (progress) => {
|
||||
esptoolProgress = progress;
|
||||
reportProgress();
|
||||
})
|
||||
: undefined;
|
||||
const files = Object.values(meta.update.esptool.files).map(
|
||||
(file, i) =>
|
||||
[
|
||||
file,
|
||||
progressFetch(`${meta.path}/${file}`, (progress) => {
|
||||
otherProgress[i] = progress;
|
||||
reportProgress();
|
||||
}),
|
||||
] as const,
|
||||
);
|
||||
console.log(files);
|
||||
const JSZip = await import("jszip").then((m) => m.default);
|
||||
const zip = new JSZip();
|
||||
const esptoolZipPromise = esptool
|
||||
? esptool.then((it) => JSZip.loadAsync(it))
|
||||
: undefined;
|
||||
for (const [file, dataPromise] of files) {
|
||||
zip.file(file, dataPromise);
|
||||
}
|
||||
|
||||
const esptoolScript = ".\\esptool\\esptool.exe";
|
||||
const port = "%1";
|
||||
const ext = os === "Windows" ? "bat" : "sh";
|
||||
zip.file(
|
||||
`flash.${ext}`,
|
||||
[
|
||||
esptoolScript,
|
||||
"--chip",
|
||||
meta.update.esptool.chip,
|
||||
"--port",
|
||||
port,
|
||||
"--baud",
|
||||
meta.update.esptool.baud,
|
||||
"--before",
|
||||
meta.update.esptool.before,
|
||||
"--after",
|
||||
meta.update.esptool.after,
|
||||
"write_flash",
|
||||
"-z",
|
||||
"--flash_mode",
|
||||
meta.update.esptool.flash_mode,
|
||||
"--flash_freq",
|
||||
meta.update.esptool.flash_freq,
|
||||
"--flash_size",
|
||||
meta.update.esptool.flash_size,
|
||||
...Object.entries(meta.update.esptool.files).flatMap(
|
||||
([address, file]) => [address, file],
|
||||
),
|
||||
].join(" "),
|
||||
{ unixPermissions: "755" },
|
||||
);
|
||||
zip.file(`${meta.device}-${meta.version}`, "");
|
||||
if (os === "Windows") {
|
||||
zip.file(
|
||||
"factory-flash.ps1",
|
||||
import("$lib/assets/factory-flash.ps1?raw").then((m) => m.default),
|
||||
);
|
||||
}
|
||||
zip.file(
|
||||
"README.txt",
|
||||
[
|
||||
`Factory Kit for ${meta.device} ${meta.version}`,
|
||||
"",
|
||||
os !== "Windows"
|
||||
? "Requires esptool, please download from https://github.com/espressif/esptool/releases"
|
||||
: "",
|
||||
os === "Windows"
|
||||
? 'Right click factory-flash.ps1 and select "Run with PowerShell'
|
||||
: `Run flash.sh with \`./flash.sh <port>\``,
|
||||
].join("\n"),
|
||||
);
|
||||
if (esptoolZipPromise) {
|
||||
const esptoolZip = await esptoolZipPromise;
|
||||
for (const [path, file] of Object.entries(esptoolZip.files)) {
|
||||
if (!file.dir) {
|
||||
zip.file(
|
||||
path.replace(/^esptool[^\/]*\//, "esptool/"),
|
||||
file.async("arraybuffer"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return zip.generateAsync({ type: "blob" }, ({ percent }) => {
|
||||
compressProgress = percent / 100;
|
||||
reportProgress();
|
||||
});
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
sync,
|
||||
syncProgress,
|
||||
syncStatus,
|
||||
waitForDevice,
|
||||
} from "$lib/serial/connection";
|
||||
import ProgressButton from "$lib/ProgressButton.svelte";
|
||||
import { tick } from "svelte";
|
||||
@@ -136,6 +137,7 @@
|
||||
empty.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
changes.update((changes) => {
|
||||
changes.push([
|
||||
...empty.keys().map(
|
||||
@@ -242,6 +244,8 @@
|
||||
|
||||
let progressCurrent = 0;
|
||||
|
||||
await waitForDevice(port);
|
||||
|
||||
function updateProgress() {
|
||||
syncProgress.set({
|
||||
max: progressMax,
|
||||
|
||||
BIN
static/esptool-v5.2.0-windows-amd64.zip
Normal file
BIN
static/esptool-v5.2.0-windows-amd64.zip
Normal file
Binary file not shown.
@@ -29,6 +29,7 @@ export interface VisualLayoutSwitch extends Positionable {
|
||||
s: number;
|
||||
d: number;
|
||||
};
|
||||
rotationAnchor?: true;
|
||||
}
|
||||
|
||||
const fileRegex = /\.(layout\.yml)$/;
|
||||
@@ -53,6 +54,7 @@ export function compileLayout(layout: VisualLayout): CompiledLayout {
|
||||
name: layout.name,
|
||||
size: [0, 0],
|
||||
keys: [],
|
||||
fixedKeys: [],
|
||||
};
|
||||
|
||||
let y = 0;
|
||||
@@ -80,13 +82,16 @@ export function compileLayout(layout: VisualLayout): CompiledLayout {
|
||||
} else if ("switch" in info) {
|
||||
const cx = x + ox + 1;
|
||||
const cy = y + oy + 1;
|
||||
if (info.rotationAnchor) {
|
||||
compiled.rotationAnchor = [cx, cy];
|
||||
}
|
||||
for (const [i, id] of [
|
||||
info.switch.s,
|
||||
info.switch.w,
|
||||
info.switch.n,
|
||||
info.switch.e,
|
||||
].entries()) {
|
||||
compiled.keys.push({
|
||||
(info.rotationAnchor ? compiled.fixedKeys : compiled.keys).push({
|
||||
id,
|
||||
shape: "quarter-circle",
|
||||
cornerRadius: 0,
|
||||
@@ -96,7 +101,7 @@ export function compileLayout(layout: VisualLayout): CompiledLayout {
|
||||
});
|
||||
}
|
||||
if (info.switch.d !== undefined) {
|
||||
compiled.keys.push({
|
||||
(info.rotationAnchor ? compiled.fixedKeys : compiled.keys).push({
|
||||
id: info.switch.d,
|
||||
shape: "square",
|
||||
cornerRadius: 0.5,
|
||||
|
||||
@@ -21,7 +21,7 @@ const { homepage, bugs, repository } = JSON.parse(
|
||||
process.env["VITE_HOMEPAGE_URL"] = repository.url.replace(/\.git$/, "");
|
||||
process.env["VITE_DOCS_URL"] = homepage;
|
||||
process.env["VITE_BUGS_URL"] = bugs.url;
|
||||
process.env["VITE_LEARN_URL"] = "https://www.iq-eq.io/";
|
||||
process.env["VITE_LEARN_URL"] = "https://adventure.charachorder.io/";
|
||||
process.env["VITE_LATEST_FIRMWARE"] = "1.1.4";
|
||||
process.env["VITE_STORE_URL"] = "https://www.charachorder.com/";
|
||||
process.env["VITE_MATRIX_URL"] = "https://charachorder.io/";
|
||||
|
||||
Reference in New Issue
Block a user