Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/publish_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ jobs:
python -m pip install --upgrade pip
pip install requests pandas markdown pytablereader tabulate
npm install
- name: Set up Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 3.1.61
actions-cache-folder: emsdk-cache
- name: Build browser WebAssembly decoder
run: npm run build:web
working-directory: nodejs/theengs-decoder
- name: Build documentation
run: |
npm run docs:build
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ docs/.vitepress/dist/
docs/.vitepress/cache/
docs/.vitepress/public/commonConfig.js
docs/.vitepress/public/img/
docs/.vitepress/public/wasm/
docs/.vitepress/data/devices.json
nodejs/theengs-decoder/build-web/
docs/devices/_app_devices.md
generated/
21 changes: 21 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ elseif(BUILD_WASM)
LINK_FLAGS "-lembind -sMODULARIZE=1 -sEXPORT_NAME=createTheengsDecoderModule -sENVIRONMENT=node -sALLOW_MEMORY_GROWTH=1 -sSINGLE_FILE=1 -sNODEJS_CATCH_EXIT=0 -sNODEJS_CATCH_REJECTION=0"
)

elseif(BUILD_WASM_WEB)
message(STATUS "Building WebAssembly module via Emscripten (browser target)")

add_executable(theengs_decoder_web
nodejs/theengs-decoder/src/wasm_bindings.cc
src/decoder.cpp
)

target_include_directories(theengs_decoder_web
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/arduino_json/src
${CMAKE_CURRENT_SOURCE_DIR}/src
)

target_compile_features(theengs_decoder_web PRIVATE cxx_std_17)

set_target_properties(theengs_decoder_web PROPERTIES
SUFFIX ".js"
LINK_FLAGS "-lembind -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createTheengsDecoderModule -sENVIRONMENT=web,worker -sALLOW_MEMORY_GROWTH=1 -sSINGLE_FILE=1"
)

else()

add_library(decoder
Expand Down
311 changes: 311 additions & 0 deletions docs/.vitepress/components/WebParser.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef } from 'vue'

const DEFAULT_PAYLOAD = {
id: '88:22:44:44:11:44',
rssi: -85,
servicedata: '08094411444422880104e500c6020702aa2702012d',
servicedatauuid: '0xfdcd',
}

const input = ref(JSON.stringify(DEFAULT_PAYLOAD, null, 2))
const status = ref<'idle' | 'loading' | 'ready' | 'error'>('idle')
const errorMsg = ref<string | null>(null)
const result = ref<unknown>(null)
const noMatch = ref(false)
const decoder = shallowRef<any>(null)

const formattedResult = computed(() =>
result.value === null ? '' : JSON.stringify(result.value, null, 2),
)

async function loadDecoder() {
if (decoder.value || status.value === 'loading') return
status.value = 'loading'
try {
const base = (import.meta as any).env?.BASE_URL ?? '/'
const path = `${base}wasm/theengs_decoder_web.js`
const absoluteUrl = new URL(path, window.location.href).href
const shimSource = `export { default } from ${JSON.stringify(absoluteUrl)}`
const blob = new Blob([shimSource], { type: 'application/javascript' })
const blobUrl = URL.createObjectURL(blob)
let mod: any
try {
mod = await import(/* @vite-ignore */ blobUrl)
} finally {
URL.revokeObjectURL(blobUrl)
}
const factory = mod.default ?? mod.createTheengsDecoderModule
const Module = await factory()
decoder.value = new Module.TheengsDecoder()
status.value = 'ready'
} catch (err: any) {
status.value = 'error'
errorMsg.value = `Failed to load decoder: ${err?.message ?? err}`
}
}

function decode() {
errorMsg.value = null
noMatch.value = false
result.value = null

if (!decoder.value) {
errorMsg.value = 'Decoder not ready yet. Try again in a moment.'
return
}

let parsed: Record<string, unknown>
try {
parsed = JSON.parse(input.value)
} catch (err: any) {
errorMsg.value = `Invalid JSON: ${err?.message ?? err}`
return
}

if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
errorMsg.value = 'Input must be a JSON object.'
return
}

if (!('servicedata' in parsed) && !('manufacturerdata' in parsed)) {
errorMsg.value =
'Object must include at least one of "servicedata" or "manufacturerdata".'
return
}

try {
const raw = decoder.value.decodeBLE(JSON.stringify(parsed))
if (!raw) {
noMatch.value = true
return
}
result.value = JSON.parse(raw)
} catch (err: any) {
errorMsg.value = `Decode failed: ${err?.message ?? err}`
}
}

function resetExample() {
input.value = JSON.stringify(DEFAULT_PAYLOAD, null, 2)
errorMsg.value = null
noMatch.value = false
result.value = null
}

onMounted(() => {
loadDecoder()
})
</script>

<template>
<div class="web-parser">
<p class="intro">
Paste a BLE advertisement as a JSON object below and the decoder runs
locally in your browser via WebAssembly — no data leaves your device.
The object must include
<code>servicedata</code> or <code>manufacturerdata</code> (hex strings),
optionally with <code>servicedatauuid</code>, <code>name</code>,
<code>mac</code>, or <code>id</code>.
</p>

<div class="controls">
<button type="button" class="reset-btn" @click="resetExample">
Reset to example
</button>
<span class="status" :data-state="status">
<template v-if="status === 'loading'">Loading decoder…</template>
<template v-else-if="status === 'ready'">Decoder ready</template>
<template v-else-if="status === 'error'">Decoder unavailable</template>
<template v-else>Initializing…</template>
</span>
</div>

<div class="grid">
<div class="pane">
<label class="pane-label" for="parser-input">Input JSON</label>
<textarea
id="parser-input"
v-model="input"
spellcheck="false"
autocomplete="off"
autocapitalize="off"
rows="10"
/>
<button
class="decode-btn"
:disabled="status !== 'ready'"
@click="decode"
>
Decode
</button>
</div>

<div class="pane">
<label class="pane-label">Decoded output</label>
<pre v-if="errorMsg" class="output error">{{ errorMsg }}</pre>
<pre v-else-if="noMatch" class="output muted">No decoder matched this payload.</pre>
<pre v-else-if="formattedResult" class="output">{{ formattedResult }}</pre>
<pre v-else class="output muted">Click <strong>Decode</strong> to see the result.</pre>
</div>
</div>
</div>
</template>

<style scoped>
.web-parser {
margin: 1rem 0 2rem;
}

.intro {
color: var(--vp-c-text-2);
font-size: 0.95rem;
line-height: 1.55;
margin-bottom: 1.25rem;
}

.intro code {
font-size: 0.85em;
padding: 0.1em 0.35em;
border-radius: 4px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}

.controls {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1rem;
}

.reset-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.85rem;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}

.reset-btn:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}

.status {
font-size: 0.85rem;
color: var(--vp-c-text-2);
padding: 0.25rem 0.6rem;
border-radius: 999px;
background: var(--vp-c-bg-soft);
}

.status[data-state="ready"] {
color: var(--vp-c-brand-1);
}

.status[data-state="error"] {
color: var(--vp-c-danger-1, #d93f3f);
}

.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}

@media (max-width: 720px) {
.grid {
grid-template-columns: 1fr;
}
}

.pane {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
}

.pane-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-transform: uppercase;
letter-spacing: 0.04em;
}

textarea {
width: 100%;
min-height: 220px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
line-height: 1.5;
resize: vertical;
transition: border-color 0.2s;
}

textarea:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}

.decode-btn {
align-self: flex-start;
padding: 0.55rem 1.2rem;
border: none;
border-radius: 6px;
background: var(--vp-c-brand-1);
color: var(--vp-c-white, #fff);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s, opacity 0.2s;
}

.decode-btn:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}

.decode-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}

.output {
margin: 0;
padding: 0.75rem;
min-height: 220px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
overflow: auto;
}

.output.error {
border-color: var(--vp-c-danger-1, #d93f3f);
color: var(--vp-c-danger-1, #d93f3f);
background: var(--vp-c-danger-soft, rgba(217, 63, 63, 0.08));
}

.output.muted {
color: var(--vp-c-text-3, var(--vp-c-text-2));
font-style: italic;
}
</style>
3 changes: 2 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export default defineConfig({
{ text: 'Development contributions', link: '/participate/development' }
]
},
{ text: 'Compatible devices', link: '/devices/devices' }
{ text: 'Compatible devices', link: '/devices/devices' },
{ text: 'Web Parser', link: '/parser' }
],
search: { provider: 'local' }
},
Expand Down
2 changes: 2 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import mediumZoom from 'medium-zoom'
import { onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vitepress'
import DevicesTable from '../components/DevicesTable.vue'
import WebParser from '../components/WebParser.vue'
import './custom.css'

export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('DevicesTable', DevicesTable)
app.component('WebParser', WebParser)
},
setup() {
const route = useRoute()
Expand Down
Loading
Loading