-
Notifications
You must be signed in to change notification settings - Fork 1.8k
SITL running in-browser / in Configurator as WASM (web assembly) for comparison #11314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: maintenance-9.x
Are you sure you want to change the base?
SITL running in-browser / in Configurator as WASM (web assembly) for comparison #11314
Conversation
Add CMake infrastructure for building INAV as WebAssembly: - New wasm.cmake toolchain file for Emscripten compiler - WASM-specific settings in sitl.cmake (ASYNCIFY, exports, memory growth) - Build type detection in settings.cmake - Emscripten SDK path checks in wasm-checks.cmake This enables building SITL firmware that runs in web browsers.
Modify parameter group system to support WASM lazy allocation: - Add wasmPgEnsureAllocated() call in PG accessor macros for __EMSCRIPTEN__ - Include wasm_pg_runtime.h header when building for WASM - Restore ZERO_FARRAY macro in config_streamer_ram.c In WASM builds, PG memory cannot be allocated by the linker at compile-time, so accessor macros trigger runtime allocation on first access.
Integrate WASM support into flight controller core: - main.c: Call wasmMspProcess() in main loop for MSP handling - fc_init.c: Initialize WASM PG system and serial port at boot - fc_core.c: Use emscripten_force_exit() for clean WASM reboot - config.c: Support WASM PG initialization in config system - serial.c: Register WASM virtual serial port These hooks enable the firmware to run as WebAssembly while maintaining compatibility with native SITL builds.
New serial driver that communicates over WebSocket for SITL: - Enables configurator PWA to connect to native SITL via WebSocket - Implements standard serialPort_t interface (read, write, available) - Supports both WASM and native SITL builds - Ring buffer based TX/RX with configurable sizes This allows the web-based configurator to connect to SITL running either natively (via WebSocket) or in-browser (via WASM).
Complete WASM SITL implementation for running firmware in browsers: Serial transport (serial_wasm.c/h): - Virtual serial port with ring buffers for JS<->WASM communication - Exported functions: serialWriteByte, serialReadByte, serialAvailable - Interrupt-style callback notification when responses ready MSP bridge (wasm_msp_bridge.c/h): - Connects WASM serial port to standard MSP infrastructure - Reuses existing mspSerialProcessOnePort() - zero MSP code duplication Parameter group system (wasm_pg_registry.c/h, wasm_pg_runtime.c/h): - Manual PG registry (linker script not supported in WASM) - Lazy memory allocation on first PG access - Handles both system configs and profile arrays Platform stubs (wasm_stubs.c): - Stub implementations for hardware-dependent functions - Time functions using emscripten_get_now() Target config (target.c): - WASM-specific target initialization
Testing and development utilities for WASM SITL: Test harness (src/test/wasm/): - Browser-based MSP test interface - Sends MSP commands and displays responses - Useful for validating WASM build without full configurator PG registry generator (src/utils/): - Script to generate wasm_pg_registry.c from PG declarations - Scans source for PG_REGISTER macros - Outputs manual registry for WASM builds
Implement persistent storage for WASM SITL settings using browser IndexedDB. Settings now survive page reloads. Firmware changes: - Add wasm_eeprom_bridge.c/h exposing EEPROM buffer to JavaScript - Add wasmNotifyEepromSaved() callback in config_streamer_ram.c - Export EEPROM functions and HEAPU8/callMain in cmake/sitl.cmake The JavaScript side (in configurator) loads stored EEPROM data from IndexedDB before calling main(), allowing the firmware to initialize with previously saved settings.
PR Compliance Guide 🔍All compliance sections have been disabled in the configurations. |
| uint8_t payload[WS_MAX_PACKET_SIZE]; | ||
| size_t payload_len = 0; | ||
| ssize_t consumed = ws_decode_frame(port, buffer, recvSize, payload, &payload_len); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: In wsReceive, reduce the payload buffer size from WS_MAX_PACKET_SIZE to WS_BUFFER_SIZE to prevent a potential stack overflow, as the received data chunk is limited to WS_BUFFER_SIZE. [possible issue, importance: 9]
| uint8_t payload[WS_MAX_PACKET_SIZE]; | |
| size_t payload_len = 0; | |
| ssize_t consumed = ws_decode_frame(port, buffer, recvSize, payload, &payload_len); | |
| static ssize_t ws_decode_frame(wsPort_t *port, const uint8_t *data, size_t len, uint8_t *payload, size_t *payload_len, size_t max_payload_len) | |
| { | |
| if (len < 2) return 0; // Need at least 2 bytes | |
| ... | |
| // Decode payload (unmask if needed) | |
| if (opcode == WS_OPCODE_BINARY || opcode == WS_OPCODE_TEXT || opcode == WS_OPCODE_CONTINUATION) { | |
| if (payload_length > max_payload_len) { | |
| // Prevent buffer overflow if decoded payload is larger than the provided buffer | |
| return -1; // Or handle error appropriately | |
| } | |
| for (size_t i = 0; i < payload_length; i++) { | |
| payload[i] = masked ? (p[i] ^ mask[i % 4]) : p[i]; | |
| } | |
| *payload_len = payload_length; | |
| } | |
| ... | |
| } | |
| ... | |
| static int wsReceive(wsPort_t *port) | |
| { | |
| ... | |
| // Receive WebSocket frame | |
| uint8_t buffer[WS_BUFFER_SIZE]; | |
| ssize_t recvSize = recv(port->clientSocketFd, buffer, WS_BUFFER_SIZE, 0); | |
| ... | |
| // Decode WebSocket frame | |
| uint8_t payload[WS_BUFFER_SIZE]; | |
| size_t payload_len = 0; | |
| ssize_t consumed = ws_decode_frame(port, buffer, recvSize, payload, &payload_len, sizeof(payload)); | |
| ... | |
| } |
| void wsWriteBuf(serialPort_t *instance, const void *data, int count) | ||
| { | ||
| wsPort_t *port = (wsPort_t*)instance; | ||
|
|
||
| if (!port->isClientConnected || !port->isHandshakeComplete) { | ||
| return; | ||
| } | ||
|
|
||
| // Encode as WebSocket binary frame | ||
| uint8_t frame[WS_MAX_PACKET_SIZE + 14]; // +14 for header | ||
| size_t frame_len; | ||
| ws_encode_frame((const uint8_t *)data, count, WS_OPCODE_BINARY, frame, &frame_len); | ||
|
|
||
| fprintf(stderr, "[WEBSOCKET] UART%d TX %d bytes: ", port->id, count); | ||
| const uint8_t *payload = (const uint8_t *)data; | ||
| for (int i = 0; i < count && i < 32; i++) { | ||
| fprintf(stderr, "%02x ", payload[i]); | ||
| } | ||
| fprintf(stderr, "\n"); | ||
|
|
||
| send(port->clientSocketFd, frame, frame_len, 0); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: In wsWriteBuf, replace the large stack-allocated frame buffer with heap allocation (malloc/free) to prevent potential stack overflows when handling large data writes. [possible issue, importance: 9]
| void wsWriteBuf(serialPort_t *instance, const void *data, int count) | |
| { | |
| wsPort_t *port = (wsPort_t*)instance; | |
| if (!port->isClientConnected || !port->isHandshakeComplete) { | |
| return; | |
| } | |
| // Encode as WebSocket binary frame | |
| uint8_t frame[WS_MAX_PACKET_SIZE + 14]; // +14 for header | |
| size_t frame_len; | |
| ws_encode_frame((const uint8_t *)data, count, WS_OPCODE_BINARY, frame, &frame_len); | |
| fprintf(stderr, "[WEBSOCKET] UART%d TX %d bytes: ", port->id, count); | |
| const uint8_t *payload = (const uint8_t *)data; | |
| for (int i = 0; i < count && i < 32; i++) { | |
| fprintf(stderr, "%02x ", payload[i]); | |
| } | |
| fprintf(stderr, "\n"); | |
| send(port->clientSocketFd, frame, frame_len, 0); | |
| } | |
| void wsWriteBuf(serialPort_t *instance, const void *data, int count) | |
| { | |
| wsPort_t *port = (wsPort_t*)instance; | |
| if (!port->isClientConnected || !port->isHandshakeComplete) { | |
| return; | |
| } | |
| // Allocate frame buffer on the heap to avoid stack overflow | |
| size_t max_frame_size = count + 14; // payload + max header size | |
| uint8_t *frame = malloc(max_frame_size); | |
| if (!frame) { | |
| fprintf(stderr, "[WEBSOCKET] Failed to allocate memory for send buffer\n"); | |
| return; | |
| } | |
| // Encode as WebSocket binary frame | |
| size_t frame_len; | |
| ws_encode_frame((const uint8_t *)data, count, WS_OPCODE_BINARY, frame, &frame_len); | |
| fprintf(stderr, "[WEBSOCKET] UART%d TX %d bytes: ", port->id, count); | |
| const uint8_t *payload = (const uint8_t *)data; | |
| for (int i = 0; i < count && i < 32; i++) { | |
| fprintf(stderr, "%02x ", payload[i]); | |
| } | |
| fprintf(stderr, "\n"); | |
| send(port->clientSocketFd, frame, frame_len, 0); | |
| free(frame); | |
| } |
| static void* fixupProfilePointer(const pgRegistry_t *reg, pgRegistry_t *mutableReg) | ||
| { | ||
| if (!mutableReg->ptr) { | ||
| // Allocate the pointer variable | ||
| uint8_t **currentPtr = (uint8_t**)calloc(1, sizeof(uint8_t*)); | ||
| if (!currentPtr) { | ||
| PG_ALLOC_ERROR(pgN(reg), sizeof(uint8_t*)); | ||
| return NULL; | ||
| } | ||
| *currentPtr = mutableReg->address; | ||
| mutableReg->ptr = currentPtr; | ||
| } else if (!*mutableReg->ptr) { | ||
| // Pointer exists but points to NULL | ||
| *mutableReg->ptr = mutableReg->address; | ||
| } | ||
| return *mutableReg->ptr; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: In fixupProfilePointer, avoid dereferencing mutableReg->ptr in the else if condition to prevent potential crashes from garbage pointer values, which could lead to undefined behavior. [possible issue, importance: 7]
| static void* fixupProfilePointer(const pgRegistry_t *reg, pgRegistry_t *mutableReg) | |
| { | |
| if (!mutableReg->ptr) { | |
| // Allocate the pointer variable | |
| uint8_t **currentPtr = (uint8_t**)calloc(1, sizeof(uint8_t*)); | |
| if (!currentPtr) { | |
| PG_ALLOC_ERROR(pgN(reg), sizeof(uint8_t*)); | |
| return NULL; | |
| } | |
| *currentPtr = mutableReg->address; | |
| mutableReg->ptr = currentPtr; | |
| } else if (!*mutableReg->ptr) { | |
| // Pointer exists but points to NULL | |
| *mutableReg->ptr = mutableReg->address; | |
| } | |
| return *mutableReg->ptr; | |
| } | |
| static void* fixupProfilePointer(const pgRegistry_t *reg, pgRegistry_t *mutableReg) | |
| { | |
| if (!mutableReg->ptr) { | |
| // Allocate the pointer variable | |
| uint8_t **currentPtr = (uint8_t**)calloc(1, sizeof(uint8_t*)); | |
| if (!currentPtr) { | |
| PG_ALLOC_ERROR(pgN(reg), sizeof(uint8_t*)); | |
| return NULL; | |
| } | |
| *currentPtr = mutableReg->address; | |
| mutableReg->ptr = currentPtr; | |
| } | |
| // After allocation (or if it existed), ensure the pointer it holds is valid. | |
| if (!*mutableReg->ptr) { | |
| *mutableReg->ptr = mutableReg->address; | |
| } | |
| return *mutableReg->ptr; | |
| } |
| int err = pthread_create(&port->receiveThread, NULL, wsReceiveThread, (void*)port); | ||
| if (err < 0){ | ||
| fprintf(stderr, "[WEBSOCKET] Unable to create receive thread for UART%d\n", id); | ||
| return NULL; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Correct the error check for pthread_create from err < 0 to err != 0, as the function returns a positive error code on failure, not a negative one. [possible issue, importance: 8]
| int err = pthread_create(&port->receiveThread, NULL, wsReceiveThread, (void*)port); | |
| if (err < 0){ | |
| fprintf(stderr, "[WEBSOCKET] Unable to create receive thread for UART%d\n", id); | |
| return NULL; | |
| } | |
| int err = pthread_create(&port->receiveThread, NULL, wsReceiveThread, (void*)port); | |
| if (err != 0){ | |
| fprintf(stderr, "[WEBSOCKET] Unable to create receive thread for UART%d\n", id); | |
| return NULL; | |
| } |
| int one = 1; | ||
| err = setsockopt(port->socketFd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); | ||
| err = setsockopt(port->socketFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); | ||
| err = fcntl(port->socketFd, F_SETFL, fcntl(port->socketFd, F_GETFL, 0) | O_NONBLOCK); | ||
|
|
||
| if (err < 0){ | ||
| fprintf(stderr, "[WEBSOCKET] Unable to set socket options\n"); | ||
| return NULL; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: In wsReConfigure, check the return value of each setsockopt and fcntl call individually to ensure all socket options are set correctly and to handle any specific failures. [possible issue, importance: 6]
| int one = 1; | |
| err = setsockopt(port->socketFd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); | |
| err = setsockopt(port->socketFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); | |
| err = fcntl(port->socketFd, F_SETFL, fcntl(port->socketFd, F_GETFL, 0) | O_NONBLOCK); | |
| if (err < 0){ | |
| fprintf(stderr, "[WEBSOCKET] Unable to set socket options\n"); | |
| return NULL; | |
| } | |
| int one = 1; | |
| if (setsockopt(port->socketFd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) < 0) { | |
| fprintf(stderr, "[WEBSOCKET] Failed to set TCP_NODELAY: %s\n", strerror(errno)); | |
| return NULL; | |
| } | |
| if (setsockopt(port->socketFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { | |
| fprintf(stderr, "[WEBSOCKET] Failed to set SO_REUSEADDR: %s\n", strerror(errno)); | |
| return NULL; | |
| } | |
| if (fcntl(port->socketFd, F_SETFL, fcntl(port->socketFd, F_GETFL, 0) | O_NONBLOCK) < 0) { | |
| fprintf(stderr, "[WEBSOCKET] Failed to set non-blocking mode: %s\n", strerror(errno)); | |
| return NULL; | |
| } |
| -sASYNCIFY=1 | ||
| -sWEBSOCKET_URL="ws://localhost:5771" | ||
| -sFORCE_FILESYSTEM=1 | ||
| -sEXPORTED_FUNCTIONS=_main,_serialWriteByte,_serialReadByte,_serialAvailable,_serialGetRxDroppedBytes,_serialGetTxDroppedBytes,_wasmGetEepromPtr,_wasmGetEepromSize,_wasmReloadConfig,_wasmIsEepromValid,_malloc,_free |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Add the missing MSP bridge functions (_wasm_msp_process_command, _wasm_msp_get_api_version, _wasm_msp_get_fc_variant) to the EXPORTED_FUNCTIONS list in cmake/sitl.cmake to ensure they are available to the JavaScript test harness. [possible issue, importance: 9]
| -sEXPORTED_FUNCTIONS=_main,_serialWriteByte,_serialReadByte,_serialAvailable,_serialGetRxDroppedBytes,_serialGetTxDroppedBytes,_wasmGetEepromPtr,_wasmGetEepromSize,_wasmReloadConfig,_wasmIsEepromValid,_malloc,_free | |
| -sEXPORTED_FUNCTIONS=_main,_serialWriteByte,_serialReadByte,_serialAvailable,_serialGetRxDroppedBytes,_serialGetTxDroppedBytes,_wasmGetEepromPtr,_wasmGetEepromSize,_wasmReloadConfig,_wasmIsEepromValid,_wasm_msp_process_command,_wasm_msp_get_api_version,_wasm_msp_get_fc_variant,_malloc,_free |
| PG_NAMES=$(cd "$INAV_ROOT" && grep -r "^PG_REGISTER" --include="*.c" src/main/ | \ | ||
| grep -v "ledstrip.c:\|light_ws2811strip.c:\|esc_sensor.c:\|piniobox.c:\|osd_joystick.c:\|lights.c:\|rpm_filter.c:\|smartport_master.c:" | \ | ||
| sed -E 's/.*:PG_REGISTER_ARRAY[^(]*\([^,]*,[^,]*, ([a-zA-Z0-9_]+),.*/\1/; t; s/.*:PG_REGISTER[^(]*\([^,]*, ([a-zA-Z0-9_]+),.*/\1/' | \ | ||
| sort -u) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Update the sed command in generate_wasm_pg_registry.sh to correctly parse all variants of the PG_REGISTER macro, not just two, to prevent an incomplete parameter group registry. [possible issue, importance: 8]
| PG_NAMES=$(cd "$INAV_ROOT" && grep -r "^PG_REGISTER" --include="*.c" src/main/ | \ | |
| grep -v "ledstrip.c:\|light_ws2811strip.c:\|esc_sensor.c:\|piniobox.c:\|osd_joystick.c:\|lights.c:\|rpm_filter.c:\|smartport_master.c:" | \ | |
| sed -E 's/.*:PG_REGISTER_ARRAY[^(]*\([^,]*,[^,]*, ([a-zA-Z0-9_]+),.*/\1/; t; s/.*:PG_REGISTER[^(]*\([^,]*, ([a-zA-Z0-9_]+),.*/\1/' | \ | |
| sort -u) | |
| PG_NAMES=$(cd "$INAV_ROOT" && grep -r "^PG_REGISTER" --include="*.c" src/main/ | \ | |
| grep -v "ledstrip.c:\|light_ws2811strip.c:\|esc_sensor.c:\|piniobox.c:\|osd_joystick.c:\|lights.c:\|rpm_filter.c:\|smartport_master.c:" | \ | |
| sed -E 's/.*:PG_REGISTER(_ARRAY|_WITH_RESET_FN)?[^(]*\([^,]*,[^,]*, *([a-zA-Z0-9_]+),.*/\2/; t; s/.*:PG_REGISTER[^(]*\([^,]*, *([a-zA-Z0-9_]+),.*/\1/' | \ | |
| sort -u) |
| // Check if we have full payload | ||
| if (len < bytes_consumed + payload_length) { | ||
| return 0; // Need more data | ||
| } | ||
|
|
||
| // Handle control frames | ||
| if (opcode == WS_OPCODE_CLOSE) { | ||
| return -1; // Connection close | ||
| } | ||
|
|
||
| if (opcode == WS_OPCODE_PING) { | ||
| // Respond with PONG | ||
| uint8_t pong_frame[2] = {0x8A, 0x00}; // FIN + PONG opcode, no payload | ||
| send(port->clientSocketFd, pong_frame, 2, 0); | ||
| return bytes_consumed + payload_length; | ||
| } | ||
|
|
||
| // Decode payload (unmask if needed) | ||
| if (opcode == WS_OPCODE_BINARY || opcode == WS_OPCODE_TEXT || opcode == WS_OPCODE_CONTINUATION) { | ||
| for (size_t i = 0; i < payload_length; i++) { | ||
| payload[i] = masked ? (p[i] ^ mask[i % 4]) : p[i]; | ||
| } | ||
| *payload_len = payload_length; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Validate that payload_length does not exceed the provided payload buffer capacity (e.g. WS_MAX_PACKET_SIZE) before copying/unmasking, and return a clear error/close on overflow. [Learned best practice, importance: 6]
| // Check if we have full payload | |
| if (len < bytes_consumed + payload_length) { | |
| return 0; // Need more data | |
| } | |
| // Handle control frames | |
| if (opcode == WS_OPCODE_CLOSE) { | |
| return -1; // Connection close | |
| } | |
| if (opcode == WS_OPCODE_PING) { | |
| // Respond with PONG | |
| uint8_t pong_frame[2] = {0x8A, 0x00}; // FIN + PONG opcode, no payload | |
| send(port->clientSocketFd, pong_frame, 2, 0); | |
| return bytes_consumed + payload_length; | |
| } | |
| // Decode payload (unmask if needed) | |
| if (opcode == WS_OPCODE_BINARY || opcode == WS_OPCODE_TEXT || opcode == WS_OPCODE_CONTINUATION) { | |
| for (size_t i = 0; i < payload_length; i++) { | |
| payload[i] = masked ? (p[i] ^ mask[i % 4]) : p[i]; | |
| } | |
| *payload_len = payload_length; | |
| } | |
| // Check if we have full payload | |
| if (len < bytes_consumed + payload_length) { | |
| return 0; // Need more data | |
| } | |
| // Guard against payload buffer overflow | |
| if (payload_length > WS_MAX_PACKET_SIZE) { | |
| return -1; // Protocol error / too large for our buffer | |
| } | |
| ... | |
| // Decode payload (unmask if needed) | |
| if (opcode == WS_OPCODE_BINARY || opcode == WS_OPCODE_TEXT || opcode == WS_OPCODE_CONTINUATION) { | |
| for (size_t i = 0; i < payload_length; i++) { | |
| payload[i] = masked ? (p[i] ^ mask[i % 4]) : p[i]; | |
| } | |
| *payload_len = (size_t)payload_length; | |
| } |
|
Haha, great! I'll have to take a closer look at that. The serial interface looks good, I'm not particularly pleased with the polling either, but there was no other way as my approach uses threading, (emscripten_main_loop ‘manages’ a maximum of 60 fps/hz) and otherwise there were always race conditions. The approach of using WebAssembly as a generic platform for everyone is brilliant, I hadn't even thought of that. Incidentally, Emscripten can also create ES6-compatible code, which makes integration much easier. Above all, webassembly can be completely unloaded and reloaded without reloading the entire browser page, which makes the ‘FC reboot’ much more convenient. One more thing: I've found that the Origin Private File System (OPFS) works much more simply and reliably than indexedDb. |
|
To me, the advantage of running in a browser is that you don't have to run native code - you can do PWA and run it right on your phone or whatever. My thought on X-Plane is if you're running X-Plane, you're running native code, so it's okay that you need a native code exe to provide the socket. |
https://github.com/emscripten-core/emscripten/tree/main/tools/websocket_to_posix_proxy Possible solutions:
Electron: |
What advantage does that native executable have vs this one ? X-Plane 12 requires a beefy GPU to keep up with INAV, X-Plane 11 doesn't. I'm not seeing a reason to run a 500 KB native executable AND running wasm if you can do the same thing better - with much better performance, more simply, by running the 500 KB executable and not wasm. If you're already running a native executable, does wasm also give you anything besides slowness? And complexity. |
Do you mean serial_porxy? That's something completely different. Serial proxy to use the FC as a receiver in SITL.
Yeah, thats true, we should keep supporting X-Plane 11, Just an idea.
So, what would be the solution? Leave it as it is and use SITL in PWA primarily for demo mode, mark SITL as “Works but not recommended,” and tell people to use the native app for SITL? |
|
Yeah as far as I can see, the use case for wasm is when you're just running the browser, not a native exe. So demo mode or similar.
If you want a native executable that accepts tcp sockets, that's just target.c Once we have a native executable running, I'm not seeing the benefit of then mucking about with websockets. |
User description
This allows demo mode to work without a needing a platform-specific sitl.exe.
This is one step to allowing Configurator to load as a PWA if desired. It runs successfully in Configurator, including saving settings, rebooting, etc.
Scavanger's implentation is probably better overall #11282
I am putting this here mostly for comparison and there are a couple of things in this implementation that might be added to PR #11282 .
Scavanger's handling of PGs looks significantly better (though I haven't actually tested his to see that it works). Also the pthread support may be better (I used ASYNCIFY).
The serial communication in this implementation is "interrupt driven", while the other uses polling.
This implementation is more similar to how hardware is handled - the hardware triggers an interrupt when data is available. In my wasm implementation, that interrupt is of course triggered by software, but otherwise works like a hardware interrupt, the same as a hardware FC.
The approach to rebooting used in PR #11282 didn't work for me when I had tried it, so I used a different method for reboot.
Lastly, I added -gsource-map to wasm.cmake so that you can debug in the C code in the browser. This is very helpful for debugging and has no runtime cost.
PR Type
Enhancement, Tests
Description
This description is generated by an AI tool. It may have inaccuracies
Enables SITL (Software In The Loop) to run as WebAssembly (WASM) in-browser and within Configurator without requiring platform-specific executables
Allows demo mode functionality to work without needing
sitl.exeor other platform-specific binariesImplements interrupt-driven serial communication similar to hardware FC behavior, where interrupts are triggered by software events
Adds source map support (
-gsource-mapinwasm.cmake) for debugging C code directly in the browser with no runtime costSupports essential operations including settings persistence and device rebooting within the WASM environment
Provides an alternative implementation approach for comparison with other WASM SITL implementations, featuring different handling of parameter groups (PGs) and threading models
Uses ASYNCIFY for pthread support rather than alternative approaches
Implements custom reboot mechanism that differs from polling-based approaches
Represents a step toward enabling Configurator to function as a Progressive Web App (PWA)
Diagram Walkthrough
File Walkthrough