Skip to content
Draft
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
1,024 changes: 879 additions & 145 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 13 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,28 +144,29 @@ export function App() {
? await getDeviceInfoFast()
: await getDeviceInfo()
const info = parseESP800(raw)
const wsComm = info['webcommunication']?.trim() ?? ''
const parts = wsComm.split(':')
const asyncMode = parts[0]?.trim() === 'Async'
const wsPort = parts[1]?.trim() ?? '80'
const wsIp = parts[2]?.trim() ?? httpHost.split(':')[0]
const asyncMode = info['WebCommunication']?.trim() === 'Asynchronous'
const wsPort = info['WebSocketPort']?.trim() || '80'
const wsIp = info['WebSocketIP']?.trim() || httpHost.split(':')[0]
const wsHost = asyncMode
? `${httpHost}/ws`
: wsPort === '80' ? wsIp : `${wsIp}:${wsPort}`
cachedWsHost.current = wsHost
cachedHostFailures.current = 0
// Only refresh ESP info on a fresh probe.
const axes = parseInt(info['axis'] ?? '3', 10)
const axes = (info['Axisletters'] ?? '').length
setEspInfo({
version: info['FW version']?.trim() ?? '',
hostname: info['hostname']?.trim() ?? httpHost,
authentication: info['authentication']?.trim() === 'yes',
version: info['FWVersion']?.trim() ?? '',
hostname: info['HostName']?.trim() ?? httpHost,
authentication: info['Authentication']?.trim() === 'Enabled',
asyncMode,
wsPort: parseInt(wsPort, 10),
wsIp,
axes: isNaN(axes) ? 3 : axes,
primarySd: info['primary sd']?.trim() ?? '/sd/',
secondarySd: info['secondary sd']?.trim() ?? '/ext/',
axes: axes || 3,
// No JSON equivalent for these (the legacy plain-text response's
// "primary sd"/"secondary sd" fields) -- same defaults the old
// parse already fell back to whenever they were absent/unparseable.
primarySd: '/sd/',
secondarySd: '/ext/',
})
return { wsHost, info }
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const sendSilent = (cmd: string) =>
get('/command_silent', { plain: cmd })

export const getDeviceInfoFast = () =>
get('/command', { plain: '[ESP800]' }, 4000)
get('/command', { plain: '[ESP800]json=yes' }, 4000)

export function listFiles(path: string, fs: 'sd' | 'local' = 'sd'): Promise<FileListResult> {
return serialize(async () => {
Expand Down Expand Up @@ -214,7 +214,7 @@ export async function saveFileContent(
await uploadFile(path, file, fs)
}

export const getDeviceInfo = () => sendCommand('[ESP800]')
export const getDeviceInfo = () => sendCommand('[ESP800]json=yes')

export function uploadFirmware(
file: File,
Expand Down
25 changes: 19 additions & 6 deletions src/lib/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,27 @@ function parsePos(str: string): Position {
return { x: x ?? 0, y: y ?? 0, z: z ?? 0, a, b, c }
}

// [ESP800]json=yes wraps the same fields the legacy "FW version:... #
// FW target:... # ..." plain-text response carried in {cmd,status,data}
// instead -- see WifiConfig.cpp's showFwInfoJSON()/wasm/FwInfo.cpp. Field
// names change shape too (e.g. "webcommunication:Sync:81:127.0.0.1"
// becomes separate WebCommunication/WebSocketPort/WebSocketIP fields, and
// "authentication:yes" becomes "Authentication":"Enabled") -- see
// App.tsx's resolveWsHost(), the only real consumer, for how those are
// read now.
export function parseESP800(raw: string): Record<string, string> {
let parsed: unknown
try {
parsed = JSON.parse(raw)
} catch {
return {}
}
const data = (parsed as { data?: Record<string, unknown> })?.data
if (!data) return {}
const result: Record<string, string> = {}
raw.split('#').forEach(part => {
const idx = part.indexOf(':')
if (idx > -1) {
result[part.slice(0, idx).trim()] = part.slice(idx + 1).trim()
}
})
for (const [key, value] of Object.entries(data)) {
result[key] = String(value)
}
return result
}

Expand Down
7 changes: 7 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ import { App } from './App'
import './store/terminal'
import './lib/jogWatchdog'
import { disconnect as disconnectWs } from './lib/ws'
import { installWasmBridgeIfActive } from './wasmBridge'

if (import.meta.env.VITE_DEMO_MODE) {
const { installDemoMode } = await import('./demo')
installDemoMode()
}

// Not gated by import.meta.env, unlike installDemoMode() above -- this must
// survive in every build mode (including the real `build:esp32` artifact),
// since it's how a stock build detects it's running inside the FluidNC WASM
// demo at runtime. See wasmBridge/index.ts.
installWasmBridgeIfActive()

window.addEventListener('pagehide', () => { disconnectWs() })

// Suppress expected connection errors
Expand Down
109 changes: 109 additions & 0 deletions src/wasmBridge/WasmBridgeWebSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// A WebSocket-shaped class backed by the WASM demo's postMessage bridge
// instead of a real network socket -- installed as `window.WebSocket` (see
// index.ts) so src/lib/ws.ts works completely unmodified, the same way it
// talks to a real FluidNC WebSocket. Modeled directly on src/demo/
// wsSimulator.ts's FakeWebSocket (same public surface, since that's what
// ws.ts's `new WebSocket(url, 'arduino')` call needs), but instead of
// simulating a machine in JS, it relays to a real compiled FluidNC instance
// via shimTransport's postMessage channel.
//
// On real hardware, /command (src/lib/http.ts) and this WebSocket are
// genuinely independent connections, each answered by its own Channel
// object, so their response streams never cross. Here they both share
// the one physical ShimChannel, so demo/index.html serializes every send
// -- both /command's 'fluidnc-shim-command' RPC and this class's own
// raw sends below -- through one central queue (see its deliverShimLine()
// /maybeStartNextShimSend()), so only one thing is ever unacknowledged on
// the shim at a time and nothing can misattribute another's ok/error
// line. (An earlier version only queued /command calls against each
// other, on the theory that a raw send here couldn't be affected either
// way -- that turned out to be wrong: FluidNC's output for one call can
// arrive split across more than one output event, leaving a gap where a
// second queued /command could start and steal this class's own ok/error
// line. Send() below still posts straight to the shim -- the ordering
// guarantee now lives entirely in demo/index.html's queue instead.)
import { sendToShim, addShimLineListener } from './shimTransport'

export class WasmBridgeWebSocket extends EventTarget {
static readonly CONNECTING = 0
static readonly OPEN = 1
static readonly CLOSING = 2
static readonly CLOSED = 3

readonly CONNECTING = 0
readonly OPEN = 1
readonly CLOSING = 2
readonly CLOSED = 3

readyState: number = 0
binaryType: BinaryType = 'arraybuffer'
readonly url: string
readonly protocol: string
readonly bufferedAmount = 0
readonly extensions = ''

onopen: ((e: Event) => void) | null = null
onclose: ((e: CloseEvent) => void) | null = null
onerror: ((e: Event) => void) | null = null
onmessage: ((e: MessageEvent) => void) | null = null

private unsubscribe: (() => void) | null = null

constructor(url: string, protocols?: string | string[]) {
super()
this.url = url
this.protocol = Array.isArray(protocols) ? (protocols[0] ?? '') : (protocols ?? '')
// Deferred rather than synchronous so callers that set onopen/onmessage
// right after `new WebSocket(...)` (as ws.ts does) don't miss the open
// event -- matches a real WebSocket's inherently async connect.
setTimeout(() => this._open(), 0)
}

private _open() {
this.readyState = 1
// isJson is irrelevant here: whether a line came from a [JSON:...]
// reassembly or not, ws.ts wants the same thing real hardware's
// WSChannel would have sent it -- plain unwrapped text.
this.unsubscribe = addShimLineListener((line) => {
const ev = new MessageEvent('message', { data: `${line}\n` })
this.onmessage?.(ev)
this.dispatchEvent(ev)
})
const ev = new Event('open')
this.onopen?.(ev)
this.dispatchEvent(ev)
}

send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
if (this.readyState !== 1) return

if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buf =
data instanceof ArrayBuffer
? new Uint8Array(data)
: new Uint8Array((data as ArrayBufferView).buffer, (data as ArrayBufferView).byteOffset, (data as ArrayBufferView).byteLength)
// Realtime bytes ('?', ctrl-X, ...) -- ws.ts sends these as a single
// byte, same as it would write to a real socket. sendToShim() takes a
// string (emscripten's cwrap marshals it as UTF-8), and every
// realtime byte FluidNC defines is single-byte-UTF8-safe.
sendToShim(String.fromCharCode(...buf))
return
}

if (typeof data === 'string') {
sendToShim(data)
}
}

close(code?: number, reason?: string) {
if (this.readyState === 3) return
this.readyState = 3
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
}
const ev = new CloseEvent('close', { wasClean: true, code: code ?? 1000, reason: reason ?? '' })
this.onclose?.(ev)
this.dispatchEvent(ev)
}
}
18 changes: 18 additions & 0 deletions src/wasmBridge/commandBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Request/response command sends for the wasm bridge's HTTP-shaped surface
// (src/lib/http.ts's sendCommand/sendSilent/getDeviceInfo(Fast), which real
// hardware answers over its /command endpoint -- see WebCommands.cpp /
// WebUIServer.cpp). That handler isn't compiled into the wasm build at all
// (WebUI/ is excluded -- see platformio.ini's [env:wasm] build_src_filter),
// so every command here is forwarded through the same shim channel
// WasmBridgeWebSocket reads from, using shimTransport's sendShimCommand()
// (demo/index.html collects the response and queues concurrent sends
// centrally, so this doesn't need to serialize them itself anymore).
// [ESP800] used to be spoofed locally here (its real handler lives in
// WebUI/WifiConfig.cpp, also excluded, and needs WiFi/WebUI_Server APIs
// that don't exist in this build) -- wasm/FwInfo.cpp now provides a real,
// wasm-specific handler instead, so it's just another command.
import { sendShimCommand } from './shimTransport'

export function sendBridgeCommand(cmd: string): Promise<string> {
return sendShimCommand(cmd)
}
Loading