Repository: derkyjadex/M8WebDisplay
Branch: master
Commit: 6037d59f9322
Files: 39
Total size: 96.1 KB
Directory structure:
gitextract_wchr2nwj/
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── app.webmanifest
├── css/
│ ├── common.scss
│ ├── display.scss
│ ├── firmware.scss
│ ├── form.scss
│ ├── index.scss
│ └── settings.scss
├── index.html
├── js/
│ ├── audio.js
│ ├── firmware.js
│ ├── gl-renderer.js
│ ├── hex.js
│ ├── input.js
│ ├── keyboard.js
│ ├── main.js
│ ├── parser.js
│ ├── renderer.js
│ ├── serial.js
│ ├── settings.js
│ ├── usb.js
│ ├── util.js
│ ├── wake.js
│ ├── worker-setup.js
│ └── worker.js
├── package.json
└── shaders/
├── blit.frag
├── blit.vert
├── rect.frag
├── rect.vert
├── text1.frag
├── text1.vert
├── text2.frag
├── text2.vert
├── wave.frag
└── wave.vert
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules/
build/
cert/
deploy
================================================
FILE: LICENSE
================================================
Copyright 2021-2022 James Deery
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
# Copyright 2021-2022 James Deery
# Released under the MIT licence, https://opensource.org/licenses/MIT
DEPLOY = \
build/index.html \
build/worker.js \
app.webmanifest \
build/icon.png
CACHE_FILES = \
build/index.html \
build/icon.png \
app.webmanifest
DEPLOY_DIR = deploy/
NPM = node_modules/
ifeq ($(shell uname -s),Darwin)
BASE64 = base64 -i
MD5 = md5
else
BASE64 = base64 -w0
MD5 = md5sum
endif
index.html: build/index.css js/main.js
js/main.js: $(filter-out js/main.js,$(wildcard js/*.js)) build/shaders.js build/font1.js build/font2.js
@touch $@
build/shaders.js: $(wildcard shaders/*.vert) $(wildcard shaders/*.frag)
@echo Building $@
@mkdir -p $(@D)
@for i in $^; do \
printf "export const $$(basename $${i} | tr . _) = \`"; \
sed 's/\/\/.*$$//g' $$i \
| perl -0pe 's/([\n;,{}()\[\]=+\-*\/])[ \t\r\n]+/$$1/g'; \
echo "\`;"; \
done > $@
build/font1.js: font1.png
@echo Building $@
@mkdir -p $(@D)
@printf "export const font1 = 'data:image/png;base64,$$($(BASE64) $^)';" > $@
build/font2.js: font2.png
@echo Building $@
@mkdir -p $(@D)
@printf "export const font2 = 'data:image/png;base64,$$($(BASE64) $^)';" > $@
build/main.js: js/main.js $(NPM)
@echo Building $@
@mkdir -p $(@D)
@npx rollup $< \
| npx terser --mangle --toplevel --compress > $@
build/worker.js: js/worker.js $(CACHE_FILES) $(NPM)
@echo Building $@
@mkdir -p $(@D)
@sed "s/INDEXHASH/$$(cat $(CACHE_FILES) | $(MD5))/" $< \
| npx terser --mangle --compress > $@
css/index.scss: $(filter-out css/index.scss,$(wildcard css/*.scss)) build/font1.scss build/font2.scss
@touch $@
build/font1.scss: m8stealth57.woff2
@echo Building $@
@mkdir -p $(@D)
@printf "@font-face {\n\
font-family: 'm8stealth57';\n\
src: url('data:font/woff2;base64,$$($(BASE64) $^)') format('woff2');\n\
}" > $@
build/font2.scss: m8stealth89.woff2
@echo Building $@
@mkdir -p $(@D)
@printf "@font-face {\n\
font-family: 'm8stealth89';\n\
src: url('data:font/woff2;base64,$$($(BASE64) $^)') format('woff2');\n\
}" > $@
build/index.css: css/index.scss $(NPM)
@echo Building $@
@mkdir -p $(@D)
@npx sass --style=compressed $< > $@
build/index.html: index.html build/index.css build/main.js favicon.png $(NPM)
@echo Building $@
@mkdir -p $(@D)
@sed "s/BUILDNUM/$$(date -u +"%Y-%m-%dT%H:%M:%S") $$(git rev-parse --short HEAD)$$(test -z "$$(git status --porcelain)" || printf X)/" $< \
| sed -e 's/"build\/index.css"/"index.css"/' \
| sed -e 's/"js\/main.js"/"main.js"/' \
| sed -e 's|"favicon.png"|"data:image/png;base64,'$$($(BASE64) favicon.png)'"|' \
| sed -e 's/^ *//' \
| perl -0pe 's/>[ \t\r\n]+> $@.tmp
@npx juice \
--apply-style-tags false \
--remove-style-tags false \
$@.tmp $@
@rm $@.tmp
build/icon.png: icon.png
@echo Building $@
@cp $< $@
cert/cert.conf: $(NPM)
@echo Building $@
@mkdir -p cert
@echo "[req]\n\
distinguished_name=dn\n\
req_extensions=ext\n\
prompt=no\n\
[dn]\n\
CN=DevCert\n\
OU=DEV\n\
[ext]\n\
keyUsage=nonRepudiation,digitalSignature,keyEncipherment\n\
basicConstraints=critical,CA:TRUE,pathlen:0\n\
subjectAltName=DNS:localhost,$$(\
npx ws --list-network-interfaces \
| grep '^-' \
| sed -E 's/^- .+: ([0-9.]+)$$/IP:\1/g' \
| sed -E 's/^- .+: (.+)$$/DNS:\1/g' \
| paste -sd ',' -)" > $@
cert/private-key.pem:
@echo Building $@
@mkdir -p cert
@openssl genrsa -out $@ 2048
cert/server.csr: cert/private-key.pem cert/cert.conf
@echo Building $@
@openssl req \-new \
-nodes \
-sha256 \
-key cert/private-key.pem \
-config cert/cert.conf \
-out $@
cert/server.crt: cert/private-key.pem cert/cert.conf cert/server.csr
@echo Building $@
@openssl x509 \
-req \
-sha256 \
-days 90 \
-in cert/server.csr \
-signkey cert/private-key.pem \
-extfile cert/cert.conf \
-extensions ext \
-out $@
$(NPM):
@echo Installing node packages
@npm ci
all: $(DEPLOY)
clean:
@echo Cleaning
@rm -r build/*
ifeq ($(HTTPS),true)
run: index.html cert/private-key.pem cert/server.crt $(NPM)
@npx ws \
--log.format dev \
--rewrite '/worker.js -> /js/worker.js' \
--blacklist /cert/private-key.pem \
--key cert/private-key.pem \
--cert cert/server.crt
else
run: index.html $(NPM)
@npx ws \
--log.format dev \
--rewrite '/worker.js -> /js/worker.js' \
--blacklist /cert/private-key.pem
endif
deploy: $(DEPLOY)
@echo Deploying
@mkdir -p $(DEPLOY_DIR)
@rm -rf $(DEPLOY_DIR)/*
@cp $^ $(DEPLOY_DIR)
.PHONY: all run deploy clean
================================================
FILE: README.md
================================================
# M8 Headless Web Display
This is alternative frontend for [M8 Headless](https://github.com/DirtyWave/M8HeadlessFirmware).
It runs entirely in the browser and only needs to be hosted on a server to satisfy browser security policies. No network communication is involved.
Try it out at https://derkyjadex.github.io/M8WebDisplay/.
Features:
- Render the M8 display
- Route M8's audio out to the default audio output
- Keyboard and gamepad input
- Custom key/button mapping
- Touch-compatible on-screen keys
- Firmware loader
- Full offline support
- Installable as a [PWA](https://en.wikipedia.org/wiki/Progressive_web_application)
## Supported Platforms
The following should generally work, details are below.
- Chrome 89+ on macOS, Windows and Linux1
- Edge 89+ on macOS and Windows
- Chrome on Android2, without audio3
The web display uses the Web Serial API to communicate with the M8. This API is currently only supported by desktop versions of Google Chrome and Microsoft Edge in versions 89 or later. For Chrome on Android the code can fallback to using the WebUSB API.
1. On Ubuntu and Debian systems (and perhaps others) users do not have permission to access the M8's serial port by default. You will need to add yourself to the `dialout` group and restart your login session/reboot. After this you should be able to connect normally.
2. Newer Samsung phones appear to handle USB serial in a way that prevents Chrome from being able to open the device. There is an [outstanding Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=1099521#c21) to fix this.
3. The way that that Android handles USB audio devices (such as the M8) prevents us from being able to redirect the audio to the phone's speakers or headphone output. When the M8 is attached, Android appears to completely disable the internal audio interface and uses the M8 for all audio input and output instead. So the page is able to receive the audio from the M8 but it does not have anywhere to redirect it to other than the M8 itself.
## Developing
To build this project you need a standard unix-like environment and a recent-ish version of [Node.js](https://nodejs.org/) (15.6 works, earlier versions might not). You should be able to build on macOS, Linux and [WSL](https://docs.microsoft.com/en-us/windows/wsl/) on Windows.
From a fresh clone, run this in your terminal:
```
make run
```
This will download the necessary node packages, build the files required to run a debug version of the display and launch a local web server. If this is successful you can open http://localhost:8000/ in Chrome to launch the display. Press `ctrl-c` to stop the server.
You can edit the `*.js` files and simply refresh the page to see the changes. If you edit the `*.scss` files or the shaders you will need to run `make` to regenerate the necessary files before refreshing. You can do this from another terminal window/tab, there is no need to restart the server.
Chrome requires that pages are served securely in order to enable features such as the Serial API. Normally this means using HTTPS but there is an exception when you use `localhost`. If you want to test your changes on another computer on your network you will need to run the local web server with HTTPS:
```
make run HTTPS=true
```
This will generate a certificate and the local web server will now work from `https://:8000` (the full list of addresses is shown in the command output). When you use this address you will need to either ignore the security warning or install the certificate at `cert/server.crt` as a trusted Certificate Authority on your device.
To build a release version of the display run:
```
make deploy
```
This will build and copy the release files to the `deploy/` directory. These files can be hosted on any static web server as long as has an HTTPS address.
## TODO/Ideas
- Avoid/automatically recover from bad frames
- Auto-reboot for firmware loader/real M8 support
- Selectable audio output device
## Licence
This code is released under the MIT licence.
See LICENSE for more details.
================================================
FILE: app.webmanifest
================================================
{
"name": "M8 Display",
"short_name": "M8 Display",
"description": "Web display for M8 Headless",
"start_url": ".",
"display": "standalone",
"background_color": "#000",
"theme_color": "#000",
"icons": [
{
"src": "icon.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
}
]
}
================================================
FILE: css/common.scss
================================================
// Copyright 2021 James Deery
// Released under the MIT licence, https://opensource.org/licenses/MIT
$m8: #00efff;
$m8-font: 'm8stealth57';
$m8-font-big: 'm8stealth89';
$shield-z: 1000;
================================================
FILE: css/display.scss
================================================
// Copyright 2021-2022 James Deery
// Released under the MIT licence, https://opensource.org/licenses/MIT
@use 'common' as *;
#display {
position: relative;
width: 100vw;
height: calc(100vw * 3 / 4);
max-width: calc(100vh * 4 / 3);
max-height: 100vh;
margin: 0 auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
user-select: none;
canvas, svg {
position: absolute;
image-rendering: pixelated;
width: 100%;
height: 100%;
}
svg text {
font-family: $m8-font;
font-size: 16px;
}
svg.big text {
font-family: $m8-font-big;
font-size: 24px;
}
#buttons {
position: absolute;
top: 25px;
width: 100%;
text-align: center;
body.mapping & {
display: none;
}
}
#mapping-buttons {
display: none;
position: absolute;
top: 25px;
width: 100%;
text-align: center;
body.mapping & {
display: block;
}
button {
margin: 0 5px 10px;
}
}
&.with-controls, body.mapping & {
height: calc(100vw * 6 / 4);
max-width: calc(100vh * 4 / 6);
canvas, svg {
height: 50%;
}
#controls {
display: block;
}
}
#controls {
display: none;
position: relative;
width: 100%;
height: 50%;
top: 50%;
> div {
position: absolute;
width: 20.3125%;
height: 27.0833%;
border: 3px solid #aaa;
border-radius: 10px;
background-color: #333;
box-sizing: border-box;
padding: 5px;
text-align: center;
font-size: 80%;
transition-property: border-color, background-color;
transition-duration: 200ms;
&.active {
border-color: #0cf;
background-color: #0cf;
transition-duration: 0s;
&[data-action="edit"] {
border-color: #d48;
background-color: #d48;
}
&[data-action="option"] {
border-color: #66c;
background-color: #66c;
}
&[data-action="select"] {
border-color: #d73;
background-color: #d73;
}
&[data-action="start"] {
border-color: #5a3;
background-color: #5a3;
}
}
&.mapping {
border-color: #e30;
z-index: $shield-z + 1;
}
}
}
#mapping-help {
display: none;
position: absolute;
bottom: 50%;
width: 100%;
font-family: $m8-font;
text-align: center;
body.mapping & {
display: block;
.select-action {
display: block;
}
.enter-input {
display: none;
}
}
body.mapping.capturing & {
.select-action {
display: none;
}
.enter-input {
display: block;
position: relative;
z-index: $shield-z + 1;
}
}
}
}
#capture-shield {
display: none;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.75);
z-index: $shield-z;
body.mapping.capturing & {
display: block;
}
}
================================================
FILE: css/firmware.scss
================================================
// Copyright 2021 James Deery
// Released under the MIT licence, https://opensource.org/licenses/MIT
@use 'common' as *;
#firmware {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 100px;
background-color: rgba(0, 0, 0, 0.75);
text-align: center;
font-family: $m8-font;
line-height: 1.5;
&.hidden {
display: none;
}
p.select-file-msg,
input[type=file],
p.file-loading-msg,
p.file-error-msg,
p.select-device-msg,
button.select-device,
p.device-error-msg,
p.flash-msg,
button.flash,
p.flashing-msg,
progress.flash,
p.flash-error-msg,
p.flash-success-msg {
display: none;
margin: 20px auto;
}
p.file-error-msg,
p.device-error-msg,
p.flash-error-msg {
color: #e22;
}
&.file-select {
p.select-file-msg,
input[type=file] {
display: block;
}
}
&.file-loading {
p.file-loading-msg {
display: block;
}
button.close {
display: none;
}
}
&.file-error {
p.select-file-msg,
input[type=file],
p.file-error-msg {
display: block;
}
}
&.file-loaded {
p.select-device-msg,
button.select-device {
display: block;
}
}
&.device-error {
p.select-device-msg,
button.select-device,
p.device-error-msg {
display: block;
}
}
&.device-selected {
p.flash-msg,
button.flash {
display: block;
}
}
&.flashing {
p.flashing-msg,
progress.flash {
display: block;
}
button.close {
display: none;
}
}
&.flash-error {
p.select-device-msg,
button.select-device,
p.flash-error-msg {
display: block;
}
}
&.flash-success {
p.flash-success-msg {
display: block;
}
}
button.close {
display: block;
position: absolute;
right: 100px;
bottom: 100px;
}
}
================================================
FILE: css/form.scss
================================================
// Copyright 2021 James Deery
// Released under the MIT licence, https://opensource.org/licenses/MIT
@use 'common' as *;
$menu-time: 80ms;
label, input, select, button {
appearance: none;
font-family: $m8-font;
font-size: 14px;
background-color: #000;
color: #fff;
&:hover {
background-color: $m8;
color: #000;
}
&:active {
background-color: darken($m8, 10%);
color: #fff;
}
&:focus, &:focus-within {
outline: none;
color: $m8;
&:hover {
color: #000;
}
&:active {
color: #fff;
}
}
}
button {
border: 3px solid #fff;
padding: 20px;
}
input[type=checkbox] {
font-family: $m8-font;
&::after {
content: 'OFF';
}
&:checked::after {
content: 'ON';
}
}
input[type=file] {
border: 3px solid #fff;
padding: 20px;
}
select {
padding: 5px 32px 5px 5px;
background-color: transparent;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-width='3' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 16px;
font-family: sans-serif;
font-size: 12px;
}
progress {
border: 3px solid #fff;
height: 40px;
width: 100%;
&::-webkit-progress-bar {
background-color: #000;
}
&::-webkit-progress-value {
background-color: $m8;
}
}
================================================
FILE: css/index.scss
================================================
// Copyright 2021 James Deery
// Released under the MIT licence, https://opensource.org/licenses/MIT
@use 'common' as *;
@use '../build/font1';
@use '../build/font2';
body {
font-family: sans-serif;
background-color: #000;
color: #eee;
margin: 0px;
font-size: 16px;
}
a, a:visited {
color: $m8;
text-decoration: none;
&:hover {
text-decoration: underline;
}
&:focus {
outline: 1px solid $m8;
}
}
kbd {
font-family: sans-serif;
background-color: #444;
color: $m8;
font-size: 16px;
margin: 0 2px;
padding: 2px 5px;
border-radius: 3px;
}
.hidden {
display: none;
}
.build {
color: #888;
}
@import 'form';
@import 'display';
@import 'settings';
@import 'firmware';
#info {
position: absolute;
top: 100px;
left: 100px;
right: 100px;
padding: 20px;
background-color: rgba(0, 0, 0, 0.75);
text-align: center;
line-height: 1.5;
h1 {
font-family: $m8-font;
font-size: 32px;
margin: 0 0 16px;
}
}
.error {
position: absolute;
top: 100px;
left: 100px;
right: 100px;
border: 3px solid #fff;
padding: 20px;
background-color: #a22;
user-select: text;
p {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
#reload {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
button {
border-bottom-width: 0;
}
}
================================================
FILE: css/settings.scss
================================================
// Copyright 2021 James Deery
// Released under the MIT licence, https://opensource.org/licenses/MIT
@use 'common' as *;
#settings {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: background-color $menu-time linear 0s;
user-select: none;
&.hidden {
display: block;
width: 0;
height: 0;
background-color: rgba(0, 0, 0, 0);
.setting {
visibility: hidden;
margin-left: -323px;
transition: none;
}
#menu-button {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-width='1.5' d='M2 4h12M2 8h12M2 12h12'/%3e%3c/svg%3e");
}
}
&.hidden.auto-hide {
#menu-button {
opacity: 0%;
transition: opacity 0.8s linear 0.8s;
&:hover, &:focus {
opacity: 100%;
transition: opacity 0s;
}
}
}
#menu-button {
width: 32px;
height: 32px;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-width='1.5' d='M3 3l10 10M3 13l10-10'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-color: transparent;
border-color: transparent;
font-size: 0;
margin-bottom: -3px;
&:hover {
background-color: $m8;
}
&:active {
background-color: darken($m8, 10%);
color: #fff;
}
&:focus {
color: $m8;
border-color: $m8;
&:hover {
color: #000;
}
&:active {
color: #fff;
}
}
}
.setting {
width: 320px;
border: 3px solid #fff;
border-width: 3px 3px 0 0;
margin-left: 0;
transition: margin-left $menu-time linear 0s;
&:last-child {
border-bottom-width: 3px;
}
label {
display: block;
position: relative;
padding: 10px;
input, select {
position: absolute;
top: 0;
bottom: 0;
right: 0;
margin: 0;
border: 0;
}
input[type=checkbox] {
padding: 10px;
}
select {
width: 50%;
}
}
button {
display: block;
width: 100%;
margin: 0;
border: 0;
padding: 10px;
}
}
}
================================================
FILE: index.html
================================================
M8 Display
Click a key to add a new mapping
Press a keyboard key or gamepad button
Failed to connect over Serial.
Are you sure your Teensy is connected and the M8 Headless firmware is loaded?
Make sure that there are no other programs running which may have the serial port open.
On Linux you may also need make sure you have permission to open the serial port (eg. make yourself a member of the dialout group).
It is also possible there's a bug in this code.
There may be some messages in the developer console that will help with debugging.
Failed to connect with WebUSB.
Are you sure your Teensy is connected and the M8 Headless firmware is loaded?
Connecting with WebUSB is known not to work on Windows, Linux and some Samsung phones.
Please make sure you are using an up to date version of Chrome or Edge (89+).
Your browser doesn't appear to have Serial or WebUSB support.
These are only currently supported in Chrome and some Chrome-derived browsers.
M8 Display
This is a display for the M8 Headless firmware running on a Teensy 4.1, or a real M8 device.
You can use keyboard and gamepad input or use on-screen controls.
Where possible, the audio output from the M8 is routed to your default audio output device.
Firmware can be installed and updated from the menu.
By default the arrow keys on your keyboard map to the arrow keys on the M8.
Shift is Left Shift, Play is Space, Option is Z, and Edit is X.
These control mappings and other settings can be configured from the menu.
A virtual keyboard from A to ' lets you send MIDI notes.
Use -/= to change octaves and [/] to change velocity.
Now that this page has loaded it will work completely offline.
All settings are stored locally by your browser.