Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build-firmware.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-24.04

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-prf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-24.04

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-qemu-sdkshell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-24.04

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-qemu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-24.04

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-translation-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
BOARD_NAME: "asterix"

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

steps:
- name: Mark Github workspace as safe
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-24.04

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

strategy:
matrix:
Expand Down Expand Up @@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-24.04

container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2

strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
if: needs.changes-test.outputs.should-test == 'true'
runs-on: ubuntu-24.04
container:
image: ghcr.io/pebble-dev/pebbleos-docker:v1
image: ghcr.io/pebble-dev/pebbleos-docker:v2
steps:
- name: Mark Github workspace as safe
run: git config --system --add safe.directory "${GITHUB_WORKSPACE}"
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@
[submodule "third_party/resources/iconography"]
path = third_party/resources/iconography
url = https://github.com/pebble-dev/iconography
[submodule "third_party/moddable/moddable"]
path = third_party/moddable/moddable
url = https://github.com/Moddable-OpenSource/moddable.git
33 changes: 33 additions & 0 deletions docs/development/moddable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Moddable JS Engine

PebbleOS supports the [Moddable SDK](https://github.com/pebble-dev/moddable)'s XS JavaScript engine as an alternative to the default RockyJS engine. The Moddable integration provides a lightweight sandbox for running JavaScript applications and watchfaces on Pebble hardware.

## Building with Moddable

To build PebbleOS with the Moddable JS engine, pass `--js-engine moddable` to `./waf configure`.

### For Pebble Watch hardware

```
./waf configure --board obelix_bb2 --js-engine moddable
./waf build
```

### For QEMU emulator

```
./waf configure --board snowy_emery --js-engine moddable --qemu
./waf build qemu_image_spi qemu
```

## Installable applications

Installable applications — those built with `pebble build` — follow the same basic pattern as RockyJS apps. The JavaScript is stored in a resource. A small native C application bootstraps JavaScript execution by calling `moddable_createMachine`.

The JavaScript itself is a Moddable SDK **mod**, which is a precompiled collection of modules. The modules are compiled using the `mcrun` tool in the Moddable SDK. See Moddable's [Pebble Examples repository](https://github.com/moddable-OpenSource/pebble-examples) for further details. The Pebble resource containing the precompiled mod is currently loaded into RAM for execution; eventually, it should be executed directly from flash (this is what the Moddable SDK does on most platforms).

## Sandbox

The JavaScript in installable applications runs inside a lightweight sandbox, which can limit access to certain JavaScript global variables and modules.

Installable applications can be either a normal application or a watchface. The sandbox prevents watchfaces from subscribing to the Pebble hardware buttons (which is slightly redundant since PebbleOS also blocks them — but PebbleOS blocks them silently, whereas the sandbox throws an exception, which is more clear to developers).
3 changes: 2 additions & 1 deletion docs/development/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Keep in mind that some targets may not currently compile as-is.
## Main features

:`--js-engine`:
Specify JavaScript engine (rocky or none). Default is 'rocky'.
Specify JavaScript engine (moddable, or none). Default is 'moddable' with fallback to none if the board does not support Moddable XS.
Use 'moddable' for the Moddable SDK's XS engine (see {doc}`moddable`).
Use 'none' to disable JavaScript support.

## Debugging
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ development/options.md
development/building_fw.md
development/prf.md
development/qemu.md
development/moddable.md
```

```{toctree}
Expand Down
1 change: 1 addition & 0 deletions platform/platform_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
"HAS_MAGNETOMETER",
"HAS_PMIC",
"HAS_FLASH_OTP",
"HAS_MODDABLE_XS",
},
},
{
Expand Down
10 changes: 7 additions & 3 deletions sdk/waftools/pebble_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,13 @@ def configure(conf):
project_info = _extract_project_info(conf, info_json, info_json_node.name)

conf.env.PROJECT_INFO = project_info
conf.env.BUILD_TYPE = (
"rocky" if project_info.get("projectType", None) == "rocky" else "app"
)
project_type = project_info.get("projectType", None)
if project_type == "rocky":
conf.env.BUILD_TYPE = "rocky"
elif project_type == "moddable":
conf.env.BUILD_TYPE = "moddable"
else:
conf.env.BUILD_TYPE = "app"

if getattr(conf.env.PROJECT_INFO, "enableMultiJS", False):
if not conf.env.WEBPACK:
Expand Down
28 changes: 26 additions & 2 deletions sdk/waftools/process_sdk_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from waflib import Node

from resources.find_resource_filename import find_most_specific_filename
from resources.types.resource_definition import ResourceDefinition
from resources.types.resource_object import ResourceObject
from resources.resource_map import resource_generator
from sdk_helpers import is_sdk_2x, validate_resource_not_larger_than
Expand Down Expand Up @@ -116,6 +117,29 @@ def generate_resources(bld, resource_source_path):

resource_definitions.append(d)

# Auto-inject MOD resource for moddable projects
if getattr(bld.env, "BUILD_TYPE", None) == "moddable":
import os

platform = bld.env.PLATFORM_NAME
mod_path = "build/mods/{}/mc.xsa".format(platform)
full_mod_path = os.path.join(bld.path.abspath(), mod_path)
if os.path.exists(full_mod_path):
mod_definition = ResourceDefinition(
type="raw", name="MOD", file=mod_path, target_platforms=[platform]
)
resource_definitions.append(mod_definition)
resource_file_mapping["MOD"] = mod_path
print(
"Auto-injected MOD resource for {} from {}".format(platform, mod_path)
)
else:
print(
"WARNING: MOD resource not found at {} for {}".format(
full_mod_path, platform
)
)

bld_dir = bld.path.get_bld().make_node(bld.env.BUILD_DIR)
lib_resources = []
for lib in bld.env.LIB_JSON:
Expand Down Expand Up @@ -223,7 +247,7 @@ def generate_resources(bld, resource_source_path):
resource_ball=resource_ball,
resource_id_header_target=resource_id_header,
use_extern=build_type == "lib",
use_define=build_type == "app",
use_define=build_type in ("app", "moddable"),
published_media=published_media_json,
)

Expand All @@ -236,7 +260,7 @@ def generate_resources(bld, resource_source_path):
published_media=published_media_json,
)

if not bld.env.BUILD_TYPE or bld.env.BUILD_TYPE in ("app", "rocky"):
if not bld.env.BUILD_TYPE or bld.env.BUILD_TYPE in ("app", "rocky", "moddable"):
# Create a resource pack for distribution with an application binary
pbpack = bld_dir.make_node("app_resources.pbpack")
bld(
Expand Down
26 changes: 13 additions & 13 deletions src/fw/applib/graphics/graphics_private.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,20 @@ void graphics_private_set_pixel(GContext* ctx, GPoint point) {

// ## Private blending wrapper functions for non-aa

static const uint16_t grays[14] = {
0x0000, 0x0000,
0x1111, 0x4444,
0x5555, 0xAAAA,
0x5555, 0xAAAA,
0x5555, 0xAAAA,
0xEEEE, 0xBBBB,
0xFFFF, 0xFFFF
};

uint32_t graphics_private_get_1bit_grayscale_pattern(GColor color, uint8_t row_number) {
const GColor8Component luminance = (color.r + color.g + color.b) / 3;
switch (luminance) {
case 0:
return 0x00000000;
case 1:
case 2:
// This is done to create a checkerboard pattern for gray
return (row_number % 2) ? 0xAAAAAAAA : 0x55555555;
case 3:
return 0xFFFFFFFF;
default:
WTF;
}
uint16_t luma = ((color.r << 1) + color.r + (color.g << 2) + color.b) >> 1; // 0 to 12
luma = grays[(luma & ~1) + (row_number & 1)];
return (luma << 16) | luma;
}

void prv_assign_line_horizontal_non_aa(GContext* ctx, int16_t y, int16_t x1, int16_t x2) {
Expand Down
94 changes: 92 additions & 2 deletions src/fw/applib/moddable/moddable.c
Original file line number Diff line number Diff line change
@@ -1,11 +1,101 @@
/* SPDX-FileCopyrightText: 2026 Core Devices LLC */
/* SPDX-License-Identifier: Apache-2.0 */
#include "applib/app.h"
#include "kernel/logging_private.h"
#include "services/common/evented_timer.h"
#include "syscall/syscall_internal.h"
#include "applib/app_logging.h"
#include "applib/moddable/moddable.h"
#include "syscall/syscall_internal.h"

#if CAPABILITY_HAS_MODDABLE_XS && !defined(RECOVERY_FW)
#include "xsmc.h"
#include "xsHost.h"
#include "xsHosts.h"
#include "moddableAppState.h"
#include "kernel/pbl_malloc.h"

static void startMachine(void *data)
{
modRunMachineSetup((xsMachine *)data); // want this to be called after event loop is active
}

void moddable_cleanup(void)
{
ModdablePebbleAppState state = (ModdablePebbleAppState)app_state_get_rocky_memory_api_context();

xsDeleteMachine(state->the);

extern void modTimerExit(void);
modTimerExit();

app_state_set_rocky_memory_api_context(NULL);
task_free(state);
}

DEFINE_SYSCALL(void, moddable_createMachine, ModdableCreationRecord *cr)
{
xsMachine *the;

if (NULL == cr)
the = modCloneMachine(NULL, NULL);
else {
if (cr->recordSize < sizeof(ModdableCreationRecord)) {
PBL_LOG_ERR("invalid recordSize");
return;
}

uint32_t stack = cr->stack, slot = cr->slot, chunk = cr->chunk;
if (!stack && !slot && !chunk)
the = modCloneMachine(NULL, NULL);
else {
if (!stack || !slot || !chunk) {
PBL_LOG_ERR("invalid ModdableCreationRecord");
return;
}

stack = (stack + 3) & ~3;
slot = (slot + 3) & ~3;
chunk = (chunk + 3) & ~3;

xsCreation *defaultCreation;
extern void *xsPreparationAndCreation(xsCreation **creation);
(void)xsPreparationAndCreation(&defaultCreation);
struct xsCreationRecord creation = *defaultCreation;
creation.stackCount = stack / sizeof(xsSlot);
creation.initialHeapCount = slot / sizeof(xsSlot);
creation.initialChunkSize = chunk;
if ((stack + slot + chunk) <= (uint32_t)creation.staticSize)
creation.staticSize = stack + slot + chunk;
else {
creation.incrementalChunkSize = 0;
creation.incrementalHeapCount = 0;
creation.staticSize = 0;
}
the = modCloneMachine(&creation, NULL);
}
}

if (NULL == the) {
PBL_LOG_ERR("Failed to create XS machine");
return;
}

ModdablePebbleAppState state = task_zalloc_check(sizeof(ModdablePebbleAppStateRecord));
state->the = the;
state->eventedTimer = EVENTED_TIMER_INVALID_ID;
app_state_set_rocky_memory_api_context((void *)state);

evented_timer_register(2, false, startMachine, the);

app_event_loop();

moddable_cleanup();
}

#else

DEFINE_SYSCALL(void, moddable_createMachine, ModdableCreationRecord *cr)
{
PBL_LOG_ERR("Moddable XS not supported in this build");
}
}
#endif
1 change: 1 addition & 0 deletions src/fw/applib/wscript
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def build(bld):
'jerry_runtime_config',
'jerry_common_config',
'jerry_core',
'libxs',
'freertos_includes',
'root_includes'])

Expand Down
11 changes: 11 additions & 0 deletions src/fw/process_management/process_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,17 @@ bool process_manager_make_process_safe_to_kill(PebbleTask task, bool gracefully)
context->closing_state = ProcessRunState_ForceClosing;
PBL_LOG_DBG("task is privileged, setting the syscall exit trap");

// Send a DEINIT event to wake up the app if it's blocked waiting for events
// (e.g., in sys_get_pebble_event). This allows the syscall to return and
// trigger process_manager_handle_syscall_exit() which will mark the process
// as safe to kill.
PBL_LOG_DBG("Sending DEINIT event to wake %s from syscall",
pebble_task_get_name(task));
PebbleEvent deinit_event = {
.type = PEBBLE_PROCESS_DEINIT_EVENT,
};
process_manager_send_event_to_process(task, &deinit_event);

bool success = new_timer_start(s_deinit_timer_id, 3 * 1000, prv_force_close_timer_callback, (void*)task,
0 /*flags*/);
PBL_ASSERTN(success);
Expand Down
Loading
Loading